Skip to content

Commit d8a85fb

Browse files
authored
Merge pull request #6875 from thaJeztah/optimize_formatter
cli/command/formatter: assorted fixes and cleanups
2 parents efddff6 + b309524 commit d8a85fb

File tree

5 files changed

+133
-58
lines changed

5 files changed

+133
-58
lines changed

cli/command/container/formatter_stats.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ func NewStatsFormat(source, osType string) formatter.Format {
112112
return formatter.Format(source)
113113
}
114114

115-
// NewStats returns a new Stats entity and sets in it the given name
116-
func NewStats(container string) *Stats {
117-
return &Stats{StatsEntry: StatsEntry{Container: container}}
115+
// NewStats returns a new Stats entity using the given ID, ID-prefix, or
116+
// name to resolve the container.
117+
func NewStats(idOrName string) *Stats {
118+
// FIXME(thaJeztah): "idOrName" is used for fuzzy-matching the container, which can result in multiple stats for the same container.
119+
// We should resolve the canonical ID once, then use that as reference
120+
// to prevent duplicates. Various parts in the code compare Container
121+
// against "ID" only (not considering "name" or "ID-prefix").
122+
return &Stats{StatsEntry: StatsEntry{Container: idOrName}}
118123
}
119124

120125
// statsFormatWrite renders the context for a list of containers statistics

cli/command/formatter/container.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,28 @@ func (c *ContainerContext) ID() string {
141141

142142
// Names returns a comma-separated string of the container's names, with their
143143
// slash (/) prefix stripped. Additional names for the container (related to the
144-
// legacy `--link` feature) are omitted.
144+
// legacy `--link` feature) are omitted when formatting "truncated".
145145
func (c *ContainerContext) Names() string {
146-
names := StripNamePrefix(c.c.Names)
147-
if c.trunc {
148-
for _, name := range names {
149-
if len(strings.Split(name, "/")) == 1 {
150-
names = []string{name}
151-
break
146+
var b strings.Builder
147+
for i, n := range c.c.Names {
148+
name := strings.TrimPrefix(n, "/")
149+
if c.trunc {
150+
// When printing truncated, we only print a single name.
151+
//
152+
// Pick the first name that's not a legacy link (does not have
153+
// slashes inside the name itself (e.g., "/other-container/link")).
154+
// Normally this would be the first name found.
155+
if strings.IndexByte(name, '/') == -1 {
156+
return name
152157
}
158+
continue
159+
}
160+
if i > 0 {
161+
b.WriteByte(',')
153162
}
163+
b.WriteString(name)
154164
}
155-
return strings.Join(names, ",")
165+
return b.String()
156166
}
157167

158168
// StripNamePrefix removes any "/" prefix from container names returned

cli/command/formatter/disk_usage.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ func (ctx *DiskUsageContext) startSubsection(format Format) (*template.Template,
4646
ctx.buffer = &bytes.Buffer{}
4747
ctx.header = ""
4848
ctx.Format = format
49-
ctx.preFormat()
5049

5150
return ctx.parseFormat()
5251
}
@@ -88,7 +87,6 @@ func (ctx *DiskUsageContext) Write() (err error) {
8887
return ctx.verboseWrite()
8988
}
9089
ctx.buffer = &bytes.Buffer{}
91-
ctx.preFormat()
9290

9391
tmpl, err := ctx.parseFormat()
9492
if err != nil {
@@ -213,7 +211,6 @@ func (ctx *DiskUsageContext) verboseWrite() error {
213211
return ctx.verboseWriteTable(duc)
214212
}
215213

216-
ctx.preFormat()
217214
tmpl, err := ctx.parseFormat()
218215
if err != nil {
219216
return err

cli/command/formatter/formatter.go

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (f Format) IsTable() bool {
3333
return strings.HasPrefix(string(f), TableFormatKey)
3434
}
3535

36-
// IsJSON returns true if the format is the json format
36+
// IsJSON returns true if the format is the JSON format
3737
func (f Format) IsJSON() bool {
3838
return string(f) == JSONFormatKey
3939
}
@@ -43,6 +43,31 @@ func (f Format) Contains(sub string) bool {
4343
return strings.Contains(string(f), sub)
4444
}
4545

46+
// templateString pre-processes the format and returns it as a string
47+
// for templating.
48+
func (f Format) templateString() string {
49+
out := string(f)
50+
switch out {
51+
case TableFormatKey:
52+
// A bare "--format table" should already be handled before we
53+
// hit this; a literal "table" here means a custom "table" format
54+
// without template.
55+
return ""
56+
case JSONFormatKey:
57+
// "--format json" only; not JSON formats ("--format '{{json .Field}}'").
58+
return JSONFormat
59+
}
60+
61+
// "--format 'table {{.Field}}\t{{.Field}}'" -> "{{.Field}}\t{{.Field}}"
62+
if after, isTable := strings.CutPrefix(out, TableFormatKey); isTable {
63+
out = after
64+
}
65+
66+
out = strings.Trim(out, " ") // trim spaces, but preserve other whitespace.
67+
out = strings.NewReplacer(`\t`, "\t", `\n`, "\n").Replace(out)
68+
return out
69+
}
70+
4671
// Context contains information required by the formatter to print the output as desired.
4772
type Context struct {
4873
// Output is the output stream to which the formatted string is written.
@@ -53,49 +78,34 @@ type Context struct {
5378
Trunc bool
5479

5580
// internal element
56-
finalFormat string
57-
header any
58-
buffer *bytes.Buffer
59-
}
60-
61-
func (c *Context) preFormat() {
62-
c.finalFormat = string(c.Format)
63-
// TODO: handle this in the Format type
64-
switch {
65-
case c.Format.IsTable():
66-
c.finalFormat = c.finalFormat[len(TableFormatKey):]
67-
case c.Format.IsJSON():
68-
c.finalFormat = JSONFormat
69-
}
70-
71-
c.finalFormat = strings.Trim(c.finalFormat, " ")
72-
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
73-
c.finalFormat = r.Replace(c.finalFormat)
81+
header any
82+
buffer *bytes.Buffer
7483
}
7584

7685
func (c *Context) parseFormat() (*template.Template, error) {
77-
tmpl, err := templates.Parse(c.finalFormat)
86+
tmpl, err := templates.Parse(c.Format.templateString())
7887
if err != nil {
7988
return nil, fmt.Errorf("template parsing error: %w", err)
8089
}
8190
return tmpl, nil
8291
}
8392

8493
func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {
85-
if c.Output == nil {
86-
c.Output = io.Discard
94+
out := c.Output
95+
if out == nil {
96+
out = io.Discard
8797
}
88-
if c.Format.IsTable() {
89-
t := tabwriter.NewWriter(c.Output, 10, 1, 3, ' ', 0)
90-
buffer := bytes.NewBufferString("")
91-
tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader())
92-
buffer.WriteTo(t)
93-
t.Write([]byte("\n"))
94-
c.buffer.WriteTo(t)
95-
t.Flush()
96-
} else {
97-
c.buffer.WriteTo(c.Output)
98+
if !c.Format.IsTable() {
99+
_, _ = c.buffer.WriteTo(out)
100+
return
98101
}
102+
103+
// Write column-headers and rows to the tab-writer buffer, then flush the output.
104+
tw := tabwriter.NewWriter(out, 10, 1, 3, ' ', 0)
105+
_ = tmpl.Funcs(templates.HeaderFunctions).Execute(tw, subContext.FullHeader())
106+
_, _ = tw.Write([]byte{'\n'})
107+
_, _ = c.buffer.WriteTo(tw)
108+
_ = tw.Flush()
99109
}
100110

101111
func (c *Context) contextFormat(tmpl *template.Template, subContext SubContext) error {
@@ -115,8 +125,6 @@ type SubFormat func(func(SubContext) error) error
115125
// Write the template to the buffer using this Context
116126
func (c *Context) Write(sub SubContext, f SubFormat) error {
117127
c.buffer = &bytes.Buffer{}
118-
c.preFormat()
119-
120128
tmpl, err := c.parseFormat()
121129
if err != nil {
122130
return err

cli/command/formatter/formatter_test.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,75 @@ import (
88
"testing"
99

1010
"gotest.tools/v3/assert"
11+
is "gotest.tools/v3/assert/cmp"
1112
)
1213

1314
func TestFormat(t *testing.T) {
14-
f := Format("json")
15-
assert.Assert(t, f.IsJSON())
16-
assert.Assert(t, !f.IsTable())
17-
18-
f = Format("table")
19-
assert.Assert(t, !f.IsJSON())
20-
assert.Assert(t, f.IsTable())
15+
tests := []struct {
16+
doc string
17+
f Format
18+
isJSON bool
19+
isTable bool
20+
template string
21+
}{
22+
{
23+
doc: "json format",
24+
f: "json",
25+
isJSON: true,
26+
isTable: false,
27+
template: JSONFormat,
28+
},
29+
{
30+
doc: "empty table format (no template)",
31+
f: "table",
32+
isJSON: false,
33+
isTable: true,
34+
template: "",
35+
},
36+
{
37+
doc: "table with escaped tabs",
38+
f: "table {{.Field}}\\t{{.Field2}}",
39+
isJSON: false,
40+
isTable: true,
41+
template: "{{.Field}}\t{{.Field2}}",
42+
},
43+
{
44+
doc: "table with raw string",
45+
f: `table {{.Field}}\t{{.Field2}}`,
46+
isJSON: false,
47+
isTable: true,
48+
template: "{{.Field}}\t{{.Field2}}",
49+
},
50+
{
51+
doc: "other format",
52+
f: "other",
53+
isJSON: false,
54+
isTable: false,
55+
template: "other",
56+
},
57+
{
58+
doc: "other with spaces",
59+
f: " other ",
60+
isJSON: false,
61+
isTable: false,
62+
template: "other",
63+
},
64+
{
65+
doc: "other with newline preserved",
66+
f: " other\n ",
67+
isJSON: false,
68+
isTable: false,
69+
template: "other\n",
70+
},
71+
}
2172

22-
f = Format("other")
23-
assert.Assert(t, !f.IsJSON())
24-
assert.Assert(t, !f.IsTable())
73+
for _, tc := range tests {
74+
t.Run(tc.doc, func(t *testing.T) {
75+
assert.Check(t, is.Equal(tc.f.IsJSON(), tc.isJSON))
76+
assert.Check(t, is.Equal(tc.f.IsTable(), tc.isTable))
77+
assert.Check(t, is.Equal(tc.f.templateString(), tc.template))
78+
})
79+
}
2580
}
2681

2782
type fakeSubContext struct {

0 commit comments

Comments
 (0)