Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regexp flag to grep and support quotes #61

Merged
merged 1 commit into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
Core features are:

- recursive operations on paths with `cp`, `mv` or `rm`
- term search with `grep`
- search with `grep` (substring or regular-expression)
- transparency towards differences between KV1 and KV2, i.e., you can freely move/copy secrets between both
- non-interactive mode for automation (`vsh -c "<cmd>"`)
- merging keys with different strategies through `append`
Expand Down Expand Up @@ -48,7 +48,7 @@ cp <from-path> <to-path>
append <from-secret> <to-secret> [flag]
rm <dir-path or filel-path>
ls <dir-path // optional>
grep <search-term> <path>
grep <search> <path> [-e|--regexp]
cd <dir-path>
cat <file-path>
```
Expand Down Expand Up @@ -131,8 +131,8 @@ tree=oak

### grep

`grep` recursively searches the given term in key and value pairs. It does not support regex.
If you are looking for copies or just trying to find the path to a certain term, this command might come in handy.
`grep` recursively searches the given substring in key and value pairs. To treat the search string as a regular-expression, add `-e` or `--regexp` to the end of the command.
If you are looking for copies or just trying to find the path to a certain string, this command might come in handy.

## Setting the vault token

Expand Down
79 changes: 66 additions & 13 deletions cli/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"index/suffixarray"
"io"
"path/filepath"
"regexp"
"sort"

"github.com/fatih/color"
Expand All @@ -21,6 +22,7 @@ type GrepCommand struct {
stdout io.Writer
Path string
Search string
Regexp *regexp.Regexp
}

// Match structure to keep indices of matched terms
Expand All @@ -29,9 +31,9 @@ type Match struct {
term string
key string
value string
// sorted slices of indices of match starts
keyIndex []int
valueIndex []int
// sorted slices of indices of match starts and length
keyIndex [][]int
valueIndex [][]int
}

// NewGrepCommand creates a new GrepCommand parameter container
Expand All @@ -56,16 +58,28 @@ func (cmd *GrepCommand) IsSane() bool {

// PrintUsage print command usage
func (cmd *GrepCommand) PrintUsage() {
log.UserInfo("Usage:\ngrep <term-string> <path>")
log.UserInfo("Usage:\ngrep <term-string> <path> [-e|--regexp]")
}

// Parse given arguments and return status
func (cmd *GrepCommand) Parse(args []string) error {
if len(args) != 3 {
if len(args) < 3 {
return fmt.Errorf("cannot parse arguments")
}
cmd.Search = args[1]
cmd.Path = args[2]
flags := args[3:]

for _, v := range flags {
switch v {
case "-e", "--regexp":
re, err := regexp.Compile(cmd.Search)
if err != nil {
return fmt.Errorf("cannot parse regex pattern")
}
cmd.Regexp = re
}
}
return nil
}

Expand Down Expand Up @@ -111,20 +125,27 @@ func (cmd *GrepCommand) grepFile(search string, path string) (matches []*Match,
if rec, ok := v.(map[string]interface{}); ok {
// KV 2
for kk, vv := range rec {
matches = append(matches, match(path, kk, fmt.Sprintf("%v", vv), search)...)
matches = append(matches, cmd.doMatch(path, kk, fmt.Sprintf("%v", vv), search)...)
}
} else {
// KV 1
matches = append(matches, match(path, k, fmt.Sprintf("%v", v), search)...)
matches = append(matches, cmd.doMatch(path, k, fmt.Sprintf("%v", v), search)...)
}
}
}

return matches, nil
}

func (cmd *GrepCommand) doMatch(path string, k string, v string, search string) (m []*Match) {
if cmd.Regexp != nil {
return regexpMatch(path, k, v, cmd.Regexp)
}
return substrMatch(path, k, v, search)
}

// find all indices for matches in key and value
func match(path string, k string, v string, substr string) (m []*Match) {
func substrMatch(path string, k string, v string, substr string) (m []*Match) {
keyIndex := suffixarray.New([]byte(k))
keyMatches := keyIndex.Lookup([]byte(substr), -1)
sort.Ints(keyMatches)
Expand All @@ -133,13 +154,43 @@ func match(path string, k string, v string, substr string) (m []*Match) {
valueMatches := valueIndex.Lookup([]byte(substr), -1)
sort.Ints(valueMatches)

substrLength := len(substr)
keyMatchPairs := make([][]int, 0)
for _, offset := range keyMatches {
keyMatchPairs = append(keyMatchPairs, []int{offset, substrLength})
}
valueMatchPairs := make([][]int, 0)
for _, offset := range valueMatches {
valueMatchPairs = append(valueMatchPairs, []int{offset, substrLength})
}

if len(keyMatches) > 0 || len(valueMatches) > 0 {
m = []*Match{
{
path: path,
term: substr,
key: k,
value: v,
keyIndex: keyMatchPairs,
valueIndex: valueMatchPairs,
},
}
}

return m
}

func regexpMatch(path string, k string, v string, pattern *regexp.Regexp) (m []*Match) {
mattlqx marked this conversation as resolved.
Show resolved Hide resolved
keyMatches := pattern.FindAllIndex([]byte(k), -1)
valueMatches := pattern.FindAllIndex([]byte(v), -1)

if len(keyMatches) > 0 || len(valueMatches) > 0 {
m = []*Match{
{
path: path,
term: pattern.String(),
key: k,
value: v,
keyIndex: keyMatches,
valueIndex: valueMatches,
},
Expand All @@ -151,18 +202,20 @@ func match(path string, k string, v string, substr string) (m []*Match) {

func (match *Match) print(out io.Writer) {
fmt.Fprint(out, match.path, "> ")
highlightMatches(match.key, match.term, match.keyIndex, out)
highlightMatches(match.key, match.keyIndex, out)
fmt.Fprintf(out, " = ")
highlightMatches(match.value, match.term, match.valueIndex, out)
highlightMatches(match.value, match.valueIndex, out)
fmt.Fprintf(out, "\n")
}

func highlightMatches(s string, term string, index []int, out io.Writer) {
func highlightMatches(s string, index [][]int, out io.Writer) {
matchColor := color.New(color.FgYellow).SprintFunc()
cur := 0
if len(index) > 0 {
for _, next := range index {
end := next + len(term)
for _, pair := range index {
next := pair[0]
length := pair[1]
end := next + length
fmt.Fprint(out, s[cur:next])
fmt.Fprint(out, matchColor(s[next:end]))
cur = end
Expand Down
2 changes: 1 addition & 1 deletion completer/completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (c *Completer) commandSuggestions(arg string) (result []prompt.Suggest) {
{Text: "append", Description: "append <from> <to> [-f|--force] | [-s|--skip] | [-r|--rename] | -s is default"},
{Text: "rm", Description: "rm <path> | -r is implied"},
{Text: "mv", Description: "mv <from> <to>"},
{Text: "grep", Description: "grep <term> <path>"},
{Text: "grep", Description: "grep <search> <path> [-e|--regexp]"},
{Text: "cat", Description: "cat <path>"},
{Text: "ls", Description: "ls <path>"},
{Text: "toggle-auto-completion", Description: "toggle path auto-completion on/off"},
Expand Down
43 changes: 41 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,47 @@ func printVersion() {
}

func parseInput(line string) (args []string) {
// TODO: allow "" and "\"\""
return strings.Fields(line)
quote := '0'
mattlqx marked this conversation as resolved.
Show resolved Hide resolved
escaped := false
arg := ""

for _, c := range line {
switch {
case c == '\\' && !escaped: // next char will be escaped
escaped = true
continue
case escaped:
escaped = false
arg += string(c) // append char to current arg buffer
continue
case c == quote: // terminating quote
quote = '0'
args = append(args, arg)
arg = ""
case c == '"' || c == '\'':
if quote == '0' { // beginning quote
quote = c
} else if c != quote { // non-matching quote char
arg += string(c)
}
case c == ' ':
if quote == '0' {
if arg != "" { // unquoted space, store non-empty arg
args = append(args, arg)
arg = ""
}
continue
}
fallthrough
default:
arg += string(c) // append char to current arg buffer
}
}

if arg != "" { // store non-empty arg
args = append(args, arg)
}
return args
}

var completerInstance *completer.Completer
Expand Down
26 changes: 26 additions & 0 deletions test/suites/commands/grep.bats
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ load ../../bin/plugins/bats-assert/load
run ${APP_BIN} -c "grep beer ${KV_BACKEND}/src/tooling"
assert_line --partial "/${KV_BACKEND}/src/tooling"

#######################################
echo "==== case: grep value with quotes ===="
run ${APP_BIN} -c "grep \\\"quoted\\\" ${KV_BACKEND}/src/quoted/foo"
assert_line --partial "/${KV_BACKEND}/src/quoted/foo"

#######################################
echo "==== case: regexp pattern ===="
run ${APP_BIN} -c "grep app.* ${KV_BACKEND}/src -e"
assert_line --partial "/${KV_BACKEND}/src/dev/1"
assert_line --partial "/${KV_BACKEND}/src/ambivalence/1"

#######################################
echo "==== case: pattern with spaces ===="
run ${APP_BIN} -c "grep 'a spaced val' ${KV_BACKEND}/src/spaces"
assert_line --partial "/${KV_BACKEND}/src/spaces/foo"

#######################################
echo "==== case: pattern with escaped spaces ===="
run ${APP_BIN} -c "grep a\ spaced\ val ${KV_BACKEND}/src/spaces"
assert_line --partial "/${KV_BACKEND}/src/spaces/foo"

#######################################
echo "==== case: pattern with apostrophe ===="
run ${APP_BIN} -c "grep \"steve's\" ${KV_BACKEND}/src/apostrophe"
assert_line --partial "/${KV_BACKEND}/src/apostrophe"

mattlqx marked this conversation as resolved.
Show resolved Hide resolved
#######################################
echo "==== TODO case: grep term on directory with reduced permissions ===="

Expand Down
5 changes: 4 additions & 1 deletion test/util/util.bash
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ setup() {
vault_exec "vault kv put ${kv_backend}/src/a/foo/bar value=2"
vault_exec "vault kv put ${kv_backend}/src/b/foo value=1"
vault_exec "vault kv put ${kv_backend}/src/b/foo/bar value=2"
vault_exec "echo -n \"a spaced value\" | vault kv put ${kv_backend}/src/spaces/foo bar=-"
vault_exec "vault kv put ${kv_backend}/src/apostrophe/foo bar=steve\'s"
vault_exec "echo -n 'a \"quoted\" value' | vault kv put ${kv_backend}/src/quoted/foo bar=-"
done
}

Expand All @@ -63,7 +66,7 @@ teardown() {
}

vault_exec() {
docker exec ${VAULT_CONTAINER_NAME} ${1} &> /dev/null
docker exec ${VAULT_CONTAINER_NAME} /bin/sh -c "$1" &> /dev/null
}

get_vault_value() {
Expand Down