Skip to content

Commit 95071bf

Browse files
authored
Merge pull request #319 from AkihiroSuda/more-format
nerdctl (ps|images|volume ls|network ls|version) --format
2 parents d97c5cd + ee13c58 commit 95071bf

File tree

9 files changed

+469
-84
lines changed

9 files changed

+469
-84
lines changed

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,9 @@ Flags:
480480
- :whale: `-a, --all`: Show all containers (default shows just running)
481481
- :whale: `--no-trunc`: Don't truncate output
482482
- :whale: `-q, --quiet`: Only display container IDs
483+
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
483484

484-
Unimplemented `docker ps` flags: `--filter`, `--format`, `--last`, `--size`
485+
Unimplemented `docker ps` flags: `--filter`, `--last`, `--size`
485486

486487
### :whale: nerdctl inspect
487488
Display detailed information on one or more containers.
@@ -604,8 +605,9 @@ Usage: `nerdctl images [OPTIONS] [REPOSITORY[:TAG]]`
604605
Flags:
605606
- :whale: `-q, --quiet`: Only show numeric IDs
606607
- :whale: `--no-trunc`: Don't truncate output
608+
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
607609

608-
Unimplemented `docker images` flags: `--all`, `--digests`, `--filter`, `--format`
610+
Unimplemented `docker images` flags: `--all`, `--digests`, `--filter`
609611

610612
### :whale: nerdctl pull
611613
Pull an image from a registry.
@@ -717,7 +719,11 @@ List networks
717719

718720
Usage: `nerdctl network ls [OPTIONS]`
719721

720-
Unimplemented `docker network ls` flags: `--filter`, `--format`, `--no-trunc`, `--quiet`
722+
Flags:
723+
- :whale: `-q, --quiet`: Only display network IDs
724+
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
725+
726+
Unimplemented `docker network ls` flags: `--filter`, `--no-trunc`
721727

722728
### :whale: nerdctl network inspect
723729
Display detailed information on one or more networks
@@ -749,8 +755,9 @@ Usage: `nerdctl volume ls [OPTIONS]`
749755

750756
Flags:
751757
- :whale: `-q, --quiet`: Only display volume names
758+
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
752759

753-
Unimplemented `docker volume ls` flags: `--filter`, `--format`
760+
Unimplemented `docker volume ls` flags: `--filter`
754761

755762
### :whale: nerdctl volume inspect
756763
Display detailed information on one or more volumes
@@ -800,7 +807,8 @@ Show the nerdctl version information
800807

801808
Usage: `nerdctl version [OPTIONS]`
802809

803-
Unimplemented `docker version` flags: `--format`
810+
Flags:
811+
- :whale: `-f, --format`: Format the output using the given Go template, e.g, `{{json .}}`
804812

805813
## Stats
806814
### :whale: nerdctl top

cmd/nerdctl/fmtutil.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
// Flusher is implemented by text/tabwriter.Writer
20+
type Flusher interface {
21+
Flush() error
22+
}
23+
24+
func formatLabels(labels map[string]string) string {
25+
var res string
26+
for k, v := range labels {
27+
s := k + "=" + v
28+
if res == "" {
29+
res = s
30+
} else {
31+
res += "," + s
32+
}
33+
}
34+
return res
35+
}

cmd/nerdctl/images.go

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package main
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"fmt"
2223
"strings"
2324
"text/tabwriter"
25+
"text/template"
26+
"time"
2427

2528
"github.com/containerd/containerd"
2629
"github.com/containerd/containerd/content"
@@ -29,6 +32,7 @@ import (
2932
refdocker "github.com/containerd/containerd/reference/docker"
3033
"github.com/containerd/containerd/snapshots"
3134
"github.com/containerd/nerdctl/pkg/imgutil"
35+
"github.com/docker/cli/templates"
3236
"github.com/opencontainers/image-spec/identity"
3337
"github.com/pkg/errors"
3438
"github.com/sirupsen/logrus"
@@ -50,6 +54,11 @@ var imagesCommand = &cli.Command{
5054
Name: "no-trunc",
5155
Usage: "Don't truncate output",
5256
},
57+
&cli.StringFlag{
58+
Name: "format",
59+
// Alias "-f" is reserved for "--filter"
60+
Usage: "Format the output using the given Go template, e.g, '{{json .}}'",
61+
},
5362
},
5463
}
5564

