Browse Source

Merge pull request #28213 from yongtang/11062016-service-ps-format

Add `--format` to `docker service ps`
Anusha Ragunathan 8 years ago
parent
commit
b1debf1374

+ 1 - 0
cli/command/cli.go

@@ -38,6 +38,7 @@ type Cli interface {
 	Out() *OutStream
 	Out() *OutStream
 	Err() io.Writer
 	Err() io.Writer
 	In() *InStream
 	In() *InStream
+	ConfigFile() *configfile.ConfigFile
 }
 }
 
 
 // DockerCli is an instance the docker command line client.
 // DockerCli is an instance the docker command line client.

+ 145 - 0
cli/command/formatter/task.go

@@ -0,0 +1,145 @@
+package formatter
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/go-units"
+)
+
+const (
+	defaultTaskTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Image}}\t{{.Node}}\t{{.DesiredState}}\t{{.CurrentState}}\t{{.Error}}\t{{.Ports}}"
+
+	nodeHeader         = "NODE"
+	taskIDHeader       = "ID"
+	desiredStateHeader = "DESIRED STATE"
+	currentStateHeader = "CURRENT STATE"
+	errorHeader        = "ERROR"
+
+	maxErrLength = 30
+)
+
+// NewTaskFormat returns a Format for rendering using a task Context
+func NewTaskFormat(source string, quiet bool) Format {
+	switch source {
+	case TableFormatKey:
+		if quiet {
+			return defaultQuietFormat
+		}
+		return defaultTaskTableFormat
+	case RawFormatKey:
+		if quiet {
+			return `id: {{.ID}}`
+		}
+		return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n`
+	}
+	return Format(source)
+}
+
+// TaskWrite writes the context
+func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error {
+	render := func(format func(subContext subContext) error) error {
+		for _, task := range tasks {
+			taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]}
+			if err := format(taskCtx); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+	return ctx.Write(&taskContext{}, render)
+}
+
+type taskContext struct {
+	HeaderContext
+	trunc bool
+	task  swarm.Task
+	name  string
+	node  string
+}
+
+func (c *taskContext) MarshalJSON() ([]byte, error) {
+	return marshalJSON(c)
+}
+
+func (c *taskContext) ID() string {
+	c.AddHeader(taskIDHeader)
+	if c.trunc {
+		return stringid.TruncateID(c.task.ID)
+	}
+	return c.task.ID
+}
+
+func (c *taskContext) Name() string {
+	c.AddHeader(nameHeader)
+	return c.name
+}
+
+func (c *taskContext) Image() string {
+	c.AddHeader(imageHeader)
+	image := c.task.Spec.ContainerSpec.Image
+	if c.trunc {
+		ref, err := reference.ParseNormalizedNamed(image)
+		if err == nil {
+			// update image string for display, (strips any digest)
+			if nt, ok := ref.(reference.NamedTagged); ok {
+				if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
+					image = reference.FamiliarString(namedTagged)
+				}
+			}
+		}
+	}
+	return image
+}
+
+func (c *taskContext) Node() string {
+	c.AddHeader(nodeHeader)
+	return c.node
+}
+
+func (c *taskContext) DesiredState() string {
+	c.AddHeader(desiredStateHeader)
+	return command.PrettyPrint(c.task.DesiredState)
+}
+
+func (c *taskContext) CurrentState() string {
+	c.AddHeader(currentStateHeader)
+	return fmt.Sprintf("%s %s ago",
+		command.PrettyPrint(c.task.Status.State),
+		strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))),
+	)
+}
+
+func (c *taskContext) Error() string {
+	c.AddHeader(errorHeader)
+	// Trim and quote the error message.
+	taskErr := c.task.Status.Err
+	if c.trunc && len(taskErr) > maxErrLength {
+		taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1])
+	}
+	if len(taskErr) > 0 {
+		taskErr = fmt.Sprintf("\"%s\"", taskErr)
+	}
+	return taskErr
+}
+
+func (c *taskContext) Ports() string {
+	c.AddHeader(portsHeader)
+	if len(c.task.Status.PortStatus.Ports) == 0 {
+		return ""
+	}
+	ports := []string{}
+	for _, pConfig := range c.task.Status.PortStatus.Ports {
+		ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
+			pConfig.PublishedPort,
+			pConfig.TargetPort,
+			pConfig.Protocol,
+		))
+	}
+	return strings.Join(ports, ",")
+}

+ 107 - 0
cli/command/formatter/task_test.go

@@ -0,0 +1,107 @@
+package formatter
+
+import (
+	"bytes"
+	"encoding/json"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestTaskContextWrite(t *testing.T) {
+	cases := []struct {
+		context  Context
+		expected string
+	}{
+		{
+			Context{Format: "{{InvalidFunction}}"},
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
+`,
+		},
+		{
+			Context{Format: "{{nil}}"},
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
+`,
+		},
+		{
+			Context{Format: NewTaskFormat("table", true)},
+			`taskID1
+taskID2
+`,
+		},
+		{
+			Context{Format: NewTaskFormat("table {{.Name}} {{.Node}} {{.Ports}}", false)},
+			`NAME                NODE                PORTS
+foobar_baz foo1 
+foobar_bar foo2 
+`,
+		},
+		{
+			Context{Format: NewTaskFormat("table {{.Name}}", true)},
+			`NAME
+foobar_baz
+foobar_bar
+`,
+		},
+		{
+			Context{Format: NewTaskFormat("raw", true)},
+			`id: taskID1
+id: taskID2
+`,
+		},
+		{
+			Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)},
+			`foobar_baz foo1
+foobar_bar foo2
+`,
+		},
+	}
+
+	for _, testcase := range cases {
+		tasks := []swarm.Task{
+			{ID: "taskID1"},
+			{ID: "taskID2"},
+		}
+		names := map[string]string{
+			"taskID1": "foobar_baz",
+			"taskID2": "foobar_bar",
+		}
+		nodes := map[string]string{
+			"taskID1": "foo1",
+			"taskID2": "foo2",
+		}
+		out := bytes.NewBufferString("")
+		testcase.context.Output = out
+		err := TaskWrite(testcase.context, tasks, names, nodes)
+		if err != nil {
+			assert.Error(t, err, testcase.expected)
+		} else {
+			assert.Equal(t, out.String(), testcase.expected)
+		}
+	}
+}
+
+func TestTaskContextWriteJSONField(t *testing.T) {
+	tasks := []swarm.Task{
+		{ID: "taskID1"},
+		{ID: "taskID2"},
+	}
+	names := map[string]string{
+		"taskID1": "foobar_baz",
+		"taskID2": "foobar_bar",
+	}
+	out := bytes.NewBufferString("")
+	err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
+		var s string
+		if err := json.Unmarshal([]byte(line), &s); err != nil {
+			t.Fatal(err)
+		}
+		assert.Equal(t, s, tasks[i].ID)
+	}
+}

+ 15 - 1
cli/command/node/ps.go

@@ -8,6 +8,7 @@ import (
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/task"
 	"github.com/docker/docker/cli/command/task"
 	"github.com/docker/docker/opts"
 	"github.com/docker/docker/opts"
@@ -19,6 +20,8 @@ type psOptions struct {
 	nodeIDs   []string
 	nodeIDs   []string
 	noResolve bool
 	noResolve bool
 	noTrunc   bool
 	noTrunc   bool
+	quiet     bool
+	format    string
 	filter    opts.FilterOpt
 	filter    opts.FilterOpt
 }
 }
 
 
@@ -43,6 +46,8 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command {
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
+	flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
 
 
 	return cmd
 	return cmd
 }
 }
@@ -81,7 +86,16 @@ func runPs(dockerCli command.Cli, opts psOptions) error {
 		tasks = append(tasks, nodeTasks...)
 		tasks = append(tasks, nodeTasks...)
 	}
 	}
 
 
-	if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc); err != nil {
+	format := opts.format
+	if len(format) == 0 {
+		if dockerCli.ConfigFile() != nil && len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
+			format = dockerCli.ConfigFile().TasksFormat
+		} else {
+			format = formatter.TableFormatKey
+		}
+	}
+
+	if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil {
 		errs = append(errs, err.Error())
 		errs = append(errs, err.Error())
 	}
 	}
 
 

+ 12 - 3
cli/command/service/ps.go

@@ -10,6 +10,7 @@ import (
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/node"
 	"github.com/docker/docker/cli/command/node"
 	"github.com/docker/docker/cli/command/task"
 	"github.com/docker/docker/cli/command/task"
@@ -22,6 +23,7 @@ type psOptions struct {
 	quiet     bool
 	quiet     bool
 	noResolve bool
 	noResolve bool
 	noTrunc   bool
 	noTrunc   bool
+	format    string
 	filter    opts.FilterOpt
 	filter    opts.FilterOpt
 }
 }
 
 
@@ -41,6 +43,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
+	flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
 
 
 	return cmd
 	return cmd
@@ -107,8 +110,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
 		return err
 		return err
 	}
 	}
 
 
-	if opts.quiet {
-		return task.PrintQuiet(dockerCli, tasks)
+	format := opts.format
+	if len(format) == 0 {
+		if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
+			format = dockerCli.ConfigFile().TasksFormat
+		} else {
+			format = formatter.TableFormatKey
+		}
 	}
 	}
-	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc)
+
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format)
 }
 }

+ 15 - 1
cli/command/stack/ps.go

@@ -8,6 +8,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/task"
 	"github.com/docker/docker/cli/command/task"
 	"github.com/docker/docker/opts"
 	"github.com/docker/docker/opts"
@@ -19,6 +20,8 @@ type psOptions struct {
 	noTrunc   bool
 	noTrunc   bool
 	namespace string
 	namespace string
 	noResolve bool
 	noResolve bool
+	quiet     bool
+	format    string
 }
 }
 
 
 func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
 func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
@@ -37,6 +40,8 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
 	flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
 	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
+	flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
 
 
 	return cmd
 	return cmd
 }
 }
@@ -58,5 +63,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc)
+	format := opts.format
+	if len(format) == 0 {
+		if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
+			format = dockerCli.ConfigFile().TasksFormat
+		} else {
+			format = formatter.TableFormatKey
+		}
+	}
+
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format)
 }
 }

+ 15 - 90
cli/command/task/print.go

@@ -2,42 +2,16 @@ package task
 
 
 import (
 import (
 	"fmt"
 	"fmt"
-	"io"
 	"sort"
 	"sort"
-	"strings"
-	"text/tabwriter"
-	"time"
 
 
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
 
 
-	"github.com/docker/distribution/reference"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/cli/command/idresolver"
 	"github.com/docker/docker/cli/command/idresolver"
-	"github.com/docker/docker/pkg/stringid"
-	"github.com/docker/go-units"
 )
 )
 
 
-const (
-	psTaskItemFmt = "%s\t%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n"
-	maxErrLength  = 30
-)
-
-type portStatus swarm.PortStatus
-
-func (ps portStatus) String() string {
-	if len(ps.Ports) == 0 {
-		return ""
-	}
-
-	str := fmt.Sprintf("*:%d->%d/%s", ps.Ports[0].PublishedPort, ps.Ports[0].TargetPort, ps.Ports[0].Protocol)
-	for _, pConfig := range ps.Ports[1:] {
-		str += fmt.Sprintf(",*:%d->%d/%s", pConfig.PublishedPort, pConfig.TargetPort, pConfig.Protocol)
-	}
-
-	return str
-}
-
 type tasksBySlot []swarm.Task
 type tasksBySlot []swarm.Task
 
 
 func (t tasksBySlot) Len() int {
 func (t tasksBySlot) Len() int {
@@ -58,42 +32,23 @@ func (t tasksBySlot) Less(i, j int) bool {
 	return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
 	return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
 }
 }
 
 
-// Print task information in a table format.
+// Print task information in a format.
 // Besides this, command `docker node ps <node>`
 // Besides this, command `docker node ps <node>`
 // and `docker stack ps` will call this, too.
 // and `docker stack ps` will call this, too.
-func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error {
+func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error {
 	sort.Stable(tasksBySlot(tasks))
 	sort.Stable(tasksBySlot(tasks))
 
 
-	writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0)
-
-	// Ignore flushing errors
-	defer writer.Flush()
-	fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t"))
+	names := map[string]string{}
+	nodes := map[string]string{}
 
 
-	return print(writer, ctx, tasks, resolver, noTrunc)
-}
-
-// PrintQuiet shows task list in a quiet way.
-func PrintQuiet(dockerCli command.Cli, tasks []swarm.Task) error {
-	sort.Stable(tasksBySlot(tasks))
-
-	out := dockerCli.Out()
-
-	for _, task := range tasks {
-		fmt.Fprintln(out, task.ID)
+	tasksCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewTaskFormat(format, quiet),
+		Trunc:  trunc,
 	}
 	}
 
 
-	return nil
-}
-
-func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error {
 	prevName := ""
 	prevName := ""
 	for _, task := range tasks {
 	for _, task := range tasks {
-		id := task.ID
-		if !noTrunc {
-			id = stringid.TruncateID(id)
-		}
-
 		serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
 		serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -118,42 +73,12 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr
 		}
 		}
 		prevName = name
 		prevName = name
 
 
-		// Trim and quote the error message.
-		taskErr := task.Status.Err
-		if !noTrunc && len(taskErr) > maxErrLength {
-			taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1])
-		}
-		if len(taskErr) > 0 {
-			taskErr = fmt.Sprintf("\"%s\"", taskErr)
-		}
-
-		image := task.Spec.ContainerSpec.Image
-		if !noTrunc {
-			ref, err := reference.ParseNormalizedNamed(image)
-			if err == nil {
-				// update image string for display, (strips any digest)
-				if nt, ok := ref.(reference.NamedTagged); ok {
-					if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
-						image = reference.FamiliarString(namedTagged)
-					}
-				}
-
-			}
+		names[task.ID] = name
+		if tasksCtx.Format.IsTable() {
+			names[task.ID] = indentedName
 		}
 		}
-
-		fmt.Fprintf(
-			out,
-			psTaskItemFmt,
-			id,
-			indentedName,
-			image,
-			nodeValue,
-			command.PrettyPrint(task.DesiredState),
-			command.PrettyPrint(task.Status.State),
-			strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))),
-			taskErr,
-			portStatus(task.Status.PortStatus),
-		)
+		nodes[task.ID] = nodeValue
 	}
 	}
-	return nil
+
+	return formatter.TaskWrite(tasksCtx, tasks, names, nodes)
 }
 }

+ 1 - 0
cli/config/configfile/file.go

@@ -36,6 +36,7 @@ type ConfigFile struct {
 	Filename             string                      `json:"-"` // Note: for internal use only
 	Filename             string                      `json:"-"` // Note: for internal use only
 	ServiceInspectFormat string                      `json:"serviceInspectFormat,omitempty"`
 	ServiceInspectFormat string                      `json:"serviceInspectFormat,omitempty"`
 	ServicesFormat       string                      `json:"servicesFormat,omitempty"`
 	ServicesFormat       string                      `json:"servicesFormat,omitempty"`
+	TasksFormat          string                      `json:"tasksFormat,omitempty"`
 }
 }
 
 
 // LegacyLoadFromReader reads the non-nested configuration data given and sets up the
 // LegacyLoadFromReader reads the non-nested configuration data given and sets up the

+ 37 - 4
docs/reference/commandline/node_ps.md

@@ -22,10 +22,12 @@ Usage:  docker node ps [OPTIONS] [NODE...]
 List tasks running on one or more nodes, defaults to current node.
 List tasks running on one or more nodes, defaults to current node.
 
 
 Options:
 Options:
-  -f, --filter value   Filter output based on conditions provided
-      --help           Print usage
-      --no-resolve     Do not map IDs to Names
-      --no-trunc       Do not truncate output
+  -f, --filter filter   Filter output based on conditions provided
+      --format string   Pretty-print tasks using a Go template
+      --help            Print usage
+      --no-resolve      Do not map IDs to Names
+      --no-trunc        Do not truncate output
+  -q, --quiet           Only display task IDs
 ```
 ```
 
 
 ## Description
 ## Description
@@ -105,6 +107,37 @@ redis.7.bg8c07zzg87di2mufeq51a2qp  redis:3.0.6  swarm-manager1  Running        R
 The `desired-state` filter can take the values `running`, `shutdown`, and `accepted`.
 The `desired-state` filter can take the values `running`, `shutdown`, and `accepted`.
 
 
 
 
+### Formatting
+
+The formatting options (`--format`) pretty-prints tasks output
+using a Go template.
+
+Valid placeholders for the Go template are listed below:
+
+Placeholder     | Description
+----------------|------------------------------------------------------------------------------------------
+`.Name`         | Task name
+`.Image`        | Task image
+`.Node`         | Node ID
+`.DesiredState` | Desired state of the task (`running`, `shutdown`, and `accepted`)
+`.CurrentState` | Current state of the task
+`.Error`        | Error
+`.Ports`        | Task published ports
+
+When using the `--format` option, the `node ps` command will either
+output the data exactly as the template declares or, when using the
+`table` directive, includes column headers as well.
+
+The following example uses a template without headers and outputs the
+`ID` and `Driver` entries separated by a colon for all tasks:
+
+```bash
+$ docker node ps --format "{{.Name}}: {{.Image}}"
+top.1: busybox
+top.2: busybox
+top.3: busybox
+```
+
 ## Related commands
 ## Related commands
 
 
 * [node demote](node_demote.md)
 * [node demote](node_demote.md)

+ 33 - 0
docs/reference/commandline/service_ps.md

@@ -23,6 +23,7 @@ List the tasks of one or more services
 
 
 Options:
 Options:
   -f, --filter filter   Filter output based on conditions provided
   -f, --filter filter   Filter output based on conditions provided
+      --format string   Pretty-print tasks using a Go template
       --help            Print usage
       --help            Print usage
       --no-resolve      Do not map IDs to Names
       --no-resolve      Do not map IDs to Names
       --no-trunc        Do not truncate output
       --no-trunc        Do not truncate output
@@ -152,6 +153,38 @@ ID            NAME      IMAGE        NODE      DESIRED STATE  CURRENT STATE
 The `desired-state` filter can take the values `running`, `shutdown`, and `accepted`.
 The `desired-state` filter can take the values `running`, `shutdown`, and `accepted`.
 
 
 
 
+### Formatting
+
+The formatting options (`--format`) pretty-prints tasks output
+using a Go template.
+
+Valid placeholders for the Go template are listed below:
+
+Placeholder     | Description
+----------------|------------------------------------------------------------------------------------------
+`.ID`           | Task ID
+`.Name`         | Task name
+`.Image`        | Task image
+`.Node`         | Node ID
+`.DesiredState` | Desired state of the task (`running`, `shutdown`, and `accepted`)
+`.CurrentState` | Current state of the task
+`.Error`        | Error
+`.Ports`        | Task published ports
+
+When using the `--format` option, the `service ps` command will either
+output the data exactly as the template declares or, when using the
+`table` directive, includes column headers as well.
+
+The following example uses a template without headers and outputs the
+`ID` and `Driver` entries separated by a colon for all tasks:
+
+```bash
+$ docker service ps --format "{{.Name}}: {{.Image}}" top
+top.1: busybox
+top.2: busybox
+top.3: busybox
+```
+
 ## Related commands
 ## Related commands
 
 
 * [service create](service_create.md)
 * [service create](service_create.md)

+ 2 - 0
docs/reference/commandline/stack_ps.md

@@ -22,9 +22,11 @@ List the tasks in the stack
 
 
 Options:
 Options:
   -f, --filter filter   Filter output based on conditions provided
   -f, --filter filter   Filter output based on conditions provided
+      --format string   Pretty-print tasks using a Go template
       --help            Print usage
       --help            Print usage
       --no-resolve      Do not map IDs to Names
       --no-resolve      Do not map IDs to Names
       --no-trunc        Do not truncate output
       --no-trunc        Do not truncate output
+  -q, --quiet           Only display task IDs
 ```
 ```
 
 
 ## Description
 ## Description