Selaa lähdekoodia

Merge pull request #30733 from yongtang/02022017-formatter-header

Allow `--format` to use different delim in `table` format
Kenfe-Mickaël Laventure 8 vuotta sitten
vanhempi
commit
9369acc776

+ 33 - 25
cli/command/formatter/container.go

@@ -15,7 +15,7 @@ import (
 )
 
 const (
-	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
+	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
 
 	containerIDHeader = "CONTAINER ID"
 	namesHeader       = "NAMES"
@@ -72,7 +72,17 @@ func ContainerWrite(ctx Context, containers []types.Container) error {
 		}
 		return nil
 	}
-	return ctx.Write(&containerContext{}, render)
+	return ctx.Write(newContainerContext(), render)
+}
+
+type containerHeaderContext map[string]string
+
+func (c containerHeaderContext) Label(name string) string {
+	n := strings.Split(name, ".")
+	r := strings.NewReplacer("-", " ", "_", " ")
+	h := r.Replace(n[len(n)-1])
+
+	return h
 }
 
 type containerContext struct {
@@ -81,12 +91,31 @@ type containerContext struct {
 	c     types.Container
 }
 
+func newContainerContext() *containerContext {
+	containerCtx := containerContext{}
+	containerCtx.header = containerHeaderContext{
+		"ID":           containerIDHeader,
+		"Names":        namesHeader,
+		"Image":        imageHeader,
+		"Command":      commandHeader,
+		"CreatedAt":    createdAtHeader,
+		"RunningFor":   runningForHeader,
+		"Ports":        portsHeader,
+		"Status":       statusHeader,
+		"Size":         sizeHeader,
+		"Labels":       labelsHeader,
+		"Mounts":       mountsHeader,
+		"LocalVolumes": localVolumes,
+		"Networks":     networksHeader,
+	}
+	return &containerCtx
+}
+
 func (c *containerContext) MarshalJSON() ([]byte, error) {
 	return marshalJSON(c)
 }
 
 func (c *containerContext) ID() string {
-	c.AddHeader(containerIDHeader)
 	if c.trunc {
 		return stringid.TruncateID(c.c.ID)
 	}
@@ -94,7 +123,6 @@ func (c *containerContext) ID() string {
 }
 
 func (c *containerContext) Names() string {
-	c.AddHeader(namesHeader)
 	names := stripNamePrefix(c.c.Names)
 	if c.trunc {
 		for _, name := range names {
@@ -108,7 +136,6 @@ func (c *containerContext) Names() string {
 }
 
 func (c *containerContext) Image() string {
-	c.AddHeader(imageHeader)
 	if c.c.Image == "" {
 		return "<no image>"
 	}
@@ -136,7 +163,6 @@ func (c *containerContext) Image() string {
 }
 
 func (c *containerContext) Command() string {
-	c.AddHeader(commandHeader)
 	command := c.c.Command
 	if c.trunc {
 		command = stringutils.Ellipsis(command, 20)
@@ -145,28 +171,23 @@ func (c *containerContext) Command() string {
 }
 
 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))
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
 }
 
 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.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
 	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
 
@@ -178,7 +199,6 @@ func (c *containerContext) Size() string {
 }
 
 func (c *containerContext) Labels() string {
-	c.AddHeader(labelsHeader)
 	if c.c.Labels == nil {
 		return ""
 	}
@@ -191,12 +211,6 @@ func (c *containerContext) Labels() string {
 }
 
 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 ""
 	}
@@ -204,8 +218,6 @@ func (c *containerContext) Label(name string) string {
 }
 
 func (c *containerContext) Mounts() string {
-	c.AddHeader(mountsHeader)
-
 	var name string
 	var mounts []string
 	for _, m := range c.c.Mounts {
@@ -223,8 +235,6 @@ func (c *containerContext) Mounts() string {
 }
 
 func (c *containerContext) LocalVolumes() string {
-	c.AddHeader(localVolumes)
-
 	count := 0
 	for _, m := range c.c.Mounts {
 		if m.Driver == "local" {
@@ -236,8 +246,6 @@ func (c *containerContext) LocalVolumes() string {
 }
 
 func (c *containerContext) Networks() string {
-	c.AddHeader(networksHeader)
-
 	if c.c.NetworkSettings == nil {
 		return ""
 	}

+ 29 - 42
cli/command/formatter/container_test.go

@@ -22,22 +22,20 @@ func TestContainerPsContext(t *testing.T) {
 		container types.Container
 		trunc     bool
 		expValue  string
-		expHeader string
 		call      func() string
 	}{
-		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
-		{types.Container{ID: containerID}, false, containerID, containerIDHeader, 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: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
-		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
+		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID},
+		{types.Container{ID: containerID}, false, containerID, ctx.ID},
+		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names},
+		{types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image},
+		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image},
+		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image},
 		{types.Container{
 			Image:   "a5a665ff33eced1e0803148700880edab4",
 			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
 		},
 			true,
 			"a5a665ff33ec",
-			imageHeader,
 			ctx.Image,
 		},
 		{types.Container{
@@ -46,19 +44,18 @@ func TestContainerPsContext(t *testing.T) {
 		},
 			false,
 			"a5a665ff33eced1e0803148700880edab4",
-			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: 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, "10B", sizeHeader, ctx.Size},
-		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", sizeHeader, ctx.Size},
-		{types.Container{}, true, "", labelsHeader, ctx.Labels},
-		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
-		{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
+		{types.Container{Image: ""}, true, "<no image>", ctx.Image},
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command},
+		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt},
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports},
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status},
+		{types.Container{SizeRw: 10}, true, "10B", ctx.Size},
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size},
+		{types.Container{}, true, "", ctx.Labels},
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels},
+		{types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor},
 		{types.Container{
 			Mounts: []types.MountPoint{
 				{
@@ -67,7 +64,7 @@ func TestContainerPsContext(t *testing.T) {
 					Source: "/a/path",
 				},
 			},
-		}, true, "this-is-a-lo...", mountsHeader, ctx.Mounts},
+		}, true, "this-is-a-lo...", ctx.Mounts},
 		{types.Container{
 			Mounts: []types.MountPoint{
 				{
@@ -75,7 +72,7 @@ func TestContainerPsContext(t *testing.T) {
 					Source: "/a/path",
 				},
 			},
-		}, false, "/a/path", mountsHeader, ctx.Mounts},
+		}, false, "/a/path", ctx.Mounts},
 		{types.Container{
 			Mounts: []types.MountPoint{
 				{
@@ -84,7 +81,7 @@ func TestContainerPsContext(t *testing.T) {
 					Source: "/a/path",
 				},
 			},
-		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts},
+		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts},
 	}
 
 	for _, c := range cases {
@@ -95,11 +92,6 @@ func TestContainerPsContext(t *testing.T) {
 		} 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)
-		}
 	}
 
 	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
@@ -115,12 +107,6 @@ func TestContainerPsContext(t *testing.T) {
 		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)
-
-	}
-
 	c2 := types.Container{}
 	ctx = containerContext{c: c2, trunc: true}
 
