From 6794b3a9b2b4fdbe39023a948d3341a94bfdc066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lilian=20J=C3=B3nsd=C3=B3ttir?= Date: Tue, 18 Jun 2024 15:56:55 -0700 Subject: [PATCH] add reverse glob and regex filters --- internal/filter/filter.go | 72 +++++++++++++++++++++++++-------- internal/filter/filter_test.go | 74 ++++++++++++++++++++++++++++++++-- main.go | 15 ++++++- 3 files changed, 139 insertions(+), 22 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 379ebb1..74941f3 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -14,8 +14,10 @@ import ( type Filter struct { on, before, after time.Time glob, pattern string + unglob, unpattern string filenames []string matcher *regexp.Regexp + unmatcher *regexp.Regexp } func (f *Filter) On() time.Time { return f.on } @@ -47,6 +49,14 @@ func (f *Filter) Match(filename string, modified time.Time) bool { return false } } + if f.has_unregex() && f.unmatcher.MatchString(filename) { + return false + } + if f.unglob != "" { + if match, err := filepath.Match(f.unglob, filename); err != nil || match { + return false + } + } if len(f.filenames) > 0 && !slices.Contains(f.filenames, filename) { return false } @@ -61,10 +71,19 @@ func (f *Filter) SetPattern(pattern string) error { return err } +func (f *Filter) SetUnPattern(unpattern string) error { + var err error + f.unpattern = unpattern + f.unmatcher, err = regexp.Compile(f.unpattern) + return err +} + func (f *Filter) Blank() bool { t := time.Time{} return !f.has_regex() && + !f.has_unregex() && f.glob == "" && + f.unglob == "" && f.after.Equal(t) && f.before.Equal(t) && f.on.Equal(t) && @@ -72,13 +91,18 @@ func (f *Filter) Blank() bool { } func (f *Filter) String() string { - var m string + var m, unm string if f.matcher != nil { m = f.matcher.String() } - return fmt.Sprintf("on:'%s' before:'%s' after:'%s' glob:'%s' regex:'%s' filenames:'%v'", + if f.unmatcher != nil { + unm = f.unmatcher.String() + } + return fmt.Sprintf("on:'%s' before:'%s' after:'%s' glob:'%s' regex:'%s' unglob:'%s' unregex:'%s' filenames:'%v'", f.on, f.before, f.after, - f.glob, m, f.filenames, + f.glob, m, + f.unglob, unm, + f.filenames, ) } @@ -89,45 +113,59 @@ func (f *Filter) has_regex() bool { return f.matcher.String() != "" } -func New(o, b, a, g, p string, names ...string) (*Filter, error) { - // o b a g p +func (f *Filter) has_unregex() bool { + if f.unmatcher == nil { + return false + } + return f.unmatcher.String() != "" +} + +func New(on, before, after, glob, pattern, unglob, unpattern string, names ...string) (*Filter, error) { var ( err error now = time.Now() ) f := &Filter{ - glob: g, + glob: glob, + unglob: unglob, filenames: append([]string{}, names...), } - if o != "" { - on, err := anytime.Parse(o, now) + if on != "" { + o, err := anytime.Parse(on, now) if err != nil { return &Filter{}, err } - f.on = on + f.on = o } - if a != "" { - after, err := anytime.Parse(a, now) + if after != "" { + a, err := anytime.Parse(after, now) if err != nil { return &Filter{}, err } - f.after = after + f.after = a } - if b != "" { - before, err := anytime.Parse(b, now) + if before != "" { + b, err := anytime.Parse(before, now) if err != nil { return &Filter{}, err } - f.before = before + f.before = b } - err = f.SetPattern(p) + err = f.SetPattern(pattern) + if err != nil { + return nil, err + } + err = f.SetUnPattern(unpattern) + if err != nil { + return nil, err + } - return f, err + return f, nil } func same_day(a, b time.Time) bool { diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 8753a61..83e5bf6 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -22,6 +22,7 @@ var ( type testholder struct { pattern, glob string + unpattern, unglob string before, after, on string filenames []string good, bad []singletest @@ -47,7 +48,7 @@ func testmatch(t *testing.T, testers []testholder) { err error ) for _, tester := range testers { - f, err = New(tester.on, tester.before, tester.after, tester.glob, tester.pattern, tester.filenames...) + f, err = New(tester.on, tester.before, tester.after, tester.glob, tester.pattern, tester.unglob, tester.unpattern, tester.filenames...) if err != nil { t.Fatal(err) } @@ -261,6 +262,46 @@ func TestFilterGlob(t *testing.T) { }) } +func TestFilterUnMatch(t *testing.T) { + testmatch(t, []testholder{ + { + unpattern: "^ss_.*\\.zip", + good: blanktime("hello.zip", "ss_potato.png", "sss.zip"), + bad: blanktime("ss_ost_flac.zip", "ss_guide.zip", "ss_controls.zip"), + }, + { + unpattern: "^h.*o$", + good: blanktime("hi", "test", "hellO", "Hello", "oh hello there"), + bad: blanktime("hello", "hippo", "how about some pasta with alfredo"), + }, + }) +} + +func TestFilterUnGlob(t *testing.T) { + testmatch(t, []testholder{ + { + unglob: "*.txt", + good: blanktime("test.md", "test.go", "test.tar.gz", "testxt", "test.text"), + bad: blanktime("test.txt", "alsotest.txt"), + }, + { + unglob: "*.tar.*", + good: blanktime("test.tar", "test.txt", "test.targz", "test.tgz"), + bad: blanktime("test.tar.gz", "test.tar.xz", "test.tar.zst", "test.tar.bz2"), + }, + { + unglob: "pot*o", + good: blanktime("salad", "test", "alsotest"), + bad: blanktime("potato", "potdonkeyo", "potesto"), + }, + { + unglob: "t?st", + good: blanktime("best", "fast", "most", "past"), + bad: blanktime("test", "tast", "tfst", "tnst"), + }, + }) +} + func TestFilterFilenames(t *testing.T) { testmatch(t, []testholder{ { @@ -282,6 +323,12 @@ func TestFilterFilenames(t *testing.T) { } func TestFilterMultipleParameters(t *testing.T) { + y, m, d := now.Date() + threepm := time.Date(y, m, d, 15, 0, 0, 0, time.Local) + tenpm := time.Date(y, m, d, 22, 0, 0, 0, time.Local) + twoam := time.Date(y, m, d, 2, 0, 0, 0, time.Local) + sevenam := time.Date(y, m, d, 7, 0, 0, 0, time.Local) + testmatch(t, []testholder{ { pattern: "[Tt]est", @@ -318,16 +365,29 @@ func TestFilterMultipleParameters(t *testing.T) { on: "today", after: "two weeks ago", before: "one week ago", - good: blankfilename(now, time.Date(now.Year(), now.Month(), now.Day(), 18, 42, 0, 0, time.Local), time.Date(now.Year(), now.Month(), now.Day(), 8, 17, 33, 0, time.Local)), + good: blankfilename(now, twoam, sevenam, threepm, tenpm), bad: blankfilename(yesterday, oneweekago, onemonthago, oneyearago), }, + { + unpattern: ".*\\.(jpg|png)", + on: "today", + good: []singletest{ + {filename: "test.txt", modified: now}, + {filename: "hello.md", modified: tenpm}, + }, + bad: []singletest{ + {filename: "test.png", modified: now}, + {filename: "test.jpg", modified: twoam}, + {filename: "hello.md", modified: twomonthsago}, + }, + }, }) } func TestFilterBlank(t *testing.T) { var f *Filter t.Run("new", func(t *testing.T) { - f, _ = New("", "", "", "", "") + f, _ = New("", "", "", "", "", "", "") if !f.Blank() { t.Fatalf("filter isn't blank? %s", f) } @@ -351,6 +411,12 @@ func TestFilterNotBlank(t *testing.T) { { glob: "*test*", }, + { + unpattern: ".*\\.(jpg|png)", + }, + { + unglob: "*.jpg", + }, { before: "yesterday", after: "one week ago", @@ -369,7 +435,7 @@ func TestFilterNotBlank(t *testing.T) { for _, tester := range testers { t.Run("notblank"+tester.String(), func(t *testing.T) { - f, _ = New(tester.on, tester.before, tester.after, tester.glob, tester.pattern, tester.filenames...) + f, _ = New(tester.on, tester.before, tester.after, tester.glob, tester.pattern, tester.unglob, tester.unpattern, tester.filenames...) if f.Blank() { t.Fatalf("filter is blank?? %s", f) } diff --git a/main.go b/main.go index e0f8ff7..78e90cd 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ var ( loglvl string f *filter.Filter o, b, a, g, p string + ung, unp string workdir string recursive bool termwidth int @@ -59,7 +60,7 @@ var ( before_commands = func(ctx *cli.Context) (err error) { // setup filter if f == nil { - f, err = filter.New(o, b, a, g, p, ctx.Args().Slice()...) + f, err = filter.New(o, b, a, g, p, ung, unp, ctx.Args().Slice()...) } log.Debugf("filter: %s", f.String()) return @@ -227,6 +228,18 @@ var ( Aliases: []string{"g"}, Destination: &g, }, + &cli.StringFlag{ + Name: "not-match", + Usage: "operate on files not matching regex `PATTERN`", + Aliases: []string{"M"}, + Destination: &unp, + }, + &cli.StringFlag{ + Name: "not-glob", + Usage: "operate on files not matching `GLOB`", + Aliases: []string{"G"}, + Destination: &ung, + }, &cli.StringFlag{ Name: "on", Usage: "operate on files modified on `DATE`",