Forráskód Böngészése

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

Carry #10255: Docker ps format
David Calavera 10 éve
szülő
commit
40b922418c

+ 4 - 0
api/client/cli.go

@@ -179,6 +179,10 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
 	return nil
 }
 
+func (cli *DockerCli) PsFormat() string {
+	return cli.configFile.PsFormat
+}
+
 // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
 // The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config
 // is set the client scheme will be set to https.

+ 14 - 83
api/client/ps.go

@@ -2,21 +2,14 @@ package client
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/url"
 	"strconv"
-	"strings"
-	"text/tabwriter"
-	"time"
 
-	"github.com/docker/docker/api"
+	"github.com/docker/docker/api/client/ps"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/opts"
 	flag "github.com/docker/docker/pkg/mflag"
 	"github.com/docker/docker/pkg/parsers/filters"
-	"github.com/docker/docker/pkg/stringid"
-	"github.com/docker/docker/pkg/stringutils"
-	"github.com/docker/docker/pkg/units"
 )
 
 // CmdPs outputs a list of Docker containers.
@@ -38,6 +31,7 @@ func (cli *DockerCli) CmdPs(args ...string) error {
 		since    = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running")
 		before   = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name")
 		last     = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running")
+		format   = cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template")
 		flFilter = opts.NewListOpts(nil)
 	)
 	cmd.Require(flag.Exact, 0)
@@ -98,87 +92,24 @@ func (cli *DockerCli) CmdPs(args ...string) error {
 		return err
 	}
 
-	w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
-	if !*quiet {
-		fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
-
-		if *size {
-			fmt.Fprintln(w, "\tSIZE")
+	f := *format
+	if len(f) == 0 {
+		if len(cli.PsFormat()) > 0 {
+			f = cli.PsFormat()
 		} else {
-			fmt.Fprint(w, "\n")
+			f = "table"
 		}
 	}
 
-	stripNamePrefix := func(ss []string) []string {
-		for i, s := range ss {
-			ss[i] = s[1:]
-		}
-
-		return ss
+	psCtx := ps.Context{
+		Output: cli.out,
+		Format: f,
+		Quiet:  *quiet,
+		Size:   *size,
+		Trunc:  !*noTrunc,
 	}
 
-	for _, container := range containers {
-		ID := container.ID
-
-		if !*noTrunc {
-			ID = stringid.TruncateID(ID)
-		}
-
-		if *quiet {
-			fmt.Fprintln(w, ID)
-
-			continue
-		}
-
-		var (
-			names       = stripNamePrefix(container.Names)
-			command     = strconv.Quote(container.Command)
-			displayPort string
-		)
-
-		if !*noTrunc {
-			command = stringutils.Truncate(command, 20)
-
-			// only display the default name for the container with notrunc is passed
-			for _, name := range names {
-				if len(strings.Split(name, "/")) == 1 {
-					names = []string{name}
-					break
-				}
-			}
-		}
-
-		image := container.Image
-		if image == "" {
-			image = "<no image>"
-		}
-
-		if container.HostConfig.NetworkMode == "host" {
-			displayPort = "*/tcp, */udp"
-		} else {
-			displayPort = api.DisplayablePorts(container.Ports)
-		}
-
-		fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", ID, image, command,
-			units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))),
-			container.Status, displayPort, strings.Join(names, ","))
-
-		if *size {
-			if container.SizeRootFs > 0 {
-				fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(float64(container.SizeRw)), units.HumanSize(float64(container.SizeRootFs)))
-			} else {
-				fmt.Fprintf(w, "%s\n", units.HumanSize(float64(container.SizeRw)))
-			}
-
-			continue
-		}
-
-		fmt.Fprint(w, "\n")
-	}
-
-	if !*quiet {
-		w.Flush()
-	}
+	ps.Format(psCtx, containers)
 
 	return nil
 }

+ 210 - 0
api/client/ps/custom.go