@@ -128,13 +114,6 @@ func TestContainerPsContext(t *testing.T) {
 	if label != "" {
 		t.Fatalf("Expected an empty string, was %s", label)
 	}
-
-	ctx = containerContext{c: c2, trunc: true}
-	FullHeader := ctx.FullHeader()
-	if FullHeader != "" {
-		t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader)
-	}
-
 }
 
 func TestContainerContextWrite(t *testing.T) {
@@ -247,6 +226,14 @@ size: 0B
 			Context{Format: NewContainerFormat("{{.Image}}", false, true)},
 			"ubuntu\nubuntu\n",
 		},
+		// Special headers for customerized table format
+		{
+			Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
+			`CONTAINER ID        IMAGE CREATED/STATUS/  PORTS  .NAMES STATUS
+conta               "ubuntu" 24 hours ago//.FOOBAR_BAZ 
+conta               "ubuntu" 24 hours ago//.FOOBAR_BAR 
+`,
+		},
 	}
 
 	for _, testcase := range cases {
@@ -333,8 +320,8 @@ func TestContainerContextWriteJSON(t *testing.T) {
 	}
 	expectedCreated := time.Unix(unix, 0).String()
 	expectedJSONs := []map[string]interface{}{
-		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""},
-		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""},
+		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""},
+		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""},
 	}
 	out := bytes.NewBufferString("")
 	err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers)

+ 5 - 21
cli/command/formatter/custom.go

@@ -1,9 +1,5 @@
 package formatter
 
-import (
-	"strings"
-)
-
 const (
 	imageHeader        = "IMAGE"
 	createdSinceHeader = "CREATED"
@@ -16,29 +12,17 @@ const (
 )
 
 type subContext interface {
-	FullHeader() string
-	AddHeader(header string)
+	FullHeader() interface{}
 }
 
 // HeaderContext provides the subContext interface for managing headers
 type HeaderContext struct {
-	header []string
-}
-
-// FullHeader returns the header as a string
-func (c *HeaderContext) FullHeader() string {
-	if c.header == nil {
-		return ""
-	}
-	return strings.Join(c.header, "\t")
+	header interface{}
 }
 
-// AddHeader adds another column to the header
-func (c *HeaderContext) AddHeader(header string) {
-	if c.header == nil {
-		c.header = []string{}
-	}
-	c.header = append(c.header, strings.ToUpper(header))
+// FullHeader returns the header as an interface
+func (c *HeaderContext) FullHeader() interface{} {
+	return c.header
 }
 
 func stripNamePrefix(ss []string) []string {

+ 12 - 19
cli/command/formatter/disk_usage.go

@@ -77,7 +77,15 @@ func (ctx *DiskUsageContext) Write() {
 			return
 		}
 
-		ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}})
+		diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
+		diskUsageContainersCtx.header = map[string]string{
+			"Type":        typeHeader,
+			"TotalCount":  totalHeader,
+			"Active":      activeHeader,
+			"Size":        sizeHeader,
+			"Reclaimable": reclaimableHeader,
+		}
+		ctx.postFormat(tmpl, &diskUsageContainersCtx)
 
 		return
 	}
@@ -114,7 +122,7 @@ func (ctx *DiskUsageContext) Write() {
 			return
 		}
 	}
