Browse code

Merge pull request #14699 from estesp/docker-ps-format

Carry #10255: Docker ps format

David Calavera authored on 2015/07/23 02:58:34
Showing 11 changed files
... ...
@@ -179,6 +179,10 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
179 179
 	return nil
180 180
 }
181 181
 
182
+func (cli *DockerCli) PsFormat() string {
183
+	return cli.configFile.PsFormat
184
+}
185
+
182 186
 // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
183 187
 // The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config
184 188
 // is set the client scheme will be set to https.
... ...
@@ -2,21 +2,14 @@ package client
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
-	"fmt"
6 5
 	"net/url"
7 6
 	"strconv"
8
-	"strings"
9
-	"text/tabwriter"
10
-	"time"
11 7
 
12
-	"github.com/docker/docker/api"
8
+	"github.com/docker/docker/api/client/ps"
13 9
 	"github.com/docker/docker/api/types"
14 10
 	"github.com/docker/docker/opts"
15 11
 	flag "github.com/docker/docker/pkg/mflag"
16 12
 	"github.com/docker/docker/pkg/parsers/filters"
17
-	"github.com/docker/docker/pkg/stringid"
18
-	"github.com/docker/docker/pkg/stringutils"
19
-	"github.com/docker/docker/pkg/units"
20 13
 )
21 14
 
22 15
 // CmdPs outputs a list of Docker containers.
... ...
@@ -38,6 +31,7 @@ func (cli *DockerCli) CmdPs(args ...string) error {
38 38
 		since    = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running")
39 39
 		before   = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name")
40 40
 		last     = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running")
41
+		format   = cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template")
41 42
 		flFilter = opts.NewListOpts(nil)
42 43
 	)
43 44
 	cmd.Require(flag.Exact, 0)
... ...
@@ -98,87 +92,24 @@ func (cli *DockerCli) CmdPs(args ...string) error {
98 98
 		return err
99 99
 	}
100 100
 
101
-	w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
102
-	if !*quiet {
103
-		fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
104
-
105
-		if *size {
106
-			fmt.Fprintln(w, "\tSIZE")
101
+	f := *format
102
+	if len(f) == 0 {
103
+		if len(cli.PsFormat()) > 0 {
104
+			f = cli.PsFormat()
107 105
 		} else {
108
-			fmt.Fprint(w, "\n")
106
+			f = "table"
109 107
 		}
110 108
 	}
111 109
 
112
-	stripNamePrefix := func(ss []string) []string {
113
-		for i, s := range ss {
114
-			ss[i] = s[1:]
115
-		}
116
-
117
-		return ss
110
+	psCtx := ps.Context{
111
+		Output: cli.out,
112
+		Format: f,
113
+		Quiet:  *quiet,
114
+		Size:   *size,
115
+		Trunc:  !*noTrunc,
118 116
 	}
119 117
 
120
-	for _, container := range containers {
121
-		ID := container.ID
122
-
123
-		if !*noTrunc {
124
-			ID = stringid.TruncateID(ID)
125
-		}
126
-
127
-		if *quiet {
128
-			fmt.Fprintln(w, ID)
129
-
130
-			continue
131
-		}
132
-
133
-		var (
134
-			names       = stripNamePrefix(container.Names)
135
-			command     = strconv.Quote(container.Command)
136
-			displayPort string
137
-		)
138
-
139
-		if !*noTrunc {
140
-			command = stringutils.Truncate(command, 20)
141
-
142
-			// only display the default name for the container with notrunc is passed
143
-			for _, name := range names {
144
-				if len(strings.Split(name, "/")) == 1 {
145
-					names = []string{name}
146
-					break
147
-				}
148
-			}
149
-		}
150
-
151
-		image := container.Image
152
-		if image == "" {
153
-			image = "<no image>"
154
-		}
155
-
156
-		if container.HostConfig.NetworkMode == "host" {
157
-			displayPort = "*/tcp, */udp"
158
-		} else {
159
-			displayPort = api.DisplayablePorts(container.Ports)
160
-		}
161
-
162
-		fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", ID, image, command,
163
-			units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))),
164
-			container.Status, displayPort, strings.Join(names, ","))
165
-
166
-		if *size {
167
-			if container.SizeRootFs > 0 {
168
-				fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(float64(container.SizeRw)), units.HumanSize(float64(container.SizeRootFs)))
169
-			} else {
170
-				fmt.Fprintf(w, "%s\n", units.HumanSize(float64(container.SizeRw)))
171
-			}
172
-
173
-			continue
174
-		}
175
-
176
-		fmt.Fprint(w, "\n")
177
-	}
178
-
179
-	if !*quiet {
180
-		w.Flush()
181
-	}
118
+	ps.Format(psCtx, containers)
182 119
 
