Browse code

Add --format support to images command

- rename `api/client/ps` to `api/client/formatter`
- add a a image formatter

Signed-off-by: Vincent Demeester <vincent@sbr.pm>

Vincent Demeester authored on 2015/12/18 22:03:41
Showing 19 changed files
... ...
@@ -68,11 +68,17 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
68 68
 }
69 69
 
70 70
 // PsFormat returns the format string specified in the configuration.
71
-// String contains columns and format specification, for example {{ID}\t{{Name}}.
71
+// String contains columns and format specification, for example {{ID}}\t{{Name}}.
72 72
 func (cli *DockerCli) PsFormat() string {
73 73
 	return cli.configFile.PsFormat
74 74
 }
75 75
 
76
+// ImagesFormat returns the format string specified in the configuration.
77
+// String contains columns and format specification, for example {{ID}}\t{{Name}}.
78
+func (cli *DockerCli) ImagesFormat() string {
79
+	return cli.configFile.ImagesFormat
80
+}
81
+
76 82
 // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
77 83
 // The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config
78 84
 // is set the client scheme will be set to https.
79 85
new file mode 100644
... ...
@@ -0,0 +1,222 @@
0
+package formatter
1
+
2
+import (
3
+	"fmt"
4
+	"strconv"
5
+	"strings"
6
+	"time"
7
+
8
+	"github.com/docker/docker/api"
9
+	"github.com/docker/docker/api/types"
10
+	"github.com/docker/docker/pkg/stringid"
11
+	"github.com/docker/docker/pkg/stringutils"
12
+	"github.com/docker/go-units"
13
+)
14
+
15
+const (
16
+	tableKey = "table"
17
+
18
+	containerIDHeader  = "CONTAINER ID"
19
+	imageHeader        = "IMAGE"
20
+	namesHeader        = "NAMES"
21
+	commandHeader      = "COMMAND"
22
+	createdSinceHeader = "CREATED"
23
+	createdAtHeader    = "CREATED AT"
24
+	runningForHeader   = "CREATED"
25
+	statusHeader       = "STATUS"
26
+	portsHeader        = "PORTS"
27
+	sizeHeader         = "SIZE"
28
+	labelsHeader       = "LABELS"
29
+	imageIDHeader      = "IMAGE ID"
30
+	repositoryHeader   = "REPOSITORY"
31
+	tagHeader          = "TAG"
32
+	digestHeader       = "DIGEST"
33
+)
34
+
35
+type containerContext struct {
36
+	baseSubContext
37
+	trunc bool
38
+	c     types.Container
39
+}
40
+
41
+func (c *containerContext) ID() string {
42
+	c.addHeader(containerIDHeader)
43
+	if c.trunc {
44
+		return stringid.TruncateID(c.c.ID)
45
+	}
46
+	return c.c.ID
47
+}
48
+
49
+func (c *containerContext) Names() string {
50
+	c.addHeader(namesHeader)
51
+	names := stripNamePrefix(c.c.Names)
52
+	if c.trunc {
53
+		for _, name := range names {
54
+			if len(strings.Split(name, "/")) == 1 {
55
+				names = []string{name}
56
+				break
57
+			}
58
+		}
59
+	}
60
+	return strings.Join(names, ",")
61
+}
62
+
63
+func (c *containerContext) Image() string {
64
+	c.addHeader(imageHeader)
65
+	if c.c.Image == "" {
66
+		return "<no image>"
67
+	}
68
+	if c.trunc {
69
+		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
70
+			return trunc
71
+		}
72
+	}
73
+	return c.c.Image
74
+}
75
+
76
+func (c *containerContext) Command() string {
77
+	c.addHeader(commandHeader)
78
+	command := c.c.Command
79
+	if c.trunc {
80
+		command = stringutils.Truncate(command, 20)
81
+	}
82
+	return strconv.Quote(command)
83
+}
84
+
85
+func (c *containerContext) CreatedAt() string {
86
+	c.addHeader(createdAtHeader)
87
+	return time.Unix(int64(c.c.Created), 0).String()
88
+}
89
+
90
+func (c *containerContext) RunningFor() string {
91
+	c.addHeader(runningForHeader)
92
+	createdAt := time.Unix(int64(c.c.Created), 0)
93
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
94
+}
95
+
96
+func (c *containerContext) Ports() string {
97
+	c.addHeader(portsHeader)
98
+	return api.DisplayablePorts(c.c.Ports)
99
+}
100
+
101
+func (c *containerContext) Status() string {
102
+	c.addHeader(statusHeader)
103
+	return c.c.Status
104
+}
105
+
106
+func (c *containerContext) Size() string {
107
+	c.addHeader(sizeHeader)
108
+	srw := units.HumanSize(float64(c.c.SizeRw))
109
+	sv := units.HumanSize(float64(c.c.SizeRootFs))
110
+
111
+	sf := srw
112
+	if c.c.SizeRootFs > 0 {
113
+		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
114
+	}
115
+	return sf
116
+}
117
+
118
+func (c *containerContext) Labels() string {
119
+	c.addHeader(labelsHeader)
120
+	if c.c.Labels == nil {
121
+		return ""
122
+	}
123
+
124
+	var joinLabels []string
125
+	for k, v := range c.c.Labels {
126
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
127
+	}
128
+	return strings.Join(joinLabels, ",")
129
+}
130
+
131
+func (c *containerContext) Label(name string) string {
132
+	n := strings.Split(name, ".")
133
+	r := strings.NewReplacer("-", " ", "_", " ")
134
+	h := r.Replace(n[len(n)-1])
135
+
136
+	c.addHeader(h)
137
+
138
+	if c.c.Labels == nil {
139
+		return ""
140
+	}
141
+	return c.c.Labels[name]
142
+}
143
+
144
+type imageContext struct {
145
+	baseSubContext
146
+	trunc  bool
147
+	i      types.Image
148
+	repo   string
149
+	tag    string
150
+	digest string
151
+}
152
+
153
+func (c *imageContext) ID() string {
154
+	c.addHeader(imageIDHeader)
155
+	if c.trunc {
156
+		return stringid.TruncateID(c.i.ID)
157
+	}
158
+	return c.i.ID
159
+}
160
+
161
+func (c *imageContext) Repository() string {
162
+	c.addHeader(repositoryHeader)
163
+	return c.repo
164
+}
165
+
166
+func (c *imageContext) Tag() string {
167
+	c.addHeader(tagHeader)
168
+	return c.tag
169
+}
170
+
171
+func (c *imageContext) Digest() string {
172
+	c.addHeader(digestHeader)
173
+	return c.digest
174
+}
175
+
176
+func (c *imageContext) CreatedSince() string {
177
+	c.addHeader(createdSinceHeader)
178
+	createdAt := time.Unix(int64(c.i.Created), 0)
179
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
180
+}
181
+
182
+func (c *imageContext) CreatedAt() string {
183
+	c.addHeader(createdAtHeader)
184
+	return time.Unix(int64(c.i.Created), 0).String()
185
+}
186
+
187
+func (c *imageContext) Size() string {
188
+	c.addHeader(sizeHeader)
189
+	return units.HumanSize(float64(c.i.Size))
190
+}
191
+
192
+type subContext interface {
193
+	fullHeader() string
194
+	addHeader(header string)
195
+}
196
+
197
+type baseSubContext struct {
198
+	header []string
199
+}
200
+
201
+func (c *baseSubContext) fullHeader() string {
202
+	if c.header == nil {
203
+		return ""
204
+	}
205
+	return strings.Join(c.header, "\t")
206
+}
207
+
208
+func (c *baseSubContext) addHeader(header string) {
209
+	if c.header == nil {
210
+		c.header = []string{}
211
+	}
212
+	c.header = append(c.header, strings.ToUpper(header))
213
+}
214
+
215
+func stripNamePrefix(ss []string) []string {
216
+	for i, s := range ss {
217
+		ss[i] = s[1:]
218
+	}
219
+
220
+	return ss
221
+}
0 222
new file mode 100644
... ...
@@ -0,0 +1,192 @@
0
+package formatter
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 TestContainerPsContext(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), containerIDHeader, ctx.ID},
25
+		{types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
26
+		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
27
+		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
28
+		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
29
+		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
30
+		{types.Container{
31
+			Image:   "a5a665ff33eced1e0803148700880edab4",
32
+			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
33
+		},
34
+			true,
35
+			"a5a665ff33ec",
36
+			imageHeader,
37
+			ctx.Image,
38
+		},
39
+		{types.Container{
40
+			Image:   "a5a665ff33eced1e0803148700880edab4",
41
+			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
42
+		},
43
+			false,
44
+			"a5a665ff33eced1e0803148700880edab4",
45
+			imageHeader,
46
+			ctx.Image,
47
+		},
48
+		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
49
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
50
+		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
51
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
52
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
53
+		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
54
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
55
+		{types.Container{}, true, "", labelsHeader, ctx.Labels},
56
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
57
+		{types.Container{Created: unix}, true, "Less than a second", runningForHeader, ctx.RunningFor},
58
+	}
59
+
60
+	for _, c := range cases {
61
+		ctx = containerContext{c: c.container, trunc: c.trunc}
62
+		v := c.call()
63
+		if strings.Contains(v, ",") {
64
+			compareMultipleValues(t, v, c.expValue)
65
+		} else if v != c.expValue {
66
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
67
+		}
68
+
69
+		h := ctx.fullHeader()
70
+		if h != c.expHeader {
71
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
72
+		}
73
+	}
74
+
75
+	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
76
+	ctx = containerContext{c: c1, trunc: true}
77
+
78
+	sid := ctx.Label("com.docker.swarm.swarm-id")
79
+	node := ctx.Label("com.docker.swarm.node_name")
80
+	if sid != "33" {
81
+		t.Fatalf("Expected 33, was %s\n", sid)
82
+	}
83
+
84
+	if node != "ubuntu" {
85
+		t.Fatalf("Expected ubuntu, was %s\n", node)
86
+	}
87
+
88
+	h := ctx.fullHeader()
89
+	if h != "SWARM ID\tNODE NAME" {
90
+		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
91
+
92
+	}
93
+
94
+	c2 := types.Container{}
95
+	ctx = containerContext{c: c2, trunc: true}
96
+
97
+	label := ctx.Label("anything.really")
98
+	if label != "" {
99
+		t.Fatalf("Expected an empty string, was %s", label)
100
+	}
101
+
102
+	ctx = containerContext{c: c2, trunc: true}
103
+	fullHeader := ctx.fullHeader()
104
+	if fullHeader != "" {
105
+		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
106
+	}
107
+
108
+}
109
+
110
+func TestImagesContext(t *testing.T) {
111
+	imageID := stringid.GenerateRandomID()
112
+	unix := time.Now().Unix()
113
+
114
+	var ctx imageContext
115
+	cases := []struct {
116
+		imageCtx  imageContext
117
+		expValue  string
118
+		expHeader string
119
+		call      func() string
120
+	}{
121
+		{imageContext{
122
+			i:     types.Image{ID: imageID},
123
+			trunc: true,
124
+		}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
125
+		{imageContext{
126
+			i:     types.Image{ID: imageID},
127
+			trunc: false,
128
+		}, imageID, imageIDHeader, ctx.ID},
129
+		{imageContext{
130
+			i:     types.Image{Size: 10},
131
+			trunc: true,
132
+		}, "10 B", sizeHeader, ctx.Size},
133
+		{imageContext{
134
+			i:     types.Image{Created: unix},
135
+			trunc: true,
136
+		}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
137
+		// FIXME
138
+		// {imageContext{
139
+		// 	i:     types.Image{Created: unix},
140
+		// 	trunc: true,
141
+		// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
142
+		{imageContext{
143
+			i:    types.Image{},
144
+			repo: "busybox",
145
+		}, "busybox", repositoryHeader, ctx.Repository},
146
+		{imageContext{
147
+			i:   types.Image{},
148
+			tag: "latest",
149
+		}, "latest", tagHeader, ctx.Tag},
150
+		{imageContext{
151
+			i:      types.Image{},
152
+			digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
153
+		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
154
+	}
155
+
156
+	for _, c := range cases {
157
+		ctx = c.imageCtx
158
+		v := c.call()
159
+		if strings.Contains(v, ",") {
160
+			compareMultipleValues(t, v, c.expValue)
161
+		} else if v != c.expValue {
162
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
163
+		}
164
+
165
+		h := ctx.fullHeader()
166
+		if h != c.expHeader {
167
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
168
+		}
169
+	}
170
+}
171
+
172
+func compareMultipleValues(t *testing.T, value, expected string) {
173
+	// comma-separated values means probably a map input, which won't
174
+	// be guaranteed to have the same order as our expected value
175
+	// We'll create maps and use reflect.DeepEquals to check instead:
176
+	entriesMap := make(map[string]string)
177
+	expMap := make(map[string]string)
178
+	entries := strings.Split(value, ",")
179
+	expectedEntries := strings.Split(expected, ",")
180
+	for _, entry := range entries {
181
+		keyval := strings.Split(entry, "=")
182
+		entriesMap[keyval[0]] = keyval[1]
183
+	}
184
+	for _, expected := range expectedEntries {
185
+		keyval := strings.Split(expected, "=")
186
+		expMap[keyval[0]] = keyval[1]
187
+	}
188
+	if !reflect.DeepEqual(expMap, entriesMap) {
189
+		t.Fatalf("Expected entries: %v, got: %v", expected, value)
190
+	}
191
+}
0 192
new file mode 100644
... ...
@@ -0,0 +1,254 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io"
6
+	"strings"
7
+	"text/tabwriter"
8
+	"text/template"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"github.com/docker/docker/reference"
12
+)
13
+
14
+const (
15
+	tableFormatKey = "table"
16
+	rawFormatKey   = "raw"
17
+
18
+	defaultContainerTableFormat       = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
19
+	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
20
+	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
21
+	defaultQuietFormat                = "{{.ID}}"
22
+)
23
+
24
+// Context contains information required by the formatter to print the output as desired.
25
+type Context struct {
26
+	// Output is the output stream to which the formatted string is written.
27
+	Output io.Writer
28
+	// Format is used to choose raw, table or custom format for the output.
29
+	Format string
30
+	// Quiet when set to true will simply print minimal information.
31
+	Quiet bool
32
+	// Trunc when set to true will truncate the output of certain fields such as Container ID.
33
+	Trunc bool
34
+
35
+	// internal element
36
+	table       bool
37
+	finalFormat string
38
+	header      string
39
+	buffer      *bytes.Buffer
40
+}
41
+
42
+func (c *Context) preformat() {
43
+	c.finalFormat = c.Format
44
+
45
+	if strings.HasPrefix(c.Format, tableKey) {
46
+		c.table = true
47
+		c.finalFormat = c.finalFormat[len(tableKey):]
48
+	}
49
+
50
+	c.finalFormat = strings.Trim(c.finalFormat, " ")
51
+	r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
52
+	c.finalFormat = r.Replace(c.finalFormat)
53
+}
54
+
55
+func (c *Context) parseFormat() (*template.Template, error) {
56
+	tmpl, err := template.New("").Parse(c.finalFormat)
57
+	if err != nil {
58
+		c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
59
+		c.buffer.WriteTo(c.Output)
60
+	}
61
+	return tmpl, err
62
+}
63
+
64
+func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
65
+	if c.table {
66
+		if len(c.header) == 0 {
67
+			// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
68
+			tmpl.Execute(bytes.NewBufferString(""), subContext)
69
+			c.header = subContext.fullHeader()
70
+		}
71
+
72
+		t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
73
+		t.Write([]byte(c.header))
74
+		t.Write([]byte("\n"))
75
+		c.buffer.WriteTo(t)
76
+		t.Flush()
77
+	} else {
78
+		c.buffer.WriteTo(c.Output)
79
+	}
80
+}
81
+
82
+func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
83
+	if err := tmpl.Execute(c.buffer, subContext); err != nil {
84
+		c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
85
+		c.buffer.WriteTo(c.Output)
86
+		return err
87
+	}
88
+	if c.table && len(c.header) == 0 {
89
+		c.header = subContext.fullHeader()
90
+	}
91
+	c.buffer.WriteString("\n")
92
+	return nil
93
+}
94
+
95
+// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
96
+type ContainerContext struct {
97
+	Context
98
+	// Size when set to true will display the size of the output.
99
+	Size bool
100
+	// Containers
101
+	Containers []types.Container
102
+}
103
+
104
+// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
105
+type ImageContext struct {
106
+	Context
107
+	Digest bool
108
+	// Images
109
+	Images []types.Image
110
+}
111
+
112
+func (ctx ContainerContext) Write() {
113
+	switch ctx.Format {
114
+	case tableFormatKey:
115
+		ctx.Format = defaultContainerTableFormat
116
+		if ctx.Quiet {
117
+			ctx.Format = defaultQuietFormat
118
+		}
119
+	case rawFormatKey:
120
+		if ctx.Quiet {
121
+			ctx.Format = `container_id: {{.ID}}`
122
+		} else {
123
+			ctx.Format = `container_id: {{.ID}}
124
+image: {{.Image}}
125
+command: {{.Command}}
126
+created_at: {{.CreatedAt}}
127
+status: {{.Status}}
128
+names: {{.Names}}
129
+labels: {{.Labels}}
130
+ports: {{.Ports}}
131
+`
132
+			if ctx.Size {
133
+				ctx.Format += `size: {{.Size}}
134
+`
135
+			}
136
+		}
137
+	}
138
+
139
+	ctx.buffer = bytes.NewBufferString("")
140
+	ctx.preformat()
141
+	if ctx.table && ctx.Size {
142
+		ctx.finalFormat += "\t{{.Size}}"
143
+	}
144
+
145
+	tmpl, err := ctx.parseFormat()
146
+	if err != nil {
147
+		return
148
+	}
149
+
150
+	for _, container := range ctx.Containers {
151
+		containerCtx := &containerContext{
152
+			trunc: ctx.Trunc,
153
+			c:     container,
154
+		}
155
+		err = ctx.contextFormat(tmpl, containerCtx)
156
+		if err != nil {
157
+			return
158
+		}
159
+	}
160
+
161
+	ctx.postformat(tmpl, &containerContext{})
162
+}
163
+
164
+func (ctx ImageContext) Write() {
165
+	switch ctx.Format {
166
+	case tableFormatKey:
167
+		ctx.Format = defaultImageTableFormat
168
+		if ctx.Digest {
169
+			ctx.Format = defaultImageTableFormatWithDigest
170
+		}
171
+		if ctx.Quiet {
172
+			ctx.Format = defaultQuietFormat
173
+		}
174
+	case rawFormatKey:
175
+		if ctx.Quiet {
176
+			ctx.Format = `image_id: {{.ID}}`
177
+		} else {
178
+			if ctx.Digest {
179
+				ctx.Format = `repository: {{ .Repository }}
180
+tag: {{.Tag}}
181
+digest: {{.Digest}}
182
+image_id: {{.ID}}
183
+created_at: {{.CreatedAt}}
184
+virtual_size: {{.Size}}
185
+`
186
+			} else {
187
+				ctx.Format = `repository: {{ .Repository }}
188
+tag: {{.Tag}}
189
+image_id: {{.ID}}
190
+created_at: {{.CreatedAt}}
191
+virtual_size: {{.Size}}
192
+`
193
+			}
194
+		}
195
+	}
196
+
197
+	ctx.buffer = bytes.NewBufferString("")
198
+	ctx.preformat()
199
+	if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
200
+		ctx.finalFormat += "\t{{.Digest}}"
201
+	}
202
+
203
+	tmpl, err := ctx.parseFormat()
204
+	if err != nil {
205
+		return
206
+	}
207
+
208
+	for _, image := range ctx.Images {
209
+
210
+		repoTags := image.RepoTags
211
+		repoDigests := image.RepoDigests
212
+
213
+		if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
214
+			// dangling image - clear out either repoTags or repoDigests so we only show it once below
215
+			repoDigests = []string{}
216
+		}
217
+		// combine the tags and digests lists
218
+		tagsAndDigests := append(repoTags, repoDigests...)
219
+		for _, repoAndRef := range tagsAndDigests {
220
+			repo := "<none>"
221
+			tag := "<none>"
222
+			digest := "<none>"
223
+
224
+			if !strings.HasPrefix(repoAndRef, "<none>") {
225
+				ref, err := reference.ParseNamed(repoAndRef)
226
+				if err != nil {
227
+					continue
228
+				}
229
+				repo = ref.Name()
230
+
231
+				switch x := ref.(type) {
232
+				case reference.Canonical:
233
+					digest = x.Digest().String()
234
+				case reference.NamedTagged:
235
+					tag = x.Tag()
236
+				}
237
+			}
238
+			imageCtx := &imageContext{
239
+				trunc:  ctx.Trunc,
240
+				i:      image,
241
+				repo:   repo,
242
+				tag:    tag,
243
+				digest: digest,
244
+			}
245
+			err = ctx.contextFormat(tmpl, imageCtx)
246
+			if err != nil {
247
+				return
248
+			}
249
+		}
250
+	}
251
+
252
+	ctx.postformat(tmpl, &imageContext{})
253
+}
0 254
new file mode 100644
... ...
@@ -0,0 +1,535 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"testing"
6
+	"time"
7
+
8
+	"github.com/docker/docker/api/types"
9
+)
10
+
11
+func TestContainerContextWrite(t *testing.T) {
12
+	unixTime := time.Now().AddDate(0, 0, -1).Unix()
13
+	expectedTime := time.Unix(unixTime, 0).String()
14
+
15
+	contexts := []struct {
16
+		context  ContainerContext
17
+		expected string
18
+	}{
19
+		// Errors
20
+		{
21
+			ContainerContext{
22
+				Context: Context{
23
+					Format: "{{InvalidFunction}}",
24
+				},
25
+			},
26
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
27
+`,
28
+		},
29
+		{
30
+			ContainerContext{
31
+				Context: Context{
32
+					Format: "{{nil}}",
33
+				},
34
+			},
35
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
36
+`,
37
+		},
38
+		// Table Format
39
+		{
40
+			ContainerContext{
41
+				Context: Context{
42
+					Format: "table",
43
+				},
44
+			},
45
+			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
46
+containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz
47
+containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar
48
+`,
49
+		},
50
+		{
51
+			ContainerContext{
52
+				Context: Context{
53
+					Format: "table {{.Image}}",
54
+				},
55
+			},
56
+			"IMAGE\nubuntu\nubuntu\n",
57
+		},
58
+		{
59
+			ContainerContext{
60
+				Context: Context{
61
+					Format: "table {{.Image}}",
62
+				},
63
+				Size: true,
64
+			},
65
+			"IMAGE               SIZE\nubuntu              0 B\nubuntu              0 B\n",
66
+		},
67
+		{
68
+			ContainerContext{
69
+				Context: Context{
70
+					Format: "table {{.Image}}",
71
+					Quiet:  true,
72
+				},
73
+			},
74
+			"IMAGE\nubuntu\nubuntu\n",
75
+		},
76
+		{
77
+			ContainerContext{
78
+				Context: Context{
79
+					Format: "table",
80
+					Quiet:  true,
81
+				},
82
+			},
83
+			"containerID1\ncontainerID2\n",
84
+		},
85
+		// Raw Format
86
+		{
87
+			ContainerContext{
88
+				Context: Context{
89
+					Format: "raw",
90
+				},
91
+			},
92
+			fmt.Sprintf(`container_id: containerID1
93
+image: ubuntu
94
+command: ""
95
+created_at: %s
96
+status: 
97
+names: foobar_baz
98
+labels: 
99
+ports: 
100
+
101
+container_id: containerID2
102
+image: ubuntu
103
+command: ""
104
+created_at: %s
105
+status: 
106
+names: foobar_bar
107
+labels: 
108
+ports: 
109
+
110
+`, expectedTime, expectedTime),
111
+		},
112
+		{
113
+			ContainerContext{
114
+				Context: Context{
115
+					Format: "raw",
116
+				},
117
+				Size: true,
118
+			},
119
+			fmt.Sprintf(`container_id: containerID1
120
+image: ubuntu
121
+command: ""
122
+created_at: %s
123
+status: 
124
+names: foobar_baz
125
+labels: 
126
+ports: 
127
+size: 0 B
128
+
129
+container_id: containerID2
130
+image: ubuntu
131
+command: ""
132
+created_at: %s
133
+status: 
134
+names: foobar_bar
135
+labels: 
136
+ports: 
137
+size: 0 B
138
+
139
+`, expectedTime, expectedTime),
140
+		},
141
+		{
142
+			ContainerContext{
143
+				Context: Context{
144
+					Format: "raw",
145
+					Quiet:  true,
146
+				},
147
+			},
148
+			"container_id: containerID1\ncontainer_id: containerID2\n",
149
+		},
150
+		// Custom Format
151
+		{
152
+			ContainerContext{
153
+				Context: Context{
154
+					Format: "{{.Image}}",
155
+				},
156
+			},
157
+			"ubuntu\nubuntu\n",
158
+		},
159
+		{
160
+			ContainerContext{
161
+				Context: Context{
162
+					Format: "{{.Image}}",
163
+				},
164
+				Size: true,
165
+			},
166
+			"ubuntu\nubuntu\n",
167
+		},
168
+	}
169
+
170
+	for _, context := range contexts {
171
+		containers := []types.Container{
172
+			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
173
+			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
174
+		}
175
+		out := bytes.NewBufferString("")
176
+		context.context.Output = out
177
+		context.context.Containers = containers
178
+		context.context.Write()
179
+		actual := out.String()
180
+		if actual != context.expected {
181
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
182
+		}
183
+		// Clean buffer
184
+		out.Reset()
185
+	}
186
+}
187
+
188
+func TestContainerContextWriteWithNoContainers(t *testing.T) {
189
+	out := bytes.NewBufferString("")
190
+	containers := []types.Container{}
191
+
192
+	contexts := []struct {
193
+		context  ContainerContext
194
+		expected string
195
+	}{
196
+		{
197
+			ContainerContext{
198
+				Context: Context{
199
+					Format: "{{.Image}}",
200
+					Output: out,
201
+				},
202
+			},
203
+			"",
204
+		},
205
+		{
206
+			ContainerContext{
207
+				Context: Context{
208
+					Format: "table {{.Image}}",
209
+					Output: out,
210
+				},
211
+			},
212
+			"IMAGE\n",
213
+		},
214
+		{
215
+			ContainerContext{
216
+				Context: Context{
217
+					Format: "{{.Image}}",
218
+					Output: out,
219
+				},
220
+				Size: true,
221
+			},
222
+			"",
223
+		},
224
+		{
225
+			ContainerContext{
226
+				Context: Context{
227
+					Format: "table {{.Image}}",
228
+					Output: out,
229
+				},
230
+				Size: true,
231
+			},
232
+			"IMAGE               SIZE\n",
233
+		},
234
+	}
235
+
236
+	for _, context := range contexts {
237
+		context.context.Containers = containers
238
+		context.context.Write()
239
+		actual := out.String()
240
+		if actual != context.expected {
241
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
242
+		}
243
+		// Clean buffer
244
+		out.Reset()
245
+	}
246
+}
247
+
248
+func TestImageContextWrite(t *testing.T) {
249
+	unixTime := time.Now().AddDate(0, 0, -1).Unix()
250
+	expectedTime := time.Unix(unixTime, 0).String()
251
+
252
+	contexts := []struct {
253
+		context  ImageContext
254
+		expected string
255
+	}{
256
+		// Errors
257
+		{
258
+			ImageContext{
259
+				Context: Context{
260
+					Format: "{{InvalidFunction}}",
261
+				},
262
+			},
263
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
264
+`,
265
+		},
266
+		{
267
+			ImageContext{
268
+				Context: Context{
269
+					Format: "{{nil}}",
270
+				},
271
+			},
272
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
273
+`,
274
+		},
275
+		// Table Format
276
+		{
277
+			ImageContext{
278
+				Context: Context{
279
+					Format: "table",
280
+				},
281
+			},
282
+			`REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
283
+image               tag1                imageID1            24 hours ago        0 B
284
+image               <none>              imageID1            24 hours ago        0 B
285
+image               tag2                imageID2            24 hours ago        0 B
286
+<none>              <none>              imageID3            24 hours ago        0 B
287
+`,
288
+		},
289
+		{
290
+			ImageContext{
291
+				Context: Context{
292
+					Format: "table {{.Repository}}",
293
+				},
294
+			},
295
+			"REPOSITORY\nimage\nimage\nimage\n<none>\n",
296
+		},
297
+		{
298
+			ImageContext{
299
+				Context: Context{
300
+					Format: "table {{.Repository}}",
301
+				},
302
+				Digest: true,
303
+			},
304
+			`REPOSITORY          DIGEST
305
+image               <none>
306
+image               sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
307
+image               <none>
308
+<none>              <none>
309
+`,
310
+		},
311
+		{
312
+			ImageContext{
313
+				Context: Context{
314
+					Format: "table {{.Repository}}",
315
+					Quiet:  true,
316
+				},
317
+			},
318
+			"REPOSITORY\nimage\nimage\nimage\n<none>\n",
319
+		},
320
+		{
321
+			ImageContext{
322
+				Context: Context{
323
+					Format: "table",
324
+					Quiet:  true,
325
+				},
326
+			},
327
+			"imageID1\nimageID1\nimageID2\nimageID3\n",
328
+		},
329
+		{
330
+			ImageContext{
331
+				Context: Context{
332
+					Format: "table",
333
+					Quiet:  false,
334
+				},
335
+				Digest: true,
336
+			},
337
+			`REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
338
+image               tag1                <none>                                                                    imageID1            24 hours ago        0 B
339
+image               <none>              sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf   imageID1            24 hours ago        0 B
340
+image               tag2                <none>                                                                    imageID2            24 hours ago        0 B
341
+<none>              <none>              <none>                                                                    imageID3            24 hours ago        0 B
342
+`,
343
+		},
344
+		{
345
+			ImageContext{
346
+				Context: Context{
347
+					Format: "table",
348
+					Quiet:  true,
349
+				},
350
+				Digest: true,
351
+			},
352
+			"imageID1\nimageID1\nimageID2\nimageID3\n",
353
+		},
354
+		// Raw Format
355
+		{
356
+			ImageContext{
357
+				Context: Context{
358
+					Format: "raw",
359
+				},
360
+			},
361
+			fmt.Sprintf(`repository: image
362
+tag: tag1
363
+image_id: imageID1
364
+created_at: %s
365
+virtual_size: 0 B
366
+
367
+repository: image
368
+tag: <none>
369
+image_id: imageID1
370
+created_at: %s
371
+virtual_size: 0 B
372
+
373
+repository: image
374
+tag: tag2
375
+image_id: imageID2
376
+created_at: %s
377
+virtual_size: 0 B
378
+
379
+repository: <none>
380
+tag: <none>
381
+image_id: imageID3
382
+created_at: %s
383
+virtual_size: 0 B
384
+
385
+`, expectedTime, expectedTime, expectedTime, expectedTime),
386
+		},
387
+		{
388
+			ImageContext{
389
+				Context: Context{
390
+					Format: "raw",
391
+				},
392
+				Digest: true,
393
+			},
394
+			fmt.Sprintf(`repository: image
395
+tag: tag1
396
+digest: <none>
397
+image_id: imageID1
398
+created_at: %s
399
+virtual_size: 0 B
400
+
401
+repository: image
402
+tag: <none>
403
+digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
404
+image_id: imageID1
405
+created_at: %s
406
+virtual_size: 0 B
407
+
408
+repository: image
409
+tag: tag2
410
+digest: <none>
411
+image_id: imageID2
412
+created_at: %s
413
+virtual_size: 0 B
414
+
415
+repository: <none>
416
+tag: <none>
417
+digest: <none>
418
+image_id: imageID3
419
+created_at: %s
420
+virtual_size: 0 B
421
+
422
+`, expectedTime, expectedTime, expectedTime, expectedTime),
423
+		},
424
+		{
425
+			ImageContext{
426
+				Context: Context{
427
+					Format: "raw",
428
+					Quiet:  true,
429
+				},
430
+			},
431
+			`image_id: imageID1
432
+image_id: imageID1
433
+image_id: imageID2
434
+image_id: imageID3
435
+`,
436
+		},
437
+		// Custom Format
438
+		{
439
+			ImageContext{
440
+				Context: Context{
441
+					Format: "{{.Repository}}",
442
+				},
443
+			},
444
+			"image\nimage\nimage\n<none>\n",
445
+		},
446
+		{
447
+			ImageContext{
448
+				Context: Context{
449
+					Format: "{{.Repository}}",
450
+				},
451
+				Digest: true,
452
+			},
453
+			"image\nimage\nimage\n<none>\n",
454
+		},
455
+	}
456
+
457
+	for _, context := range contexts {
458
+		images := []types.Image{
459
+			{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
460
+			{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
461
+			{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
462
+		}
463
+		out := bytes.NewBufferString("")
464
+		context.context.Output = out
465
+		context.context.Images = images
466
+		context.context.Write()
467
+		actual := out.String()
468
+		if actual != context.expected {
469
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
470
+		}
471
+		// Clean buffer
472
+		out.Reset()
473
+	}
474
+}
475
+
476
+func TestImageContextWriteWithNoImage(t *testing.T) {
477
+	out := bytes.NewBufferString("")
478
+	images := []types.Image{}
479
+
480
+	contexts := []struct {
481
+		context  ImageContext
482
+		expected string
483
+	}{
484
+		{
485
+			ImageContext{
486
+				Context: Context{
487
+					Format: "{{.Repository}}",
488
+					Output: out,
489
+				},
490
+			},
491
+			"",
492
+		},
493
+		{
494
+			ImageContext{
495
+				Context: Context{
496
+					Format: "table {{.Repository}}",
497
+					Output: out,
498
+				},
499
+			},
500
+			"REPOSITORY\n",
501
+		},
502
+		{
503
+			ImageContext{
504
+				Context: Context{
505
+					Format: "{{.Repository}}",
506
+					Output: out,
507
+				},
508
+				Digest: true,
509
+			},
510
+			"",
511
+		},
512
+		{
513
+			ImageContext{
514
+				Context: Context{
515
+					Format: "table {{.Repository}}",
516
+					Output: out,
517
+				},
518
+				Digest: true,
519
+			},
520
+			"REPOSITORY          DIGEST\n",
521
+		},
522
+	}
523
+
524
+	for _, context := range contexts {
525
+		context.context.Images = images
526
+		context.context.Write()
527
+		actual := out.String()
528
+		if actual != context.expected {
529
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
530
+		}
531
+		// Clean buffer
532
+		out.Reset()
533
+	}
534
+}
... ...
@@ -1,19 +1,12 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"fmt"
5
-	"strings"
6
-	"text/tabwriter"
7
-	"time"
8
-
4
+	"github.com/docker/docker/api/client/formatter"
9 5
 	"github.com/docker/docker/api/types"
10 6
 	"github.com/docker/docker/api/types/filters"
11 7
 	Cli "github.com/docker/docker/cli"
12 8
 	"github.com/docker/docker/opts"
13 9
 	flag "github.com/docker/docker/pkg/mflag"
14
-	"github.com/docker/docker/pkg/stringid"
15
-	"github.com/docker/docker/reference"
16
-	"github.com/docker/go-units"
17 10
 )
18 11
 
19 12
 // CmdImages lists the images in a specified repository, or all top-level images if no repository is specified.
... ...
@@ -25,6 +18,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
25 25
 	all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)")
26 26
 	noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output")
27 27
 	showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests")
28
+	format := cmd.String([]string{"-format"}, "", "Pretty-print images using a Go template")
28 29
 
29 30
 	flFilter := opts.NewListOpts(nil)
30 31
 	cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
... ...
@@ -59,66 +53,27 @@ func (cli *DockerCli) CmdImages(args ...string) error {
59 59
 		return err
60 60
 	}
61 61
 
62
-	w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
63
-	if !*quiet {
64
-		if *showDigests {
65
-			fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tSIZE")
62
+	f := *format
63
+	if len(f) == 0 {
64
+		if len(cli.ImagesFormat()) > 0 && !*quiet {
65
+			f = cli.ImagesFormat()
66 66
 		} else {
67
-			fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE")
67
+			f = "table"
68 68
 		}
69 69
 	}
70 70
 
71
-	for _, image := range images {
72
-		ID := image.ID
73
-		if !*noTrunc {
74
-			ID = stringid.TruncateID(ID)
75
-		}
76
-
77
-		repoTags := image.RepoTags
78
-		repoDigests := image.RepoDigests
79
-
80
-		if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
81
-			// dangling image - clear out either repoTags or repoDigsts so we only show it once below
82
-			repoDigests = []string{}
83
-		}
84
-
85
-		// combine the tags and digests lists
86
-		tagsAndDigests := append(repoTags, repoDigests...)
87
-		for _, repoAndRef := range tagsAndDigests {
88
-			// default repo, tag, and digest to none - if there's a value, it'll be set below
89
-			repo := "<none>"
90
-			tag := "<none>"
91
-			digest := "<none>"
92
-
93
-			if !strings.HasPrefix(repoAndRef, "<none>") {
94
-				ref, err := reference.ParseNamed(repoAndRef)
95
-				if err != nil {
96
-					return err
97
-				}
98
-				repo = ref.Name()
99
-
100
-				switch x := ref.(type) {
101
-				case reference.Canonical:
102
-					digest = x.Digest().String()
103
-				case reference.NamedTagged:
104
-					tag = x.Tag()
105
-				}
106
-			}
107
-
108
-			if !*quiet {
109
-				if *showDigests {
110
-					fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size)))
111
-				} else {
112
-					fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size)))
113
-				}
114
-			} else {
115
-				fmt.Fprintln(w, ID)
116
-			}
117
-		}
71
+	imagesCtx := formatter.ImageContext{
72
+		Context: formatter.Context{
73
+			Output: cli.out,
74
+			Format: f,
75
+			Quiet:  *quiet,
76
+			Trunc:  !*noTrunc,
77
+		},
78
+		Digest: *showDigests,
79
+		Images: images,
118 80
 	}
119 81
 
120
-	if !*quiet {
121
-		w.Flush()
122
-	}
82
+	imagesCtx.Write()
83
+
123 84
 	return nil
124 85
 }
... ...
@@ -1,7 +1,7 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"github.com/docker/docker/api/client/ps"
4
+	"github.com/docker/docker/api/client/formatter"
5 5
 	"github.com/docker/docker/api/types"
6 6
 	"github.com/docker/docker/api/types/filters"
7 7
 	Cli "github.com/docker/docker/cli"
... ...
@@ -70,15 +70,18 @@ func (cli *DockerCli) CmdPs(args ...string) error {
70 70
 		}
71 71
 	}
72 72
 
73
-	psCtx := ps.Context{
74
-		Output: cli.out,
75
-		Format: f,
76
-		Quiet:  *quiet,
77
-		Size:   *size,
78
-		Trunc:  !*noTrunc,
73
+	psCtx := formatter.ContainerContext{
74
+		Context: formatter.Context{
75
+			Output: cli.out,
76
+			Format: f,
77
+			Quiet:  *quiet,
78
+			Trunc:  !*noTrunc,
79
+		},
80
+		Size:       *size,
81
+		Containers: containers,
79 82
 	}
80 83
 
81
-	ps.Format(psCtx, containers)
84
+	psCtx.Write()
82 85
 
83 86
 	return nil
84 87
 }
85 88
deleted file mode 100644
... ...
@@ -1,160 +0,0 @@
1
-package ps
2
-
3
-import (
4
-	"fmt"
5
-	"strconv"
6
-	"strings"
7
-	"time"
8
-
9
-	"github.com/docker/docker/api"
10
-	"github.com/docker/docker/api/types"
11
-	"github.com/docker/docker/pkg/stringid"
12
-	"github.com/docker/docker/pkg/stringutils"
13
-	"github.com/docker/go-units"
14
-)
15
-
16
-const (
17
-	tableKey = "table"
18
-
19
-	idHeader         = "CONTAINER ID"
20
-	imageHeader      = "IMAGE"
21
-	namesHeader      = "NAMES"
22
-	commandHeader    = "COMMAND"
23
-	createdAtHeader  = "CREATED AT"
24
-	runningForHeader = "CREATED"
25
-	statusHeader     = "STATUS"
26
-	portsHeader      = "PORTS"
27
-	sizeHeader       = "SIZE"
28
-	labelsHeader     = "LABELS"
29
-)
30
-
31
-type containerContext struct {
32
-	trunc  bool
33
-	header []string
34
-	c      types.Container
35
-}
36
-
37
-func (c *containerContext) ID() string {
38
-	c.addHeader(idHeader)
39
-	if c.trunc {
40
-		return stringid.TruncateID(c.c.ID)
41
-	}
42
-	return c.c.ID
43
-}
44
-
45
-func (c *containerContext) Names() string {
46
-	c.addHeader(namesHeader)
47
-	names := stripNamePrefix(c.c.Names)
48
-	if c.trunc {
49
-		for _, name := range names {
50
-			if len(strings.Split(name, "/")) == 1 {
51
-				names = []string{name}
52
-				break
53
-			}
54
-		}
55
-	}
56
-	return strings.Join(names, ",")
57
-}
58
-
59
-func (c *containerContext) Image() string {
60
-	c.addHeader(imageHeader)
61
-	if c.c.Image == "" {
62
-		return "<no image>"
63
-	}
64
-	if c.trunc {
65
-		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
66
-			return trunc
67
-		}
68
-	}
69
-	return c.c.Image
70
-}
71
-
72
-func (c *containerContext) Command() string {
73
-	c.addHeader(commandHeader)
74
-	command := c.c.Command
75
-	if c.trunc {
76
-		command = stringutils.Truncate(command, 20)
77
-	}
78
-	return strconv.Quote(command)
79
-}
80
-
81
-func (c *containerContext) CreatedAt() string {
82
-	c.addHeader(createdAtHeader)
83
-	return time.Unix(int64(c.c.Created), 0).String()
84
-}
85
-
86
-func (c *containerContext) RunningFor() string {
87
-	c.addHeader(runningForHeader)
88
-	createdAt := time.Unix(int64(c.c.Created), 0)
89
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
90
-}
91
-
92
-func (c *containerContext) Ports() string {
93
-	c.addHeader(portsHeader)
94
-	return api.DisplayablePorts(c.c.Ports)
95
-}
96
-
97
-func (c *containerContext) Status() string {
98
-	c.addHeader(statusHeader)
99
-	return c.c.Status
100
-}
101
-
102
-func (c *containerContext) Size() string {
103
-	c.addHeader(sizeHeader)
104
-	srw := units.HumanSize(float64(c.c.SizeRw))
105
-	sv := units.HumanSize(float64(c.c.SizeRootFs))
106
-
107
-	sf := srw
108
-	if c.c.SizeRootFs > 0 {
109
-		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
110
-	}
111
-	return sf
112
-}
113
-
114
-func (c *containerContext) Labels() string {
115
-	c.addHeader(labelsHeader)
116
-	if c.c.Labels == nil {
117
-		return ""
118
-	}
119
-
120
-	var joinLabels []string
121
-	for k, v := range c.c.Labels {
122
-		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
123
-	}
124
-	return strings.Join(joinLabels, ",")
125
-}
126
-
127
-func (c *containerContext) Label(name string) string {
128
-	n := strings.Split(name, ".")
129
-	r := strings.NewReplacer("-", " ", "_", " ")
130
-	h := r.Replace(n[len(n)-1])
131
-
132
-	c.addHeader(h)
133
-
134
-	if c.c.Labels == nil {
135
-		return ""
136
-	}
137
-	return c.c.Labels[name]
138
-}
139
-
140
-func (c *containerContext) fullHeader() string {
141
-	if c.header == nil {
142
-		return ""
143
-	}
144
-	return strings.Join(c.header, "\t")
145
-}
146
-
147
-func (c *containerContext) addHeader(header string) {
148
-	if c.header == nil {
149
-		c.header = []string{}
150
-	}
151
-	c.header = append(c.header, strings.ToUpper(header))
152
-}
153
-
154
-func stripNamePrefix(ss []string) []string {
155
-	for i, s := range ss {
156
-		ss[i] = s[1:]
157
-	}
158
-
159
-	return ss
160
-}
161 1
deleted file mode 100644
... ...
@@ -1,126 +0,0 @@
1
-package ps
2
-
3
-import (
4
-	"reflect"
5
-	"strings"
6
-	"testing"
7
-	"time"
8
-
9
-	"github.com/docker/docker/api/types"
10
-	"github.com/docker/docker/pkg/stringid"
11
-)
12
-
13
-func TestContainerPsContext(t *testing.T) {
14
-	containerID := stringid.GenerateRandomID()
15
-	unix := time.Now().Unix()
16
-
17
-	var ctx containerContext
18
-	cases := []struct {
19
-		container types.Container
20
-		trunc     bool
21
-		expValue  string
22
-		expHeader string
23
-		call      func() string
24
-	}{
25
-		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), idHeader, ctx.ID},
26
-		{types.Container{ID: containerID}, false, containerID, idHeader, ctx.ID},
27
-		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
28
-		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
29
-		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
30
-		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
31
-		{types.Container{
32
-			Image:   "a5a665ff33eced1e0803148700880edab4",
33
-			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
34
-		},
35
-			true,
36
-			"a5a665ff33ec",
37
-			imageHeader,
38
-			ctx.Image,
39
-		},
40
-		{types.Container{
41
-			Image:   "a5a665ff33eced1e0803148700880edab4",
42
-			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
43
-		},
44
-			false,
45
-			"a5a665ff33eced1e0803148700880edab4",
46
-			imageHeader,
47
-			ctx.Image,
48
-		},
49
-		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
50
-		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
51
-		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
52
-		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
53
-		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
54
-		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
55
-		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
56
-		{types.Container{}, true, "", labelsHeader, ctx.Labels},
57
-		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
58
-		{types.Container{Created: unix}, true, "Less than a second", runningForHeader, ctx.RunningFor},
59
-	}
60
-
61
-	for _, c := range cases {
62
-		ctx = containerContext{c: c.container, trunc: c.trunc}
63
-		v := c.call()
64
-		if strings.Contains(v, ",") {
65
-			// comma-separated values means probably a map input, which won't
66
-			// be guaranteed to have the same order as our expected value
67
-			// We'll create maps and use reflect.DeepEquals to check instead:
68
-			entriesMap := make(map[string]string)
69
-			expMap := make(map[string]string)
70
-			entries := strings.Split(v, ",")
71
-			expectedEntries := strings.Split(c.expValue, ",")
72
-			for _, entry := range entries {
73
-				keyval := strings.Split(entry, "=")
74
-				entriesMap[keyval[0]] = keyval[1]
75
-			}
76
-			for _, expected := range expectedEntries {
77
-				keyval := strings.Split(expected, "=")
78
-				expMap[keyval[0]] = keyval[1]
79
-			}
80
-			if !reflect.DeepEqual(expMap, entriesMap) {
81
-				t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
82
-			}
83
-		} else if v != c.expValue {
84
-			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
85
-		}
86
-
87
-		h := ctx.fullHeader()
88
-		if h != c.expHeader {
89
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
90
-		}
91
-	}
92
-
93
-	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
94
-	ctx = containerContext{c: c1, trunc: true}
95
-
96
-	sid := ctx.Label("com.docker.swarm.swarm-id")
97
-	node := ctx.Label("com.docker.swarm.node_name")
98
-	if sid != "33" {
99
-		t.Fatalf("Expected 33, was %s\n", sid)
100
-	}
101
-
102
-	if node != "ubuntu" {
103
-		t.Fatalf("Expected ubuntu, was %s\n", node)
104
-	}
105
-
106
-	h := ctx.fullHeader()
107
-	if h != "SWARM ID\tNODE NAME" {
108
-		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
109
-
110
-	}
111
-
112
-	c2 := types.Container{}
113
-	ctx = containerContext{c: c2, trunc: true}
114
-
115
-	label := ctx.Label("anything.really")
116
-	if label != "" {
117
-		t.Fatalf("Expected an empty string, was %s", label)
118
-	}
119
-
120
-	ctx = containerContext{c: c2, trunc: true}
121
-	fullHeader := ctx.fullHeader()
122
-	if fullHeader != "" {
123
-		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
124
-	}
125
-
126
-}
127 1
deleted file mode 100644
... ...
@@ -1,140 +0,0 @@
1
-package ps
2
-
3
-import (
4
-	"bytes"
5
-	"fmt"
6
-	"io"
7
-	"strings"
8
-	"text/tabwriter"
9
-	"text/template"
10
-
11
-	"github.com/docker/docker/api/types"
12
-)
13
-
14
-const (
15
-	tableFormatKey = "table"
16
-	rawFormatKey   = "raw"
17
-
18
-	defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
19
-	defaultQuietFormat = "{{.ID}}"
20
-)
21
-
22
-// Context contains information required by the formatter to print the output as desired.
23
-type Context struct {
24
-	// Output is the output stream to which the formatted string is written.
25
-	Output io.Writer
26
-	// Format is used to choose raw, table or custom format for the output.
27
-	Format string
28
-	// Size when set to true will display the size of the output.
29
-	Size bool
30
-	// Quiet when set to true will simply print minimal information.
31
-	Quiet bool
32
-	// Trunc when set to true will truncate the output of certain fields such as Container ID.
33
-	Trunc bool
34
-}
35
-
36
-// Format helps to format the output using the parameters set in the Context.
37
-// Currently Format allow to display in raw, table or custom format the output.
38
-func Format(ctx Context, containers []types.Container) {
39
-	switch ctx.Format {
40
-	case tableFormatKey:
41
-		tableFormat(ctx, containers)
42
-	case rawFormatKey:
43
-		rawFormat(ctx, containers)
44
-	default:
45
-		customFormat(ctx, containers)
46
-	}
47
-}
48
-
49
-func rawFormat(ctx Context, containers []types.Container) {
50
-	if ctx.Quiet {
51
-		ctx.Format = `container_id: {{.ID}}`
52
-	} else {
53
-		ctx.Format = `container_id: {{.ID}}
54
-image: {{.Image}}
55
-command: {{.Command}}
56
-created_at: {{.CreatedAt}}
57
-status: {{.Status}}
58
-names: {{.Names}}
59
-labels: {{.Labels}}
60
-ports: {{.Ports}}
61
-`
62
-		if ctx.Size {
63
-			ctx.Format += `size: {{.Size}}
64
-`
65
-		}
66
-	}
67
-
68
-	customFormat(ctx, containers)
69
-}
70
-
71
-func tableFormat(ctx Context, containers []types.Container) {
72
-	ctx.Format = defaultTableFormat
73
-	if ctx.Quiet {
74
-		ctx.Format = defaultQuietFormat
75
-	}
76
-
77
-	customFormat(ctx, containers)
78
-}
79
-
80
-func customFormat(ctx Context, containers []types.Container) {
81
-	var (
82
-		table  bool
83
-		header string
84
-		format = ctx.Format
85
-		buffer = bytes.NewBufferString("")
86
-	)
87
-
88
-	if strings.HasPrefix(ctx.Format, tableKey) {
89
-		table = true
90
-		format = format[len(tableKey):]
91
-	}
92
-
93
-	format = strings.Trim(format, " ")
94
-	r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
95
-	format = r.Replace(format)
96
-
97
-	if table && ctx.Size {
98
-		format += "\t{{.Size}}"
99
-	}
100
-
101
-	tmpl, err := template.New("").Parse(format)
102
-	if err != nil {
103
-		buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
104
-		buffer.WriteTo(ctx.Output)
105
-		return
106
-	}
107
-
108
-	for _, container := range containers {
109
-		containerCtx := &containerContext{
110
-			trunc: ctx.Trunc,
111
-			c:     container,
112
-		}
113
-		if err := tmpl.Execute(buffer, containerCtx); err != nil {
114
-			buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
115
-			buffer.WriteTo(ctx.Output)
116
-			return
117
-		}
118
-		if table && len(header) == 0 {
119
-			header = containerCtx.fullHeader()
120
-		}
121
-		buffer.WriteString("\n")
122
-	}
123
-
124
-	if table {
125
-		if len(header) == 0 {
126
-			// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
127
-			containerCtx := &containerContext{}
128
-			tmpl.Execute(bytes.NewBufferString(""), containerCtx)
129
-			header = containerCtx.fullHeader()
130
-		}
131
-
132
-		t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0)
133
-		t.Write([]byte(header))
134
-		t.Write([]byte("\n"))
135
-		buffer.WriteTo(t)
136
-		t.Flush()
137
-	} else {
138
-		buffer.WriteTo(ctx.Output)
139
-	}
140
-}
141 1
deleted file mode 100644
... ...
@@ -1,213 +0,0 @@
1
-package ps
2
-
3
-import (
4
-	"bytes"
5
-	"fmt"
6
-	"testing"
7
-	"time"
8
-
9
-	"github.com/docker/docker/api/types"
10
-)
11
-
12
-func TestFormat(t *testing.T) {
13
-	unixTime := time.Now().Add(-50 * time.Hour).Unix()
14
-	expectedTime := time.Unix(unixTime, 0).String()
15
-
16
-	contexts := []struct {
17
-		context  Context
18
-		expected string
19
-	}{
20
-		// Errors
21
-		{
22
-			Context{
23
-				Format: "{{InvalidFunction}}",
24
-			},
25
-			`Template parsing error: template: :1: function "InvalidFunction" not defined
26
-`,
27
-		},
28
-		{
29
-			Context{
30
-				Format: "{{nil}}",
31
-			},
32
-			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
33
-`,
34
-		},
35
-		// Table Format
36
-		{
37
-			Context{
38
-				Format: "table",
39
-			},
40
-			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
41
-containerID1        ubuntu              ""                  2 days ago                                                  foobar_baz
42
-containerID2        ubuntu              ""                  2 days ago                                                  foobar_bar
43
-`,
44
-		},
45
-		{
46
-			Context{
47
-				Format: "table {{.Image}}",
48
-			},
49
-			"IMAGE\nubuntu\nubuntu\n",
50
-		},
51
-		{
52
-			Context{
53
-				Format: "table {{.Image}}",
54
-				Size:   true,
55
-			},
56
-			"IMAGE               SIZE\nubuntu              0 B\nubuntu              0 B\n",
57
-		},
58
-		{
59
-			Context{
60
-				Format: "table {{.Image}}",
61
-				Quiet:  true,
62
-			},
63
-			"IMAGE\nubuntu\nubuntu\n",
64
-		},
65
-		{
66
-			Context{
67
-				Format: "table",
68
-				Quiet:  true,
69
-			},
70
-			"containerID1\ncontainerID2\n",
71
-		},
72
-		// Raw Format
73
-		{
74
-			Context{
75
-				Format: "raw",
76
-			},
77
-			fmt.Sprintf(`container_id: containerID1
78
-image: ubuntu
79
-command: ""
80
-created_at: %s
81
-status: 
82
-names: foobar_baz
83
-labels: 
84
-ports: 
85
-
86
-container_id: containerID2
87
-image: ubuntu
88
-command: ""
89
-created_at: %s
90
-status: 
91
-names: foobar_bar
92
-labels: 
93
-ports: 
94
-
95
-`, expectedTime, expectedTime),
96
-		},
97
-		{
98
-			Context{
99
-				Format: "raw",
100
-				Size:   true,
101
-			},
102
-			fmt.Sprintf(`container_id: containerID1
103
-image: ubuntu
104
-command: ""
105
-created_at: %s
106
-status: 
107
-names: foobar_baz
108
-labels: 
109
-ports: 
110
-size: 0 B
111
-
112
-container_id: containerID2
113
-image: ubuntu
114
-command: ""
115
-created_at: %s
116
-status: 
117
-names: foobar_bar
118
-labels: 
119
-ports: 
120
-size: 0 B
121
-
122
-`, expectedTime, expectedTime),
123
-		},
124
-		{
125
-			Context{
126
-				Format: "raw",
127
-				Quiet:  true,
128
-			},
129
-			"container_id: containerID1\ncontainer_id: containerID2\n",
130
-		},
131
-		// Custom Format
132
-		{
133
-			Context{
134
-				Format: "{{.Image}}",
135
-			},
136
-			"ubuntu\nubuntu\n",
137
-		},
138
-		{
139
-			Context{
140
-				Format: "{{.Image}}",
141
-				Size:   true,
142
-			},
143
-			"ubuntu\nubuntu\n",
144
-		},
145
-	}
146
-
147
-	for _, context := range contexts {
148
-		containers := []types.Container{
149
-			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
150
-			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
151
-		}
152
-		out := bytes.NewBufferString("")
153
-		context.context.Output = out
154
-		Format(context.context, containers)
155
-		actual := out.String()
156
-		if actual != context.expected {
157
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
158
-		}
159
-		// Clean buffer
160
-		out.Reset()
161
-	}
162
-}
163
-
164
-func TestCustomFormatNoContainers(t *testing.T) {
165
-	out := bytes.NewBufferString("")
166
-	containers := []types.Container{}
167
-
168
-	contexts := []struct {
169
-		context  Context
170
-		expected string
171
-	}{
172
-		{
173
-			Context{
174
-				Format: "{{.Image}}",
175
-				Output: out,
176
-			},
177
-			"",
178
-		},
179
-		{
180
-			Context{
181
-				Format: "table {{.Image}}",
182
-				Output: out,
183
-			},
184
-			"IMAGE\n",
185
-		},
186
-		{
187
-			Context{
188
-				Format: "{{.Image}}",
189
-				Output: out,
190
-				Size:   true,
191
-			},
192
-			"",
193
-		},
194
-		{
195
-			Context{
196
-				Format: "table {{.Image}}",
197
-				Output: out,
198
-				Size:   true,
199
-			},
200
-			"IMAGE               SIZE\n",
201
-		},
202
-	}
203
-
204
-	for _, context := range contexts {
205
-		customFormat(context.context, containers)
206
-		actual := out.String()
207
-		if actual != context.expected {
208
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
209
-		}
210
-		// Clean buffer
211
-		out.Reset()
212
-	}
213
-}
... ...
@@ -47,10 +47,11 @@ func SetConfigDir(dir string) {
47 47
 
48 48
 // ConfigFile ~/.docker/config.json file info
49 49
 type ConfigFile struct {
50
-	AuthConfigs map[string]types.AuthConfig `json:"auths"`
51
-	HTTPHeaders map[string]string           `json:"HttpHeaders,omitempty"`
52
-	PsFormat    string                      `json:"psFormat,omitempty"`
53
-	filename    string                      // Note: not serialized - for internal use only
50
+	AuthConfigs  map[string]types.AuthConfig `json:"auths"`
51
+	HTTPHeaders  map[string]string           `json:"HttpHeaders,omitempty"`
52
+	PsFormat     string                      `json:"psFormat,omitempty"`
53
+	ImagesFormat string                      `json:"imagesFormat,omitempty"`
54
+	filename     string                      // Note: not serialized - for internal use only
54 55
 }
55 56
 
56 57
 // NewConfigFile initializes an empty configuration file for the given filename 'fn'
... ...
@@ -927,6 +927,9 @@ _docker_images() {
927 927
 			fi
928 928
 			return
929 929
 			;;
930
+                --format)
931
+			return
932
+			;;
930 933
 	esac
931 934
 
932 935
 	case "${words[$cword-2]}$prev=" in
... ...
@@ -941,7 +944,7 @@ _docker_images() {
941 941
 
942 942
 	case "$cur" in
943 943
 		-*)
944
-			COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --help --no-trunc --quiet -q" -- "$cur" ) )
944
+			COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --format --help --no-trunc --quiet -q" -- "$cur" ) )
945 945
 			;;
946 946
 		=)
947 947
 			return
... ...
@@ -692,8 +692,9 @@ __docker_subcommand() {
692 692
             _arguments $(__docker_arguments) \
693 693
                 $opts_help \
694 694
                 "($help -a --all)"{-a,--all}"[Show all images]" \
695
-                "($help)--digest[Show digests]" \
695
+                "($help)--digests[Show digests]" \
696 696
                 "($help)*"{-f=,--filter=}"[Filter values]:filter: " \
697
+                "($help)--format[Pretty-print containers using a Go template]:format: " \
697 698
                 "($help)--no-trunc[Do not truncate output]" \
698 699
                 "($help -q --quiet)"{-q,--quiet}"[Only show numeric IDs]" \
699 700
                 "($help -): :__docker_repositories" && ret=0
... ...
@@ -103,6 +103,12 @@ Docker's client uses this property. If this property is not set, the client
103 103
 falls back to the default table format. For a list of supported formatting
104 104
 directives, see the [**Formatting** section in the `docker ps` documentation](ps.md)
105 105
 
106
+The property `imagesFormat` specifies the default format for `docker images` output.
107
+When the `--format` flag is not provided with the `docker images` command,
108
+Docker's client uses this property. If this property is not set, the client
109
+falls back to the default table format. For a list of supported formatting
110
+directives, see the [**Formatting** section in the `docker images` documentation](images.md)
111
+
106 112
 Following is a sample `config.json` file:
107 113
 
108 114
     {
... ...
@@ -110,6 +116,7 @@ Following is a sample `config.json` file:
110 110
         "MyHeader": "MyValue"
111 111
       },
112 112
       "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
113
+      "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}"
113 114
     }
114 115
 
115 116
 ### Notary
... ...
@@ -177,3 +177,53 @@ In this example, with the `0.1` value, it returns an empty set because no matche
177 177
 
178 178
     $ docker images --filter "label=com.example.version=0.1"
179 179
     REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
180
+
181
+## Formatting
182
+
183
+The formatting option (`--format`) will pretty print container output
184
+using a Go template.
185
+
186
+Valid placeholders for the Go template are listed below:
187
+
188
+Placeholder | Description
189
+---- | ----
190
+`.ID` | Image ID
191
+`.Repository` | Image repository
192
+`.Tag` | Image tag
193
+`.Digest` | Image digest
194
+`.CreatedSince` | Elapsed time since the image was created.
195
+`.CreatedAt` | Time when the image was created.
196
+`.Size` | Image disk size.
197
+
198
+When using the `--format` option, the `image` command will either
199
+output the data exactly as the template declares or, when using the
200
+`table` directive, will include column headers as well.
201
+
202
+The following example uses a template without headers and outputs the
203
+`ID` and `Repository` entries separated by a colon for all images:
204
+
205
+    $ docker images --format "{{.ID}}: {{.Repository}}"
206
+    77af4d6b9913: <none>
207
+    b6fa739cedf5: committ
208
+    78a85c484f71: <none>
209
+    30557a29d5ab: docker
210
+    5ed6274db6ce: <none>
211
+    746b819f315e: postgres
212
+    746b819f315e: postgres
213
+    746b819f315e: postgres
214
+    746b819f315e: postgres
215
+
216
+To list all images with their repository and tag in a table format you
217
+can use:
218
+
219
+    $ docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
220
+    IMAGE ID            REPOSITORY                TAG
221
+    77af4d6b9913        <none>                    <none>
222
+    b6fa739cedf5        committ                   latest
223
+    78a85c484f71        <none>                    <none>
224
+    30557a29d5ab        docker                    latest
225
+    5ed6274db6ce        <none>                    <none>
226
+    746b819f315e        postgres                  9
227
+    746b819f315e        postgres                  9.3
228
+    746b819f315e        postgres                  9.3.5
229
+    746b819f315e        postgres                  latest
... ...
@@ -2,6 +2,9 @@ package main
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"io/ioutil"
6
+	"os"
7
+	"path/filepath"
5 8
 	"reflect"
6 9
 	"sort"
7 10
 	"strings"
... ...
@@ -48,17 +51,17 @@ func (s *DockerSuite) TestImagesOrderedByCreationDate(c *check.C) {
48 48
 	testRequires(c, DaemonIsLinux)
49 49
 	id1, err := buildImage("order:test_a",
50 50
 		`FROM scratch
51
-		MAINTAINER dockerio1`, true)
51
+                MAINTAINER dockerio1`, true)
52 52
 	c.Assert(err, checker.IsNil)
53 53
 	time.Sleep(1 * time.Second)
54 54
 	id2, err := buildImage("order:test_c",
55 55
 		`FROM scratch
56
-		MAINTAINER dockerio2`, true)
56
+                MAINTAINER dockerio2`, true)
57 57
 	c.Assert(err, checker.IsNil)
58 58
 	time.Sleep(1 * time.Second)
59 59
 	id3, err := buildImage("order:test_b",
60 60
 		`FROM scratch
61
-		MAINTAINER dockerio3`, true)
61
+                MAINTAINER dockerio3`, true)
62 62
 	c.Assert(err, checker.IsNil)
63 63
 
64 64
 	out, _ := dockerCmd(c, "images", "-q", "--no-trunc")
... ...
@@ -81,17 +84,17 @@ func (s *DockerSuite) TestImagesFilterLabelMatch(c *check.C) {
81 81
 	imageName3 := "images_filter_test3"
82 82
 	image1ID, err := buildImage(imageName1,
83 83
 		`FROM scratch
84
-		 LABEL match me`, true)
84
+                 LABEL match me`, true)
85 85
 	c.Assert(err, check.IsNil)
86 86
 
87 87
 	image2ID, err := buildImage(imageName2,
88 88
 		`FROM scratch
89
-		 LABEL match="me too"`, true)
89
+                 LABEL match="me too"`, true)
90 90
 	c.Assert(err, check.IsNil)
91 91
 
92 92
 	image3ID, err := buildImage(imageName3,
93 93
 		`FROM scratch
94
-		 LABEL nomatch me`, true)
94
+                 LABEL nomatch me`, true)
95 95
 	c.Assert(err, check.IsNil)
96 96
 
97 97
 	out, _ := dockerCmd(c, "images", "--no-trunc", "-q", "-f", "label=match")
... ...
@@ -123,9 +126,9 @@ func (s *DockerSuite) TestImagesFilterSpaceTrimCase(c *check.C) {
123 123
 	imageName := "images_filter_test"
124 124
 	buildImage(imageName,
125 125
 		`FROM scratch
126
-		 RUN touch /test/foo
127
-		 RUN touch /test/bar
128
-		 RUN touch /test/baz`, true)
126
+                 RUN touch /test/foo
127
+                 RUN touch /test/bar
128
+                 RUN touch /test/baz`, true)
129 129
 
