Browse Source

Merge pull request #26537 from dnephin/refactor_cli_formatter

Refactor cli/command/formatter
Vincent Demeester 8 năm trước cách đây
mục cha
commit
a7c25f9540

+ 9 - 17
cli/command/container/ps.go

@@ -106,27 +106,19 @@ func runPs(dockerCli *command.DockerCli, opts *psOptions) error {
 		return err
 		return err
 	}
 	}
 
 
-	f := opts.format
-	if len(f) == 0 {
+	format := opts.format
+	if len(format) == 0 {
 		if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
 		if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
-			f = dockerCli.ConfigFile().PsFormat
+			format = dockerCli.ConfigFile().PsFormat
 		} else {
 		} else {
-			f = "table"
+			format = formatter.TableFormatKey
 		}
 		}
 	}
 	}
 
 
-	psCtx := formatter.ContainerContext{
-		Context: formatter.Context{
-			Output: dockerCli.Out(),
-			Format: f,
-			Quiet:  opts.quiet,
-			Trunc:  !opts.noTrunc,
-		},
-		Size:       listOptions.Size,
-		Containers: containers,
+	containerCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewContainerFormat(format, opts.quiet, listOptions.Size),
+		Trunc:  !opts.noTrunc,
 	}
 	}
-
-	psCtx.Write()
-
-	return nil
+	return formatter.ContainerWrite(containerCtx, containers)
 }
 }

+ 53 - 60
cli/command/formatter/container.go

