Skip to content

Commit

Permalink
Restore "table" --format from V1
Browse files Browse the repository at this point in the history
* --format "table {{.field..." will print fields out in a table with
  headings.  Table keyword is removed, spaces between fields are
  converted to tabs
* Update parse.MatchesJSONFormat()'s regex to be more inclusive
* Add report.Headers(), obtain all the field names to be used as
  column headers, a map of field name to column headers may be provided
  to override the field names
* Update several commands to use new functions

Signed-off-by: Jhon Honce <[email protected]>
  • Loading branch information
jwhonce authored and mheon committed Oct 14, 2020
1 parent b5f7ed1 commit f8aac65
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 57 deletions.
9 changes: 5 additions & 4 deletions cmd/podman/containers/diff.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package containers

import (
"github.com/containers/podman/v2/cmd/podman/parse"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/report"
"github.com/containers/podman/v2/cmd/podman/validate"
Expand Down Expand Up @@ -52,11 +53,11 @@ func diff(cmd *cobra.Command, args []string) error {
return err
}

switch diffOpts.Format {
case "":
return report.ChangesToTable(results)
case "json":
switch {
case parse.MatchesJSONFormat(diffOpts.Format):
return report.ChangesToJSON(results)
case diffOpts.Format == "":
return report.ChangesToTable(results)
default:
return errors.New("only supported value for '--format' is 'json'")
}
Expand Down
74 changes: 35 additions & 39 deletions cmd/podman/images/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"unicode"

"github.com/containers/image/v5/docker/reference"
"github.com/containers/podman/v2/cmd/podman/parse"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/report"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/docker/go-units"
"github.com/pkg/errors"
Expand Down Expand Up @@ -106,9 +108,12 @@ func images(cmd *cobra.Command, args []string) error {
switch {
case listFlag.quiet:
return writeID(imgs)
case cmd.Flag("format").Changed && listFlag.format == "json":
case parse.MatchesJSONFormat(listFlag.format):
return writeJSON(imgs)
default:
if cmd.Flag("format").Changed {
listFlag.noHeading = true // V1 compatibility
}
return writeTemplate(imgs)
}
}
Expand Down Expand Up @@ -156,25 +161,29 @@ func writeJSON(images []imageReporter) error {
}

func writeTemplate(imgs []imageReporter) error {
var (
hdr, row string
)
if len(listFlag.format) < 1 {
hdr, row = imageListFormat(listFlag)
hdrs := report.Headers(imageReporter{}, map[string]string{
"ID": "IMAGE ID",
"ReadOnly": "R/O",
})

var row string
if listFlag.format == "" {
row = lsFormatFromFlags(listFlag)
} else {
row = listFlag.format
if !strings.HasSuffix(row, "\n") {
row += "\n"
}
row = report.NormalizeFormat(listFlag.format)
}
format := hdr + "{{range . }}" + row + "{{end}}"
tmpl, err := template.New("list").Parse(format)
if err != nil {
return err
}
tmpl = template.Must(tmpl, nil)

format := "{{range . }}" + row + "{{end}}"
tmpl := template.Must(template.New("list").Parse(format))
w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0)
defer w.Flush()

if !listFlag.noHeading {
if err := tmpl.Execute(w, hdrs); err != nil {
return err
}
}

return tmpl.Execute(w, imgs)
}

Expand Down Expand Up @@ -276,40 +285,27 @@ func sortFunc(key string, data []imageReporter) func(i, j int) bool {
}
}

func imageListFormat(flags listFlagType) (string, string) {
// Defaults
hdr := "REPOSITORY\tTAG"
row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}"
func lsFormatFromFlags(flags listFlagType) string {
row := []string{
"{{if .Repository}}{{.Repository}}{{else}}<none>{{end}}",
"{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}",
}

if flags.digests {
hdr += "\tDIGEST"
row += "\t{{.Digest}}"
row = append(row, "{{.Digest}}")
}

hdr += "\tIMAGE ID"
row += "\t{{.ID}}"

hdr += "\tCREATED\tSIZE"
row += "\t{{.Created}}\t{{.Size}}"
row = append(row, "{{.ID}}", "{{.Created}}", "{{.Size}}")

if flags.history {
hdr += "\tHISTORY"
row += "\t{{if .History}}{{.History}}{{else}}<none>{{end}}"
row = append(row, "{{if .History}}{{.History}}{{else}}<none>{{end}}")
}

if flags.readOnly {
hdr += "\tReadOnly"
row += "\t{{.ReadOnly}}"
}

if flags.noHeading {
hdr = ""
} else {
hdr += "\n"
row = append(row, "{{.ReadOnly}}")
}

row += "\n"
return hdr, row
return strings.Join(row, "\t") + "\n"
}

type imageReporter struct {
Expand Down
3 changes: 2 additions & 1 deletion cmd/podman/parse/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package parse

import "regexp"

var jsonFormatRegex = regexp.MustCompile(`^(\s*json\s*|\s*{{\s*json\s*\.\s*}}\s*)$`)
var jsonFormatRegex = regexp.MustCompile(`^\s*(json|{{\s*json\s*( \.)?\s*}})\s*$`)

// MatchesJSONFormat test CLI --format string to be a JSON request
func MatchesJSONFormat(s string) bool {
return jsonFormatRegex.Match([]byte(s))
}
25 changes: 20 additions & 5 deletions cmd/podman/parse/json_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package parse

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -13,18 +15,31 @@ func TestMatchesJSONFormat(t *testing.T) {
}{
{"json", true},
{" json", true},
{"json ", true},
{" json ", true},
{" json ", true},
{"{{json}}", true},
{"{{json }}", true},
{"{{json .}}", true},
{"{{ json .}}", true},
{"{{json . }}", true},
{" {{ json . }} ", true},
{"{{json }}", false},
{"{{json .", false},
{"{{ json . }}", true},
{" {{ json . }} ", true},
{"{{ json .", false},
{"json . }}", false},
{"{{.ID }} json .", false},
{"json .", false},
{"{{json.}}", false},
}

for _, tt := range tests {
assert.Equal(t, tt.expected, MatchesJSONFormat(tt.input))
}

for _, tc := range tests {
tc := tc
label := "MatchesJSONFormat/" + strings.ReplaceAll(tc.input, " ", "_")
t.Run(label, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.expected, MatchesJSONFormat(tc.input), fmt.Sprintf("Scanning %q failed", tc.input))
})
}
}
68 changes: 68 additions & 0 deletions cmd/podman/report/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package report

