diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index 32eb5bf57..dd583e840 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/pflag" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" ) @@ -53,5 +54,5 @@ func bindOutputFlag(cmd *cobra.Command, varRef *string) { // NOTE(taylor): A possible refactor here is that we can implement all the // validation for the OutputFormat type here so we don't have to do the // parsing and checking in the command - cmd.Flags().StringVarP(varRef, outputFlag, "o", string(action.Table), fmt.Sprintf("prints the output in the specified format. Allowed values: %s, %s, %s", action.Table, action.JSON, action.YAML)) + cmd.Flags().StringVarP(varRef, outputFlag, "o", output.Table.String(), fmt.Sprintf("prints the output in the specified format. Allowed values: %s, %s, %s", output.Table, output.JSON, output.YAML)) } diff --git a/cmd/helm/get.go b/cmd/helm/get.go index cbf588cff..d9c8a2d8c 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -23,6 +23,7 @@ import ( "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" ) var getHelp = ` @@ -58,7 +59,8 @@ func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } return tpl(template, data, out) } - return action.PrintRelease(out, res, true) + + return output.Table.Write(out, &statusPrinter{res, true}) }, } diff --git a/cmd/helm/history.go b/cmd/helm/history.go index 3db7e167c..d6ffec837 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -27,6 +27,7 @@ import ( "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" ) @@ -59,7 +60,7 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - output, err := action.ParseOutputFormat(client.OutputFormat) + output, err := output.ParseFormat(client.OutputFormat) if err != nil { return err } @@ -92,11 +93,11 @@ type releaseInfo struct { type releaseHistory []releaseInfo func (r releaseHistory) WriteJSON(out io.Writer) error { - return action.EncodeJSON(out, r) + return output.EncodeJSON(out, r) } func (r releaseHistory) WriteYAML(out io.Writer) error { - return action.EncodeYAML(out, r) + return output.EncodeYAML(out, r) } func (r releaseHistory) WriteTable(out io.Writer) error { @@ -105,7 +106,7 @@ func (r releaseHistory) WriteTable(out io.Writer) error { for _, item := range r { tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) } - return action.EncodeTable(out, tbl) + return output.EncodeTable(out, tbl) } func getHistory(client *action.History, name string) (releaseHistory, error) { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 8d9c76443..2b7cf3499 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -28,6 +28,7 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/getter" @@ -113,7 +114,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(_ *cobra.Command, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - output, err := action.ParseOutputFormat(client.OutputFormat) + output, err := output.ParseFormat(client.OutputFormat) if err != nil { return err } diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 57af01e51..ab5f52ebd 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -26,6 +26,7 @@ import ( "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/release" ) @@ -68,7 +69,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - output, err := action.ParseOutputFormat(client.OutputFormat) + output, err := output.ParseFormat(client.OutputFormat) if err != nil { return err } @@ -153,13 +154,13 @@ func (r *releaseListWriter) WriteTable(out io.Writer) error { for _, r := range r.releases { table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion) } - return action.EncodeTable(out, table) + return output.EncodeTable(out, table) } func (r *releaseListWriter) WriteJSON(out io.Writer) error { - return action.EncodeJSON(out, r.releases) + return output.EncodeJSON(out, r.releases) } func (r *releaseListWriter) WriteYAML(out io.Writer) error { - return action.EncodeYAML(out, r.releases) + return output.EncodeYAML(out, r.releases) } diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 5abc73c09..c450539f9 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -24,12 +24,12 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/repo" ) func newRepoListCmd(out io.Writer) *cobra.Command { - var output string + var outputFormat string cmd := &cobra.Command{ Use: "list", Short: "list chart repositories", @@ -37,7 +37,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - outfmt, err := action.ParseOutputFormat(output) + outfmt, err := output.ParseFormat(outputFormat) if err != nil { return err } @@ -50,7 +50,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command { }, } - bindOutputFlag(cmd, &output) + bindOutputFlag(cmd, &outputFormat) return cmd } @@ -70,18 +70,18 @@ func (r *repoListWriter) WriteTable(out io.Writer) error { for _, re := range r.repos { table.AddRow(re.Name, re.URL) } - return action.EncodeTable(out, table) + return output.EncodeTable(out, table) } func (r *repoListWriter) WriteJSON(out io.Writer) error { - return r.encodeByFormat(out, action.JSON) + return r.encodeByFormat(out, output.JSON) } func (r *repoListWriter) WriteYAML(out io.Writer) error { - return r.encodeByFormat(out, action.YAML) + return r.encodeByFormat(out, output.YAML) } -func (r *repoListWriter) encodeByFormat(out io.Writer, format action.OutputFormat) error { +func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) error { // Initialize the array so no results returns an empty array instead of null repolist := make([]repositoryElement, 0, len(r.repos)) @@ -90,10 +90,10 @@ func (r *repoListWriter) encodeByFormat(out io.Writer, format action.OutputForma } switch format { - case action.JSON: - return action.EncodeJSON(out, repolist) - case action.YAML: - return action.EncodeYAML(out, repolist) + case output.JSON: + return output.EncodeJSON(out, repolist) + case output.YAML: + return output.EncodeYAML(out, repolist) } // Because this is a non-exported function and only called internally by diff --git a/cmd/helm/search_hub.go b/cmd/helm/search_hub.go index bf2a3aeab..78cd9eb12 100644 --- a/cmd/helm/search_hub.go +++ b/cmd/helm/search_hub.go @@ -26,7 +26,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v3/internal/monocular" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" ) const searchHubDesc = ` @@ -70,7 +70,7 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { func (o *searchHubOptions) run(out io.Writer, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - outfmt, err := action.ParseOutputFormat(o.outputFormat) + outfmt, err := output.ParseFormat(o.outputFormat) if err != nil { return err } @@ -125,18 +125,18 @@ func (h *hubSearchWriter) WriteTable(out io.Writer) error { for _, r := range h.elements { table.AddRow(r.URL, r.Version, r.AppVersion, r.Description) } - return action.EncodeTable(out, table) + return output.EncodeTable(out, table) } func (h *hubSearchWriter) WriteJSON(out io.Writer) error { - return h.encodeByFormat(out, action.JSON) + return h.encodeByFormat(out, output.JSON) } func (h *hubSearchWriter) WriteYAML(out io.Writer) error { - return h.encodeByFormat(out, action.YAML) + return h.encodeByFormat(out, output.YAML) } -func (h *hubSearchWriter) encodeByFormat(out io.Writer, format action.OutputFormat) error { +func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { // Initialize the array so no results returns an empty array instead of null chartList := make([]hubChartElement, 0, len(h.elements)) @@ -145,10 +145,10 @@ func (h *hubSearchWriter) encodeByFormat(out io.Writer, format action.OutputForm } switch format { - case action.JSON: - return action.EncodeJSON(out, chartList) - case action.YAML: - return action.EncodeYAML(out, chartList) + case output.JSON: + return output.EncodeJSON(out, chartList) + case output.YAML: + return output.EncodeYAML(out, chartList) } // Because this is a non-exported function and only called internally by diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go index a30629420..b9a10d2dd 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -28,7 +28,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/search" - "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/repo" ) @@ -81,7 +81,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command { func (o *searchRepoOptions) run(out io.Writer, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - outfmt, err := action.ParseOutputFormat(o.outputFormat) + outfmt, err := output.ParseFormat(o.outputFormat) if err != nil { return err } @@ -188,18 +188,18 @@ func (r *repoSearchWriter) WriteTable(out io.Writer) error { for _, r := range r.results { table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) } - return action.EncodeTable(out, table) + return output.EncodeTable(out, table) } func (r *repoSearchWriter) WriteJSON(out io.Writer) error { - return r.encodeByFormat(out, action.JSON) + return r.encodeByFormat(out, output.JSON) } func (r *repoSearchWriter) WriteYAML(out io.Writer) error { - return r.encodeByFormat(out, action.YAML) + return r.encodeByFormat(out, output.YAML) } -func (r *repoSearchWriter) encodeByFormat(out io.Writer, format action.OutputFormat) error { +func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { // Initialize the array so no results returns an empty array instead of null chartList := make([]repoChartElement, 0, len(r.results)) @@ -208,10 +208,10 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format action.OutputFor } switch format { - case action.JSON: - return action.EncodeJSON(out, chartList) - case action.YAML: - return action.EncodeYAML(out, chartList) + case output.JSON: + return output.EncodeJSON(out, chartList) + case output.YAML: + return output.EncodeYAML(out, chartList) } // Because this is a non-exported function and only called internally by diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 53f03b8ba..80e61c3b5 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -17,12 +17,17 @@ limitations under the License. package main import ( + "fmt" "io" + "strings" + "time" "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/release" ) @@ -48,7 +53,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - outfmt, err := action.ParseOutputFormat(client.OutputFormat) + outfmt, err := output.ParseFormat(client.OutputFormat) if err != nil { return err } @@ -78,13 +83,88 @@ type statusPrinter struct { } func (s statusPrinter) WriteJSON(out io.Writer) error { - return action.EncodeJSON(out, s.release) + return output.EncodeJSON(out, s.release) } func (s statusPrinter) WriteYAML(out io.Writer) error { - return action.EncodeYAML(out, s.release) + return output.EncodeYAML(out, s.release) } func (s statusPrinter) WriteTable(out io.Writer) error { - return action.PrintRelease(out, s.release, s.debug) + if s.release == nil { + return nil + } + fmt.Fprintf(out, "NAME: %s\n", s.release.Name) + if !s.release.Info.LastDeployed.IsZero() { + fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) + } + fmt.Fprintf(out, "NAMESPACE: %s\n", s.release.Namespace) + fmt.Fprintf(out, "STATUS: %s\n", s.release.Info.Status.String()) + fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) + + executions := executionsByHookEvent(s.release) + if tests, ok := executions[release.HookTest]; ok { + for _, h := range tests { + // Don't print anything if hook has not been initiated + if h.LastRun.StartedAt.IsZero() { + continue + } + fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n\n", + h.Name, + fmt.Sprintf("Last Started: %s", h.LastRun.StartedAt.Format(time.ANSIC)), + fmt.Sprintf("Last Completed: %s", h.LastRun.CompletedAt.Format(time.ANSIC)), + fmt.Sprintf("Phase: %s", h.LastRun.Phase), + ) + } + } + + if s.debug { + fmt.Fprintln(out, "USER-SUPPLIED VALUES:") + err := output.EncodeYAML(out, s.release.Config) + if err != nil { + return err + } + // Print an extra newline + fmt.Fprintln(out) + + cfg, err := chartutil.CoalesceValues(s.release.Chart, s.release.Config) + if err != nil { + return err + } + + fmt.Fprintln(out, "COMPUTED VALUES:") + err = output.EncodeYAML(out, cfg.AsMap()) + if err != nil { + return err + } + // Print an extra newline + fmt.Fprintln(out) + } + + if strings.EqualFold(s.release.Info.Description, "Dry run complete") || s.debug { + fmt.Fprintln(out, "HOOKS:") + for _, h := range s.release.Hooks { + fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) + } + fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) + } + + if len(s.release.Info.Notes) > 0 { + fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) + } + return nil +} + +func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook { + result := make(map[release.HookEvent][]*release.Hook) + for _, h := range rel.Hooks { + for _, e := range h.Events { + executions, ok := result[e] + if !ok { + executions = []*release.Hook{} + } + result[e] = append(executions, h) + } + } + return result } diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index c68576abd..64c7985f9 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -27,6 +27,7 @@ import ( "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/storage/driver" @@ -71,7 +72,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { // validate the output format first so we don't waste time running a // request that we'll throw away - output, err := action.ParseOutputFormat(client.OutputFormat) + outfmt, err := output.ParseFormat(client.OutputFormat) if err != nil { return err } @@ -100,7 +101,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { histClient.Max = 1 if _, err := histClient.Run(args[0]); err == driver.ErrReleaseNotFound { // Only print this to stdout for table output - if output == action.Table { + if outfmt == output.Table { fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) } instClient := action.NewInstall(cfg) @@ -117,7 +118,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - return output.Write(out, &statusPrinter{rel, settings.Debug}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug}) } } @@ -137,11 +138,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return errors.Wrap(err, "UPGRADE FAILED") } - if output == action.Table { + if outfmt == output.Table { fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) } - return output.Write(out, &statusPrinter{rel, settings.Debug}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug}) }, } diff --git a/pkg/action/printer.go b/pkg/action/printer.go deleted file mode 100644 index 5e8e86495..000000000 --- a/pkg/action/printer.go +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "fmt" - "io" - "strings" - "time" - - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/release" -) - -// PrintRelease prints info about a release. If fullInfo is true, it will print -// the user supplied values and the computed values used to render the chart -func PrintRelease(out io.Writer, rel *release.Release, fullInfo bool) error { - if rel == nil { - return nil - } - fmt.Fprintf(out, "NAME: %s\n", rel.Name) - if !rel.Info.LastDeployed.IsZero() { - fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Format(time.ANSIC)) - } - fmt.Fprintf(out, "NAMESPACE: %s\n", rel.Namespace) - fmt.Fprintf(out, "STATUS: %s\n", rel.Info.Status.String()) - fmt.Fprintf(out, "REVISION: %d\n", rel.Version) - - executions := executionsByHookEvent(rel) - if tests, ok := executions[release.HookTest]; ok { - for _, h := range tests { - // Don't print anything if hook has not been initiated - if h.LastRun.StartedAt.IsZero() { - continue - } - fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n\n", - h.Name, - fmt.Sprintf("Last Started: %s", h.LastRun.StartedAt.Format(time.ANSIC)), - fmt.Sprintf("Last Completed: %s", h.LastRun.CompletedAt.Format(time.ANSIC)), - fmt.Sprintf("Phase: %s", h.LastRun.Phase), - ) - } - } - - if fullInfo { - fmt.Fprintln(out, "USER-SUPPLIED VALUES:") - err := EncodeYAML(out, rel.Config) - if err != nil { - return err - } - // Print an extra newline - fmt.Fprintln(out) - - cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config) - if err != nil { - return err - } - - fmt.Fprintln(out, "COMPUTED VALUES:") - err = EncodeYAML(out, cfg.AsMap()) - if err != nil { - return err - } - // Print an extra newline - fmt.Fprintln(out) - } - - if strings.EqualFold(rel.Info.Description, "Dry run complete") || fullInfo { - fmt.Fprintln(out, "HOOKS:") - for _, h := range rel.Hooks { - fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) - } - fmt.Fprintf(out, "MANIFEST:\n%s\n", rel.Manifest) - } - - if len(rel.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(rel.Info.Notes)) - } - return nil -} - -func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook { - result := make(map[release.HookEvent][]*release.Hook) - for _, h := range rel.Hooks { - for _, e := range h.Events { - executions, ok := result[e] - if !ok { - executions = []*release.Hook{} - } - result[e] = append(executions, h) - } - } - return result -} diff --git a/pkg/action/output.go b/pkg/cli/output/output.go similarity index 86% rename from pkg/action/output.go rename to pkg/cli/output/output.go index 0b3ee029a..da9ee63a8 100644 --- a/pkg/action/output.go +++ b/pkg/cli/output/output.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package action +package output import ( "encoding/json" @@ -26,26 +26,26 @@ import ( "sigs.k8s.io/yaml" ) -// OutputFormat is a type for capturing supported output formats -type OutputFormat string +// Format is a type for capturing supported output formats +type Format string const ( - Table OutputFormat = "table" - JSON OutputFormat = "json" - YAML OutputFormat = "yaml" + Table Format = "table" + JSON Format = "json" + YAML Format = "yaml" ) // ErrInvalidFormatType is returned when an unsupported format type is used var ErrInvalidFormatType = fmt.Errorf("invalid format type") -// String returns the string reprsentation of the OutputFormat -func (o OutputFormat) String() string { +// String returns the string reprsentation of the Format +func (o Format) String() string { return string(o) } // Write the output in the given format to the io.Writer. Unsupported formats // will return an error -func (o OutputFormat) Write(out io.Writer, w Writer) error { +func (o Format) Write(out io.Writer, w Writer) error { switch o { case Table: return w.WriteTable(out) @@ -57,9 +57,9 @@ func (o OutputFormat) Write(out io.Writer, w Writer) error { return ErrInvalidFormatType } -// ParseOutputFormat takes a raw string and returns the matching OutputFormat. +// ParseFormat takes a raw string and returns the matching Format. // If the format does not exists, ErrInvalidFormatType is returned -func ParseOutputFormat(s string) (out OutputFormat, err error) { +func ParseFormat(s string) (out Format, err error) { switch s { case Table.String(): out, err = Table, nil