diff options
| author | Vlastimil Ovčáčík <vovcacik@github.ovcacik.org> | 2021-10-15 15:31:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-10-15 22:31:59 +0900 |
| commit | 61339a8ae2f6be27f28c243f00a41cc3aa5f54c2 (patch) | |
| tree | e8bab761dde12006078deedffe1555ab66cce095 /src/terminal.go | |
| parent | 50eb2e38552f57bce84c417fad8f4b48fcbf16ac (diff) | |
| download | fzf-61339a8ae2f6be27f28c243f00a41cc3aa5f54c2.tar.gz | |
Add more tests of placeholder flags and simplify its logic (#2624)
* [tests] Test fzf's placeholders and escaping on practical commands
This tests some reasonable commands in fzf's templates (for commands,
previews, rebinds etc.), how are those commands escaped (backslashes,
double quotes), and documents if the output is executable in cmd.exe.
Both on Unix and Windows.
* [tests] Add testing of placeholder parsing and matching
Adds tests and bit of docs for the curly brackets placeholders in fzf's
template strings. Also tests the "placeholder" regex.
* [tests] Add more test cases of replacing placeholders focused on flags
Replacing placeholders in templates is already tested, this adds tests
that focus more on the parameters of placeholders - e.g. flags, token
ranges.
There is at least one test for each flag, not all combinations are
tested though.
* [refactoring] Split OS-specific function quoteEntry() to corresponding source file
This is minor refactoring, and also the function's test was made
crossplatform.
* [refactoring] Simplify replacePlaceholder function
Should be equivalent to the original, but has simpler structure.
Diffstat (limited to 'src/terminal.go')
| -rw-r--r-- | src/terminal.go | 154 |
1 files changed, 84 insertions, 70 deletions
diff --git a/src/terminal.go b/src/terminal.go index c296e443..8a8e1658 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -23,6 +23,26 @@ import ( // import "github.com/pkg/profile" +/* + Placeholder regex is used to extract placeholders from fzf's template + strings. Acts as input validation for parsePlaceholder function. + Describes the syntax, but it is fairly lenient. + + The following pseudo regex has been reverse engineered from the + implementation. It is overly strict, but better describes whats possible. + As such it is not useful for validation, but rather to generate test + cases for example. + + \\?(?: # escaped type + {\+?s?f?RANGE(?:,RANGE)*} # token type + |{q} # query type + |{\+?n?f?} # item type (notice no mandatory element inside brackets) + ) + RANGE = (?: + (?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y) + |-?[0-9]+ # shorthand syntax (x..x) + ) +*/ var placeholder *regexp.Regexp var whiteSuffix *regexp.Regexp var offsetComponentRegex *regexp.Regexp @@ -1520,22 +1540,6 @@ func keyMatch(key tui.Event, event tui.Event) bool { key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double } -func quoteEntryCmd(entry string) string { - escaped := strings.Replace(entry, `\`, `\\`, -1) - escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"` - r, _ := regexp.Compile(`[&|<>()@^%!"]`) - return r.ReplaceAllStringFunc(escaped, func(match string) string { - return "^" + match - }) -} - -func quoteEntry(entry string) string { - if util.IsWindows() { - return quoteEntryCmd(entry) - } - return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" -} - func parsePlaceholder(match string) (bool, string, placeholderFlags) { flags := placeholderFlags{} @@ -1561,6 +1565,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) { skipChars++ case 'q': flags.query = true + // query flag is not skipped default: break } @@ -1648,77 +1653,86 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr if selected[0] == nil { selected = []*Item{} } + + // replace placeholders one by one return placeholder.ReplaceAllStringFunc(template, func(match string) string { escaped, match, flags := parsePlaceholder(match) - if escaped { - return match - } + // this function implements the effects a placeholder has on items + var replace func(*Item) string - // Current query - if match == "{q}" { + // placeholder types (escaped, query type, item type, token type) + switch { + case escaped: + return match + case match == "{q}": return quoteEntry(query) - } - - items := current - if flags.plus || forcePlus { - items = selected - } - - replacements := make([]string, len(items)) - - if match == "{}" { - for idx, item := range items { - if flags.number { + case match == "{}": + replace = func(item *Item) string { + switch { + case flags.number: n := int(item.text.Index) if n < 0 { - replacements[idx] = "" - } else { - replacements[idx] = strconv.Itoa(n) + return "" } - } else if flags.file { - replacements[idx] = item.AsString(stripAnsi) - } else { - replacements[idx] = quoteEntry(item.AsString(stripAnsi)) + return strconv.Itoa(n) + case flags.file: + return item.AsString(stripAnsi) + default: + return quoteEntry(item.AsString(stripAnsi)) } } - if flags.file { - return writeTemporaryFile(replacements, printsep) + default: + // token type and also failover (below) + rangeExpressions := strings.Split(match[1:len(match)-1], ",") + ranges := make([]Range, len(rangeExpressions)) + for idx, s := range rangeExpressions { + r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax + if !ok { + // Invalid expression, just return the original string in the template + return match + } + ranges[idx] = r } - return strings.Join(replacements, " ") - } - tokens := strings.Split(match[1:len(match)-1], ",") - ranges := make([]Range, len(tokens)) - for idx, s := range tokens { - r, ok := ParseRange(&s) - if !ok { - // Invalid expression, just return the original string in the template - return match + replace = func(item *Item) string { + tokens := Tokenize(item.AsString(stripAnsi), delimiter) + trans := Transform(tokens, ranges) + str := joinTokens(trans) + + // trim the last delimiter + if delimiter.str != nil { + str = strings.TrimSuffix(str, *delimiter.str) + } else if delimiter.regex != nil { + delims := delimiter.regex.FindAllStringIndex(str, -1) + // make sure the delimiter is at the very end of the string + if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { + str = str[:delims[len(delims)-1][0]] + } + } + + if !flags.preserveSpace { + str = strings.TrimSpace(str) + } + if !flags.file { + str = quoteEntry(str) + } + return str } - ranges[idx] = r } + // apply 'replace' function over proper set of items and return result + + items := current + if flags.plus || forcePlus { + items = selected + } + replacements := make([]string, len(items)) + for idx, item := range items { - tokens := Tokenize(item.AsString(stripAnsi), delimiter) - trans := Transform(tokens, ranges) - str := joinTokens(trans) - if delimiter.str != nil { - str = strings.TrimSuffix(str, *delimiter.str) - } else if delimiter.regex != nil { - delims := delimiter.regex.FindAllStringIndex(str, -1) - if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { - str = str[:delims[len(delims)-1][0]] - } - } - if !flags.preserveSpace { - str = strings.TrimSpace(str) - } - if !flags.file { - str = quoteEntry(str) - } - replacements[idx] = str + replacements[idx] = replace(item) } + if flags.file { return writeTemporaryFile(replacements, printsep) } |