183 120
 	return nil
184 121
 }
185 122
new file mode 100644
... ...
@@ -0,0 +1,210 @@
0
+package ps
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strconv"
6
+	"strings"
7
+	"text/tabwriter"
8
+	"text/template"
9
+	"time"
10
+
11
+	"github.com/docker/docker/api"
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/pkg/stringid"
14
+	"github.com/docker/docker/pkg/stringutils"
15
+	"github.com/docker/docker/pkg/units"
16
+)
17
+
18
+const (
19
+	tableKey = "table"
20
+
21
+	idHeader         = "CONTAINER ID"
22
+	imageHeader      = "IMAGE"
23
+	namesHeader      = "NAMES"
24
+	commandHeader    = "COMMAND"
25
+	createdAtHeader  = "CREATED AT"
26
+	runningForHeader = "CREATED"
27
+	statusHeader     = "STATUS"
28
+	portsHeader      = "PORTS"
29
+	sizeHeader       = "SIZE"
30
+	labelsHeader     = "LABELS"
31
+)
32
+
33
+type containerContext struct {
34
+	trunc  bool
35
+	header []string
36
+	c      types.Container
37
+}
38
+
39
+func (c *containerContext) ID() string {
40
+	c.addHeader(idHeader)
41
+	if c.trunc {
42
+		return stringid.TruncateID(c.c.ID)
43
+	}
44
+	return c.c.ID
45
+}
46
+
47
+func (c *containerContext) Names() string {
48
+	c.addHeader(namesHeader)
49
+	names := stripNamePrefix(c.c.Names)
50
+	if c.trunc {
51
+		for _, name := range names {
52
+			if len(strings.Split(name, "/")) == 1 {
53
+				names = []string{name}
54
+				break
55
+			}
56
+		}
57
+	}
58
+	return strings.Join(names, ",")
59
+}
60
+
61
+func (c *containerContext) Image() string {
62
+	c.addHeader(imageHeader)
63
+	if c.c.Image == "" {
64
+		return "<no image>"
65
+	}
66
+	return c.c.Image
67
+}
68
+
69
+func (c *containerContext) Command() string {
70
+	c.addHeader(commandHeader)
71
+	command := c.c.Command
72
+	if c.trunc {
73
+		command = stringutils.Truncate(command, 20)
74
+	}
75
+	return strconv.Quote(command)
76
+}
77
+
78
+func (c *containerContext) CreatedAt() string {
79
+	c.addHeader(createdAtHeader)
80
+	return time.Unix(int64(c.c.Created), 0).String()
81
+}
82
+
83
+func (c *containerContext) RunningFor() string {
84
+	c.addHeader(runningForHeader)
85
+	createdAt := time.Unix(int64(c.c.Created), 0)
86
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
87
+}
88
+
89
+func (c *containerContext) Ports() string {
90
+	c.addHeader(portsHeader)
91
+	return api.DisplayablePorts(c.c.Ports)
92
+}
93
+
94
+func (c *containerContext) Status() string {
95
+	c.addHeader(statusHeader)
96
+	return c.c.Status
97
+}
98
+
99
+func (c *containerContext) Size() string {
100
+	c.addHeader(sizeHeader)
101
+	srw := units.HumanSize(float64(c.c.SizeRw))
102
+	sv := units.HumanSize(float64(c.c.SizeRootFs))
103
+
104
+	sf := srw
105
+	if c.c.SizeRootFs > 0 {
106
+		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
107
+	}
108
+	return sf
109
+}
110
+
111
+func (c *containerContext) Labels() string {
112
+	c.addHeader(labelsHeader)
113
+	if c.c.Labels == nil {
114
+		return ""
115
+	}
116
+
117
+	var joinLabels []string
118
+	for k, v := range c.c.Labels {
119
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
120
+	}
121
+	return strings.Join(joinLabels, ",")
122
+}
123
+
124
+func (c *containerContext) Label(name string) string {
125
+	n := strings.Split(name, ".")
126
+	r := strings.NewReplacer("-", " ", "_", " ")
127
+	h := r.Replace(n[len(n)-1])
128
+
129
+	c.addHeader(h)
130
+
131
+	if c.c.Labels == nil {
132
+		return ""
133
+	}
134
+	return c.c.Labels[name]
135
+}
136
+
137
+func (c *containerContext) fullHeader() string {
138
+	if c.header == nil {
139
+		return ""
140
+	}
141
+	return strings.Join(c.header, "\t")
142
+}
143
+
144
+func (c *containerContext) addHeader(header string) {
145
+	if c.header == nil {
146
+		c.header = []string{}
147
+	}
148
+	c.header = append(c.header, strings.ToUpper(header))
149
+}
150
+
151
+func customFormat(ctx Context, containers []types.Container) {
152
+	var (
153
+		table  bool
154
+		header string
155
+		format = ctx.Format
156
+		buffer = bytes.NewBufferString("")
157
+	)
158
+
159
+	if strings.HasPrefix(ctx.Format, tableKey) {
160
+		table = true
161
+		format = format[len(tableKey):]
162
+	}
163
+
164
+	format = strings.Trim(format, " ")
165
+	r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
166
+	format = r.Replace(format)
167
+
168
+	if table && ctx.Size {
169
+		format += "\t{{.Size}}"
170
+	}
171
+
172
+	tmpl, err := template.New("ps template").Parse(format)
173
+	if err != nil {
174
+		buffer.WriteString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err))
175
+	}
176
+
177
+	for _, container := range containers {
178
+		containerCtx := &containerContext{
179
+			trunc: ctx.Trunc,
180
+			c:     container,
181
+		}
182
+		if err := tmpl.Execute(buffer, containerCtx); err != nil {
183
+			buffer = bytes.NewBufferString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err))
184
+			break
185
+		}
186
+		if table && len(header) == 0 {
187
+			header = containerCtx.fullHeader()
188
+		}
189
+		buffer.WriteString("\n")
190
+	}
191
+
192
+	if table {
193
+		t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0)
194
+		t.Write([]byte(header))
195
+		t.Write([]byte("\n"))
196
+		buffer.WriteTo(t)
197
+		t.Flush()
198
+	} else {
199
+		buffer.WriteTo(ctx.Output)
200
+	}
201
+}
202
+
203
+func stripNamePrefix(ss []string) []string {
204
+	for i, s := range ss {
205
+		ss[i] = s[1:]
206
+	}
207
+
208
+	return ss
209
+}
0 210
new file mode 100644
... ...
@@ -0,0 +1,88 @@
0
+package ps
1
+
2
+import (
3
+	"reflect"
4
+	"strings"
5
+	"testing"
6
+	"time"
7
+
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/docker/docker/pkg/stringid"
10
+)
11
+
12
+func TestContainerContextID(t *testing.T) {
13
+	containerId := stringid.GenerateRandomID()
14
+	unix := time.Now().Unix()
15
+
16
+	var ctx containerContext
17
+	cases := []struct {
18
+		container types.Container
19
+		trunc     bool
20
+		expValue  string
21
+		expHeader string
22
+		call      func() string
23
+	}{
24
+		{types.Container{ID: containerId}, true, stringid.TruncateID(containerId), idHeader, ctx.ID},
25
+		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
26
+		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
27
+		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
28
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
29
+		{types.Container{Created: int(unix)}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
30
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
31
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
32
+		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
33
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
34
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
35
+	}
36
+
37
+	for _, c := range cases {
38
+		ctx = containerContext{c: c.container, trunc: c.trunc}
39
+		v := c.call()
40
+		if strings.Contains(v, ",") {
41
+			// comma-separated values means probably a map input, which won't
42
+			// be guaranteed to have the same order as our expected value
43
+			// We'll create maps and use reflect.DeepEquals to check instead:
44
+			entriesMap := make(map[string]string)
45
+			expMap := make(map[string]string)
46
+			entries := strings.Split(v, ",")
47
+			expectedEntries := strings.Split(c.expValue, ",")
48
+			for _, entry := range entries {
49
+				keyval := strings.Split(entry, "=")
50
+				entriesMap[keyval[0]] = keyval[1]
51
+			}
52
+			for _, expected := range expectedEntries {
53
+				keyval := strings.Split(expected, "=")
54
+				expMap[keyval[0]] = keyval[1]
55
+			}
56
+			if !reflect.DeepEqual(expMap, entriesMap) {
57
+				t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
58
+			}
59
+		} else if v != c.expValue {
60
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
61
+		}
62
+
63
+		h := ctx.fullHeader()
64
+		if h != c.expHeader {
65
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
66
+		}
67
+	}
68
+
69
+	c := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
70
+	ctx = containerContext{c: c, trunc: true}
71
+
72
+	sid := ctx.Label("com.docker.swarm.swarm-id")
73
+	node := ctx.Label("com.docker.swarm.node_name")
74
+	if sid != "33" {
75
+		t.Fatalf("Expected 33, was %s\n", sid)
76
+	}
77
+
78
+	if node != "ubuntu" {
79
+		t.Fatalf("Expected ubuntu, was %s\n", node)
80
+	}
81
+
82
+	h := ctx.fullHeader()
83
+	if h != "SWARM ID\tNODE NAME" {
84
+		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
85
+
86
+	}
87
+}
0 88
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package ps
1
+
2
+import (
3
+	"io"
4
+
5
+	"github.com/docker/docker/api/types"
6
+)
7
+
8
+const (
9
+	tableFormatKey = "table"
10
+	rawFormatKey   = "raw"
11
+
12
+	defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
13
+	defaultQuietFormat = "{{.ID}}"
14
+)
15
+
16
+type Context struct {
17
+	Output io.Writer
18
+	Format string
19
+	Size   bool
20
+	Quiet  bool
21
+	Trunc  bool
22
+}
23
+
24
+func Format(ctx Context, containers []types.Container) {
25
+	switch ctx.Format {
26
+	case tableFormatKey:
27
+		tableFormat(ctx, containers)
28
+	case rawFormatKey:
29
+		rawFormat(ctx, containers)
30
+	default:
31
+		customFormat(ctx, containers)
32
+	}
33
+}
34
+
35
+func rawFormat(ctx Context, containers []types.Container) {
36
+	if ctx.Quiet {
37
+		ctx.Format = `container_id: {{.ID}}`
38
+	} else {
39
+		ctx.Format = `container_id: {{.ID}}
40
+image: {{.Image}}
41
+command: {{.Command}}
42
+created_at: {{.CreatedAt}}
43
+status: {{.Status}}
44
+names: {{.Names}}
45
+labels: {{.Labels}}
46
+ports: {{.Ports}}
47
+`
48
+		if ctx.Size {
49
+			ctx.Format += `size: {{.Size}}
50
+`
51
+		}
52
+	}
53
+
54
+	customFormat(ctx, containers)
55
+}
56
+
57
+func tableFormat(ctx Context, containers []types.Container) {
58
+	ctx.Format = defaultTableFormat
59
+	if ctx.Quiet {
60
+		ctx.Format = defaultQuietFormat
61
+	}
62
+
63
+	customFormat(ctx, containers)
64
+}
... ...
@@ -57,6 +57,7 @@ type AuthConfig struct {
57 57
 type ConfigFile struct {
58 58
 	AuthConfigs map[string]AuthConfig `json:"auths"`
59 59
 	HTTPHeaders map[string]string     `json:"HttpHeaders,omitempty"`
60
+	PsFormat    string                `json:"psFormat,omitempty"`
60 61
 	filename    string                // Note: not serialized - for internal use only
61 62
 }
62 63
 
... ...
@@ -155,3 +155,34 @@ func TestNewJson(t *testing.T) {
155 155
 		t.Fatalf("Should have save in new form: %s", string(buf))
156 156
 	}
157 157
 }
158
+
159
+func TestJsonWithPsFormat(t *testing.T) {
160
+	tmpHome, _ := ioutil.TempDir("", "config-test")
161
+	fn := filepath.Join(tmpHome, ConfigFileName)
162
+	js := `{
163
+		"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
164
+		"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
165
+}`
166
+	ioutil.WriteFile(fn, []byte(js), 0600)
167
+
168
+	config, err := Load(tmpHome)
169
+	if err != nil {
170
+		t.Fatalf("Failed loading on empty json file: %q", err)
171
+	}
172
+
173
+	if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
174
+		t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
175
+	}
176
+
177
+	// Now save it and make sure it shows up in new form
178
+	err = config.Save()
179
+	if err != nil {
180
+		t.Fatalf("Failed to save: %q", err)
181
+	}
182
+
183
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName))
184
+	if !strings.Contains(string(buf), `"psFormat":`) ||
185
+		!strings.Contains(string(buf), "{{.ID}}") {
186
+		t.Fatalf("Should have save in new form: %s", string(buf))
187
+	}
188
+}
... ...
@@ -85,18 +85,26 @@ mechanisms, you must keep in mind the order of precedence among them. Command
85 85
 line options override environment variables and environment variables override
