浏览代码

Merge pull request #30962 from TheHipbot/30431-implement-format-for-history-with-docs

30431 implement format for history with docs
Sebastiaan van Stijn 8 年之前
父节点
当前提交
a3ab46361e
共有 4 个文件被更改,包括 381 次插入50 次删除
  1. 113 0
      cli/command/formatter/history.go
  2. 213 0
      cli/command/formatter/history_test.go
  3. 11 46
      cli/command/image/history.go
  4. 44 4
      docs/reference/commandline/history.md

+ 113 - 0
cli/command/formatter/history.go

@@ -0,0 +1,113 @@
+package formatter
+
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/docker/docker/api/types/image"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/stringutils"
+	units "github.com/docker/go-units"
+)
+
+const (
+	defaultHistoryTableFormat  = "table {{.ID}}\t{{.CreatedSince}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}"
+	nonHumanHistoryTableFormat = "table {{.ID}}\t{{.CreatedAt}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}"
+
+	historyIDHeader = "IMAGE"
+	createdByHeader = "CREATED BY"
+	commentHeader   = "COMMENT"
+)
+
+// NewHistoryFormat returns a format for rendering an HistoryContext
+func NewHistoryFormat(source string, quiet bool, human bool) Format {
+	switch source {
+	case TableFormatKey:
+		switch {
+		case quiet:
+			return defaultQuietFormat
+		case !human:
+			return nonHumanHistoryTableFormat
+		default:
+			return defaultHistoryTableFormat
+		}
+	}
+
+	return Format(source)
+}
+
+// HistoryWrite writes the context
+func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error {
+	render := func(format func(subContext subContext) error) error {
+		for _, history := range histories {
+			historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human}
+			if err := format(historyCtx); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+	historyCtx := &historyContext{}
+	historyCtx.header = map[string]string{
+		"ID":           historyIDHeader,
+		"CreatedSince": createdSinceHeader,
+		"CreatedAt":    createdAtHeader,
+		"CreatedBy":    createdByHeader,
+		"Size":         sizeHeader,
+		"Comment":      commentHeader,
+	}
+	return ctx.Write(historyCtx, render)
+}
+
+type historyContext struct {
+	HeaderContext
+	trunc bool
+	human bool
+	h     image.HistoryResponseItem
+}
+
+func (c *historyContext) MarshalJSON() ([]byte, error) {
+	return marshalJSON(c)
+}
+
+func (c *historyContext) ID() string {
+	if c.trunc {
+		return stringid.TruncateID(c.h.ID)
+	}
+	return c.h.ID
+}
+
+func (c *historyContext) CreatedAt() string {
+	var created string
+	created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
+	return created
+}
+
+func (c *historyContext) CreatedSince() string {
+	var created string
+	created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
+	return created + " ago"
+}
+
+func (c *historyContext) CreatedBy() string {
+	createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1)
+	if c.trunc {
+		createdBy = stringutils.Ellipsis(createdBy, 45)
+	}
+	return createdBy
+}
+
+func (c *historyContext) Size() string {
+	size := ""
+	if c.human {
+		size = units.HumanSizeWithPrecision(float64(c.h.Size), 3)
+	} else {
+		size = strconv.FormatInt(c.h.Size, 10)
+	}
+	return size
+}
+
+func (c *historyContext) Comment() string {
+	return c.h.Comment
+}

+ 213 - 0
cli/command/formatter/history_test.go

