diff --git a/api/client/formatter/formatter.go b/api/client/formatter/formatter.go index 749148ac82..bc3d50c948 100644 --- a/api/client/formatter/formatter.go +++ b/api/client/formatter/formatter.go @@ -9,6 +9,7 @@ import ( "text/template" "github.com/docker/docker/reference" + "github.com/docker/docker/utils/templates" "github.com/docker/engine-api/types" ) @@ -54,7 +55,7 @@ func (c *Context) preformat() { } func (c *Context) parseFormat() (*template.Template, error) { - tmpl, err := template.New("").Parse(c.finalFormat) + tmpl, err := templates.Parse(c.finalFormat) if err != nil { c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err)) c.buffer.WriteTo(c.Output) diff --git a/api/client/inspect.go b/api/client/inspect.go index 5401d3bc3d..72092a830c 100644 --- a/api/client/inspect.go +++ b/api/client/inspect.go @@ -1,23 +1,15 @@ package client import ( - "encoding/json" "fmt" - "text/template" "github.com/docker/docker/api/client/inspect" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils/templates" "github.com/docker/engine-api/client" ) -var funcMap = template.FuncMap{ - "json": func(v interface{}) string { - a, _ := json.Marshal(v) - return string(a) - }, -} - // CmdInspect displays low-level information on one or more containers or images. // // Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...] @@ -123,7 +115,7 @@ func (cli *DockerCli) inspectErrorStatus(err error) (status int) { func (cli *DockerCli) newInspectorWithTemplate(tmplStr string) (inspect.Inspector, error) { elementInspector := inspect.NewIndentedInspector(cli.out) if tmplStr != "" { - tmpl, err := template.New("").Funcs(funcMap).Parse(tmplStr) + tmpl, err := templates.Parse(tmplStr) if err != nil { return nil, fmt.Errorf("Template parsing error: %s", err) } diff --git a/api/client/inspect/inspector_test.go b/api/client/inspect/inspector_test.go index 80cb297551..1ce1593ab7 100644 --- a/api/client/inspect/inspector_test.go +++ b/api/client/inspect/inspector_test.go @@ -4,7 +4,8 @@ import ( "bytes" "strings" "testing" - "text/template" + + "github.com/docker/docker/utils/templates" ) type testElement struct { @@ -13,7 +14,7 @@ type testElement struct { func TestTemplateInspectorDefault(t *testing.T) { b := new(bytes.Buffer) - tmpl, err := template.New("test").Parse("{{.DNS}}") + tmpl, err := templates.Parse("{{.DNS}}") if err != nil { t.Fatal(err) } @@ -32,7 +33,7 @@ func TestTemplateInspectorDefault(t *testing.T) { func TestTemplateInspectorEmpty(t *testing.T) { b := new(bytes.Buffer) - tmpl, err := template.New("test").Parse("{{.DNS}}") + tmpl, err := templates.Parse("{{.DNS}}") if err != nil { t.Fatal(err) } @@ -48,7 +49,7 @@ func TestTemplateInspectorEmpty(t *testing.T) { func TestTemplateInspectorTemplateError(t *testing.T) { b := new(bytes.Buffer) - tmpl, err := template.New("test").Parse("{{.Foo}}") + tmpl, err := templates.Parse("{{.Foo}}") if err != nil { t.Fatal(err) } @@ -66,7 +67,7 @@ func TestTemplateInspectorTemplateError(t *testing.T) { func TestTemplateInspectorRawFallback(t *testing.T) { b := new(bytes.Buffer) - tmpl, err := template.New("test").Parse("{{.Dns}}") + tmpl, err := templates.Parse("{{.Dns}}") if err != nil { t.Fatal(err) } @@ -85,7 +86,7 @@ func TestTemplateInspectorRawFallback(t *testing.T) { func TestTemplateInspectorRawFallbackError(t *testing.T) { b := new(bytes.Buffer) - tmpl, err := template.New("test").Parse("{{.Dns}}") + tmpl, err := templates.Parse("{{.Dns}}") if err != nil { t.Fatal(err) } @@ -102,7 +103,7 @@ func TestTemplateInspectorRawFallbackError(t *testing.T) { func TestTemplateInspectorMultiple(t *testing.T) { b := new(bytes.Buffer) - tmpl, err := template.New("test").Parse("{{.DNS}}") + tmpl, err := templates.Parse("{{.DNS}}") if err != nil { t.Fatal(err) } diff --git a/api/client/version.go b/api/client/version.go index a64deef69a..1eb8ac2871 100644 --- a/api/client/version.go +++ b/api/client/version.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/dockerversion" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/utils" + "github.com/docker/docker/utils/templates" "github.com/docker/engine-api/types" ) @@ -48,7 +49,7 @@ func (cli *DockerCli) CmdVersion(args ...string) (err error) { } var tmpl *template.Template - if tmpl, err = template.New("").Funcs(funcMap).Parse(templateFormat); err != nil { + if tmpl, err = templates.Parse(templateFormat); err != nil { return Cli.StatusError{StatusCode: 64, Status: "Template parsing error: " + err.Error()} } diff --git a/daemon/logger/loggerutils/log_tag.go b/daemon/logger/loggerutils/log_tag.go index df24379acf..6653b9c431 100644 --- a/daemon/logger/loggerutils/log_tag.go +++ b/daemon/logger/loggerutils/log_tag.go @@ -3,10 +3,10 @@ package loggerutils import ( "bytes" "fmt" - "text/template" "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/logger" + "github.com/docker/docker/utils/templates" ) // ParseLogTag generates a context aware tag for consistency across different @@ -14,7 +14,7 @@ import ( func ParseLogTag(ctx logger.Context, defaultTemplate string) (string, error) { tagTemplate := lookupTagTemplate(ctx, defaultTemplate) - tmpl, err := template.New("log-tag").Parse(tagTemplate) + tmpl, err := templates.NewParse("log-tag", tagTemplate) if err != nil { return "", err } diff --git a/docs/admin/formatting.md b/docs/admin/formatting.md new file mode 100644 index 0000000000..d7c764d715 --- /dev/null +++ b/docs/admin/formatting.md @@ -0,0 +1,66 @@ + + +# Formatting reference + +Docker uses [Go templates](https://golang.org/pkg/text/template/) to allow users manipulate the output format +of certain commands and log drivers. Each command a driver provides a detailed +list of elements they support in their templates: + +- [Docker Images formatting](https://docs.docker.com/engine/reference/commandline/images/#formatting) +- [Docker Inspect formatting](https://docs.docker.com/engine/reference/commandline/inspect/#examples) +- [Docker Log Tag formatting](https://docs.docker.com/engine/admin/logging/log_tags/) +- [Docker Network Inspect formatting](https://docs.docker.com/engine/reference/commandline/network_inspect/) +- [Docker PS formatting](https://docs.docker.com/engine/reference/commandline/ps/#formatting) +- [Docker Volume Inspect formatting](https://docs.docker.com/engine/reference/commandline/volume_inspect/) +- [Docker Version formatting](https://docs.docker.com/engine/reference/commandline/version/#examples) + +## Template functions + +Docker provides a set of basic functions to manipulate template elements. +This is the complete list of the available functions with examples: + +### Join + +Join concatenates a list of strings to create a single string. +It puts a separator between each element in the list. + + $ docker ps --format '{{join .Names " or "}}' + +### Json + +Json encodes an element as a json string. + + $ docker inspect --format '{{json .Mounts}}' container + +### Lower + +Lower turns a string into its lower case representation. + + $ docker inspect --format "{{lower .Name}}" container + +### Split + +Split slices a string into a list of strings separated by a separator. + + # docker inspect --format '{{split (join .Names "/") "/"}}' container + +### Title + +Title capitalizes a string. + + $ docker inspect --format "{{title .Name}}" container + +### Upper + +Upper turms a string into its upper case representation. + + $ docker inspect --format "{{upper .Name}}" container diff --git a/profiles/apparmor/apparmor.go b/profiles/apparmor/apparmor.go index ab139a860a..72ed7f0583 100644 --- a/profiles/apparmor/apparmor.go +++ b/profiles/apparmor/apparmor.go @@ -8,9 +8,9 @@ import ( "os" "path" "strings" - "text/template" "github.com/docker/docker/pkg/aaparser" + "github.com/docker/docker/utils/templates" ) var ( @@ -36,7 +36,7 @@ type profileData struct { // generateDefault creates an apparmor profile from ProfileData. func (p *profileData) generateDefault(out io.Writer) error { - compiled, err := template.New("apparmor_profile").Parse(baseTemplate) + compiled, err := templates.NewParse("apparmor_profile", baseTemplate) if err != nil { return err } diff --git a/utils/templates/templates.go b/utils/templates/templates.go new file mode 100644 index 0000000000..749da3d5af --- /dev/null +++ b/utils/templates/templates.go @@ -0,0 +1,33 @@ +package templates + +import ( + "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 { + a, _ := json.Marshal(v) + return string(a) + }, + "split": strings.Split, + "join": strings.Join, + "title": strings.Title, + "lower": strings.ToLower, + "upper": strings.ToUpper, +} + +// Parse creates a new annonymous 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) +} diff --git a/utils/templates/templates_test.go b/utils/templates/templates_test.go new file mode 100644 index 0000000000..dd42901aed --- /dev/null +++ b/utils/templates/templates_test.go @@ -0,0 +1,38 @@ +package templates + +import ( + "bytes" + "testing" +) + +func TestParseStringFunctions(t *testing.T) { + tm, err := Parse(`{{join (split . ":") "/"}}`) + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + if err := tm.Execute(&b, "text:with:colon"); err != nil { + t.Fatal(err) + } + want := "text/with/colon" + if b.String() != want { + t.Fatalf("expected %s, got %s", want, b.String()) + } +} + +func TestNewParse(t *testing.T) { + tm, err := NewParse("foo", "this is a {{ . }}") + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + if err := tm.Execute(&b, "string"); err != nil { + t.Fatal(err) + } + want := "this is a string" + if b.String() != want { + t.Fatalf("expected %s, got %s", want, b.String()) + } +}