diff --git a/cli/command/container/list.go b/cli/command/container/list.go index 844ef57519..ec1ad69e13 100644 --- a/cli/command/container/list.go +++ b/cli/command/container/list.go @@ -7,8 +7,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/opts" + "github.com/docker/cli/templates" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/cli/command/formatter/formatter.go b/cli/command/formatter/formatter.go index 3f07aee963..c63e1a4908 100644 --- a/cli/command/formatter/formatter.go +++ b/cli/command/formatter/formatter.go @@ -7,7 +7,7 @@ import ( "text/tabwriter" "text/template" - "github.com/docker/docker/pkg/templates" + "github.com/docker/cli/templates" "github.com/pkg/errors" ) diff --git a/cli/command/inspect/inspector.go b/cli/command/inspect/inspector.go index 054381f50f..b03dbed264 100644 --- a/cli/command/inspect/inspector.go +++ b/cli/command/inspect/inspector.go @@ -9,7 +9,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/cli/cli" - "github.com/docker/docker/pkg/templates" + "github.com/docker/cli/templates" "github.com/pkg/errors" ) diff --git a/cli/command/inspect/inspector_test.go b/cli/command/inspect/inspector_test.go index 721bcf84c5..7d19fceda2 100644 --- a/cli/command/inspect/inspector_test.go +++ b/cli/command/inspect/inspector_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/docker/docker/pkg/templates" + "github.com/docker/cli/templates" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/cli/command/system/events.go b/cli/command/system/events.go index ba83d26fa3..6dbc51e2bc 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -12,10 +12,10 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/opts" + "github.com/docker/cli/templates" "github.com/docker/docker/api/types" eventtypes "github.com/docker/docker/api/types/events" "github.com/docker/docker/pkg/jsonlog" - "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/cli/command/system/info.go b/cli/command/system/info.go index f2a404e905..73f1765435 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -9,9 +9,9 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/debug" + "github.com/docker/cli/templates" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/pkg/templates" "github.com/docker/go-units" "github.com/spf13/cobra" "golang.org/x/net/context" diff --git a/cli/command/system/version.go b/cli/command/system/version.go index 21c5a6df30..9cca599fe3 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -8,8 +8,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/templates" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" ) diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000000..80cab5ed34 --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,78 @@ +package templates + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" +) + +// basicFunctions are the set of initial +// functions provided to every template. +var basicFunctions = template.FuncMap{ + "json": func(v interface{}) string { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + enc.Encode(v) + // Remove the trailing new line added by the encoder + return strings.TrimSpace(buf.String()) + }, + "split": strings.Split, + "join": strings.Join, + "title": strings.Title, + "lower": strings.ToLower, + "upper": strings.ToUpper, + "pad": padWithSpace, + "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 irrelevant 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, _ 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) { + return NewParse("", format) +} + +// NewParse creates a new tagged template with the basic functions +// and parses the given format. +func NewParse(tag, format string) (*template.Template, error) { + 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) +} + +// truncateWithLength truncates the source string up to the length provided by the input +func truncateWithLength(source string, length int) string { + if len(source) < length { + return source + } + return source[:length] +} diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 0000000000..296bcb7107 --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,88 @@ +package templates + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Github #32120 +func TestParseJSONFunctions(t *testing.T) { + tm, err := Parse(`{{json .Ports}}`) + assert.NoError(t, err) + + var b bytes.Buffer + assert.NoError(t, tm.Execute(&b, map[string]string{"Ports": "0.0.0.0:2->8/udp"})) + want := "\"0.0.0.0:2->8/udp\"" + assert.Equal(t, want, b.String()) +} + +func TestParseStringFunctions(t *testing.T) { + tm, err := Parse(`{{join (split . ":") "/"}}`) + assert.NoError(t, err) + + var b bytes.Buffer + assert.NoError(t, tm.Execute(&b, "text:with:colon")) + want := "text/with/colon" + assert.Equal(t, want, b.String()) +} + +func TestNewParse(t *testing.T) { + tm, err := NewParse("foo", "this is a {{ . }}") + assert.NoError(t, err) + + var b bytes.Buffer + assert.NoError(t, tm.Execute(&b, "string")) + want := "this is a string" + assert.Equal(t, want, b.String()) +} + +func TestParseTruncateFunction(t *testing.T) { + source := "tupx5xzf6hvsrhnruz5cr8gwp" + + testCases := []struct { + template string + expected string + }{ + { + template: `{{truncate . 5}}`, + expected: "tupx5", + }, + { + template: `{{truncate . 25}}`, + expected: "tupx5xzf6hvsrhnruz5cr8gwp", + }, + { + template: `{{truncate . 30}}`, + expected: "tupx5xzf6hvsrhnruz5cr8gwp", + }, + { + template: `{{pad . 3 3}}`, + expected: " tupx5xzf6hvsrhnruz5cr8gwp ", + }, + } + + for _, testCase := range testCases { + tm, err := Parse(testCase.template) + assert.NoError(t, err) + + t.Run("Non Empty Source Test with template: "+testCase.template, func(t *testing.T) { + var b bytes.Buffer + assert.NoError(t, tm.Execute(&b, source)) + assert.Equal(t, testCase.expected, b.String()) + }) + + t.Run("Empty Source Test with template: "+testCase.template, func(t *testing.T) { + var c bytes.Buffer + assert.NoError(t, tm.Execute(&c, "")) + assert.Equal(t, "", c.String()) + }) + + t.Run("Nil Source Test with template: "+testCase.template, func(t *testing.T) { + var c bytes.Buffer + assert.Error(t, tm.Execute(&c, nil)) + assert.Equal(t, "", c.String()) + }) + } +}