@@ -0,0 +1,213 @@
+package formatter
+
+import (
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+
+	"bytes"
+	"github.com/docker/docker/api/types/image"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/stringutils"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+type historyCase struct {
+	historyCtx historyContext
+	expValue   string
+	call       func() string
+}
+
+func TestHistoryContext_ID(t *testing.T) {
+	id := stringid.GenerateRandomID()
+
+	var ctx historyContext
+	cases := []historyCase{
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{ID: id},
+				trunc: false,
+			}, id, ctx.ID,
+		},
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{ID: id},
+				trunc: true,
+			}, stringid.TruncateID(id), ctx.ID,
+		},
+	}
+
+	for _, c := range cases {
+		ctx = c.historyCtx
+		v := c.call()
+		if strings.Contains(v, ",") {
+			compareMultipleValues(t, v, c.expValue)
+		} else if v != c.expValue {
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
+		}
+	}
+}
+
+func TestHistoryContext_CreatedSince(t *testing.T) {
+	unixTime := time.Now().AddDate(0, 0, -7).Unix()
+	expected := "7 days ago"
+
+	var ctx historyContext
+	cases := []historyCase{
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{Created: unixTime},
+				trunc: false,
+				human: true,
+			}, expected, ctx.CreatedSince,
+		},
+	}
+
+	for _, c := range cases {
+		ctx = c.historyCtx
+		v := c.call()
+		if strings.Contains(v, ",") {
+			compareMultipleValues(t, v, c.expValue)
+		} else if v != c.expValue {
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
+		}
+	}
+}
+
+func TestHistoryContext_CreatedBy(t *testing.T) {
+	withTabs := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80	--recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62	&& echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list  && apt-get update  && apt-get install --no-install-recommends --no-install-suggests -y       ca-certificates       nginx=${NGINX_VERSION}       nginx-module-xslt       nginx-module-geoip       nginx-module-image-filter       nginx-module-perl       nginx-module-njs       gettext-base  && rm -rf /var/lib/apt/lists/*`
+	expected := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list  && apt-get update  && apt-get install --no-install-recommends --no-install-suggests -y       ca-certificates       nginx=${NGINX_VERSION}       nginx-module-xslt       nginx-module-geoip       nginx-module-image-filter       nginx-module-perl       nginx-module-njs       gettext-base  && rm -rf /var/lib/apt/lists/*`
+
+	var ctx historyContext
+	cases := []historyCase{
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{CreatedBy: withTabs},
+				trunc: false,
+			}, expected, ctx.CreatedBy,
+		},
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{CreatedBy: withTabs},
+				trunc: true,
+			}, stringutils.Ellipsis(expected, 45), ctx.CreatedBy,
+		},
+	}
+
+	for _, c := range cases {
+		ctx = c.historyCtx
+		v := c.call()
+		if strings.Contains(v, ",") {
+			compareMultipleValues(t, v, c.expValue)
+		} else if v != c.expValue {
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
+		}
+	}
+}
+
+func TestHistoryContext_Size(t *testing.T) {
+	size := int64(182964289)
+	expected := "183MB"
+
+	var ctx historyContext
+	cases := []historyCase{
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{Size: size},
+				trunc: false,
+				human: true,
+			}, expected, ctx.Size,
+		}, {
+			historyContext{
+				h:     image.HistoryResponseItem{Size: size},
+				trunc: false,
+				human: false,
+			}, strconv.Itoa(182964289), ctx.Size,
+		},
+	}
+
+	for _, c := range cases {
+		ctx = c.historyCtx
+		v := c.call()
+		if strings.Contains(v, ",") {
+			compareMultipleValues(t, v, c.expValue)
+		} else if v != c.expValue {
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
+		}
+	}
+}
+
+func TestHistoryContext_Comment(t *testing.T) {
+	comment := "Some comment"
+
+	var ctx historyContext
+	cases := []historyCase{
+		{
+			historyContext{
+				h:     image.HistoryResponseItem{Comment: comment},
+				trunc: false,
+			}, comment, ctx.Comment,
+		},
+	}
+
+	for _, c := range cases {
+		ctx = c.historyCtx
+		v := c.call()
+		if strings.Contains(v, ",") {
+			compareMultipleValues(t, v, c.expValue)
+		} else if v != c.expValue {
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
+		}
+	}
+}
+
+func TestHistoryContext_Table(t *testing.T) {
+	out := bytes.NewBufferString("")
+	unixTime := time.Now().AddDate(0, 0, -1).Unix()
+	histories := []image.HistoryResponseItem{
+		{ID: "imageID1", Created: unixTime, CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
+		{ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
+		{ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
+		{ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
+	}
+	expectedNoTrunc := `IMAGE               CREATED             CREATED BY                                                                                                                     SIZE                COMMENT
+imageID1            24 hours ago        /bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on   183MB               Hi
+imageID2            24 hours ago        /bin/bash echo                                                                                                                 183MB               Hi
+imageID3            24 hours ago        /bin/bash ls                                                                                                                   183MB               Hi
+imageID4            24 hours ago        /bin/bash grep                                                                                                                 183MB               Hi
+`
+	expectedTrunc := `IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
+imageID1            24 hours ago        /bin/bash ls && npm i && npm run test && k...   183MB               Hi
+imageID2            24 hours ago        /bin/bash echo                                  183MB               Hi
+imageID3            24 hours ago        /bin/bash ls                                    183MB               Hi
+imageID4            24 hours ago        /bin/bash grep                                  183MB               Hi
+`
+
+	contexts := []struct {
+		context  Context
+		expected string
+	}{
+		{Context{
+			Format: NewHistoryFormat("table", false, true),
+			Trunc:  true,
+			Output: out,
+		},
+			expectedTrunc,
+		},
+		{Context{
+			Format: NewHistoryFormat("table", false, true),
+			Trunc:  false,
+			Output: out,
+		},
+			expectedNoTrunc,
+		},
+	}
+
+	for _, context := range contexts {
+		HistoryWrite(context.context, true, histories)
+		assert.Equal(t, out.String(), context.expected)
+		// Clean buffer
+		out.Reset()
+	}
+}

+ 11 - 46
cli/command/image/history.go

@@ -1,19 +1,11 @@
 package image
 
 import (
-	"fmt"
-	"strconv"
-	"strings"
-	"text/tabwriter"
-	"time"
-
 	"golang.org/x/net/context"
 
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
-	"github.com/docker/docker/pkg/stringid"
-	"github.com/docker/docker/pkg/stringutils"
-	"github.com/docker/go-units"
+	"github.com/docker/docker/cli/command/formatter"
 	"github.com/spf13/cobra"
 )
 
@@ -23,6 +15,7 @@ type historyOptions struct {
 	human   bool
 	quiet   bool
 	noTrunc bool
+	format  string
 }
 
 // NewHistoryCommand creates a new `docker history` command
@@ -44,6 +37,7 @@ func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command {
 	flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format")
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs")
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
+	flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
 
 	return cmd
 }
@@ -56,44 +50,15 @@ func runHistory(dockerCli *command.DockerCli, opts historyOptions) error {
 		return err
 	}
 
-	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
-
-	if opts.quiet {
-		for _, entry := range history {
-			if opts.noTrunc {
-				fmt.Fprintf(w, "%s\n", entry.ID)
-			} else {
-				fmt.Fprintf(w, "%s\n", stringid.TruncateID(entry.ID))
-			}
-		}
-		w.Flush()
-		return nil
+	format := opts.format
+	if len(format) == 0 {
+		format = formatter.TableFormatKey
 	}
 
-	var imageID string
-	var createdBy string
-	var created string
-	var size string
-
-	fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT")
-	for _, entry := range history {
-		imageID = entry.ID
-		createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1)
-		if !opts.noTrunc {
-			createdBy = stringutils.Ellipsis(createdBy, 45)
-			imageID = stringid.TruncateID(entry.ID)
-		}
-
-		if opts.human {
-			created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago"
-			size = units.HumanSizeWithPrecision(float64(entry.Size), 3)
-		} else {
-			created = time.Unix(entry.Created, 0).Format(time.RFC3339)
-			size = strconv.FormatInt(entry.Size, 10)
-		}
-
-		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", imageID, created, createdBy, size, entry.Comment)
+	historyCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human),
+		Trunc:  !opts.noTrunc,
 	}
-	w.Flush()
-	return nil
+	return formatter.HistoryWrite(historyCtx, opts.human, history)
 }

+ 44 - 4
docs/reference/commandline/history.md

@@ -21,10 +21,11 @@ Usage:  docker history [OPTIONS] IMAGE
 Show the history of an image
 
 Options:
-      --help       Print usage
-  -H, --human      Print sizes and dates in human readable format (default true)
-      --no-trunc   Don't truncate output
-  -q, --quiet      Only show numeric IDs
+      --format string   Pretty-print images using a Go template
+      --help            Print usage
+  -H, --human           Print sizes and dates in human readable format (default true)
+      --no-trunc        Don't truncate output
+  -q, --quiet           Only show numeric IDs
 ```
 
 
@@ -54,3 +55,42 @@ IMAGE               CREATED             CREATED BY
 c69cab00d6ef        5 months ago        /bin/sh -c #(nop) MAINTAINER Lokesh Mandvekar   0 B
 511136ea3c5a        19 months ago                                                       0 B                 Imported from -
 ```
+
+### Format the output
+
+The formatting option (`--format`) will pretty print history output
+using a Go template.
+
+Valid placeholders for the Go template are listed below:
+
+| Placeholder | Description|
+| ---- | ---- |
+| `.ID` | Image ID |
+| `.CreatedSince` | Elapsed time since the image was created if --human=true, otherwise timestamp of when image was created |
+| `.CreatedAt` | Timestamp of when image was created |
+| `.CreatedBy` | Command that was used to create the image |
+| `.Size` | Image disk size |
+| `.Comment` | Comment for image |
+
+When using the `--format` option, the `history` command will either
+output the data exactly as the template declares or, when using the
+`table` directive, will include column headers as well.
+
+The following example uses a template without headers and outputs the
+`ID` and `CreatedSince` entries separated by a colon for all images:
+
+```bash
+{% raw %}
+$ docker images --format "{{.ID}}: {{.Created}} ago"
+
+cc1b61406712: 2 weeks ago
+<missing>: 2 weeks ago
+<missing>: 2 weeks ago
+<missing>: 2 weeks ago
+<missing>: 2 weeks ago
+<missing>: 3 weeks ago
+<missing>: 3 weeks ago
+<missing>: 3 weeks ago
+
+{% endraw %}
+```