mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-10 15:50:56 +00:00
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
@ -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…
Reference in New Issue
Block a user