From 940a2fc14f7117b819e05df4f30a4cd8cfa7894b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lilian=20J=C3=B3nsd=C3=B3ttir?= Date: Mon, 15 Jul 2024 21:05:21 -0700 Subject: [PATCH] add flag to filter by file mode --- internal/files/disk.go | 12 ++++--- internal/files/files.go | 2 ++ internal/files/trash.go | 2 ++ internal/filter/filter.go | 25 +++++++++----- internal/filter/filter_test.go | 62 ++++++++++++++++++++++++++++++---- main.go | 61 +++++++++++++++++++++++++-------- 6 files changed, 132 insertions(+), 32 deletions(-) diff --git a/internal/files/disk.go b/internal/files/disk.go index 134fe7e..3ad3e32 100644 --- a/internal/files/disk.go +++ b/internal/files/disk.go @@ -18,12 +18,14 @@ type DiskFile struct { filesize int64 modified time.Time isdir bool + mode fs.FileMode } -func (f DiskFile) Name() string { return f.name } -func (f DiskFile) Path() string { return f.path } -func (f DiskFile) Date() time.Time { return f.modified } -func (f DiskFile) IsDir() bool { return f.isdir } +func (f DiskFile) Name() string { return f.name } +func (f DiskFile) Path() string { return f.path } +func (f DiskFile) Date() time.Time { return f.modified } +func (f DiskFile) IsDir() bool { return f.isdir } +func (f DiskFile) Mode() fs.FileMode { return f.mode } func (f DiskFile) Filesize() int64 { if f.isdir { return -1 @@ -61,6 +63,7 @@ func NewDisk(path string) (DiskFile, error) { filesize: info.Size(), modified: info.ModTime(), isdir: info.IsDir(), + mode: info.Mode(), }, nil } @@ -170,6 +173,7 @@ func read_dir(dir string, f *filter.Filter) (files Files) { modified: info.ModTime(), filesize: info.Size(), isdir: info.IsDir(), + mode: info.Mode(), }) } } diff --git a/internal/files/files.go b/internal/files/files.go index 0cf74d6..8bdd63c 100644 --- a/internal/files/files.go +++ b/internal/files/files.go @@ -3,6 +3,7 @@ package files import ( "cmp" + "io/fs" "strings" "time" ) @@ -13,6 +14,7 @@ type File interface { Date() time.Time Filesize() int64 IsDir() bool + Mode() fs.FileMode String() string } diff --git a/internal/files/trash.go b/internal/files/trash.go index 1052004..77c349a 100644 --- a/internal/files/trash.go +++ b/internal/files/trash.go @@ -39,6 +39,7 @@ type TrashInfo struct { isdir bool trashed time.Time filesize int64 + mode fs.FileMode } func (t TrashInfo) Name() string { return t.name } @@ -47,6 +48,7 @@ func (t TrashInfo) Path() string { return t.ogpath } func (t TrashInfo) TrashInfo() string { return t.trashinfo } func (t TrashInfo) Date() time.Time { return t.trashed } func (t TrashInfo) IsDir() bool { return t.isdir } +func (t TrashInfo) Mode() fs.FileMode { return t.mode } func (t TrashInfo) Filesize() int64 { if t.isdir { return -1 diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 3eb2016..d91ee15 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -25,6 +25,7 @@ type Filter struct { matcher *regexp.Regexp unmatcher *regexp.Regexp minsize, maxsize int64 + mode fs.FileMode } func (f *Filter) On() time.Time { return f.on } @@ -38,6 +39,7 @@ func (f *Filter) DirsOnly() bool { return f.dirsonly } func (f *Filter) IgnoreHidden() bool { return f.ignorehidden } func (f *Filter) MinSize() int64 { return f.minsize } func (f *Filter) MaxSize() int64 { return f.maxsize } +func (f *Filter) Mode() fs.FileMode { return f.mode } func (f *Filter) AddFileName(filename string) { filename = filepath.Clean(filename) @@ -55,6 +57,7 @@ func (f *Filter) Match(info fs.FileInfo) bool { modified := info.ModTime() isdir := info.IsDir() size := info.Size() + mode := info.Mode() // on or before/after, not both if !f.on.IsZero() { @@ -127,6 +130,11 @@ func (f *Filter) Match(info fs.FileInfo) bool { return false } + if f.mode != 0 && f.mode != mode && f.mode-fs.ModeDir != mode { + log.Debugf("%s mode:'%s' (%d) isn't '%s' (%d), bye!", filename, mode, mode, f.mode, f.mode) + return false + } + // okay it was good log.Debugf("%s modified:'%s' dir:'%T' mode:'%s' was a good one!", filename, modified, isdir, mode) return true @@ -160,7 +168,8 @@ func (f *Filter) Blank() bool { !f.filesonly && !f.dirsonly && f.minsize == 0 && - f.maxsize == 0 + f.maxsize == 0 && + f.mode == 0 } func (f *Filter) String() string { @@ -172,13 +181,12 @@ func (f *Filter) String() string { unm = f.unmatcher.String() } 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'", + "unregex:'%s' filenames:'%v' filesonly:'%t' dirsonly:'%t' ignorehidden:'%t' "+ + "minsize:'%d' maxsize:'%d' mode:'%s'", f.on, f.before, f.after, - f.glob, m, - f.unglob, unm, - f.filenames, - f.filesonly, f.dirsonly, - f.ignorehidden, + f.glob, m, f.unglob, unm, + f.filenames, f.filesonly, f.dirsonly, + f.ignorehidden, f.minsize, f.maxsize, f.mode, ) } @@ -196,7 +204,7 @@ func (f *Filter) has_unregex() bool { return f.unmatcher.String() != "" } -func New(on, before, after, glob, pattern, unglob, unpattern string, filesonly, dirsonly, ignorehidden bool, minsize, maxsize string, names ...string) (*Filter, error) { +func New(on, before, after, glob, pattern, unglob, unpattern string, filesonly, dirsonly, ignorehidden bool, minsize, maxsize string, mode fs.FileMode, names ...string) (*Filter, error) { var ( err error now = time.Now() @@ -208,6 +216,7 @@ func New(on, before, after, glob, pattern, unglob, unpattern string, filesonly, filesonly: filesonly, dirsonly: dirsonly, ignorehidden: ignorehidden, + mode: mode, } f.AddFileNames(names...) diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 391b087..b290b2b 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -33,6 +33,7 @@ type testholder struct { ignorehidden bool good, bad []singletest minsize, maxsize string + mode fs.FileMode } func (t testholder) String() string { @@ -74,7 +75,7 @@ func testmatch(t *testing.T, testers []testholder) { f, err = filter.New( tester.on, tester.before, tester.after, tester.glob, tester.pattern, tester.unglob, tester.unpattern, tester.filesonly, tester.dirsonly, - tester.ignorehidden, tester.minsize, tester.maxsize, + tester.ignorehidden, tester.minsize, tester.maxsize, tester.mode, tester.filenames..., ) if err != nil { @@ -102,7 +103,7 @@ func testmatch(t *testing.T, testers []testholder) { func nameonly(dir bool, names ...string) []singletest { out := make([]singletest, 0, len(names)) for _, name := range names { - out = append(out, singletest{filename: name, modified: time.Time{}, isdir: dir, size: 0}) + out = append(out, singletest{filename: name, modified: time.Time{}, isdir: dir, size: 0, mode: 0000}) } return out } @@ -110,7 +111,7 @@ func nameonly(dir bool, names ...string) []singletest { func timeonly(dir bool, times ...time.Time) []singletest { out := make([]singletest, 0, len(times)) for _, time := range times { - out = append(out, singletest{filename: "blank.txt", modified: time, isdir: dir, size: 0}) + out = append(out, singletest{filename: "blank.txt", modified: time, isdir: dir, size: 0, mode: 0000}) } return out } @@ -118,7 +119,15 @@ func timeonly(dir bool, times ...time.Time) []singletest { func sizeonly(dir bool, sizes ...int64) []singletest { out := make([]singletest, 0, len(sizes)) for _, size := range sizes { - out = append(out, singletest{filename: "blank", modified: time.Time{}, isdir: dir, size: size}) + out = append(out, singletest{filename: "blank", modified: time.Time{}, isdir: dir, size: size, mode: 0000}) + } + return out +} + +func modeonly(dir bool, modes ...fs.FileMode) []singletest { + out := make([]singletest, 0, len(modes)) + for _, mode := range modes { + out = append(out, singletest{filename: "blank", modified: time.Time{}, isdir: dir, size: 0, mode: mode}) } return out } @@ -412,6 +421,16 @@ func TestFilesize(t *testing.T) { }) } +func TestMode(t *testing.T) { + testmatch(t, []testholder{ + { + mode: fs.FileMode(0755), + good: modeonly(false, fs.FileMode(0755)), + bad: modeonly(false, fs.FileMode(0644)), + }, + }) +} + func TestFilterMultipleParameters(t *testing.T) { y, m, d := now.Date() threepm := time.Date(y, m, d, 15, 0, 0, 0, time.Local) @@ -529,13 +548,41 @@ func TestFilterMultipleParameters(t *testing.T) { }, }, }, + { + mode: fs.FileMode(0600), + ignorehidden: true, + good: []singletest{ + { + mode: fs.FileMode(0600), + filename: "hello.txt", + }, + { + mode: fs.FileMode(0600), + filename: "main.go", + }, + }, + bad: []singletest{ + { + mode: fs.FileMode(0600), + filename: ".bashrc", + }, + { + mode: fs.FileMode(0644), + filename: "hello.txt", + }, + { + mode: fs.FileMode(0644), + filename: "main.go", + }, + }, + }, }) } func TestFilterBlank(t *testing.T) { var f *filter.Filter t.Run("new", func(t *testing.T) { - f, _ = filter.New("", "", "", "", "", "", "", false, false, false, "0", "0") + f, _ = filter.New("", "", "", "", "", "", "", false, false, false, "0", "0", 0) if !f.Blank() { t.Fatalf("filter isn't blank? %s", f) } @@ -587,6 +634,9 @@ func TestFilterNotBlank(t *testing.T) { { ignorehidden: true, }, + { + mode: fs.FileMode(0644), + }, } ) @@ -595,7 +645,7 @@ func TestFilterNotBlank(t *testing.T) { f, _ = filter.New( tester.on, tester.before, tester.after, tester.glob, tester.pattern, tester.unglob, tester.unpattern, tester.filesonly, tester.dirsonly, - tester.ignorehidden, tester.minsize, tester.maxsize, + tester.ignorehidden, tester.minsize, tester.maxsize, tester.mode, tester.filenames..., ) if f.Blank() { diff --git a/main.go b/main.go index 4b98f11..e022abc 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "slices" + "strconv" "time" "git.burning.moe/celediel/gt/internal/files" @@ -29,17 +30,17 @@ const ( ) var ( - loglvl string - f *filter.Filter - o, b, a, g, p string - sm, lg string - ung, unp string - fo, do, sh, ni bool - askconfirm bool - workdir, ogdir cli.Path - recursive bool - termwidth int - termheight int + loglvl string + f *filter.Filter + o, b, a, g, p, m string + sm, lg string + ung, unp string + fo, do, sh, ni bool + askconfirm bool + workdir, ogdir cli.Path + recursive bool + termwidth int + termheight int trashDir = filepath.Join(xdg.DataHome, "Trash") @@ -91,7 +92,11 @@ var ( ) if f == nil { - f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, false, sm, lg) + md, e := getMode(m) + if e != nil { + return e + } + f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, false, sm, lg, md) } if err != nil { return err @@ -115,7 +120,11 @@ var ( beforeCommands = func(ctx *cli.Context) (err error) { // setup filter if f == nil { - f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, false, sm, lg, ctx.Args().Slice()...) + md, e := getMode(m) + if e != nil { + return e + } + f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, false, sm, lg, md, ctx.Args().Slice()...) } log.Debugf("filter: %s", f.String()) return @@ -123,7 +132,11 @@ var ( beforeTrash = func(_ *cli.Context) (err error) { if f == nil { - f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, !sh, sm, lg) + md, e := getMode(m) + if e != nil { + return e + } + f, err = filter.New(o, b, a, g, p, ung, unp, fo, do, !sh, sm, lg, md) } log.Debugf("filter: %s", f.String()) return @@ -360,6 +373,12 @@ var ( Aliases: []string{"X"}, Destination: &lg, }, + &cli.StringFlag{ + Name: "mode", + Usage: "operate on files matching mode `MODE`", + Aliases: []string{"x"}, + Destination: &m, + }, } trashFlags = []cli.Flag{ @@ -524,3 +543,17 @@ func confirmTrash(fs files.Files) error { } return nil } + +func getMode(in string) (fs.FileMode, error) { + if in == "" { + return fs.FileMode(0), nil + } + if len(m) == 3 { + in = "0" + in + } + md, e := strconv.ParseUint(in, 8, 64) + if e != nil { + return 0, e + } + return fs.FileMode(md), nil +}