Skip to content

Commit

Permalink
add output formatting option for replace command
Browse files Browse the repository at this point in the history
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.

```
  • Loading branch information
Matt Kulka committed Feb 22, 2021
1 parent af2a284 commit c02d3aa
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion cli/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 12 additions & 10 deletions cli/replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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]
Expand Down
76 changes: 56 additions & 20 deletions cli/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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 {
Expand All @@ -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,
},
Expand All @@ -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))
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
4 changes: 4 additions & 0 deletions doc/commands/replace.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions test/suites/commands/replace.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit c02d3aa

Please sign in to comment.