From de0e883331c85cc7617184900369e1ec9f723b6c Mon Sep 17 00:00:00 2001 From: Jeff Mickey Date: Wed, 21 Jan 2015 15:52:22 -0800 Subject: [PATCH 1/3] docker ps: add fields for ordering and column selection * api/client/ps.go: Refactor CmdPs to use a fields list of characters to determine which columns to print on `docker ps` invocation. This adds an ability for the docker command to print the columns of output in arbitrary order. Signed-off-by: Jeff Mickey Docker-DCO-1.1-Signed-off-by: Jeff Mickey --- api/client/ps.go | 136 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/api/client/ps.go b/api/client/ps.go index 1a96c52d3db97..c95d473b85304 100644 --- a/api/client/ps.go +++ b/api/client/ps.go @@ -38,6 +38,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running") before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name") last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running") + fields = cmd.String([]string{"-fields"}, "cimtspn", "Choose fields to print, and order (c,i,m,t,s,p,n,z)") flFilter = opts.NewListOpts(nil) ) cmd.Require(flag.Exact, 0) @@ -99,13 +100,35 @@ func (cli *DockerCli) CmdPs(args ...string) error { } w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + if *quiet { + *fields = "c" + } + + if *size { + *fields = *fields + "z" + } + if !*quiet { - fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + headermap := map[rune]string{ + 'c': "CONTAINER ID", + 'i': "IMAGE", + 'm': "COMMAND", + 's': "STATUS", + 't': "CREATED", + 'p': "PORTS", + 'n': "NAMES", + 'z': "SIZE", + } - if *size { - fmt.Fprintln(w, "\tSIZE") - } else { - fmt.Fprint(w, "\n") + headers := make([]string, 0) + for _, v := range *fields { + if title, ok := headermap[v]; ok { + headers = append(headers, title) + } + } + + if len(headers) > 0 { + fmt.Fprint(w, strings.Join(headers, "\t")+"\n") } } @@ -117,63 +140,90 @@ func (cli *DockerCli) CmdPs(args ...string) error { return ss } - for _, container := range containers { - ID := container.ID - - if !*noTrunc { - ID = stringid.TruncateID(ID) - } + type containerMeta struct { + c string + i string + m string + t string + s string + p string + n string + z string + } - if *quiet { - fmt.Fprintln(w, ID) + var displayPort string + if container.HostConfig.NetworkMode == "host" { + displayPort = "*/tcp, */udp" + } else { + displayPort = api.DisplayablePorts(container.Ports) + } - continue + outp := make([]containerMeta, 0) + for _, container := range containers { + next := containerMeta{ + c: container.ID, + n: "", + m: strconv.Quote(container.Command), + i: container.Image, + t: units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))) + " ago", + s: container.Status, + p: displayPort, + z: fmt.Sprintf("%s", units.HumanSize(float64(container.SizeRw))), } - var ( - names = stripNamePrefix(container.Names) - command = strconv.Quote(container.Command) - displayPort string - ) - + // handle truncation + outNames := stripNamePrefix(container.Names) if !*noTrunc { - command = stringutils.Truncate(command, 20) - + next.c = stringid.TruncateID(next.c) + next.m = stringutils.Truncate(next.m, 20) // only display the default name for the container with notrunc is passed - for _, name := range names { + for _, name := range outNames { if len(strings.Split(name, "/")) == 1 { - names = []string{name} + outNames = []string{name} break } } } + next.n = strings.Join(outNames, ",") - image := container.Image - if image == "" { - image = "" + if next.i == "" { + next.i = "" } - if container.HostConfig.NetworkMode == "host" { - displayPort = "*/tcp, */udp" - } else { - displayPort = api.DisplayablePorts(container.Ports) + // handle rootfs sizing + if container.SizeRootFs > 0 { + next.z = next.z + fmt.Sprintf(" (virtual %s)", units.HumanSize(float64(container.SizeRootFs))) } - fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", ID, image, command, - units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))), - container.Status, displayPort, strings.Join(names, ",")) + outp = append(outp, next) + } - if *size { - if container.SizeRootFs > 0 { - fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(float64(container.SizeRw)), units.HumanSize(float64(container.SizeRootFs))) - } else { - fmt.Fprintf(w, "%s\n", units.HumanSize(float64(container.SizeRw))) - } + for _, out := range outp { + of := make([]string, 0) + for _, v := range *fields { + switch v { + case 'c': + of = append(of, out.c) + case 'i': + of = append(of, out.i) + case 'm': + of = append(of, out.m) + case 't': + of = append(of, out.t) + case 's': + of = append(of, out.s) + case 'p': + of = append(of, out.p) + case 'n': + of = append(of, out.n) + case 'z': + of = append(of, out.z) - continue + } + } + if len(of) > 0 { + fmt.Fprintf(w, "%s\n", strings.Join(of, "\t")) } - - fmt.Fprint(w, "\n") } if !*quiet { From 37209190c76de66531acba848f1537da2899e32d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 1 May 2015 14:23:27 -0700 Subject: [PATCH 2/3] Docker ps custom formatting. Docker-DCO-1.1-Signed-off-by: David Calavera --- api/client/cli.go | 4 + api/client/ps.go | 149 +++---------------------- api/client/ps/custom.go | 210 +++++++++++++++++++++++++++++++++++ api/client/ps/custom_test.go | 68 ++++++++++++ api/client/ps/formatter.go | 65 +++++++++++ cliconfig/config.go | 1 + cliconfig/config_test.go | 31 ++++++ man/docker-ps.1.md | 41 +++++++ 8 files changed, 435 insertions(+), 134 deletions(-) create mode 100644 api/client/ps/custom.go create mode 100644 api/client/ps/custom_test.go create mode 100644 api/client/ps/formatter.go diff --git a/api/client/cli.go b/api/client/cli.go index 29560c26d7a8a..a8dd276cee52d 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -179,6 +179,10 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { return nil } +func (cli *DockerCli) PsFormat() string { + return cli.configFile.PsFormat +} + // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. // The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config // is set the client scheme will be set to https. diff --git a/api/client/ps.go b/api/client/ps.go index c95d473b85304..97f3207fc4b5b 100644 --- a/api/client/ps.go +++ b/api/client/ps.go @@ -2,21 +2,14 @@ package client import ( "encoding/json" - "fmt" "net/url" "strconv" - "strings" - "text/tabwriter" - "time" - "github.com/docker/docker/api" + "github.com/docker/docker/api/client/ps" "github.com/docker/docker/api/types" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/parsers/filters" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - "github.com/docker/docker/pkg/units" ) // CmdPs outputs a list of Docker containers. @@ -38,7 +31,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running") before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name") last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running") - fields = cmd.String([]string{"-fields"}, "cimtspn", "Choose fields to print, and order (c,i,m,t,s,p,n,z)") + format = cmd.String([]string{"F", "-format"}, "", "Pretty-print containers using a Go template") flFilter = opts.NewListOpts(nil) ) cmd.Require(flag.Exact, 0) @@ -99,136 +92,24 @@ func (cli *DockerCli) CmdPs(args ...string) error { return err } - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) - if *quiet { - *fields = "c" - } - - if *size { - *fields = *fields + "z" - } - - if !*quiet { - headermap := map[rune]string{ - 'c': "CONTAINER ID", - 'i': "IMAGE", - 'm': "COMMAND", - 's': "STATUS", - 't': "CREATED", - 'p': "PORTS", - 'n': "NAMES", - 'z': "SIZE", - } - - headers := make([]string, 0) - for _, v := range *fields { - if title, ok := headermap[v]; ok { - headers = append(headers, title) - } - } - - if len(headers) > 0 { - fmt.Fprint(w, strings.Join(headers, "\t")+"\n") - } - } - - stripNamePrefix := func(ss []string) []string { - for i, s := range ss { - ss[i] = s[1:] - } - - return ss - } - - type containerMeta struct { - c string - i string - m string - t string - s string - p string - n string - z string - } - - var displayPort string - if container.HostConfig.NetworkMode == "host" { - displayPort = "*/tcp, */udp" - } else { - displayPort = api.DisplayablePorts(container.Ports) - } - - outp := make([]containerMeta, 0) - for _, container := range containers { - next := containerMeta{ - c: container.ID, - n: "", - m: strconv.Quote(container.Command), - i: container.Image, - t: units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))) + " ago", - s: container.Status, - p: displayPort, - z: fmt.Sprintf("%s", units.HumanSize(float64(container.SizeRw))), - } - - // handle truncation - outNames := stripNamePrefix(container.Names) - if !*noTrunc { - next.c = stringid.TruncateID(next.c) - next.m = stringutils.Truncate(next.m, 20) - // only display the default name for the container with notrunc is passed - for _, name := range outNames { - if len(strings.Split(name, "/")) == 1 { - outNames = []string{name} - break - } - } + f := *format + if len(f) == 0 { + if len(cli.PsFormat()) > 0 { + f = cli.PsFormat() + } else { + f = "table" } - next.n = strings.Join(outNames, ",") - - if next.i == "" { - next.i = "" - } - - // handle rootfs sizing - if container.SizeRootFs > 0 { - next.z = next.z + fmt.Sprintf(" (virtual %s)", units.HumanSize(float64(container.SizeRootFs))) - } - - outp = append(outp, next) } - for _, out := range outp { - of := make([]string, 0) - for _, v := range *fields { - switch v { - case 'c': - of = append(of, out.c) - case 'i': - of = append(of, out.i) - case 'm': - of = append(of, out.m) - case 't': - of = append(of, out.t) - case 's': - of = append(of, out.s) - case 'p': - of = append(of, out.p) - case 'n': - of = append(of, out.n) - case 'z': - of = append(of, out.z) - - } - } - if len(of) > 0 { - fmt.Fprintf(w, "%s\n", strings.Join(of, "\t")) - } + psCtx := ps.Context{ + Output: cli.out, + Format: f, + Quiet: *quiet, + Size: *size, + Trunc: !*noTrunc, } - if !*quiet { - w.Flush() - } + ps.Format(psCtx, containers) return nil } diff --git a/api/client/ps/custom.go b/api/client/ps/custom.go new file mode 100644 index 0000000000000..17863d4b641c3 --- /dev/null +++ b/api/client/ps/custom.go @@ -0,0 +1,210 @@ +package ps + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/pkg/units" +) + +const ( + tableKey = "table" + + idHeader = "CONTAINER ID" + imageHeader = "IMAGE" + namesHeader = "NAMES" + commandHeader = "COMMAND" + createdAtHeader = "CREATED AT" + runningForHeader = "CREATED" + statusHeader = "STATUS" + portsHeader = "PORTS" + sizeHeader = "SIZE" + labelsHeader = "LABELS" +) + +type containerContext struct { + trunc bool + header []string + c types.Container +} + +func (c *containerContext) ID() string { + c.addHeader(idHeader) + if c.trunc { + return stringid.TruncateID(c.c.ID) + } + return c.c.ID +} + +func (c *containerContext) Names() string { + c.addHeader(namesHeader) + names := stripNamePrefix(c.c.Names) + if c.trunc { + for _, name := range names { + if len(strings.Split(name, "/")) == 1 { + names = []string{name} + break + } + } + } + return strings.Join(names, ",") +} + +func (c *containerContext) Image() string { + c.addHeader(imageHeader) + if c.c.Image == "" { + return "" + } + return c.c.Image +} + +func (c *containerContext) Command() string { + c.addHeader(commandHeader) + command := c.c.Command + if c.trunc { + command = stringutils.Truncate(command, 20) + } + return strconv.Quote(command) +} + +func (c *containerContext) CreatedAt() string { + c.addHeader(createdAtHeader) + return time.Unix(int64(c.c.Created), 0).String() +} + +func (c *containerContext) RunningFor() string { + c.addHeader(runningForHeader) + createdAt := time.Unix(int64(c.c.Created), 0) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) +} + +func (c *containerContext) Ports() string { + c.addHeader(portsHeader) + return api.DisplayablePorts(c.c.Ports) +} + +func (c *containerContext) Status() string { + c.addHeader(statusHeader) + return c.c.Status +} + +func (c *containerContext) Size() string { + c.addHeader(sizeHeader) + srw := units.HumanSize(float64(c.c.SizeRw)) + sv := units.HumanSize(float64(c.c.SizeRootFs)) + + sf := srw + if c.c.SizeRootFs > 0 { + sf = fmt.Sprintf("%s (virtual %s)", srw, sv) + } + return sf +} + +func (c *containerContext) Labels() string { + c.addHeader(labelsHeader) + if c.c.Labels == nil { + return "" + } + + var joinLabels []string + for k, v := range c.c.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *containerContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + c.addHeader(h) + + if c.c.Labels == nil { + return "" + } + return c.c.Labels[name] +} + +func (c *containerContext) fullHeader() string { + if c.header == nil { + return "" + } + return strings.Join(c.header, "\t") +} + +func (c *containerContext) addHeader(header string) { + if c.header == nil { + c.header = []string{} + } + c.header = append(c.header, strings.ToUpper(header)) +} + +func customFormat(ctx Context, containers []types.Container) { + var ( + table bool + header string + format = ctx.Format + buffer = bytes.NewBufferString("") + ) + + if strings.HasPrefix(ctx.Format, tableKey) { + table = true + format = format[len(tableKey):] + } + + format = strings.Trim(format, " ") + r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") + format = r.Replace(format) + + if table && ctx.Size { + format += "\t{{.Size}}" + } + + tmpl, err := template.New("ps template").Parse(format) + if err != nil { + buffer.WriteString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err)) + } + + for _, container := range containers { + containerCtx := &containerContext{ + trunc: ctx.Trunc, + c: container, + } + if err := tmpl.Execute(buffer, containerCtx); err != nil { + buffer = bytes.NewBufferString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err)) + break + } + if table && len(header) == 0 { + header = containerCtx.fullHeader() + } + buffer.WriteString("\n") + } + + if table { + t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0) + t.Write([]byte(header)) + t.Write([]byte("\n")) + buffer.WriteTo(t) + t.Flush() + } else { + buffer.WriteTo(ctx.Output) + } +} + +func stripNamePrefix(ss []string) []string { + for i, s := range ss { + ss[i] = s[1:] + } + + return ss +} diff --git a/api/client/ps/custom_test.go b/api/client/ps/custom_test.go new file mode 100644 index 0000000000000..9882d2bba4c3c --- /dev/null +++ b/api/client/ps/custom_test.go @@ -0,0 +1,68 @@ +package ps + +import ( + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" +) + +func TestContainerContextID(t *testing.T) { + containerId := stringid.GenerateRandomID() + unix := time.Now().Unix() + + var ctx containerContext + cases := []struct { + container types.Container + trunc bool + expValue string + expHeader string + call func() string + }{ + {types.Container{ID: containerId}, true, stringid.TruncateID(containerId), idHeader, ctx.ID}, + {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names}, + {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image}, + {types.Container{Image: ""}, true, "", imageHeader, ctx.Image}, + {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command}, + {types.Container{Created: int(unix)}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + {types.Container{Ports: []types.Port{types.Port{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, + {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, + {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size}, + {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels}, + } + + for _, c := range cases { + ctx = containerContext{c: c.container, trunc: c.trunc} + v := c.call() + if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } + + c := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} + ctx = containerContext{c: c, trunc: true} + + sid := ctx.Label("com.docker.swarm.swarm-id") + node := ctx.Label("com.docker.swarm.node_name") + if sid != "33" { + t.Fatal("Expected 33, was %s\n", sid) + } + + if node != "ubuntu" { + t.Fatal("Expected ubuntu, was %s\n", node) + } + + h := ctx.fullHeader() + if h != "SWARM ID\tNODE NAME" { + t.Fatal("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) + + } + +} diff --git a/api/client/ps/formatter.go b/api/client/ps/formatter.go new file mode 100644 index 0000000000000..9b3bdc812a703 --- /dev/null +++ b/api/client/ps/formatter.go @@ -0,0 +1,65 @@ +package ps + +import ( + "io" + + "github.com/docker/docker/api/types" +) + +const ( + tableFormatKey = "table" + rawFormatKey = "raw" + + defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" + defaultQuietFormat = "{{.ID}}" +) + +type Context struct { + Output io.Writer + Format string + Size bool + Quiet bool + Trunc bool +} + +func Format(ctx Context, containers []types.Container) { + switch ctx.Format { + case tableFormatKey: + tableFormat(ctx, containers) + case rawFormatKey: + rawFormat(ctx, containers) + default: + customFormat(ctx, containers) + } +} + +func rawFormat(ctx Context, containers []types.Container) { + if ctx.Quiet { + ctx.Format = `container_id: {{.ID}}` + } else { + ctx.Format = `container_id: {{.ID}} +image: {{.Image}} +command: {{.Command}} +created_at: {{.CreatedAt}} +status: {{.Status}} +names: {{.Names}} +labels: {{.Labels}} +ports: {{.Ports}} +` + if ctx.Size { + ctx.Format += `size: {{.Size}} +` + } + } + + customFormat(ctx, containers) +} + +func tableFormat(ctx Context, containers []types.Container) { + ctx.Format = defaultTableFormat + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } + + customFormat(ctx, containers) +} diff --git a/cliconfig/config.go b/cliconfig/config.go index 014984218deb5..d00bc716d4421 100644 --- a/cliconfig/config.go +++ b/cliconfig/config.go @@ -57,6 +57,7 @@ type AuthConfig struct { type ConfigFile struct { AuthConfigs map[string]AuthConfig `json:"auths"` HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + PsFormat string `json:"psFormat,omitempty"` filename string // Note: not serialized - for internal use only } diff --git a/cliconfig/config_test.go b/cliconfig/config_test.go index 4ac303a8b169e..93c4519d8bac0 100644 --- a/cliconfig/config_test.go +++ b/cliconfig/config_test.go @@ -155,3 +155,34 @@ func TestNewJson(t *testing.T) { t.Fatalf("Should have save in new form: %s", string(buf)) } } + +func TestJsonWithPsFormat(t *testing.T) { + tmpHome, _ := ioutil.TempDir("", "config-test") + fn := filepath.Join(tmpHome, CONFIGFILE) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + ioutil.WriteFile(fn, []byte(js), 0600) + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { + t.Fatalf("Unknown ps format: %s\n", config.PsFormat) + } + + // Now save it and make sure it shows up in new form + err = config.Save() + if err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE)) + if !strings.Contains(string(buf), `"psFormat":`) || + !strings.Contains(string(buf), "{{.ID}}") { + t.Fatalf("Should have save in new form: %s", string(buf)) + } +} diff --git a/man/docker-ps.1.md b/man/docker-ps.1.md index 6c7b18ee34b1c..d8c0db68d7275 100644 --- a/man/docker-ps.1.md +++ b/man/docker-ps.1.md @@ -16,6 +16,7 @@ docker-ps - List containers [**-q**|**--quiet**[=*false*]] [**-s**|**--size**[=*false*]] [**--since**[=*SINCE*]] +[**-F**|**--format**=*"TEMPLATE"*] # DESCRIPTION @@ -59,6 +60,20 @@ the running containers. **--since**="" Show only containers created since Id or Name, include non-running ones. +**-F**, **--format**=*"TEMPLATE"* + Pretty-print containers using a Go template. + Valid placeholders: + .ID - Container ID + .Image - Image ID + .Command - Quoted command + .CreatedAt - Time when the container was created. + .RunningFor - Elapsed time since the container was started. + .Ports - Exposed ports. + .Status - Container status. + .Size - Container disk size. + .Labels - All labels asigned to the container. + .Label - Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}` + # EXAMPLES # Display all containers, including non-running @@ -82,6 +97,32 @@ the running containers. # docker ps -a -q --filter=name=determined_torvalds c1d3b0166030 +# Display containers with their commands + + # docker ps --format "{{.ID}}: {{.Command}}" + a87ecb4f327c: /bin/sh -c #(nop) MA + 01946d9d34d8: /bin/sh -c #(nop) MA + c1d3b0166030: /bin/sh -c yum -y up + 41d50ecd2f57: /bin/sh -c #(nop) MA + +# Display containers with their labels in a table + + # docker ps --format "table {{.ID}}\t{{.Labels}}" + CONTAINER ID LABELS + a87ecb4f327c com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd + 01946d9d34d8 + c1d3b0166030 com.docker.swarm.node=debian,com.docker.swarm.cpu=6 + 41d50ecd2f57 com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd + +# Display containers with their node label in a table + + # docker ps --format 'table {{.ID}}\t{{(.Label "com.docker.swarm.node")}}' + CONTAINER ID NODE + a87ecb4f327c ubuntu + 01946d9d34d8 + c1d3b0166030 debian + 41d50ecd2f57 fedora + # HISTORY April 2014, Originally compiled by William Henry (whenry at redhat dot com) based on docker.com source material and internal work. From 542b58d8f7a2ff3b78a71b7d2c3145dd79f1fa97 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Fri, 17 Jul 2015 00:03:16 -0400 Subject: [PATCH 3/3] ps --format: Add config.js doc, fix gofmt, add integration tests Re-add the docs from @calavera's PR to the moved cli cmd reference docs. Fix gofmt and vet issues from carried commits Add integration test for using format with --no-trunc and multi-names Fix custom_test map order dependency on expected value check Add docs to reference/commandline/ps.md Remove "-F" flag option from original carried PR content Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- api/client/ps.go | 2 +- api/client/ps/custom_test.go | 32 ++++++++++++++++---- cliconfig/config_test.go | 4 +-- docs/reference/commandline/cli.md | 14 +++++++-- docs/reference/commandline/ps.md | 42 +++++++++++++++++++++++++-- integration-cli/docker_cli_ps_test.go | 31 ++++++++++++++++++++ man/docker-ps.1.md | 4 +-- 7 files changed, 113 insertions(+), 16 deletions(-) diff --git a/api/client/ps.go b/api/client/ps.go index 97f3207fc4b5b..28b9615639511 100644 --- a/api/client/ps.go +++ b/api/client/ps.go @@ -31,7 +31,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running") before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name") last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running") - format = cmd.String([]string{"F", "-format"}, "", "Pretty-print containers using a Go template") + format = cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template") flFilter = opts.NewListOpts(nil) ) cmd.Require(flag.Exact, 0) diff --git a/api/client/ps/custom_test.go b/api/client/ps/custom_test.go index 9882d2bba4c3c..d04c9597db296 100644 --- a/api/client/ps/custom_test.go +++ b/api/client/ps/custom_test.go @@ -1,6 +1,8 @@ package ps import ( + "reflect" + "strings" "testing" "time" @@ -26,7 +28,7 @@ func TestContainerContextID(t *testing.T) { {types.Container{Image: ""}, true, "", imageHeader, ctx.Image}, {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command}, {types.Container{Created: int(unix)}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, - {types.Container{Ports: []types.Port{types.Port{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, + {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size}, {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size}, @@ -36,7 +38,26 @@ func TestContainerContextID(t *testing.T) { for _, c := range cases { ctx = containerContext{c: c.container, trunc: c.trunc} v := c.call() - if v != c.expValue { + if strings.Contains(v, ",") { + // comma-separated values means probably a map input, which won't + // be guaranteed to have the same order as our expected value + // We'll create maps and use reflect.DeepEquals to check instead: + entriesMap := make(map[string]string) + expMap := make(map[string]string) + entries := strings.Split(v, ",") + expectedEntries := strings.Split(c.expValue, ",") + for _, entry := range entries { + keyval := strings.Split(entry, "=") + entriesMap[keyval[0]] = keyval[1] + } + for _, expected := range expectedEntries { + keyval := strings.Split(expected, "=") + expMap[keyval[0]] = keyval[1] + } + if !reflect.DeepEqual(expMap, entriesMap) { + t.Fatalf("Expected entries: %v, got: %v", c.expValue, v) + } + } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } @@ -52,17 +73,16 @@ func TestContainerContextID(t *testing.T) { sid := ctx.Label("com.docker.swarm.swarm-id") node := ctx.Label("com.docker.swarm.node_name") if sid != "33" { - t.Fatal("Expected 33, was %s\n", sid) + t.Fatalf("Expected 33, was %s\n", sid) } if node != "ubuntu" { - t.Fatal("Expected ubuntu, was %s\n", node) + t.Fatalf("Expected ubuntu, was %s\n", node) } h := ctx.fullHeader() if h != "SWARM ID\tNODE NAME" { - t.Fatal("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) + t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) } - } diff --git a/cliconfig/config_test.go b/cliconfig/config_test.go index 93c4519d8bac0..25fb58a45aef9 100644 --- a/cliconfig/config_test.go +++ b/cliconfig/config_test.go @@ -158,7 +158,7 @@ func TestNewJson(t *testing.T) { func TestJsonWithPsFormat(t *testing.T) { tmpHome, _ := ioutil.TempDir("", "config-test") - fn := filepath.Join(tmpHome, CONFIGFILE) + fn := filepath.Join(tmpHome, ConfigFileName) js := `{ "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" @@ -180,7 +180,7 @@ func TestJsonWithPsFormat(t *testing.T) { t.Fatalf("Failed to save: %q", err) } - buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE)) + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) if !strings.Contains(string(buf), `"psFormat":`) || !strings.Contains(string(buf), "{{.ID}}") { t.Fatalf("Should have save in new form: %s", string(buf)) diff --git a/docs/reference/commandline/cli.md b/docs/reference/commandline/cli.md index 69a6e3e31ee7a..c6561cd3651f6 100644 --- a/docs/reference/commandline/cli.md +++ b/docs/reference/commandline/cli.md @@ -85,18 +85,26 @@ mechanisms, you must keep in mind the order of precedence among them. Command line options override environment variables and environment variables override properties you specify in a `config.json` file. -The `config.json` file stores a JSON encoding of a single `HttpHeaders` -property. The property specifies a set of headers to include in all messages +The `config.json` file stores a JSON encoding of several properties: + +The property `HttpHeaders` specifies a set of headers to include in all messages sent from the Docker client to the daemon. Docker does not try to interpret or understand these header; it simply puts them into the messages. Docker does not allow these headers to change any headers it sets for itself. +The property `psFormat` specifies the default format for `docker ps` output. +When the `--format` flag is not provided with the `docker ps` command, +Docker's client uses this property. If this property is not set, the client +falls back to the default table format. For a list of supported formatting +directives, see the [**Formatting** section in the `docker ps` documentation](../ps) + Following is a sample `config.json` file: { "HttpHeaders: { "MyHeader": "MyValue" - } + }, + "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}" } ## Help diff --git a/docs/reference/commandline/ps.md b/docs/reference/commandline/ps.md index 2c68dd73ff5af..9511336fc1cbb 100644 --- a/docs/reference/commandline/ps.md +++ b/docs/reference/commandline/ps.md @@ -24,6 +24,7 @@ weight=1 -q, --quiet=false Only display numeric IDs -s, --size=false Display total file sizes --since="" Show created since Id or Name, include non-running + --format=[] Pretty-print containers using a Go template Running `docker ps --no-trunc` showing 2 linked containers. @@ -60,5 +61,42 @@ The currently supported filters are: This shows all the containers that have exited with status of '0' - - +## Formatting + +The formatting option (`--format`) will pretty-print container output using a Go template. + +Valid placeholders for the Go template are listed below: + +Placeholder | Description +---- | ---- +`.ID` | Container ID +`.Image` | Image ID +`.Command` | Quoted command +`.CreatedAt` | Time when the container was created. +`.RunningFor` | Elapsed time since the container was started. +`.Ports` | Exposed ports. +`.Status` | Container status. +`.Size` | Container disk size. +`.Labels` | All labels asigned to the container. +`.Label` | Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}` + +When using the `--format` option, the `ps` command will either output the data exactly as the template +declares or, when using the `table` directive, will include column headers as well. + +The following example uses a template without headers and outputs the `ID` and `Command` +entries separated by a colon for all running containers: + + $ docker ps --format "{{.ID}}: {{.Command}}" + a87ecb4f327c: /bin/sh -c #(nop) MA + 01946d9d34d8: /bin/sh -c #(nop) MA + c1d3b0166030: /bin/sh -c yum -y up + 41d50ecd2f57: /bin/sh -c #(nop) MA + +To list all running containers with their labels in a table format you can use: + + $ docker ps --format "table {{.ID}}\t{{.Labels}}" + CONTAINER ID LABELS + a87ecb4f327c com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd + 01946d9d34d8 + c1d3b0166030 com.docker.swarm.node=debian,com.docker.swarm.cpu=6 + 41d50ecd2f57 com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go index 7419b28e318b6..e279563f4c207 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/integration-cli/docker_cli_ps_test.go @@ -508,3 +508,34 @@ func (s *DockerSuite) TestPsListContainersFilterCreated(c *check.C) { c.Fatalf("Expected id %s, got %s for filter, out: %s", cID, containerOut, out) } } + +func (s *DockerSuite) TestPsFormatMultiNames(c *check.C) { + //create 2 containers and link them + dockerCmd(c, "run", "--name=child", "-d", "busybox", "top") + dockerCmd(c, "run", "--name=parent", "--link=child:linkedone", "-d", "busybox", "top") + + //use the new format capabilities to only list the names and --no-trunc to get all names + out, _ := dockerCmd(c, "ps", "--format", "{{.Names}}", "--no-trunc") + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + expected := []string{"parent", "child,parent/linkedone"} + var names []string + for _, l := range lines { + names = append(names, l) + } + if !reflect.DeepEqual(expected, names) { + c.Fatalf("Expected array with non-truncated names: %v, got: %v", expected, names) + } + + //now list without turning off truncation and make sure we only get the non-link names + out, _ = dockerCmd(c, "ps", "--format", "{{.Names}}") + lines = strings.Split(strings.TrimSpace(string(out)), "\n") + expected = []string{"parent", "child"} + var truncNames []string + for _, l := range lines { + truncNames = append(truncNames, l) + } + if !reflect.DeepEqual(expected, truncNames) { + c.Fatalf("Expected array with truncated names: %v, got: %v", expected, truncNames) + } + +} diff --git a/man/docker-ps.1.md b/man/docker-ps.1.md index d8c0db68d7275..06c105115cecb 100644 --- a/man/docker-ps.1.md +++ b/man/docker-ps.1.md @@ -16,7 +16,7 @@ docker-ps - List containers [**-q**|**--quiet**[=*false*]] [**-s**|**--size**[=*false*]] [**--since**[=*SINCE*]] -[**-F**|**--format**=*"TEMPLATE"*] +[**--format**=*"TEMPLATE"*] # DESCRIPTION @@ -60,7 +60,7 @@ the running containers. **--since**="" Show only containers created since Id or Name, include non-running ones. -**-F**, **--format**=*"TEMPLATE"* +**--format**=*"TEMPLATE"* Pretty-print containers using a Go template. Valid placeholders: .ID - Container ID