Browse code

Add `--format` to `docker service ls` This fix tries to improve the display of `docker service ls` and adds `--format` flag to `docker service ls`.

In addition to `--format` flag, several other improvement:
1. Updates `docker stacks service`.
2. Adds `servicesFormat` to config file.

Related docs has been updated.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2017/01/27 06:08:07
Showing 8 changed files
... ...
@@ -5,9 +5,11 @@ import (
5 5
 	"strings"
6 6
 	"time"
7 7
 
8
+	distreference "github.com/docker/distribution/reference"
8 9
 	mounttypes "github.com/docker/docker/api/types/mount"
9 10
 	"github.com/docker/docker/api/types/swarm"
10 11
 	"github.com/docker/docker/cli/command/inspect"
12
+	"github.com/docker/docker/pkg/stringid"
11 13
 	units "github.com/docker/go-units"
12 14
 )
13 15
 
... ...
@@ -327,3 +329,93 @@ func (ctx *serviceInspectContext) EndpointMode() string {
327 327
 func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
328 328
 	return ctx.Service.Endpoint.Ports
329 329
 }
330
+
331
+const (
332
+	defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}"
333
+
334
+	serviceIDHeader = "ID"
335
+	modeHeader      = "MODE"
336
+	replicasHeader  = "REPLICAS"
337
+)
338
+
339
+// NewServiceListFormat returns a Format for rendering using a service Context
340
+func NewServiceListFormat(source string, quiet bool) Format {
341
+	switch source {
342
+	case TableFormatKey:
343
+		if quiet {
344
+			return defaultQuietFormat
345
+		}
346
+		return defaultServiceTableFormat
347
+	case RawFormatKey:
348
+		if quiet {
349
+			return `id: {{.ID}}`
350
+		}
351
+		return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\n`
352
+	}
353
+	return Format(source)
354
+}
355
+
356
+// ServiceListInfo stores the information about mode and replicas to be used by template
357
+type ServiceListInfo struct {
358
+	Mode     string
359
+	Replicas string
360
+}
361
+
362
+// ServiceListWrite writes the context
363
+func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error {
364
+	render := func(format func(subContext subContext) error) error {
365
+		for _, service := range services {
366
+			serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
367
+			if err := format(serviceCtx); err != nil {
368
+				return err
369
+			}
370
+		}
371
+		return nil
372
+	}
373
+	return ctx.Write(&serviceContext{}, render)
374
+}
375
+
376
+type serviceContext struct {
377
+	HeaderContext
378
+	service  swarm.Service
379
+	mode     string
380
+	replicas string
381
+}
382
+
383
+func (c *serviceContext) MarshalJSON() ([]byte, error) {
384
+	return marshalJSON(c)
385
+}
386
+
387
+func (c *serviceContext) ID() string {
388
+	c.AddHeader(serviceIDHeader)
389
+	return stringid.TruncateID(c.service.ID)
390
+}
391
+
392
+func (c *serviceContext) Name() string {
393
+	c.AddHeader(nameHeader)
394
+	return c.service.Spec.Name
395
+}
396
+
397
+func (c *serviceContext) Mode() string {
398
+	c.AddHeader(modeHeader)
399
+	return c.mode
400
+}
401
+
402
+func (c *serviceContext) Replicas() string {
403
+	c.AddHeader(replicasHeader)
404
+	return c.replicas
405
+}
406
+
407
+func (c *serviceContext) Image() string {
408
+	c.AddHeader(imageHeader)
409
+	image := c.service.Spec.TaskTemplate.ContainerSpec.Image
410
+	if ref, err := distreference.ParseNamed(image); err == nil {
411
+		// update image string for display
412
+		namedTagged, ok := ref.(distreference.NamedTagged)
413
+		if ok {
414
+			image = namedTagged.Name() + ":" + namedTagged.Tag()
415
+		}
416
+	}
417
+
418
+	return image
419
+}
330 420
new file mode 100644
... ...
@@ -0,0 +1,177 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"strings"
6
+	"testing"
7
+
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"github.com/docker/docker/pkg/testutil/assert"
10
+)
11
+
12
+func TestServiceContextWrite(t *testing.T) {
13
+	cases := []struct {
14
+		context  Context
15
+		expected string
16
+	}{
17
+		// Errors
18
+		{
19
+			Context{Format: "{{InvalidFunction}}"},
20
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
21
+`,
22
+		},
23
+		{
24
+			Context{Format: "{{nil}}"},
25
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
26
+`,
27
+		},
28
+		// Table format
29
+		{
30
+			Context{Format: NewServiceListFormat("table", false)},
31
+			`ID                  NAME                MODE                REPLICAS            IMAGE
32
+id_baz              baz                 global              2/4                 
33
+id_bar              bar                 replicated          2/4                 
34
+`,
35
+		},
36
+		{
37
+			Context{Format: NewServiceListFormat("table", true)},
38
+			`id_baz
39
+id_bar
40
+`,
41
+		},
42
+		{
43
+			Context{Format: NewServiceListFormat("table {{.Name}}", false)},
44
+			`NAME
45
+baz
46
+bar
47
+`,
48
+		},
49
+		{
50
+			Context{Format: NewServiceListFormat("table {{.Name}}", true)},
51
+			`NAME
52
+baz
53
+bar
54
+`,
55
+		},
56
+		// Raw Format
57
+		{
58
+			Context{Format: NewServiceListFormat("raw", false)},
59
+			`id: id_baz
60
+name: baz
61
+mode: global
62
+replicas: 2/4
63
+image: 
64
+
65
+id: id_bar
66
+name: bar
67
+mode: replicated
68
+replicas: 2/4
69
+image: 
70
+
71
+`,
72
+		},
73
+		{
74
+			Context{Format: NewServiceListFormat("raw", true)},
75
+			`id: id_baz
76
+id: id_bar
77
+`,
78
+		},
79
+		// Custom Format
80
+		{
81
+			Context{Format: NewServiceListFormat("{{.Name}}", false)},
82
+			`baz
83
+bar
84
+`,
85
+		},
86
+	}
87
+
88
+	for _, testcase := range cases {
89
+		services := []swarm.Service{
90
+			{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
91
+			{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
92
+		}
93
+		info := map[string]ServiceListInfo{
94
+			"id_baz": {
95
+				Mode:     "global",
96
+				Replicas: "2/4",
97
+			},
98
+			"id_bar": {
99
+				Mode:     "replicated",
100
+				Replicas: "2/4",
101
+			},
102
+		}
103
+		out := bytes.NewBufferString("")
104
+		testcase.context.Output = out
105
+		err := ServiceListWrite(testcase.context, services, info)
106
+		if err != nil {
107
+			assert.Error(t, err, testcase.expected)
108
+		} else {
109
+			assert.Equal(t, out.String(), testcase.expected)
110
+		}
111
+	}
112
+}
113
+
114
+func TestServiceContextWriteJSON(t *testing.T) {
115
+	services := []swarm.Service{
116
+		{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
117
+		{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
118
+	}
119
+	info := map[string]ServiceListInfo{
120
+		"id_baz": {
121
+			Mode:     "global",
122
+			Replicas: "2/4",
123
+		},
124
+		"id_bar": {
125
+			Mode:     "replicated",
126
+			Replicas: "2/4",
127
+		},
128
+	}
129
+	expectedJSONs := []map[string]interface{}{
130
+		{"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": ""},
131
+		{"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": ""},
132
+	}
133
+
134
+	out := bytes.NewBufferString("")
135
+	err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info)
136
+	if err != nil {
137
+		t.Fatal(err)
138
+	}
139
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
140
+		t.Logf("Output: line %d: %s", i, line)
141
+		var m map[string]interface{}
142
+		if err := json.Unmarshal([]byte(line), &m); err != nil {
143
+			t.Fatal(err)
144
+		}
145
+		assert.DeepEqual(t, m, expectedJSONs[i])
146
+	}
147
+}
148
+func TestServiceContextWriteJSONField(t *testing.T) {
149
+	services := []swarm.Service{
150
+		{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
151
+		{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
152
+	}
153
+	info := map[string]ServiceListInfo{
154
+		"id_baz": {
155
+			Mode:     "global",
156
+			Replicas: "2/4",
157
+		},
158
+		"id_bar": {
159
+			Mode:     "replicated",
160
+			Replicas: "2/4",
161
+		},
162
+	}
163
+	out := bytes.NewBufferString("")
164
+	err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info)
165
+	if err != nil {
166
+		t.Fatal(err)
167
+	}
168
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
169
+		t.Logf("Output: line %d: %s", i, line)
170
+		var s string
171
+		if err := json.Unmarshal([]byte(line), &s); err != nil {
172
+			t.Fatal(err)
173
+		}
174
+		assert.Equal(t, s, services[i].Spec.Name)
175
+	}
176
+}
... ...
@@ -2,27 +2,21 @@ package service
2 2
 
3 3
 import (
4 4
 	"fmt"
5
-	"io"
6
-	"text/tabwriter"
7 5
 
8
-	distreference "github.com/docker/distribution/reference"
9 6
 	"github.com/docker/docker/api/types"
10 7
 	"github.com/docker/docker/api/types/filters"
11 8
 	"github.com/docker/docker/api/types/swarm"
12 9
 	"github.com/docker/docker/cli"
13 10
 	"github.com/docker/docker/cli/command"
11
+	"github.com/docker/docker/cli/command/formatter"
14 12
 	"github.com/docker/docker/opts"
15
-	"github.com/docker/docker/pkg/stringid"
16 13
 	"github.com/spf13/cobra"
17 14
 	"golang.org/x/net/context"
18 15
 )
19 16
 
20
-const (
21
-	listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
22
-)
23
-
24 17
 type listOptions struct {
25 18
 	quiet  bool
19
+	format string
26 20
 	filter opts.FilterOpt
27 21
 }
28 22
 
... ...
@@ -41,6 +35,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
41 41
 
42 42
 	flags := cmd.Flags()
43 43
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
44
+	flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template")
44 45
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
45 46
 
46 47
 	return cmd
... ...
@@ -49,13 +44,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
49 49
 func runList(dockerCli *command.DockerCli, opts listOptions) error {
50 50
 	ctx := context.Background()
51 51
 	client := dockerCli.Client()
52
-	out := dockerCli.Out()
53 52
 
54 53
 	services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: opts.filter.Value()})
55 54
 	if err != nil {
56 55
 		return err
57 56
 	}
58 57
 
58
+	info := map[string]formatter.ServiceListInfo{}
59 59
 	if len(services) > 0 && !opts.quiet {
60 60
 		// only non-empty services and not quiet, should we call TaskList and NodeList api
61 61
 		taskFilter := filters.NewArgs()
... ...
@@ -73,20 +68,30 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
73 73
 			return err
74 74
 		}
75 75
 
76
-		PrintNotQuiet(out, services, nodes, tasks)
77
-	} else if !opts.quiet {
78
-		// no services and not quiet, print only one line with columns ID, NAME, MODE, REPLICAS...
79
-		PrintNotQuiet(out, services, []swarm.Node{}, []swarm.Task{})
80
-	} else {
81
-		PrintQuiet(out, services)
76
+		info = GetServicesStatus(services, nodes, tasks)
77
+	}
78
+
79
+	format := opts.format
80
+	if len(format) == 0 {
81
+		if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
82
+			format = dockerCli.ConfigFile().ServicesFormat
83
+		} else {
84
+			format = formatter.TableFormatKey
85
+		}
82 86
 	}
83 87
 
84
-	return nil
88
+	servicesCtx := formatter.Context{
89
+		Output: dockerCli.Out(),
90
+		Format: formatter.NewServiceListFormat(format, opts.quiet),
91
+	}
92
+	return formatter.ServiceListWrite(servicesCtx, services, info)
85 93
 }
86 94
 
87
-// PrintNotQuiet shows service list in a non-quiet way.
88
-// Besides this, command `docker stack services xxx` will call this, too.
89
-func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) {
95
+// GetServicesStatus returns a map of mode and replicas
96
+func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo {
97
+	running := map[string]int{}
98
+	tasksNoShutdown := map[string]int{}
99
+
90 100
 	activeNodes := make(map[string]struct{})
91 101
 	for _, n := range nodes {
92 102
 		if n.Status.State != swarm.NodeStateDown {
... ...
@@ -94,9 +99,6 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node,
94 94
 		}
95 95
 	}
96 96
 
97
-	running := map[string]int{}
98
-	tasksNoShutdown := map[string]int{}
99
-
100 97
 	for _, task := range tasks {
101 98
 		if task.DesiredState != swarm.TaskStateShutdown {
102 99
 			tasksNoShutdown[task.ServiceID]++
... ...
@@ -107,52 +109,20 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node,
107 107
 		}
108 108
 	}
109 109
 
110
-	printTable(out, services, running, tasksNoShutdown)
111
-}
112
-
113
-func printTable(out io.Writer, services []swarm.Service, running, tasksNoShutdown map[string]int) {
114
-	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
115
-
116
-	// Ignore flushing errors
117
-	defer writer.Flush()
118
-
119
-	fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MODE", "REPLICAS", "IMAGE")
120
-
110
+	info := map[string]formatter.ServiceListInfo{}
121 111
 	for _, service := range services {
122
-		mode := ""
123
-		replicas := ""
112
+		info[service.ID] = formatter.ServiceListInfo{}
124 113
 		if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
125
-			mode = "replicated"
126
-			replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas)
114
+			info[service.ID] = formatter.ServiceListInfo{
115
+				Mode:     "replicated",
116
+				Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas),
117
+			}
127 118
 		} else if service.Spec.Mode.Global != nil {
128
-			mode = "global"
129
-			replicas = fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID])
130
-		}
131
-		image := service.Spec.TaskTemplate.ContainerSpec.Image
132
-		ref, err := distreference.ParseNamed(image)
133
-		if err == nil {
134
-			// update image string for display
135
-			namedTagged, ok := ref.(distreference.NamedTagged)
136
-			if ok {
137
-				image = namedTagged.Name() + ":" + namedTagged.Tag()
119
+			info[service.ID] = formatter.ServiceListInfo{
120
+				Mode:     "global",
121
+				Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]),
138 122
 			}
139 123
 		}
140
-
141
-		fmt.Fprintf(
142
-			writer,
143
-			listItemFmt,
144
-			stringid.TruncateID(service.ID),
145
-			service.Spec.Name,
146
-			mode,
147
-			replicas,
148
-			image)
149
-	}
150
-}
151
-
152
-// PrintQuiet shows service list in a quiet way.
153
-// Besides this, command `docker stack services xxx` will call this, too.
154
-func PrintQuiet(out io.Writer, services []swarm.Service) {
155
-	for _, service := range services {
156
-		fmt.Fprintln(out, service.ID)
157 124
 	}
125
+	return info
158 126
 }
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"github.com/docker/docker/api/types/filters"
10 10
 	"github.com/docker/docker/cli"
11 11
 	"github.com/docker/docker/cli/command"
12
+	"github.com/docker/docker/cli/command/formatter"
12 13
 	"github.com/docker/docker/cli/command/service"
13 14
 	"github.com/docker/docker/opts"
14 15
 	"github.com/spf13/cobra"
... ...
@@ -16,6 +17,7 @@ import (
16 16
 
17 17
 type servicesOptions struct {
18 18
 	quiet     bool
19
+	format    string
19 20
 	filter    opts.FilterOpt
20 21
 	namespace string
21 22
 }
... ...
@@ -34,6 +36,7 @@ func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command {
34 34
 	}
35 35
 	flags := cmd.Flags()
36 36
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
37
+	flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template")
37 38
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
38 39
 
39 40
 	return cmd
... ...
@@ -57,9 +60,8 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
57 57
 		return nil
58 58
 	}
59 59
 
60
-	if opts.quiet {
61
-		service.PrintQuiet(out, services)
62
-	} else {
60
+	info := map[string]formatter.ServiceListInfo{}
61
+	if !opts.quiet {
63 62
 		taskFilter := filters.NewArgs()
64 63
 		for _, service := range services {
65 64
 			taskFilter.Add("service", service.ID)
... ...
@@ -69,11 +71,27 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
69 69
 		if err != nil {
70 70
 			return err
71 71
 		}
72
+
72 73
 		nodes, err := client.NodeList(ctx, types.NodeListOptions{})
73 74
 		if err != nil {
74 75
 			return err
75 76
 		}
76
-		service.PrintNotQuiet(out, services, nodes, tasks)
77
+
78
+		info = service.GetServicesStatus(services, nodes, tasks)
79
+	}
80
+
81
+	format := opts.format
82
+	if len(format) == 0 {
83
+		if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
84
+			format = dockerCli.ConfigFile().ServicesFormat
85
+		} else {
86
+			format = formatter.TableFormatKey
87
+		}
88
+	}
89
+
90
+	servicesCtx := formatter.Context{
91
+		Output: dockerCli.Out(),
92
+		Format: formatter.NewServiceListFormat(format, opts.quiet),
77 93
 	}
78
-	return nil
94
+	return formatter.ServiceListWrite(servicesCtx, services, info)
79 95
 }
... ...
@@ -35,6 +35,7 @@ type ConfigFile struct {
35 35
 	CredentialHelpers    map[string]string           `json:"credHelpers,omitempty"`
36 36
 	Filename             string                      `json:"-"` // Note: for internal use only
37 37
 	ServiceInspectFormat string                      `json:"serviceInspectFormat,omitempty"`
38
+	ServicesFormat       string                      `json:"servicesFormat,omitempty"`
38 39
 }
39 40
 
40 41
 // LegacyLoadFromReader reads the non-nested configuration data given and sets up the
... ...
@@ -137,6 +137,13 @@ Docker's client uses this property. If this property is not set, the client
137 137
 falls back to the default table format. For a list of supported formatting
138 138
 directives, see the [**Formatting** section in the `docker plugin ls` documentation](plugin_ls.md)
139 139
 
140
+The property `servicesFormat` specifies the default format for `docker
141
+service ls` output. When the `--format` flag is not provided with the
142
+`docker service ls` command, Docker's client uses this property. If this
143
+property is not set, the client falls back to the default json format. For a
144
+list of supported formatting directives, see the
145
+[**Formatting** section in the `docker service ls` documentation](service_ls.md)
146
+
140 147
 The property `serviceInspectFormat` specifies the default format for `docker
141 148
 service inspect` output. When the `--format` flag is not provided with the
142 149
 `docker service inspect` command, Docker's client uses this property. If this
... ...
@@ -194,6 +201,7 @@ Following is a sample `config.json` file:
194 194
       "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
195 195
       "pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}",
196 196
       "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
197
+      "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
197 198
       "serviceInspectFormat": "pretty",
198 199
       "detachKeys": "ctrl-e,e",
199 200
       "credsStore": "secretservice",
... ...
@@ -24,9 +24,10 @@ Aliases:
24 24
   ls, list
25 25
 
26 26
 Options:
27
-  -f, --filter value   Filter output based on conditions provided
28
-      --help           Print usage
29
-  -q, --quiet          Only display IDs
27
+  -f, --filter filter   Filter output based on conditions provided
28
+      --format string   Pretty-print services using a Go template
29
+      --help            Print usage
30
+  -q, --quiet           Only display IDs
30 31
 ```
31 32
 
32 33
 This command when run targeting a manager, lists services are running in the
... ...
@@ -103,6 +104,34 @@ ID            NAME   MODE        REPLICAS  IMAGE
103 103
 0bcjwfh8ychr  redis  replicated  1/1       redis:3.0.6
104 104
 ```
105 105
 
106
+## Formatting
107
+
108
+The formatting options (`--format`) pretty-prints services output
109
+using a Go template.
110
+
111
+Valid placeholders for the Go template are listed below:
112
+
113
+Placeholder | Description
114
+------------|------------------------------------------------------------------------------------------
115
+`.ID`       | Service ID
116
+`.Name`     | Service name
117
+`.Mode`     | Service mode (replicated, global)
118
+`.Replicas` | Service replicas
119
+`.Image`    | Service image
120
+
121
+When using the `--format` option, the `service ls` command will either
122
+output the data exactly as the template declares or, when using the
123
+`table` directive, includes column headers as well.
124
+
125
+The following example uses a template without headers and outputs the
126
+`ID`, `Mode`, and `Replicas` entries separated by a colon for all services:
127
+
128
+```bash
129
+$ docker service ls --format "{{.ID}}: {{.Mode}} {{.Replicas}}"
130
+0zmvwuiu3vue: replicated 10/10
131
+fm6uf97exkul: global 5/5
132
+```
133
+
106 134
 ## Related information
107 135
 
108 136
 * [service create](service_create.md)
... ...
@@ -22,9 +22,10 @@ Usage:	docker stack services [OPTIONS] STACK
22 22
 List the services in the stack
23 23
 
24 24
 Options:
25
-  -f, --filter value   Filter output based on conditions provided
26
-      --help           Print usage
27
-  -q, --quiet          Only display IDs
25
+  -f, --filter filter   Filter output based on conditions provided
26
+      --format string   Pretty-print services using a Go template
27
+      --help            Print usage
28
+  -q, --quiet           Only display IDs
28 29
 ```
29 30
 
30 31
 Lists the services that are running as part of the specified stack. This
... ...
@@ -62,6 +63,35 @@ The currently supported filters are:
62 62
 * name (`--filter name=myapp_web`)
63 63
 * label (`--filter label=key=value`)
64 64
 
65
+## Formatting
66
+
67
+The formatting options (`--format`) pretty-prints services output
68
+using a Go template.
69
+
70
+Valid placeholders for the Go template are listed below:
71
+
72
+Placeholder | Description
73
+------------|------------------------------------------------------------------------------------------
74
+`.ID`       | Service ID
75
+`.Name`     | Service name
76
+`.Mode`     | Service mode (replicated, global)
77
+`.Replicas` | Service replicas
78
+`.Image`    | Service image
79
+
80
+When using the `--format` option, the `stack services` command will either
81
+output the data exactly as the template declares or, when using the
82
+`table` directive, includes column headers as well.
83
+
84
+The following example uses a template without headers and outputs the
85
+`ID`, `Mode`, and `Replicas` entries separated by a colon for all services:
86
+
87
+```bash
88
+$ docker stack services --format "{{.ID}}: {{.Mode}} {{.Replicas}}"
89
+0zmvwuiu3vue: replicated 10/10
90
+fm6uf97exkul: global 5/5
91
+```
92
+
93
+
65 94
 ## Related information
66 95
 
67 96
 * [stack deploy](stack_deploy.md)