mirror of
https://github.com/restic/restic.git
synced 2024-08-25 05:37:25 +02:00
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
487 lines
14 KiB
Go
487 lines
14 KiB
Go
package filter_test
|
|
|
|
import (
|
|
"bufio"
|
|
"compress/bzip2"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/restic/restic/internal/filter"
|
|
)
|
|
|
|
var matchTests = []struct {
|
|
pattern string
|
|
path string
|
|
match bool
|
|
}{
|
|
{"", "", true},
|
|
{"", "foo", true},
|
|
{"", "/x/y/z/foo", true},
|
|
{"*.go", "/foo/bar/test.go", true},
|
|
{"*.c", "/foo/bar/test.go", false},
|
|
{"*", "/foo/bar/test.go", true},
|
|
{"**", "/foo/bar/test.go", true},
|
|
{"foo*", "/foo/bar/test.go", true},
|
|
{"bar*", "/foo/bar/test.go", true},
|
|
{"/bar*", "/foo/bar/test.go", false},
|
|
{"bar/*", "/foo/bar/test.go", true},
|
|
{"baz/*", "/foo/bar/test.go", false},
|
|
{"bar/test.go", "/foo/bar/test.go", true},
|
|
{"bar/*.go", "/foo/bar/test.go", true},
|
|
{"ba*/*.go", "/foo/bar/test.go", true},
|
|
{"bb*/*.go", "/foo/bar/test.go", false},
|
|
{"test.*", "/foo/bar/test.go", true},
|
|
{"tesT.*", "/foo/bar/test.go", false},
|
|
{"bar/*", "/foo/bar/baz", true},
|
|
{"bar", "/foo/bar", true},
|
|
{"/foo/bar", "/foo/bar", true},
|
|
{"/foo/bar/", "/foo/bar", true},
|
|
{"/foo/bar", "/foo/baz", false},
|
|
{"/foo/bar", "/foo/baz/", false},
|
|
{"/foo///bar", "/foo/bar", true},
|
|
{"/foo/../bar", "/foo/bar", false},
|
|
{"/foo/../bar", "/bar", true},
|
|
{"/foo", "/foo/baz", true},
|
|
{"/foo/", "/foo/baz", true},
|
|
{"/foo/*", "/foo", false},
|
|
{"/foo/*", "/foo/baz", true},
|
|
{"bar", "/foo/bar/baz", true},
|
|
{"bar", "/foo/bar/test.go", true},
|
|
{"/foo/*test.*", "/foo/bar/test.go", false},
|
|
{"/foo/*/test.*", "/foo/bar/test.go", true},
|
|
{"/foo/*/bar/test.*", "/foo/bar/test.go", false},
|
|
{"/*/*/bar/test.*", "/foo/bar/test.go", false},
|
|
{"/*/*/bar/test.*", "/foo/bar/baz/test.go", false},
|
|
{"/*/*/baz/test.*", "/foo/bar/baz/test.go", true},
|
|
{"/*/foo/bar/test.*", "/foo/bar/baz/test.go", false},
|
|
{"/*/foo/bar/test.*", "/foo/bar/baz/test.go", false},
|
|
{"/foo/bar/test.*", "bar/baz/test.go", false},
|
|
{"/x/y/bar/baz/test.*", "bar/baz/test.go", false},
|
|
{"/x/y/bar/baz/test.c", "bar/baz/test.go", false},
|
|
{"baz/test.*", "bar/baz/test.go", true},
|
|
{"baz/tesT.*", "bar/baz/test.go", false},
|
|
{"test.go", "bar/baz/test.go", true},
|
|
{"*.go", "bar/baz/test.go", true},
|
|
{"*.c", "bar/baz/test.go", false},
|
|
{"sdk", "/foo/bar/sdk", true},
|
|
{"sdk", "/foo/bar/sdk/test/sdk_foo.go", true},
|
|
{
|
|
"sdk/*/cpp/*/*vars*.html",
|
|
"/usr/share/doc/libreoffice/sdk/docs/cpp/ref/a00517.html",
|
|
false,
|
|
},
|
|
{"foo/**/bar/*.go", "/home/user/foo/work/special/project/bar/test.go", true},
|
|
{"foo/**/bar/*.go", "/home/user/foo/bar/test.go", true},
|
|
{"foo/**/bar/*.go", "x/foo/bar/test.go", true},
|
|
{"foo/**/bar/*.go", "foo/bar/test.go", true},
|
|
{"foo/**/bar/*.go", "foo/bar/baz/bar/test.go", true},
|
|
{"foo/**/bar/*.go", "/home/user/foo/test.c", false},
|
|
{"foo/**/bar/*.go", "bar/foo/main.go", false},
|
|
{"foo/**/bar/*.go", "/foo/bar/main.go", true},
|
|
{"foo/**/bar/*.go", "bar/main.go", false},
|
|
{"foo/**/bar", "/home/user/foo/x/y/bar", true},
|
|
{"foo/**/bar", "/home/user/foo/x/y/bar/main.go", true},
|
|
{"foo/**/bar/**/x", "/home/user/foo/bar/x", true},
|
|
{"foo/**/bar/**/x", "/home/user/foo/blaaa/blaz/bar/shared/work/x", true},
|
|
{"user/**/important*", "/home/user/work/x/y/hidden/x", false},
|
|
{"user/**/hidden*/**/c", "/home/user/work/x/y/hidden/z/a/b/c", true},
|
|
{"c:/foo/*test.*", "c:/foo/bar/test.go", false},
|
|
{"c:/foo", "c:/foo/bar", true},
|
|
{"c:/foo/", "c:/foo/bar", true},
|
|
{"c:/foo/*/test.*", "c:/foo/bar/test.go", true},
|
|
{"c:/foo/*/bar/test.*", "c:/foo/bar/test.go", false},
|
|
}
|
|
|
|
func testpattern(t *testing.T, pattern, path string, shouldMatch bool) {
|
|
match, err := filter.Match(pattern, path)
|
|
if err != nil {
|
|
t.Errorf("test pattern %q failed: expected no error for path %q, but error returned: %v",
|
|
pattern, path, err)
|
|
}
|
|
|
|
if match != shouldMatch {
|
|
t.Errorf("test: filter.Match(%q, %q): expected %v, got %v",
|
|
pattern, path, shouldMatch, match)
|
|
}
|
|
}
|
|
|
|
func TestMatch(t *testing.T) {
|
|
for _, test := range matchTests {
|
|
t.Run("", func(t *testing.T) {
|
|
testpattern(t, test.pattern, test.path, test.match)
|
|
})
|
|
|
|
// Test with native path separator
|
|
if filepath.Separator != '/' {
|
|
pattern := strings.Replace(test.pattern, "/", string(filepath.Separator), -1)
|
|
// Test with pattern as native
|
|
t.Run("pattern-native", func(t *testing.T) {
|
|
testpattern(t, pattern, test.path, test.match)
|
|
})
|
|
|
|
path := strings.Replace(test.path, "/", string(filepath.Separator), -1)
|
|
t.Run("path-native", func(t *testing.T) {
|
|
// Test with path as native
|
|
testpattern(t, test.pattern, path, test.match)
|
|
})
|
|
|
|
t.Run("both-native", func(t *testing.T) {
|
|
// Test with both pattern and path as native
|
|
testpattern(t, pattern, path, test.match)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
var childMatchTests = []struct {
|
|
pattern string
|
|
path string
|
|
match bool
|
|
}{
|
|
{"", "", true},
|
|
{"", "/foo", true},
|
|
{"", "/x/y/z/foo", true},
|
|
{"foo/bar", "/foo", true},
|
|
{"baz/bar", "/foo", true},
|
|
{"foo", "/foo/bar", true},
|
|
{"bar", "/foo", true},
|
|
{"baz", "/foo/bar", true},
|
|
{"*", "/foo", true},
|
|
{"*", "/foo/bar", true},
|
|
{"/foo/bar", "/foo", true},
|
|
{"/foo/bar/baz", "/foo", true},
|
|
{"/foo/bar/baz", "/foo/bar", true},
|
|
{"/foo/bar/baz", "/foo/baz", false},
|
|
{"/foo/**/baz", "/foo/bar/baz", true},
|
|
{"/foo/**/baz", "/foo/bar/baz/blah", true},
|
|
{"/foo/**/qux", "/foo/bar/baz/qux", true},
|
|
{"/foo/**/qux", "/foo/bar/baz", true},
|
|
{"/foo/**/qux", "/foo/bar/baz/boo", true},
|
|
{"/foo/**", "/foo/bar/baz", true},
|
|
{"/foo/**", "/foo/bar", true},
|
|
{"foo/**/bar/**/x", "/home/user/foo", true},
|
|
{"foo/**/bar/**/x", "/home/user/foo/bar", true},
|
|
{"foo/**/bar/**/x", "/home/user/foo/blaaa/blaz/bar/shared/work/x", true},
|
|
{"/foo/*/qux", "/foo/bar", true},
|
|
{"/foo/*/qux", "/foo/bar/boo", false},
|
|
{"/foo/*/qux", "/foo/bar/boo/xx", false},
|
|
{"/baz/bar", "/foo", false},
|
|
{"/foo", "/foo/bar", true},
|
|
{"/*", "/foo", true},
|
|
{"/*", "/foo/bar", true},
|
|
{"/foo", "/foo/bar", true},
|
|
{"/**", "/foo", true},
|
|
{"/*/**", "/foo", true},
|
|
{"/*/**", "/foo/bar", true},
|
|
{"/*/bar", "/foo", true},
|
|
{"/bar/*", "/foo", false},
|
|
{"/foo/*/baz", "/foo/bar", true},
|
|
{"/foo/*/baz", "/foo/baz", true},
|
|
{"/foo/*/baz", "/bar/baz", false},
|
|
{"/**/*", "/foo", true},
|
|
{"/**/bar", "/foo/bar", true},
|
|
}
|
|
|
|
func testchildpattern(t *testing.T, pattern, path string, shouldMatch bool) {
|
|
match, err := filter.ChildMatch(pattern, path)
|
|
if err != nil {
|
|
t.Errorf("test child pattern %q failed: expected no error for path %q, but error returned: %v",
|
|
pattern, path, err)
|
|
}
|
|
|
|
if match != shouldMatch {
|
|
t.Errorf("test: filter.ChildMatch(%q, %q): expected %v, got %v",
|
|
pattern, path, shouldMatch, match)
|
|
}
|
|
}
|
|
|
|
func TestChildMatch(t *testing.T) {
|
|
for _, test := range childMatchTests {
|
|
t.Run("", func(t *testing.T) {
|
|
testchildpattern(t, test.pattern, test.path, test.match)
|
|
})
|
|
|
|
// Test with native path separator
|
|
if filepath.Separator != '/' {
|
|
pattern := strings.Replace(test.pattern, "/", string(filepath.Separator), -1)
|
|
// Test with pattern as native
|
|
t.Run("pattern-native", func(t *testing.T) {
|
|
testchildpattern(t, pattern, test.path, test.match)
|
|
})
|
|
|
|
path := strings.Replace(test.path, "/", string(filepath.Separator), -1)
|
|
t.Run("path-native", func(t *testing.T) {
|
|
// Test with path as native
|
|
testchildpattern(t, test.pattern, path, test.match)
|
|
})
|
|
|
|
t.Run("both-native", func(t *testing.T) {
|
|
// Test with both pattern and path as native
|
|
testchildpattern(t, pattern, path, test.match)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleMatch() {
|
|
match, _ := filter.Match("*.go", "/home/user/file.go")
|
|
fmt.Printf("match: %v\n", match)
|
|
// Output:
|
|
// match: true
|
|
}
|
|
|
|
func ExampleMatch_wildcards() {
|
|
match, _ := filter.Match("/home/[uU]ser/?.go", "/home/user/F.go")
|
|
fmt.Printf("match: %v\n", match)
|
|
// Output:
|
|
// match: true
|
|
}
|
|
|
|
var filterListTests = []struct {
|
|
patterns []string
|
|
path string
|
|
match bool
|
|
childMatch bool
|
|
}{
|
|
{[]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},
|
|
{[]string{"x"}, "/foo/bar/test.go", false, true},
|
|
{[]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{"!**", "*.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},
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
for i, test := range filterListTests {
|
|
patterns := filter.ParsePatterns(test.patterns)
|
|
match, err := filter.List(patterns, test.path)
|
|
if err != nil {
|
|
t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v",
|
|
i, test.patterns, err)
|
|
continue
|
|
}
|
|
|
|
if match != test.match {
|
|
t.Errorf("test %d: filter.List(%q, %q): expected %v, got %v",
|
|
i, test.patterns, test.path, test.match, match)
|
|
}
|
|
|
|
match, childMatch, err := filter.ListWithChild(patterns, test.path)
|
|
if err != nil {
|
|
t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v",
|
|
i, test.patterns, err)
|
|
continue
|
|
}
|
|
|
|
if match != test.match || childMatch != test.childMatch {
|
|
t.Errorf("test %d: filter.ListWithChild(%q, %q): expected %v, %v, got %v, %v",
|
|
i, test.patterns, test.path, test.match, test.childMatch, match, childMatch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ExampleList() {
|
|
patterns := filter.ParsePatterns([]string{"*.c", "*.go"})
|
|
match, _ := filter.List(patterns, "/home/user/file.go")
|
|
fmt.Printf("match: %v\n", match)
|
|
// Output:
|
|
// match: true
|
|
}
|
|
|
|
func TestInvalidStrs(t *testing.T) {
|
|
_, err := filter.Match("test", "")
|
|
if err == nil {
|
|
t.Error("Match accepted invalid path")
|
|
}
|
|
|
|
_, err = filter.ChildMatch("test", "")
|
|
if err == nil {
|
|
t.Error("ChildMatch accepted invalid path")
|
|
}
|
|
|
|
patterns := []string{"test"}
|
|
_, err = filter.List(filter.ParsePatterns(patterns), "")
|
|
if err == nil {
|
|
t.Error("List accepted invalid path")
|
|
}
|
|
}
|
|
|
|
func TestInvalidPattern(t *testing.T) {
|
|
patterns := []string{"test/["}
|
|
_, err := filter.List(filter.ParsePatterns(patterns), "test/example")
|
|
if err == nil {
|
|
t.Error("List accepted invalid pattern")
|
|
}
|
|
|
|
patterns = []string{"test/**/["}
|
|
_, err = filter.List(filter.ParsePatterns(patterns), "test/example")
|
|
if err == nil {
|
|
t.Error("List accepted invalid pattern")
|
|
}
|
|
}
|
|
|
|
func extractTestLines(t testing.TB) (lines []string) {
|
|
f, err := os.Open("testdata/libreoffice.txt.bz2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() {
|
|
if err := f.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
sc := bufio.NewScanner(bzip2.NewReader(f))
|
|
for sc.Scan() {
|
|
lines = append(lines, sc.Text())
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
func TestFilterPatternsFile(t *testing.T) {
|
|
lines := extractTestLines(t)
|
|
|
|
var testPatterns = []struct {
|
|
pattern string
|
|
hits uint
|
|
}{
|
|
{"*.html", 18249},
|
|
{"sdk", 22186},
|
|
{"sdk/*/cpp/*/*vars.html", 3},
|
|
}
|
|
|
|
for _, test := range testPatterns {
|
|
var c uint
|
|
for _, line := range lines {
|
|
match, err := filter.Match(test.pattern, line)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
|
|
if match {
|
|
c++
|
|
// fmt.Printf("pattern %q, line %q\n", test.pattern, line)
|
|
}
|
|
}
|
|
|
|
if c != test.hits {
|
|
t.Errorf("wrong number of hits for pattern %q: want %d, got %d",
|
|
test.pattern, test.hits, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkFilterLines(b *testing.B) {
|
|
pattern := "sdk/*/cpp/*/*vars.html"
|
|
lines := extractTestLines(b)
|
|
var c uint
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
c = 0
|
|
for _, line := range lines {
|
|
match, err := filter.Match(pattern, line)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
if match {
|
|
c++
|
|
}
|
|
}
|
|
|
|
if c != 3 {
|
|
b.Fatalf("wrong number of matches: expected 3, got %d", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkFilterPatterns(b *testing.B) {
|
|
lines := extractTestLines(b)
|
|
modlines := make([]string, 200)
|
|
for i, line := range lines {
|
|
if i >= len(modlines) {
|
|
break
|
|
}
|
|
modlines[i] = line + "-does-not-match"
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
patterns []filter.Pattern
|
|
matches uint
|
|
}{
|
|
{"Relative", filter.ParsePatterns([]string{
|
|
"does-not-match",
|
|
"sdk/*",
|
|
"*.html",
|
|
}), 22185},
|
|
{"Absolute", filter.ParsePatterns([]string{
|
|
"/etc",
|
|
"/home/*/test",
|
|
"/usr/share/doc/libreoffice/sdk/docs/java",
|
|
}), 150},
|
|
{"Wildcard", filter.ParsePatterns([]string{
|
|
"/etc/**/example",
|
|
"/home/**/test",
|
|
"/usr/**/java",
|
|
}), 150},
|
|
{"ManyNoMatch", filter.ParsePatterns(modlines), 0},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
b.Run(test.name, func(b *testing.B) {
|
|
var c uint
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
c = 0
|
|
for _, line := range lines {
|
|
match, err := filter.List(test.patterns, line)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
if match {
|
|
c++
|
|
}
|
|
}
|
|
|
|
if c != test.matches {
|
|
b.Fatalf("wrong number of matches: expected %d, got %d", test.matches, c)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|