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 }