mirror of
https://github.com/restic/restic.git
synced 2026-05-28 04:35:41 -04:00
Merge aadf0d0605 into f000da3b35
This commit is contained in:
commit
581abb1336
3 changed files with 309 additions and 1 deletions
208
cmd/restic/cmd_test_pattern_test.go
Normal file
208
cmd/restic/cmd_test_pattern_test.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
func newTestTerm() (*termstatus.Terminal, func()) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
term := termstatus.New(io.NopCloser(bytes.NewReader(nil)), io.Discard, io.Discard, true)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
term.Run(ctx)
|
||||
}()
|
||||
|
||||
return term, func() {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestPatternMatch(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
pattern string
|
||||
path string
|
||||
caseInsensitive bool
|
||||
wantMatch bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple wildcard match",
|
||||
pattern: "*.go",
|
||||
path: "/home/user/main.go",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "simple wildcard no match",
|
||||
pattern: "*.txt",
|
||||
path: "/home/user/main.go",
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "exact filename match",
|
||||
pattern: "main.go",
|
||||
path: "main.go",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "case insensitive match",
|
||||
pattern: "*.GO",
|
||||
path: "/home/user/main.go",
|
||||
caseInsensitive: true,
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "case insensitive no match",
|
||||
pattern: "*.txt",
|
||||
path: "/home/user/main.go",
|
||||
caseInsensitive: true,
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "recursive wildcard match",
|
||||
pattern: "**/.git/**",
|
||||
path: "/home/user/project/.git/config",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "recursive wildcard child match",
|
||||
pattern: "**/.git/**",
|
||||
path: "/home/user/project/.git",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "absolute pattern match",
|
||||
pattern: "/home/user/*.txt",
|
||||
path: "/home/user/readme.txt",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "absolute pattern no match different dir",
|
||||
pattern: "/home/user/*.txt",
|
||||
path: "/home/other/readme.txt",
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "single character wildcard",
|
||||
pattern: "file.?o",
|
||||
path: "/path/file.go",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "question mark no match",
|
||||
pattern: "file.?o",
|
||||
path: "/path/file.py",
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "character class match",
|
||||
pattern: "file.[gx]o",
|
||||
path: "/path/file.go",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "character class no match",
|
||||
pattern: "file.[ab]o",
|
||||
path: "/path/file.go",
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "directory wildcard match",
|
||||
pattern: "/home/*/main.go",
|
||||
path: "/home/user/main.go",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "directory wildcard no match subdir",
|
||||
pattern: "/home/*/main.go",
|
||||
path: "/home/user/sub/main.go",
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "directory wildcard multi-level",
|
||||
pattern: "/home/*/*/main.go",
|
||||
path: "/home/user/sub/main.go",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
pattern: "[",
|
||||
path: "/home/user/main.go",
|
||||
wantErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
term, cleanup := newTestTerm()
|
||||
defer cleanup()
|
||||
|
||||
gopts := global.Options{
|
||||
Term: term,
|
||||
}
|
||||
|
||||
opts := TestPatternOptions{
|
||||
CaseInsensitive: test.caseInsensitive,
|
||||
}
|
||||
|
||||
args := []string{test.pattern, test.path}
|
||||
err := runTestPattern(opts, gopts, args)
|
||||
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if test.wantMatch {
|
||||
if err != nil {
|
||||
t.Fatalf("expected match but got error: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatal("expected no match but got error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "pattern did not match") {
|
||||
t.Fatalf("expected 'pattern did not match' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestPatternWrongArgCount(t *testing.T) {
|
||||
term, cleanup := newTestTerm()
|
||||
defer cleanup()
|
||||
|
||||
gopts := global.Options{
|
||||
Term: term,
|
||||
}
|
||||
|
||||
opts := TestPatternOptions{}
|
||||
|
||||
err := runTestPattern(opts, gopts, []string{})
|
||||
if err == nil || !strings.Contains(err.Error(), "wrong number of arguments") {
|
||||
t.Fatalf("expected 'wrong number of arguments' error, got: %v", err)
|
||||
}
|
||||
|
||||
err = runTestPattern(opts, gopts, []string{"*.go"})
|
||||
if err == nil || !strings.Contains(err.Error(), "wrong number of arguments") {
|
||||
t.Fatalf("expected 'wrong number of arguments' error, got: %v", err)
|
||||
}
|
||||
|
||||
err = runTestPattern(opts, gopts, []string{"*.go", "/path", "extra"})
|
||||
if err == nil || !strings.Contains(err.Error(), "wrong number of arguments") {
|
||||
t.Fatalf("expected 'wrong number of arguments' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,6 +101,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
|||
newSnapshotsCommand(globalOptions),
|
||||
newStatsCommand(globalOptions),
|
||||
newTagCommand(globalOptions),
|
||||
newTestPatternCommand(globalOptions),
|
||||
newUnlockCommand(globalOptions),
|
||||
newVersionCommand(globalOptions),
|
||||
)
|
||||
|
|
@ -118,7 +119,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
|||
// user for authentication).
|
||||
func needsPassword(cmd string) bool {
|
||||
switch cmd {
|
||||
case "cache", "generate", "help", "options", "self-update", "version", "__complete", "__completeNoDesc":
|
||||
case "cache", "generate", "help", "options", "self-update", "test-pattern", "version", "__complete", "__completeNoDesc":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
|
|
|||
99
cmd_test_pattern.go
Normal file
99
cmd_test_pattern.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newTestPatternCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts TestPatternOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "test-pattern [flags] PATTERN PATH",
|
||||
Short: "Test a pattern against a path",
|
||||
Long: `
|
||||
The "test-pattern" command tests whether a given pattern matches a specific
|
||||
path. It uses the same pattern matching logic as the "find", "backup", and
|
||||
"restore" commands, supporting the '**' recursive wildcard in addition to
|
||||
filepath.Match patterns.
|
||||
|
||||
This is useful for verifying exclude/include patterns before running a backup.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the pattern matched the path.
|
||||
Exit status is 1 if the pattern did not match.
|
||||
`,
|
||||
Example: `restic test-pattern '*.go' /home/user/main.go
|
||||
restic test-pattern --ignore-case '*.GO' /home/user/main.go
|
||||
restic test-pattern '**/.git/**' /home/user/project/.git/config
|
||||
restic test-pattern '/home/user/*.txt' /home/user/readme.txt`,
|
||||
DisableAutoGenTag: true,
|
||||
GroupID: cmdGroupDefault,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return runTestPattern(opts, *globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
opts.AddFlags(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TestPatternOptions collects all options for the test-pattern command.
|
||||
type TestPatternOptions struct {
|
||||
CaseInsensitive bool
|
||||
}
|
||||
|
||||
func (opts *TestPatternOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.BoolVarP(&opts.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
|
||||
}
|
||||
|
||||
func runTestPattern(opts TestPatternOptions, gopts global.Options, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.Fatal("wrong number of arguments, expecting: test-pattern [flags] PATTERN PATH")
|
||||
}
|
||||
|
||||
pattern := args[0]
|
||||
path := args[1]
|
||||
|
||||
normalizedPattern := pattern
|
||||
normalizedPath := path
|
||||
if opts.CaseInsensitive {
|
||||
normalizedPattern = strings.ToLower(pattern)
|
||||
normalizedPath = strings.ToLower(path)
|
||||
}
|
||||
|
||||
matched, err := filter.Match(normalizedPattern, normalizedPath)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error matching pattern: %v", err)
|
||||
}
|
||||
|
||||
childMayMatch, err := filter.ChildMatch(normalizedPattern, normalizedPath)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error testing child match: %v", err)
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
|
||||
if opts.CaseInsensitive {
|
||||
printer.S("pattern : %s (case-insensitive)\n", pattern)
|
||||
} else {
|
||||
printer.S("pattern : %s\n", pattern)
|
||||
}
|
||||
printer.S("path : %s\n", path)
|
||||
printer.S("match : %v\n", matched)
|
||||
printer.S("child match: %v\n", childMayMatch)
|
||||
|
||||
if !matched {
|
||||
return errors.Fatal("pattern did not match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in a new issue