86 86
 properties you specify in a `config.json` file.
87 87
 
88
-The `config.json` file stores a JSON encoding of a single `HttpHeaders`
89
-property. The property specifies a set of headers to include in all messages
88
+The `config.json` file stores a JSON encoding of several properties:
89
+
90
+The property `HttpHeaders` specifies a set of headers to include in all messages
90 91
 sent from the Docker client to the daemon. Docker does not try to interpret or
91 92
 understand these header; it simply puts them into the messages. Docker does
92 93
 not allow these headers to change any headers it sets for itself.
93 94
 
95
+The property `psFormat` specifies the default format for `docker ps` output.
96
+When the `--format` flag is not provided with the `docker ps` command,
97
+Docker's client uses this property. If this property is not set, the client
98
+falls back to the default table format. For a list of supported formatting
99
+directives, see the [**Formatting** section in the `docker ps` documentation](../ps)
100
+
94 101
 Following is a sample `config.json` file:
95 102
 
96 103
     {
97 104
       "HttpHeaders: {
98 105
         "MyHeader": "MyValue"
99
-      }
106
+      },
107
+      "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
100 108
     }
101 109
 
102 110
 ## Help
... ...
@@ -24,6 +24,7 @@ weight=1
24 24
       -q, --quiet=false     Only display numeric IDs