@@ -0,0 +1,210 @@
+package ps
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+	"text/template"
+	"time"
+
+	"github.com/docker/docker/api"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/stringutils"
+	"github.com/docker/docker/pkg/units"
+)
+
+const (
+	tableKey = "table"
+
+	idHeader         = "CONTAINER ID"
+	imageHeader      = "IMAGE"
+	namesHeader      = "NAMES"
+	commandHeader    = "COMMAND"
+	createdAtHeader  = "CREATED AT"
+	runningForHeader = "CREATED"
+	statusHeader     = "STATUS"
+	portsHeader      = "PORTS"
+	sizeHeader       = "SIZE"
+	labelsHeader     = "LABELS"
+)
+
+type containerContext struct {
+	trunc  bool
+	header []string
+	c      types.Container
+}
+
+func (c *containerContext) ID() string {
+	c.addHeader(idHeader)
+	if c.trunc {
+		return stringid.TruncateID(c.c.ID)
+	}
+	return c.c.ID
+}
+
+func (c *containerContext) Names() string {
+	c.addHeader(namesHeader)
+	names := stripNamePrefix(c.c.Names)
+	if c.trunc {
+		for _, name := range names {
+			if len(strings.Split(name, "/")) == 1 {
+				names = []string{name}
+				break
+			}
+		}
+	}
+	return strings.Join(names, ",")
+}
+
+func (c *containerContext) Image() string {
+	c.addHeader(imageHeader)
+	if c.c.Image == "" {
+		return "<no image>"
+	}
+	return c.c.Image
+}
+
+func (c *containerContext) Command() string {
+	c.addHeader(commandHeader)
+	command := c.c.Command
+	if c.trunc {
+		command = stringutils.Truncate(command, 20)
+	}
+	return strconv.Quote(command)
+}
+
+func (c *containerContext) CreatedAt() string {
+	c.addHeader(createdAtHeader)
+	return time.Unix(int64(c.c.Created), 0).String()
+}
+
+func (c *containerContext) RunningFor() string {
+	c.addHeader(runningForHeader)
+	createdAt := time.Unix(int64(c.c.Created), 0)
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
+}
+
+func (c *containerContext) Ports() string {
+	c.addHeader(portsHeader)
+	return api.DisplayablePorts(c.c.Ports)
+}
+
+func (c *containerContext) Status() string {
+	c.addHeader(statusHeader)
+	return c.c.Status
+}
+
+func (c *containerContext) Size() string {
+	c.addHeader(sizeHeader)
+	srw := units.HumanSize(float64(c.c.SizeRw))
+	sv := units.HumanSize(float64(c.c.SizeRootFs))
+
+	sf := srw
+	if c.c.SizeRootFs > 0 {
+		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
+	}
+	return sf
+}
+
+func (c *containerContext) Labels() string {
+	c.addHeader(labelsHeader)
+	if c.c.Labels == nil {
+		return ""
+	}
+
+	var joinLabels []string
+	for k, v := range c.c.Labels {
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
+	}
+	return strings.Join(joinLabels, ",")
+}
+
+func (c *containerContext) Label(name string) string {
+	n := strings.Split(name, ".")
+	r := strings.NewReplacer("-", " ", "_", " ")
+	h := r.Replace(n[len(n)-1])
+
+	c.addHeader(h)
+
+	if c.c.Labels == nil {
+		return ""
+	}
+	return c.c.Labels[name]
+}
+
+func (c *containerContext) fullHeader() string {
+	if c.header == nil {
+		return ""
+	}
+	return strings.Join(c.header, "\t")
+}
+
+func (c *containerContext) addHeader(header string) {
+	if c.header == nil {
+		c.header = []string{}
+	}
+	c.header = append(c.header, strings.ToUpper(header))
+}
+
+func customFormat(ctx Context, containers []types.Container) {
+	var (
+		table  bool
+		header string
+		format = ctx.Format
+		buffer = bytes.NewBufferString("")
+	)
+
+	if strings.HasPrefix(ctx.Format, tableKey) {
+		table = true
+		format = format[len(tableKey):]
+	}
+
+	format = strings.Trim(format, " ")
+	r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
+	format = r.Replace(format)
+
+	if table && ctx.Size {
+		format += "\t{{.Size}}"
+	}
+
+	tmpl, err := template.New("ps template").Parse(format)
+	if err != nil {
+		buffer.WriteString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err))
+	}
+
+	for _, container := range containers {
+		containerCtx := &containerContext{
+			trunc: ctx.Trunc,
+			c:     container,
+		}
+		if err := tmpl.Execute(buffer, containerCtx); err != nil {
+			buffer = bytes.NewBufferString(fmt.Sprintf("Invalid `docker ps` format: %v\n", err))
+			break
+		}
+		if table && len(header) == 0 {
+			header = containerCtx.fullHeader()
+		}
+		buffer.WriteString("\n")
+	}
+
+	if table {
+		t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0)
+		t.Write([]byte(header))
+		t.Write([]byte("\n"))
+		buffer.WriteTo(t)
+		t.Flush()
+	} else {
+		buffer.WriteTo(ctx.Output)
+	}
+}
+
+func stripNamePrefix(ss []string) []string {
+	for i, s := range ss {
+		ss[i] = s[1:]
+	}
+
+	return ss
+}

