From 721a9786a7084bbbbc16d0dd72127465f1679016 Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Fri, 2 Apr 2021 16:11:21 +0200 Subject: [PATCH] ls: introduce and global option to print human-readable sizes and consider it for ls commands Fixes #1890 --- fs/config.go | 1 + fs/config/configflags/configflags.go | 1 + fs/operations/operations.go | 63 ++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/fs/config.go b/fs/config.go index 1ed3000eb..e5fabb6ad 100644 --- a/fs/config.go +++ b/fs/config.go @@ -130,6 +130,7 @@ type ConfigInfo struct { FsCacheExpireDuration time.Duration FsCacheExpireInterval time.Duration DisableHTTP2 bool + HumanReadable bool } // NewConfig creates a new config with everything set to the default diff --git a/fs/config/configflags/configflags.go b/fs/config/configflags/configflags.go index d893f0017..0b6fd8ee5 100644 --- a/fs/config/configflags/configflags.go +++ b/fs/config/configflags/configflags.go @@ -131,6 +131,7 @@ func AddFlags(ci *fs.ConfigInfo, flagSet *pflag.FlagSet) { flags.DurationVarP(flagSet, &ci.FsCacheExpireDuration, "fs-cache-expire-duration", "", ci.FsCacheExpireDuration, "cache remotes for this long (0 to disable caching)") flags.DurationVarP(flagSet, &ci.FsCacheExpireInterval, "fs-cache-expire-interval", "", ci.FsCacheExpireInterval, "interval to check for expired remotes") flags.BoolVarP(flagSet, &ci.DisableHTTP2, "disable-http2", "", ci.DisableHTTP2, "Disable HTTP/2 in the global transport.") + flags.BoolVarP(flagSet, &ci.HumanReadable, "human-readable", "", ci.HumanReadable, "Print numbers in a human-readable format. Sizes with suffix Ki|Mi|Gi|Ti|Pi.") } // ParseHeaders converts the strings passed in via the header flags into HTTPOptions diff --git a/fs/operations/operations.go b/fs/operations/operations.go index 6ee7be2bf..ed28543fe 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -860,14 +860,69 @@ func syncFprintf(w io.Writer, format string, a ...interface{}) { } } +// SizeString make string representation of size for output +// +// Optional human-readable format including a binary suffix +func SizeString(size int64, humanReadable bool) string { + if humanReadable { + if size < 0 { + return "-" + fs.SizeSuffix(-size).String() + } + return fs.SizeSuffix(size).String() + } + return strconv.FormatInt(size, 10) +} + +// SizeStringField make string representation of size for output in fixed width field +// +// Optional human-readable format including a binary suffix +// Argument rawWidth is used to format field with of raw value. When humanReadable +// option the width is hard coded to 9, since SizeSuffix strings have precision 3 +// and longest value will be "999.999Ei". This way the width can be optimized +// depending to the humanReadable option. To always use a longer width the return +// value can always be fed into another format string with a specific field with. +func SizeStringField(size int64, humanReadable bool, rawWidth int) string { + str := SizeString(size, humanReadable) + if humanReadable { + return fmt.Sprintf("%9s", str) + } + return fmt.Sprintf("%[2]*[1]s", str, rawWidth) +} + +// CountString make string representation of count for output +// +// Optional human-readable format including a decimal suffix +func CountString(count int64, humanReadable bool) string { + if humanReadable { + if count < 0 { + return "-" + fs.CountSuffix(-count).String() + } + return fs.CountSuffix(count).String() + } + return strconv.FormatInt(count, 10) +} + +// CountStringField make string representation of count for output in fixed width field +// +// Similar to SizeStringField, but human readable with decimal prefix and field width 8 +// since there is no 'i' in the decimal prefix symbols (e.g. "999.999E") +func CountStringField(count int64, humanReadable bool, rawWidth int) string { + str := CountString(count, humanReadable) + if humanReadable { + return fmt.Sprintf("%8s", str) + } + return fmt.Sprintf("%[2]*[1]s", str, rawWidth) +} + // List the Fs to the supplied writer // // Shows size and path - obeys includes and excludes // // Lists in parallel which may get them out of order func List(ctx context.Context, f fs.Fs, w io.Writer) error { + ci := fs.GetConfig(ctx) return ListFn(ctx, f, func(o fs.Object) { - syncFprintf(w, "%9d %s\n", o.Size(), o.Remote()) + syncFprintf(w, "%s %s\n", SizeStringField(o.Size(), ci.HumanReadable, 9), o.Remote()) }) } @@ -877,13 +932,14 @@ func List(ctx context.Context, f fs.Fs, w io.Writer) error { // // Lists in parallel which may get them out of order func ListLong(ctx context.Context, f fs.Fs, w io.Writer) error { + ci := fs.GetConfig(ctx) return ListFn(ctx, f, func(o fs.Object) { tr := accounting.Stats(ctx).NewCheckingTransfer(o) defer func() { tr.Done(ctx, nil) }() modTime := o.ModTime(ctx) - syncFprintf(w, "%9d %s %s\n", o.Size(), modTime.Local().Format("2006-01-02 15:04:05.000000000"), o.Remote()) + syncFprintf(w, "%s %s %s\n", SizeStringField(o.Size(), ci.HumanReadable, 9), modTime.Local().Format("2006-01-02 15:04:05.000000000"), o.Remote()) }) } @@ -1012,10 +1068,11 @@ func ConfigMaxDepth(ctx context.Context, recursive bool) int { // ListDir lists the directories/buckets/containers in the Fs to the supplied writer func ListDir(ctx context.Context, f fs.Fs, w io.Writer) error { + ci := fs.GetConfig(ctx) return walk.ListR(ctx, f, "", false, ConfigMaxDepth(ctx, false), walk.ListDirs, func(entries fs.DirEntries) error { entries.ForDir(func(dir fs.Directory) { if dir != nil { - syncFprintf(w, "%12d %13s %9d %s\n", dir.Size(), dir.ModTime(ctx).Local().Format("2006-01-02 15:04:05"), dir.Items(), dir.Remote()) + syncFprintf(w, "%s %13s %s %s\n", SizeStringField(dir.Size(), ci.HumanReadable, 12), dir.ModTime(ctx).Local().Format("2006-01-02 15:04:05"), CountStringField(dir.Items(), ci.HumanReadable, 9), dir.Remote()) } }) return nil