@@ -1,7 +1,6 @@
 package formatter
 package formatter
 
 
 import (
 import (
-	"bytes"
 	"fmt"
 	"fmt"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
@@ -11,7 +10,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/pkg/stringutils"
 	"github.com/docker/docker/pkg/stringutils"
-	"github.com/docker/go-units"
+	units "github.com/docker/go-units"
 )
 )
 
 
 const (
 const (
@@ -26,67 +25,61 @@ const (
 	mountsHeader      = "MOUNTS"
 	mountsHeader      = "MOUNTS"
 )
 )
 
 
-// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
-type ContainerContext struct {
-	Context
-	// Size when set to true will display the size of the output.
-	Size bool
-	// Containers
-	Containers []types.Container
-}
-
-func (ctx ContainerContext) Write() {
-	switch ctx.Format {
-	case tableFormatKey:
-		if ctx.Quiet {
-			ctx.Format = defaultQuietFormat
-		} else {
-			ctx.Format = defaultContainerTableFormat
-			if ctx.Size {
-				ctx.Format += `\t{{.Size}}`
-			}
+// NewContainerFormat returns a Format for rendering using a Context
+func NewContainerFormat(source string, quiet bool, size bool) Format {
+	switch source {
+	case TableFormatKey:
+		if quiet {
+			return defaultQuietFormat
 		}
 		}
-	case rawFormatKey:
-		if ctx.Quiet {
-			ctx.Format = `container_id: {{.ID}}`
-		} else {
-			ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
-			if ctx.Size {
-				ctx.Format += `size: {{.Size}}\n`
-			}
+		format := defaultContainerTableFormat
+		if size {
+			format += `\t{{.Size}}`
 		}
 		}
-	}
-
-	ctx.buffer = bytes.NewBufferString("")
-	ctx.preformat()
-
-	tmpl, err := ctx.parseFormat()
-	if err != nil {
-		return
-	}
-
-	for _, container := range ctx.Containers {
-		containerCtx := &containerContext{
-			trunc: ctx.Trunc,
-			c:     container,
+		return Format(format)
+	case RawFormatKey:
+		if quiet {
+			return `container_id: {{.ID}}`
 		}
 		}
-		err = ctx.contextFormat(tmpl, containerCtx)
-		if err != nil {
-			return
+		format := `container_id: {{.ID}}
+image: {{.Image}}
+command: {{.Command}}
+created_at: {{.CreatedAt}}
+status: {{- pad .Status 1 0}}
+names: {{.Names}}
+labels: {{- pad .Labels 1 0}}
+ports: {{- pad .Ports 1 0}}
+`
+		if size {
+			format += `size: {{.Size}}\n`
 		}
 		}
+		return Format(format)
 	}
 	}
+	return Format(source)
+}
 
 
-	ctx.postformat(tmpl, &containerContext{})
+// ContainerWrite renders the context for a list of containers
+func ContainerWrite(ctx Context, containers []types.Container) error {
+	render := func(format func(subContext subContext) error) error {
+		for _, container := range containers {
+			err := format(&containerContext{trunc: ctx.Trunc, c: container})
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+	return ctx.Write(&containerContext{}, render)
 }
 }
 
 
 type containerContext struct {
 type containerContext struct {
-	baseSubContext
+	HeaderContext
 	trunc bool
 	trunc bool
 	c     types.Container
 	c     types.Container
 }
 }
 
 
 func (c *containerContext) ID() string {
 func (c *containerContext) ID() string {
-	c.addHeader(containerIDHeader)
+	c.AddHeader(containerIDHeader)
 	if c.trunc {
 	if c.trunc {
 		return stringid.TruncateID(c.c.ID)
 		return stringid.TruncateID(c.c.ID)
 	}
 	}
@@ -94,7 +87,7 @@ func (c *containerContext) ID() string {
 }
 }
 
 
 func (c *containerContext) Names() string {
 func (c *containerContext) Names() string {
-	c.addHeader(namesHeader)
+	c.AddHeader(namesHeader)
 	names := stripNamePrefix(c.c.Names)
 	names := stripNamePrefix(c.c.Names)
 	if c.trunc {
 	if c.trunc {
 		for _, name := range names {
 		for _, name := range names {
@@ -108,7 +101,7 @@ func (c *containerContext) Names() string {
 }
 }
 
 
 func (c *containerContext) Image() string {
 func (c *containerContext) Image() string {
-	c.addHeader(imageHeader)
+	c.AddHeader(imageHeader)
 	if c.c.Image == "" {
 	if c.c.Image == "" {
 		return "<no image>"
 		return "<no image>"
 	}
 	}
@@ -121,7 +114,7 @@ func (c *containerContext) Image() string {
 }
 }
 
 
 func (c *containerContext) Command() string {
 func (c *containerContext) Command() string {
-	c.addHeader(commandHeader)
+	c.AddHeader(commandHeader)
 	command := c.c.Command
 	command := c.c.Command
 	if c.trunc {
 	if c.trunc {
 		command = stringutils.Ellipsis(command, 20)
 		command = stringutils.Ellipsis(command, 20)
@@ -130,28 +123,28 @@ func (c *containerContext) Command() string {
 }
 }
 
 
 func (c *containerContext) CreatedAt() string {
 func (c *containerContext) CreatedAt() string {
-	c.addHeader(createdAtHeader)
+	c.AddHeader(createdAtHeader)
 	return time.Unix(int64(c.c.Created), 0).String()
 	return time.Unix(int64(c.c.Created), 0).String()
 }
 }
 
 
 func (c *containerContext) RunningFor() string {
 func (c *containerContext) RunningFor() string {
-	c.addHeader(runningForHeader)
+	c.AddHeader(runningForHeader)
 	createdAt := time.Unix(int64(c.c.Created), 0)
 	createdAt := time.Unix(int64(c.c.Created), 0)
 	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
 	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
 }
 }
 
 
 func (c *containerContext) Ports() string {
 func (c *containerContext) Ports() string {
-	c.addHeader(portsHeader)
+	c.AddHeader(portsHeader)
 	return api.DisplayablePorts(c.c.Ports)
 	return api.DisplayablePorts(c.c.Ports)
 }
 }
 
 
 func (c *containerContext) Status() string {
 func (c *containerContext) Status() string {
-	c.addHeader(statusHeader)
+	c.AddHeader(statusHeader)
 	return c.c.Status
 	return c.c.Status
 }
 }
 
 
 func (c *containerContext) Size() string {
 func (c *containerContext) Size() string {
-	c.addHeader(sizeHeader)
+	c.AddHeader(sizeHeader)
 	srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
 	srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
 	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
 	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
 
 
@@ -163,7 +156,7 @@ func (c *containerContext) Size() string {
 }
 }
 
 
 func (c *containerContext) Labels() string {
 func (c *containerContext) Labels() string {
-	c.addHeader(labelsHeader)
+	c.AddHeader(labelsHeader)
 	if c.c.Labels == nil {
 	if c.c.Labels == nil {
 		return ""
 		return ""
 	}
 	}
@@ -180,7 +173,7 @@ func (c *containerContext) Label(name string) string {
 	r := strings.NewReplacer("-", " ", "_", " ")
 	r := strings.NewReplacer("-", " ", "_", " ")
 	h := r.Replace(n[len(n)-1])
 	h := r.Replace(n[len(n)-1])
 
 
-	c.addHeader(h)
+	c.AddHeader(h)
 
 
 	if c.c.Labels == nil {
 	if c.c.Labels == nil {
 		return ""
 		return ""
@@ -189,7 +182,7 @@ func (c *containerContext) Label(name string) string {
 }
 }
 
 
 func (c *containerContext) Mounts() string {
 func (c *containerContext) Mounts() string {
-	c.addHeader(mountsHeader)
+	c.AddHeader(mountsHeader)
 
 
 	var name string
 	var name string
 	var mounts []string
 	var mounts []string

+ 61 - 139
cli/command/formatter/container_test.go

@@ -95,7 +95,7 @@ func TestContainerPsContext(t *testing.T) {
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 		}
 		}
 
 
-		h := ctx.fullHeader()
+		h := ctx.FullHeader()
 		if h != c.expHeader {
 		if h != c.expHeader {
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 		}
 		}
@@ -114,7 +114,7 @@ func TestContainerPsContext(t *testing.T) {
 		t.Fatalf("Expected ubuntu, was %s\n", node)
 		t.Fatalf("Expected ubuntu, was %s\n", node)
 	}
 	}
 
 
-	h := ctx.fullHeader()
+	h := ctx.FullHeader()
 	if h != "SWARM ID\tNODE NAME" {
 	if h != "SWARM ID\tNODE NAME" {
 		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
 		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
 
 
@@ -129,9 +129,9 @@ func TestContainerPsContext(t *testing.T) {
 	}
 	}
 
 
 	ctx = containerContext{c: c2, trunc: true}
 	ctx = containerContext{c: c2, trunc: true}
-	fullHeader := ctx.fullHeader()
-	if fullHeader != "" {
-		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
+	FullHeader := ctx.FullHeader()
+	if FullHeader != "" {
+		t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader)
 	}
 	}
 
 
 }
 }
@@ -140,186 +140,127 @@ func TestContainerContextWrite(t *testing.T) {
 	unixTime := time.Now().AddDate(0, 0, -1).Unix()
 	unixTime := time.Now().AddDate(0, 0, -1).Unix()
 	expectedTime := time.Unix(unixTime, 0).String()
 	expectedTime := time.Unix(unixTime, 0).String()
 
 
-	contexts := []struct {
-		context  ContainerContext
+	cases := []struct {
+		context  Context
 		expected string
 		expected string
 	}{
 	}{
 		// Errors
 		// Errors
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "{{InvalidFunction}}",
-				},
-			},
+			Context{Format: "{{InvalidFunction}}"},
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
 `,
 `,
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "{{nil}}",
-				},
-			},
+			Context{Format: "{{nil}}"},
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
 `,
 `,
 		},
 		},
 		// Table Format
 		// Table Format
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table",
-				},
-				Size: true,
-			},
+			Context{Format: NewContainerFormat("table", false, true)},
 			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES               SIZE
 			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES               SIZE
 containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz          0 B
 containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz          0 B
 containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar          0 B
 containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar          0 B
 `,
 `,
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table",
-				},
-			},
+			Context{Format: NewContainerFormat("table", false, false)},
 			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
 			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
 containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz
 containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz
 containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar
 containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar
 `,
 `,
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}",
-				},
-			},
+			Context{Format: NewContainerFormat("table {{.Image}}", false, false)},
 			"IMAGE\nubuntu\nubuntu\n",
 			"IMAGE\nubuntu\nubuntu\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}",
-				},
-				Size: true,
-			},
+			Context{Format: NewContainerFormat("table {{.Image}}", false, true)},
 			"IMAGE\nubuntu\nubuntu\n",
 			"IMAGE\nubuntu\nubuntu\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewContainerFormat("table {{.Image}}", true, false)},
 			"IMAGE\nubuntu\nubuntu\n",
 			"IMAGE\nubuntu\nubuntu\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewContainerFormat("table", true, false)},
 			"containerID1\ncontainerID2\n",
 			"containerID1\ncontainerID2\n",
 		},
 		},
 		// Raw Format
 		// Raw Format
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "raw",
-				},
-			},
+			Context{Format: NewContainerFormat("raw", false, false)},
 			fmt.Sprintf(`container_id: containerID1
 			fmt.Sprintf(`container_id: containerID1
 image: ubuntu
 image: ubuntu
 command: ""
 command: ""
 created_at: %s
 created_at: %s
-status: 
+status:
 names: foobar_baz
 names: foobar_baz
-labels: 
-ports: 
+labels:
+ports:
 
 
 container_id: containerID2
 container_id: containerID2
 image: ubuntu
 image: ubuntu
 command: ""
 command: ""
 created_at: %s
 created_at: %s
-status: 
+status:
 names: foobar_bar
 names: foobar_bar
-labels: 
-ports: 
+labels:
+ports:
 
 
 `, expectedTime, expectedTime),
 `, expectedTime, expectedTime),
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "raw",
-				},
-				Size: true,
-			},
+			Context{Format: NewContainerFormat("raw", false, true)},
 			fmt.Sprintf(`container_id: containerID1
 			fmt.Sprintf(`container_id: containerID1
 image: ubuntu
 image: ubuntu
 command: ""
 command: ""
 created_at: %s
 created_at: %s
-status: 
+status:
 names: foobar_baz
 names: foobar_baz
-labels: 
-ports: 
+labels:
+ports:
 size: 0 B
 size: 0 B
 
 
 container_id: containerID2
 container_id: containerID2
 image: ubuntu
 image: ubuntu
 command: ""
 command: ""
 created_at: %s
 created_at: %s
-status: 
+status:
 names: foobar_bar
 names: foobar_bar
-labels: 
-ports: 
+labels:
+ports:
 size: 0 B
 size: 0 B
 
 
 `, expectedTime, expectedTime),
 `, expectedTime, expectedTime),
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "raw",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewContainerFormat("raw", true, false)},
 			"container_id: containerID1\ncontainer_id: containerID2\n",
 			"container_id: containerID1\ncontainer_id: containerID2\n",
 		},
 		},
 		// Custom Format
 		// Custom Format
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "{{.Image}}",
-				},
-			},
+			Context{Format: "{{.Image}}"},
 			"ubuntu\nubuntu\n",
 			"ubuntu\nubuntu\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "{{.Image}}",
-				},
-				Size: true,
-			},
+			Context{Format: NewContainerFormat("{{.Image}}", false, true)},
 			"ubuntu\nubuntu\n",
 			"ubuntu\nubuntu\n",
 		},
 		},
 	}
 	}
 
 
-	for _, context := range contexts {
+	for _, testcase := range cases {
 		containers := []types.Container{
 		containers := []types.Container{
 			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
 			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
 			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
 			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
 		}
 		}
 		out := bytes.NewBufferString("")
 		out := bytes.NewBufferString("")
-		context.context.Output = out
-		context.context.Containers = containers
-		context.context.Write()
-		actual := out.String()
-		assert.Equal(t, actual, context.expected)
-		// Clean buffer
-		out.Reset()
+		testcase.context.Output = out
+		err := ContainerWrite(testcase.context, containers)
+		if err != nil {
+			assert.Error(t, err, testcase.expected)
+		} else {
+			assert.Equal(t, out.String(), testcase.expected)
+		}
 	}
 	}
 }
 }
 
 
