From 060a44202fb0da073414e6c6672a5abfc28ac9ed Mon Sep 17 00:00:00 2001 From: Winfried Plappert Date: Mon, 3 Feb 2025 21:07:04 +0000 Subject: [PATCH] ls: sort output by size, atime, ctime, mtime, time(=mtime), extension (#5182) Enhancement: create ability to sort output of restic ls -l by name, size, atime, ctime, mtime, time(=mtime), X(=extension), extension --------- Co-authored-by: Michael Eischer --- changelog/unreleased/issue-4179 | 11 ++ cmd/restic/cmd_ls.go | 138 ++++++++++++++++++++++++- cmd/restic/cmd_ls_integration_test.go | 61 ++++++++++- cmd/restic/testdata/backup-data.tar.gz | Bin 11704 -> 12126 bytes doc/045_working_with_repos.rst | 47 ++++++++- 5 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 changelog/unreleased/issue-4179 diff --git a/changelog/unreleased/issue-4179 b/changelog/unreleased/issue-4179 new file mode 100644 index 000000000..37955b060 --- /dev/null +++ b/changelog/unreleased/issue-4179 @@ -0,0 +1,11 @@ +Enhancement: add sort options for `ls` command + +in the past, the output of ls -l was sorted by name. Now it can be sorted by +one of the specifiers (name|size|time=mtime|atime|ctime|extension). +Use --sort to achieve this. + +Reverse sorting also has been implemtented. Use --reverse to indicate reverse +sorting. + +https://github.com/restic/restic/issues/4179 +https://github.com/restic/restic/pull/5182 diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 06ae6cc20..6e0d230b1 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -1,11 +1,14 @@ package main import ( + "cmp" "context" "encoding/json" "fmt" "io" "os" + "path/filepath" + "slices" "strings" "time" @@ -36,6 +39,10 @@ will allow traversing into matching directories' subfolders. Any directory paths specified must be absolute (starting with a path separator); paths use the forward slash '/' as separator. +File listings can be sorted by specifying --sort followed by one of the +sort specifiers '(name|size|time=mtime|atime|ctime|extension)'. +The sorting can be reversed by specifying --reverse. + EXIT STATUS =========== @@ -59,6 +66,8 @@ type LsOptions struct { Recursive bool HumanReadable bool Ncdu bool + Sort string + Reverse bool } var lsOptions LsOptions @@ -72,6 +81,8 @@ func init() { flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories") flags.BoolVar(&lsOptions.HumanReadable, "human-readable", false, "print sizes in human readable format") flags.BoolVar(&lsOptions.Ncdu, "ncdu", false, "output NCDU export format (pipe into 'ncdu -f -')") + flags.StringVarP(&lsOptions.Sort, "sort", "s", "name", "sort output by (name|size|time=mtime|atime|ctime|extension)") + flags.BoolVar(&lsOptions.Reverse, "reverse", false, "reverse sorted output") } type lsPrinter interface { @@ -277,6 +288,12 @@ func (p *textLsPrinter) Close() error { return nil } +// for ls -l output sorting +type toSortOutput struct { + nodepath string + node *restic.Node +} + func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string) error { if len(args) == 0 { return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'") @@ -284,6 +301,18 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri if opts.Ncdu && gopts.JSON { return errors.Fatal("only either '--json' or '--ncdu' can be specified") } + if opts.Sort != "name" && opts.Ncdu { + return errors.Fatal("--sort and --ncdu are mutually exclusive") + } + if opts.Reverse && opts.Ncdu { + return errors.Fatal("--reverse and --ncdu are mutually exclusive") + } + + sortMode := SortModeName + err := sortMode.Set(opts.Sort) + if err != nil { + return err + } // extract any specific directories to walk var dirs []string @@ -347,6 +376,8 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri } var printer lsPrinter + collector := []toSortOutput{} + outputSort := sortMode != SortModeName || opts.Reverse if gopts.JSON { printer = &jsonLsPrinter{ @@ -356,6 +387,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri printer = &ncduLsPrinter{ out: globalOptions.stdout, } + outputSort = false } else { printer = &textLsPrinter{ dirs: dirs, @@ -393,8 +425,12 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri printedDir := false if withinDir(nodepath) { // if we're within a target path, print the node - if err := printer.Node(nodepath, node, false); err != nil { - return err + if outputSort { + collector = append(collector, toSortOutput{nodepath, node}) + } else { + if err := printer.Node(nodepath, node, false); err != nil { + return err + } } printedDir = true @@ -409,7 +445,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri // there yet), signal the walker to descend into any subdirs if approachingMatchingTree(nodepath) { // print node leading up to the target paths - if !printedDir { + if !printedDir && !outputSort { return printer.Node(nodepath, node, true) } return nil @@ -444,5 +480,101 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri return err } + if outputSort { + printSortedOutput(printer, opts, sortMode, collector) + } + return printer.Close() } + +func printSortedOutput(printer lsPrinter, opts LsOptions, sortMode SortMode, collector []toSortOutput) { + switch sortMode { + case SortModeName: + case SortModeSize: + slices.SortStableFunc(collector, func(a, b toSortOutput) int { + return cmp.Or( + cmp.Compare(a.node.Size, b.node.Size), + cmp.Compare(a.nodepath, b.nodepath), + ) + }) + case SortModeMtime: + slices.SortStableFunc(collector, func(a, b toSortOutput) int { + return cmp.Or( + a.node.ModTime.Compare(b.node.ModTime), + cmp.Compare(a.nodepath, b.nodepath), + ) + }) + case SortModeAtime: + slices.SortStableFunc(collector, func(a, b toSortOutput) int { + return cmp.Or( + a.node.AccessTime.Compare(b.node.AccessTime), + cmp.Compare(a.nodepath, b.nodepath), + ) + }) + case SortModeCtime: + slices.SortStableFunc(collector, func(a, b toSortOutput) int { + return cmp.Or( + a.node.ChangeTime.Compare(b.node.ChangeTime), + cmp.Compare(a.nodepath, b.nodepath), + ) + }) + case SortModeExt: + // map name to extension + mapExt := make(map[string]string, len(collector)) + for _, item := range collector { + ext := filepath.Ext(item.nodepath) + mapExt[item.nodepath] = ext + } + + slices.SortStableFunc(collector, func(a, b toSortOutput) int { + return cmp.Or( + cmp.Compare(mapExt[a.nodepath], mapExt[b.nodepath]), + cmp.Compare(a.nodepath, b.nodepath), + ) + }) + } + + if opts.Reverse { + slices.Reverse(collector) + } + for _, elem := range collector { + _ = printer.Node(elem.nodepath, elem.node, false) + } +} + +// SortMode defines the allowed sorting modes +type SortMode string + +// Allowed sort modes +const ( + SortModeName SortMode = "name" + SortModeSize SortMode = "size" + SortModeAtime SortMode = "atime" + SortModeCtime SortMode = "ctime" + SortModeMtime SortMode = "mtime" + SortModeExt SortMode = "extension" + SortModeInvalid SortMode = "--invalid--" +) + +// Set implements the method needed for pflag command flag parsing. +func (c *SortMode) Set(s string) error { + switch s { + case "name": + *c = SortModeName + case "size": + *c = SortModeSize + case "atime": + *c = SortModeAtime + case "ctime": + *c = SortModeCtime + case "mtime", "time": + *c = SortModeMtime + case "extension": + *c = SortModeExt + default: + *c = SortModeInvalid + return fmt.Errorf("invalid sort mode %q, must be one of (name|size|atime|ctime|mtime=time|extension)", s) + } + + return nil +} diff --git a/cmd/restic/cmd_ls_integration_test.go b/cmd/restic/cmd_ls_integration_test.go index f5655bdff..29e153419 100644 --- a/cmd/restic/cmd_ls_integration_test.go +++ b/cmd/restic/cmd_ls_integration_test.go @@ -19,7 +19,7 @@ func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args [ } func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string { - out := testRunLsWithOpts(t, gopts, LsOptions{}, []string{snapshotID}) + out := testRunLsWithOpts(t, gopts, LsOptions{Sort: "name"}, []string{snapshotID}) return strings.Split(string(out), "\n") } @@ -45,7 +45,64 @@ func TestRunLsNcdu(t *testing.T) { {"latest", "/0"}, {"latest", "/0", "/0/9"}, } { - ncdu := testRunLsWithOpts(t, env.gopts, LsOptions{Ncdu: true}, paths) + ncdu := testRunLsWithOpts(t, env.gopts, LsOptions{Ncdu: true, Sort: "name"}, paths) assertIsValidJSON(t, ncdu) } } + +func TestRunLsSort(t *testing.T) { + compareName := []string{ + "/for_cmd_ls", + "/for_cmd_ls/file1.txt", + "/for_cmd_ls/file2.txt", + "/for_cmd_ls/python.py", + "", // last empty line + } + + compareSize := []string{ + "/for_cmd_ls", + "/for_cmd_ls/file2.txt", + "/for_cmd_ls/file1.txt", + "/for_cmd_ls/python.py", + "", + } + + compareExt := []string{ + "/for_cmd_ls", + "/for_cmd_ls/python.py", + "/for_cmd_ls/file1.txt", + "/for_cmd_ls/file2.txt", + "", + } + + env, cleanup := withTestEnvironment(t) + defer cleanup() + + testSetupBackupData(t, env) + opts := BackupOptions{} + testRunBackup(t, env.testdata+"/0", []string{"for_cmd_ls"}, opts, env.gopts) + + // sort by size + out := testRunLsWithOpts(t, env.gopts, LsOptions{Sort: "size"}, []string{"latest"}) + fileList := strings.Split(string(out), "\n") + rtest.Assert(t, len(fileList) == 5, "invalid ls --sort size, expected 5 array elements, got %v", len(fileList)) + for i, item := range compareSize { + rtest.Assert(t, item == fileList[i], "invalid ls --sort size, expected element '%s', got '%s'", item, fileList[i]) + } + + // sort by file extension + out = testRunLsWithOpts(t, env.gopts, LsOptions{Sort: "extension"}, []string{"latest"}) + fileList = strings.Split(string(out), "\n") + rtest.Assert(t, len(fileList) == 5, "invalid ls --sort extension, expected 5 array elements, got %v", len(fileList)) + for i, item := range compareExt { + rtest.Assert(t, item == fileList[i], "invalid ls --sort extension, expected element '%s', got '%s'", item, fileList[i]) + } + + // explicit name sort + out = testRunLsWithOpts(t, env.gopts, LsOptions{Sort: "name"}, []string{"latest"}) + fileList = strings.Split(string(out), "\n") + rtest.Assert(t, len(fileList) == 5, "invalid ls --sort name, expected 5 array elements, got %v", len(fileList)) + for i, item := range compareName { + rtest.Assert(t, item == fileList[i], "invalid ls --sort name, expected element '%s', got '%s'", item, fileList[i]) + } +} diff --git a/cmd/restic/testdata/backup-data.tar.gz b/cmd/restic/testdata/backup-data.tar.gz index 6ba5881ae4f2a1824bd9a7978ba772a6f86d5384..5d7cd215cab0eb3da390acd74f11dc7000a682f8 100644 GIT binary patch literal 12126 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 diff --git a/doc/045_working_with_repos.rst b/doc/045_working_with_repos.rst index f31e75c84..f8ff390f0 100644 --- a/doc/045_working_with_repos.rst +++ b/doc/045_working_with_repos.rst @@ -121,7 +121,7 @@ as separator. .. code-block:: console $ restic ls latest /home - + snapshot 073a90db of [/home/user/work.txt] filtered by [/home] at 2024-01-21 16:51:18.474558607 +0100 CET): /home /home/user @@ -153,6 +153,49 @@ outputting information about a snapshot in the NCDU format using the ``--ncdu`` You can use it as follows: ``restic ls latest --ncdu | ncdu -f -`` +You can use the options ``--sort`` and ``--reverse`` to tailor ``ls`` output to your needs. +``--sort`` can be one of ``name | size | time=mtime | atime | ctime | extension``. The default +sorting option is ``name``. The sorting order can be reversed by specifying ``--reverse``. + +.. code-block:: console + + $ restic ls --long latest --sort size --reverse + + snapshot 711b0bb6 of [/tmp/restic] at 2025-02-03 08:16:05.310764668 +0000 UTC filtered by []: + -rw-rw-r-- 1000 1000 16772 2025-02-03 08:09:11 /tmp/restic/cmd_find.go + -rw-rw-r-- 1000 1000 3077 2025-02-03 08:15:46 /tmp/restic/conf.py + -rw-rw-r-- 1000 1000 2834 2025-02-03 08:09:35 /tmp/restic/find.go + -rw-rw-r-- 1000 1000 1473 2025-02-03 08:15:30 /tmp/restic/010_introduction.rst + drwxrwxr-x 1000 1000 0 2025-02-03 08:15:46 /tmp/restic + dtrwxrwxrwx 0 0 0 2025-02-03 08:14:22 /tmp + +.. code-block:: console + + $ restic ls --long latest --sort time + + snapshot 711b0bb6 of [/tmp/restic] at 2025-02-03 08:16:05.310764668 +0000 UTC filtered by []: + -rw-rw-r-- 1000 1000 16772 2025-02-03 08:09:11 /tmp/restic/cmd_find.go + -rw-rw-r-- 1000 1000 2834 2025-02-03 08:09:35 /tmp/restic/find.go + dtrwxrwxrwx 0 0 0 2025-02-03 08:14:22 /tmp + -rw-rw-r-- 1000 1000 1473 2025-02-03 08:15:30 /tmp/restic/010_introduction.rst + drwxrwxr-x 1000 1000 0 2025-02-03 08:15:46 /tmp/restic + -rw-rw-r-- 1000 1000 3077 2025-02-03 08:15:46 /tmp/restic/conf.py + +Sorting works with option ``--json`` as well. Sorting and option ``--ncdu`` are mutually exclusive. +It works also without specifying the option ``--long``. + +.. code-block:: console + + $ restic ls latest --sort extension + + snapshot 711b0bb6 of [/tmp/restic] at 2025-02-03 08:16:05.310764668 +0000 UTC filtered by []: + /tmp + /tmp/restic + /tmp/restic/cmd_find.go + /tmp/restic/find.go + /tmp/restic/conf.py + /tmp/restic/010_introduction.rst + Copying snapshots between repositories ====================================== @@ -317,7 +360,7 @@ Modifying metadata of snapshots =============================== Sometimes it may be desirable to change the metadata of an existing snapshot. -Currently, rewriting the hostname and the time of the backup is supported. +Currently, rewriting the hostname and the time of the backup is supported. This is possible using the ``rewrite`` command with the option ``--new-host`` followed by the desired new hostname or the option ``--new-time`` followed by the desired new timestamp. .. code-block:: console