130 130
 	filters := []string{
131 131
 		"dangling=true",
... ...
@@ -233,3 +236,46 @@ func (s *DockerSuite) TestImagesFilterNameWithPort(c *check.C) {
233 233
 	out, _ = dockerCmd(c, "images", tag+":no-such-tag")
234 234
 	c.Assert(out, checker.Not(checker.Contains), tag)
235 235
 }
236
+
237
+func (s *DockerSuite) TestImagesFormat(c *check.C) {
238
+	// testRequires(c, DaemonIsLinux)
239
+	tag := "myimage"
240
+	dockerCmd(c, "tag", "busybox", tag+":v1")
241
+	dockerCmd(c, "tag", "busybox", tag+":v2")
242
+
243
+	out, _ := dockerCmd(c, "images", "--format", "{{.Repository}}", tag)
244
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
245
+
246
+	expected := []string{"myimage", "myimage"}
247
+	var names []string
248
+	for _, l := range lines {
249
+		names = append(names, l)
250
+	}
251
+	c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
252
+}
253
+
254
+// ImagesDefaultFormatAndQuiet
255
+func (s *DockerSuite) TestImagesFormatDefaultFormat(c *check.C) {
256
+	testRequires(c, DaemonIsLinux)
257
+
258
+	// create container 1
259
+	out, _ := dockerCmd(c, "run", "-d", "busybox", "true")
260
+	containerID1 := strings.TrimSpace(out)
261
+
262
+	// tag as foobox
263
+	out, _ = dockerCmd(c, "commit", containerID1, "myimage")
264
+	imageID := stringid.TruncateID(strings.TrimSpace(out))
265
+
266
+	config := `{
267
+		"imagesFormat": "{{ .ID }} default"
268
+}`
269
+	d, err := ioutil.TempDir("", "integration-cli-")
270
+	c.Assert(err, checker.IsNil)
271
+	defer os.RemoveAll(d)
272
+
273
+	err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644)
274
+	c.Assert(err, checker.IsNil)
275
+
276
+	out, _ = dockerCmd(c, "--config", d, "images", "-q", "myimage")
277
+	c.Assert(out, checker.Equals, imageID+"\n", check.Commentf("Expected to print only the image id, got %v\n", out))
278
+}
... ...
@@ -568,7 +568,7 @@ func (s *DockerSuite) TestPsFormatHeaders(c *check.C) {
568 568
 func (s *DockerSuite) TestPsDefaultFormatAndQuiet(c *check.C) {
569 569
 	testRequires(c, DaemonIsLinux)
570 570
 	config := `{
571
-		"psFormat": "{{ .ID }} default"
571
+		"psFormat": "default {{ .ID }}"
572 572
 }`
573 573
 	d, err := ioutil.TempDir("", "integration-cli-")
574 574
 	c.Assert(err, checker.IsNil)
... ...
@@ -40,6 +40,17 @@ versions.
40 40
 **-f**, **--filter**=[]
41 41
    Filters the output. The dangling=true filter finds unused images. While label=com.foo=amd64 filters for images with a com.foo value of amd64. The label=com.foo filter finds images with the label com.foo of any value.
42 42
 
43
+**--format**="*TEMPLATE*"
44
+   Pretty-print containers using a Go template.
45
+   Valid placeholders:
46
+      .ID - Image ID
47
+      .Repository - Image repository
48
+      .Tag - Image tag
49
+      .Digest - Image digest
50
+      .CreatedSince - Elapsed time since the image was created.
51
+      .CreatedAt - Time when the image was created..
52
+      .Size - Image disk size.
53
+
43 54
 **--help**
44 55
   Print usage statement
45 56