@@ -328,75 +269,56 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
 	containers := []types.Container{}
 	containers := []types.Container{}
 
 
 	contexts := []struct {
 	contexts := []struct {
-		context  ContainerContext
+		context  Context
 		expected string
 		expected string
 	}{
 	}{
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "{{.Image}}",
-					Output: out,
-				},
+			Context{
+				Format: "{{.Image}}",
+				Output: out,
 			},
 			},
 			"",
 			"",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}",
-					Output: out,
-				},
+			Context{
+				Format: "table {{.Image}}",
+				Output: out,
 			},
 			},
 			"IMAGE\n",
 			"IMAGE\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "{{.Image}}",
-					Output: out,
-				},
-				Size: true,
+			Context{
+				Format: NewContainerFormat("{{.Image}}", false, true),
+				Output: out,
 			},
 			},
 			"",
 			"",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}",
-					Output: out,
-				},
-				Size: true,
+			Context{
+				Format: NewContainerFormat("table {{.Image}}", false, true),
+				Output: out,
 			},
 			},
 			"IMAGE\n",
 			"IMAGE\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}\t{{.Size}}",
-					Output: out,
-				},
+			Context{
+				Format: "table {{.Image}}\t{{.Size}}",
+				Output: out,
 			},
 			},
 			"IMAGE               SIZE\n",
 			"IMAGE               SIZE\n",
 		},
 		},
 		{
 		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}\t{{.Size}}",
-					Output: out,
-				},
-				Size: true,
+			Context{
+				Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true),
+				Output: out,
 			},
 			},
 			"IMAGE               SIZE\n",
 			"IMAGE               SIZE\n",
 		},
 		},
 	}
 	}
 
 
 	for _, context := range contexts {
 	for _, context := range contexts {
-		context.context.Containers = containers
-		context.context.Write()
-		actual := out.String()
-		if actual != context.expected {
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
-		}
+		ContainerWrite(context.context, containers)
+		assert.Equal(t, context.expected, out.String())
 		// Clean buffer
 		// Clean buffer
 		out.Reset()
 		out.Reset()
 	}
 	}

+ 8 - 7
cli/command/formatter/custom.go