+ 88 - 0
api/client/ps/custom_test.go

@@ -0,0 +1,88 @@
+package ps
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/pkg/stringid"
+)
+
+func TestContainerContextID(t *testing.T) {
+	containerId := stringid.GenerateRandomID()
+	unix := time.Now().Unix()
+
+	var ctx containerContext
+	cases := []struct {
+		container types.Container
+		trunc     bool
+		expValue  string
+		expHeader string
+		call      func() string
+	}{
+		{types.Container{ID: containerId}, true, stringid.TruncateID(containerId), idHeader, ctx.ID},
+		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
+		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
+		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
+		{types.Container{Created: int(unix)}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
+		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
+	}
+
+	for _, c := range cases {
+		ctx = containerContext{c: c.container, trunc: c.trunc}
+		v := c.call()
+		if strings.Contains(v, ",") {
+			// comma-separated values means probably a map input, which won't
+			// be guaranteed to have the same order as our expected value
+			// We'll create maps and use reflect.DeepEquals to check instead:
+			entriesMap := make(map[string]string)
+			expMap := make(map[string]string)
+			entries := strings.Split(v, ",")
+			expectedEntries := strings.Split(c.expValue, ",")
+			for _, entry := range entries {
+				keyval := strings.Split(entry, "=")
+				entriesMap[keyval[0]] = keyval[1]
+			}
+			for _, expected := range expectedEntries {
+				keyval := strings.Split(expected, "=")
+				expMap[keyval[0]] = keyval[1]
+			}
+			if !reflect.DeepEqual(expMap, entriesMap) {
+				t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
+			}
+		} else if v != c.expValue {
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
+		}
+
+		h := ctx.fullHeader()
+		if h != c.expHeader {
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
+		}
+	}
+
+	c := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
+	ctx = containerContext{c: c, trunc: true}
+
+	sid := ctx.Label("com.docker.swarm.swarm-id")
+	node := ctx.Label("com.docker.swarm.node_name")
+	if sid != "33" {
+		t.Fatalf("Expected 33, was %s\n", sid)
+	}
+
+	if node != "ubuntu" {
+		t.Fatalf("Expected ubuntu, was %s\n", node)
+	}
+
+	h := ctx.fullHeader()
+	if h != "SWARM ID\tNODE NAME" {
+		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
+
+	}
+}

+ 65 - 0
api/client/ps/formatter.go

@@ -0,0 +1,65 @@
+package ps
+
+import (
+	"io"
+
+	"github.com/docker/docker/api/types"
+)
+
+const (
+	tableFormatKey = "table"
+	rawFormatKey   = "raw"
+
+	defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
+	defaultQuietFormat = "{{.ID}}"
+)
+
+type Context struct {
+	Output io.Writer
+	Format string
+	Size   bool
+	Quiet  bool
+	Trunc  bool
+}
+
+func Format(ctx Context, containers []types.Container) {
+	switch ctx.Format {
+	case tableFormatKey:
+		tableFormat(ctx, containers)
+	case rawFormatKey:
+		rawFormat(ctx, containers)
+	default:
+		customFormat(ctx, containers)
+	}
+}
+
+func rawFormat(ctx Context, containers []types.Container) {
+	if ctx.Quiet {
+		ctx.Format = `container_id: {{.ID}}`
+	} else {
+		ctx.Format = `container_id: {{.ID}}
+image: {{.Image}}
+command: {{.Command}}
+created_at: {{.CreatedAt}}
+status: {{.Status}}
+names: {{.Names}}
+labels: {{.Labels}}
+ports: {{.Ports}}
+`
+		if ctx.Size {
+			ctx.Format += `size: {{.Size}}
+`
+		}
+	}
+
+	customFormat(ctx, containers)
+}
+
+func tableFormat(ctx Context, containers []types.Container) {
+	ctx.Format = defaultTableFormat
+	if ctx.Quiet {
+		ctx.Format = defaultQuietFormat
+	}
+
+	customFormat(ctx, containers)
+}

+ 1 - 0
cliconfig/config.go

@@ -57,6 +57,7 @@ type AuthConfig struct {
 type ConfigFile struct {
 	AuthConfigs map[string]AuthConfig `json:"auths"`
 	HTTPHeaders map[string]string     `json:"HttpHeaders,omitempty"`
+	PsFormat    string                `json:"psFormat,omitempty"`
 	filename    string                // Note: not serialized - for internal use only
 }
 

+ 31 - 0
cliconfig/config_test.go

@@ -155,3 +155,34 @@ func TestNewJson(t *testing.T) {
 		t.Fatalf("Should have save in new form: %s", string(buf))
 	}
 }
+
+func TestJsonWithPsFormat(t *testing.T) {
+	tmpHome, _ := ioutil.TempDir("", "config-test")
+	fn := filepath.Join(tmpHome, ConfigFileName)
+	js := `{
+		"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
+		"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
+}`
+	ioutil.WriteFile(fn, []byte(js), 0600)
+
+	config, err := Load(tmpHome)
+	if err != nil {
+		t.Fatalf("Failed loading on empty json file: %q", err)
+	}
+
+	if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
+		t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
+	}
+
+	// Now save it and make sure it shows up in new form
+	err = config.Save()
+	if err != nil {
+		t.Fatalf("Failed to save: %q", err)
+	}
+
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName))
+	if !strings.Contains(string(buf), `"psFormat":`) ||
+		!strings.Contains(string(buf), "{{.ID}}") {
+		t.Fatalf("Should have save in new form: %s", string(buf))
+	}
+}

