github.com/vmware/govmomi@v0.51.0/cli/flags/output.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package flags 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "flag" 12 "fmt" 13 "io" 14 "os" 15 "reflect" 16 "strings" 17 "time" 18 19 "github.com/dougm/pretty" 20 21 "github.com/vmware/govmomi/cli" 22 "github.com/vmware/govmomi/task" 23 "github.com/vmware/govmomi/vim25/progress" 24 "github.com/vmware/govmomi/vim25/soap" 25 "github.com/vmware/govmomi/vim25/types" 26 "github.com/vmware/govmomi/vim25/xml" 27 ) 28 29 type OutputWriter interface { 30 Write(io.Writer) error 31 } 32 33 type OutputFlag struct { 34 common 35 36 JSON bool 37 XML bool 38 TTY bool 39 Dump bool 40 Out io.Writer 41 Spec bool 42 43 formatError bool 44 formatIndent bool 45 } 46 47 var outputFlagKey = flagKey("output") 48 49 func NewOutputFlag(ctx context.Context) (*OutputFlag, context.Context) { 50 if v := ctx.Value(outputFlagKey); v != nil { 51 return v.(*OutputFlag), ctx 52 } 53 54 v := &OutputFlag{Out: os.Stdout} 55 ctx = context.WithValue(ctx, outputFlagKey, v) 56 return v, ctx 57 } 58 59 func (flag *OutputFlag) Register(ctx context.Context, f *flag.FlagSet) { 60 flag.RegisterOnce(func() { 61 f.BoolVar(&flag.JSON, "json", false, "Enable JSON output") 62 f.BoolVar(&flag.XML, "xml", false, "Enable XML output") 63 f.BoolVar(&flag.Dump, "dump", false, "Enable Go output") 64 if cli.ShowUnreleased() { 65 f.BoolVar(&flag.Spec, "spec", false, "Output spec without sending request") 66 } 67 // Avoid adding more flags for now.. 68 flag.formatIndent = os.Getenv("GOVC_INDENT") != "false" // Default to indented output 69 flag.formatError = os.Getenv("GOVC_FORMAT_ERROR") != "false" // Default to formatted errors 70 }) 71 } 72 73 func (flag *OutputFlag) Process(ctx context.Context) error { 74 return flag.ProcessOnce(func() error { 75 if !flag.All() { 76 // Assume we have a tty if not outputting JSON 77 flag.TTY = true 78 } 79 80 return nil 81 }) 82 } 83 84 // Log outputs the specified string, prefixed with the current time. 85 // A newline is not automatically added. If the specified string 86 // starts with a '\r', the current line is cleared first. 87 func (flag *OutputFlag) Log(s string) (int, error) { 88 if len(s) > 0 && s[0] == '\r' { 89 flag.Write([]byte{'\r', 033, '[', 'K'}) 90 s = s[1:] 91 } 92 93 return flag.WriteString(time.Now().Format("[02-01-06 15:04:05] ") + s) 94 } 95 96 func (flag *OutputFlag) Write(b []byte) (int, error) { 97 if !flag.TTY { 98 return 0, nil 99 } 100 101 w := flag.Out 102 if w == nil { 103 w = os.Stdout 104 } 105 n, err := w.Write(b) 106 if w == os.Stdout { 107 os.Stdout.Sync() 108 } 109 return n, err 110 } 111 112 func (flag *OutputFlag) WriteString(s string) (int, error) { 113 return flag.Write([]byte(s)) 114 } 115 116 func (flag *OutputFlag) All() bool { 117 return flag.JSON || flag.XML || flag.Dump 118 } 119 120 func dumpValue(val any) any { 121 type dumper interface { 122 Dump() any 123 } 124 125 if d, ok := val.(dumper); ok { 126 return d.Dump() 127 } 128 129 rval := reflect.ValueOf(val) 130 if rval.Type().Kind() != reflect.Ptr { 131 return val 132 } 133 134 rval = rval.Elem() 135 if rval.Type().Kind() == reflect.Struct { 136 f := rval.Field(0) 137 if f.Type().Kind() == reflect.Slice { 138 // common case for the various 'type infoResult' 139 if f.Len() == 1 { 140 return f.Index(0).Interface() 141 } 142 return f.Interface() 143 } 144 145 if rval.NumField() == 1 && rval.Type().Field(0).Anonymous { 146 // common case where govc type wraps govmomi type to implement OutputWriter 147 return f.Interface() 148 } 149 } 150 151 return val 152 } 153 154 type outputAny struct { 155 Value any 156 } 157 158 func (*outputAny) Write(io.Writer) error { 159 return nil 160 } 161 162 func (a *outputAny) Dump() any { 163 return a.Value 164 } 165 166 func (flag *OutputFlag) WriteAny(val any) error { 167 if !flag.All() { 168 flag.XML = true 169 } 170 return flag.WriteResult(&outputAny{val}) 171 } 172 173 func (flag *OutputFlag) WriteResult(result OutputWriter) error { 174 var err error 175 176 switch { 177 case flag.Dump: 178 format := "%#v\n" 179 if flag.formatIndent { 180 format = "%# v\n" 181 } 182 _, err = pretty.Fprintf(flag.Out, format, dumpValue(result)) 183 case flag.JSON: 184 e := json.NewEncoder(flag.Out) 185 if flag.formatIndent { 186 e.SetIndent("", " ") 187 } 188 err = e.Encode(result) 189 case flag.XML: 190 e := xml.NewEncoder(flag.Out) 191 if flag.formatIndent { 192 e.Indent("", " ") 193 } 194 err = e.Encode(dumpValue(result)) 195 if err == nil { 196 fmt.Fprintln(flag.Out) 197 } 198 default: 199 err = result.Write(flag.Out) 200 } 201 202 return err 203 } 204 205 func (flag *OutputFlag) WriteError(err error) bool { 206 if flag.formatError { 207 flag.Out = os.Stderr 208 return flag.WriteResult(&errorOutput{err}) == nil 209 } 210 return false 211 } 212 213 type errorOutput struct { 214 error 215 } 216 217 func (e errorOutput) Write(w io.Writer) error { 218 reason := e.Error() 219 var messages []string 220 var faults []types.LocalizableMessage 221 222 switch err := e.error.(type) { 223 case task.Error: 224 faults = err.LocalizedMethodFault.Fault.GetMethodFault().FaultMessage 225 if err.Description != nil { 226 reason = fmt.Sprintf("%s (%s)", reason, err.Description.Message) 227 } 228 default: 229 if soap.IsSoapFault(err) { 230 detail := soap.ToSoapFault(err).Detail.Fault 231 if f, ok := detail.(types.BaseMethodFault); ok { 232 faults = f.GetMethodFault().FaultMessage 233 } 234 } 235 } 236 237 for _, m := range faults { 238 if m.Message != "" && !strings.HasPrefix(m.Message, "[context]") { 239 messages = append(messages, fmt.Sprintf("%s (%s)", m.Message, m.Key)) 240 } 241 } 242 243 messages = append(messages, reason) 244 245 for _, message := range messages { 246 if _, err := fmt.Fprintf(w, "%s: %s\n", os.Args[0], message); err != nil { 247 return err 248 } 249 } 250 251 return nil 252 } 253 254 func (e errorOutput) Dump() any { 255 if f, ok := e.error.(task.Error); ok { 256 return f.LocalizedMethodFault 257 } 258 if soap.IsSoapFault(e.error) { 259 return soap.ToSoapFault(e.error) 260 } 261 if soap.IsVimFault(e.error) { 262 return soap.ToVimFault(e.error) 263 } 264 return e 265 } 266 267 func (e errorOutput) canEncode() bool { 268 switch e.error.(type) { 269 case task.Error: 270 return true 271 } 272 return soap.IsSoapFault(e.error) || soap.IsVimFault(e.error) 273 } 274 275 // errCannotEncode causes cli.Run to output err.Error() as it would without an error format specified 276 var errCannotEncode = errors.New("cannot encode error") 277 278 func (e errorOutput) MarshalJSON() ([]byte, error) { 279 _, ok := e.error.(json.Marshaler) 280 if ok || e.canEncode() { 281 return json.Marshal(e.error) 282 } 283 return nil, errCannotEncode 284 } 285 286 func (e errorOutput) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error { 287 _, ok := e.error.(xml.Marshaler) 288 if ok || e.canEncode() { 289 return encoder.Encode(e.error) 290 } 291 return errCannotEncode 292 } 293 294 func (flag *OutputFlag) ProgressLogger(prefix string) *progress.ProgressLogger { 295 return progress.NewProgressLogger(flag.Log, prefix) 296 }