25 25
       -s, --size=false      Display total file sizes
26 26
       --since=""            Show created since Id or Name, include non-running
27
+      --format=[]       Pretty-print containers using a Go template
27 28
 
28 29
 Running `docker ps --no-trunc` showing 2 linked containers.
29 30
 
... ...
@@ -60,5 +61,42 @@ The currently supported filters are:
60 60
 
61 61
 This shows all the containers that have exited with status of '0'
62 62
 
63
-
64
-
63
+## Formatting
64
+
65
+The formatting option (`--format`) will pretty-print container output using a Go template.
66
+
67
+Valid placeholders for the Go template are listed below:
68
+
69
+Placeholder | Description
70
+---- | ----
71
+`.ID` | Container ID
72
+`.Image` | Image ID
73
+`.Command` | Quoted command
74
+`.CreatedAt` | Time when the container was created.
75
+`.RunningFor` | Elapsed time since the container was started.
76
+`.Ports` | Exposed ports.
77
+`.Status` | Container status.
78
+`.Size` | Container disk size.
79
+`.Labels` | All labels asigned to the container.
80
+`.Label` | Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}`
81
+
82
+When using the `--format` option, the `ps` command will either output the data exactly as the template
83
+declares or, when using the `table` directive, will include column headers as well.
84
+
85
+The following example uses a template without headers and outputs the `ID` and `Command`
86
+entries separated by a colon for all running containers:
87
+
88
+    $ docker ps --format "{{.ID}}: {{.Command}}"
89
+    a87ecb4f327c: /bin/sh -c #(nop) MA
90
+    01946d9d34d8: /bin/sh -c #(nop) MA
91
+    c1d3b0166030: /bin/sh -c yum -y up
92
+    41d50ecd2f57: /bin/sh -c #(nop) MA
93
+
94
+To list all running containers with their labels in a table format you can use:
95
+
96
+    $ docker ps --format "table {{.ID}}\t{{.Labels}}"
97
+    CONTAINER ID        LABELS
98
+    a87ecb4f327c        com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd
99
+    01946d9d34d8
100
+    c1d3b0166030        com.docker.swarm.node=debian,com.docker.swarm.cpu=6
101
+    41d50ecd2f57        com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd
... ...
@@ -508,3 +508,34 @@ func (s *DockerSuite) TestPsListContainersFilterCreated(c *check.C) {
508 508
 		c.Fatalf("Expected id %s, got %s for filter, out: %s", cID, containerOut, out)
509 509
 	}
510 510
 }
511
+
512
+func (s *DockerSuite) TestPsFormatMultiNames(c *check.C) {
513
+	//create 2 containers and link them
514
+	dockerCmd(c, "run", "--name=child", "-d", "busybox", "top")
515
+	dockerCmd(c, "run", "--name=parent", "--link=child:linkedone", "-d", "busybox", "top")
516
+
517
+	//use the new format capabilities to only list the names and --no-trunc to get all names
518
+	out, _ := dockerCmd(c, "ps", "--format", "{{.Names}}", "--no-trunc")
519
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
520
+	expected := []string{"parent", "child,parent/linkedone"}
521
+	var names []string
522
+	for _, l := range lines {
523
+		names = append(names, l)
524
+	}
525
+	if !reflect.DeepEqual(expected, names) {
526
+		c.Fatalf("Expected array with non-truncated names: %v, got: %v", expected, names)
527
+	}
528
+
529
+	//now list without turning off truncation and make sure we only get the non-link names
530
+	out, _ = dockerCmd(c, "ps", "--format", "{{.Names}}")
531
+	lines = strings.Split(strings.TrimSpace(string(out)), "\n")
532
+	expected = []string{"parent", "child"}
533
+	var truncNames []string
534
+	for _, l := range lines {
535
+		truncNames = append(truncNames, l)
536
+	}
537
+	if !reflect.DeepEqual(expected, truncNames) {
538
+		c.Fatalf("Expected array with truncated names: %v, got: %v", expected, truncNames)
539
+	}
540
+
541
+}
... ...
@@ -16,6 +16,7 @@ docker-ps - List containers
16 16
 [**-q**|**--quiet**[=*false*]]
17 17
 [**-s**|**--size**[=*false*]]
18 18
 [**--since**[=*SINCE*]]
19
+[**--format**=*"TEMPLATE"*]
19 20
 
20 21
 
21 22
 # DESCRIPTION
... ...
@@ -59,6 +60,20 @@ the running containers.
59 59
 **--since**=""
60 60
    Show only containers created since Id or Name, include non-running ones.
61 61
 
62
+**--format**=*"TEMPLATE"*
63
+   Pretty-print containers using a Go template.
64
+   Valid placeholders:
65
+      .ID - Container ID
66
+      .Image - Image ID
67
+      .Command - Quoted command
68
+      .CreatedAt - Time when the container was created.
69
+      .RunningFor - Elapsed time since the container was started.
70
+      .Ports - Exposed ports.
71
+      .Status - Container status.
72
+      .Size - Container disk size.
73
+      .Labels - All labels asigned to the container.
74
+      .Label - Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}`
75
+
62 76
 # EXAMPLES
