filter: ability to use negative patterns

This is quite similar to gitignore. If a pattern is suffixed by an
exclamation mark and match a file that was previously matched by a
regular pattern, the match is cancelled. Notably, this can be used
with `--exclude-file` to cancel the exclusion of some files.

Like for gitignore, once a directory is excluded, it is not possible
to include files inside the directory. For example, a user wanting to
only keep `*.c` in some directory should not use:

    ~/work
    !~/work/*.c

But:

    ~/work/*
    !~/work/*.c

I didn't write documentation or changelog entry. I would like to get
feedback if this is the right approach for excluding/including files
at will for backups. I use something like this as an exclude file to
backup my home:

    $HOME/**/*
    !$HOME/Documents
    !$HOME/code
    !$HOME/.emacs.d
    !$HOME/games
    # [...]
    node_modules
    *~
    *.o
    *.lo
    *.pyc
    # [...]
    $HOME/code/linux/*
    !$HOME/code/linux/.git
    # [...]

There are some limitations for this change:

 - Patterns are not mixed accross methods: patterns from file are
   handled first and if a file is excluded with this method, it's not
   possible to reinclude it with `--exclude !something`.

 - Patterns starting with `!` are now interpreted as a negative
   pattern. I don't think anyone was relying on that.

 - The whole list of patterns is walked for each match. We may
   optimize later by exiting early if we know no pattern is starting
   with `!`.

Fix #233
This commit is contained in:
Vincent Bernat
2019-07-02 21:36:23 +02:00
committed by Alexander Neumann
parent 12606b575f
commit 2ee07ded2b
4 changed files with 86 additions and 10 deletions

View File

@@ -18,7 +18,8 @@ type patternPart struct {
// Pattern represents a preparsed filter pattern
type Pattern struct {
parts []patternPart
parts []patternPart
isNegated bool
}
func prepareStr(str string) ([]string, error) {
@@ -29,6 +30,12 @@ func prepareStr(str string) ([]string, error) {
}
func preparePattern(patternStr string) Pattern {
var negate bool
if patternStr[0] == '!' {
negate = true
patternStr = patternStr[1:]
}
pathParts := splitPath(filepath.Clean(patternStr))
parts := make([]patternPart, len(pathParts))
for i, part := range pathParts {
@@ -41,7 +48,7 @@ func preparePattern(patternStr string) Pattern {
parts[i] = patternPart{part, isSimple}
}
return Pattern{parts}
return Pattern{parts, negate}
}
// Split p into path components. Assuming p has been Cleaned, no component
@@ -123,7 +130,7 @@ func childMatch(pattern Pattern, strs []string) (matched bool, err error) {
} else {
l = len(strs)
}
return match(Pattern{pattern.parts[0:l]}, strs)
return match(Pattern{pattern.parts[0:l], pattern.isNegated}, strs)
}
func hasDoubleWildcard(list Pattern) (ok bool, pos int) {
@@ -151,7 +158,7 @@ func match(pattern Pattern, strs []string) (matched bool, err error) {
}
newPat = append(newPat, pattern.parts[pos+1:]...)
matched, err := match(Pattern{newPat}, strs)
matched, err := match(Pattern{newPat, pattern.isNegated}, strs)
if err != nil {
return false, err
}
@@ -234,7 +241,9 @@ func ListWithChild(patterns []Pattern, str string) (matched bool, childMayMatch
return list(patterns, true, str)
}
// List returns true if str matches one of the patterns. Empty patterns are ignored.
// list returns true if str matches one of the patterns. Empty patterns are ignored.
// Patterns prefixed by "!" are negated: any matching file excluded by a previous pattern
// will become included again.
func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool, childMayMatch bool, err error) {
if len(patterns) == 0 {
return false, false, nil
@@ -260,11 +269,12 @@ func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool,
c = true
}
matched = matched || m
childMayMatch = childMayMatch || c
if matched && childMayMatch {
return true, true, nil
if pat.isNegated {
matched = matched && !m
childMayMatch = childMayMatch && !m
} else {
matched = matched || m
childMayMatch = childMayMatch || c
}
}

View File

@@ -259,7 +259,20 @@ var filterListTests = []struct {
{[]string{"/*/*/bar/test.*"}, "/foo/bar/bar", false, true},
{[]string{"/*/*/bar/test.*", "*.go"}, "/foo/bar/test.go", true, true},
{[]string{"", "*.c"}, "/foo/bar/test.go", false, true},
{[]string{"!**", "*.go"}, "/foo/bar/test.go", true, true},
{[]string{"!**", "*.c"}, "/foo/bar/test.go", false, true},
{[]string{"/foo/*/test.*", "!*.c"}, "/foo/bar/test.c", false, false},
{[]string{"/foo/*/test.*", "!*.c"}, "/foo/bar/test.go", true, true},
{[]string{"/foo/*/*", "!test.*", "*.c"}, "/foo/bar/test.go", false, true},
{[]string{"/foo/*/*", "!test.*", "*.c"}, "/foo/bar/test.c", true, true},
{[]string{"/foo/*/*", "!test.*", "*.c"}, "/foo/bar/file.go", true, true},
{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/other/test.go", true, true},
{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/bar", false, false},
{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/bar/test.go", false, false},
{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar"}, "/foo/bar/test.go/child", false, false},
{[]string{"/**/*", "!/foo", "/foo/*", "!/foo/bar", "/foo/bar/test*"}, "/foo/bar/test.go/child", true, true},
{[]string{"/foo/bar/*"}, "/foo", false, true},
{[]string{"/foo/bar/*", "!/foo/bar/[a-m]*"}, "/foo", false, true},
{[]string{"/foo/**/test.c"}, "/foo/bar/foo/bar/test.c", true, true},
{[]string{"/foo/*/test.c"}, "/foo/bar/foo/bar/test.c", false, false},
}