Merge pull request #6242 from nalind/common-formats
Use containers/common's formats package instead of our own
This commit is contained in:
		
						commit
						9f6205610c
					
				|  | @ -8,8 +8,8 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/containers/buildah" | 	"github.com/containers/buildah" | ||||||
| 	"github.com/containers/buildah/define" | 	"github.com/containers/buildah/define" | ||||||
| 	"github.com/containers/buildah/pkg/formats" |  | ||||||
| 	"github.com/containers/buildah/util" | 	"github.com/containers/buildah/util" | ||||||
|  | 	"github.com/containers/common/pkg/formats" | ||||||
| 	"github.com/containers/storage" | 	"github.com/containers/storage" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -10,9 +10,9 @@ import ( | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	buildahcli "github.com/containers/buildah/pkg/cli" | 	buildahcli "github.com/containers/buildah/pkg/cli" | ||||||
| 	"github.com/containers/buildah/pkg/formats" |  | ||||||
| 	"github.com/containers/buildah/pkg/parse" | 	"github.com/containers/buildah/pkg/parse" | ||||||
| 	"github.com/containers/common/libimage" | 	"github.com/containers/common/libimage" | ||||||
|  | 	"github.com/containers/common/pkg/formats" | ||||||
| 	"github.com/docker/go-units" | 	"github.com/docker/go-units" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -6,10 +6,10 @@ import ( | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"text/template" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/containers/buildah" | 	"github.com/containers/buildah" | ||||||
| 	"github.com/containers/buildah/define" | 	"github.com/containers/buildah/define" | ||||||
|  | 	"github.com/containers/common/pkg/formats" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	"golang.org/x/term" | 	"golang.org/x/term" | ||||||
| ) | ) | ||||||
|  | @ -71,7 +71,7 @@ func infoCmd(c *cobra.Command, iopts infoResults) error { | ||||||
| 		} else if !matched { | 		} else if !matched { | ||||||
| 			return fmt.Errorf("invalid format provided: %s", format) | 			return fmt.Errorf("invalid format provided: %s", format) | ||||||
| 		} | 		} | ||||||
| 		t, err := template.New("format").Parse(format) | 		t, err := formats.NewParse("info", format) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("template parsing error: %w", err) | 			return fmt.Errorf("template parsing error: %w", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -6,11 +6,11 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"text/template" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/containers/buildah" | 	"github.com/containers/buildah" | ||||||
| 	buildahcli "github.com/containers/buildah/pkg/cli" | 	buildahcli "github.com/containers/buildah/pkg/cli" | ||||||
| 	"github.com/containers/buildah/pkg/parse" | 	"github.com/containers/buildah/pkg/parse" | ||||||
|  | 	"github.com/containers/common/pkg/formats" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	"golang.org/x/term" | 	"golang.org/x/term" | ||||||
| ) | ) | ||||||
|  | @ -113,7 +113,7 @@ func inspectCmd(c *cobra.Command, args []string, iopts inspectResults) error { | ||||||
| 		} else if !matched { | 		} else if !matched { | ||||||
| 			return fmt.Errorf("invalid format provided: %s", format) | 			return fmt.Errorf("invalid format provided: %s", format) | ||||||
| 		} | 		} | ||||||
| 		t, err := template.New("format").Parse(format) | 		t, err := formats.NewParse("inspect", format) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("template parsing error: %w", err) | 			return fmt.Errorf("template parsing error: %w", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										2
									
								
								go.mod
								
								
								
								
							|  | @ -41,7 +41,6 @@ require ( | ||||||
| 	golang.org/x/sync v0.15.0 | 	golang.org/x/sync v0.15.0 | ||||||
| 	golang.org/x/sys v0.33.0 | 	golang.org/x/sys v0.33.0 | ||||||
| 	golang.org/x/term v0.32.0 | 	golang.org/x/term v0.32.0 | ||||||
| 	sigs.k8s.io/yaml v1.4.0 |  | ||||||
| 	tags.cncf.io/container-device-interface v1.0.1 | 	tags.cncf.io/container-device-interface v1.0.1 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -156,5 +155,6 @@ require ( | ||||||
| 	google.golang.org/protobuf v1.36.6 // indirect | 	google.golang.org/protobuf v1.36.6 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| 	k8s.io/klog v1.0.0 // indirect | 	k8s.io/klog v1.0.0 // indirect | ||||||
|  | 	sigs.k8s.io/yaml v1.4.0 // indirect | ||||||
| 	tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect | 	tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | // The formats package is an API compatibility wrapper which merely calls into github.com/containers/common/pkg/formats.  Newer code should instead call that package directly.
 | ||||||
|  | package formats | ||||||
|  | @ -1,166 +1,30 @@ | ||||||
| package formats | package formats | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"github.com/containers/common/pkg/formats" | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"os" |  | ||||||
| 	"strings" |  | ||||||
| 	"text/tabwriter" |  | ||||||
| 	"text/template" |  | ||||||
| 
 |  | ||||||
| 	"golang.org/x/term" |  | ||||||
| 	"sigs.k8s.io/yaml" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	// JSONString const to save on duplicate variable names
 | 	// JSONString const to save on duplicate variable names
 | ||||||
| 	JSONString = "json" | 	JSONString = formats.JSONString | ||||||
| 	// IDString const to save on duplicates for Go templates
 | 	// IDString const to save on duplicates for Go templates
 | ||||||
| 	IDString = "{{.ID}}" | 	IDString = formats.IDString | ||||||
| 
 |  | ||||||
| 	parsingErrorStr = "Template parsing error" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Writer interface for outputs
 | // Writer interface for outputs
 | ||||||
| type Writer interface { | type Writer = formats.Writer | ||||||
| 	Out() error |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // JSONStructArray for JSON output
 | // JSONStructArray for JSON output
 | ||||||
| type JSONStructArray struct { | type JSONStructArray = formats.JSONStructArray | ||||||
| 	Output []any |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // StdoutTemplateArray for Go template output
 | // StdoutTemplateArray for Go template output
 | ||||||
| type StdoutTemplateArray struct { | type StdoutTemplateArray = formats.StdoutTemplateArray | ||||||
| 	Output   []any |  | ||||||
| 	Template string |  | ||||||
| 	Fields   map[string]string |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // JSONStruct for JSON output
 | // JSONStruct for JSON output
 | ||||||
| type JSONStruct struct { | type JSONStruct = formats.JSONStruct | ||||||
| 	Output any |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // StdoutTemplate for Go template output
 | // StdoutTemplate for Go template output
 | ||||||
| type StdoutTemplate struct { | type StdoutTemplate = formats.StdoutTemplate | ||||||
| 	Output   any |  | ||||||
| 	Template string |  | ||||||
| 	Fields   map[string]string |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // YAMLStruct for YAML output
 | // YAMLStruct for YAML output
 | ||||||
| type YAMLStruct struct { | type YAMLStruct = formats.YAMLStruct | ||||||
| 	Output any |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func setJSONFormatEncoder(isTerminal bool, w io.Writer) *json.Encoder { |  | ||||||
| 	enc := json.NewEncoder(w) |  | ||||||
| 	enc.SetIndent("", "    ") |  | ||||||
| 	if isTerminal { |  | ||||||
| 		enc.SetEscapeHTML(false) |  | ||||||
| 	} |  | ||||||
| 	return enc |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Out method for JSON Arrays
 |  | ||||||
| func (j JSONStructArray) Out() error { |  | ||||||
| 	buf := bytes.NewBuffer(nil) |  | ||||||
| 	enc := setJSONFormatEncoder(term.IsTerminal(int(os.Stdout.Fd())), buf) |  | ||||||
| 	if err := enc.Encode(j.Output); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	data := buf.Bytes() |  | ||||||
| 
 |  | ||||||
| 	// JSON returns a byte array with a literal null [110 117 108 108] in it
 |  | ||||||
| 	// if it is passed empty data.  We used bytes.Compare to see if that is
 |  | ||||||
| 	// the case.
 |  | ||||||
| 	if diff := bytes.Compare(data, []byte("null")); diff == 0 { |  | ||||||
| 		data = []byte("[]") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// If the we did get NULL back, we should spit out {} which is
 |  | ||||||
| 	// at least valid JSON for the consumer.
 |  | ||||||
| 	fmt.Printf("%s", data) |  | ||||||
| 	humanNewLine() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Out method for Go templates
 |  | ||||||
| func (t StdoutTemplateArray) Out() error { |  | ||||||
| 	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) |  | ||||||
| 	if strings.HasPrefix(t.Template, "table") { |  | ||||||
| 		// replace any spaces with tabs in template so that tabwriter can align it
 |  | ||||||
| 		t.Template = strings.ReplaceAll(strings.TrimSpace(t.Template[5:]), " ", "\t") |  | ||||||
| 		headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("%v: %w", parsingErrorStr, err) |  | ||||||
| 		} |  | ||||||
| 		err = headerTmpl.Execute(w, t.Fields) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		fmt.Fprintln(w, "") |  | ||||||
| 	} |  | ||||||
| 	t.Template = strings.ReplaceAll(t.Template, " ", "\t") |  | ||||||
| 	tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("%v: %w", parsingErrorStr, err) |  | ||||||
| 	} |  | ||||||
| 	for _, raw := range t.Output { |  | ||||||
| 		basicTmpl := tmpl.Funcs(basicFunctions) |  | ||||||
| 		if err := basicTmpl.Execute(w, raw); err != nil { |  | ||||||
| 			return fmt.Errorf("%v: %w", parsingErrorStr, err) |  | ||||||
| 		} |  | ||||||
| 		fmt.Fprintln(w, "") |  | ||||||
| 	} |  | ||||||
| 	return w.Flush() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Out method for JSON struct
 |  | ||||||
| func (j JSONStruct) Out() error { |  | ||||||
| 	data, err := json.MarshalIndent(j.Output, "", "    ") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	fmt.Printf("%s", data) |  | ||||||
| 	humanNewLine() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Out method for Go templates
 |  | ||||||
| func (t StdoutTemplate) Out() error { |  | ||||||
| 	tmpl, err := template.New("image").Parse(t.Template) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("template parsing error: %w", err) |  | ||||||
| 	} |  | ||||||
| 	err = tmpl.Execute(os.Stdout, t.Output) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	humanNewLine() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Out method for YAML
 |  | ||||||
| func (y YAMLStruct) Out() error { |  | ||||||
| 	var buf []byte |  | ||||||
| 	var err error |  | ||||||
| 	buf, err = yaml.Marshal(y.Output) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	fmt.Printf("%s", string(buf)) |  | ||||||
| 	humanNewLine() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // humanNewLine prints a new line at the end of the output only if stdout is the terminal
 |  | ||||||
| func humanNewLine() { |  | ||||||
| 	if term.IsTerminal(int(os.Stdout.Fd())) { |  | ||||||
| 		fmt.Println() |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,45 +0,0 @@ | ||||||
| package formats |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type ImageData struct { |  | ||||||
| 	Author string `json:"Author"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestSetJSONFormatEncoder(t *testing.T) { |  | ||||||
| 	t.Parallel() |  | ||||||
| 	tt := []struct { |  | ||||||
| 		name       string |  | ||||||
| 		imageData  *ImageData |  | ||||||
| 		expected   string |  | ||||||
| 		isTerminal bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:       "HTML tags are not escaped", |  | ||||||
| 			imageData:  &ImageData{Author: "dave <dave@corp.io>"}, |  | ||||||
| 			expected:   `"Author": "dave <dave@corp.io>"`, |  | ||||||
| 			isTerminal: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:       "HTML tags are escaped", |  | ||||||
| 			imageData:  &ImageData{Author: "dave <dave@corp.io>"}, |  | ||||||
| 			expected:   `"Author": "dave \u003cdave@corp.io\u003e"`, |  | ||||||
| 			isTerminal: false, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tc := range tt { |  | ||||||
| 		buf := bytes.NewBuffer(nil) |  | ||||||
| 		enc := setJSONFormatEncoder(tc.isTerminal, buf) |  | ||||||
| 		if err := enc.Encode(tc.imageData); err != nil { |  | ||||||
| 			t.Errorf("test %#v failed encoding: %s", tc.name, err) |  | ||||||
| 		} |  | ||||||
| 		if !strings.Contains(buf.String(), tc.expected) { |  | ||||||
| 			t.Errorf("test %#v expected output to contain %#v. Output:\n%v\n", tc.name, tc.expected, buf.String()) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,82 +1,19 @@ | ||||||
| package formats | package formats | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"strings" |  | ||||||
| 	"text/template" | 	"text/template" | ||||||
|  | 
 | ||||||
|  | 	"github.com/containers/common/pkg/formats" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // basicFunctions are the set of initial
 |  | ||||||
| // functions provided to every template.
 |  | ||||||
| var basicFunctions = template.FuncMap{ |  | ||||||
| 	"json": func(v any) string { |  | ||||||
| 		buf := &bytes.Buffer{} |  | ||||||
| 		enc := json.NewEncoder(buf) |  | ||||||
| 		enc.SetEscapeHTML(false) |  | ||||||
| 		_ = enc.Encode(v) |  | ||||||
| 		// Remove the trailing new line added by the encoder
 |  | ||||||
| 		return strings.TrimSpace(buf.String()) |  | ||||||
| 	}, |  | ||||||
| 	"split": strings.Split, |  | ||||||
| 	"join":  strings.Join, |  | ||||||
| 	// strings.Title is deprecated since go 1.18
 |  | ||||||
| 	// However for our use case it is still fine. The recommended replacement
 |  | ||||||
| 	// is adding about 400kb binary size so lets keep using this for now.
 |  | ||||||
| 	//nolint:staticcheck
 |  | ||||||
| 	"title":    strings.Title, |  | ||||||
| 	"lower":    strings.ToLower, |  | ||||||
| 	"upper":    strings.ToUpper, |  | ||||||
| 	"pad":      padWithSpace, |  | ||||||
| 	"truncate": truncateWithLength, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // HeaderFunctions are used to created headers of a table.
 |  | ||||||
| // This is a replacement of basicFunctions for header generation
 |  | ||||||
| // because we want the header to remain intact.
 |  | ||||||
| // Some functions like `split` are irrelevant so not added.
 |  | ||||||
| var headerFunctions = template.FuncMap{ |  | ||||||
| 	"json": func(v string) string { |  | ||||||
| 		return v |  | ||||||
| 	}, |  | ||||||
| 	"title": func(v string) string { |  | ||||||
| 		return v |  | ||||||
| 	}, |  | ||||||
| 	"lower": func(v string) string { |  | ||||||
| 		return v |  | ||||||
| 	}, |  | ||||||
| 	"upper": func(v string) string { |  | ||||||
| 		return v |  | ||||||
| 	}, |  | ||||||
| 	"truncate": func(v string, _ int) string { |  | ||||||
| 		return v |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Parse creates a new anonymous template with the basic functions
 | // Parse creates a new anonymous template with the basic functions
 | ||||||
| // and parses the given format.
 | // and parses the given format.
 | ||||||
| func Parse(format string) (*template.Template, error) { | func Parse(format string) (*template.Template, error) { | ||||||
| 	return NewParse("", format) | 	return formats.Parse(format) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewParse creates a new tagged template with the basic functions
 | // NewParse creates a new tagged template with the basic functions
 | ||||||
| // and parses the given format.
 | // and parses the given format.
 | ||||||
| func NewParse(tag, format string) (*template.Template, error) { | func NewParse(tag, format string) (*template.Template, error) { | ||||||
| 	return template.New(tag).Funcs(basicFunctions).Parse(format) | 	return formats.NewParse(tag, format) | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // padWithSpace adds whitespace to the input if the input is non-empty
 |  | ||||||
| func padWithSpace(source string, prefix, suffix int) string { |  | ||||||
| 	if source == "" { |  | ||||||
| 		return source |  | ||||||
| 	} |  | ||||||
| 	return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // truncateWithLength truncates the source string up to the length provided by the input
 |  | ||||||
| func truncateWithLength(source string, length int) string { |  | ||||||
| 	if len(source) < length { |  | ||||||
| 		return source |  | ||||||
| 	} |  | ||||||
| 	return source[:length] |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,8 +28,8 @@ load helpers | ||||||
|   run_buildah inspect --type image --format '{{.OCIv1.Config}}' alpine-image |   run_buildah inspect --type image --format '{{.OCIv1.Config}}' alpine-image | ||||||
|   inspect_after_commit=$output |   inspect_after_commit=$output | ||||||
| 
 | 
 | ||||||
|   # ...except that at some point in November 2019 buildah-inspect started |   # ...except that in #2510/#3036/#3829 we started adding a label with | ||||||
|   # including version. Strip it out, |   # buildah's version. Strip it out for comparison. | ||||||
|   run_buildah --version |   run_buildah --version | ||||||
|   local -a output_fields=($output) |   local -a output_fields=($output) | ||||||
|   buildah_version=${output_fields[2]} |   buildah_version=${output_fields[2]} | ||||||
|  |  | ||||||
|  | @ -0,0 +1,166 @@ | ||||||
|  | package formats | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"text/tabwriter" | ||||||
|  | 	"text/template" | ||||||
|  | 
 | ||||||
|  | 	terminal "golang.org/x/term" | ||||||
|  | 	"sigs.k8s.io/yaml" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// JSONString const to save on duplicate variable names
 | ||||||
|  | 	JSONString = "json" | ||||||
|  | 	// IDString const to save on duplicates for Go templates
 | ||||||
|  | 	IDString = "{{.ID}}" | ||||||
|  | 
 | ||||||
|  | 	parsingErrorStr = "Template parsing error" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Writer interface for outputs
 | ||||||
|  | type Writer interface { | ||||||
|  | 	Out() error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JSONStructArray for JSON output
 | ||||||
|  | type JSONStructArray struct { | ||||||
|  | 	Output []any | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // StdoutTemplateArray for Go template output
 | ||||||
|  | type StdoutTemplateArray struct { | ||||||
|  | 	Output   []any | ||||||
|  | 	Template string | ||||||
|  | 	Fields   map[string]string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JSONStruct for JSON output
 | ||||||
|  | type JSONStruct struct { | ||||||
|  | 	Output any | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // StdoutTemplate for Go template output
 | ||||||
|  | type StdoutTemplate struct { | ||||||
|  | 	Output   any | ||||||
|  | 	Template string | ||||||
|  | 	Fields   map[string]string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // YAMLStruct for YAML output
 | ||||||
|  | type YAMLStruct struct { | ||||||
|  | 	Output any | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setJSONFormatEncoder(isTerminal bool, w io.Writer) *json.Encoder { | ||||||
|  | 	enc := json.NewEncoder(w) | ||||||
|  | 	enc.SetIndent("", "    ") | ||||||
|  | 	if isTerminal { | ||||||
|  | 		enc.SetEscapeHTML(false) | ||||||
|  | 	} | ||||||
|  | 	return enc | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Out method for JSON Arrays
 | ||||||
|  | func (j JSONStructArray) Out() error { | ||||||
|  | 	buf := bytes.NewBuffer(nil) | ||||||
|  | 	enc := setJSONFormatEncoder(terminal.IsTerminal(int(os.Stdout.Fd())), buf) | ||||||
|  | 	if err := enc.Encode(j.Output); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	data := buf.Bytes() | ||||||
|  | 
 | ||||||
|  | 	// JSON returns a byte array with a literal null [110 117 108 108] in it
 | ||||||
|  | 	// if it is passed empty data.  We used bytes.Compare to see if that is
 | ||||||
|  | 	// the case.
 | ||||||
|  | 	if diff := bytes.Compare(data, []byte("null")); diff == 0 { | ||||||
|  | 		data = []byte("[]") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// If the we did get NULL back, we should spit out {} which is
 | ||||||
|  | 	// at least valid JSON for the consumer.
 | ||||||
|  | 	fmt.Printf("%s", data) //nolint:forbidigo
 | ||||||
|  | 	humanNewLine() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Out method for Go templates
 | ||||||
|  | func (t StdoutTemplateArray) Out() error { | ||||||
|  | 	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) | ||||||
|  | 	if strings.HasPrefix(t.Template, "table") { | ||||||
|  | 		// replace any spaces with tabs in template so that tabwriter can align it
 | ||||||
|  | 		t.Template = strings.ReplaceAll(strings.TrimSpace(t.Template[5:]), " ", "\t") | ||||||
|  | 		headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("%s: %w", parsingErrorStr, err) | ||||||
|  | 		} | ||||||
|  | 		err = headerTmpl.Execute(w, t.Fields) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		fmt.Fprintln(w, "") | ||||||
|  | 	} | ||||||
|  | 	t.Template = strings.ReplaceAll(t.Template, " ", "\t") | ||||||
|  | 	tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("%s: %w", parsingErrorStr, err) | ||||||
|  | 	} | ||||||
|  | 	for _, raw := range t.Output { | ||||||
|  | 		basicTmpl := tmpl.Funcs(basicFunctions) | ||||||
|  | 		if err := basicTmpl.Execute(w, raw); err != nil { | ||||||
|  | 			return fmt.Errorf("%s: %w", parsingErrorStr, err) | ||||||
|  | 		} | ||||||
|  | 		fmt.Fprintln(w, "") | ||||||
|  | 	} | ||||||
|  | 	return w.Flush() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Out method for JSON struct
 | ||||||
|  | func (j JSONStruct) Out() error { | ||||||
|  | 	data, err := json.MarshalIndent(j.Output, "", "    ") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("%s", data) //nolint:forbidigo
 | ||||||
|  | 	humanNewLine() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Out method for Go templates
 | ||||||
|  | func (t StdoutTemplate) Out() error { | ||||||
|  | 	tmpl, err := template.New("image").Parse(t.Template) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("template parsing error: %w", err) | ||||||
|  | 	} | ||||||
|  | 	err = tmpl.Execute(os.Stdout, t.Output) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	humanNewLine() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Out method for YAML
 | ||||||
|  | func (y YAMLStruct) Out() error { | ||||||
|  | 	var buf []byte | ||||||
|  | 	var err error | ||||||
|  | 	buf, err = yaml.Marshal(y.Output) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Printf("%s", string(buf)) //nolint:forbidigo
 | ||||||
|  | 	humanNewLine() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // humanNewLine prints a new line at the end of the output only if stdout is the terminal
 | ||||||
|  | func humanNewLine() { | ||||||
|  | 	if terminal.IsTerminal(int(os.Stdout.Fd())) { | ||||||
|  | 		fmt.Println() //nolint:forbidigo
 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,78 @@ | ||||||
|  | package formats | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"strings" | ||||||
|  | 	"text/template" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // basicFunctions are the set of initial
 | ||||||
|  | // functions provided to every template.
 | ||||||
|  | var basicFunctions = template.FuncMap{ | ||||||
|  | 	"json": func(v any) string { | ||||||
|  | 		buf := &bytes.Buffer{} | ||||||
|  | 		enc := json.NewEncoder(buf) | ||||||
|  | 		enc.SetEscapeHTML(false) | ||||||
|  | 		_ = enc.Encode(v) | ||||||
|  | 		// Remove the trailing new line added by the encoder
 | ||||||
|  | 		return strings.TrimSpace(buf.String()) | ||||||
|  | 	}, | ||||||
|  | 	"split":    strings.Split, | ||||||
|  | 	"join":     strings.Join, | ||||||
|  | 	"title":    strings.Title, //nolint:staticcheck
 | ||||||
|  | 	"lower":    strings.ToLower, | ||||||
|  | 	"upper":    strings.ToUpper, | ||||||
|  | 	"pad":      padWithSpace, | ||||||
|  | 	"truncate": truncateWithLength, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HeaderFunctions are used to created headers of a table.
 | ||||||
|  | // This is a replacement of basicFunctions for header generation
 | ||||||
|  | // because we want the header to remain intact.
 | ||||||
|  | // Some functions like `split` are irrelevant so not added.
 | ||||||
|  | var headerFunctions = template.FuncMap{ | ||||||
|  | 	"json": func(v string) string { | ||||||
|  | 		return v | ||||||
|  | 	}, | ||||||
|  | 	"title": func(v string) string { | ||||||
|  | 		return v | ||||||
|  | 	}, | ||||||
|  | 	"lower": func(v string) string { | ||||||
|  | 		return v | ||||||
|  | 	}, | ||||||
|  | 	"upper": func(v string) string { | ||||||
|  | 		return v | ||||||
|  | 	}, | ||||||
|  | 	"truncate": func(v string, _ int) string { | ||||||
|  | 		return v | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Parse creates a new anonymous 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) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // padWithSpace adds whitespace to the input if the input is non-empty
 | ||||||
|  | func padWithSpace(source string, prefix, suffix int) string { | ||||||
|  | 	if source == "" { | ||||||
|  | 		return source | ||||||
|  | 	} | ||||||
|  | 	return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // truncateWithLength truncates the source string up to the length provided by the input
 | ||||||
|  | func truncateWithLength(source string, length int) string { | ||||||
|  | 	if len(source) < length { | ||||||
|  | 		return source | ||||||
|  | 	} | ||||||
|  | 	return source[:length] | ||||||
|  | } | ||||||
|  | @ -131,6 +131,7 @@ github.com/containers/common/pkg/completion | ||||||
| github.com/containers/common/pkg/config | github.com/containers/common/pkg/config | ||||||
| github.com/containers/common/pkg/download | github.com/containers/common/pkg/download | ||||||
| github.com/containers/common/pkg/filters | github.com/containers/common/pkg/filters | ||||||
|  | github.com/containers/common/pkg/formats | ||||||
| github.com/containers/common/pkg/hooks | github.com/containers/common/pkg/hooks | ||||||
| github.com/containers/common/pkg/hooks/0.1.0 | github.com/containers/common/pkg/hooks/0.1.0 | ||||||
| github.com/containers/common/pkg/hooks/1.0.0 | github.com/containers/common/pkg/hooks/1.0.0 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue