Allow escaping meta characters with backslashes

One can escape meta characters in extended-search mode with backslashes.

  Prefixes:
    \'
    \!
    \^

  Suffix:
    \$

  Term separator:
    \<SPACE>

To keep things simple, we are not going to support escaping of escaped
sequences (e.g. \\') for matching them literally.

Since this is a breaking change, we will bump the minor version.

Close #444
pull/1015/head
Junegunn Choi 7 years ago
parent dc55e68524
commit e85a8a68d0
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -53,13 +53,15 @@ type Pattern struct {
}
var (
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_escapedPrefixRegex *regexp.Regexp
_cache ChunkCache
)
func init() {
_splitRegex = regexp.MustCompile("\\s+")
_splitRegex = regexp.MustCompile(" +")
_escapedPrefixRegex = regexp.MustCompile("^\\\\['!^]")
clearPatternCache()
clearChunkCache()
}
@ -80,7 +82,10 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
var asString string
if extended {
asString = strings.Trim(string(runes), " ")
asString = strings.TrimLeft(string(runes), " ")
for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") {
asString = asString[:len(asString)-1]
}
} else {
asString = string(runes)
}
@ -140,12 +145,13 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
}
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
str = strings.Replace(str, "\\ ", "\t", -1)
tokens := _splitRegex.Split(str, -1)
sets := []termSet{}
set := termSet{}
switchSet := false
for _, token := range tokens {
typ, inv, text := termFuzzy, false, token
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText
@ -167,6 +173,15 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
text = text[1:]
}
if strings.HasSuffix(text, "$") {
if strings.HasSuffix(text, "\\$") {
text = text[:len(text)-2] + "$"
} else {
typ = termSuffix
text = text[:len(text)-1]
}
}
if strings.HasPrefix(text, "'") {
// Flip exactness
if fuzzy && !inv {
@ -177,16 +192,16 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
text = text[1:]
}
} else if strings.HasPrefix(text, "^") {
if strings.HasSuffix(text, "$") {
if typ == termSuffix {
typ = termEqual
text = text[1 : len(text)-1]
} else {
typ = termPrefix
text = text[1:]
}
} else if strings.HasSuffix(text, "$") {
typ = termSuffix
text = text[:len(text)-1]
text = text[1:]
}
if _escapedPrefixRegex.MatchString(text) {
text = text[1:]
}
if len(text) > 0 {
@ -236,7 +251,7 @@ func (p *Pattern) CacheKey() string {
cacheableTerms = append(cacheableTerms, string(termSet[0].text))
}
}
return strings.Join(cacheableTerms, " ")
return strings.Join(cacheableTerms, "\t")
}
// Match returns the list of matches Items in the given Chunk

@ -165,15 +165,15 @@ func TestCacheKey(t *testing.T) {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if pat.cacheable != cacheable {
t.Errorf("Expected: %s, actual: %s (%s)", cacheable, pat.cacheable, patStr)
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
}
clearPatternCache()
}
test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true)
test(true, "foo bar baz", "foo bar baz", true)
test(true, "foo bar baz", "foo\tbar\tbaz", true)
test(true, "foo !bar", "foo", false)
test(true, "foo !bar baz", "foo baz", false)
test(true, "foo !bar baz", "foo\tbaz", false)
test(true, "foo | bar baz", "baz", false)
test(true, "foo | bar | baz", "", false)
test(true, "foo | bar !baz", "", false)
@ -192,11 +192,11 @@ func TestCacheable(t *testing.T) {
}
clearPatternCache()
}
test(true, "foo bar", "foo bar", true)
test(true, "foo 'bar", "foo bar", false)
test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false)
test(true, "foo !bar", "foo", false)
test(false, "foo bar", "foo bar", true)
test(false, "foo bar", "foo\tbar", true)
test(false, "foo 'bar", "foo", false)
test(false, "foo '", "foo", true)
test(false, "foo 'bar", "foo", false)

@ -281,9 +281,13 @@ func defaultKeymap() map[int][]action {
return keymap
}
func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query)
input := trimQuery(opts.Query)
var header []string
if opts.Reverse {
header = opts.Header
@ -1694,13 +1698,13 @@ func (t *Terminal) Loop() {
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.input = trimQuery(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.input = trimQuery(t.history.next())
t.cx = len(t.input)
}
case actSigStop:

@ -1378,6 +1378,28 @@ class TestGoFZF < TestBase
tmux.send_keys 'a'
tmux.until { |lines| lines.none? { |line| line.include? '1 2 3 4 5' } }
end
def test_escaped_meta_characters
input = <<~EOF
foo^bar
foo$bar
foo!bar
foo'bar
foo bar
bar foo
EOF
writelines tempname, input.lines.map(&:chomp)
assert_equal input.lines.count, `#{FZF} -f'foo bar' < #{tempname}`.lines.count
assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines.map(&:chomp)
assert_equal ['bar foo'], `#{FZF} -f'foo$' < #{tempname}`.lines.map(&:chomp)
assert_equal ['foo$bar'], `#{FZF} -f'foo\\$' < #{tempname}`.lines.map(&:chomp)
assert_equal [], `#{FZF} -f'!bar' < #{tempname}`.lines.map(&:chomp)
assert_equal ['foo!bar'], `#{FZF} -f'\\!bar' < #{tempname}`.lines.map(&:chomp)
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp)
assert_equal [], `#{FZF} -f"'br" < #{tempname}`.lines.map(&:chomp)
assert_equal ["foo'bar"], `#{FZF} -f"\\'br" < #{tempname}`.lines.map(&:chomp)
end
end
module TestShell

Loading…
Cancel
Save