@@ -87,13 +96,40 @@ func imagesAction(clicontext *cli.Context) error {
8796
return printImages(ctx, clicontext, client, imageList, cs)
8897
}
8998

99+
type imagePrintable struct {
100+
// TODO: "Containers"
101+
CreatedAt string
102+
CreatedSince string
103+
// TODO: "Digest" (only when --digests is set)
104+
ID string
105+
Repository string
106+
Tag string
107+
Size string
108+
// TODO: "SharedSize", "UniqueSize", "VirtualSize"
109+
}
110+
90111
func printImages(ctx context.Context, clicontext *cli.Context, client *containerd.Client, imageList []images.Image, cs content.Store) error {
91112
quiet := clicontext.Bool("quiet")
92113
noTrunc := clicontext.Bool("no-trunc")
93-
94-
w := tabwriter.NewWriter(clicontext.App.Writer, 4, 8, 4, ' ', 0)
95-
if !quiet {
96-
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE")
114+
w := clicontext.App.Writer
115+
var tmpl *template.Template
116+
switch format := clicontext.String("format"); format {
117+
case "", "table":
118+
w = tabwriter.NewWriter(clicontext.App.Writer, 4, 8, 4, ' ', 0)
119+
if !quiet {
120+
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE")
121+
}
122+
case "raw":
123+
return errors.New("unsupported format: \"raw\"")
124+
default:
125+
if quiet {
126+
return errors.New("format and quiet must not be specified together")
127+
}
128+
var err error
129+
tmpl, err = templates.Parse(format)
130+
if err != nil {
131+
return err
132+
}
97133
}
98134

99135
s := client.SnapshotService(clicontext.String("snapshotter"))
@@ -106,34 +142,48 @@ func printImages(ctx context.Context, clicontext *cli.Context, client *container
106142
}
107143
repository, tag := imgutil.ParseRepoTag(img.Name)
108144

109-
var digest string
145+
p := imagePrintable{
146+
CreatedAt: img.CreatedAt.Round(time.Second).Local().String(), // format like "2021-08-07 02:19:45 +0900 JST"
147+
CreatedSince: timeSinceInHuman(img.CreatedAt),
148+
ID: img.Target.Digest.String(),
149+
Repository: repository,
150+
Tag: tag,
151+
Size: progress.Bytes(size).String(),
152+
}
110153
if !noTrunc {
111-
digest = strings.Split(img.Target.Digest.String(), ":")[1][:12]
112-
} else {
113-
digest = img.Target.Digest.String()
154+
p.ID = strings.Split(img.Target.Digest.String(), ":")[1][:12]
114155
}
115-
116-
if quiet {
117-
if _, err := fmt.Fprintf(w, "%s\n", digest); err != nil {
156+
if tmpl != nil {
157+
var b bytes.Buffer
158+
if err := tmpl.Execute(&b, p); err != nil {
159+
return err
160+
}
161+
if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil {
162+
return err
163+
}
164+
} else if quiet {
165+
if _, err := fmt.Fprintf(w, "%s\n", p.ID); err != nil {
166+
return err
167+
}
168+
} else {
169+
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
170+
repository,
171+
tag,
172+
p.ID,
173+
p.CreatedSince,
174+
p.Size,
175+
); err != nil {
118176
return err
119177
}
120-
continue
121-
}
122-
123-
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
124-
repository,
125-
tag,
126-
digest,
127-
timeSinceInHuman(img.CreatedAt),
128-
progress.Bytes(size),
129-
); err != nil {
130-
return err
131178
}
132179
}
133180
if len(errs) > 0 {
134181
logrus.Warn("failed to compute image(s) size")
135182
}
136-
return w.Flush()
183+
if f, ok := w.(Flusher); ok {
184+
return f.Flush()
185+
}
186+
return nil
137187
}
138188