-	ctx.postFormat(tmpl, &imageContext{})
+	ctx.postFormat(tmpl, newImageContext())
 
 	// Now containers
 	ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
@@ -133,7 +141,7 @@ func (ctx *DiskUsageContext) Write() {
 			return
 		}
 	}
-	ctx.postFormat(tmpl, &containerContext{})
+	ctx.postFormat(tmpl, newContainerContext())
 
 	// And volumes
 	ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
@@ -149,7 +157,7 @@ func (ctx *DiskUsageContext) Write() {
 			return
 		}
 	}
-	ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}})
+	ctx.postFormat(tmpl, newVolumeContext())
 }
 
 type diskUsageImagesContext struct {
@@ -163,17 +171,14 @@ func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *diskUsageImagesContext) Type() string {
-	c.AddHeader(typeHeader)
 	return "Images"
 }
 
 func (c *diskUsageImagesContext) TotalCount() string {
-	c.AddHeader(totalHeader)
 	return fmt.Sprintf("%d", len(c.images))
 }
 
 func (c *diskUsageImagesContext) Active() string {
-	c.AddHeader(activeHeader)
 	used := 0
 	for _, i := range c.images {
 		if i.Containers > 0 {
@@ -185,7 +190,6 @@ func (c *diskUsageImagesContext) Active() string {
 }
 
 func (c *diskUsageImagesContext) Size() string {
-	c.AddHeader(sizeHeader)
 	return units.HumanSize(float64(c.totalSize))
 
 }
@@ -193,7 +197,6 @@ func (c *diskUsageImagesContext) Size() string {
 func (c *diskUsageImagesContext) Reclaimable() string {
 	var used int64
 
-	c.AddHeader(reclaimableHeader)
 	for _, i := range c.images {
 		if i.Containers != 0 {
 			if i.VirtualSize == -1 || i.SharedSize == -1 {
@@ -221,12 +224,10 @@ func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *diskUsageContainersContext) Type() string {
-	c.AddHeader(typeHeader)
 	return "Containers"
 }
 
 func (c *diskUsageContainersContext) TotalCount() string {
-	c.AddHeader(totalHeader)
 	return fmt.Sprintf("%d", len(c.containers))
 }
 
@@ -237,7 +238,6 @@ func (c *diskUsageContainersContext) isActive(container types.Container) bool {
 }
 
 func (c *diskUsageContainersContext) Active() string {
-	c.AddHeader(activeHeader)
 	used := 0
 	for _, container := range c.containers {
 		if c.isActive(*container) {
@@ -251,7 +251,6 @@ func (c *diskUsageContainersContext) Active() string {
 func (c *diskUsageContainersContext) Size() string {
 	var size int64
 
-	c.AddHeader(sizeHeader)
 	for _, container := range c.containers {
 		size += container.SizeRw
 	}
@@ -263,7 +262,6 @@ func (c *diskUsageContainersContext) Reclaimable() string {
 	var reclaimable int64
 	var totalSize int64
 
-	c.AddHeader(reclaimableHeader)
 	for _, container := range c.containers {
 		if !c.isActive(*container) {
 			reclaimable += container.SizeRw
@@ -289,17 +287,14 @@ func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *diskUsageVolumesContext) Type() string {
-	c.AddHeader(typeHeader)
 	return "Local Volumes"
 }
 
 func (c *diskUsageVolumesContext) TotalCount() string {
-	c.AddHeader(totalHeader)
 	return fmt.Sprintf("%d", len(c.volumes))
 }
 
 func (c *diskUsageVolumesContext) Active() string {
-	c.AddHeader(activeHeader)
 
 	used := 0
 	for _, v := range c.volumes {
@@ -314,7 +309,6 @@ func (c *diskUsageVolumesContext) Active() string {
 func (c *diskUsageVolumesContext) Size() string {
 	var size int64
 
-	c.AddHeader(sizeHeader)
 	for _, v := range c.volumes {
 		if v.UsageData.Size != -1 {
 			size += v.UsageData.Size
@@ -328,7 +322,6 @@ func (c *diskUsageVolumesContext) Reclaimable() string {
 	var reclaimable int64
 	var totalSize int64
 
-	c.AddHeader(reclaimableHeader)
 	for _, v := range c.volumes {
 		if v.UsageData.Size != -1 {
 			if v.UsageData.RefCount == 0 {

+ 47 - 0
cli/command/formatter/disk_usage_test.go

@@ -0,0 +1,47 @@
+package formatter
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestDiskUsageContextFormatWrite(t *testing.T) {
+	// Check default output format (verbose and non-verbose mode) for table headers
+	cases := []struct {
+		context  DiskUsageContext
+		expected string
+	}{
+		{
+			DiskUsageContext{Verbose: false},
+			`TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
+Images              0                   0                   0B                  0B
+Containers          0                   0                   0B                  0B
+Local Volumes       0                   0                   0B                  0B
+`,
+		},
+		{
+			DiskUsageContext{Verbose: true},
+			`Images space usage:
+
+REPOSITORY          TAG                 IMAGE ID            CREATED ago         SIZE                SHARED SIZE         UNIQUE SiZE         CONTAINERS
+
+Containers space usage:
+
+CONTAINER ID        IMAGE               COMMAND             LOCAL VOLUMES       SIZE                CREATED ago         STATUS              NAMES
+
+Local Volumes space usage:
+
+VOLUME NAME         LINKS               SIZE
+`,
+		},
+	}
+
+	for _, testcase := range cases {
+		out := bytes.NewBufferString("")
+		testcase.context.Output = out
+		testcase.context.Write()
+		assert.Equal(t, out.String(), testcase.expected)
+	}
+}

+ 5 - 9
cli/command/formatter/formatter.go

@@ -44,7 +44,7 @@ type Context struct {
 
 	// internal element
 	finalFormat string
-	header      string
+	header      interface{}
 	buffer      *bytes.Buffer
 }
 
@@ -71,14 +71,10 @@ func (c *Context) parseFormat() (*template.Template, error) {
 
 func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
 	if c.Format.IsTable() {
-		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
-			tmpl.Execute(bytes.NewBufferString(""), subContext)
-			c.header = subContext.FullHeader()
-		}
-
 		t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
-		t.Write([]byte(c.header))
+		buffer := bytes.NewBufferString("")
+		tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader())
+		buffer.WriteTo(t)
 		t.Write([]byte("\n"))
 		c.buffer.WriteTo(t)
 		t.Flush()
@@ -91,7 +87,7 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext)
 	if err := tmpl.Execute(c.buffer, subContext); err != nil {
 		return fmt.Errorf("Template parsing error: %v\n", err)
 	}
-	if c.Format.IsTable() && len(c.header) == 0 {
+	if c.Format.IsTable() && c.header != nil {
 		c.header = subContext.FullHeader()
 	}
 	c.buffer.WriteString("\n")

+ 22 - 15
cli/command/formatter/image.go

@@ -11,8 +11,8 @@ import (
 )
 
 const (
-	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
-	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
+	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"
+	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"
 
 	imageIDHeader    = "IMAGE ID"
 	repositoryHeader = "REPOSITORY"
@@ -76,7 +76,7 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
 	render := func(format func(subContext subContext) error) error {
 		return imageFormat(ctx, images, format)
 	}
-	return ctx.Write(&imageContext{}, render)
+	return ctx.Write(newImageContext(), render)
 }
 
 func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
@@ -192,12 +192,29 @@ type imageContext struct {
 	digest string
 }
 
+func newImageContext() *imageContext {
+	imageCtx := imageContext{}
+	imageCtx.header = map[string]string{
+		"ID":           imageIDHeader,
+		"Repository":   repositoryHeader,
+		"Tag":          tagHeader,
+		"Digest":       digestHeader,
+		"CreatedSince": createdSinceHeader,
+		"CreatedAt":    createdAtHeader,
+		"Size":         sizeHeader,
+		"Containers":   containersHeader,
+		"VirtualSize":  sizeHeader,
+		"SharedSize":   sharedSizeHeader,
+		"UniqueSize":   uniqueSizeHeader,
+	}
+	return &imageCtx
+}
+
 func (c *imageContext) MarshalJSON() ([]byte, error) {
 	return marshalJSON(c)
 }
 
 func (c *imageContext) ID() string {
-	c.AddHeader(imageIDHeader)
 	if c.trunc {
 		return stringid.TruncateID(c.i.ID)
 	}
@@ -205,38 +222,31 @@ func (c *imageContext) ID() string {
 }
 
 func (c *imageContext) Repository() string {
-	c.AddHeader(repositoryHeader)
 	return c.repo
 }
 
 func (c *imageContext) Tag() string {
-	c.AddHeader(tagHeader)
 	return c.tag
 }
 
 func (c *imageContext) Digest() string {
-	c.AddHeader(digestHeader)
 	return c.digest
 }
 
 func (c *imageContext) CreatedSince() string {
-	c.AddHeader(createdSinceHeader)
 	createdAt := time.Unix(int64(c.i.Created), 0)
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
 }
 
 func (c *imageContext) CreatedAt() string {
-	c.AddHeader(createdAtHeader)
 	return time.Unix(int64(c.i.Created), 0).String()
 }
 
 func (c *imageContext) Size() string {
-	c.AddHeader(sizeHeader)
 	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
 }
 
 func (c *imageContext) Containers() string {
-	c.AddHeader(containersHeader)
 	if c.i.Containers == -1 {
 		return "N/A"
 	}
@@ -244,12 +254,10 @@ func (c *imageContext) Containers() string {
 }
 
 func (c *imageContext) VirtualSize() string {
-	c.AddHeader(sizeHeader)
 	return units.HumanSize(float64(c.i.VirtualSize))
 }
 
 func (c *imageContext) SharedSize() string {
-	c.AddHeader(sharedSizeHeader)
 	if c.i.SharedSize == -1 {
 		return "N/A"
 	}
@@ -257,7 +265,6 @@ func (c *imageContext) SharedSize() string {
 }
 
 func (c *imageContext) UniqueSize() string {
-	c.AddHeader(uniqueSizeHeader)
 	if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
 		return "N/A"
 	}

+ 10 - 16
cli/command/formatter/image_test.go

@@ -18,27 +18,26 @@ func TestImageContext(t *testing.T) {
 
 	var ctx imageContext
 	cases := []struct {
-		imageCtx  imageContext
-		expValue  string
-		expHeader string
-		call      func() string
+		imageCtx imageContext
+		expValue string
+		call     func() string
 	}{
 		{imageContext{
 			i:     types.ImageSummary{ID: imageID},
 			trunc: true,
-		}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
+		}, stringid.TruncateID(imageID), ctx.ID},
 		{imageContext{
 			i:     types.ImageSummary{ID: imageID},
 			trunc: false,
-		}, imageID, imageIDHeader, ctx.ID},
+		}, imageID, ctx.ID},
 		{imageContext{
 			i:     types.ImageSummary{Size: 10, VirtualSize: 10},
 			trunc: true,
-		}, "10B", sizeHeader, ctx.Size},
+		}, "10B", ctx.Size},
 		{imageContext{
 			i:     types.ImageSummary{Created: unix},
 			trunc: true,
-		}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
+		}, time.Unix(unix, 0).String(), ctx.CreatedAt},
 		// FIXME
 		// {imageContext{
 		// 	i:     types.ImageSummary{Created: unix},
@@ -47,15 +46,15 @@ func TestImageContext(t *testing.T) {
 		{imageContext{
 			i:    types.ImageSummary{},
 			repo: "busybox",
-		}, "busybox", repositoryHeader, ctx.Repository},
+		}, "busybox", ctx.Repository},
 		{imageContext{
 			i:   types.ImageSummary{},
 			tag: "latest",
-		}, "latest", tagHeader, ctx.Tag},
+		}, "latest", ctx.Tag},
 		{imageContext{
 			i:      types.ImageSummary{},
 			digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
-		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
+		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest},
 	}
 
 	for _, c := range cases {
@@ -66,11 +65,6 @@ func TestImageContext(t *testing.T) {
 		} 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)
-		}
 	}
 }
 

+ 22 - 15
cli/command/formatter/network.go

@@ -44,7 +44,28 @@ func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
 		}
 		return nil
 	}
-	return ctx.Write(&networkContext{}, render)
+	networkCtx := networkContext{}
+	networkCtx.header = networkHeaderContext{
+		"ID":        networkIDHeader,
+		"Name":      nameHeader,
+		"Driver":    driverHeader,
+		"Scope":     scopeHeader,
+		"IPv6":      ipv6Header,
+		"Internal":  internalHeader,
+		"Labels":    labelsHeader,
+		"CreatedAt": createdAtHeader,
+	}
+	return ctx.Write(&networkCtx, render)
+}
+
+type networkHeaderContext map[string]string
+
+func (c networkHeaderContext) Label(name string) string {
+	n := strings.Split(name, ".")
+	r := strings.NewReplacer("-", " ", "_", " ")
+	h := r.Replace(n[len(n)-1])
+
+	return h
 }
 
 type networkContext struct {
@@ -58,7 +79,6 @@ func (c *networkContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *networkContext) ID() string {
-	c.AddHeader(networkIDHeader)
 	if c.trunc {
 		return stringid.TruncateID(c.n.ID)
 	}
@@ -66,32 +86,26 @@ func (c *networkContext) ID() string {
 }
 
 func (c *networkContext) Name() string {
-	c.AddHeader(nameHeader)
 	return c.n.Name
 }
 
 func (c *networkContext) Driver() string {
-	c.AddHeader(driverHeader)
 	return c.n.Driver
 }
 
 func (c *networkContext) Scope() string {
-	c.AddHeader(scopeHeader)
 	return c.n.Scope
 }
 
 func (c *networkContext) IPv6() string {
-	c.AddHeader(ipv6Header)
 	return fmt.Sprintf("%v", c.n.EnableIPv6)
 }
 
 func (c *networkContext) Internal() string {
-	c.AddHeader(internalHeader)
 	return fmt.Sprintf("%v", c.n.Internal)
 }
 
 func (c *networkContext) Labels() string {
-	c.AddHeader(labelsHeader)
 	if c.n.Labels == nil {
 		return ""
 	}
@@ -104,12 +118,6 @@ func (c *networkContext) Labels() string {
 }
 
 func (c *networkContext) Label(name string) string {
-	n := strings.Split(name, ".")
-	r := strings.NewReplacer("-", " ", "_", " ")
-	h := r.Replace(n[len(n)-1])
-
-	c.AddHeader(h)
-
 	if c.n.Labels == nil {
 		return ""
 	}
@@ -117,6 +125,5 @@ func (c *networkContext) Label(name string) string {
 }
 
 func (c *networkContext) CreatedAt() string {
-	c.AddHeader(createdAtHeader)
 	return c.n.Created.String()
 }

+ 10 - 16
cli/command/formatter/network_test.go

@@ -19,41 +19,40 @@ func TestNetworkContext(t *testing.T) {
 	cases := []struct {
 		networkCtx networkContext
 		expValue   string
-		expHeader  string
 		call       func() string
 	}{
 		{networkContext{
 			n:     types.NetworkResource{ID: networkID},
 			trunc: false,
-		}, networkID, networkIDHeader, ctx.ID},
+		}, networkID, ctx.ID},
 		{networkContext{
 			n:     types.NetworkResource{ID: networkID},
 			trunc: true,
-		}, stringid.TruncateID(networkID), networkIDHeader, ctx.ID},
+		}, stringid.TruncateID(networkID), ctx.ID},
 		{networkContext{
 			n: types.NetworkResource{Name: "network_name"},
-		}, "network_name", nameHeader, ctx.Name},
+		}, "network_name", ctx.Name},
 		{networkContext{
 			n: types.NetworkResource{Driver: "driver_name"},
-		}, "driver_name", driverHeader, ctx.Driver},
+		}, "driver_name", ctx.Driver},
 		{networkContext{
 			n: types.NetworkResource{EnableIPv6: true},
-		}, "true", ipv6Header, ctx.IPv6},
+		}, "true", ctx.IPv6},
 		{networkContext{
 			n: types.NetworkResource{EnableIPv6: false},
-		}, "false", ipv6Header, ctx.IPv6},
+		}, "false", ctx.IPv6},
 		{networkContext{
 			n: types.NetworkResource{Internal: true},
-		}, "true", internalHeader, ctx.Internal},
+		}, "true", ctx.Internal},
 		{networkContext{
 			n: types.NetworkResource{Internal: false},
-		}, "false", internalHeader, ctx.Internal},
+		}, "false", ctx.Internal},
 		{networkContext{
 			n: types.NetworkResource{},
-		}, "", labelsHeader, ctx.Labels},
+		}, "", ctx.Labels},
 		{networkContext{
 			n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
-		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
+		}, "label1=value1,label2=value2", ctx.Labels},
 	}
 
 	for _, c := range cases {
@@ -64,11 +63,6 @@ func TestNetworkContext(t *testing.T) {
 		} 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)
-		}
 	}
 }
 