@@ -5,8 +5,6 @@ import (
 )
 )
 
 
 const (
 const (
-	tableKey = "table"
-
 	imageHeader        = "IMAGE"
 	imageHeader        = "IMAGE"
 	createdSinceHeader = "CREATED"
 	createdSinceHeader = "CREATED"
 	createdAtHeader    = "CREATED AT"
 	createdAtHeader    = "CREATED AT"
@@ -18,22 +16,25 @@ const (
 )
 )
 
 
 type subContext interface {
 type subContext interface {
-	fullHeader() string
-	addHeader(header string)
+	FullHeader() string
+	AddHeader(header string)
 }
 }
 
 
-type baseSubContext struct {
+// HeaderContext provides the subContext interface for managing headers
+type HeaderContext struct {
 	header []string
 	header []string
 }
 }
 
 
-func (c *baseSubContext) fullHeader() string {
+// FullHeader returns the header as a string
+func (c *HeaderContext) FullHeader() string {
 	if c.header == nil {
 	if c.header == nil {
 		return ""
 		return ""
 	}
 	}
 	return strings.Join(c.header, "\t")
 	return strings.Join(c.header, "\t")
 }
 }
 
 
-func (c *baseSubContext) addHeader(header string) {
+// AddHeader adds another column to the header
+func (c *HeaderContext) AddHeader(header string) {
 	if c.header == nil {
 	if c.header == nil {
 		c.header = []string{}
 		c.header = []string{}
 	}
 	}

+ 54 - 21
cli/command/formatter/formatter.go

@@ -12,36 +12,48 @@ import (
 )
 )
 
 
 const (
 const (
-	tableFormatKey = "table"
-	rawFormatKey   = "raw"
+	// TableFormatKey is the key used to format as a table
+	TableFormatKey = "table"
+	// RawFormatKey is the key used to format as raw JSON
+	RawFormatKey = "raw"
 
 
 	defaultQuietFormat = "{{.ID}}"
 	defaultQuietFormat = "{{.ID}}"
 )
 )
 
 
+// Format is the format string rendered using the Context
+type Format string
+
+// IsTable returns true if the format is a table-type format
+func (f Format) IsTable() bool {
+	return strings.HasPrefix(string(f), TableFormatKey)
+}
+
+// Contains returns true if the format contains the substring
+func (f Format) Contains(sub string) bool {
+	return strings.Contains(string(f), sub)
+}
+
 // Context contains information required by the formatter to print the output as desired.
 // Context contains information required by the formatter to print the output as desired.
 type Context struct {
 type Context struct {
 	// Output is the output stream to which the formatted string is written.
 	// Output is the output stream to which the formatted string is written.
 	Output io.Writer
 	Output io.Writer
 	// Format is used to choose raw, table or custom format for the output.
 	// Format is used to choose raw, table or custom format for the output.
-	Format string
-	// Quiet when set to true will simply print minimal information.
-	Quiet bool
+	Format Format
 	// Trunc when set to true will truncate the output of certain fields such as Container ID.
 	// Trunc when set to true will truncate the output of certain fields such as Container ID.
 	Trunc bool
 	Trunc bool
 
 
 	// internal element
 	// internal element
-	table       bool
 	finalFormat string
 	finalFormat string
 	header      string
 	header      string
 	buffer      *bytes.Buffer
 	buffer      *bytes.Buffer
 }
 }
 
 
-func (c *Context) preformat() {
-	c.finalFormat = c.Format
+func (c *Context) preFormat() {
+	c.finalFormat = string(c.Format)
 
 
-	if strings.HasPrefix(c.Format, tableKey) {
-		c.table = true
-		c.finalFormat = c.finalFormat[len(tableKey):]
+	// TODO: handle this in the Format type
+	if c.Format.IsTable() {
+		c.finalFormat = c.finalFormat[len(TableFormatKey):]
 	}
 	}
 
 
 	c.finalFormat = strings.Trim(c.finalFormat, " ")
 	c.finalFormat = strings.Trim(c.finalFormat, " ")
@@ -52,18 +64,17 @@ func (c *Context) preformat() {
 func (c *Context) parseFormat() (*template.Template, error) {
 func (c *Context) parseFormat() (*template.Template, error) {
 	tmpl, err := templates.Parse(c.finalFormat)
 	tmpl, err := templates.Parse(c.finalFormat)
 	if err != nil {
 	if err != nil {
-		c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
-		c.buffer.WriteTo(c.Output)
+		return tmpl, fmt.Errorf("Template parsing error: %v\n", err)
 	}
 	}
 	return tmpl, err
 	return tmpl, err
 }
 }
 
 
-func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
-	if c.table {
+func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
+	if c.Format.IsTable() {
 		if len(c.header) == 0 {
 		if len(c.header) == 0 {
 			// 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
 			// 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
 			tmpl.Execute(bytes.NewBufferString(""), subContext)
 			tmpl.Execute(bytes.NewBufferString(""), subContext)
-			c.header = subContext.fullHeader()
+			c.header = subContext.FullHeader()
 		}
 		}
 
 
 		t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
 		t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
@@ -78,13 +89,35 @@ func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
 
 
 func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
 func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
 	if err := tmpl.Execute(c.buffer, subContext); err != nil {
 	if err := tmpl.Execute(c.buffer, subContext); err != nil {
-		c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
-		c.buffer.WriteTo(c.Output)
-		return err
+		return fmt.Errorf("Template parsing error: %v\n", err)
 	}
 	}
-	if c.table && len(c.header) == 0 {
-		c.header = subContext.fullHeader()
+	if c.Format.IsTable() && len(c.header) == 0 {
+		c.header = subContext.FullHeader()
 	}
 	}
 	c.buffer.WriteString("\n")
 	c.buffer.WriteString("\n")
 	return nil
 	return nil
 }
 }
+
+// SubFormat is a function type accepted by Write()
+type SubFormat func(func(subContext) error) error
+
+// Write the template to the buffer using this Context
+func (c *Context) Write(sub subContext, f SubFormat) error {
+	c.buffer = bytes.NewBufferString("")
+	c.preFormat()
+
+	tmpl, err := c.parseFormat()
+	if err != nil {
+		return err
+	}
+
+	subFormat := func(subContext subContext) error {
+		return c.contextFormat(tmpl, subContext)
+	}
+	if err := f(subFormat); err != nil {
+		return err
+	}
+
+	c.postFormat(tmpl, sub)
+	return nil
+}

+ 44 - 44
cli/command/formatter/image.go

@@ -1,14 +1,12 @@
 package formatter
 package formatter
 
 
 import (
 import (
-	"bytes"
-	"strings"
 	"time"
 	"time"
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/reference"
-	"github.com/docker/go-units"
+	units "github.com/docker/go-units"
 )
 )
 
 
 const (
 const (
@@ -25,59 +23,63 @@ const (
 type ImageContext struct {
 type ImageContext struct {
 	Context
 	Context
 	Digest bool
 	Digest bool
-	// Images
-	Images []types.Image
 }
 }
 
 
 func isDangling(image types.Image) bool {
 func isDangling(image types.Image) bool {
 	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
 	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
 }
 }
 
 
-func (ctx ImageContext) Write() {
-	switch ctx.Format {
-	case tableFormatKey:
-		ctx.Format = defaultImageTableFormat
-		if ctx.Digest {
-			ctx.Format = defaultImageTableFormatWithDigest
+// NewImageFormat returns a format for rendering an ImageContext
+func NewImageFormat(source string, quiet bool, digest bool) Format {
+	switch source {
+	case TableFormatKey:
+		switch {
+		case quiet:
+			return defaultQuietFormat
+		case digest:
+			return defaultImageTableFormatWithDigest
+		default:
+			return defaultImageTableFormat
 		}
 		}
-		if ctx.Quiet {
-			ctx.Format = defaultQuietFormat
-		}
-	case rawFormatKey:
-		if ctx.Quiet {
-			ctx.Format = `image_id: {{.ID}}`
-		} else {
-			if ctx.Digest {
-				ctx.Format = `repository: {{ .Repository }}
+	case RawFormatKey:
+		switch {
+		case quiet:
+			return `image_id: {{.ID}}`
+		case digest:
+			return `repository: {{ .Repository }}
 tag: {{.Tag}}
 tag: {{.Tag}}
 digest: {{.Digest}}
 digest: {{.Digest}}
 image_id: {{.ID}}
 image_id: {{.ID}}
 created_at: {{.CreatedAt}}
 created_at: {{.CreatedAt}}
 virtual_size: {{.Size}}
 virtual_size: {{.Size}}
 `
 `
-			} else {
-				ctx.Format = `repository: {{ .Repository }}
+		default:
+			return `repository: {{ .Repository }}
 tag: {{.Tag}}
 tag: {{.Tag}}
 image_id: {{.ID}}
 image_id: {{.ID}}
 created_at: {{.CreatedAt}}
 created_at: {{.CreatedAt}}
 virtual_size: {{.Size}}
 virtual_size: {{.Size}}
 `
 `
-			}
 		}
 		}
 	}
 	}
 
 
-	ctx.buffer = bytes.NewBufferString("")
-	ctx.preformat()
-	if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
-		ctx.finalFormat += "\t{{.Digest}}"
+	format := Format(source)
+	if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
+		format += "\t{{.Digest}}"
 	}
 	}
+	return format
+}
 
 
-	tmpl, err := ctx.parseFormat()
-	if err != nil {
-		return
+// ImageWrite writes the formatter images using the ImageContext
+func ImageWrite(ctx ImageContext, images []types.Image) error {
+	render := func(format func(subContext subContext) error) error {
+		return imageFormat(ctx, images, format)
 	}
 	}
+	return ctx.Write(&imageContext{}, render)
+}
 
 
-	for _, image := range ctx.Images {
+func imageFormat(ctx ImageContext, images []types.Image, format func(subContext subContext) error) error {
+	for _, image := range images {
 		images := []*imageContext{}
 		images := []*imageContext{}
 		if isDangling(image) {
 		if isDangling(image) {
 			images = append(images, &imageContext{
 			images = append(images, &imageContext{
@@ -170,18 +172,16 @@ virtual_size: {{.Size}}
 			}
 			}
 		}
 		}
 		for _, imageCtx := range images {
 		for _, imageCtx := range images {
-			err = ctx.contextFormat(tmpl, imageCtx)
-			if err != nil {
-				return
+			if err := format(imageCtx); err != nil {
+				return err
 			}
 			}
 		}
 		}
 	}
 	}
-
-	ctx.postformat(tmpl, &imageContext{})
+	return nil
 }
 }
 
 
 type imageContext struct {
 type imageContext struct {
-	baseSubContext
+	HeaderContext
 	trunc  bool
 	trunc  bool
 	i      types.Image
 	i      types.Image
 	repo   string
 	repo   string
@@ -190,7 +190,7 @@ type imageContext struct {
 }
 }
 
 
 func (c *imageContext) ID() string {
 func (c *imageContext) ID() string {
-	c.addHeader(imageIDHeader)
+	c.AddHeader(imageIDHeader)
 	if c.trunc {
 	if c.trunc {
 		return stringid.TruncateID(c.i.ID)
 		return stringid.TruncateID(c.i.ID)
 	}
 	}
@@ -198,32 +198,32 @@ func (c *imageContext) ID() string {
 }
 }
 
 
 func (c *imageContext) Repository() string {
 func (c *imageContext) Repository() string {
-	c.addHeader(repositoryHeader)
+	c.AddHeader(repositoryHeader)
 	return c.repo
 	return c.repo
 }
 }
 
 
 func (c *imageContext) Tag() string {
 func (c *imageContext) Tag() string {
-	c.addHeader(tagHeader)
+	c.AddHeader(tagHeader)
 	return c.tag
 	return c.tag
 }
 }
 
 
 func (c *imageContext) Digest() string {
 func (c *imageContext) Digest() string {
-	c.addHeader(digestHeader)
+	c.AddHeader(digestHeader)
 	return c.digest
 	return c.digest
 }
 }
 
 
 func (c *imageContext) CreatedSince() string {
 func (c *imageContext) CreatedSince() string {
-	c.addHeader(createdSinceHeader)
+	c.AddHeader(createdSinceHeader)
 	createdAt := time.Unix(int64(c.i.Created), 0)
 	createdAt := time.Unix(int64(c.i.Created), 0)
 	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
 	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
 }
 }
 
 
 func (c *imageContext) CreatedAt() string {
 func (c *imageContext) CreatedAt() string {
-	c.addHeader(createdAtHeader)
+	c.AddHeader(createdAtHeader)
 	return time.Unix(int64(c.i.Created), 0).String()
 	return time.Unix(int64(c.i.Created), 0).String()
 }
 }
 
 
 func (c *imageContext) Size() string {
 func (c *imageContext) Size() string {
-	c.addHeader(sizeHeader)
+	c.AddHeader(sizeHeader)
 	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
 	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
 }
 }

+ 28 - 40
cli/command/formatter/image_test.go

@@ -9,6 +9,7 @@ import (
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/testutil/assert"
 )
 )
 
 
 func TestImageContext(t *testing.T) {
 func TestImageContext(t *testing.T) {
@@ -66,7 +67,7 @@ func TestImageContext(t *testing.T) {
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 		}
 		}
 
 
-		h := ctx.fullHeader()
+		h := ctx.FullHeader()
 		if h != c.expHeader {
 		if h != c.expHeader {
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 		}
 		}
@@ -77,7 +78,7 @@ func TestImageContextWrite(t *testing.T) {
 	unixTime := time.Now().AddDate(0, 0, -1).Unix()
 	unixTime := time.Now().AddDate(0, 0, -1).Unix()
 	expectedTime := time.Unix(unixTime, 0).String()
 	expectedTime := time.Unix(unixTime, 0).String()
 
 
-	contexts := []struct {
+	cases := []struct {
 		context  ImageContext
 		context  ImageContext
 		expected string
 		expected string
 	}{
 	}{
@@ -104,7 +105,7 @@ func TestImageContextWrite(t *testing.T) {
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table",
+					Format: NewImageFormat("table", false, false),
 				},
 				},
 			},
 			},
 			`REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
 			`REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
@@ -116,7 +117,7 @@ image               tag2                imageID2            24 hours ago
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table {{.Repository}}",
+					Format: NewImageFormat("table {{.Repository}}", false, false),
 				},
 				},
 			},
 			},
 			"REPOSITORY\nimage\nimage\n<none>\n",
 			"REPOSITORY\nimage\nimage\n<none>\n",
@@ -124,7 +125,7 @@ image               tag2                imageID2            24 hours ago
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table {{.Repository}}",
+					Format: NewImageFormat("table {{.Repository}}", false, true),
 				},
 				},
 				Digest: true,
 				Digest: true,
 			},
 			},
@@ -137,8 +138,7 @@ image               <none>
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table {{.Repository}}",
-					Quiet:  true,
+					Format: NewImageFormat("table {{.Repository}}", true, false),
 				},
 				},
 			},
 			},
 			"REPOSITORY\nimage\nimage\n<none>\n",
 			"REPOSITORY\nimage\nimage\n<none>\n",