+ 11 - 3
docs/reference/commandline/cli.md

@@ -85,18 +85,26 @@ mechanisms, you must keep in mind the order of precedence among them. Command
 line options override environment variables and environment variables override
 properties you specify in a `config.json` file.
 
-The `config.json` file stores a JSON encoding of a single `HttpHeaders`
-property. The property specifies a set of headers to include in all messages
+The `config.json` file stores a JSON encoding of several properties:
+
+The property `HttpHeaders` specifies a set of headers to include in all messages
 sent from the Docker client to the daemon. Docker does not try to interpret or
 understand these header; it simply puts them into the messages. Docker does
 not allow these headers to change any headers it sets for itself.
 
+The property `psFormat` specifies the default format for `docker ps` output.
+When the `--format` flag is not provided with the `docker ps` command,
+Docker's client uses this property. If this property is not set, the client
+falls back to the default table format. For a list of supported formatting
+directives, see the [**Formatting** section in the `docker ps` documentation](../ps)
+
 Following is a sample `config.json` file:
 
     {
       "HttpHeaders: {
         "MyHeader": "MyValue"
-      }
+      },
+      "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
     }
 
 ## Help

+ 40 - 2
docs/reference/commandline/ps.md

@@ -24,6 +24,7 @@ weight=1
       -q, --quiet=false     Only display numeric IDs
       -s, --size=false      Display total file sizes
       --since=""            Show created since Id or Name, include non-running
+      --format=[]       Pretty-print containers using a Go template
 
 Running `docker ps --no-trunc` showing 2 linked containers.
 
@@ -60,5 +61,42 @@ The currently supported filters are:
 
 This shows all the containers that have exited with status of '0'
 