+ 9 - 6
cli/command/formatter/plugin.go

@@ -44,7 +44,15 @@ func PluginWrite(ctx Context, plugins []*types.Plugin) error {
 		}
 		return nil
 	}
-	return ctx.Write(&pluginContext{}, render)
+	pluginCtx := pluginContext{}
+	pluginCtx.header = map[string]string{
+		"ID":              pluginIDHeader,
+		"Name":            nameHeader,
+		"Description":     descriptionHeader,
+		"Enabled":         enabledHeader,
+		"PluginReference": imageHeader,
+	}
+	return ctx.Write(&pluginCtx, render)
 }
 
 type pluginContext struct {
@@ -58,7 +66,6 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *pluginContext) ID() string {
-	c.AddHeader(pluginIDHeader)
 	if c.trunc {
 		return stringid.TruncateID(c.p.ID)
 	}
@@ -66,12 +73,10 @@ func (c *pluginContext) ID() string {
 }
 
 func (c *pluginContext) Name() string {
-	c.AddHeader(nameHeader)
 	return c.p.Name
 }
 
 func (c *pluginContext) Description() string {
-	c.AddHeader(descriptionHeader)
 	desc := strings.Replace(c.p.Config.Description, "\n", "", -1)
 	desc = strings.Replace(desc, "\r", "", -1)
 	if c.trunc {
@@ -82,11 +87,9 @@ func (c *pluginContext) Description() string {
 }
 
 func (c *pluginContext) Enabled() bool {
-	c.AddHeader(enabledHeader)
 	return c.p.Enabled
 }
 
 func (c *pluginContext) PluginReference() string {
-	c.AddHeader(imageHeader)
 	return c.p.PluginReference
 }

+ 4 - 10
cli/command/formatter/plugin_test.go

@@ -18,23 +18,22 @@ func TestPluginContext(t *testing.T) {
 	cases := []struct {
 		pluginCtx pluginContext
 		expValue  string
-		expHeader string
 		call      func() string
 	}{
 		{pluginContext{
 			p:     types.Plugin{ID: pluginID},
 			trunc: false,
-		}, pluginID, pluginIDHeader, ctx.ID},
+		}, pluginID, ctx.ID},
 		{pluginContext{
 			p:     types.Plugin{ID: pluginID},
 			trunc: true,
-		}, stringid.TruncateID(pluginID), pluginIDHeader, ctx.ID},
+		}, stringid.TruncateID(pluginID), ctx.ID},
 		{pluginContext{
 			p: types.Plugin{Name: "plugin_name"},
-		}, "plugin_name", nameHeader, ctx.Name},
+		}, "plugin_name", ctx.Name},
 		{pluginContext{
 			p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}},