@@ -146,8 +146,7 @@ image               <none>
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table",
-					Quiet:  true,
+					Format: NewImageFormat("table", true, false),
 				},
 				},
 			},
 			},
 			"imageID1\nimageID2\nimageID3\n",
 			"imageID1\nimageID2\nimageID3\n",
@@ -155,8 +154,7 @@ image               <none>
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table",
-					Quiet:  false,
+					Format: NewImageFormat("table", false, true),
 				},
 				},
 				Digest: true,
 				Digest: true,
 			},
 			},
@@ -169,8 +167,7 @@ image               tag2                <none>
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table",
-					Quiet:  true,
+					Format: NewImageFormat("table", true, true),
 				},
 				},
 				Digest: true,
 				Digest: true,
 			},
 			},
@@ -180,7 +177,7 @@ image               tag2                <none>
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "raw",
+					Format: NewImageFormat("raw", false, false),
 				},
 				},
 			},
 			},
 			fmt.Sprintf(`repository: image
 			fmt.Sprintf(`repository: image
@@ -206,7 +203,7 @@ virtual_size: 0 B
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "raw",
+					Format: NewImageFormat("raw", false, true),
 				},
 				},
 				Digest: true,
 				Digest: true,
 			},
 			},
@@ -236,8 +233,7 @@ virtual_size: 0 B
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "raw",
-					Quiet:  true,
+					Format: NewImageFormat("raw", true, false),
 				},
 				},
 			},
 			},
 			`image_id: imageID1
 			`image_id: imageID1
@@ -249,7 +245,7 @@ image_id: imageID3
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "{{.Repository}}",
+					Format: NewImageFormat("{{.Repository}}", false, false),
 				},
 				},
 			},
 			},
 			"image\nimage\n<none>\n",
 			"image\nimage\n<none>\n",
@@ -257,7 +253,7 @@ image_id: imageID3
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "{{.Repository}}",
+					Format: NewImageFormat("{{.Repository}}", false, true),
 				},
 				},
 				Digest: true,
 				Digest: true,
 			},
 			},
@@ -265,22 +261,20 @@ image_id: imageID3
 		},
 		},
 	}
 	}
 
 
-	for _, context := range contexts {
+	for _, testcase := range cases {
 		images := []types.Image{
 		images := []types.Image{
 			{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
 			{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
 			{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
 			{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
 			{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
 			{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
 		}
 		}
 		out := bytes.NewBufferString("")
 		out := bytes.NewBufferString("")
-		context.context.Output = out
-		context.context.Images = images
-		context.context.Write()
-		actual := out.String()
-		if actual != context.expected {
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
+		testcase.context.Output = out
+		err := ImageWrite(testcase.context, images)
+		if err != nil {
+			assert.Error(t, err, testcase.expected)
+		} else {
+			assert.Equal(t, out.String(), testcase.expected)
 		}
 		}
-		// Clean buffer
-		out.Reset()
 	}
 	}
 }
 }
 
 
@@ -295,7 +289,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "{{.Repository}}",
+					Format: NewImageFormat("{{.Repository}}", false, false),
 					Output: out,
 					Output: out,
 				},
 				},
 			},
 			},
@@ -304,7 +298,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table {{.Repository}}",
+					Format: NewImageFormat("table {{.Repository}}", false, false),
 					Output: out,
 					Output: out,
 				},
 				},
 			},
 			},
@@ -313,32 +307,26 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "{{.Repository}}",
+					Format: NewImageFormat("{{.Repository}}", false, true),
 					Output: out,
 					Output: out,
 				},
 				},
-				Digest: true,
 			},
 			},
 			"",
 			"",
 		},
 		},
 		{
 		{
 			ImageContext{
 			ImageContext{
 				Context: Context{
 				Context: Context{
-					Format: "table {{.Repository}}",
+					Format: NewImageFormat("table {{.Repository}}", false, true),
 					Output: out,
 					Output: out,
 				},
 				},
-				Digest: true,
 			},
 			},
 			"REPOSITORY          DIGEST\n",
 			"REPOSITORY          DIGEST\n",
 		},
 		},
 	}
 	}
 
 
 	for _, context := range contexts {
 	for _, context := range contexts {
-		context.context.Images = images
-		context.context.Write()
-		actual := out.String()
-		if actual != context.expected {
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
-		}
+		ImageWrite(context.context, images)
+		assert.Equal(t, out.String(), context.expected)
 		// Clean buffer
 		// Clean buffer
 		out.Reset()
 		out.Reset()
 	}
 	}

+ 32 - 48
cli/command/formatter/network.go

