mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2025-01-28 17:48:25 +00:00
3b0c86e401
Fix #3984
688 lines
28 KiB
Go
688 lines
28 KiB
Go
package fzf
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
|
|
"github.com/junegunn/fzf/src/util"
|
|
)
|
|
|
|
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
|
replaced, _ := replacePlaceholder(replacePlaceholderParams{
|
|
template: template,
|
|
stripAnsi: stripAnsi,
|
|
delimiter: delimiter,
|
|
printsep: printsep,
|
|
forcePlus: forcePlus,
|
|
query: query,
|
|
allItems: allItems,
|
|
lastAction: actBackwardDeleteCharEof,
|
|
prompt: "prompt",
|
|
executor: util.NewExecutor(""),
|
|
})
|
|
return replaced
|
|
}
|
|
|
|
func TestReplacePlaceholder(t *testing.T) {
|
|
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
|
items1 := []*Item{item1, item1}
|
|
items2 := []*Item{
|
|
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
|
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
|
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
|
|
|
delim := "'"
|
|
var regex *regexp.Regexp
|
|
|
|
var result string
|
|
check := func(expected string) {
|
|
if result != expected {
|
|
t.Errorf("expected: %s, actual: %s", expected, result)
|
|
}
|
|
}
|
|
// helper function that converts template format into string and carries out the check()
|
|
checkFormat := func(format string) {
|
|
type quotes struct{ O, I, S string } // outer, inner quotes, print separator
|
|
unixStyle := quotes{`'`, `'\''`, "\n"}
|
|
windowsStyle := quotes{`^"`, `'`, "\n"}
|
|
var effectiveStyle quotes
|
|
|
|
if util.IsWindows() {
|
|
effectiveStyle = windowsStyle
|
|
} else {
|
|
effectiveStyle = unixStyle
|
|
}
|
|
|
|
expected := templateToString(format, effectiveStyle)
|
|
check(expected)
|
|
}
|
|
printsep := "\n"
|
|
|
|
/*
|
|
Test multiple placeholders and the function parameters.
|
|
*/
|
|
|
|
// {}, preserve ansi
|
|
result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
|
|
|
// {}, strip ansi
|
|
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
|
|
|
// {}, with multiple items
|
|
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
|
|
|
// {..}, strip leading whitespaces, preserve ansi
|
|
result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
|
|
|
// {..}, strip leading whitespaces, strip ansi
|
|
result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
|
|
|
// {q}
|
|
result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
|
|
|
// {q}, multiple items
|
|
result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
|
|
|
// forcePlus
|
|
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
|
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
|
|
|
// Whitespace preserving flag with "'" delimiter
|
|
result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}}bar baz{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
|
|
|
// Whitespace preserving flag with regex delimiter
|
|
regex = regexp.MustCompile(`\w+`)
|
|
|
|
result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} {{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}}{{.I}}{{.O}}")
|
|
|
|
result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} {{.O}}")
|
|
|
|
// No match
|
|
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
|
check("echo /")
|
|
|
|
// No match, but with selections
|
|
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
|
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
|
|
|
// String delimiter
|
|
result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
|
|
|
// Regex delimiter
|
|
regex = regexp.MustCompile("[oa]+")
|
|
// foo'bar baz
|
|
result = replacePlaceholderTest("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
|
|
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}}"
|
|
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
|
|
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
|
|
|
|
// IV. escaping placeholder
|
|
templateToOutput[`\{}`] = `{}`
|
|
templateToOutput[`\{q}`] = `{q}`
|
|
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
|
|
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
|
|
templateToOutput[`\{++}`] = `{++}`
|
|
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
|
|
|
for giveTemplate, wantOutput := range templateToOutput {
|
|
result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
|
checkFormat(wantOutput)
|
|
}
|
|
for giveTemplate, wantOutput := range templateToFile {
|
|
path := replacePlaceholderTest(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 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
|
|
exec := util.NewExecutor("")
|
|
|
|
if util.IsWindows() {
|
|
effectiveStyle = windowsStyle
|
|
} else {
|
|
effectiveStyle = unixStyle
|
|
}
|
|
|
|
tests := map[string]string{
|
|
`'`: `{{.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 := exec.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
|
|
// (not necessarily unexpected)
|
|
|
|
// 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) {
|
|
// XXX Deprecated
|
|
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
|
|
// (not necessarily unexpected)
|
|
|
|
// 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"`
|
|
// TODO this should actually work
|
|
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
|
|
}
|
|
testCommands(t, tests)
|
|
}
|
|
|
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
|
|
func TestPowershellCommands(t *testing.T) {
|
|
if !util.IsWindows() {
|
|
t.SkipNow()
|
|
}
|
|
|
|
tests := []testCase{
|
|
// reference: give{template, query, items}, want{output OR match}
|
|
|
|
/*
|
|
You can read each line in the following table as a pipeline that
|
|
consist of series of parsers that act upon your input (col. 1) and
|
|
each cell represents the output value.
|
|
|
|
For example:
|
|
- exec.Command("program.exe", `\''`)
|
|
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
|
|
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
|
|
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
|
|
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
|
|
|
|
Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
|
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
|
" empty string² missing argument error ... ... |
|
|
\" literal " unbalanced quote error ... ... |
|
|
'\"' literal '"' literal " empty string empty string (match all) | yes
|
|
'\\\"' literal '\"' literal \" literal " literal " |
|
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
|
\ transparent transparent transparent regex error |
|
|
'\' transparent literal \ literal \ regex error | yes
|
|
\\ transparent transparent transparent literal \ |
|
|
'\\' transparent literal \\ literal \\ literal \ |
|
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
|
' transparent unbalanced quote error ... ... |
|
|
\' transparent literal \ and unb. quote error ... ... |
|
|
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
|
|
''' transparent unbalanced quote error ... ... |
|
|
'''' transparent literal ' literal ' literal ' | yes
|
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
|
|
|
⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
|
|
¹: native commands like grep, git grep, ripgrep
|
|
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
|
|
|
|
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
|
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
|
|
*/
|
|
|
|
// 1) working examples
|
|
|
|
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
|
|
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
|
|
|
|
// example of escaping single quotes
|
|
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
|
|
|
|
// chaining powershells
|
|
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
|
|
|
|
// 2) problematic examples
|
|
// (not necessarily unexpected)
|
|
|
|
// looking for a path string will only work with escaped backslashes
|
|
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
|
|
// looking for a literal double quote will only work with triple escaped double quotes
|
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
|
|
|
|
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
|
|
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
|
|
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
|
|
|
|
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
|
|
// to explorer, which will prompt user to pick editing program for the fzf-preview file
|
|
// the temp file contains: `cat "C:\test.txt"`
|
|
// TODO this should actually work
|
|
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
|
|
}
|
|
|
|
// to force powershell-style escaping we temporarily set environment variable that fzf honors
|
|
shellBackup := os.Getenv("SHELL")
|
|
os.Setenv("SHELL", "powershell")
|
|
testCommands(t, tests)
|
|
os.Setenv("SHELL", shellBackup)
|
|
}
|
|
|
|
/*
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractPassthroughs(t *testing.T) {
|
|
for _, middle := range []string{
|
|
"\x1bPtmux;\x1b\x1bbar\x1b\\",
|
|
"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
|
|
"\x1b]1337;bar\x1b\\",
|
|
"\x1b]1337;bar\x1bbar\x1b\\",
|
|
"\x1b]1337;bar\a",
|
|
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
|
|
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
|
|
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
|
|
"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
|
|
"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
|
|
"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
|
|
} {
|
|
line := "foo" + middle + "baz"
|
|
loc := findPassThrough(line)
|
|
if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
|
|
t.Error("failed to find passthrough")
|
|
}
|
|
garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
|
|
line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
|
|
passthroughs, result := extractPassThroughs(line)
|
|
if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
|
|
t.Error("failed to extract passthroughs")
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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{}
|
|
|
|
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
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 := replacePlaceholderTest(
|
|
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.forceUpdate { // FIXME
|
|
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
|
|
}
|
|
}
|
|
}
|