diff --git a/internal/files/files.go b/internal/files/files.go index 46bb055..853379f 100644 --- a/internal/files/files.go +++ b/internal/files/files.go @@ -61,7 +61,7 @@ func walk_dir(dir string, f *filter.Filter) (files Files) { } name := d.Name() info, _ := d.Info() - if f.Match(name, info.ModTime()) { + if f.Match(name, info.ModTime(), info.IsDir()) { log.Debugf("found matching file: %s %s", name, info.ModTime()) i, _ := os.Stat(p) files = append(files, File{ @@ -101,7 +101,7 @@ func read_dir(dir string, f *filter.Filter) (files Files) { path := filepath.Dir(filepath.Join(dir, name)) - if f.Match(name, info.ModTime()) { + if f.Match(name, info.ModTime(), info.IsDir()) { log.Debugf("found matching file: %s %s", name, info.ModTime()) files = append(files, File{ name: name, diff --git a/internal/filter/filter.go b/internal/filter/filter.go index fc0b3b2..1924fcc 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -6,18 +6,21 @@ import ( "path/filepath" "regexp" "slices" + "strings" "time" "github.com/ijt/go-anytime" ) type Filter struct { - on, before, after time.Time - glob, pattern string - unglob, unpattern string - filenames []string - matcher *regexp.Regexp - unmatcher *regexp.Regexp + on, before, after time.Time + glob, pattern string + unglob, unpattern string + filenames []string + dirsonly, filesonly bool + ignorehidden bool + matcher *regexp.Regexp + unmatcher *regexp.Regexp } func (f *Filter) On() time.Time { return f.on } @@ -26,6 +29,9 @@ func (f *Filter) Before() time.Time { return f.before } func (f *Filter) Glob() string { return f.glob } func (f *Filter) Pattern() string { return f.pattern } func (f *Filter) FileNames() []string { return f.filenames } +func (f *Filter) FilesOnly() bool { return f.filesonly } +func (f *Filter) DirsOnly() bool { return f.dirsonly } +func (f *Filter) IgnoreHidden() bool { return f.ignorehidden } func (f *Filter) AddFileName(filename string) { filename = filepath.Clean(filename) @@ -38,7 +44,7 @@ func (f *Filter) AddFileNames(filenames ...string) { } } -func (f *Filter) Match(filename string, modified time.Time) bool { +func (f *Filter) Match(filename string, modified time.Time, isdir bool) bool { // this might be unnessary but w/e filename = filepath.Clean(filename) // on or before/after, not both @@ -73,6 +79,15 @@ func (f *Filter) Match(filename string, modified time.Time) bool { if len(f.filenames) > 0 && !slices.Contains(f.filenames, filename) { return false } + if f.filesonly && isdir { + return false + } + if f.dirsonly && !isdir { + return false + } + if f.ignorehidden && strings.HasPrefix(filename, ".") { + return false + } // okay it was good return true } @@ -100,7 +115,10 @@ func (f *Filter) Blank() bool { f.after.Equal(t) && f.before.Equal(t) && f.on.Equal(t) && - len(f.filenames) == 0 + len(f.filenames) == 0 && + !f.ignorehidden && + !f.filesonly && + !f.dirsonly } func (f *Filter) String() string { @@ -111,11 +129,14 @@ func (f *Filter) String() string { 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'", + return fmt.Sprintf("on:'%s' before:'%s' after:'%s' glob:'%s' regex:'%s' unglob:'%s' "+ + "unregex:'%s' filenames:'%v' filesonly:'%t' dirsonly:'%t' ignorehidden:'%t'", f.on, f.before, f.after, f.glob, m, f.unglob, unm, f.filenames, + f.filesonly, f.dirsonly, + f.ignorehidden, ) } @@ -133,15 +154,18 @@ func (f *Filter) has_unregex() bool { return f.unmatcher.String() != "" } -func New(on, before, after, glob, pattern, unglob, unpattern string, names ...string) (*Filter, error) { +func New(on, before, after, glob, pattern, unglob, unpattern string, filesonly, dirsonly, ignorehidden bool, names ...string) (*Filter, error) { var ( err error now = time.Now() ) f := &Filter{ - glob: glob, - unglob: unglob, + glob: glob, + unglob: unglob, + filesonly: filesonly, + dirsonly: dirsonly, + ignorehidden: ignorehidden, } f.AddFileNames(names...) diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 83e5bf6..f6f5a3f 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -21,24 +21,32 @@ var ( ) type testholder struct { - pattern, glob string - unpattern, unglob string - before, after, on string - filenames []string - good, bad []singletest + pattern, glob string + unpattern, unglob string + before, after, on string + filenames []string + filesonly, dirsonly bool + ignorehidden bool + good, bad []singletest } func (t testholder) String() string { - return fmt.Sprintf("pattern:'%s' glob:'%s' filenames:'%v' before:'%s' after:'%s' on:'%s'", t.pattern, t.glob, t.filenames, t.before, t.after, t.on) + return fmt.Sprintf( + "pattern:'%s' glob:'%s' unpattern:'%s' unglob:'%s' filenames:'%v' "+ + "before:'%s' after:'%s' on:'%s' filesonly:'%t' dirsonly:'%t' ignorehidden:'%t'", + t.pattern, t.glob, t.unpattern, t.unglob, t.filenames, t.before, t.after, t.on, + t.filesonly, t.dirsonly, t.ignorehidden, + ) } type singletest struct { filename string + isdir bool modified time.Time } func (s singletest) String() string { - return fmt.Sprintf("filename:'%s' modified:'%s'", s.filename, s.modified) + return fmt.Sprintf("filename:'%s' modified:'%s' isdir:'%t'", s.filename, s.modified, s.isdir) } func testmatch(t *testing.T, testers []testholder) { @@ -48,14 +56,18 @@ 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.unglob, tester.unpattern, tester.filenames...) + f, err = New( + tester.on, tester.before, tester.after, tester.glob, tester.pattern, + tester.unglob, tester.unpattern, tester.filesonly, tester.dirsonly, tester.ignorehidden, + tester.filenames..., + ) if err != nil { t.Fatal(err) } for _, tst := range tester.good { t.Run(fmt.Sprintf(testnamefmt+"_good", tst.filename, tst.modified), func(t *testing.T) { - if !f.Match(tst.filename, tst.modified) { + if !f.Match(tst.filename, tst.modified, tst.isdir) { t.Fatalf("(filename:%s modified:%s) didn't match (%s) but should have", tst.filename, tst.modified, tester) } }) @@ -63,7 +75,7 @@ func testmatch(t *testing.T, testers []testholder) { for _, tst := range tester.bad { t.Run(fmt.Sprintf(testnamefmt+"_bad", tst.filename, tst.modified), func(t *testing.T) { - if f.Match(tst.filename, tst.modified) { + if f.Match(tst.filename, tst.modified, tst.isdir) { t.Fatalf("(filename:%s modified:%s) matched (%s) but shouldn't have", tst.filename, tst.modified, tester) } }) @@ -74,15 +86,31 @@ func testmatch(t *testing.T, testers []testholder) { func blankfilename(times ...time.Time) []singletest { out := make([]singletest, 0, len(times)) for _, time := range times { - out = append(out, singletest{filename: "blank.txt", modified: time}) + out = append(out, singletest{filename: "blank.txt", modified: time, isdir: false}) } return out } -func blanktime(filenames ...string) []singletest { +func blankdirname(times ...time.Time) []singletest { + out := make([]singletest, 0, len(times)) + for _, time := range times { + out = append(out, singletest{filename: "blank", modified: time, isdir: true}) + } + return out +} + +func blanktimefile(filenames ...string) []singletest { out := make([]singletest, 0, len(filenames)) for _, filename := range filenames { - out = append(out, singletest{filename: filename, modified: time.Time{}}) + out = append(out, singletest{filename: filename, modified: time.Time{}, isdir: false}) + } + return out +} + +func blanktimedir(dirnames ...string) []singletest { + out := make([]singletest, 0, len(dirnames)) + for _, dirname := range dirnames { + out = append(out, singletest{filename: dirname, modified: time.Time{}, isdir: true}) } return out } @@ -226,13 +254,13 @@ func TestFilterMatch(t *testing.T) { testmatch(t, []testholder{ { pattern: "[Tt]est", - good: blanktime("test", "Test"), - bad: blanktime("TEST", "tEst", "tEST", "TEst"), + good: blanktimefile("test", "Test"), + bad: blanktimefile("TEST", "tEst", "tEST", "TEst"), }, { pattern: "^h.*o$", - good: blanktime("hello", "hippo", "how about some pasta with alfredo"), - bad: blanktime("hi", "test", "hellO", "Hello", "oh hello there"), + good: blanktimefile("hello", "hippo", "how about some pasta with alfredo"), + bad: blanktimefile("hi", "test", "hellO", "Hello", "oh hello there"), }, }) } @@ -241,23 +269,23 @@ func TestFilterGlob(t *testing.T) { testmatch(t, []testholder{ { glob: "*.txt", - good: blanktime("test.txt", "alsotest.txt"), - bad: blanktime("test.md", "test.go", "test.tar.gz", "testxt", "test.text"), + good: blanktimefile("test.txt", "alsotest.txt"), + bad: blanktimefile("test.md", "test.go", "test.tar.gz", "testxt", "test.text"), }, { glob: "*.tar.*", - good: blanktime("test.tar.gz", "test.tar.xz", "test.tar.zst", "test.tar.bz2"), - bad: blanktime("test.tar", "test.txt", "test.targz", "test.tgz"), + good: blanktimefile("test.tar.gz", "test.tar.xz", "test.tar.zst", "test.tar.bz2"), + bad: blanktimefile("test.tar", "test.txt", "test.targz", "test.tgz"), }, { glob: "pot*o", - good: blanktime("potato", "potdonkeyo", "potesto"), - bad: blanktime("salad", "test", "alsotest"), + good: blanktimefile("potato", "potdonkeyo", "potesto"), + bad: blanktimefile("salad", "test", "alsotest"), }, { glob: "t?st", - good: blanktime("test", "tast", "tfst", "tnst"), - bad: blanktime("best", "fast", "most", "past"), + good: blanktimefile("test", "tast", "tfst", "tnst"), + bad: blanktimefile("best", "fast", "most", "past"), }, }) } @@ -266,13 +294,13 @@ 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"), + good: blanktimefile("hello.zip", "ss_potato.png", "sss.zip"), + bad: blanktimefile("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"), + good: blanktimefile("hi", "test", "hellO", "Hello", "oh hello there"), + bad: blanktimefile("hello", "hippo", "how about some pasta with alfredo"), }, }) } @@ -281,23 +309,23 @@ 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"), + good: blanktimefile("test.md", "test.go", "test.tar.gz", "testxt", "test.text"), + bad: blanktimefile("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"), + good: blanktimefile("test.tar", "test.txt", "test.targz", "test.tgz"), + bad: blanktimefile("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"), + good: blanktimefile("salad", "test", "alsotest"), + bad: blanktimefile("potato", "potdonkeyo", "potesto"), }, { unglob: "t?st", - good: blanktime("best", "fast", "most", "past"), - bad: blanktime("test", "tast", "tfst", "tnst"), + good: blanktimefile("best", "fast", "most", "past"), + bad: blanktimefile("test", "tast", "tfst", "tnst"), }, }) } @@ -306,18 +334,53 @@ func TestFilterFilenames(t *testing.T) { testmatch(t, []testholder{ { filenames: []string{"test.txt", "alsotest.txt"}, - good: blanktime("test.txt", "alsotest.txt"), - bad: blanktime("test.md", "test.go", "test.tar.gz", "testxt", "test.text"), + good: blanktimefile("test.txt", "alsotest.txt"), + bad: blanktimefile("test.md", "test.go", "test.tar.gz", "testxt", "test.text"), }, { filenames: []string{"test.md", "test.txt"}, - good: blanktime("test.txt", "test.md"), - bad: blanktime("alsotest.txt", "test.go", "test.tar.gz", "testxt", "test.text"), + good: blanktimefile("test.txt", "test.md"), + bad: blanktimefile("alsotest.txt", "test.go", "test.tar.gz", "testxt", "test.text"), }, { filenames: []string{"hello.world"}, - good: blanktime("hello.world"), - bad: blanktime("test.md", "test.go", "test.tar.gz", "testxt", "test.text", "helloworld", "Hello.world"), + good: blanktimefile("hello.world"), + bad: blanktimefile("test.md", "test.go", "test.tar.gz", "testxt", "test.text", "helloworld", "Hello.world"), + }, + }) +} + +func TestFilterFilesOnly(t *testing.T) { + testmatch(t, []testholder{ + { + filesonly: true, + good: blanktimefile("test", "hellowold.txt", "test.md", "test.jpg"), + bad: blanktimedir("test", "alsotest", "helloworld"), + }, + }) +} + +func TestFilterDirsOnly(t *testing.T) { + testmatch(t, []testholder{ + { + dirsonly: true, + good: blanktimedir("test", "alsotest", "helloworld"), + bad: blanktimefile("test", "hellowold.txt", "test.md", "test.jpg"), + }, + { + dirsonly: true, + good: blankdirname(fourmonthsago, twomonthsago, onemonthago, oneweekago, yesterday, now), + bad: blankfilename(fourmonthsago, twomonthsago, onemonthago, oneweekago, yesterday, now), + }, + }) +} + +func TestFilterIgnoreHidden(t *testing.T) { + testmatch(t, []testholder{ + { + ignorehidden: true, + good: append(blanktimedir("test", "alsotest", "helloworld"), blanktimefile("test", "alsotest", "helloworld")...), + bad: append(blanktimedir(".test", ".alsotest", ".helloworld"), blanktimefile(".test", ".alsotest", ".helloworld")...), }, }) } @@ -381,13 +444,47 @@ func TestFilterMultipleParameters(t *testing.T) { {filename: "hello.md", modified: twomonthsago}, }, }, + { + filesonly: true, + unglob: "*.txt", + good: blanktimefile("test.md", "test.jpg", "test.png"), + bad: []singletest{ + { + filename: "test", + isdir: true, + }, + { + filename: "test.txt", + isdir: false, + }, + { + filename: "test.md", + isdir: true, + }, + }, + }, + { + dirsonly: true, + pattern: "w(or|ea)ld", + good: blanktimedir("hello world", "high weald"), + bad: []singletest{ + { + filename: "hello_world.txt", + isdir: false, + }, + { + filename: "highweald.txt", + isdir: false, + }, + }, + }, }) } func TestFilterBlank(t *testing.T) { var f *Filter t.Run("new", func(t *testing.T) { - f, _ = New("", "", "", "", "", "", "") + f, _ = New("", "", "", "", "", "", "", false, false, false) if !f.Blank() { t.Fatalf("filter isn't blank? %s", f) } @@ -430,12 +527,25 @@ func TestFilterNotBlank(t *testing.T) { { filenames: []string{""}, }, + { + filesonly: true, + }, + { + dirsonly: true, + }, + { + ignorehidden: true, + }, } ) 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.unglob, tester.unpattern, tester.filenames...) + f, _ = New( + tester.on, tester.before, tester.after, tester.glob, tester.pattern, + tester.unglob, tester.unpattern, tester.filesonly, tester.dirsonly, tester.ignorehidden, + tester.filenames..., + ) if f.Blank() { t.Fatalf("filter is blank?? %s", f) } diff --git a/internal/trash/trash.go b/internal/trash/trash.go index ffbe500..e6efd01 100644 --- a/internal/trash/trash.go +++ b/internal/trash/trash.go @@ -79,7 +79,7 @@ func FindFiles(trashdir, ogdir string, f *filter.Filter) (files Infos, outerr er return nil } - if f.Match(filename, date) { + if f.Match(filename, date, info.IsDir()) { log.Debugf("%s: deleted on %s", filename, date.Format(trash_info_date_fmt)) files = append(files, Info{ name: filename, diff --git a/main.go b/main.go index d32e9d5..2053f1a 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ var ( f *filter.Filter o, b, a, g, p string ung, unp string + fo, do, ih bool workdir, ogdir cli.Path recursive bool termwidth int @@ -66,7 +67,7 @@ var ( before_commands = func(ctx *cli.Context) (err error) { // setup filter if f == nil { - f, err = filter.New(o, b, a, g, p, ung, unp, ctx.Args().Slice()...) + f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, ih, ctx.Args().Slice()...) } log.Debugf("filter: %s", f.String()) return @@ -117,7 +118,7 @@ var ( ) if f == nil { - f, err = filter.New(o, b, a, g, p, ung, unp) + f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, ih) } if err != nil { return err @@ -286,6 +287,24 @@ var ( Aliases: []string{"b"}, Destination: &b, }, + &cli.BoolFlag{ + Name: "files-only", + Usage: "operate on files only", + Aliases: []string{"f"}, + Destination: &fo, + }, + &cli.BoolFlag{ + Name: "dirs-only", + Usage: "operate on directories only", + Aliases: []string{"d"}, + Destination: &do, + }, + &cli.BoolFlag{ + Name: "ignore-hidden", + Usage: "operate on unhidden files only", + Aliases: []string{"i"}, + Destination: &ih, + }, } trash_flags = []cli.Flag{