@@ -1,7 +1,6 @@
 package formatter
 package formatter
 
 
 import (
 import (
-	"bytes"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
 
 
@@ -17,60 +16,45 @@ const (
 	internalHeader  = "INTERNAL"
 	internalHeader  = "INTERNAL"
 )
 )
 
 
-// NetworkContext contains network specific information required by the formatter,
-// encapsulate a Context struct.
-type NetworkContext struct {
-	Context
-	// Networks
-	Networks []types.NetworkResource
-}
-
-func (ctx NetworkContext) Write() {
-	switch ctx.Format {
-	case tableFormatKey:
-		if ctx.Quiet {
-			ctx.Format = defaultQuietFormat
-		} else {
-			ctx.Format = defaultNetworkTableFormat
+// NewNetworkFormat returns a Format for rendering using a network Context
+func NewNetworkFormat(source string, quiet bool) Format {
+	switch source {
+	case TableFormatKey:
+		if quiet {
+			return defaultQuietFormat
 		}
 		}
-	case rawFormatKey:
-		if ctx.Quiet {
-			ctx.Format = `network_id: {{.ID}}`
-		} else {
-			ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
+		return defaultNetworkTableFormat
+	case RawFormatKey:
+		if quiet {
+			return `network_id: {{.ID}}`
 		}
 		}
+		return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
 	}
 	}
+	return Format(source)
+}
 
 
-	ctx.buffer = bytes.NewBufferString("")
-	ctx.preformat()
-
-	tmpl, err := ctx.parseFormat()
-	if err != nil {
-		return
-	}
-
-	for _, network := range ctx.Networks {
-		networkCtx := &networkContext{
-			trunc: ctx.Trunc,
-			n:     network,
-		}
-		err = ctx.contextFormat(tmpl, networkCtx)
-		if err != nil {
-			return
+// NetworkWrite writes the context
+func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
+	render := func(format func(subContext subContext) error) error {
+		for _, network := range networks {
+			networkCtx := &networkContext{trunc: ctx.Trunc, n: network}
+			if err := format(networkCtx); err != nil {
+				return err
+			}
 		}
 		}
+		return nil
 	}
 	}
-
-	ctx.postformat(tmpl, &networkContext{})
+	return ctx.Write(&networkContext{}, render)
 }
 }
 
 
 type networkContext struct {
 type networkContext struct {
-	baseSubContext
+	HeaderContext
 	trunc bool
 	trunc bool
 	n     types.NetworkResource
 	n     types.NetworkResource
 }
 }
 
 
 func (c *networkContext) ID() string {
 func (c *networkContext) ID() string {
-	c.addHeader(networkIDHeader)
+	c.AddHeader(networkIDHeader)
 	if c.trunc {
 	if c.trunc {
 		return stringid.TruncateID(c.n.ID)
 		return stringid.TruncateID(c.n.ID)
 	}
 	}
@@ -78,32 +62,32 @@ func (c *networkContext) ID() string {
 }
 }
 
 
 func (c *networkContext) Name() string {
 func (c *networkContext) Name() string {
-	c.addHeader(nameHeader)
+	c.AddHeader(nameHeader)
 	return c.n.Name
 	return c.n.Name
 }
 }
 
 
 func (c *networkContext) Driver() string {
 func (c *networkContext) Driver() string {
-	c.addHeader(driverHeader)
+	c.AddHeader(driverHeader)
 	return c.n.Driver
 	return c.n.Driver
 }
 }
 
 
 func (c *networkContext) Scope() string {
 func (c *networkContext) Scope() string {
-	c.addHeader(scopeHeader)
+	c.AddHeader(scopeHeader)
 	return c.n.Scope
 	return c.n.Scope
 }
 }
 
 
 func (c *networkContext) IPv6() string {
 func (c *networkContext) IPv6() string {
-	c.addHeader(ipv6Header)
+	c.AddHeader(ipv6Header)
 	return fmt.Sprintf("%v", c.n.EnableIPv6)
 	return fmt.Sprintf("%v", c.n.EnableIPv6)
 }
 }
 
 
 func (c *networkContext) Internal() string {
 func (c *networkContext) Internal() string {
-	c.addHeader(internalHeader)
+	c.AddHeader(internalHeader)
 	return fmt.Sprintf("%v", c.n.Internal)
 	return fmt.Sprintf("%v", c.n.Internal)
 }
 }
 
 
 func (c *networkContext) Labels() string {
 func (c *networkContext) Labels() string {
-	c.addHeader(labelsHeader)
+	c.AddHeader(labelsHeader)
 	if c.n.Labels == nil {
 	if c.n.Labels == nil {
 		return ""
 		return ""
 	}
 	}
@@ -120,7 +104,7 @@ func (c *networkContext) Label(name string) string {
 	r := strings.NewReplacer("-", " ", "_", " ")
 	r := strings.NewReplacer("-", " ", "_", " ")
 	h := r.Replace(n[len(n)-1])
 	h := r.Replace(n[len(n)-1])
 
 
-	c.addHeader(h)
+	c.AddHeader(h)
 
 
 	if c.n.Labels == nil {
 	if c.n.Labels == nil {
 		return ""
 		return ""

+ 21 - 60
cli/command/formatter/network_test.go

@@ -7,6 +7,7 @@ import (
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/testutil/assert"
 )
 )
 
 
 func TestNetworkContext(t *testing.T) {
 func TestNetworkContext(t *testing.T) {
@@ -62,7 +63,7 @@ func TestNetworkContext(t *testing.T) {
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 		}
 		}
 
 
-		h := ctx.fullHeader()
+		h := ctx.FullHeader()
 		if h != c.expHeader {
 		if h != c.expHeader {
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 		}
 		}
@@ -70,71 +71,45 @@ func TestNetworkContext(t *testing.T) {
 }
 }
 
 
 func TestNetworkContextWrite(t *testing.T) {
 func TestNetworkContextWrite(t *testing.T) {
-	contexts := []struct {
-		context  NetworkContext
+	cases := []struct {
+		context  Context
 		expected string
 		expected string
 	}{
 	}{
 
 
 		// Errors
 		// Errors
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "{{InvalidFunction}}",
-				},
-			},
+			Context{Format: "{{InvalidFunction}}"},
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
 `,
 `,
 		},
 		},
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "{{nil}}",
-				},
-			},
+			Context{Format: "{{nil}}"},
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
 `,
 `,
 		},
 		},
 		// Table format
 		// Table format
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "table",
-				},
-			},
+			Context{Format: NewNetworkFormat("table", false)},
 			`NETWORK ID          NAME                DRIVER              SCOPE
 			`NETWORK ID          NAME                DRIVER              SCOPE
 networkID1          foobar_baz          foo                 local
 networkID1          foobar_baz          foo                 local
 networkID2          foobar_bar          bar                 local
 networkID2          foobar_bar          bar                 local
 `,
 `,
 		},
 		},
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "table",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewNetworkFormat("table", true)},
 			`networkID1
 			`networkID1
 networkID2
 networkID2
 `,
 `,
 		},
 		},
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "table {{.Name}}",
-				},
-			},
+			Context{Format: NewNetworkFormat("table {{.Name}}", false)},
 			`NAME
 			`NAME
 foobar_baz
 foobar_baz
 foobar_bar
 foobar_bar
 `,
 `,
 		},
 		},
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "table {{.Name}}",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewNetworkFormat("table {{.Name}}", true)},
 			`NAME
 			`NAME
 foobar_baz
 foobar_baz
 foobar_bar
 foobar_bar
@@ -142,11 +117,8 @@ foobar_bar
 		},
 		},
 		// Raw Format
 		// Raw Format
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "raw",
-				},
-			}, `network_id: networkID1
+			Context{Format: NewNetworkFormat("raw", false)},
+			`network_id: networkID1
 name: foobar_baz
 name: foobar_baz
 driver: foo
 driver: foo
 scope: local
 scope: local
@@ -159,43 +131,32 @@ scope: local
 `,
 `,
 		},
 		},
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "raw",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewNetworkFormat("raw", true)},
 			`network_id: networkID1
 			`network_id: networkID1
 network_id: networkID2
 network_id: networkID2
 `,
 `,
 		},
 		},
 		// Custom Format
 		// Custom Format
 		{
 		{
-			NetworkContext{
-				Context: Context{
-					Format: "{{.Name}}",
-				},
-			},
+			Context{Format: NewNetworkFormat("{{.Name}}", false)},
 			`foobar_baz
 			`foobar_baz
 foobar_bar
 foobar_bar
 `,
 `,
 		},
 		},
 	}
 	}
 
 
