From 1c031d4fa8450550fa9a6bd5abe7fac45816156e Mon Sep 17 00:00:00 2001 From: Chok Yip Lau Date: Thu, 2 Sep 2021 13:41:07 -0400 Subject: [PATCH] Beautify kubectl help flag commands --- .../pkg/genericclioptions/print_flags.go | 2 +- .../pkg/cmd/apiresources/apiresources.go | 2 +- .../pkg/cmd/apply/apply_view_last_applied.go | 2 +- .../kubectl/pkg/cmd/config/get_contexts.go | 2 +- .../k8s.io/kubectl/pkg/cmd/get/get_flags.go | 2 +- .../pkg/util/templates/help_flags_printer.go | 76 +++++++++++++++++++ .../kubectl/pkg/util/templates/templater.go | 50 ++++++------ .../kubectl/pkg/util/term/term_writer.go | 36 +++++++-- 8 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 staging/src/k8s.io/kubectl/pkg/util/templates/help_flags_printer.go diff --git a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/print_flags.go b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/print_flags.go index 1ec93aecc486..0a69aad8b9e9 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/print_flags.go +++ b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/print_flags.go @@ -136,7 +136,7 @@ func (f *PrintFlags) AddFlags(cmd *cobra.Command) { f.TemplatePrinterFlags.AddFlags(cmd) if f.OutputFormat != nil { - cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf("Output format. One of: %s.", strings.Join(f.AllowedFormats(), "|"))) + cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: (%s).`, strings.Join(f.AllowedFormats(), ", "))) if f.OutputFlagSpecified == nil { f.OutputFlagSpecified = func() bool { return cmd.Flag("output").Changed diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go b/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go index 94ce9d32e28c..5a099c7a4802 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go @@ -105,7 +105,7 @@ func NewCmdAPIResources(f cmdutil.Factory, ioStreams genericclioptions.IOStreams } cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).") - cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "Output format. One of: wide|name.") + cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, `Output format. One of: (wide, name).`) cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.") cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.") diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_view_last_applied.go b/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_view_last_applied.go index c0606f408363..64b0959d33e5 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_view_last_applied.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_view_last_applied.go @@ -85,7 +85,7 @@ func NewCmdApplyViewLastApplied(f cmdutil.Factory, ioStreams genericclioptions.I }, } - cmd.Flags().StringVarP(&options.OutputFormat, "output", "o", options.OutputFormat, "Output format. Must be one of yaml|json") + cmd.Flags().StringVarP(&options.OutputFormat, "output", "o", options.OutputFormat, `Output format. Must be one of (yaml, json)`) cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all resources in the namespace of the specified resource types") usage := "that contains the last-applied-configuration annotations" cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go index a8fdb753850e..033a179ae46e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go @@ -80,7 +80,7 @@ func NewCmdConfigGetContexts(streams genericclioptions.IOStreams, configAccess c } cmd.Flags().Bool("no-headers", false, "When using the default or custom-column output format, don't print headers (default print headers).") - cmd.Flags().StringP("output", "o", "", "Output format. One of: name") + cmd.Flags().StringP("output", "o", "", `Output format. One of: (name).`) return cmd } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go b/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go index b53abf17b447..bbe2e6667c06 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go @@ -162,7 +162,7 @@ func (f *PrintFlags) AddFlags(cmd *cobra.Command) { f.CustomColumnsFlags.AddFlags(cmd) if f.OutputFormat != nil { - cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf("Output format. One of: %s See custom columns [https://kubernetes.io/docs/reference/kubectl/overview/#custom-columns], golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [https://kubernetes.io/docs/reference/kubectl/jsonpath/].", strings.Join(f.AllowedFormats(), "|"))) + cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: (%s). See custom columns [https://kubernetes.io/docs/reference/kubectl/overview/#custom-columns], golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [https://kubernetes.io/docs/reference/kubectl/jsonpath/].`, strings.Join(f.AllowedFormats(), ", "))) util.CheckErr(cmd.RegisterFlagCompletionFunc( "output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/staging/src/k8s.io/kubectl/pkg/util/templates/help_flags_printer.go b/staging/src/k8s.io/kubectl/pkg/util/templates/help_flags_printer.go new file mode 100644 index 000000000000..fdfdf08eeb1a --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/util/templates/help_flags_printer.go @@ -0,0 +1,76 @@ +/* +Copyright 2022 The Kubernetes 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 templates + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/mitchellh/go-wordwrap" + flag "github.com/spf13/pflag" +) + +const offset = 10 + +// HelpFlagPrinter is a printer that +// processes the help flag and print +// it to i/o writer +type HelpFlagPrinter struct { + wrapLimit uint + out io.Writer +} + +// NewHelpFlagPrinter will initialize a HelpFlagPrinter given the +// i/o writer +func NewHelpFlagPrinter(out io.Writer, wrapLimit uint) *HelpFlagPrinter { + return &HelpFlagPrinter{ + wrapLimit: wrapLimit, + out: out, + } +} + +// PrintHelpFlag will beautify the help flags and print it out to p.out +func (p *HelpFlagPrinter) PrintHelpFlag(flag *flag.Flag) { + formatBuf := new(bytes.Buffer) + writeFlag(formatBuf, flag) + + wrappedStr := formatBuf.String() + flagAndUsage := strings.Split(formatBuf.String(), "\n") + flagStr := flagAndUsage[0] + + // if the flag usage is longer than one line, wrap it again + if len(flagAndUsage) > 1 { + nextLines := strings.Join(flagAndUsage[1:], " ") + wrappedUsages := wordwrap.WrapString(nextLines, p.wrapLimit-offset) + wrappedStr = flagStr + "\n" + wrappedUsages + } + appendTabStr := strings.ReplaceAll(wrappedStr, "\n", "\n\t") + + fmt.Fprintf(p.out, appendTabStr+"\n\n") +} + +// writeFlag will output the help flag based +// on the format provided by getFlagFormat to i/o writer +func writeFlag(out io.Writer, f *flag.Flag) { + deprecated := "" + if f.Deprecated != "" { + deprecated = fmt.Sprintf(" (DEPRECATED: %s)", f.Deprecated) + } + fmt.Fprintf(out, getFlagFormat(f), f.Shorthand, f.Name, f.DefValue, f.Usage, deprecated) +} diff --git a/staging/src/k8s.io/kubectl/pkg/util/templates/templater.go b/staging/src/k8s.io/kubectl/pkg/util/templates/templater.go index b55adc6b9ced..3ef297c85c58 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/templates/templater.go +++ b/staging/src/k8s.io/kubectl/pkg/util/templates/templater.go @@ -23,10 +23,10 @@ import ( "text/template" "unicode" - "k8s.io/kubectl/pkg/util/term" - "github.com/spf13/cobra" flag "github.com/spf13/pflag" + + "k8s.io/kubectl/pkg/util/term" ) type FlagExposer interface { @@ -160,7 +160,7 @@ func (t *templater) cmdGroupsString(c *cobra.Command) string { cmds := []string{cmdGroup.Message} for _, cmd := range cmdGroup.Commands { if cmd.IsAvailableCommand() { - cmds = append(cmds, " "+rpad(cmd.Name(), cmd.NamePadding())+" "+cmd.Short) + cmds = append(cmds, " "+rpad(cmd.Name(), cmd.NamePadding())+" "+cmd.Short) } } groups = append(groups, strings.Join(cmds, "\n")) @@ -218,34 +218,40 @@ func (t *templater) usageLine(c *cobra.Command) string { return usage } -func flagsUsages(f *flag.FlagSet) string { - x := new(bytes.Buffer) +// flagsUsages will print out the kubectl help flags +func flagsUsages(f *flag.FlagSet) (string, error) { + flagBuf := new(bytes.Buffer) + wrapLimit, err := term.GetWordWrapperLimit() + if err != nil { + return "", err + } + printer := NewHelpFlagPrinter(flagBuf, wrapLimit) f.VisitAll(func(flag *flag.Flag) { if flag.Hidden { return } - format := "--%s=%s: %s%s\n" - - if flag.Value.Type() == "string" { - format = "--%s='%s': %s%s\n" - } + printer.PrintHelpFlag(flag) + }) - if len(flag.Shorthand) > 0 { - format = " -%s, " + format - } else { - format = " %s " + format - } + return flagBuf.String(), nil +} - deprecated := "" - if flag.Deprecated != "" { - deprecated = fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated) - } +// getFlagFormat will output the flag format +func getFlagFormat(f *flag.Flag) string { + var format string + format = "--%s=%s:\n%s%s" + if f.Value.Type() == "string" { + format = "--%s='%s':\n%s%s" + } - fmt.Fprintf(x, format, flag.Shorthand, flag.Name, flag.DefValue, flag.Usage, deprecated) - }) + if len(f.Shorthand) > 0 { + format = " -%s, " + format + } else { + format = " %s" + format + } - return x.String() + return format } func rpad(s string, padding int) string { diff --git a/staging/src/k8s.io/kubectl/pkg/util/term/term_writer.go b/staging/src/k8s.io/kubectl/pkg/util/term/term_writer.go index 1733764fce0a..ea254bb6861d 100644 --- a/staging/src/k8s.io/kubectl/pkg/util/term/term_writer.go +++ b/staging/src/k8s.io/kubectl/pkg/util/term/term_writer.go @@ -17,11 +17,14 @@ limitations under the License. package term import ( + "errors" "io" "os" wordwrap "github.com/mitchellh/go-wordwrap" "github.com/moby/term" + + "k8s.io/client-go/tools/remotecommand" ) type wordWrapWriter struct { @@ -51,7 +54,21 @@ func NewResponsiveWriter(w io.Writer) io.Writer { if terminalSize == nil { return w } + limit := getTerminalLimitWidth(terminalSize) + + return NewWordWrapWriter(w, limit) +} + +// NewWordWrapWriter is a Writer that supports a limit of characters on every line +// and does auto word wrapping that respects that limit. +func NewWordWrapWriter(w io.Writer, limit uint) io.Writer { + return &wordWrapWriter{ + limit: limit, + writer: w, + } +} +func getTerminalLimitWidth(terminalSize *remotecommand.TerminalSize) uint { var limit uint switch { case terminalSize.Width >= 120: @@ -61,17 +78,20 @@ func NewResponsiveWriter(w io.Writer) io.Writer { case terminalSize.Width >= 80: limit = 80 } - - return NewWordWrapWriter(w, limit) + return limit } -// NewWordWrapWriter is a Writer that supports a limit of characters on every line -// and does auto word wrapping that respects that limit. -func NewWordWrapWriter(w io.Writer, limit uint) io.Writer { - return &wordWrapWriter{ - limit: limit, - writer: w, +func GetWordWrapperLimit() (uint, error) { + stdout := os.Stdout + fd := stdout.Fd() + if !term.IsTerminal(fd) { + return 0, errors.New("file descriptor is not a terminal") + } + terminalSize := GetSize(fd) + if terminalSize == nil { + return 0, errors.New("terminal size is nil") } + return getTerminalLimitWidth(terminalSize), nil } func (w wordWrapWriter) Write(p []byte) (nn int, err error) {