forked from TrueCloudLab/restic
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:
parent
12606b575f
commit
2ee07ded2b
4 changed files with 86 additions and 10 deletions
31
changelog/unreleased/issue-233
Normal file
31
changelog/unreleased/issue-233
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
Enhancement: Add negative patterns for include/exclude
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
It works similarly to `gitignore`, with the same limitation: once a
|
||||||
|
directory is excluded, it is not possible to include files inside the
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Example of use (as an exclude pattern for backup):
|
||||||
|
|
||||||
|
$HOME/**/*
|
||||||
|
!$HOME/Documents
|
||||||
|
!$HOME/code
|
||||||
|
!$HOME/.emacs.d
|
||||||
|
!$HOME/games
|
||||||
|
# [...]
|
||||||
|
node_modules
|
||||||
|
*~
|
||||||
|
*.o
|
||||||
|
*.lo
|
||||||
|
*.pyc
|
||||||
|
# [...]
|
||||||
|
$HOME/code/linux/*
|
||||||
|
!$HOME/code/linux/.git
|
||||||
|
# [...]
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/233
|
||||||
|
https://github.com/restic/restic/pull/2311
|
|
@ -289,6 +289,28 @@ On most Unixy shells, you can either quote or use backslashes. For example:
|
||||||
* ``--exclude="foo bar star/foo.txt"``
|
* ``--exclude="foo bar star/foo.txt"``
|
||||||
* ``--exclude=foo\ bar\ star/foo.txt``
|
* ``--exclude=foo\ bar\ star/foo.txt``
|
||||||
|
|
||||||
|
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.
|
||||||
|
It works similarly to ``gitignore``, with the same limitation: once a
|
||||||
|
directory is excluded, it is not possible to include files inside the
|
||||||
|
directory. Here is a complete example to backup a selection of
|
||||||
|
directories inside the home directory. It works by excluding any
|
||||||
|
directory, then selectively add back some of them.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$HOME/**/*
|
||||||
|
!$HOME/Documents
|
||||||
|
!$HOME/code
|
||||||
|
!$HOME/.emacs.d
|
||||||
|
!$HOME/games
|
||||||
|
# [...]
|
||||||
|
node_modules
|
||||||
|
*~
|
||||||
|
*.o
|
||||||
|
*.lo
|
||||||
|
*.pyc
|
||||||
|
|
||||||
By specifying the option ``--one-file-system`` you can instruct restic
|
By specifying the option ``--one-file-system`` you can instruct restic
|
||||||
to only backup files from the file systems the initially specified files
|
to only backup files from the file systems the initially specified files
|
||||||
or directories reside on. In other words, it will prevent restic from crossing
|
or directories reside on. In other words, it will prevent restic from crossing
|
||||||
|
|
|
@ -18,7 +18,8 @@ type patternPart struct {
|
||||||
|
|
||||||
// Pattern represents a preparsed filter pattern
|
// Pattern represents a preparsed filter pattern
|
||||||
type Pattern struct {
|
type Pattern struct {
|
||||||
parts []patternPart
|
parts []patternPart
|
||||||
|
isNegated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareStr(str string) ([]string, error) {
|
func prepareStr(str string) ([]string, error) {
|
||||||
|
@ -29,6 +30,12 @@ func prepareStr(str string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func preparePattern(patternStr string) Pattern {
|
func preparePattern(patternStr string) Pattern {
|
||||||
|
var negate bool
|
||||||
|
if patternStr[0] == '!' {
|
||||||
|
negate = true
|
||||||
|
patternStr = patternStr[1:]
|
||||||
|
}
|
||||||
|
|
||||||
pathParts := splitPath(filepath.Clean(patternStr))
|
pathParts := splitPath(filepath.Clean(patternStr))
|
||||||
parts := make([]patternPart, len(pathParts))
|
parts := make([]patternPart, len(pathParts))
|
||||||
for i, part := range pathParts {
|
for i, part := range pathParts {
|
||||||
|
@ -41,7 +48,7 @@ func preparePattern(patternStr string) Pattern {
|
||||||
parts[i] = patternPart{part, isSimple}
|
parts[i] = patternPart{part, isSimple}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pattern{parts}
|
return Pattern{parts, negate}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split p into path components. Assuming p has been Cleaned, no component
|
// 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 {
|
} else {
|
||||||
l = len(strs)
|
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) {
|
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:]...)
|
newPat = append(newPat, pattern.parts[pos+1:]...)
|
||||||
|
|
||||||
matched, err := match(Pattern{newPat}, strs)
|
matched, err := match(Pattern{newPat, pattern.isNegated}, strs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -234,7 +241,9 @@ func ListWithChild(patterns []Pattern, str string) (matched bool, childMayMatch
|
||||||
return list(patterns, true, str)
|
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) {
|
func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool, childMayMatch bool, err error) {
|
||||||
if len(patterns) == 0 {
|
if len(patterns) == 0 {
|
||||||
return false, false, nil
|
return false, false, nil
|
||||||
|
@ -260,11 +269,12 @@ func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool,
|
||||||
c = true
|
c = true
|
||||||
}
|
}
|
||||||
|
|
||||||
matched = matched || m
|
if pat.isNegated {
|
||||||
childMayMatch = childMayMatch || c
|
matched = matched && !m
|
||||||
|
childMayMatch = childMayMatch && !m
|
||||||
if matched && childMayMatch {
|
} else {
|
||||||
return true, true, nil
|
matched = matched || m
|
||||||
|
childMayMatch = childMayMatch || c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -259,7 +259,20 @@ var filterListTests = []struct {
|
||||||
{[]string{"/*/*/bar/test.*"}, "/foo/bar/bar", false, true},
|
{[]string{"/*/*/bar/test.*"}, "/foo/bar/bar", false, true},
|
||||||
{[]string{"/*/*/bar/test.*", "*.go"}, "/foo/bar/test.go", true, true},
|
{[]string{"/*/*/bar/test.*", "*.go"}, "/foo/bar/test.go", true, true},
|
||||||
{[]string{"", "*.c"}, "/foo/bar/test.go", false, 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", 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", true, true},
|
||||||
{[]string{"/foo/*/test.c"}, "/foo/bar/foo/bar/test.c", false, false},
|
{[]string{"/foo/*/test.c"}, "/foo/bar/foo/bar/test.c", false, false},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue