summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVlastimil Ovčáčík <vovcacik@github.ovcacik.org>2021-10-15 15:31:59 +0200
committerGitHub <noreply@github.com>2021-10-15 22:31:59 +0900
commit61339a8ae2f6be27f28c243f00a41cc3aa5f54c2 (patch)
treee8bab761dde12006078deedffe1555ab66cce095
parent50eb2e38552f57bce84c417fad8f4b48fcbf16ac (diff)
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.
-rw-r--r--src/terminal.go154
-rw-r--r--src/terminal_test.go417
-rw-r--r--src/terminal_unix.go5
-rw-r--r--src/terminal_windows.go11
4 files changed, 499 insertions, 88 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)
}
diff --git a/src/terminal_test.go b/src/terminal_test.go
index bb10ec1b..69d5b1ef 100644
--- a/src/terminal_test.go
+++ b/src/terminal_test.go
@@ -2,19 +2,16 @@ package fzf
import (
"bytes"
+ "io"
+ "os"
"regexp"
+ "strings"
"testing"
"text/template"
"github.com/junegunn/fzf/src/util"
)
-func newItem(str string) *Item {
- bytes := []byte(str)
- trimmed, _, _ := extractColor(str, nil, nil)
- return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
-}
-
func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
@@ -34,9 +31,9 @@ func TestReplacePlaceholder(t *testing.T) {
}
// helper function that converts template format into string and carries out the check()
checkFormat := func(format string) {
- type quotes struct{ O, I string } // outer, inner quotes
- unixStyle := quotes{"'", "'\\''"}
- windowsStyle := quotes{"^\"", "'"}
+ type quotes struct{ O, I, S string } // outer, inner quotes, print separator
+ unixStyle := quotes{`'`, `'\''`, "\n"}
+ windowsStyle := quotes{`^"`, `'`, "\n"}
var effectiveStyle quotes
if util.IsWindows() {
@@ -49,6 +46,11 @@ func TestReplacePlaceholder(t *testing.T) {
check(expected)
}
printsep := "\n"
+
+ /*
+ Test multiple placeholders and the function parameters.
+ */
+
// {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
@@ -135,27 +137,291 @@ func TestReplacePlaceholder(t *testing.T) {
// foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
+
+ /*
+ Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
+ see: TestParsePlaceholder
+ */
+ items3 := []*Item{
+ // single line
+ newItem("1a 1b 1c 1d 1e 1f"),
+ // multi line
+ newItem("1a 1b 1c 1d 1e 1f"),
+ newItem("2a 2b 2c 2d 2e 2f"),
+ newItem("3a 3b 3c 3d 3e 3f"),
+ newItem("4a 4b 4c 4d 4e 4f"),
+ newItem("5a 5b 5c 5d 5e 5f"),
+ newItem("6a 6b 6c 6d 6e 6f"),
+ newItem("7a 7b 7c 7d 7e 7f"),
+ }
+ stripAnsi := false
+ printsep = "\n"
+ forcePlus := false
+ query := "sample query"
+
+ templateToOutput := make(map[string]string)
+ templateToFile := make(map[string]string) // same as above, but the file contents will be matched
+ // I. item type placeholder
+ templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
+ templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
+ templateToOutput[`{n}`] = `0`
+ templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
+ templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
+ templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
+ templateToFile[`{nf}`] = `0{{.S}}`
+ templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
+
+ // II. token type placeholders
+ templateToOutput[`{..}`] = templateToOutput[`{}`]
+ templateToOutput[`{1..}`] = templateToOutput[`{}`]
+ templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
+ templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
+ templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
+ // shorthand for x..x range
+ templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
+ templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
+ templateToOutput[`{-6}`] = templateToOutput[`{1}`]
+ // multiple ranges
+ templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
+ templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
+ templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
+ templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
+ // flags
+ templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
+ templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
+ templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
+ templateToFile[`{f1}`] = `1a{{.S}}`
+ templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
+ templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
+
+ // III. query type placeholder
+ // query flag is not removed after parsing, so it gets doubled
+ // while the double q is invalid, it is useful here for testing purposes
+ templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
+
+ // IV. escaping placeholder
+ templateToOutput[`\{}`] = `{}`
+ templateToOutput[`\{++}`] = `{++}`
+ templateToOutput[`{++}`] = templateToOutput[`{+}`]
+
+ for giveTemplate, wantOutput := range templateToOutput {
+ result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
+ checkFormat(wantOutput)
+ }
+ for giveTemplate, wantOutput := range templateToFile {
+ path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
+
+ data, err := readFile(path)
+ if err != nil {
+ t.Errorf("Cannot read the content of the temp file %s.", path)
+ }
+ result = string(data)
+
+ checkFormat(wantOutput)
+ }
}
-func TestQuoteEntryCmd(t *testing.T) {
+func TestQuoteEntry(t *testing.T) {
+ type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
+ unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
+ windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
+ var effectiveStyle quotes
+
+ if util.IsWindows() {
+ effectiveStyle = windowsStyle
+ } else {
+ effectiveStyle = unixStyle
+ }
+
tests := map[string]string{
- `"`: `^"\^"^"`,
- `\`: `^"\\^"`,
- `\"`: `^"\\\^"^"`,
- `"\\\"`: `^"\^"\\\\\\\^"^"`,
- `&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
- `%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
- `C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
+ `'`: `{{.O}}{{.SQ}}{{.O}}`,
+ `"`: `{{.O}}{{.DQ}}{{.O}}`,
+ `\`: `{{.O}}{{.BS}}{{.O}}`,
+ `\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
+ `"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
+
+ `$`: `{{.O}}${{.O}}`,
+ `$HOME`: `{{.O}}$HOME{{.O}}`,
+ `'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
+
+ `&`: `{{.O}}{{.E}}&{{.O}}`,
+ `|`: `{{.O}}{{.E}}|{{.O}}`,
+ `<`: `{{.O}}{{.E}}<{{.O}}`,
+ `>`: `{{.O}}{{.E}}>{{.O}}`,
+ `(`: `{{.O}}{{.E}}({{.O}}`,
+ `)`: `{{.O}}{{.E}}){{.O}}`,
+ `@`: `{{.O}}{{.E}}@{{.O}}`,
+ `^`: `{{.O}}{{.E}}^{{.O}}`,
+ `%`: `{{.O}}{{.E}}%{{.O}}`,
+ `!`: `{{.O}}{{.E}}!{{.O}}`,
+ `%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
+ `C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
+ `"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
}
for input, expected := range tests {
- escaped := quoteEntryCmd(input)
+ escaped := quoteEntry(input)
+ expected = templateToString(expected, effectiveStyle)
if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
}
}
}
+// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
+func TestUnixCommands(t *testing.T) {
+ if util.IsWindows() {
+ t.SkipNow()
+ }
+ tests := []testCase{
+ // reference: give{template, query, items}, want{output OR match}
+
+ // 1) working examples
+
+ // paths that does not have to evaluated will work fine, when quoted
+ {give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
+ {give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
+ {give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
+
+ // only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
+ // quoting the tilde is required (to be treated as string)
+ {give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
+
+ // 2) problematic examples
+
+ // paths that need to expand some part of it won't work (special characters and variables)
+ {give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
+ {give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
+ }
+ testCommands(t, tests)
+}
+
+// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
+func TestWindowsCommands(t *testing.T) {
+ if !util.IsWindows() {
+ t.SkipNow()
+ }
+ tests := []testCase{
+ // reference: give{template, query, items}, want{output OR match}
+
+ // 1) working examples
+
+ // example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
+ {give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
+ {give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
+ // example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
+ {give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
+ // example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
+ {give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
+
+ // 2) problematic examples
+
+ // notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
+ {give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
+
+ // cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
+ // cat: "C:\\test.txt: Invalid argument
+ {give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
+ // cat: "C:\\test.txt": Invalid argument
+ {give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
+
+ // the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
+ // the temp file contains: `cat "C:\test.txt"`
+ {give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
+ }
+ testCommands(t, tests)
+}
+
+/*
+ Test typical valid placeholders and parsing of them.
+
+ Also since the parser assumes the input is matched with `placeholder` regex,
+ the regex is tested here as well.
+*/
+func TestParsePlaceholder(t *testing.T) {
+ // give, want pairs
+ templates := map[string]string{
+ // I. item type placeholder
+ `{}`: `{}`,
+ `{+}`: `{+}`,
+ `{n}`: `{n}`,
+ `{+n}`: `{+n}`,
+ `{f}`: `{f}`,
+ `{+nf}`: `{+nf}`,
+
+ // II. token type placeholders
+ `{..}`: `{..}`,
+ `{1..}`: `{1..}`,
+ `{..2}`: `{..2}`,
+ `{1..2}`: `{1..2}`,
+ `{-2..-1}`: `{-2..-1}`,
+ // shorthand for x..x range
+ `{1}`: `{1}`,
+ `{1..1}`: `{1..1}`,
+ `{-6}`: `{-6}`,
+ // multiple ranges
+ `{1,2}`: `{1,2}`,
+ `{1,2,4}`: `{1,2,4}`,
+ `{1,2..4}`: `{1,2..4}`,
+ `{1..2,-4..-3}`: `{1..2,-4..-3}`,
+ // flags
+ `{+1}`: `{+1}`,
+ `{+-1}`: `{+-1}`,
+ `{s1}`: `{s1}`,
+ `{f1}`: `{f1}`,
+ `{+s1..2}`: `{+s1..2}`,
+ `{+sf1..2}`: `{+sf1..2}`,
+
+ // III. query type placeholder
+ // query flag is not removed after parsing, so it gets doubled
+ // while the double q is invalid, it is useful here for testing purposes
+ `{q}`: `{qq}`,
+
+ // IV. escaping placeholder
+ `\{}`: `{}`,
+ `\{++}`: `{++}`,
+ `{++}`: `{+}`,
+ }
+
+ for giveTemplate, wantTemplate := range templates {
+ if !placeholder.MatchString(giveTemplate) {
+ t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
+ continue
+ }
+
+ _, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
+ gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
+
+ if gotTemplate != wantTemplate {
+ t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
+ }
+ }
+}
+
+/* utilities section */
+
+// Item represents one line in fzf UI. Usually it is relative path to files and folders.
+func newItem(str string) *Item {
+ bytes := []byte(str)
+ trimmed, _, _ := extractColor(str, nil, nil)
+ return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
+}
+
+// Functions tested in this file require array of items (allItems). The array needs
+// to consist of at least two nils. This is helper function.
+func newItems(str ...string) []*Item {
+ result := make([]*Item, util.Max(len(str), 2))
+ for i, s := range str {
+ result[i] = newItem(s)
+ }
+ return result
+}
+
+// (for logging purposes)
+func (item *Item) String() string {
+ return item.AsString(true)
+}
+
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data interface{}) string {
bb := &bytes.Buffer{}
@@ -167,3 +433,118 @@ func templateToString(format string, data interface{}) string {
return bb.String()
}
+
+// ad hoc types for test cases
+type give struct {
+ template string
+ query string
+ allItems []*Item
+}
+type want struct {
+ /*
+ Unix:
+ The `want.output` string is supposed to be formatted for evaluation by
+ `sh -c command` system call.
+
+ Windows:
+ The `want.output` string is supposed to be formatted for evaluation by
+ `cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
+ behaviour, which is more favourable for nesting (possibly escaped)
+ special characters. This is the relevant section of `help cmd`:
+
+ ...old behavior is to see if the first character is
+ a quote character and if so, strip the leading character and
+ remove the last quote character on the command line, preserving
+ any text after the last quote character.
+ */
+ output string // literal output
+ match string // output is matched against this regex (when output is empty string)
+}
+type testCase struct {
+ give
+ want
+}
+
+func testCommands(t *testing.T, tests []testCase) {
+ // common test parameters
+ delim := "\t"
+ delimiter := Delimiter{str: &delim}
+ printsep := ""
+ stripAnsi := false
+ forcePlus := false
+
+ // evaluate the test cases
+ for idx, test := range tests {
+ gotOutput := replacePlaceholder(
+ test.give.template, stripAnsi, delimiter, printsep, forcePlus,
+ test.give.query,
+ test.give.allItems)
+ switch {
+ case test.want.output != "":
+ if gotOutput != test.want.output {
+ t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
+ idx,
+ test.give.template, test.give.query, test.give.allItems,
+ gotOutput, test.want.output)
+ }
+ case test.want.match != "":
+ wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
+ wantRegex := regexp.MustCompile(wantMatch)
+ if !wantRegex.MatchString(gotOutput) {
+ t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
+ idx,
+ test.give.template, test.give.query, test.give.allItems,
+ gotOutput, test.want.match)
+ }
+ default:
+ t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
+ }
+ }
+}
+
+// naive encoder of placeholder flags
+func (flags placeholderFlags) encodePlaceholder() string {
+ encoded := ""
+ if flags.plus {
+ encoded += "+"
+ }
+ if flags.preserveSpace {
+ encoded += "s"
+ }
+ if flags.number {
+ encoded += "n"
+ }
+ if flags.file {
+ encoded += "f"
+ }
+ if flags.query {
+ encoded += "q"
+ }
+ return encoded
+}
+
+// can be replaced with os.ReadFile() in go 1.16+
+func readFile(path string) ([]byte, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ data := make([]byte, 0, 128)
+ for {
+ if len(data) >= cap(data) {
+ d := append(data[:cap(data)], 0)
+ data = d[:len(data)]
+ }
+
+ n, err := file.Read(data[len(data):cap(data)])
+ data = data[:len(data)+n]
+ if err != nil {
+ if err == io.EOF {
+ err = nil
+ }
+ return data, err
+ }
+ }
+}
diff --git a/src/terminal_unix.go b/src/terminal_unix.go
index 2ae8175a..b14cd684 100644
--- a/src/terminal_unix.go
+++ b/src/terminal_unix.go
@@ -5,6 +5,7 @@ package fzf
import (
"os"
"os/signal"
+ "strings"
"syscall"
)
@@ -19,3 +20,7 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}
+
+func quoteEntry(entry string) string {
+ return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
+}
diff --git a/src/terminal_windows.go b/src/terminal_windows.go
index 9de7ae45..d4262b65 100644
--- a/src/terminal_windows.go
+++ b/src/terminal_windows.go
@@ -4,6 +4,8 @@ package fzf
import (
"os"
+ "regexp"
+ "strings"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
@@ -17,3 +19,12 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}
+
+func quoteEntry(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
+ })
+}