63 77
 # Display all containers, including non-running
64 78
 
... ...
@@ -82,6 +97,32 @@ the running containers.
82 82
     # docker ps -a -q --filter=name=determined_torvalds
83 83
     c1d3b0166030
84 84
 
85
+# Display containers with their commands
86
+
87
+    # docker ps --format "{{.ID}}: {{.Command}}"
88
+    a87ecb4f327c: /bin/sh -c #(nop) MA
89
+    01946d9d34d8: /bin/sh -c #(nop) MA
90
+    c1d3b0166030: /bin/sh -c yum -y up
91
+    41d50ecd2f57: /bin/sh -c #(nop) MA
92
+
93
+# Display containers with their labels in a table
94
+
95
+    # docker ps --format "table {{.ID}}\t{{.Labels}}"
96
+    CONTAINER ID        LABELS
97
+    a87ecb4f327c        com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd
98
+    01946d9d34d8
99
+    c1d3b0166030        com.docker.swarm.node=debian,com.docker.swarm.cpu=6
100
+    41d50ecd2f57        com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd
101
+
102
+# Display containers with their node label in a table
103
+
104
+    # docker ps --format 'table {{.ID}}\t{{(.Label "com.docker.swarm.node")}}'
105
+    CONTAINER ID        NODE
106
+    a87ecb4f327c        ubuntu
107
+    01946d9d34d8
108
+    c1d3b0166030        debian
109
+    41d50ecd2f57        fedora
110
+
85 111
 # HISTORY
86 112
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
87 113
 based on docker.com source material and internal work.