Skip to content

Commit

Permalink
Refactor report package to be more compatible
Browse files Browse the repository at this point in the history
* Add additional replacer for ('\', 'n') -> "\n"
* New type Formatter embeds Template and writer/tabwriter handling
* tabwriter.Init() is exposed to allow updating the tabwriter settings

Note: If template origin is OriginPodman or has "table" keyword prefix
output will be filtered through tabwriter. Otherwise, output will be
rendered using given writer.

Note: Once all podman commands have been updated a follow on PR will
remove the old report.Template and report.Writer code.

See containers/podman#10974

Signed-off-by: Jhon Honce <[email protected]>
  • Loading branch information
jwhonce committed Nov 18, 2021
1 parent 72166a4 commit 8a03111
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 18 deletions.
46 changes: 32 additions & 14 deletions pkg/report/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,66 @@ Package report provides helper structs/methods/funcs for formatting output
To format output for an array of structs:
w := report.NewWriterDefault(os.Stdout)
defer w.Flush()
ExamplePodman:
headers := report.Headers(struct {
ID string
}{}, nil)
t, _ := report.NewTemplate("command name").Parse("{{range .}}{{.ID}}{{end}}")
t.Execute(t, headers)
t.Execute(t, map[string]string{
f := report.New(os.Stdout, "Command Name")
f, _ := f.Parse(report.OriginPodman, "{{range .}}{{.ID}}{{end}}")
defer f.Flush()
if f.RenderHeaders {
f.Execute(headers)
}
f.Execute( map[string]string{
"ID":"fa85da03b40141899f3af3de6d27852b",
})
// t.IsTable() == false
or
w := report.NewWriterDefault(os.Stdout)
defer w.Flush()
// Output:
// ID
// fa85da03b40141899f3af3de6d27852b
ExampleUser:
headers := report.Headers(struct {
CID string
}{}, map[string]string{
"CID":"ID"})
t, _ := report.NewTemplate("command name").Parse("table {{.CID}}")
t.Execute(t, headers)
f, _ := report.New(os.Stdout, "Command Name").Parse(report.OriginUser, "table {{.CID}}")
defer f.Flush()
if f.RenderHeaders {
t.Execute(t, headers)
}
t.Execute(t,map[string]string{
"CID":"fa85da03b40141899f3af3de6d27852b",
})
// t.IsTable() == true
// Output:
// ID
// fa85da03b40141899f3af3de6d27852b
Helpers:
if report.IsJSON(cmd.Flag("format").Value.String()) {
... process JSON and output
}
if report.HasTable(cmd.Flag("format").Value.String()) {
... "table" keyword prefix in format text
}
Template Functions:
The following template functions are added to the template when parsed:
- join strings.Join, {{join .Field separator}}
- json encode field as JSON {{ json .Field }}
- lower strings.ToLower {{ .Field | lower }}
- pad add spaces as prefix and suffix {{ pad . 2 2 }}
- split strings.Split {{ .Field | split }}
- title strings.Title {{ .Field | title }}
- truncate limit field length {{ truncate . 10 }}
- upper strings.ToUpper {{ .Field | upper }}
report.Funcs() may be used to add additional template functions.
Expand Down
133 changes: 133 additions & 0 deletions pkg/report/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package report

import (
"io"
"strings"
"text/tabwriter"
"text/template"
)

// Flusher is the interface that wraps the Flush method.
type Flusher interface {
Flush() error
}

// NopFlusher represents a type which flush operation is nop.
type NopFlusher struct{}

// Flush is a nop operation.
func (f *NopFlusher) Flush() (err error) { return }

type Origin int

const (
OriginUnknown Origin = iota
OriginPodman
OriginUser
)

func (o Origin) String() string {
switch o {
case OriginPodman:
return "OriginPodman"
case OriginUser:
return "OriginUser"
default:
return "OriginUnknown"
}
}

// Formatter holds the configured Writer and parsed Template, additional state fields are
// maintained to assist in the podman command report writing.
type Formatter struct {
io.Writer // Destination for formatted output
Flusher // Flush any buffered formatted output
*template.Template // Go text/template for formatting output
RenderTable bool // Does template have "table" keyword
RenderHeaders bool // default behavior for given template is to include headers
Origin Origin // Source of go template. OriginUser or OriginPodman
text string
}

// Parse parses golang template returning a formatter
//
// - OriginPodman implies text is a template from podman code. Output will
// be filtered through a tabwriter.
//
// - OriginUser implies text is a template from a user. If template includes
// keyword "table" output will be filtered through a tabwriter.
func (f *Formatter) Parse(origin Origin, text string) (*Formatter, error) {
f.Origin = origin

if strings.HasPrefix(text, "table ") {
f.RenderTable = true
text = "{{range .}}" + NormalizeFormat(text) + "{{end -}}"
} else {
text = NormalizeFormat(text)
}
f.text = text

if f.RenderTable || origin == OriginPodman {
tw := tabwriter.NewWriter(f.Writer, 12, 2, 2, ' ', tabwriter.StripEscape)
f.Writer = tw
f.Flusher = tw
f.RenderHeaders = true
}

tmpl, err := f.Template.Funcs(template.FuncMap(DefaultFuncs)).Parse(text)
if err != nil {
return f, err
}
f.Template = tmpl
return f, nil
}

// Funcs adds the elements of the argument map to the template's function map.
// A default template function will be replaced if there is a key collision.
func (f *Formatter) Funcs(funcMap template.FuncMap) *Formatter {
m := make(template.FuncMap, len(DefaultFuncs)+len(funcMap))
for k, v := range DefaultFuncs {
m[k] = v
}
for k, v := range funcMap {
m[k] = v
}
f.Template = f.Template.Funcs(funcMap)
return f
}

// Init either resets the given tabwriter with new values or wraps w in tabwriter with given values
func (f *Formatter) Init(w io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Formatter {
flags |= tabwriter.StripEscape

if tw, ok := f.Writer.(*tabwriter.Writer); ok {
tw = tw.Init(w, minwidth, tabwidth, padding, padchar, flags)
f.Writer = tw
f.Flusher = tw
} else {
tw = tabwriter.NewWriter(w, minwidth, tabwidth, padding, padchar, flags)
f.Writer = tw
f.Flusher = tw
}
return f
}

// Execute applies a parsed template to the specified data object,
// and writes the output to f.Writer.
func (f *Formatter) Execute(data interface{}) error {
return f.Template.Execute(f.Writer, data)
}

// New allocates a new, undefined Formatter with the given name and Writer
func New(output io.Writer, name string) *Formatter {
f := new(Formatter)

f.Flusher = new(NopFlusher)
if flusher, ok := output.(Flusher); ok {
f.Flusher = flusher
}

f.Template = template.New(name)
f.Writer = output
return f
}
Loading

0 comments on commit 8a03111

Please sign in to comment.