summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/containers/diff.go9
-rw-r--r--cmd/podman/images/list.go74
-rw-r--r--cmd/podman/parse/json.go3
-rw-r--r--cmd/podman/parse/json_test.go25
-rw-r--r--cmd/podman/report/format.go68
-rw-r--r--cmd/podman/report/format_test.go35
-rw-r--r--test/e2e/info_test.go10
-rw-r--r--test/e2e/version_test.go11
8 files changed, 178 insertions, 57 deletions
diff --git a/cmd/podman/containers/diff.go b/cmd/podman/containers/diff.go
index 227c13f4c..a3ca6edf9 100644
--- a/cmd/podman/containers/diff.go
+++ b/cmd/podman/containers/diff.go
@@ -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"
@@ -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'")
}
diff --git a/cmd/podman/images/list.go b/cmd/podman/images/list.go
index ffb341fc4..239da9d28 100644
--- a/cmd/podman/images/list.go
+++ b/cmd/podman/images/list.go
@@ -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"
@@ -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)
}
}
@@ -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)
}
@@ -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 {
diff --git a/cmd/podman/parse/json.go b/cmd/podman/parse/json.go
index 95a6633b8..40ac415db 100644
--- a/cmd/podman/parse/json.go
+++ b/cmd/podman/parse/json.go
@@ -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))
}
diff --git a/cmd/podman/parse/json_test.go b/cmd/podman/parse/json_test.go
index 5cad185fd..ec3b5664b 100644
--- a/cmd/podman/parse/json_test.go
+++ b/cmd/podman/parse/json_test.go
@@ -1,6 +1,8 @@
package parse
import (
+ "fmt"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -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))
+ })
+ }
}
diff --git a/cmd/podman/report/format.go b/cmd/podman/report/format.go
new file mode 100644
index 000000000..32d92bec5
--- /dev/null
+++ b/cmd/podman/report/format.go
@@ -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}
+}
diff --git a/cmd/podman/report/format_test.go b/cmd/podman/report/format_test.go
new file mode 100644
index 000000000..7dd62e899
--- /dev/null
+++ b/cmd/podman/report/format_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go
index bcbfdd80a..49f5f0ce6 100644
--- a/test/e2e/info_test.go
+++ b/test/e2e/info_test.go
@@ -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)
}
})
diff --git a/test/e2e/version_test.go b/test/e2e/version_test.go
index 695cccc11..903748b58 100644
--- a/test/e2e/version_test.go
+++ b/test/e2e/version_test.go
@@ -1,6 +1,7 @@
package integration
import (
+ "fmt"
"os"
. "github.com/containers/podman/v2/test/utils"
@@ -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)
}
})