From c02d3aadc94ff7417d02c0590f73ed9e10dba73f Mon Sep 17 00:00:00 2001 From: Matt Kulka Date: Sun, 21 Feb 2021 16:34:43 -0700 Subject: [PATCH] add output formatting option for replace command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit my co-worker noted that its hard to share pastes of the replace commands dry-run output because its colorized. supporting a two-line output of changes with +/- prefixed to denote addition and subtraction for each change is better formatted for sharing. ``` ➜ build/vsh_darwin_amd64 -c 'replace -k value myValue KV1/src/a/foo -n' /KV1/src/a/foo> vmyValue = 1 Skipping write. ➜ build/vsh_darwin_amd64 -c 'replace -k value myValue KV1/src/a/foo -n -o diff' - /KV1/src/a/foo> value = 1 + /KV1/src/a/foo> myValue = 1 Skipping write. ``` --- CHANGELOG.md | 4 ++ cli/grep.go | 2 +- cli/replace.go | 22 +++++---- cli/search.go | 76 +++++++++++++++++++++++-------- doc/commands/replace.md | 4 ++ test/suites/commands/replace.bats | 13 ++++++ 6 files changed, 90 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e466d1..248daf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master - unreleased +ENHANCEMENTS: + +* Add `--output` flag to `replace` command to output as line diffs for each replacement in addition to the default inline format. ([#88](https://github.com/fishi0x01/vsh/pull/88)) + BUG FIXES: * Don't show error on empty line enter in interactive mode ([#85](https://github.com/fishi0x01/vsh/pull/85)) diff --git a/cli/grep.go b/cli/grep.go index 384aea59..2ce7a483 100644 --- a/cli/grep.go +++ b/cli/grep.go @@ -101,7 +101,7 @@ func (cmd *GrepCommand) Run() int { return 1 } for _, match := range matches { - match.print(os.Stdout, false) + match.print(os.Stdout, MatchOutputHighlight) } } return 0 diff --git a/cli/replace.go b/cli/replace.go index eef0767e..71987f48 100644 --- a/cli/replace.go +++ b/cli/replace.go @@ -21,15 +21,16 @@ type ReplaceCommand struct { // ReplaceCommandArgs provides a struct for go-arg parsing type ReplaceCommandArgs struct { - Search string `arg:"positional,required"` - Replacement string `arg:"positional,required"` - Path string `arg:"positional,required"` - 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"` + Search string `arg:"positional,required"` + Replacement string `arg:"positional,required"` + Path string `arg:"positional,required"` + 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"` + Output MatchOutputArg `arg:"-o,--output" help:"Present changes as 'inline' with color or traditional 'diff'" default:"inline"` } // Description provides detail on what the command does @@ -72,6 +73,7 @@ func (cmd *ReplaceCommand) GetSearchParams() SearchParameters { IsRegexp: cmd.args.Regexp, KeySelector: cmd.args.KeySelector, Mode: cmd.Mode, + Output: cmd.args.Output.Value, Replacement: &cmd.args.Replacement, Search: cmd.args.Search, } @@ -130,7 +132,7 @@ func (cmd *ReplaceCommand) findMatches(filePaths []string) (matchesByPath map[st return matchesByPath, err } for _, match := range matches { - match.print(os.Stdout, true) + match.print(os.Stdout, cmd.args.Output.Value) } if len(matches) > 0 { _, ok := matchesByPath[curPath] diff --git a/cli/search.go b/cli/search.go index f48e4957..aac68594 100644 --- a/cli/search.go +++ b/cli/search.go @@ -24,6 +24,7 @@ type SearchParameters struct { KeySelector string Mode KeyValueMode IsRegexp bool + Output MatchOutput } // Match structure to keep indices of matched and replaced terms @@ -34,14 +35,43 @@ type Match struct { // sorted slices of indices of match starts and length keyIndex [][]int valueIndex [][]int - // in-line diffs of key and value replacements - keyLineDiff string - valueLineDiff string + // diffs of chosen format for key and value replacements + keyDiff string + valueDiff string // final strings after replacement replacedKey string replacedValue string } +// MatchOutput contains the possible ways of presenting a match +type MatchOutput string + +// MatchOutputArg provides a struct to custom validate an arg +type MatchOutputArg struct { + Value MatchOutput +} + +const ( + // MatchOutputHighlight outputs yellow highlighted matching text + MatchOutputHighlight MatchOutput = "highlight" + // MatchOutputInline outputs red and green text to show replacements + MatchOutputInline MatchOutput = "inline" + // MatchOutputDiff outputs addition and subtraction lines to show replacements + MatchOutputDiff MatchOutput = "diff" +) + +// UnmarshalText validates the MatchOutputArg +func (a *MatchOutputArg) UnmarshalText(b []byte) error { + arg := string(b[:]) + switch MatchOutput(arg) { + case MatchOutputInline, MatchOutputDiff, MatchOutputHighlight: + a.Value = MatchOutput(arg) + return nil + default: + return fmt.Errorf("invalid output format: %s", arg) + } +} + // Searcher provides matching and replacement methods while maintaining references to the command // that provides an interface to search operations. Also maintains reference to a compiled regexp. type Searcher struct { @@ -80,8 +110,8 @@ func (s *Searcher) IsMode(mode KeyValueMode) bool { // DoSearch searches with either regexp or substring search methods func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) { // Default to original strings - replacedKey, keyLineDiff := k, k - replacedValue, valueLineDiff := v, v + replacedKey, keyDiff := k, k + replacedValue, valueDiff := v, v var keyMatchPairs, valueMatchPairs, keySelectorMatches [][]int if s.cmd.GetSearchParams().KeySelector != "" { @@ -91,14 +121,14 @@ func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) { } } if s.IsMode(ModeKeys) { - keyMatchPairs, replacedKey, keyLineDiff = s.matchData(k) + keyMatchPairs, replacedKey = s.matchData(k) } if len(keySelectorMatches) > 0 { - keyLineDiff = highlightMatches(keyLineDiff, s.keySelectorMatches(keyLineDiff)) + keyDiff = highlightMatches(keyDiff, s.keySelectorMatches(keyDiff)) } if s.IsMode(ModeValues) { - valueMatchPairs, replacedValue, valueLineDiff = s.matchData(v) + valueMatchPairs, replacedValue = s.matchData(v) } if len(keyMatchPairs) > 0 || len(valueMatchPairs) > 0 { @@ -109,8 +139,8 @@ func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) { value: v, keyIndex: keyMatchPairs, valueIndex: valueMatchPairs, - keyLineDiff: keyLineDiff, - valueLineDiff: valueLineDiff, + keyDiff: keyDiff, + valueDiff: valueDiff, replacedKey: replacedKey, replacedValue: replacedValue, }, @@ -119,10 +149,17 @@ func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) { return m } -func (match *Match) print(out io.Writer, diff bool) { - if diff == true { - fmt.Fprintf(out, "%s> %s = %s\n", match.path, match.keyLineDiff, match.valueLineDiff) - } else { +func (match *Match) print(out io.Writer, format MatchOutput) { + switch format { + case MatchOutputInline: + coloredKey := colorizeLineDiff(diff.CharacterDiff(match.key, match.replacedKey)) + coloredValue := colorizeLineDiff(diff.CharacterDiff(match.value, match.replacedValue)) + fmt.Fprintf(out, "%s> %s = %s\n", match.path, coloredKey, coloredValue) + case MatchOutputDiff: + before := fmt.Sprintf(" %s> %s = %s\n", match.path, match.key, match.value) + after := fmt.Sprintf(" %s> %s = %s\n", match.path, match.replacedKey, match.replacedValue) + fmt.Fprint(out, diff.LineDiff(before, after)+"\n") + case MatchOutputHighlight: fmt.Fprintf(out, "%s> %s = %s\n", match.path, highlightMatches(match.key, match.keyIndex), highlightMatches(match.value, match.valueIndex)) } } @@ -156,8 +193,8 @@ func highlightMatches(s string, matches [][]int) (result string) { return result } -// highlightLineDiff will consume (~~del~~)(++add++) markup and colorize in its place -func (s *Searcher) highlightLineDiff(d string) string { +// colorizeLineDiff will consume (~~del~~)(++add++) markup and colorize in its place +func colorizeLineDiff(d string) string { var buf, res []byte removeMode, addMode := false, false removeColor := color.New(color.FgWhite).Add(color.BgRed) @@ -201,8 +238,8 @@ func (s *Searcher) regexpMatchData(subject string, re *regexp.Regexp) (matchPair return re.FindAllStringIndex(subject, -1) } -func (s *Searcher) matchData(subject string) (matchPairs [][]int, replaced string, inlineDiff string) { - replaced, inlineDiff = subject, subject +func (s *Searcher) matchData(subject string) (matchPairs [][]int, replaced string) { + replaced = subject matchPairs = make([][]int, 0) if s.cmd.GetSearchParams().IsRegexp { @@ -217,8 +254,7 @@ func (s *Searcher) matchData(subject string) (matchPairs [][]int, replaced strin } else { replaced = strings.ReplaceAll(subject, s.cmd.GetSearchParams().Search, *s.cmd.GetSearchParams().Replacement) } - inlineDiff = s.highlightLineDiff(diff.CharacterDiff(subject, replaced)) } - return matchPairs, replaced, inlineDiff + return matchPairs, replaced } diff --git a/doc/commands/replace.md b/doc/commands/replace.md index 4d7f80cf..a1ad4ffd 100644 --- a/doc/commands/replace.md +++ b/doc/commands/replace.md @@ -1,3 +1,7 @@ # replace `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. + +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/replace.bats b/test/suites/commands/replace.bats index 02dca993..c92bfdd6 100644 --- a/test/suites/commands/replace.bats +++ b/test/suites/commands/replace.bats @@ -66,6 +66,19 @@ load ../../bin/plugins/bats-assert/load run get_vault_value "value" "${KV_BACKEND}/src/prod/all" assert_line all + ####################################### + echo "==== case: replace with invalid output format ====" + run ${APP_BIN} -c "replace -s 'produce' 'apple' 'orange' ${KV_BACKEND}/src/selector/1 -o invalid" + assert_failure + assert_line --partial "invalid output format: invalid" + + ####################################### + echo "==== case: replace with diff output format ====" + run ${APP_BIN} -c "replace -s 'produce' 'apple' 'orange' ${KV_BACKEND}/src/selector/1 -n -o diff" + assert_success + assert_line "- /${KV_BACKEND}/src/selector/1> produce = apple" + assert_line "+ /${KV_BACKEND}/src/selector/1> produce = orange" + ####################################### echo "==== case: replace value in single path with selector ====" run ${APP_BIN} -c "replace -s 'produce' 'apple' 'orange' ${KV_BACKEND}/src/selector/1 -y"