summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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
+ })
+}