Skip to content

Commit

Permalink
add regexp flag to grep and support quotes (#61)
Browse files Browse the repository at this point in the history
this adds a regexp flag to the `grep` command in the spirit of the userland grep binary.
  • Loading branch information
mattlqx authored Jan 26, 2021
1 parent b04299c commit 7db5a57
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 21 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<cmd>"`)
- merging keys with different strategies through `append`
Expand Down Expand Up @@ -48,7 +48,7 @@ cp <from-path> <to-path>
append <from-secret> <to-secret> [flag]
rm <dir-path or filel-path>
ls <dir-path // optional>
grep <search-term> <path>
grep <search> <path> [-e|--regexp]
cd <dir-path>
cat <file-path>
```
Expand Down Expand Up @@ -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

Expand Down
79 changes: 66 additions & 13 deletions cli/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"index/suffixarray"
"io"
"path/filepath"
"regexp"
"sort"

"github.com/fatih/color"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -56,16 +58,28 @@ func (cmd *GrepCommand) IsSane() bool {

// PrintUsage print command usage
func (cmd *GrepCommand) PrintUsage() {
log.UserInfo("Usage:\ngrep <term-string> <path>")
log.UserInfo("Usage:\ngrep <term-string> <path> [-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
}

Expand Down Expand Up @@ -111,20 +125,27 @@ 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)...)
}
}
}

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)
Expand All @@ -133,13 +154,43 @@ 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{
{
path: path,
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,
},
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion completer/completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (c *Completer) commandSuggestions(arg string) (result []prompt.Suggest) {
{Text: "append", Description: "append <from> <to> [-f|--force] | [-s|--skip] | [-r|--rename] | -s is default"},
{Text: "rm", Description: "rm <path> | -r is implied"},
{Text: "mv", Description: "mv <from> <to>"},
{Text: "grep", Description: "grep <term> <path>"},
{Text: "grep", Description: "grep <search> <path> [-e|--regexp]"},
{Text: "cat", Description: "cat <path>"},
{Text: "ls", Description: "ls <path>"},
{Text: "toggle-auto-completion", Description: "toggle path auto-completion on/off"},
Expand Down
43 changes: 41 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions test/suites/commands/grep.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===="

Expand Down
5 changes: 4 additions & 1 deletion test/util/util.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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() {
Expand Down

0 comments on commit 7db5a57

Please sign in to comment.