package formatter import ( "bytes" "fmt" "io" "strings" "text/tabwriter" "text/template" "github.com/docker/docker/reference" "github.com/docker/engine-api/types" ) const ( tableFormatKey = "table" rawFormatKey = "raw" defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" 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}}" defaultQuietFormat = "{{.ID}}" ) // Context contains information required by the formatter to print the output as desired. type Context struct { // Output is the output stream to which the formatted string is written. Output io.Writer // 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 // Trunc when set to true will truncate the output of certain fields such as Container ID. Trunc bool // internal element table bool finalFormat string header string buffer *bytes.Buffer } func (c *Context) preformat() { c.finalFormat = c.Format if strings.HasPrefix(c.Format, tableKey) { c.table = true c.finalFormat = c.finalFormat[len(tableKey):] } c.finalFormat = strings.Trim(c.finalFormat, " ") r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") c.finalFormat = r.Replace(c.finalFormat) } func (c *Context) parseFormat() (*template.Template, error) { tmpl, err := template.New("").Parse(c.finalFormat) if err != nil { c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err)) c.buffer.WriteTo(c.Output) } return tmpl, err } func (c *Context) postformat(tmpl *template.Template, subContext subContext) { if c.table { 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)) t.Write([]byte("\n")) c.buffer.WriteTo(t) t.Flush() } else { c.buffer.WriteTo(c.Output) } } func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { 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 } if c.table && len(c.header) == 0 { c.header = subContext.fullHeader() } c.buffer.WriteString("\n") return nil } // 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 } // ImageContext contains image specific information required by the formater, encapsulate a Context struct. type ImageContext struct { Context Digest bool // Images Images []types.Image } func (ctx ContainerContext) Write() { switch ctx.Format { case tableFormatKey: ctx.Format = defaultContainerTableFormat if ctx.Quiet { ctx.Format = defaultQuietFormat } case rawFormatKey: if ctx.Quiet { ctx.Format = `container_id: {{.ID}}` } else { ctx.Format = `container_id: {{.ID}} image: {{.Image}} command: {{.Command}} created_at: {{.CreatedAt}} status: {{.Status}} names: {{.Names}} labels: {{.Labels}} ports: {{.Ports}} ` if ctx.Size { ctx.Format += `size: {{.Size}} ` } } } ctx.buffer = bytes.NewBufferString("") ctx.preformat() if ctx.table && ctx.Size { ctx.finalFormat += "\t{{.Size}}" } tmpl, err := ctx.parseFormat() if err != nil { return } for _, container := range ctx.Containers { containerCtx := &containerContext{ trunc: ctx.Trunc, c: container, } err = ctx.contextFormat(tmpl, containerCtx) if err != nil { return } } ctx.postformat(tmpl, &containerContext{}) } func (ctx ImageContext) Write() { switch ctx.Format { case tableFormatKey: ctx.Format = defaultImageTableFormat if ctx.Digest { ctx.Format = defaultImageTableFormatWithDigest } if ctx.Quiet { ctx.Format = defaultQuietFormat } case rawFormatKey: if ctx.Quiet { ctx.Format = `image_id: {{.ID}}` } else { if ctx.Digest { ctx.Format = `repository: {{ .Repository }} tag: {{.Tag}} digest: {{.Digest}} image_id: {{.ID}} created_at: {{.CreatedAt}} virtual_size: {{.Size}} ` } else { ctx.Format = `repository: {{ .Repository }} tag: {{.Tag}} image_id: {{.ID}} created_at: {{.CreatedAt}} virtual_size: {{.Size}} ` } } } ctx.buffer = bytes.NewBufferString("") ctx.preformat() if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") { ctx.finalFormat += "\t{{.Digest}}" } tmpl, err := ctx.parseFormat() if err != nil { return } for _, image := range ctx.Images { repoTags := image.RepoTags repoDigests := image.RepoDigests if len(repoTags) == 1 && repoTags[0] == ":" && len(repoDigests) == 1 && repoDigests[0] == "@" { // dangling image - clear out either repoTags or repoDigests so we only show it once below repoDigests = []string{} } // combine the tags and digests lists tagsAndDigests := append(repoTags, repoDigests...) for _, repoAndRef := range tagsAndDigests { repo := "" tag := "" digest := "" if !strings.HasPrefix(repoAndRef, "") { ref, err := reference.ParseNamed(repoAndRef) if err != nil { continue } repo = ref.Name() switch x := ref.(type) { case reference.Canonical: digest = x.Digest().String() case reference.NamedTagged: tag = x.Tag() } } imageCtx := &imageContext{ trunc: ctx.Trunc, i: image, repo: repo, tag: tag, digest: digest, } err = ctx.contextFormat(tmpl, imageCtx) if err != nil { return } } } ctx.postformat(tmpl, &imageContext{}) }