github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/output/output.go (about)

     1  // Package output provides structured output for *cobra.Command.
     2  // Formatted output is enabled by setting the --output=[json|yaml] flag.
     3  package output
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"strings"
    12  
    13  	"github.com/spf13/cobra"
    14  	"gopkg.in/yaml.v3"
    15  
    16  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/global"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/dos"
    18  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    19  )
    20  
    21  // Out returns an io.Writer that writes to the OutOrStdout of the current *cobra.Command, or
    22  // if no command is active, to the os.Stdout. If formatted output is requested, the output
    23  // will be delayed until Execute is called.
    24  func Out(ctx context.Context) io.Writer {
    25  	if cmd, ok := ctx.Value(key{}).(*cobra.Command); ok {
    26  		return cmd.OutOrStdout()
    27  	}
    28  	return dos.Stdout(ctx)
    29  }
    30  
    31  // Err returns an io.Writer that writes to the ErrOrStderr of the current *cobra.Command, or
    32  // if no command is active, to the os.Stderr. If formatted output is requested, the output
    33  // will be delayed until Execute is called.
    34  func Err(ctx context.Context) io.Writer {
    35  	if cmd, ok := ctx.Value(key{}).(*cobra.Command); ok {
    36  		return cmd.ErrOrStderr()
    37  	}
    38  	return dos.Stderr(ctx)
    39  }
    40  
    41  // Info is similar to Out, but if formatted output is requested, the output will be discarded.
    42  //
    43  // Info is primarily intended for messages that are not directly related to the command that
    44  // executes, such as messages about starting up daemons or being connected to a context.
    45  func Info(ctx context.Context) io.Writer {
    46  	if cmd, ok := ctx.Value(key{}).(*cobra.Command); ok {
    47  		if _, ok := cmd.OutOrStdout().(*output); ok {
    48  			return io.Discard
    49  		}
    50  		return cmd.OutOrStdout()
    51  	}
    52  	return dos.Stdout(ctx)
    53  }
    54  
    55  // Object sets the object to be marshalled and printed on stdout when formatted output
    56  // is requested using the `--output=<fmt>` flag. Otherwise, this function does nothing.
    57  //
    58  // If override is set to true, then the formatted output will consist solely of the given
    59  // object. There will be no "cmd", "stdout", or "stderr" tags.
    60  //
    61  // The function will panic if data already has been written to the stdout of the command
    62  // or if an Object already has been called.
    63  func Object(ctx context.Context, obj any, override bool) {
    64  	if cmd, ok := ctx.Value(key{}).(*cobra.Command); ok {
    65  		if o, ok := cmd.OutOrStdout().(*output); ok {
    66  			if o.Len() > 0 {
    67  				panic("output.Object cannot be used together with output.Out")
    68  			}
    69  			if o.obj != nil {
    70  				panic("output.Object can only be used once")
    71  			}
    72  
    73  			if o.format == formatJSONStream {
    74  				if err := json.NewEncoder(o.originalStdout).Encode(obj); err != nil {
    75  					panic(err)
    76  				}
    77  			} else {
    78  				o.obj = obj
    79  			}
    80  
    81  			o.override = override
    82  		}
    83  	}
    84  }
    85  
    86  // DefaultYAML is a PersistentPRERunE function that will change the default output
    87  // format to "yaml" for the command that invokes it.
    88  func DefaultYAML(cmd *cobra.Command, _ []string) error {
    89  	fmt, err := validateFlag(cmd)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	rootCmd := cmd
    94  	for {
    95  		p := rootCmd.Parent()
    96  		if p == nil {
    97  			break
    98  		}
    99  		rootCmd = p
   100  	}
   101  	if fmt == formatDefault {
   102  		if err = rootCmd.PersistentFlags().Set(global.FlagOutput, "yaml"); err != nil {
   103  			return err
   104  		}
   105  	}
   106  	return rootCmd.PersistentPreRunE(cmd, cmd.Flags().Args())
   107  }
   108  
   109  // Execute will call ExecuteC on the given command, optionally print all formatted
   110  // output, and return a boolean indicating if formatted output was printed. The
   111  // result of the execution is provided in the second return value.
   112  func Execute(cmd *cobra.Command) (*cobra.Command, bool, error) {
   113  	setFormat(cmd)
   114  	cmd, err := cmd.ExecuteC()
   115  	o, ok := cmd.OutOrStdout().(*output)
   116  	if !ok {
   117  		return cmd, false, err
   118  	}
   119  
   120  	var obj any
   121  	if err == nil && o.override {
   122  		obj = o.obj
   123  	} else {
   124  		response := &object{
   125  			Cmd: cmd.Name(),
   126  		}
   127  		if buf := o.Buffer; buf.Len() > 0 {
   128  			response.Stdout = buf.String()
   129  		} else if o.obj != nil {
   130  			response.Stdout = o.obj
   131  		}
   132  		if buf, ok := cmd.ErrOrStderr().(*bytes.Buffer); ok && buf.Len() > 0 {
   133  			response.Stderr = buf.String()
   134  		}
   135  		if err != nil {
   136  			response.Err = err.Error()
   137  		}
   138  		// don't print out the "zero" object
   139  		if response.hasCmdOnly() {
   140  			return cmd, true, err
   141  		}
   142  		obj = response
   143  	}
   144  	switch o.format {
   145  	case formatJSON:
   146  		if encErr := json.NewEncoder(o.originalStdout).Encode(obj); encErr != nil {
   147  			panic(encErr)
   148  		}
   149  	case formatYAML:
   150  		ym, encErr := yaml.Marshal(obj)
   151  		if encErr == nil {
   152  			_, encErr = o.originalStdout.Write(ym)
   153  		}
   154  		if encErr != nil {
   155  			panic(encErr)
   156  		}
   157  	case formatJSONStream:
   158  	default:
   159  		fmt.Fprintf(o.originalStdout, "%+v", obj)
   160  	}
   161  	return cmd, true, err
   162  }
   163  
   164  // setFormat assigns a cobra.Command.PersistentPreRunE function that all sub commands will inherit. This
   165  // function checks if the global `--output` flag was used, and if so, ensures that formatted output is
   166  // initialized.
   167  func setFormat(cmd *cobra.Command) {
   168  	cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
   169  		fmt, err := validateFlag(cmd)
   170  		if err != nil {
   171  			return err
   172  		}
   173  		if fmt != formatDefault {
   174  			o := output{
   175  				format:         fmt,
   176  				originalStdout: cmd.OutOrStdout(),
   177  			}
   178  			cmd.SetOut(&o)
   179  			cmd.SetErr(&bytes.Buffer{})
   180  			cmd.SilenceErrors = true
   181  			cmd.SilenceUsage = true
   182  		}
   183  		cmd.SetContext(context.WithValue(cmd.Context(), key{}, cmd))
   184  		return nil
   185  	}
   186  }
   187  
   188  // WantsFormatted returns true if the value of the global `--output` flag is set to a valid
   189  // format different from "default".
   190  func WantsFormatted(cmd *cobra.Command) bool {
   191  	f, _ := validateFlag(cmd)
   192  	return f != formatDefault
   193  }
   194  
   195  // WantsStream returns true if the value of the global `--output` flag is set to "json-stream".
   196  func WantsStream(cmd *cobra.Command) bool {
   197  	f, _ := validateFlag(cmd)
   198  	return f == formatJSONStream
   199  }
   200  
   201  func validateFlag(cmd *cobra.Command) (format, error) {
   202  	if of := cmd.Flags().Lookup(global.FlagOutput); of != nil && of.DefValue == "default" {
   203  		fmt := strings.ToLower(of.Value.String())
   204  		switch fmt {
   205  		case "yaml":
   206  			return formatYAML, nil
   207  		case "json":
   208  			return formatJSON, nil
   209  		case "json-stream":
   210  			return formatJSONStream, nil
   211  		case "default":
   212  			return formatDefault, nil
   213  		default:
   214  			return formatDefault, errcat.User.Newf("invalid output format %q", fmt)
   215  		}
   216  	}
   217  	return formatDefault, nil
   218  }
   219  
   220  type (
   221  	format int
   222  	key    struct{}
   223  	output struct {
   224  		bytes.Buffer
   225  		format         format
   226  		obj            any
   227  		override       bool
   228  		originalStdout io.Writer
   229  	}
   230  	object struct {
   231  		Cmd    string `json:"cmd"`
   232  		Stdout any    `json:"stdout,omitempty"`
   233  		Stderr any    `json:"stderr,omitempty"`
   234  		Err    string `json:"err,omitempty"`
   235  	}
   236  )
   237  
   238  const (
   239  	formatDefault = format(iota)
   240  	formatJSON
   241  	formatYAML
   242  	formatJSONStream
   243  )
   244  
   245  func (o *output) Write(data []byte) (int, error) {
   246  	if o.obj != nil {
   247  		panic("Stdout cannot be used together with output.Object")
   248  	}
   249  	return o.Buffer.Write(data)
   250  }
   251  
   252  func (o *object) hasCmdOnly() bool {
   253  	return o.Stdout == nil && o.Stderr == nil && o.Err == ""
   254  }