Skip to content

Commit

Permalink
Add more tests of placeholder flags and simplify its logic (junegunn#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
vovcacik authored Oct 15, 2021
1 parent 50eb2e3 commit 61339a8
Show file tree
Hide file tree
Showing 4 changed files with 499 additions and 88 deletions.
154 changes: 84 additions & 70 deletions src/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{}

Expand All @@ -1561,6 +1565,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
skipChars++
case 'q':
flags.query = true
// query flag is not skipped
default:
break
}
Expand Down Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit 61339a8

Please sign in to comment.