import (
"reflect"
"strings"
)

// tableReplacer will remove 'table ' prefix and clean up tabs
var tableReplacer = strings.NewReplacer(
"table ", "",
`\t`, "\t",
`\n`, "\n",
" ", "\t",
)

// escapedReplacer will clean up escaped characters from CLI
var escapedReplacer = strings.NewReplacer(
`\t`, "\t",
`\n`, "\n",
)

// NormalizeFormat reads given go template format provided by CLI and munges it into what we need
func NormalizeFormat(format string) string {
f := format
// two replacers used so we only remove the prefix keyword `table`
if strings.HasPrefix(f, "table ") {
f = tableReplacer.Replace(f)
} else {
f = escapedReplacer.Replace(format)
}

if !strings.HasSuffix(f, "\n") {
f += "\n"
}

return f
}

// Headers queries the interface for field names
func Headers(object interface{}, overrides map[string]string) []map[string]string {
value := reflect.ValueOf(object)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}

// Column header will be field name upper-cased.
headers := make(map[string]string, value.NumField())
for i := 0; i < value.Type().NumField(); i++ {
field := value.Type().Field(i)
// Recurse to find field names from promoted structs
if field.Type.Kind() == reflect.Struct && field.Anonymous {
h := Headers(reflect.New(field.Type).Interface(), nil)
for k, v := range h[0] {
headers[k] = v
}
continue
}
headers[field.Name] = strings.ToUpper(field.Name)
}

if len(overrides) > 0 {
// Override column header as provided
for k, v := range overrides {
headers[k] = strings.ToUpper(v)
}
}
return []map[string]string{headers}
}
35 changes: 35 additions & 0 deletions cmd/podman/report/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package report

import (
"strings"
"testing"
)

func TestNormalizeFormat(t *testing.T) {
cases := []struct {
format string
expected string
}{
{"table {{.ID}}", "{{.ID}}\n"},
{"table {{.ID}} {{.C}}", "{{.ID}}\t{{.C}}\n"},
{"{{.ID}}", "{{.ID}}\n"},
{"{{.ID}}\n", "{{.ID}}\n"},
{"{{.ID}} {{.C}}", "{{.ID}} {{.C}}\n"},
{"\t{{.ID}}", "\t{{.ID}}\n"},
{`\t` + "{{.ID}}", "\t{{.ID}}\n"},
{"table {{.ID}}\t{{.C}}", "{{.ID}}\t{{.C}}\n"},
{"{{.ID}} table {{.C}}", "{{.ID}} table {{.C}}\n"},
}
for _, tc := range cases {
tc := tc

label := strings.ReplaceAll(tc.format, " ", "<sp>")
t.Run("NormalizeFormat/"+label, func(t *testing.T) {
t.Parallel()
actual := NormalizeFormat(tc.format)
if actual != tc.expected {
t.Errorf("Expected %q, actual %q", tc.expected, actual)
}
})
}
}
10 changes: 6 additions & 4 deletions test/e2e/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,17 @@ var _ = Describe("Podman Info", func() {
{"{{ json .}}", true, 0},
{"{{json . }}", true, 0},
{" {{ json . }} ", true, 0},
{"{{json }}", false, 125},
{"{{json }}", true, 0},
{"{{json .", false, 125},
{"json . }}", false, 0}, // Note: this does NOT fail but produces garbage
{"json . }}", false, 0}, // without opening {{ template seen as string literal
}
for _, tt := range tests {
session := podmanTest.Podman([]string{"info", "--format", tt.input})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(tt.exitCode))
Expect(session.IsJSONOutputValid()).To(Equal(tt.success))

desc := fmt.Sprintf("JSON test(%q)", tt.input)
Expect(session).Should(Exit(tt.exitCode), desc)
Expect(session.IsJSONOutputValid()).To(Equal(tt.success), desc)
}
})

Expand Down
11 changes: 7 additions & 4 deletions test/e2e/version_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package integration

import (
"fmt"
"os"

. "github.com/containers/podman/v2/test/utils"
Expand Down Expand Up @@ -68,15 +69,17 @@ var _ = Describe("Podman version", func() {
{"{{ json .}}", true, 0},
{"{{json . }}", true, 0},
{" {{ json . }} ", true, 0},
{"{{json }}", false, 125},
{"{{json }}", true, 0},
{"{{json .", false, 125},
{"json . }}", false, 0}, // Note: this does NOT fail but produces garbage
{"json . }}", false, 0}, // without opening {{ template seen as string literal
}
for _, tt := range tests {
session := podmanTest.Podman([]string{"version", "--format", tt.input})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(tt.exitCode))
Expect(session.IsJSONOutputValid()).To(Equal(tt.success))

desc := fmt.Sprintf("JSON test(%q)", tt.input)
Expect(session).Should(Exit(tt.exitCode), desc)
Expect(session.IsJSONOutputValid()).To(Equal(tt.success), desc)
}
})

Expand Down

0 comments on commit f8aac65

Please sign in to comment.