-	for _, context := range contexts {
+	for _, testcase := range cases {
 		networks := []types.NetworkResource{
 		networks := []types.NetworkResource{
 			{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
 			{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
 			{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
 			{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
 		}
 		}
 		out := bytes.NewBufferString("")
 		out := bytes.NewBufferString("")
-		context.context.Output = out
-		context.context.Networks = networks
-		context.context.Write()
-		actual := out.String()
-		if actual != context.expected {
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
+		testcase.context.Output = out
+		err := NetworkWrite(testcase.context, networks)
+		if err != nil {
+			assert.Error(t, err, testcase.expected)
+		} else {
+			assert.Equal(t, out.String(), testcase.expected)
 		}
 		}
-		// Clean buffer
-		out.Reset()
 	}
 	}
 }
 }

+ 30 - 46
cli/command/formatter/volume.go

@@ -1,7 +1,6 @@
 package formatter
 package formatter
 
 
 import (
 import (
-	"bytes"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
 
 
@@ -16,78 +15,63 @@ const (
 	// Status header ?
 	// Status header ?
 )
 )
 
 
-// VolumeContext contains volume specific information required by the formatter,
-// encapsulate a Context struct.
-type VolumeContext struct {
-	Context
-	// Volumes
-	Volumes []*types.Volume
-}
-
-func (ctx VolumeContext) Write() {
-	switch ctx.Format {
-	case tableFormatKey:
-		if ctx.Quiet {
-			ctx.Format = defaultVolumeQuietFormat
-		} else {
-			ctx.Format = defaultVolumeTableFormat
+// NewVolumeFormat returns a format for use with a volume Context
+func NewVolumeFormat(source string, quiet bool) Format {
+	switch source {
+	case TableFormatKey:
+		if quiet {
+			return defaultVolumeQuietFormat
 		}
 		}
-	case rawFormatKey:
-		if ctx.Quiet {
-			ctx.Format = `name: {{.Name}}`
-		} else {
-			ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
+		return defaultVolumeTableFormat
+	case RawFormatKey:
+		if quiet {
+			return `name: {{.Name}}`
 		}
 		}
+		return `name: {{.Name}}\ndriver: {{.Driver}}\n`
 	}
 	}
+	return Format(source)
+}
 
 
-	ctx.buffer = bytes.NewBufferString("")
-	ctx.preformat()
-
-	tmpl, err := ctx.parseFormat()
-	if err != nil {
-		return
-	}
-
-	for _, volume := range ctx.Volumes {
-		volumeCtx := &volumeContext{
-			v: volume,
-		}
-		err = ctx.contextFormat(tmpl, volumeCtx)
-		if err != nil {
-			return
+// VolumeWrite writes formatted volumes using the Context
+func VolumeWrite(ctx Context, volumes []*types.Volume) error {
+	render := func(format func(subContext subContext) error) error {
+		for _, volume := range volumes {
+			if err := format(&volumeContext{v: *volume}); err != nil {
+				return err
+			}
 		}
 		}
+		return nil
 	}
 	}
-
-	ctx.postformat(tmpl, &networkContext{})
+	return ctx.Write(&volumeContext{}, render)
 }
 }
 
 
 type volumeContext struct {
 type volumeContext struct {
-	baseSubContext
-	v *types.Volume
+	HeaderContext
+	v types.Volume
 }
 }
 
 
 func (c *volumeContext) Name() string {
 func (c *volumeContext) Name() string {
-	c.addHeader(nameHeader)
+	c.AddHeader(nameHeader)
 	return c.v.Name
 	return c.v.Name
 }
 }
 
 
 func (c *volumeContext) Driver() string {
 func (c *volumeContext) Driver() string {
-	c.addHeader(driverHeader)
+	c.AddHeader(driverHeader)
 	return c.v.Driver
 	return c.v.Driver
 }
 }
 
 
 func (c *volumeContext) Scope() string {
 func (c *volumeContext) Scope() string {
-	c.addHeader(scopeHeader)
+	c.AddHeader(scopeHeader)
 	return c.v.Scope
 	return c.v.Scope
 }
 }
 
 
 func (c *volumeContext) Mountpoint() string {
 func (c *volumeContext) Mountpoint() string {
-	c.addHeader(mountpointHeader)
+	c.AddHeader(mountpointHeader)
 	return c.v.Mountpoint
 	return c.v.Mountpoint
 }
 }
 
 
 func (c *volumeContext) Labels() string {
 func (c *volumeContext) Labels() string {
-	c.addHeader(labelsHeader)
+	c.AddHeader(labelsHeader)
 	if c.v.Labels == nil {
 	if c.v.Labels == nil {
 		return ""
 		return ""
 	}
 	}
@@ -105,7 +89,7 @@ func (c *volumeContext) Label(name string) string {
 	r := strings.NewReplacer("-", " ", "_", " ")
 	r := strings.NewReplacer("-", " ", "_", " ")
 	h := r.Replace(n[len(n)-1])
 	h := r.Replace(n[len(n)-1])
 
 
-	c.addHeader(h)
+	c.AddHeader(h)
 
 
 	if c.v.Labels == nil {
 	if c.v.Labels == nil {
 		return ""
 		return ""

+ 27 - 66
cli/command/formatter/volume_test.go

@@ -7,6 +7,7 @@ import (
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/testutil/assert"
 )
 )
 
 
 func TestVolumeContext(t *testing.T) {
 func TestVolumeContext(t *testing.T) {
@@ -20,22 +21,22 @@ func TestVolumeContext(t *testing.T) {
 		call      func() string
 		call      func() string
 	}{
 	}{
 		{volumeContext{
 		{volumeContext{
-			v: &types.Volume{Name: volumeName},
+			v: types.Volume{Name: volumeName},
 		}, volumeName, nameHeader, ctx.Name},
 		}, volumeName, nameHeader, ctx.Name},
 		{volumeContext{
 		{volumeContext{
-			v: &types.Volume{Driver: "driver_name"},
+			v: types.Volume{Driver: "driver_name"},
 		}, "driver_name", driverHeader, ctx.Driver},
 		}, "driver_name", driverHeader, ctx.Driver},
 		{volumeContext{
 		{volumeContext{
-			v: &types.Volume{Scope: "local"},
+			v: types.Volume{Scope: "local"},
 		}, "local", scopeHeader, ctx.Scope},
 		}, "local", scopeHeader, ctx.Scope},
 		{volumeContext{
 		{volumeContext{
-			v: &types.Volume{Mountpoint: "mountpoint"},
+			v: types.Volume{Mountpoint: "mountpoint"},
 		}, "mountpoint", mountpointHeader, ctx.Mountpoint},
 		}, "mountpoint", mountpointHeader, ctx.Mountpoint},
 		{volumeContext{
 		{volumeContext{
-			v: &types.Volume{},
+			v: types.Volume{},
 		}, "", labelsHeader, ctx.Labels},
 		}, "", labelsHeader, ctx.Labels},
 		{volumeContext{
 		{volumeContext{
-			v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
+			v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
 		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
 		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
 	}
 	}
 
 
@@ -48,7 +49,7 @@ func TestVolumeContext(t *testing.T) {
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
 		}
 		}
 
 
-		h := ctx.fullHeader()
+		h := ctx.FullHeader()
 		if h != c.expHeader {
 		if h != c.expHeader {
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 		}
 		}
@@ -56,71 +57,45 @@ func TestVolumeContext(t *testing.T) {
 }
 }
 
 
 func TestVolumeContextWrite(t *testing.T) {
 func TestVolumeContextWrite(t *testing.T) {
-	contexts := []struct {
-		context  VolumeContext
+	cases := []struct {
+		context  Context
 		expected string
 		expected string
 	}{
 	}{
 
 
 		// Errors
 		// Errors
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "{{InvalidFunction}}",
-				},
-			},
+			Context{Format: "{{InvalidFunction}}"},
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
 `,
 `,
 		},
 		},
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "{{nil}}",
-				},
-			},
+			Context{Format: "{{nil}}"},
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
 `,
 `,
 		},
 		},
 		// Table format
 		// Table format
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "table",
-				},
-			},
+			Context{Format: NewVolumeFormat("table", false)},
 			`DRIVER              NAME
 			`DRIVER              NAME
 foo                 foobar_baz
 foo                 foobar_baz
 bar                 foobar_bar
 bar                 foobar_bar
 `,
 `,
 		},
 		},
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "table",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewVolumeFormat("table", true)},
 			`foobar_baz
 			`foobar_baz
 foobar_bar
 foobar_bar
 `,
 `,
 		},
 		},
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "table {{.Name}}",
-				},
-			},
+			Context{Format: NewVolumeFormat("table {{.Name}}", false)},
 			`NAME
 			`NAME
 foobar_baz
 foobar_baz
 foobar_bar
 foobar_bar
 `,
 `,
 		},
 		},
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "table {{.Name}}",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewVolumeFormat("table {{.Name}}", true)},
 			`NAME
 			`NAME
 foobar_baz
 foobar_baz
 foobar_bar
 foobar_bar