-		}, "plugin_description", descriptionHeader, ctx.Description},
+		}, "plugin_description", ctx.Description},
 	}
 
 	for _, c := range cases {
@@ -45,11 +44,6 @@ func TestPluginContext(t *testing.T) {
 		} 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)
-		}
 	}
 }
 

+ 9 - 6
cli/command/formatter/service.go

@@ -388,7 +388,15 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser
 		}
 		return nil
 	}
-	return ctx.Write(&serviceContext{}, render)
+	serviceCtx := serviceContext{}
+	serviceCtx.header = map[string]string{
+		"ID":       serviceIDHeader,
+		"Name":     nameHeader,
+		"Mode":     modeHeader,
+		"Replicas": replicasHeader,
+		"Image":    imageHeader,
+	}
+	return ctx.Write(&serviceCtx, render)
 }
 
 type serviceContext struct {
@@ -403,27 +411,22 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *serviceContext) ID() string {
-	c.AddHeader(serviceIDHeader)
 	return stringid.TruncateID(c.service.ID)
 }
 
 func (c *serviceContext) Name() string {
-	c.AddHeader(nameHeader)
 	return c.service.Spec.Name
 }
 
 func (c *serviceContext) Mode() string {
-	c.AddHeader(modeHeader)
 	return c.mode
 }
 
 func (c *serviceContext) Replicas() string {
-	c.AddHeader(replicasHeader)
 	return c.replicas
 }
 
 func (c *serviceContext) Image() string {
-	c.AddHeader(imageHeader)
 	image := c.service.Spec.TaskTemplate.ContainerSpec.Image
 	if ref, err := reference.ParseNormalizedNamed(image); err == nil {
 		// update image string for display, (strips any digest)

+ 18 - 15
cli/command/formatter/stats.go

@@ -129,7 +129,24 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
 		}
 		return nil
 	}
