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
This commit is contained in:
parent
dc55e68524
commit
e85a8a68d0
@ -55,11 +55,13 @@ type Pattern struct {
|
|||||||
var (
|
var (
|
||||||
_patternCache map[string]*Pattern
|
_patternCache map[string]*Pattern
|
||||||
_splitRegex *regexp.Regexp
|
_splitRegex *regexp.Regexp
|
||||||
|
_escapedPrefixRegex *regexp.Regexp
|
||||||
_cache ChunkCache
|
_cache ChunkCache
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
_splitRegex = regexp.MustCompile("\\s+")
|
_splitRegex = regexp.MustCompile(" +")
|
||||||
|
_escapedPrefixRegex = regexp.MustCompile("^\\\\['!^]")
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
clearChunkCache()
|
clearChunkCache()
|
||||||
}
|
}
|
||||||
@ -80,7 +82,10 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
|||||||
|
|
||||||
var asString string
|
var asString string
|
||||||
if extended {
|
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 {
|
} else {
|
||||||
asString = string(runes)
|
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 {
|
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||||
|
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||||
tokens := _splitRegex.Split(str, -1)
|
tokens := _splitRegex.Split(str, -1)
|
||||||
sets := []termSet{}
|
sets := []termSet{}
|
||||||
set := termSet{}
|
set := termSet{}
|
||||||
switchSet := false
|
switchSet := false
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
typ, inv, text := termFuzzy, false, token
|
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
||||||
lowerText := strings.ToLower(text)
|
lowerText := strings.ToLower(text)
|
||||||
caseSensitive := caseMode == CaseRespect ||
|
caseSensitive := caseMode == CaseRespect ||
|
||||||
caseMode == CaseSmart && text != lowerText
|
caseMode == CaseSmart && text != lowerText
|
||||||
@ -167,6 +173,15 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
text = text[1:]
|
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, "'") {
|
if strings.HasPrefix(text, "'") {
|
||||||
// Flip exactness
|
// Flip exactness
|
||||||
if fuzzy && !inv {
|
if fuzzy && !inv {
|
||||||
@ -177,16 +192,16 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
text = text[1:]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(text, "^") {
|
} else if strings.HasPrefix(text, "^") {
|
||||||
if strings.HasSuffix(text, "$") {
|
if typ == termSuffix {
|
||||||
typ = termEqual
|
typ = termEqual
|
||||||
text = text[1 : len(text)-1]
|
|
||||||
} else {
|
} else {
|
||||||
typ = termPrefix
|
typ = termPrefix
|
||||||
|
}
|
||||||
text = text[1:]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
} else if strings.HasSuffix(text, "$") {
|
|
||||||
typ = termSuffix
|
if _escapedPrefixRegex.MatchString(text) {
|
||||||
text = text[:len(text)-1]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(text) > 0 {
|
if len(text) > 0 {
|
||||||
@ -236,7 +251,7 @@ func (p *Pattern) CacheKey() string {
|
|||||||
cacheableTerms = append(cacheableTerms, string(termSet[0].text))
|
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
|
// 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())
|
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||||
}
|
}
|
||||||
if pat.cacheable != cacheable {
|
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()
|
clearPatternCache()
|
||||||
}
|
}
|
||||||
test(false, "foo !bar", "foo !bar", true)
|
test(false, "foo !bar", "foo !bar", true)
|
||||||
test(false, "foo | bar !baz", "foo | bar !baz", 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", "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", "baz", false)
|
||||||
test(true, "foo | bar | baz", "", false)
|
test(true, "foo | bar | baz", "", false)
|
||||||
test(true, "foo | bar !baz", "", false)
|
test(true, "foo | bar !baz", "", false)
|
||||||
@ -192,11 +192,11 @@ func TestCacheable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
}
|
}
|
||||||
test(true, "foo bar", "foo bar", true)
|
test(true, "foo bar", "foo\tbar", true)
|
||||||
test(true, "foo 'bar", "foo bar", false)
|
test(true, "foo 'bar", "foo\tbar", false)
|
||||||
test(true, "foo !bar", "foo", 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 'bar", "foo", false)
|
||||||
test(false, "foo '", "foo", true)
|
test(false, "foo '", "foo", true)
|
||||||
test(false, "foo 'bar", "foo", false)
|
test(false, "foo 'bar", "foo", false)
|
||||||
|
@ -281,9 +281,13 @@ func defaultKeymap() map[int][]action {
|
|||||||
return keymap
|
return keymap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trimQuery(query string) []rune {
|
||||||
|
return []rune(strings.Replace(query, "\t", " ", -1))
|
||||||
|
}
|
||||||
|
|
||||||
// NewTerminal returns new Terminal object
|
// NewTerminal returns new Terminal object
|
||||||
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||||
input := []rune(opts.Query)
|
input := trimQuery(opts.Query)
|
||||||
var header []string
|
var header []string
|
||||||
if opts.Reverse {
|
if opts.Reverse {
|
||||||
header = opts.Header
|
header = opts.Header
|
||||||
@ -1694,13 +1698,13 @@ func (t *Terminal) Loop() {
|
|||||||
case actPreviousHistory:
|
case actPreviousHistory:
|
||||||
if t.history != nil {
|
if t.history != nil {
|
||||||
t.history.override(string(t.input))
|
t.history.override(string(t.input))
|
||||||
t.input = []rune(t.history.previous())
|
t.input = trimQuery(t.history.previous())
|
||||||
t.cx = len(t.input)
|
t.cx = len(t.input)
|
||||||
}
|
}
|
||||||
case actNextHistory:
|
case actNextHistory:
|
||||||
if t.history != nil {
|
if t.history != nil {
|
||||||
t.history.override(string(t.input))
|
t.history.override(string(t.input))
|
||||||
t.input = []rune(t.history.next())
|
t.input = trimQuery(t.history.next())
|
||||||
t.cx = len(t.input)
|
t.cx = len(t.input)
|
||||||
}
|
}
|
||||||
case actSigStop:
|
case actSigStop:
|
||||||
|
@ -1378,6 +1378,28 @@ class TestGoFZF < TestBase
|
|||||||
tmux.send_keys 'a'
|
tmux.send_keys 'a'
|
||||||
tmux.until { |lines| lines.none? { |line| line.include? '1 2 3 4 5' } }
|
tmux.until { |lines| lines.none? { |line| line.include? '1 2 3 4 5' } }
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
|
Loading…
x
Reference in New Issue
Block a user