@@ -128,11 +103,8 @@ foobar_bar
 		},
 		},
 		// Raw Format
 		// Raw Format
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "raw",
-				},
-			}, `name: foobar_baz
+			Context{Format: NewVolumeFormat("raw", false)},
+			`name: foobar_baz
 driver: foo
 driver: foo
 
 
 name: foobar_bar
 name: foobar_bar
@@ -141,43 +113,32 @@ driver: bar
 `,
 `,
 		},
 		},
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "raw",
-					Quiet:  true,
-				},
-			},
+			Context{Format: NewVolumeFormat("raw", true)},
 			`name: foobar_baz
 			`name: foobar_baz
 name: foobar_bar
 name: foobar_bar
 `,
 `,
 		},
 		},
 		// Custom Format
 		// Custom Format
 		{
 		{
-			VolumeContext{
-				Context: Context{
-					Format: "{{.Name}}",
-				},
-			},
+			Context{Format: NewVolumeFormat("{{.Name}}", false)},
 			`foobar_baz
 			`foobar_baz
 foobar_bar
 foobar_bar
 `,
 `,
 		},
 		},
 	}
 	}
 
 
-	for _, context := range contexts {
+	for _, testcase := range cases {
 		volumes := []*types.Volume{
 		volumes := []*types.Volume{
 			{Name: "foobar_baz", Driver: "foo"},
 			{Name: "foobar_baz", Driver: "foo"},
 			{Name: "foobar_bar", Driver: "bar"},
 			{Name: "foobar_bar", Driver: "bar"},
 		}
 		}
 		out := bytes.NewBufferString("")
 		out := bytes.NewBufferString("")
-		context.context.Output = out
-		context.context.Volumes = volumes
-		context.context.Write()
-		actual := out.String()
-		if actual != context.expected {
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
+		testcase.context.Output = out
+		err := VolumeWrite(testcase.context, volumes)
+		if err != nil {
+			assert.Error(t, err, testcase.expected)
+		} else {
+			assert.Equal(t, out.String(), testcase.expected)
 		}
 		}
-		// Clean buffer
-		out.Reset()
 	}
 	}
 }
 }

+ 7 - 12
cli/command/image/images.go

@@ -64,27 +64,22 @@ func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
 		return err
 		return err
 	}
 	}
 
 
-	f := opts.format
-	if len(f) == 0 {
+	format := opts.format
+	if len(format) == 0 {
 		if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
 		if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
-			f = dockerCli.ConfigFile().ImagesFormat
+			format = dockerCli.ConfigFile().ImagesFormat
 		} else {
 		} else {
-			f = "table"
+			format = formatter.TableFormatKey
 		}
 		}
 	}
 	}
 
 
-	imagesCtx := formatter.ImageContext{
+	imageCtx := formatter.ImageContext{
 		Context: formatter.Context{
 		Context: formatter.Context{
 			Output: dockerCli.Out(),
 			Output: dockerCli.Out(),
-			Format: f,
-			Quiet:  opts.quiet,
+			Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests),
 			Trunc:  !opts.noTrunc,
 			Trunc:  !opts.noTrunc,
 		},
 		},
 		Digest: opts.showDigests,
 		Digest: opts.showDigests,
-		Images: images,
 	}
 	}
-
-	imagesCtx.Write()
-
-	return nil
+	return formatter.ImageWrite(imageCtx, images)
 }
 }

+ 9 - 17
cli/command/network/list.go

@@ -50,35 +50,27 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
 
 
 func runList(dockerCli *command.DockerCli, opts listOptions) error {
 func runList(dockerCli *command.DockerCli, opts listOptions) error {
 	client := dockerCli.Client()
 	client := dockerCli.Client()
-
 	options := types.NetworkListOptions{Filters: opts.filter.Value()}
 	options := types.NetworkListOptions{Filters: opts.filter.Value()}
 	networkResources, err := client.NetworkList(context.Background(), options)
 	networkResources, err := client.NetworkList(context.Background(), options)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	f := opts.format
-	if len(f) == 0 {
+	format := opts.format
+	if len(format) == 0 {
 		if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
 		if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
-			f = dockerCli.ConfigFile().NetworksFormat
+			format = dockerCli.ConfigFile().NetworksFormat
 		} else {
 		} else {
-			f = "table"
+			format = formatter.TableFormatKey
 		}
 		}
 	}
 	}
 
 
 	sort.Sort(byNetworkName(networkResources))
 	sort.Sort(byNetworkName(networkResources))
 
 
-	networksCtx := formatter.NetworkContext{
-		Context: formatter.Context{
-			Output: dockerCli.Out(),
-			Format: f,
-			Quiet:  opts.quiet,
-			Trunc:  !opts.noTrunc,
-		},
-		Networks: networkResources,
+	networksCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewNetworkFormat(format, opts.quiet),
+		Trunc:  !opts.noTrunc,
 	}
 	}
-
-	networksCtx.Write()
-
-	return nil
+	return formatter.NetworkWrite(networksCtx, networkResources)
 }
 }

+ 8 - 15
cli/command/volume/list.go

@@ -56,29 +56,22 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
 		return err
 		return err
 	}
 	}
 
 
-	f := opts.format
-	if len(f) == 0 {
+	format := opts.format
+	if len(format) == 0 {
 		if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet {
 		if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet {
-			f = dockerCli.ConfigFile().VolumesFormat
+			format = dockerCli.ConfigFile().VolumesFormat
 		} else {
 		} else {
-			f = "table"
+			format = formatter.TableFormatKey
 		}
 		}
 	}
 	}
 
 
 	sort.Sort(byVolumeName(volumes.Volumes))
 	sort.Sort(byVolumeName(volumes.Volumes))
 
 
-	volumeCtx := formatter.VolumeContext{
-		Context: formatter.Context{
-			Output: dockerCli.Out(),
-			Format: f,
-			Quiet:  opts.quiet,
-		},
-		Volumes: volumes.Volumes,
+	volumeCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewVolumeFormat(format, opts.quiet),
 	}
 	}
-
-	volumeCtx.Write()
-
-	return nil
+	return formatter.VolumeWrite(volumeCtx, volumes.Volumes)
 }
 }
 
 
 var listDescription = `
 var listDescription = `

+ 9 - 0
utils/templates/templates.go

@@ -18,6 +18,7 @@ var basicFunctions = template.FuncMap{
 	"title": strings.Title,
 	"title": strings.Title,
 	"lower": strings.ToLower,
 	"lower": strings.ToLower,
 	"upper": strings.ToUpper,
 	"upper": strings.ToUpper,
+	"pad":   padWithSpace,
 }
 }
 
 
 // Parse creates a new annonymous template with the basic functions
 // Parse creates a new annonymous template with the basic functions
@@ -31,3 +32,11 @@ func Parse(format string) (*template.Template, error) {
 func NewParse(tag, format string) (*template.Template, error) {
 func NewParse(tag, format string) (*template.Template, error) {
 	return template.New(tag).Funcs(basicFunctions).Parse(format)
 	return template.New(tag).Funcs(basicFunctions).Parse(format)
 }
 }
+
+// padWithSpace adds whitespace to the input if the input is non-empty
+func padWithSpace(source string, prefix, suffix int) string {
+	if source == "" {
+		return source
+	}
+	return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix)
+}