-	return ctx.Write(&containerStatsContext{os: osType}, render)
+	memUsage := memUseHeader
+	if osType == winOSType {
+		memUsage = winMemUseHeader
+	}
+	containerStatsCtx := containerStatsContext{}
+	containerStatsCtx.header = map[string]string{
+		"Container": containerHeader,
+		"Name":      nameHeader,
+		"ID":        containerIDHeader,
+		"CPUPerc":   cpuPercHeader,
+		"MemUsage":  memUsage,
+		"MemPerc":   memPercHeader,
+		"NetIO":     netIOHeader,
+		"BlockIO":   blockIOHeader,
+		"PIDs":      pidsHeader,
+	}
+	containerStatsCtx.os = osType
+	return ctx.Write(&containerStatsCtx, render)
 }
 
 type containerStatsContext struct {
@@ -143,12 +160,10 @@ func (c *containerStatsContext) MarshalJSON() ([]byte, error) {
 }
 
 func (c *containerStatsContext) Container() string {
-	c.AddHeader(containerHeader)
 	return c.s.Container
 }
 
 func (c *containerStatsContext) Name() string {
-	c.AddHeader(nameHeader)
 	if len(c.s.Name) > 1 {
 		return c.s.Name[1:]
 	}
@@ -156,12 +171,10 @@ func (c *containerStatsContext) Name() string {
 }
 
 func (c *containerStatsContext) ID() string {
-	c.AddHeader(containerIDHeader)
 	return c.s.ID
 }
 
 func (c *containerStatsContext) CPUPerc() string {
-	c.AddHeader(cpuPercHeader)
 	if c.s.IsInvalid {
 		return fmt.Sprintf("--")
 	}
@@ -169,11 +182,6 @@ func (c *containerStatsContext) CPUPerc() string {
 }
 
 func (c *containerStatsContext) MemUsage() string {
-	header := memUseHeader
-	if c.os == winOSType {
-		header = winMemUseHeader
-	}
-	c.AddHeader(header)
 	if c.s.IsInvalid {
 		return fmt.Sprintf("-- / --")
 	}
@@ -184,8 +192,6 @@ func (c *containerStatsContext) MemUsage() string {
 }
 
 func (c *containerStatsContext) MemPerc() string {
-	header := memPercHeader
-	c.AddHeader(header)
 	if c.s.IsInvalid || c.os == winOSType {
 		return fmt.Sprintf("--")
 	}
@@ -193,7 +199,6 @@ func (c *containerStatsContext) MemPerc() string {
 }
 
 func (c *containerStatsContext) NetIO() string {
-	c.AddHeader(netIOHeader)
 	if c.s.IsInvalid {
 		return fmt.Sprintf("--")
 	}
@@ -201,7 +206,6 @@ func (c *containerStatsContext) NetIO() string {
 }
 
 func (c *containerStatsContext) BlockIO() string {
-	c.AddHeader(blockIOHeader)
 	if c.s.IsInvalid {
 		return fmt.Sprintf("--")
 	}
@@ -209,7 +213,6 @@ func (c *containerStatsContext) BlockIO() string {
 }
 
 func (c *containerStatsContext) PIDs() string {
-	c.AddHeader(pidsHeader)
 	if c.s.IsInvalid || c.os == winOSType {
 		return fmt.Sprintf("--")
 	}

+ 0 - 5
cli/command/formatter/stats_test.go

@@ -42,11 +42,6 @@ func TestContainerStatsContext(t *testing.T) {
 		if v := te.call(); v != te.expValue {
 			t.Fatalf("Expected %q, got %q", te.expValue, v)
 		}
-
-		h := ctx.FullHeader()
-		if h != te.expHeader {
-			t.Fatalf("Expected %q, got %q", te.expHeader, h)
-		}
 	}
 }
 

+ 25 - 15
cli/command/formatter/volume.go

@@ -45,7 +45,17 @@ func VolumeWrite(ctx Context, volumes []*types.Volume) error {
 		}
 		return nil
 	}
-	return ctx.Write(&volumeContext{}, render)
+	return ctx.Write(newVolumeContext(), render)
+}
+
+type volumeHeaderContext map[string]string
+
+func (c volumeHeaderContext) Label(name string) string {
+	n := strings.Split(name, ".")
+	r := strings.NewReplacer("-", " ", "_", " ")
+	h := r.Replace(n[len(n)-1])
+
+	return h
 }
 
 type volumeContext struct {
@@ -53,32 +63,41 @@ type volumeContext struct {
 	v types.Volume
 }
 
+func newVolumeContext() *volumeContext {
+	volumeCtx := volumeContext{}
+	volumeCtx.header = volumeHeaderContext{
+		"Name":       volumeNameHeader,
+		"Driver":     driverHeader,
+		"Scope":      scopeHeader,
+		"Mountpoint": mountpointHeader,
+		"Labels":     labelsHeader,
+		"Links":      linksHeader,
+		"Size":       sizeHeader,
+	}
+	return &volumeCtx
+}
+
 func (c *volumeContext) MarshalJSON() ([]byte, error) {
 	return marshalJSON(c)
 }
 
 func (c *volumeContext) Name() string {
-	c.AddHeader(volumeNameHeader)
 	return c.v.Name
 }
 
 func (c *volumeContext) Driver() string {
-	c.AddHeader(driverHeader)
 	return c.v.Driver
 }
 
 func (c *volumeContext) Scope() string {
-	c.AddHeader(scopeHeader)
 	return c.v.Scope
 }
 
 func (c *volumeContext) Mountpoint() string {
-	c.AddHeader(mountpointHeader)
 	return c.v.Mountpoint
 }
 
 func (c *volumeContext) Labels() string {
-	c.AddHeader(labelsHeader)
 	if c.v.Labels == nil {
 		return ""
 	}
@@ -91,13 +110,6 @@ func (c *volumeContext) Labels() string {
 }
 
 func (c *volumeContext) Label(name string) string {
-
-	n := strings.Split(name, ".")
-	r := strings.NewReplacer("-", " ", "_", " ")
-	h := r.Replace(n[len(n)-1])
-
-	c.AddHeader(h)
-
 	if c.v.Labels == nil {
 		return ""
 	}
@@ -105,7 +117,6 @@ func (c *volumeContext) Label(name string) string {
 }
 
 func (c *volumeContext) Links() string {
-	c.AddHeader(linksHeader)
 	if c.v.UsageData == nil {
 		return "N/A"
 	}
@@ -113,7 +124,6 @@ func (c *volumeContext) Links() string {
 }
 
 func (c *volumeContext) Size() string {
-	c.AddHeader(sizeHeader)
 	if c.v.UsageData == nil {
 		return "N/A"
 	}

+ 6 - 12
cli/command/formatter/volume_test.go

@@ -18,27 +18,26 @@ func TestVolumeContext(t *testing.T) {
 	cases := []struct {
 		volumeCtx volumeContext
 		expValue  string
-		expHeader string
 		call      func() string
 	}{
 		{volumeContext{
 			v: types.Volume{Name: volumeName},
-		}, volumeName, volumeNameHeader, ctx.Name},
+		}, volumeName, ctx.Name},
 		{volumeContext{
 			v: types.Volume{Driver: "driver_name"},
-		}, "driver_name", driverHeader, ctx.Driver},
+		}, "driver_name", ctx.Driver},
 		{volumeContext{
 			v: types.Volume{Scope: "local"},
-		}, "local", scopeHeader, ctx.Scope},
+		}, "local", ctx.Scope},
 		{volumeContext{
 			v: types.Volume{Mountpoint: "mountpoint"},
-		}, "mountpoint", mountpointHeader, ctx.Mountpoint},
+		}, "mountpoint", ctx.Mountpoint},
 		{volumeContext{
 			v: types.Volume{},
-		}, "", labelsHeader, ctx.Labels},
+		}, "", ctx.Labels},
 		{volumeContext{
 			v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
-		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
+		}, "label1=value1,label2=value2", ctx.Labels},
 	}
 
 	for _, c := range cases {
@@ -49,11 +48,6 @@ func TestVolumeContext(t *testing.T) {
 		} 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)
-		}
 	}
 }
 

+ 22 - 0
pkg/templates/templates.go

@@ -22,6 +22,28 @@ var basicFunctions = template.FuncMap{
 	"truncate": truncateWithLength,
 }
 
+// HeaderFunctions are used to created headers of a table.
+// This is a replacement of basicFunctions for header generation
+// because we want the header to remain intact.
+// Some functions like `split` are irrevelant so not added.
+var HeaderFunctions = template.FuncMap{
+	"json": func(v string) string {
+		return v
+	},
+	"title": func(v string) string {
+		return v
+	},
+	"lower": func(v string) string {
+		return v
+	},
+	"upper": func(v string) string {
+		return v
+	},
+	"truncate": func(v string, l int) string {
+		return v
+	},
+}
+
 // Parse creates a new anonymous template with the basic functions
 // and parses the given format.
 func Parse(format string) (*template.Template, error) {