Skip to content

Commit c02d3aa

Browse files
author
Matt Kulka
committed
add output formatting option for replace command
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. ```
1 parent af2a284 commit c02d3aa

File tree

6 files changed

+90
-31
lines changed

6 files changed

+90
-31
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## master - unreleased
44

5+
ENHANCEMENTS:
6+
7+
* 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))
8+
59
BUG FIXES:
610

711
* Don't show error on empty line enter in interactive mode ([#85](https://github.com/fishi0x01/vsh/pull/85))

cli/grep.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func (cmd *GrepCommand) Run() int {
101101
return 1
102102
}
103103
for _, match := range matches {
104-
match.print(os.Stdout, false)
104+
match.print(os.Stdout, MatchOutputHighlight)
105105
}
106106
}
107107
return 0

cli/replace.go

+12-10
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ type ReplaceCommand struct {
2121

2222
// ReplaceCommandArgs provides a struct for go-arg parsing
2323
type ReplaceCommandArgs struct {
24-
Search string `arg:"positional,required"`
25-
Replacement string `arg:"positional,required"`
26-
Path string `arg:"positional,required"`
27-
Regexp bool `arg:"-e,--regexp" help:"Treat search string and selector as a regexp"`
28-
KeySelector string `arg:"-s,--key-selector" help:"Limit replacements to specified key" placeholder:"PATTERN"`
29-
Keys bool `arg:"-k,--keys" help:"Match against keys (true if -v is not specified)"`
30-
Values bool `arg:"-v,--values" help:"Match against values (true if -k is not specified)"`
31-
Confirm bool `arg:"-y,--confirm" help:"Write results without prompt"`
32-
DryRun bool `arg:"-n,--dry-run" help:"Skip writing results without prompt"`
24+
Search string `arg:"positional,required"`
25+
Replacement string `arg:"positional,required"`
26+
Path string `arg:"positional,required"`
27+
Regexp bool `arg:"-e,--regexp" help:"Treat search string and selector as a regexp"`
28+
KeySelector string `arg:"-s,--key-selector" help:"Limit replacements to specified key" placeholder:"PATTERN"`
29+
Keys bool `arg:"-k,--keys" help:"Match against keys (true if -v is not specified)"`
30+
Values bool `arg:"-v,--values" help:"Match against values (true if -k is not specified)"`
31+
Confirm bool `arg:"-y,--confirm" help:"Write results without prompt"`
32+
DryRun bool `arg:"-n,--dry-run" help:"Skip writing results without prompt"`
33+
Output MatchOutputArg `arg:"-o,--output" help:"Present changes as 'inline' with color or traditional 'diff'" default:"inline"`
3334
}
3435

3536
// Description provides detail on what the command does
@@ -72,6 +73,7 @@ func (cmd *ReplaceCommand) GetSearchParams() SearchParameters {
7273
IsRegexp: cmd.args.Regexp,
7374
KeySelector: cmd.args.KeySelector,
7475
Mode: cmd.Mode,
76+
Output: cmd.args.Output.Value,
7577
Replacement: &cmd.args.Replacement,
7678
Search: cmd.args.Search,
7779
}
@@ -130,7 +132,7 @@ func (cmd *ReplaceCommand) findMatches(filePaths []string) (matchesByPath map[st
130132
return matchesByPath, err
131133
}
132134
for _, match := range matches {
133-
match.print(os.Stdout, true)
135+
match.print(os.Stdout, cmd.args.Output.Value)
134136
}
135137
if len(matches) > 0 {
136138
_, ok := matchesByPath[curPath]

cli/search.go

+56-20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type SearchParameters struct {
2424
KeySelector string
2525
Mode KeyValueMode
2626
IsRegexp bool
27+
Output MatchOutput
2728
}
2829

2930
// Match structure to keep indices of matched and replaced terms
@@ -34,14 +35,43 @@ type Match struct {
3435
// sorted slices of indices of match starts and length
3536
keyIndex [][]int
3637
valueIndex [][]int
37-
// in-line diffs of key and value replacements
38-
keyLineDiff string
39-
valueLineDiff string
38+
// diffs of chosen format for key and value replacements
39+
keyDiff string
40+
valueDiff string
4041
// final strings after replacement
4142
replacedKey string
4243
replacedValue string
4344
}
4445

46+
// MatchOutput contains the possible ways of presenting a match
47+
type MatchOutput string
48+
49+
// MatchOutputArg provides a struct to custom validate an arg
50+
type MatchOutputArg struct {
51+
Value MatchOutput
52+
}
53+
54+
const (
55+
// MatchOutputHighlight outputs yellow highlighted matching text
56+
MatchOutputHighlight MatchOutput = "highlight"
57+
// MatchOutputInline outputs red and green text to show replacements
58+
MatchOutputInline MatchOutput = "inline"
59+
// MatchOutputDiff outputs addition and subtraction lines to show replacements
60+
MatchOutputDiff MatchOutput = "diff"
61+
)
62+
63+
// UnmarshalText validates the MatchOutputArg
64+
func (a *MatchOutputArg) UnmarshalText(b []byte) error {
65+
arg := string(b[:])
66+
switch MatchOutput(arg) {
67+
case MatchOutputInline, MatchOutputDiff, MatchOutputHighlight:
68+
a.Value = MatchOutput(arg)
69+
return nil
70+
default:
71+
return fmt.Errorf("invalid output format: %s", arg)
72+
}
73+
}
74+
4575
// Searcher provides matching and replacement methods while maintaining references to the command
4676
// that provides an interface to search operations. Also maintains reference to a compiled regexp.
4777
type Searcher struct {
@@ -80,8 +110,8 @@ func (s *Searcher) IsMode(mode KeyValueMode) bool {
80110
// DoSearch searches with either regexp or substring search methods
81111
func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) {
82112
// Default to original strings
83-
replacedKey, keyLineDiff := k, k
84-
replacedValue, valueLineDiff := v, v
113+
replacedKey, keyDiff := k, k
114+
replacedValue, valueDiff := v, v
85115
var keyMatchPairs, valueMatchPairs, keySelectorMatches [][]int
86116

87117
if s.cmd.GetSearchParams().KeySelector != "" {
@@ -91,14 +121,14 @@ func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) {
91121
}
92122
}
93123
if s.IsMode(ModeKeys) {
94-
keyMatchPairs, replacedKey, keyLineDiff = s.matchData(k)
124+
keyMatchPairs, replacedKey = s.matchData(k)
95125
}
96126
if len(keySelectorMatches) > 0 {
97-
keyLineDiff = highlightMatches(keyLineDiff, s.keySelectorMatches(keyLineDiff))
127+
keyDiff = highlightMatches(keyDiff, s.keySelectorMatches(keyDiff))
98128
}
99129

100130
if s.IsMode(ModeValues) {
101-
valueMatchPairs, replacedValue, valueLineDiff = s.matchData(v)
131+
valueMatchPairs, replacedValue = s.matchData(v)
102132
}
103133

104134
if len(keyMatchPairs) > 0 || len(valueMatchPairs) > 0 {
@@ -109,8 +139,8 @@ func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) {
109139
value: v,
110140
keyIndex: keyMatchPairs,
111141
valueIndex: valueMatchPairs,
112-
keyLineDiff: keyLineDiff,
113-
valueLineDiff: valueLineDiff,
142+
keyDiff: keyDiff,
143+
valueDiff: valueDiff,
114144
replacedKey: replacedKey,
115145
replacedValue: replacedValue,
116146
},
@@ -119,10 +149,17 @@ func (s *Searcher) DoSearch(path string, k string, v string) (m []*Match) {
119149
return m
120150
}
121151

122-
func (match *Match) print(out io.Writer, diff bool) {
123-
if diff == true {
124-
fmt.Fprintf(out, "%s> %s = %s\n", match.path, match.keyLineDiff, match.valueLineDiff)
125-
} else {
152+
func (match *Match) print(out io.Writer, format MatchOutput) {
153+
switch format {
154+
case MatchOutputInline:
155+
coloredKey := colorizeLineDiff(diff.CharacterDiff(match.key, match.replacedKey))
156+
coloredValue := colorizeLineDiff(diff.CharacterDiff(match.value, match.replacedValue))
157+
fmt.Fprintf(out, "%s> %s = %s\n", match.path, coloredKey, coloredValue)
158+
case MatchOutputDiff:
159+
before := fmt.Sprintf(" %s> %s = %s\n", match.path, match.key, match.value)
160+
after := fmt.Sprintf(" %s> %s = %s\n", match.path, match.replacedKey, match.replacedValue)
161+
fmt.Fprint(out, diff.LineDiff(before, after)+"\n")
162+
case MatchOutputHighlight:
126163
fmt.Fprintf(out, "%s> %s = %s\n", match.path, highlightMatches(match.key, match.keyIndex), highlightMatches(match.value, match.valueIndex))
127164
}
128165
}
@@ -156,8 +193,8 @@ func highlightMatches(s string, matches [][]int) (result string) {
156193
return result
157194
}
158195

159-
// highlightLineDiff will consume (~~del~~)(++add++) markup and colorize in its place
160-
func (s *Searcher) highlightLineDiff(d string) string {
196+
// colorizeLineDiff will consume (~~del~~)(++add++) markup and colorize in its place
197+
func colorizeLineDiff(d string) string {
161198
var buf, res []byte
162199
removeMode, addMode := false, false
163200
removeColor := color.New(color.FgWhite).Add(color.BgRed)
@@ -201,8 +238,8 @@ func (s *Searcher) regexpMatchData(subject string, re *regexp.Regexp) (matchPair
201238
return re.FindAllStringIndex(subject, -1)
202239
}
203240

204-
func (s *Searcher) matchData(subject string) (matchPairs [][]int, replaced string, inlineDiff string) {
205-
replaced, inlineDiff = subject, subject
241+
func (s *Searcher) matchData(subject string) (matchPairs [][]int, replaced string) {
242+
replaced = subject
206243
matchPairs = make([][]int, 0)
207244

208245
if s.cmd.GetSearchParams().IsRegexp {
@@ -217,8 +254,7 @@ func (s *Searcher) matchData(subject string) (matchPairs [][]int, replaced strin
217254
} else {
218255
replaced = strings.ReplaceAll(subject, s.cmd.GetSearchParams().Search, *s.cmd.GetSearchParams().Replacement)
219256
}
220-
inlineDiff = s.highlightLineDiff(diff.CharacterDiff(subject, replaced))
221257
}
222258

223-
return matchPairs, replaced, inlineDiff
259+
return matchPairs, replaced
224260
}

doc/commands/replace.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# replace
22

33
`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.
4+
5+
This command has two output formats available via the `--output` flag:
6+
- `inline`: A colorized inline format where deletions are in red background text and additions are in green background text. This is the default.
7+
- `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.

test/suites/commands/replace.bats

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ load ../../bin/plugins/bats-assert/load
6666
run get_vault_value "value" "${KV_BACKEND}/src/prod/all"
6767
assert_line all
6868

69+
#######################################
70+
echo "==== case: replace with invalid output format ===="
71+
run ${APP_BIN} -c "replace -s 'produce' 'apple' 'orange' ${KV_BACKEND}/src/selector/1 -o invalid"
72+
assert_failure
73+
assert_line --partial "invalid output format: invalid"
74+
75+
#######################################
76+
echo "==== case: replace with diff output format ===="
77+
run ${APP_BIN} -c "replace -s 'produce' 'apple' 'orange' ${KV_BACKEND}/src/selector/1 -n -o diff"
78+
assert_success
79+
assert_line "- /${KV_BACKEND}/src/selector/1> produce = apple"
80+
assert_line "+ /${KV_BACKEND}/src/selector/1> produce = orange"
81+
6982
#######################################
7083
echo "==== case: replace value in single path with selector ===="
7184
run ${APP_BIN} -c "replace -s 'produce' 'apple' 'orange' ${KV_BACKEND}/src/selector/1 -y"

0 commit comments

Comments
 (0)