-
-
+## Formatting
+
+The formatting option (`--format`) will pretty-print container output using a Go template.
+
+Valid placeholders for the Go template are listed below:
+
+Placeholder | Description
+---- | ----
+`.ID` | Container ID
+`.Image` | Image ID
+`.Command` | Quoted command
+`.CreatedAt` | Time when the container was created.
+`.RunningFor` | Elapsed time since the container was started.
+`.Ports` | Exposed ports.
+`.Status` | Container status.
+`.Size` | Container disk size.
+`.Labels` | All labels asigned to the container.
+`.Label` | Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}`
+
+When using the `--format` option, the `ps` command will either output the data exactly as the template
+declares or, when using the `table` directive, will include column headers as well.
+
+The following example uses a template without headers and outputs the `ID` and `Command`
+entries separated by a colon for all running containers:
+
+    $ docker ps --format "{{.ID}}: {{.Command}}"
+    a87ecb4f327c: /bin/sh -c #(nop) MA
+    01946d9d34d8: /bin/sh -c #(nop) MA
+    c1d3b0166030: /bin/sh -c yum -y up
+    41d50ecd2f57: /bin/sh -c #(nop) MA
+
+To list all running containers with their labels in a table format you can use:
+
+    $ docker ps --format "table {{.ID}}\t{{.Labels}}"
+    CONTAINER ID        LABELS
+    a87ecb4f327c        com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd
+    01946d9d34d8
+    c1d3b0166030        com.docker.swarm.node=debian,com.docker.swarm.cpu=6
+    41d50ecd2f57        com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd

+ 31 - 0
integration-cli/docker_cli_ps_test.go

@@ -508,3 +508,34 @@ func (s *DockerSuite) TestPsListContainersFilterCreated(c *check.C) {
 		c.Fatalf("Expected id %s, got %s for filter, out: %s", cID, containerOut, out)
 	}
 }
+
+func (s *DockerSuite) TestPsFormatMultiNames(c *check.C) {
+	//create 2 containers and link them
+	dockerCmd(c, "run", "--name=child", "-d", "busybox", "top")
+	dockerCmd(c, "run", "--name=parent", "--link=child:linkedone", "-d", "busybox", "top")
+
+	//use the new format capabilities to only list the names and --no-trunc to get all names
+	out, _ := dockerCmd(c, "ps", "--format", "{{.Names}}", "--no-trunc")
+	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
+	expected := []string{"parent", "child,parent/linkedone"}
+	var names []string
+	for _, l := range lines {
+		names = append(names, l)
+	}
+	if !reflect.DeepEqual(expected, names) {
+		c.Fatalf("Expected array with non-truncated names: %v, got: %v", expected, names)
+	}
+
+	//now list without turning off truncation and make sure we only get the non-link names
+	out, _ = dockerCmd(c, "ps", "--format", "{{.Names}}")
+	lines = strings.Split(strings.TrimSpace(string(out)), "\n")
+	expected = []string{"parent", "child"}
+	var truncNames []string
+	for _, l := range lines {
+		truncNames = append(truncNames, l)
+	}
+	if !reflect.DeepEqual(expected, truncNames) {
+		c.Fatalf("Expected array with truncated names: %v, got: %v", expected, truncNames)
+	}
+
+}

+ 41 - 0
man/docker-ps.1.md

@@ -16,6 +16,7 @@ docker-ps - List containers
 [**-q**|**--quiet**[=*false*]]
 [**-s**|**--size**[=*false*]]
 [**--since**[=*SINCE*]]
+[**--format**=*"TEMPLATE"*]
 
 
 # DESCRIPTION
@@ -59,6 +60,20 @@ the running containers.
 **--since**=""
    Show only containers created since Id or Name, include non-running ones.
 
+**--format**=*"TEMPLATE"*
+   Pretty-print containers using a Go template.
+   Valid placeholders:
+      .ID - Container ID
+      .Image - Image ID
+      .Command - Quoted command
+      .CreatedAt - Time when the container was created.
+      .RunningFor - Elapsed time since the container was started.
+      .Ports - Exposed ports.
+      .Status - Container status.
+      .Size - Container disk size.
+      .Labels - All labels asigned to the container.
+      .Label - Value of a specific label for this container. For example `{{.Label "com.docker.swarm.cpu"}}`
+
 # EXAMPLES
 # Display all containers, including non-running
 
@@ -82,6 +97,32 @@ the running containers.
     # docker ps -a -q --filter=name=determined_torvalds
     c1d3b0166030
 
+# Display containers with their commands
+
+    # docker ps --format "{{.ID}}: {{.Command}}"
+    a87ecb4f327c: /bin/sh -c #(nop) MA
+    01946d9d34d8: /bin/sh -c #(nop) MA
+    c1d3b0166030: /bin/sh -c yum -y up
+    41d50ecd2f57: /bin/sh -c #(nop) MA
+
+# Display containers with their labels in a table
+
+    # docker ps --format "table {{.ID}}\t{{.Labels}}"
+    CONTAINER ID        LABELS
+    a87ecb4f327c        com.docker.swarm.node=ubuntu,com.docker.swarm.storage=ssd
+    01946d9d34d8
+    c1d3b0166030        com.docker.swarm.node=debian,com.docker.swarm.cpu=6
+    41d50ecd2f57        com.docker.swarm.node=fedora,com.docker.swarm.cpu=3,com.docker.swarm.storage=ssd
+
+# Display containers with their node label in a table
+
+    # docker ps --format 'table {{.ID}}\t{{(.Label "com.docker.swarm.node")}}'
+    CONTAINER ID        NODE
+    a87ecb4f327c        ubuntu
+    01946d9d34d8
+    c1d3b0166030        debian
+    41d50ecd2f57        fedora
+
 # HISTORY
 April 2014, Originally compiled by William Henry (whenry at redhat dot com)
 based on docker.com source material and internal work.