diff --git a/cli/command.go b/cli/command.go index 33a89778..ca1557bf 100644 --- a/cli/command.go +++ b/cli/command.go @@ -76,7 +76,7 @@ func cmdPath(pwd string, arg string) (result string) { func runCommandWithTraverseTwoPaths(client *client.Client, source string, target string, f func(string, string) error) { source = filepath.Clean(source) // remove potential trailing '/' - for _, path := range client.Traverse(source) { + for _, path := range client.Traverse(source, false) { target := strings.Replace(path, source, target, 1) err := f(path, target) if err != nil { diff --git a/cli/grep.go b/cli/grep.go index 0edfb7ea..a6e03c0b 100644 --- a/cli/grep.go +++ b/cli/grep.go @@ -20,11 +20,12 @@ type GrepCommand struct { // GrepCommandArgs provides a struct for go-arg parsing type GrepCommandArgs struct { - Search string `arg:"positional,required"` - Path string `arg:"positional"` - Regexp bool `arg:"-e,--regexp" help:"Treat search string as a regexp"` - Keys bool `arg:"-k,--keys" help:"Match against keys (true if -v is not specified)"` - Values bool `arg:"-v,--values" help:"Match against values (true if -k is not specified)"` + Search string `arg:"positional,required"` + Path string `arg:"positional"` + Keys bool `arg:"-k,--keys" help:"Match against keys (true if -v is not specified)"` + Regexp bool `arg:"-e,--regexp" help:"Treat search string as a regexp"` + Shallow bool `arg:"-S,--shallow" help:"Only search leaf nodes of the path rather than recurse deeper"` + Values bool `arg:"-v,--values" help:"Match against values (true if -k is not specified)"` } // Description provides detail on what the command does @@ -92,7 +93,7 @@ func (cmd *GrepCommand) Parse(args []string) error { // Run executes 'grep' with given GrepCommand's parameters func (cmd *GrepCommand) Run() int { path := cmdPath(cmd.client.Pwd, cmd.args.Path) - filePaths, err := cmd.client.SubpathsForPath(path) + filePaths, err := cmd.client.SubpathsForPath(path, cmd.args.Shallow) if err != nil { log.UserError(fmt.Sprintf("%s", err)) return 1 diff --git a/cli/replace.go b/cli/replace.go index b9e7a0ad..767af5e1 100644 --- a/cli/replace.go +++ b/cli/replace.go @@ -24,13 +24,14 @@ type ReplaceCommandArgs struct { Search string `arg:"positional,required"` Replacement string `arg:"positional,required"` Path string `arg:"positional"` - Regexp bool `arg:"-e,--regexp" help:"Treat search string and selector as a regexp"` - KeySelector string `arg:"-s,--key-selector" help:"Limit replacements to specified key" placeholder:"PATTERN"` - Keys bool `arg:"-k,--keys" help:"Match against keys (true if -v is not specified)"` - Values bool `arg:"-v,--values" help:"Match against values (true if -k is not specified)"` Confirm bool `arg:"-y,--confirm" help:"Write results without prompt"` DryRun bool `arg:"-n,--dry-run" help:"Skip writing results without prompt"` + KeySelector string `arg:"-s,--key-selector" help:"Limit replacements to specified key" placeholder:"PATTERN"` + Keys bool `arg:"-k,--keys" help:"Match against keys (true if -v is not specified)"` Output MatchOutputArg `arg:"-o,--output" help:"Present changes as 'inline' with color or traditional 'diff'" default:"inline"` + Regexp bool `arg:"-e,--regexp" help:"Treat search string and selector as a regexp"` + Shallow bool `arg:"-S,--shallow" help:"Only search leaf nodes of the path rather than recurse deeper"` + Values bool `arg:"-v,--values" help:"Match against values (true if -k is not specified)"` } // Description provides detail on what the command does @@ -113,7 +114,7 @@ func (cmd *ReplaceCommand) Parse(args []string) error { // Run executes 'replace' with given ReplaceCommand's parameters func (cmd *ReplaceCommand) Run() int { path := cmdPath(cmd.client.Pwd, cmd.args.Path) - filePaths, err := cmd.client.SubpathsForPath(path) + filePaths, err := cmd.client.SubpathsForPath(path, cmd.args.Shallow) if err != nil { log.UserError(fmt.Sprintf("%s", err)) return 1 diff --git a/cli/rm.go b/cli/rm.go index b2873a75..2a21b188 100644 --- a/cli/rm.go +++ b/cli/rm.go @@ -72,7 +72,7 @@ func (cmd *RemoveCommand) Run() int { case client.LEAF: cmd.removeSecret(newPwd) case client.NODE: - for _, path := range cmd.client.Traverse(newPwd) { + for _, path := range cmd.client.Traverse(newPwd, false) { err := cmd.removeSecret(path) if err != nil { return 1 diff --git a/client/client.go b/client/client.go index 17b77923..ffed953f 100644 --- a/client/client.go +++ b/client/client.go @@ -154,23 +154,26 @@ func (client *Client) GetType(absolutePath string) (kind PathKind) { } // Traverse traverses given absolutePath via DFS and returns sub-paths in array -func (client *Client) Traverse(absolutePath string) (paths []string) { +func (client *Client) Traverse(absolutePath string, shallow bool) (paths []string) { if client.isTopLevelPath(absolutePath) { paths = client.topLevelTraverse() } else { - paths = client.lowLevelTraverse(normalizedVaultPath(absolutePath)) + paths = client.lowLevelTraverse(normalizedVaultPath(absolutePath), shallow) } return paths } // SubpathsForPath will return an array of absolute paths at or below path -func (client *Client) SubpathsForPath(path string) (filePaths []string, err error) { +func (client *Client) SubpathsForPath(path string, shallow bool) (filePaths []string, err error) { switch t := client.GetType(path); t { case LEAF: filePaths = append(filePaths, filepath.Clean(path)) case NODE: - for _, traversedPath := range client.Traverse(path) { + for _, traversedPath := range client.Traverse(path, shallow) { + if shallow && client.GetType(traversedPath) == NODE { + continue + } filePaths = append(filePaths, traversedPath) } default: diff --git a/client/traverse.go b/client/traverse.go index 556f0d94..55d6efe7 100644 --- a/client/traverse.go +++ b/client/traverse.go @@ -13,7 +13,7 @@ func (client *Client) topLevelTraverse() (result []string) { return result } -func (client *Client) lowLevelTraverse(path string) (result []string) { +func (client *Client) lowLevelTraverse(path string, shallow bool) (result []string) { s, err := client.cache.List(client.getKVMetaDataPath(path)) if err != nil { log.AppTrace("%+v", err) @@ -27,7 +27,9 @@ func (client *Client) lowLevelTraverse(path string) (result []string) { // prevent ambiguous dir/file to be added twice if strings.HasSuffix(val, "/") { // dir - result = append(result, client.lowLevelTraverse(path+"/"+val)...) + if shallow == false { + result = append(result, client.lowLevelTraverse(path+"/"+val, false)...) + } } else { // file leaf := strings.ReplaceAll("/"+path+"/"+val, "//", "/") diff --git a/doc/commands/grep.md b/doc/commands/grep.md index 22aca442..ff77d834 100644 --- a/doc/commands/grep.md +++ b/doc/commands/grep.md @@ -1,8 +1,9 @@ # grep ```text -grep [-e|--regexp] [-k|--keys] [-v|--values] SEARCH PATH +grep [-e|--regexp] [-k|--keys] [-v|--values] [-S|--shallow] SEARCH PATH ``` `grep` recursively searches the given `SEARCH` substring in key and value pairs of given `PATH`. To treat the search string as a regular-expression, add `-e` or `--regexp` to the end of the command. By default, both keys and values will be searched. If you would like to limit the search, you may add `-k` or `--keys` to the end of the command to search only a path's keys, or `-v` or `--values` to search only a path's values. - If you are looking for copies or just trying to find the path to a certain string, this command might come in handy. +If you do not desire a deep recursive search, you can use the `-S` or `--shallow` flag to limit searching to only the leaf nodes of the given path. +If you are looking for copies or just trying to find the path to a certain string, this command might come in handy. diff --git a/doc/commands/replace.md b/doc/commands/replace.md index a1ad4ffd..4855312f 100644 --- a/doc/commands/replace.md +++ b/doc/commands/replace.md @@ -2,6 +2,8 @@ `replace` works similarly to `grep`, but has the ability to mutate data inside Vault. By default, confirmation is required before writing data. You may skip confirmation by using the `-y`/`--confirm` flags. Conversely, you may use the `-n`/`--dry-run` flags to skip both confirmation and any writes. Changes that would be made are presented in red (delete) and green (add) coloring. +If you do not desire a deep recursive search, you can use the `-S` or `--shallow` flag to limit searching to only the leaf nodes of the given path. + This command has two output formats available via the `--output` flag: - `inline`: A colorized inline format where deletions are in red background text and additions are in green background text. This is the default. - `diff`: A non-colorized format that prints changes in two lines prefixed with a `-` for before and `+` for after replacement. This is more useful for copying and pasting the result. diff --git a/test/suites/commands/grep.bats b/test/suites/commands/grep.bats index 7504bb3b..d68efa75 100644 --- a/test/suites/commands/grep.bats +++ b/test/suites/commands/grep.bats @@ -97,6 +97,13 @@ load ../../bin/plugins/bats-assert/load assert_line --partial "unknown argument --foo" assert_failure 1 + ####################################### + echo "==== case: shallow search" + run ${APP_BIN} -c "grep -S 'tree' /${KV_BACKEND}/src/staging/all" + assert_line --partial "/${KV_BACKEND}/src/staging/all" + refute_line --partial "/${KV_BACKEND}/src/staging/all/v1" + refute_line --partial "/${KV_BACKEND}/src/staging/all/v2" + ####################################### echo "==== case: match in pwd ====" export VAULT_PATH=${KV_BACKEND}/src/dev diff --git a/test/suites/commands/replace.bats b/test/suites/commands/replace.bats index 1aa740cf..818239c9 100644 --- a/test/suites/commands/replace.bats +++ b/test/suites/commands/replace.bats @@ -151,6 +151,13 @@ load ../../bin/plugins/bats-assert/load assert_failure assert_line --partial "key-selector: error parsing regexp" + ####################################### + echo "==== case: shallow replace" + run ${APP_BIN} -c "replace -e -S -k 'tree' '.*' 'maple' /${KV_BACKEND}/src/staging/all" + assert_line --partial "/${KV_BACKEND}/src/staging/all" + refute_line --partial "/${KV_BACKEND}/src/staging/all/v1" + refute_line --partial "/${KV_BACKEND}/src/staging/all/v2" + ####################################### echo "==== case: replace in pwd ====" export VAULT_PATH=${KV_BACKEND}/src/a