github.com/alecthomas/kong@v0.9.1-0.20240410131203-2ab5733f1179/kong.go (about) 1 package kong 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "reflect" 10 "regexp" 11 "strings" 12 ) 13 14 var ( 15 callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem() 16 ) 17 18 func failField(parent reflect.Value, field reflect.StructField, format string, args ...interface{}) error { 19 name := parent.Type().Name() 20 if name == "" { 21 name = "<anonymous struct>" 22 } 23 return fmt.Errorf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...)) 24 } 25 26 // Must creates a new Parser or panics if there is an error. 27 func Must(ast interface{}, options ...Option) *Kong { 28 k, err := New(ast, options...) 29 if err != nil { 30 panic(err) 31 } 32 return k 33 } 34 35 type usageOnError int 36 37 const ( 38 shortUsage usageOnError = iota + 1 39 fullUsage 40 ) 41 42 // Kong is the main parser type. 43 type Kong struct { 44 // Grammar model. 45 Model *Application 46 47 // Termination function (defaults to os.Exit) 48 Exit func(int) 49 50 Stdout io.Writer 51 Stderr io.Writer 52 53 bindings bindings 54 loader ConfigurationLoader 55 resolvers []Resolver 56 registry *Registry 57 ignoreFields []*regexp.Regexp 58 59 noDefaultHelp bool 60 usageOnError usageOnError 61 help HelpPrinter 62 shortHelp HelpPrinter 63 helpFormatter HelpValueFormatter 64 helpOptions HelpOptions 65 helpFlag *Flag 66 groups []Group 67 vars Vars 68 flagNamer func(string) string 69 70 // Set temporarily by Options. These are applied after build(). 71 postBuildOptions []Option 72 embedded []embedded 73 dynamicCommands []*dynamicCommand 74 } 75 76 // New creates a new Kong parser on grammar. 77 // 78 // See the README (https://github.com/alecthomas/kong) for usage instructions. 79 func New(grammar interface{}, options ...Option) (*Kong, error) { 80 k := &Kong{ 81 Exit: os.Exit, 82 Stdout: os.Stdout, 83 Stderr: os.Stderr, 84 registry: NewRegistry().RegisterDefaults(), 85 vars: Vars{}, 86 bindings: bindings{}, 87 helpFormatter: DefaultHelpValueFormatter, 88 ignoreFields: make([]*regexp.Regexp, 0), 89 flagNamer: func(s string) string { 90 return strings.ToLower(dashedString(s)) 91 }, 92 } 93 94 options = append(options, Bind(k)) 95 96 for _, option := range options { 97 if err := option.Apply(k); err != nil { 98 return nil, err 99 } 100 } 101 102 if k.help == nil { 103 k.help = DefaultHelpPrinter 104 } 105 106 if k.shortHelp == nil { 107 k.shortHelp = DefaultShortHelpPrinter 108 } 109 110 model, err := build(k, grammar) 111 if err != nil { 112 return k, err 113 } 114 model.Name = filepath.Base(os.Args[0]) 115 k.Model = model 116 k.Model.HelpFlag = k.helpFlag 117 118 // Embed any embedded structs. 119 for _, embed := range k.embedded { 120 tag, err := parseTagString(strings.Join(embed.tags, " ")) //nolint:govet 121 if err != nil { 122 return nil, err 123 } 124 tag.Embed = true 125 v := reflect.Indirect(reflect.ValueOf(embed.strct)) 126 node, err := buildNode(k, v, CommandNode, tag, map[string]bool{}) 127 if err != nil { 128 return nil, err 129 } 130 for _, child := range node.Children { 131 child.Parent = k.Model.Node 132 k.Model.Children = append(k.Model.Children, child) 133 } 134 k.Model.Flags = append(k.Model.Flags, node.Flags...) 135 } 136 137 // Synthesise command nodes. 138 for _, dcmd := range k.dynamicCommands { 139 tag, terr := parseTagString(strings.Join(dcmd.tags, " ")) 140 if terr != nil { 141 return nil, terr 142 } 143 tag.Name = dcmd.name 144 tag.Help = dcmd.help 145 tag.Group = dcmd.group 146 tag.Cmd = true 147 v := reflect.Indirect(reflect.ValueOf(dcmd.cmd)) 148 err = buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{ 149 Name: dcmd.name, 150 Type: v.Type(), 151 }, v, tag, dcmd.name, map[string]bool{}) 152 if err != nil { 153 return nil, err 154 } 155 } 156 157 for _, option := range k.postBuildOptions { 158 if err = option.Apply(k); err != nil { 159 return nil, err 160 } 161 } 162 k.postBuildOptions = nil 163 164 if err = k.interpolate(k.Model.Node); err != nil { 165 return nil, err 166 } 167 168 k.bindings.add(k.vars) 169 170 return k, nil 171 } 172 173 type varStack []Vars 174 175 func (v *varStack) head() Vars { return (*v)[len(*v)-1] } 176 func (v *varStack) pop() { *v = (*v)[:len(*v)-1] } 177 func (v *varStack) push(vars Vars) Vars { 178 if len(*v) != 0 { 179 vars = (*v)[len(*v)-1].CloneWith(vars) 180 } 181 *v = append(*v, vars) 182 return vars 183 } 184 185 // Interpolate variables into model. 186 func (k *Kong) interpolate(node *Node) (err error) { 187 stack := varStack{} 188 return Visit(node, func(node Visitable, next Next) error { 189 switch node := node.(type) { 190 case *Node: 191 vars := stack.push(node.Vars()) 192 node.Help, err = interpolate(node.Help, vars, nil) 193 if err != nil { 194 return fmt.Errorf("help for %s: %s", node.Path(), err) 195 } 196 err = next(nil) 197 stack.pop() 198 return err 199 200 case *Value: 201 return next(k.interpolateValue(node, stack.head())) 202 } 203 return next(nil) 204 }) 205 } 206 207 func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { 208 if len(value.Tag.Vars) > 0 { 209 vars = vars.CloneWith(value.Tag.Vars) 210 } 211 if varsContributor, ok := value.Mapper.(VarsContributor); ok { 212 vars = vars.CloneWith(varsContributor.Vars(value)) 213 } 214 215 if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil { 216 return fmt.Errorf("enum for %s: %s", value.Summary(), err) 217 } 218 219 updatedVars := map[string]string{ 220 "default": value.Default, 221 "enum": value.Enum, 222 } 223 if value.Default, err = interpolate(value.Default, vars, nil); err != nil { 224 return fmt.Errorf("default value for %s: %s", value.Summary(), err) 225 } 226 if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil { 227 return fmt.Errorf("enum value for %s: %s", value.Summary(), err) 228 } 229 if value.Flag != nil { 230 for i, env := range value.Flag.Envs { 231 if value.Flag.Envs[i], err = interpolate(env, vars, nil); err != nil { 232 return fmt.Errorf("env value for %s: %s", value.Summary(), err) 233 } 234 } 235 value.Tag.Envs = value.Flag.Envs 236 updatedVars["env"] = "" 237 if len(value.Flag.Envs) != 0 { 238 updatedVars["env"] = value.Flag.Envs[0] 239 } 240 } 241 value.Help, err = interpolate(value.Help, vars, updatedVars) 242 if err != nil { 243 return fmt.Errorf("help for %s: %s", value.Summary(), err) 244 } 245 return nil 246 } 247 248 // Provide additional builtin flags, if any. 249 func (k *Kong) extraFlags() []*Flag { 250 if k.noDefaultHelp { 251 return nil 252 } 253 var helpTarget helpValue 254 value := reflect.ValueOf(&helpTarget).Elem() 255 helpFlag := &Flag{ 256 Short: 'h', 257 Value: &Value{ 258 Name: "help", 259 Help: "Show context-sensitive help.", 260 OrigHelp: "Show context-sensitive help.", 261 Target: value, 262 Tag: &Tag{}, 263 Mapper: k.registry.ForValue(value), 264 DefaultValue: reflect.ValueOf(false), 265 }, 266 } 267 helpFlag.Flag = helpFlag 268 k.helpFlag = helpFlag 269 return []*Flag{helpFlag} 270 } 271 272 // Parse arguments into target. 273 // 274 // The return Context can be used to further inspect the parsed command-line, to format help, to find the 275 // selected command, to run command Run() methods, and so on. See Context and README for more information. 276 // 277 // Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically 278 // invalid one, which will report a normal error). 279 func (k *Kong) Parse(args []string) (ctx *Context, err error) { 280 ctx, err = Trace(k, args) 281 if err != nil { 282 return nil, err 283 } 284 if ctx.Error != nil { 285 return nil, &ParseError{error: ctx.Error, Context: ctx} 286 } 287 if err = k.applyHook(ctx, "BeforeReset"); err != nil { 288 return nil, &ParseError{error: err, Context: ctx} 289 } 290 if err = ctx.Reset(); err != nil { 291 return nil, &ParseError{error: err, Context: ctx} 292 } 293 if err = k.applyHook(ctx, "BeforeResolve"); err != nil { 294 return nil, &ParseError{error: err, Context: ctx} 295 } 296 if err = ctx.Resolve(); err != nil { 297 return nil, &ParseError{error: err, Context: ctx} 298 } 299 if err = k.applyHook(ctx, "BeforeApply"); err != nil { 300 return nil, &ParseError{error: err, Context: ctx} 301 } 302 if _, err = ctx.Apply(); err != nil { 303 return nil, &ParseError{error: err, Context: ctx} 304 } 305 if err = ctx.Validate(); err != nil { 306 return nil, &ParseError{error: err, Context: ctx} 307 } 308 if err = k.applyHook(ctx, "AfterApply"); err != nil { 309 return nil, &ParseError{error: err, Context: ctx} 310 } 311 return ctx, nil 312 } 313 314 func (k *Kong) applyHook(ctx *Context, name string) error { 315 for _, trace := range ctx.Path { 316 var value reflect.Value 317 switch { 318 case trace.App != nil: 319 value = trace.App.Target 320 case trace.Argument != nil: 321 value = trace.Argument.Target 322 case trace.Command != nil: 323 value = trace.Command.Target 324 case trace.Positional != nil: 325 value = trace.Positional.Target 326 case trace.Flag != nil: 327 value = trace.Flag.Value.Target 328 default: 329 panic("unsupported Path") 330 } 331 method := getMethod(value, name) 332 if !method.IsValid() { 333 continue 334 } 335 binds := k.bindings.clone() 336 binds.add(ctx, trace) 337 binds.add(trace.Node().Vars().CloneWith(k.vars)) 338 binds.merge(ctx.bindings) 339 if err := callFunction(method, binds); err != nil { 340 return err 341 } 342 } 343 // Path[0] will always be the app root. 344 return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name) 345 } 346 347 // Call hook on any unset flags with default values. 348 func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error { 349 if node == nil { 350 return nil 351 } 352 return Visit(node, func(n Visitable, next Next) error { 353 node, ok := n.(*Node) 354 if !ok { 355 return next(nil) 356 } 357 binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars)) 358 for _, flag := range node.Flags { 359 if !flag.HasDefault || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { 360 continue 361 } 362 method := getMethod(flag.Target, name) 363 if !method.IsValid() { 364 continue 365 } 366 path := &Path{Flag: flag} 367 if err := callFunction(method, binds.clone().add(path)); err != nil { 368 return next(err) 369 } 370 } 371 return next(nil) 372 }) 373 } 374 375 func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) { 376 lines := strings.Split(fmt.Sprintf(format, args...), "\n") 377 leader := "" 378 for _, l := range leaders { 379 if l == "" { 380 continue 381 } 382 leader += l + ": " 383 } 384 fmt.Fprintf(w, "%s%s\n", leader, lines[0]) 385 for _, line := range lines[1:] { 386 fmt.Fprintf(w, "%*s%s\n", len(leader), " ", line) 387 } 388 } 389 390 // Printf writes a message to Kong.Stdout with the application name prefixed. 391 func (k *Kong) Printf(format string, args ...interface{}) *Kong { 392 formatMultilineMessage(k.Stdout, []string{k.Model.Name}, format, args...) 393 return k 394 } 395 396 // Errorf writes a message to Kong.Stderr with the application name prefixed. 397 func (k *Kong) Errorf(format string, args ...interface{}) *Kong { 398 formatMultilineMessage(k.Stderr, []string{k.Model.Name, "error"}, format, args...) 399 return k 400 } 401 402 // Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status. 403 func (k *Kong) Fatalf(format string, args ...interface{}) { 404 k.Errorf(format, args...) 405 k.Exit(1) 406 } 407 408 // FatalIfErrorf terminates with an error message if err != nil. 409 func (k *Kong) FatalIfErrorf(err error, args ...interface{}) { 410 if err == nil { 411 return 412 } 413 msg := err.Error() 414 if len(args) > 0 { 415 msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint 416 } 417 // Maybe display usage information. 418 var parseErr *ParseError 419 if errors.As(err, &parseErr) { 420 switch k.usageOnError { 421 case fullUsage: 422 _ = k.help(k.helpOptions, parseErr.Context) 423 fmt.Fprintln(k.Stdout) 424 case shortUsage: 425 _ = k.shortHelp(k.helpOptions, parseErr.Context) 426 fmt.Fprintln(k.Stdout) 427 } 428 } 429 k.Fatalf("%s", msg) 430 } 431 432 // LoadConfig from path using the loader configured via Configuration(loader). 433 // 434 // "path" will have ~ and any variables expanded. 435 func (k *Kong) LoadConfig(path string) (Resolver, error) { 436 var err error 437 path = ExpandPath(path) 438 path, err = interpolate(path, k.vars, nil) 439 if err != nil { 440 return nil, err 441 } 442 r, err := os.Open(path) //nolint: gas 443 if err != nil { 444 return nil, err 445 } 446 defer r.Close() 447 448 return k.loader(r) 449 }