From 682dcfecaf6631f932415a83dd2d69795203265d Mon Sep 17 00:00:00 2001 From: Matt Kulka Date: Sun, 24 Jan 2021 11:29:48 -0700 Subject: [PATCH] add regexp flag to grep and support quotes this adds a regexp flag to the `grep` command in the spirit of the userland grep binary. --- README.md | 8 ++-- cli/grep.go | 79 ++++++++++++++++++++++++++++------ completer/completer.go | 2 +- main.go | 43 +++++++++++++++++- test/suites/commands/grep.bats | 26 +++++++++++ test/util/util.bash | 5 ++- 6 files changed, 142 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 94147ef5..35f51eed 100644 --- a/README.md +++ b/README.md @@ -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 ""`) - merging keys with different strategies through `append` @@ -48,7 +48,7 @@ cp append [flag] rm ls -grep +grep [-e|--regexp] cd cat ``` @@ -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 diff --git a/cli/grep.go b/cli/grep.go index 458faa6b..78be62d4 100644 --- a/cli/grep.go +++ b/cli/grep.go @@ -5,6 +5,7 @@ import ( "index/suffixarray" "io" "path/filepath" + "regexp" "sort" "github.com/fatih/color" @@ -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 @@ -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 @@ -56,16 +58,28 @@ func (cmd *GrepCommand) IsSane() bool { // PrintUsage print command usage func (cmd *GrepCommand) PrintUsage() { - log.UserInfo("Usage:\ngrep ") + log.UserInfo("Usage:\ngrep [-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 } @@ -111,11 +125,11 @@ 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)...) } } } @@ -123,8 +137,15 @@ func (cmd *GrepCommand) grepFile(search string, path string) (matches []*Match, 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) @@ -133,6 +154,16 @@ 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{ { @@ -140,6 +171,26 @@ func match(path string, k string, v string, substr string) (m []*Match) { 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) { + 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, }, @@ -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 diff --git a/completer/completer.go b/completer/completer.go index 634a5b7e..01c72dde 100644 --- a/completer/completer.go +++ b/completer/completer.go @@ -132,7 +132,7 @@ func (c *Completer) commandSuggestions(arg string) (result []prompt.Suggest) { {Text: "append", Description: "append [-f|--force] | [-s|--skip] | [-r|--rename] | -s is default"}, {Text: "rm", Description: "rm | -r is implied"}, {Text: "mv", Description: "mv "}, - {Text: "grep", Description: "grep "}, + {Text: "grep", Description: "grep [-e|--regexp]"}, {Text: "cat", Description: "cat "}, {Text: "ls", Description: "ls "}, {Text: "toggle-auto-completion", Description: "toggle path auto-completion on/off"}, diff --git a/main.go b/main.go index 77d39d35..342b413c 100644 --- a/main.go +++ b/main.go @@ -27,8 +27,47 @@ func printVersion() { } func parseInput(line string) (args []string) { - // TODO: allow "" and "\"\"" - return strings.Fields(line) + quote := '0' + 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 diff --git a/test/suites/commands/grep.bats b/test/suites/commands/grep.bats index cebacc8d..e0ee93da 100644 --- a/test/suites/commands/grep.bats +++ b/test/suites/commands/grep.bats @@ -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" + ####################################### echo "==== TODO case: grep term on directory with reduced permissions ====" diff --git a/test/util/util.bash b/test/util/util.bash index 3a492ef1..42d8e4f8 100755 --- a/test/util/util.bash +++ b/test/util/util.bash @@ -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 } @@ -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() {