From 13c40d4199e852deb6b9a9445344c6731bfbfc95 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Tue, 2 Jul 2019 21:11:38 +0200 Subject: [PATCH 1/7] filter: additional tests for filter.List() --- internal/filter/filter_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 30cee40db..149e8cf16 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -248,6 +248,7 @@ var filterListTests = []struct { }{ {[]string{}, "/foo/bar/test.go", false, false}, {[]string{"*.go"}, "/foo/bar/test.go", true, true}, + {[]string{"*.go"}, "/foo/bar", false, true}, {[]string{"*.c"}, "/foo/bar/test.go", false, true}, {[]string{"*.go", "*.c"}, "/foo/bar/test.go", true, true}, {[]string{"*"}, "/foo/bar/test.go", true, true}, @@ -255,8 +256,12 @@ var filterListTests = []struct { {[]string{"?"}, "/foo/bar/test.go", false, true}, {[]string{"?", "x"}, "/foo/bar/x", true, true}, {[]string{"/*/*/bar/test.*"}, "/foo/bar/test.go", false, false}, + {[]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{"/foo/bar/*"}, "/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}, } func TestList(t *testing.T) { From 5f145f0c7eae9ff2cad7bfbf50c648320fd7a3f1 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 17 Sep 2021 23:01:58 +0200 Subject: [PATCH 2/7] filter: introduce pattern struct --- internal/filter/filter.go | 44 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 84aa82784..230155ce0 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -17,7 +17,9 @@ type patternPart struct { } // Pattern represents a preparsed filter pattern -type Pattern []patternPart +type Pattern struct { + parts []patternPart +} func prepareStr(str string) ([]string, error) { if str == "" { @@ -39,7 +41,7 @@ func preparePattern(pattern string) Pattern { patterns[i] = patternPart{part, isSimple} } - return patterns + return Pattern{patterns} } // Split p into path components. Assuming p has been Cleaned, no component @@ -103,7 +105,7 @@ func ChildMatch(pattern, str string) (matched bool, err error) { } func childMatch(patterns Pattern, strs []string) (matched bool, err error) { - if patterns[0].pattern != "/" { + if patterns.parts[0].pattern != "/" { // relative pattern can always be nested down return true, nil } @@ -116,16 +118,16 @@ func childMatch(patterns Pattern, strs []string) (matched bool, err error) { // match path against absolute pattern prefix l := 0 - if len(strs) > len(patterns) { - l = len(patterns) + if len(strs) > len(patterns.parts) { + l = len(patterns.parts) } else { l = len(strs) } - return match(patterns[0:l], strs) + return match(Pattern{patterns.parts[0:l]}, strs) } func hasDoubleWildcard(list Pattern) (ok bool, pos int) { - for i, item := range list { + for i, item := range list.parts { if item.pattern == "" { return true, i } @@ -137,19 +139,19 @@ func hasDoubleWildcard(list Pattern) (ok bool, pos int) { func match(patterns Pattern, strs []string) (matched bool, err error) { if ok, pos := hasDoubleWildcard(patterns); ok { // gradually expand '**' into separate wildcards - newPat := make(Pattern, len(strs)) + newPat := make([]patternPart, len(strs)) // copy static prefix once - copy(newPat, patterns[:pos]) - for i := 0; i <= len(strs)-len(patterns)+1; i++ { + copy(newPat, patterns.parts[:pos]) + for i := 0; i <= len(strs)-len(patterns.parts)+1; i++ { // limit to static prefix and already appended '*' newPat := newPat[:pos+i] // in the first iteration the wildcard expands to nothing if i > 0 { newPat[pos+i-1] = patternPart{"*", false} } - newPat = append(newPat, patterns[pos+1:]...) + newPat = append(newPat, patterns.parts[pos+1:]...) - matched, err := match(newPat, strs) + matched, err := match(Pattern{newPat}, strs) if err != nil { return false, err } @@ -162,20 +164,20 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { return false, nil } - if len(patterns) == 0 && len(strs) == 0 { + if len(patterns.parts) == 0 && len(strs) == 0 { return true, nil } // an empty pattern never matches a non-empty path - if len(patterns) == 0 { + if len(patterns.parts) == 0 { return false, nil } - if len(patterns) <= len(strs) { + if len(patterns.parts) <= len(strs) { minOffset := 0 - maxOffset := len(strs) - len(patterns) + maxOffset := len(strs) - len(patterns.parts) // special case absolute patterns - if patterns[0].pattern == "/" { + if patterns.parts[0].pattern == "/" { maxOffset = 0 } else if strs[0] == "/" { // skip absolute path marker if pattern is not rooted @@ -184,12 +186,12 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { outer: for offset := maxOffset; offset >= minOffset; offset-- { - for i := len(patterns) - 1; i >= 0; i-- { + for i := len(patterns.parts) - 1; i >= 0; i-- { var ok bool - if patterns[i].isSimple { - ok = patterns[i].pattern == strs[offset+i] + if patterns.parts[i].isSimple { + ok = patterns.parts[i].pattern == strs[offset+i] } else { - ok, err = filepath.Match(patterns[i].pattern, strs[offset+i]) + ok, err = filepath.Match(patterns.parts[i].pattern, strs[offset+i]) if err != nil { return false, errors.Wrap(err, "Match") } From 12606b575f3b9c2a50684ebfb3373f7ff02bcaa0 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 17 Sep 2021 23:04:37 +0200 Subject: [PATCH 3/7] filter: Cleanup variable naming --- internal/filter/filter.go | 72 +++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 230155ce0..d48b77ab5 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -28,20 +28,20 @@ func prepareStr(str string) ([]string, error) { return splitPath(str), nil } -func preparePattern(pattern string) Pattern { - parts := splitPath(filepath.Clean(pattern)) - patterns := make([]patternPart, len(parts)) - for i, part := range parts { +func preparePattern(patternStr string) Pattern { + pathParts := splitPath(filepath.Clean(patternStr)) + parts := make([]patternPart, len(pathParts)) + for i, part := range pathParts { isSimple := !strings.ContainsAny(part, "\\[]*?") // Replace "**" with the empty string to get faster comparisons // (length-check only) in hasDoubleWildcard. if part == "**" { part = "" } - patterns[i] = patternPart{part, isSimple} + parts[i] = patternPart{part, isSimple} } - return Pattern{patterns} + return Pattern{parts} } // Split p into path components. Assuming p has been Cleaned, no component @@ -64,19 +64,19 @@ func splitPath(p string) []string { // In addition patterns suitable for filepath.Match, pattern accepts a // recursive wildcard '**', which greedily matches an arbitrary number of // intermediate directories. -func Match(pattern, str string) (matched bool, err error) { - if pattern == "" { +func Match(patternStr, str string) (matched bool, err error) { + if patternStr == "" { return true, nil } - patterns := preparePattern(pattern) + pattern := preparePattern(patternStr) strs, err := prepareStr(str) if err != nil { return false, err } - return match(patterns, strs) + return match(pattern, strs) } // ChildMatch returns true if children of str can match the pattern. When the pattern is @@ -89,28 +89,28 @@ func Match(pattern, str string) (matched bool, err error) { // In addition patterns suitable for filepath.Match, pattern accepts a // recursive wildcard '**', which greedily matches an arbitrary number of // intermediate directories. -func ChildMatch(pattern, str string) (matched bool, err error) { - if pattern == "" { +func ChildMatch(patternStr, str string) (matched bool, err error) { + if patternStr == "" { return true, nil } - patterns := preparePattern(pattern) + pattern := preparePattern(patternStr) strs, err := prepareStr(str) if err != nil { return false, err } - return childMatch(patterns, strs) + return childMatch(pattern, strs) } -func childMatch(patterns Pattern, strs []string) (matched bool, err error) { - if patterns.parts[0].pattern != "/" { +func childMatch(pattern Pattern, strs []string) (matched bool, err error) { + if pattern.parts[0].pattern != "/" { // relative pattern can always be nested down return true, nil } - ok, pos := hasDoubleWildcard(patterns) + ok, pos := hasDoubleWildcard(pattern) if ok && len(strs) >= pos { // cut off at the double wildcard strs = strs[:pos] @@ -118,12 +118,12 @@ func childMatch(patterns Pattern, strs []string) (matched bool, err error) { // match path against absolute pattern prefix l := 0 - if len(strs) > len(patterns.parts) { - l = len(patterns.parts) + if len(strs) > len(pattern.parts) { + l = len(pattern.parts) } else { l = len(strs) } - return match(Pattern{patterns.parts[0:l]}, strs) + return match(Pattern{pattern.parts[0:l]}, strs) } func hasDoubleWildcard(list Pattern) (ok bool, pos int) { @@ -136,20 +136,20 @@ func hasDoubleWildcard(list Pattern) (ok bool, pos int) { return false, 0 } -func match(patterns Pattern, strs []string) (matched bool, err error) { - if ok, pos := hasDoubleWildcard(patterns); ok { +func match(pattern Pattern, strs []string) (matched bool, err error) { + if ok, pos := hasDoubleWildcard(pattern); ok { // gradually expand '**' into separate wildcards newPat := make([]patternPart, len(strs)) // copy static prefix once - copy(newPat, patterns.parts[:pos]) - for i := 0; i <= len(strs)-len(patterns.parts)+1; i++ { + copy(newPat, pattern.parts[:pos]) + for i := 0; i <= len(strs)-len(pattern.parts)+1; i++ { // limit to static prefix and already appended '*' newPat := newPat[:pos+i] // in the first iteration the wildcard expands to nothing if i > 0 { newPat[pos+i-1] = patternPart{"*", false} } - newPat = append(newPat, patterns.parts[pos+1:]...) + newPat = append(newPat, pattern.parts[pos+1:]...) matched, err := match(Pattern{newPat}, strs) if err != nil { @@ -164,20 +164,20 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { return false, nil } - if len(patterns.parts) == 0 && len(strs) == 0 { + if len(pattern.parts) == 0 && len(strs) == 0 { return true, nil } // an empty pattern never matches a non-empty path - if len(patterns.parts) == 0 { + if len(pattern.parts) == 0 { return false, nil } - if len(patterns.parts) <= len(strs) { + if len(pattern.parts) <= len(strs) { minOffset := 0 - maxOffset := len(strs) - len(patterns.parts) + maxOffset := len(strs) - len(pattern.parts) // special case absolute patterns - if patterns.parts[0].pattern == "/" { + if pattern.parts[0].pattern == "/" { maxOffset = 0 } else if strs[0] == "/" { // skip absolute path marker if pattern is not rooted @@ -186,12 +186,12 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { outer: for offset := maxOffset; offset >= minOffset; offset-- { - for i := len(patterns.parts) - 1; i >= 0; i-- { + for i := len(pattern.parts) - 1; i >= 0; i-- { var ok bool - if patterns.parts[i].isSimple { - ok = patterns.parts[i].pattern == strs[offset+i] + if pattern.parts[i].isSimple { + ok = pattern.parts[i].pattern == strs[offset+i] } else { - ok, err = filepath.Match(patterns.parts[i].pattern, strs[offset+i]) + ok, err = filepath.Match(pattern.parts[i].pattern, strs[offset+i]) if err != nil { return false, errors.Wrap(err, "Match") } @@ -210,9 +210,9 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { } // ParsePatterns prepares a list of patterns for use with List. -func ParsePatterns(patterns []string) []Pattern { +func ParsePatterns(pattern []string) []Pattern { patpat := make([]Pattern, 0) - for _, pat := range patterns { + for _, pat := range pattern { if pat == "" { continue } From 2ee07ded2bfc5e6645daea360451204f5cdb9763 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Tue, 2 Jul 2019 21:36:23 +0200 Subject: [PATCH 4/7] 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 --- changelog/unreleased/issue-233 | 31 +++++++++++++++++++++++++++++++ doc/040_backup.rst | 22 ++++++++++++++++++++++ internal/filter/filter.go | 30 ++++++++++++++++++++---------- internal/filter/filter_test.go | 13 +++++++++++++ 4 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 changelog/unreleased/issue-233 diff --git a/changelog/unreleased/issue-233 b/changelog/unreleased/issue-233 new file mode 100644 index 000000000..a1f9aa022 --- /dev/null +++ b/changelog/unreleased/issue-233 @@ -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 diff --git a/doc/040_backup.rst b/doc/040_backup.rst index addcadef5..fa19a176b 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -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`` +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 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 diff --git a/internal/filter/filter.go b/internal/filter/filter.go index d48b77ab5..ca8be7386 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -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 } } diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 149e8cf16..72ed323f8 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -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}, } From cd190bee14528e1f4113fce6c85569ae7ea4b1f8 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 17 Sep 2021 23:13:49 +0200 Subject: [PATCH 5/7] filter: short circuit if no negative patterns --- internal/filter/filter.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index ca8be7386..3e16c9316 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -253,6 +253,12 @@ func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool, if err != nil { return false, false, err } + + hasNegatedPattern := false + for _, pat := range patterns { + hasNegatedPattern = hasNegatedPattern || pat.isNegated + } + for _, pat := range patterns { m, err := match(pat, strs) if err != nil { @@ -275,6 +281,11 @@ func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool, } else { matched = matched || m childMayMatch = childMayMatch || c + + if matched && childMayMatch && !hasNegatedPattern { + // without negative patterns the result cannot change any more + break + } } } From 53656f019a6cc30852271f067c935f1b6532dc4b Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 17 Sep 2021 23:16:37 +0200 Subject: [PATCH 6/7] filter: address review comments --- changelog/unreleased/issue-233 | 2 +- doc/040_backup.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/unreleased/issue-233 b/changelog/unreleased/issue-233 index a1f9aa022..a27af1db2 100644 --- a/changelog/unreleased/issue-233 +++ b/changelog/unreleased/issue-233 @@ -1,6 +1,6 @@ Enhancement: Add negative patterns for include/exclude -If a pattern is suffixed by an exclamation mark and match a file that +If a pattern is prefixed 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. diff --git a/doc/040_backup.rst b/doc/040_backup.rst index fa19a176b..ba297dc7f 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -289,7 +289,7 @@ 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`` -If a pattern is suffixed by an exclamation mark and match a file that +If a pattern is prefixed 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 From 29a5778626bb4bb2ff40b6aae343e8a16f574b84 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 20 Mar 2022 13:46:16 +0100 Subject: [PATCH 7/7] Improve wording --- changelog/unreleased/issue-233 | 2 +- doc/040_backup.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/unreleased/issue-233 b/changelog/unreleased/issue-233 index a27af1db2..00a4e1658 100644 --- a/changelog/unreleased/issue-233 +++ b/changelog/unreleased/issue-233 @@ -1,6 +1,6 @@ Enhancement: Add negative patterns for include/exclude -If a pattern is prefixed by an exclamation mark and match a file that +If a pattern starts with an exclamation mark and it matches 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. diff --git a/doc/040_backup.rst b/doc/040_backup.rst index ba297dc7f..80a14a87a 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -289,7 +289,7 @@ 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`` -If a pattern is prefixed by an exclamation mark and match a file that +If a pattern starts with exclamation mark and matches 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