cmd: multiple formats output support for du command

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2025-08-25 13:39:57 +02:00 committed by CrazyMax
parent a6e198a341
commit b6baad406b
No known key found for this signature in database
GPG Key ID: ADE44D8C9D44FBE4
3 changed files with 348 additions and 92 deletions

View File

@ -4,8 +4,6 @@ import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"time"
@ -13,20 +11,77 @@ import (
"github.com/docker/buildx/util/cobrautil/completion"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/go-units"
"github.com/moby/buildkit/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
const (
duIDHeader = "ID"
duParentsHeader = "PARENTS"
duCreatedAtHeader = "CREATED AT"
duMutableHeader = "MUTABLE"
duReclaimHeader = "RECLAIMABLE"
duSharedHeader = "SHARED"
duSizeHeader = "SIZE"
duDescriptionHeader = "DESCRIPTION"
duUsageHeader = "USAGE COUNT"
duLastUsedAtHeader = "LAST ACCESSED"
duTypeHeader = "TYPE"
duDefaultTableFormat = "table {{.ID}}\t{{.Reclaimable}}\t{{.Size}}\t{{.LastUsedAt}}"
duDefaultPrettyTemplate = `ID: {{.ID}}
{{- if .Parents }}
Parents:
{{- range .Parents }}
- {{.}}
{{- end }}
{{- end }}
Created at: {{.CreatedAt}}
Mutable: {{.Mutable}}
Reclaimable: {{.Reclaimable}}
Shared: {{.Shared}}
Size: {{.Size}}
{{- if .Description}}
Description: {{ .Description }}
{{- end }}
Usage count: {{.UsageCount}}
{{- if .LastUsedAt}}
Last used: {{ .LastUsedAt }}
{{- end }}
{{- if .Type}}
Type: {{ .Type }}
{{- end }}
`
)
type duOptions struct {
builder string
filter opts.FilterOpt
verbose bool
format string
}
func runDiskUsage(ctx context.Context, dockerCli command.Cli, opts duOptions) error {
if opts.format != "" && opts.verbose {
return errors.New("--format and --verbose cannot be used together")
} else if opts.format == "" {
if opts.verbose {
opts.format = duDefaultPrettyTemplate
} else {
opts.format = duDefaultTableFormat
}
} else if opts.format == formatter.PrettyFormatKey {
opts.format = duDefaultPrettyTemplate
} else if opts.format == formatter.TableFormatKey {
opts.format = duDefaultTableFormat
}
pi, err := toBuildkitPruneInfo(opts.filter.Value())
if err != nil {
return err
@ -74,33 +129,53 @@ func runDiskUsage(ctx context.Context, dockerCli command.Cli, opts duOptions) er
return err
}
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0)
first := true
fctx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.Format(opts.format),
}
var dus []*client.UsageInfo
for _, du := range out {
if du == nil {
continue
}
if opts.verbose {
printVerbose(tw, du)
} else {
if first {
printTableHeader(tw)
first = false
}
for _, di := range du {
printTableRow(tw, di)
}
tw.Flush()
if du != nil {
dus = append(dus, du...)
}
}
if opts.filter.Value().Len() == 0 {
printSummary(tw, out)
render := func(format func(subContext formatter.SubContext) error) error {
for _, du := range dus {
if err := format(&diskusageContext{
format: fctx.Format,
du: du,
}); err != nil {
return err
}
}
return nil
}
tw.Flush()
return nil
duCtx := diskusageContext{}
duCtx.Header = formatter.SubHeaderContext{
"ID": duIDHeader,
"Parents": duParentsHeader,
"CreatedAt": duCreatedAtHeader,
"Mutable": duMutableHeader,
"Reclaimable": duReclaimHeader,
"Shared": duSharedHeader,
"Size": duSizeHeader,
"Description": duDescriptionHeader,
"UsageCount": duUsageHeader,
"LastUsedAt": duLastUsedAtHeader,
"Type": duTypeHeader,
}
defer func() {
if (fctx.Format != duDefaultTableFormat && fctx.Format != duDefaultPrettyTemplate) || fctx.Format.IsJSON() || opts.filter.Value().Len() > 0 {
return
}
printSummary(dockerCli.Out(), out)
}()
return fctx.Write(&duCtx, render)
}
func duCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
@ -119,64 +194,78 @@ func duCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
flags := cmd.Flags()
flags.Var(&options.filter, "filter", "Provide filter values")
flags.BoolVar(&options.verbose, "verbose", false, "Provide a more verbose output")
flags.BoolVar(&options.verbose, "verbose", false, `Shorthand for "--format=pretty"`)
flags.StringVar(&options.format, "format", "", "Format the output")
return cmd
}
func printKV(w io.Writer, k string, v any) {
fmt.Fprintf(w, "%s:\t%v\n", k, v)
type diskusageContext struct {
formatter.HeaderContext
format formatter.Format
du *client.UsageInfo
}
func printVerbose(tw *tabwriter.Writer, du []*client.UsageInfo) {
for _, di := range du {
printKV(tw, "ID", di.ID)
if len(di.Parents) != 0 {
printKV(tw, "Parent", strings.Join(di.Parents, ","))
}
printKV(tw, "Created at", di.CreatedAt)
printKV(tw, "Mutable", di.Mutable)
printKV(tw, "Reclaimable", !di.InUse)
printKV(tw, "Shared", di.Shared)
printKV(tw, "Size", units.HumanSize(float64(di.Size)))
if di.Description != "" {
printKV(tw, "Description", di.Description)
}
printKV(tw, "Usage count", di.UsageCount)
if di.LastUsedAt != nil {
printKV(tw, "Last used", units.HumanDuration(time.Since(*di.LastUsedAt))+" ago")
}
if di.RecordType != "" {
printKV(tw, "Type", di.RecordType)
}
fmt.Fprintf(tw, "\n")
}
tw.Flush()
func (d *diskusageContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(d)
}
func printTableHeader(tw *tabwriter.Writer) {
fmt.Fprintln(tw, "ID\tRECLAIMABLE\tSIZE\tLAST ACCESSED")
}
func printTableRow(tw *tabwriter.Writer, di *client.UsageInfo) {
id := di.ID
if di.Mutable {
func (d *diskusageContext) ID() string {
id := d.du.ID
if d.format.IsTable() && d.du.Mutable {
id += "*"
}
size := units.HumanSize(float64(di.Size))
if di.Shared {
size += "*"
}
lastAccessed := ""
if di.LastUsedAt != nil {
lastAccessed = units.HumanDuration(time.Since(*di.LastUsedAt)) + " ago"
}
fmt.Fprintf(tw, "%-40s\t%-5v\t%-10s\t%s\n", id, !di.InUse, size, lastAccessed)
return id
}
func printSummary(tw *tabwriter.Writer, dus [][]*client.UsageInfo) {
func (d *diskusageContext) Parents() []string {
return d.du.Parents
}
func (d *diskusageContext) CreatedAt() string {
return d.du.CreatedAt.String()
}
func (d *diskusageContext) Mutable() bool {
return d.du.Mutable
}
func (d *diskusageContext) Reclaimable() bool {
return !d.du.InUse
}
func (d *diskusageContext) Shared() bool {
return d.du.Shared
}
func (d *diskusageContext) Size() string {
size := units.HumanSize(float64(d.du.Size))
if d.format.IsTable() && d.du.Shared {
size += "*"
}
return size
}
func (d *diskusageContext) Description() string {
return d.du.Description
}
func (d *diskusageContext) UsageCount() int {
return d.du.UsageCount
}
func (d *diskusageContext) LastUsedAt() string {
if d.du.LastUsedAt != nil {
return units.HumanDuration(time.Since(*d.du.LastUsedAt)) + " ago"
}
return ""
}
func (d *diskusageContext) Type() string {
return string(d.du.RecordType)
}
func printSummary(w io.Writer, dus [][]*client.UsageInfo) {
total := int64(0)
reclaimable := int64(0)
shared := int64(0)
@ -195,11 +284,11 @@ func printSummary(tw *tabwriter.Writer, dus [][]*client.UsageInfo) {
}
}
tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
if shared > 0 {
fmt.Fprintf(tw, "Shared:\t%s\n", units.HumanSize(float64(shared)))
fmt.Fprintf(tw, "Private:\t%s\n", units.HumanSize(float64(total-shared)))
}
fmt.Fprintf(tw, "Reclaimable:\t%s\n", units.HumanSize(float64(reclaimable)))
fmt.Fprintf(tw, "Total:\t%s\n", units.HumanSize(float64(total)))
tw.Flush()

View File

@ -3,6 +3,7 @@ package commands
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
@ -241,3 +242,55 @@ func toBuildkitPruneInfo(f filters.Args) (*client.PruneInfo, error) {
Filter: []string{strings.Join(filters, ",")},
}, nil
}
func printKV(w io.Writer, k string, v any) {
fmt.Fprintf(w, "%s:\t%v\n", k, v)
}
func printVerbose(tw *tabwriter.Writer, du []*client.UsageInfo) {
for _, di := range du {
printKV(tw, "ID", di.ID)
if len(di.Parents) != 0 {
printKV(tw, "Parent", strings.Join(di.Parents, ","))
}
printKV(tw, "Created at", di.CreatedAt)
printKV(tw, "Mutable", di.Mutable)
printKV(tw, "Reclaimable", !di.InUse)
printKV(tw, "Shared", di.Shared)
printKV(tw, "Size", units.HumanSize(float64(di.Size)))
if di.Description != "" {
printKV(tw, "Description", di.Description)
}
printKV(tw, "Usage count", di.UsageCount)
if di.LastUsedAt != nil {
printKV(tw, "Last used", units.HumanDuration(time.Since(*di.LastUsedAt))+" ago")
}
if di.RecordType != "" {
printKV(tw, "Type", di.RecordType)
}
fmt.Fprintf(tw, "\n")
}
tw.Flush()
}
func printTableHeader(tw *tabwriter.Writer) {
fmt.Fprintln(tw, "ID\tRECLAIMABLE\tSIZE\tLAST ACCESSED")
}
func printTableRow(tw *tabwriter.Writer, di *client.UsageInfo) {
id := di.ID
if di.Mutable {
id += "*"
}
size := units.HumanSize(float64(di.Size))
if di.Shared {
size += "*"
}
lastAccessed := ""
if di.LastUsedAt != nil {
lastAccessed = units.HumanDuration(time.Since(*di.LastUsedAt)) + " ago"
}
fmt.Fprintf(tw, "%-40s\t%-5v\t%-10s\t%s\n", id, !di.InUse, size, lastAccessed)
}

View File

@ -14,7 +14,8 @@ Disk usage
| [`--builder`](#builder) | `string` | | Override the configured builder instance |
| `-D`, `--debug` | `bool` | | Enable debug logging |
| `--filter` | `filter` | | Provide filter values |
| [`--verbose`](#verbose) | `bool` | | Provide a more verbose output |
| [`--format`](#format) | `string` | | Format the output |
| [`--verbose`](#verbose) | `bool` | | Shorthand for `--format=pretty` |
<!---MARKER_GEN_END-->
@ -50,7 +51,7 @@ If `RECLAIMABLE` is false, the `docker buildx du prune` command won't delete
the record, even if you use `--all`. That's because the record is actively in
use by some component of the builder.
The asterisks (\*) in the default output indicate the following:
The asterisks (\*) in the default output format indicate the following:
- An asterisk next to an ID (`zu7m6evdpebh5h8kfkpw9dlf2*`) indicates that the record
is mutable. The size of the record may change, or another build can take ownership of
@ -61,33 +62,146 @@ The asterisks (\*) in the default output indicate the following:
If you prune such a record then you will lose build cache but only metadata
will be deleted as the image still needs to actual storage layers.
### <a name="format"></a> Format the output (--format)
The formatting options (`--format`) pretty-prints usage information output
using a Go template.
Valid placeholders for the Go template are:
* `.ID`
* `.Parents`
* `.CreatedAt`
* `.Mutable`
* `.Reclaimable`
* `.Shared`
* `.Size`
* `.Description`
* `.UsageCount`
* `.LastUsedAt`
* `.Type`
When using the `--format` option, the `du` command will either output the data
exactly as the template declares.
The `pretty` format is useful for inspecting the disk usage records in more
detail. It shows the mutable and shared states more clearly, as well as
additional information about the corresponding layer:
```console
$ docker buildx du --format=pretty
...
ID: 6wqu0v6hjdwvhh8yjozrepaof
Parents:
- bqx15bcewecz4wcg14b7iodvp
Created at: 2025-06-12 15:44:02.715795569 +0000 UTC
Mutable: false
Reclaimable: true
Shared: true
Size: 1.653GB
Description: [build-base 4/4] COPY . .
Usage count: 1
Last used: 2 months ago
Type: regular
Shared: 35.57GB
Private: 97.94GB
Reclaimable: 131.5GB
Total: 133.5GB
```
The following example uses a template without headers and outputs the
`ID` and `Size` entries separated by a colon (`:`):
```console
$ docker buildx du --format "{{.ID}}: {{.Size}}"
6wqu0v6hjdwvhh8yjozrepaof: 1.653GB
4m8061kctvjyh9qleus8rgpgx: 1.723GB
fcm9mlz2641u8r5eicjqdhy1l: 1.841GB
z2qu1swvo3afzd9mhihi3l5k0: 1.873GB
nmi6asc00aa3ja6xnt6o7wbrr: 2.027GB
0qlam41jxqsq6i27yqllgxed3: 2.495GB
3w9qhzzskq5jc262snfu90bfz: 2.617GB
```
The following example uses a `table` template and outputs the `ID` and
`Description`:
```console
$ docker buildx du --format "table {{.ID}} {{.Descirption}}"
lu76wm07lk5u7fe9nul93o95o [integration-tests 1/1] COPY . .
v6zmkcmgujv34vnys9eszttnv [dev 1/1] COPY --link . .
nj4fwb6qxznswmij3fg30sns2 mount / from exec /bin/sh -c rpm-init $DISTRO_NAME
```
JSON output is also supported and will print as newline delimited JSON:
```console
$ docker buildx du --format=json
{"CreatedAt":"2025-07-29T12:36:01Z","Description":"pulled from docker.io/library/rust:1.85.1-bookworm@sha256:e51d0265072d2d9d5d320f6a44dde6b9ef13653b035098febd68cce8fa7c0bc4","ID":"ic1gfidvev5nciupzz53alel4","LastUsedAt":"2025-07-29T12:36:01Z","Mutable":false,"Parents":["hmpdhm4sjrfpmae4xm2y3m0ra"],"Reclaimable":true,"Shared":false,"Size":"829889526","Type":"regular","UsageCount":1}
{"CreatedAt":"2025-08-05T09:24:09Z","Description":"pulled from docker.io/library/node:22@sha256:3218f0d1b9e4b63def322e9ae362d581fbeac1ef21b51fc502ef91386667ce92","ID":"jsw7fx09l5zsda3bri1z4mwk5","LastUsedAt":"2025-08-05T09:24:09Z","Mutable":false,"Parents":["098jsj5ebbv1w47ikqigeuurs"],"Reclaimable":true,"Shared":true,"Size":"829898832","Type":"regular","UsageCount":1}
```
You can use `jq` to pretty-print the JSON output:
```console
$ docker buildx du --format=json | jq .
{
"CreatedAt": "2025-07-29T12:36:01Z",
"Description": "pulled from docker.io/library/rust:1.85.1-bookworm@sha256:e51d0265072d2d9d5d320f6a44dde6b9ef13653b035098febd68cce8fa7c0bc4",
"ID": "ic1gfidvev5nciupzz53alel4",
"LastUsedAt": "2025-07-29T12:36:01Z",
"Mutable": false,
"Parents": [
"hmpdhm4sjrfpmae4xm2y3m0ra"
],
"Reclaimable": true,
"Shared": false,
"Size": "829889526",
"Type": "regular",
"UsageCount": 1
}
{
"CreatedAt": "2025-08-05T09:24:09Z",
"Description": "pulled from docker.io/library/node:22@sha256:3218f0d1b9e4b63def322e9ae362d581fbeac1ef21b51fc502ef91386667ce92",
"ID": "jsw7fx09l5zsda3bri1z4mwk5",
"LastUsedAt": "2025-08-05T09:24:09Z",
"Mutable": false,
"Parents": [
"098jsj5ebbv1w47ikqigeuurs"
],
"Reclaimable": true,
"Shared": true,
"Size": "829898832",
"Type": "regular",
"UsageCount": 1
}
```
### <a name="verbose"></a> Use verbose output (--verbose)
The verbose output of the `docker buildx du` command is useful for inspecting
the disk usage records in more detail. The verbose output shows the mutable and
shared states more clearly, as well as additional information about the
corresponding layer.
Shorthand for [`--format=pretty`](#format):
```console
$ docker buildx du --verbose
...
Last used: 2 days ago
Type: regular
ID: 6wqu0v6hjdwvhh8yjozrepaof
Parents:
- bqx15bcewecz4wcg14b7iodvp
Created at: 2025-06-12 15:44:02.715795569 +0000 UTC
Mutable: false
Reclaimable: true
Shared: true
Size: 1.653GB
Description: [build-base 4/4] COPY . .
Usage count: 1
Last used: 2 months ago
Type: regular
ID: 05d0elirb4mmvpmnzbrp3ssrg
Parent: e8sfdn4mygrg7msi9ak1dy6op
Created at: 2023-11-20 09:53:30.881558721 +0000 UTC
Mutable: false
Reclaimable: true
Shared: false
Size: 0B
Description: [gobase 3/3] WORKDIR /src
Usage count: 3
Last used: 24 hours ago
Type: regular
Reclaimable: 4.453GB
Total: 4.453GB
Shared: 35.57GB
Private: 97.94GB
Reclaimable: 131.5GB
Total: 133.5GB
```
### <a name="builder"></a> Override the configured builder instance (--builder)