From 8812dcd56af987c542bff873b8568aadb52cc914 Mon Sep 17 00:00:00 2001 From: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> Date: Thu, 1 Jun 2023 21:18:18 -0400 Subject: [PATCH] Add --human-readable to ls and find output Modifies format module to add options for human readable storage size formatting, using size parsing already in ui/format. Cmd flag --human-readable added to ls and find commands. Additional option added to formatNode to support printing size in regular or new human readable format --- changelog/unreleased/issue-4159 | 13 +++++++ cmd/restic/cmd_find.go | 19 ++++++---- cmd/restic/cmd_ls.go | 6 ++- cmd/restic/format.go | 12 ++++-- cmd/restic/format_test.go | 65 +++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 changelog/unreleased/issue-4159 create mode 100644 cmd/restic/format_test.go diff --git a/changelog/unreleased/issue-4159 b/changelog/unreleased/issue-4159 new file mode 100644 index 000000000..1b3a73394 --- /dev/null +++ b/changelog/unreleased/issue-4159 @@ -0,0 +1,13 @@ +Enhancement: Add --human-readable flag to ls and find commands + +Previously, the `ls` and `find` commands showed with the `-l` option +showed size in bytes, and did not have an option for a more human +readable format that converted these longer numbers into values such +as MiB or GiB + +The new `--human-readable` option for `ls` and `find` when used with +the `-l` option will convert longer size values into more human friendly +values, with an appropriate suffix depending on the output size. For +example, a value of `14680064` will be shown as `14.000 MiB`. + +https://github.com/restic/restic/issues/4159 \ No newline at end of file diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index 6b5e32df7..e1161797b 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -51,6 +51,7 @@ type FindOptions struct { PackID, ShowPackID bool CaseInsensitive bool ListLong bool + HumanReadable bool restic.SnapshotFilter } @@ -69,6 +70,7 @@ func init() { f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)") f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern") f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") + f.BoolVar(&findOptions.HumanReadable, "human-readable", false, "print sizes in human readable format") initMultiSnapshotFilter(f, &findOptions.SnapshotFilter, true) } @@ -104,12 +106,13 @@ func parseTime(str string) (time.Time, error) { } type statefulOutput struct { - ListLong bool - JSON bool - inuse bool - newsn *restic.Snapshot - oldsn *restic.Snapshot - hits int + ListLong bool + HumanReadable bool + JSON bool + inuse bool + newsn *restic.Snapshot + oldsn *restic.Snapshot + hits int } func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) { @@ -164,7 +167,7 @@ func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) { s.oldsn = s.newsn Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat)) } - Println(formatNode(path, node, s.ListLong)) + Println(formatNode(path, node, s.ListLong, s.HumanReadable)) } func (s *statefulOutput) PrintPattern(path string, node *restic.Node) { @@ -594,7 +597,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] f := &Finder{ repo: repo, pat: pat, - out: statefulOutput{ListLong: opts.ListLong, JSON: gopts.JSON}, + out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON}, ignoreTrees: restic.NewIDSet(), } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index e8c27381c..1cd549e7c 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -50,7 +50,8 @@ Exit status is 0 if the command was successful, and non-zero if there was any er type LsOptions struct { ListLong bool restic.SnapshotFilter - Recursive bool + Recursive bool + HumanReadable bool } var lsOptions LsOptions @@ -62,6 +63,7 @@ func init() { initSingleSnapshotFilter(flags, &lsOptions.SnapshotFilter) flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") 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") } type lsSnapshot struct { @@ -206,7 +208,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time) } printNode = func(path string, node *restic.Node) { - Printf("%s\n", formatNode(path, node, lsOptions.ListLong)) + Printf("%s\n", formatNode(path, node, lsOptions.ListLong, lsOptions.HumanReadable)) } } diff --git a/cmd/restic/format.go b/cmd/restic/format.go index 2f14a4575..0bc6d9fd2 100644 --- a/cmd/restic/format.go +++ b/cmd/restic/format.go @@ -5,9 +5,10 @@ import ( "os" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" ) -func formatNode(path string, n *restic.Node, long bool) string { +func formatNode(path string, n *restic.Node, long bool, human bool) string { if !long { return path } @@ -15,6 +16,11 @@ func formatNode(path string, n *restic.Node, long bool) string { var mode os.FileMode var target string + size := fmt.Sprintf("%6d", n.Size) + if human { + size = ui.FormatBytes(n.Size) + } + switch n.Type { case "file": mode = 0 @@ -33,8 +39,8 @@ func formatNode(path string, n *restic.Node, long bool) string { mode = os.ModeSocket } - return fmt.Sprintf("%s %5d %5d %6d %s %s%s", - mode|n.Mode, n.UID, n.GID, n.Size, + return fmt.Sprintf("%s %5d %5d %s %s %s%s", + mode|n.Mode, n.UID, n.GID, size, n.ModTime.Local().Format(TimeFormat), path, target) } diff --git a/cmd/restic/format_test.go b/cmd/restic/format_test.go new file mode 100644 index 000000000..9708b03d8 --- /dev/null +++ b/cmd/restic/format_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "testing" + "time" + + "github.com/restic/restic/internal/restic" + rtest "github.com/restic/restic/internal/test" +) + +func TestFormatNode(t *testing.T) { + for _, c := range []struct { + path string + restic.Node + long bool + human bool + expect string + }{ + { + path: "/test/path", + Node: restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + }, + long: false, + human: false, + expect: "/test/path", + }, + { + path: "/test/path", + Node: restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + }, + long: true, + human: false, + expect: "---------- 1000 2000 14680064 2020-01-01 22:04:05 /test/path", + }, + { + path: "/test/path", + Node: restic.Node{ + Name: "baz", + Type: "file", + Size: 14680064, + UID: 1000, + GID: 2000, + ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + }, + long: true, + human: true, + expect: "---------- 1000 2000 14.000 MiB 2020-01-01 22:04:05 /test/path", + }, + } { + r := formatNode(c.path, &c.Node, c.long, c.human) + rtest.Equals(t, r, c.expect) + } +}