139189
func imagesBashComplete(clicontext *cli.Context) {

cmd/nerdctl/network_ls.go

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
package main
1818

1919
import (
20+
"bytes"
2021
"fmt"
2122
"strconv"
2223
"text/tabwriter"
24+
"text/template"
2325

2426
"github.com/containerd/nerdctl/pkg/netutil"
27+
"github.com/docker/cli/templates"
28+
"github.com/pkg/errors"
2529
"github.com/urfave/cli/v2"
2630
)
2731

@@ -30,9 +34,51 @@ var networkLsCommand = &cli.Command{
3034
Aliases: []string{"list"},
3135
Usage: "List networks",
3236
Action: networkLsAction,
37+
Flags: []cli.Flag{
38+
&cli.BoolFlag{
39+
Name: "quiet",
40+
Aliases: []string{"q"},
41+
Usage: "Only display network IDs",
42+
},
43+
&cli.StringFlag{
44+
Name: "format",
45+
// Alias "-f" is reserved for "--filter"
46+
Usage: "Format the output using the given Go template, e.g, '{{json .}}'",
47+
},
48+
},
49+
}
50+
51+
type networkPrintable struct {
52+
ID string // empty for non-nerdctl networks
53+
Name string
54+
Labels string
55+
// TODO: "CreatedAt", "Driver", "IPv6", "Internal", "Scope"
56+
file string `json:"-"`
3357
}
3458

3559
func networkLsAction(clicontext *cli.Context) error {
60+
quiet := clicontext.Bool("quiet")
61+
w := clicontext.App.Writer
62+
var tmpl *template.Template
63+
switch format := clicontext.String("format"); format {
64+
case "", "table":
65+
w = tabwriter.NewWriter(clicontext.App.Writer, 4, 8, 4, ' ', 0)
66+
if !quiet {
67+
fmt.Fprintln(w, "NETWORK ID\tNAME\tFILE")
68+
}
69+
case "raw":
70+
return errors.New("unsupported format: \"raw\"")
71+
default:
72+
if quiet {
73+
return errors.New("format and quiet must not be specified together")
74+
}
75+
var err error
76+
tmpl, err = templates.Parse(format)
77+
if err != nil {
78+
return err
79+
}
80+
}
81+
3682
e := &netutil.CNIEnv{
3783
Path: clicontext.String("cni-path"),
3884
NetconfPath: clicontext.String("cni-netconfpath"),
@@ -41,17 +87,50 @@ func networkLsAction(clicontext *cli.Context) error {
4187
if err != nil {
4288
return err
4389
}
44-
w := tabwriter.NewWriter(clicontext.App.Writer, 4, 8, 4, ' ', 0)
45-
fmt.Fprintln(w, "NETWORK ID\tNAME\tFILE")
46-
for _, l := range ll {
47-
var idStr string
90+
pp := make([]networkPrintable, len(ll))
91+
for i, l := range ll {
92+
p := networkPrintable{
93+
Name: l.Name,
94+
file: l.File,
95+
}
4896
if l.NerdctlID != nil {
49-
idStr = strconv.Itoa(*l.NerdctlID)
97+
p.ID = strconv.Itoa(*l.NerdctlID)
98+
}
99+
if l.NerdctlLabels != nil {
100+
p.Labels = formatLabels(*l.NerdctlLabels)
50101
}
51-
fmt.Fprintf(w, "%s\t%s\t%s\n", idStr, l.Name, l.File)
102+
pp[i] = p
103+
}
104+
105+
// append pseudo networks
106+
pp = append(pp, []networkPrintable{
107+
{
108+
Name: "host",
109+
},
110+
{
111+
Name: "none",
112+
},
113+
}...)
114+
115+
for _, p := range pp {
116+
if tmpl != nil {
117+
var b bytes.Buffer
118+
if err := tmpl.Execute(&b, p); err != nil {
119+
return err
120+
}
121+
if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil {
122+
return err
123+
}
124+
} else if quiet {
125+
if p.ID != "" {
126+
fmt.Fprintln(w, p.ID)
127+
}
128+
} else {
129+
fmt.Fprintf(w, "%s\t%s\t%s\n", p.ID, p.Name, p.file)
130+
}
131+
}
132+
if f, ok := w.(Flusher); ok {
133+
return f.Flush()
52134
}
53-
// pseudo networks
54-
fmt.Fprintf(w, "\thost\t\n")
55-
fmt.Fprintf(w, "\tnone\t\n")
56-
return w.Flush()
135+
return nil
57136
}

0 commit comments

Comments
 (0)