github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cmdline/cmdline.go (about) 1 // Copyright 2015 The Vanadium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package cmdline implements a data-driven mechanism for writing command-line 6 // programs with built-in support for help. 7 // 8 // Commands are linked together to form a command tree. Since commands may be 9 // arbitrarily nested within other commands, it's easy to create wrapper 10 // programs that invoke existing commands. 11 // 12 // The syntax for each command-line program is: 13 // 14 // command [flags] [subcommand [flags]]* [args] 15 // 16 // Each sequence of flags is associated with the command that immediately 17 // precedes it. Flags registered on flag.CommandLine are considered global 18 // flags, and are allowed anywhere a command-specific flag is allowed. 19 // 20 // Pretty usage documentation is automatically generated, and accessible either 21 // via the standard -h / -help flags from the Go flag package, or a special help 22 // command. The help command is automatically appended to commands that already 23 // have at least one child, and don't already have a "help" child. Commands 24 // that do not have any children will exit with an error if invoked with the 25 // arguments "help ..."; this behavior is relied on when generating recursive 26 // help to distinguish between external subcommands with and without children. 27 // 28 // Pitfalls 29 // 30 // The cmdline package must be in full control of flag parsing. Typically you 31 // call cmdline.Main in your main function, and flag parsing is taken care of. 32 // If a more complicated ordering is required, you can call cmdline.Parse and 33 // then handle any special initialization. 34 // 35 // The problem is that flags registered on the root command must be merged 36 // together with the global flags for the root command to be parsed. If 37 // flag.Parse is called before cmdline.Main or cmdline.Parse, it will fail if 38 // any root command flags are specified on the command line. 39 package cmdline 40 41 import ( 42 "flag" 43 "fmt" 44 "io" 45 "io/ioutil" 46 "os" 47 "os/exec" 48 "path/filepath" 49 "reflect" 50 "sort" 51 "strings" 52 "syscall" 53 54 "github.com/btwiuse/jiri/envvar" 55 _ "github.com/btwiuse/jiri/metadata" // for the -metadata flag 56 "github.com/btwiuse/jiri/timing" 57 ) 58 59 // Command represents a single command in a command-line program. A program 60 // with subcommands is represented as a root Command with children representing 61 // each subcommand. The command graph must be a tree; each command may either 62 // have no parent (the root) or exactly one parent, and cycles are not allowed. 63 type Command struct { 64 Name string // Name of the command. 65 Short string // Short description, shown in help called on parent. 66 Long string // Long description, shown in help called on itself. 67 ArgsName string // Name of the args, shown in usage line. 68 ArgsLong string // Long description of the args, shown in help. 69 70 // Flags defined for this command. When a flag F is defined on a command C, 71 // we allow F to be specified on the command line immediately after C, or 72 // after any descendant of C. This FlagSet is only used to specify the 73 // flags and their associated value variables, it is never parsed and hence 74 // methods on FlagSet that are generally used after parsing cannot be 75 // used on Flags. ParsedFlags should be used instead. 76 Flags flag.FlagSet 77 // ParsedFlags contains the FlagSet created by the Command 78 // implementation and that has had its Parse method called. It 79 // should be used instead of the Flags field for handling methods 80 // that assume Parse has been called (e.g. Parsed, Visit, 81 // NArgs etc). 82 ParsedFlags *flag.FlagSet 83 // DontPropagateFlags indicates whether to prevent the flags defined on this 84 // command and the ancestor commands from being propagated to the descendant 85 // commands. 86 DontPropagateFlags bool 87 // DontInheritFlags indicates whether to stop inheriting the flags from the 88 // ancestor commands. The flags for the ancestor commands will not be 89 // propagated to the child commands as well. 90 DontInheritFlags bool 91 92 // Children of the command. 93 Children []*Command 94 95 // LookPath indicates whether to look for external subcommands in the 96 // directories specified by the PATH environment variable. The compiled-in 97 // children always take precedence; the check for external children only 98 // occurs if none of the compiled-in children match. 99 // 100 // All global flags and flags set on ancestor commands are passed through to 101 // the external child. 102 LookPath bool 103 104 // Runner that runs the command. 105 // Use RunnerFunc to adapt regular functions into Runners. 106 // 107 // At least one of Children or Runner must be specified. If both are 108 // specified, ArgsName and ArgsLong must be empty, meaning the Runner doesn't 109 // take any args. Otherwise there's a possible conflict between child names 110 // and the runner args, and an error is returned from Parse. 111 Runner Runner 112 113 // Topics that provide additional info via the default help command. 114 Topics []Topic 115 } 116 117 // Runner is the interface for running commands. Return ErrExitCode to indicate 118 // the command should exit with a specific exit code. 119 type Runner interface { 120 Run(env *Env, args []string) error 121 } 122 123 // RunnerFunc is an adapter that turns regular functions into Runners. 124 type RunnerFunc func(*Env, []string) error 125 126 // Run implements the Runner interface method by calling f(env, args). 127 func (f RunnerFunc) Run(env *Env, args []string) error { 128 return f(env, args) 129 } 130 131 // Topic represents a help topic that is accessed via the help command. 132 type Topic struct { 133 Name string // Name of the topic. 134 Short string // Short description, shown in help for the command. 135 Long string // Long description, shown in help for this topic. 136 } 137 138 // Main implements the main function for the command tree rooted at root. 139 // 140 // It initializes a new environment from the underlying operating system, parses 141 // os.Args[1:] against the root command, and runs the resulting runner. Calls 142 // os.Exit with an exit code that is 0 for success, or non-zero for errors. 143 // 144 // Most main packages should be implemented as follows: 145 // 146 // var root := &cmdline.Command{...} 147 // 148 // func main() { 149 // cmdline.Main(root) 150 // } 151 func Main(root *Command) { 152 env := EnvFromOS() 153 if env.Timer != nil && len(env.Timer.Intervals) > 0 { 154 env.Timer.Intervals[0].Name = pathName(env.prefix(), []*Command{root}) 155 } 156 err := ParseAndRun(root, env, os.Args[1:]) 157 code := ExitCode(err, env.Stderr) 158 if *flagTime && env.Timer != nil { 159 env.Timer.Finish() 160 p := timing.IntervalPrinter{Zero: env.Timer.Zero} 161 if err := p.Print(env.Stderr, env.Timer.Intervals, env.Timer.Now()); err != nil { 162 code2 := ExitCode(err, env.Stderr) 163 if code == 0 { 164 code = code2 165 } 166 } 167 } 168 os.Exit(code) 169 } 170 171 var flagTime = flag.Bool("time", false, "Dump timing information to stderr before exiting the program.") 172 173 // Parse parses args against the command tree rooted at root down to a leaf 174 // command. A single path through the command tree is traversed, based on the 175 // sub-commands specified in args. Global and command-specific flags are parsed 176 // as the tree is traversed. 177 // 178 // On success returns the runner corresponding to the leaf command, along with 179 // the args to pass to the runner. In addition the env.Usage function is set to 180 // produce a usage message corresponding to the leaf command. 181 // 182 // Most main packages should just call Main. Parse should only be used if 183 // special processing is required after parsing the args, and before the runner 184 // is run. An example: 185 // 186 // var root := &cmdline.Command{...} 187 // 188 // func main() { 189 // env := cmdline.EnvFromOS() 190 // os.Exit(cmdline.ExitCode(parseAndRun(env), env.Stderr)) 191 // } 192 // 193 // func parseAndRun(env *cmdline.Env) error { 194 // runner, args, err := cmdline.Parse(env, root, os.Args[1:]) 195 // if err != nil { 196 // return err 197 // } 198 // // ... perform initialization that might parse flags ... 199 // return runner.Run(env, args) 200 // } 201 // 202 // Parse merges root flags into flag.CommandLine and sets ContinueOnError, so 203 // that subsequent calls to flag.Parsed return true. 204 func Parse(root *Command, env *Env, args []string) (Runner, []string, error) { 205 env.TimerPush("cmdline parse") 206 defer env.TimerPop() 207 if globalFlags == nil { 208 // Initialize our global flags to a cleaned copy. We don't want the merging 209 // in parseFlags to contaminate the global flags, even if Parse is called 210 // multiple times, so we keep a single package-level copy. 211 cleanFlags(flag.CommandLine) 212 globalFlags = copyFlags(flag.CommandLine) 213 } 214 // Set env.Usage to the usage of the root command, in case the parse fails. 215 path := []*Command{root} 216 217 env.Usage = makeHelpRunner(path, env).usageFunc 218 cleanTree(root) 219 if err := checkTreeInvariants(path, env); err != nil { 220 return nil, nil, err 221 } 222 runner, args, err := root.parse(nil, env, args, make(map[string]string)) 223 if err != nil { 224 return nil, nil, err 225 } 226 // Clear envvars that start with "CMDLINE_" when returning a user-specified 227 // runner, to avoid polluting the environment. In particular CMDLINE_PREFIX 228 // and CMDLINE_FIRST_CALL are only meant to be passed to external children, 229 // and shouldn't be propagated through the user's runner. 230 switch runner.(type) { 231 case helpRunner, binaryRunner: 232 // The help and binary runners need the envvars to be set. 233 default: 234 for key, _ := range env.Vars { 235 if strings.HasPrefix(key, "CMDLINE_") { 236 delete(env.Vars, key) 237 if err := os.Unsetenv(key); err != nil { 238 return nil, nil, err 239 } 240 } 241 } 242 } 243 return runner, args, nil 244 } 245 246 var globalFlags *flag.FlagSet 247 248 // ParseAndRun is a convenience that calls Parse, and then calls Run on the 249 // returned runner with the given env and parsed args. 250 func ParseAndRun(root *Command, env *Env, args []string) error { 251 runner, args, err := Parse(root, env, args) 252 if err != nil { 253 return err 254 } 255 env.TimerPush("cmdline run") 256 defer env.TimerPop() 257 return runner.Run(env, args) 258 } 259 260 func trimSpace(s *string) { *s = strings.TrimSpace(*s) } 261 262 func cleanTree(cmd *Command) { 263 trimSpace(&cmd.Name) 264 trimSpace(&cmd.Short) 265 trimSpace(&cmd.Long) 266 trimSpace(&cmd.ArgsName) 267 trimSpace(&cmd.ArgsLong) 268 for tx := range cmd.Topics { 269 trimSpace(&cmd.Topics[tx].Name) 270 trimSpace(&cmd.Topics[tx].Short) 271 trimSpace(&cmd.Topics[tx].Long) 272 } 273 cleanFlags(&cmd.Flags) 274 for _, child := range cmd.Children { 275 cleanTree(child) 276 } 277 } 278 279 func cleanFlags(flags *flag.FlagSet) { 280 flags.VisitAll(func(f *flag.Flag) { 281 trimSpace(&f.Usage) 282 }) 283 } 284 285 func checkTreeInvariants(path []*Command, env *Env) error { 286 cmd, cmdPath := path[len(path)-1], pathName(env.prefix(), path) 287 // Check that the root name is non-empty. 288 if cmdPath == "" { 289 return fmt.Errorf(`CODE INVARIANT BROKEN; FIX YOUR CODE 290 291 Root command name cannot be empty.`) 292 } 293 // Check that the children and topic names are non-empty and unique. 294 seen := make(map[string]bool) 295 checkName := func(name string) error { 296 if name == "" { 297 return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE 298 299 Command and topic names cannot be empty.`, cmdPath) 300 } 301 if seen[name] { 302 return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE 303 304 Each command must have unique children and topic names. 305 Saw %q multiple times.`, cmdPath, name) 306 } 307 seen[name] = true 308 return nil 309 } 310 for _, child := range cmd.Children { 311 if err := checkName(child.Name); err != nil { 312 return err 313 } 314 } 315 for _, topic := range cmd.Topics { 316 if err := checkName(topic.Name); err != nil { 317 return err 318 } 319 } 320 // Check that our Children / Runner invariant is satisfied. At least one must 321 // be specified, and if both are specified then ArgsName and ArgsLong must be 322 // empty, meaning the Runner doesn't take any args. 323 switch hasC, hasR := len(cmd.Children) > 0, cmd.Runner != nil; { 324 case !hasC && !hasR: 325 return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE 326 327 At least one of Children or Runner must be specified.`, cmdPath) 328 case hasC && hasR && (cmd.ArgsName != "" || cmd.ArgsLong != ""): 329 return fmt.Errorf(`%v: CODE INVARIANT BROKEN; FIX YOUR CODE 330 331 Since both Children and Runner are specified, the Runner cannot take args. 332 Otherwise a conflict between child names and runner args is possible.`, cmdPath) 333 } 334 // Check recursively for all children 335 for _, child := range cmd.Children { 336 if err := checkTreeInvariants(append(path, child), env); err != nil { 337 return err 338 } 339 } 340 return nil 341 } 342 343 func pathName(prefix string, path []*Command) string { 344 name := prefix 345 for _, cmd := range path { 346 if name != "" { 347 name += " " 348 } 349 name += cmd.Name 350 } 351 return name 352 } 353 354 func (cmd *Command) parse(path []*Command, env *Env, args []string, setFlags map[string]string) (Runner, []string, error) { 355 path = append(path, cmd) 356 cmdPath := pathName(env.prefix(), path) 357 runHelp := makeHelpRunner(path, env) 358 env.Usage = runHelp.usageFunc 359 // Parse flags and retrieve the args remaining after the parse, as well as the 360 // flags that were set. 361 args, setF, err := parseFlags(path, env, args) 362 switch { 363 case err == flag.ErrHelp: 364 return runHelp, nil, nil 365 case err != nil: 366 return nil, nil, env.UsageErrorf("%s: %v", cmdPath, err) 367 } 368 for key, val := range setF { 369 setFlags[key] = val 370 } 371 // First handle the no-args case. 372 if len(args) == 0 { 373 if cmd.Runner != nil { 374 return cmd.Runner, nil, nil 375 } 376 return nil, nil, env.UsageErrorf("%s: no command specified", cmdPath) 377 } 378 // INVARIANT: len(args) > 0 379 // Look for matching children. 380 subName, subArgs := args[0], args[1:] 381 if len(cmd.Children) > 0 { 382 for _, child := range cmd.Children { 383 if child.Name == subName { 384 if env.CommandName != "" { 385 env.CommandName = env.CommandName + "->" + subName 386 } else { 387 env.CommandName = subName 388 } 389 return child.parse(path, env, subArgs, setFlags) 390 } 391 } 392 // Every non-leaf command gets a default help command. 393 if helpName == subName { 394 env.CommandName = subName 395 return runHelp.newCommand().parse(path, env, subArgs, setFlags) 396 } 397 } 398 if cmd.LookPath { 399 // Look for a matching executable in PATH. 400 if subCmd, _ := env.LookPath(cmd.Name + "-" + subName); subCmd != "" { 401 extArgs := append(flagsAsArgs(setFlags), subArgs...) 402 return binaryRunner{subCmd, cmdPath}, extArgs, nil 403 } 404 } 405 // No matching subcommands, check various error cases. 406 switch { 407 case cmd.Runner == nil: 408 return nil, nil, env.UsageErrorf("%s: unknown command %q", cmdPath, subName) 409 case cmd.ArgsName == "": 410 if len(cmd.Children) > 0 { 411 return nil, nil, env.UsageErrorf("%s: unknown command %q", cmdPath, subName) 412 } 413 return nil, nil, env.UsageErrorf("%s: doesn't take arguments", cmdPath) 414 case reflect.DeepEqual(args, []string{helpName, "..."}): 415 return nil, nil, env.UsageErrorf("%s: unsupported help invocation", cmdPath) 416 } 417 // INVARIANT: 418 // cmd.Runner != nil && len(args) > 0 && 419 // cmd.ArgsName != "" && args != []string{"help", "..."} 420 return cmd.Runner, args, nil 421 } 422 423 // parseFlags parses the flags from args for the command with the given path and 424 // env. Returns the remaining non-flag args and the flags that were set. 425 func parseFlags(path []*Command, env *Env, args []string) ([]string, map[string]string, error) { 426 cmd, isRoot := path[len(path)-1], len(path) == 1 427 // Parse the merged command-specific and global flags. 428 var flags *flag.FlagSet 429 if isRoot { 430 // The root command is special, due to the pitfall described above in the 431 // package doc. Merge into flag.CommandLine and use that for parsing. This 432 // ensures that subsequent calls to flag.Parsed will return true, so the 433 // user can check whether flags have already been parsed. Global flags take 434 // precedence over command flags for the root command. 435 flags = flag.CommandLine 436 mergeFlags(flags, &cmd.Flags) 437 } else { 438 // Command flags take precedence over global flags for non-root commands. 439 flags = pathFlags(path) 440 mergeFlags(flags, globalFlags) 441 } 442 // Silence the many different ways flags.Parse can produce ugly output; we 443 // just want it to return any errors and handle the output ourselves. 444 // 1) Set flag.ContinueOnError so that Parse() doesn't exit or panic. 445 // 2) Discard all output (can't be nil, that means stderr). 446 // 3) Set an empty Usage (can't be nil, that means use the default). 447 flags.Init(cmd.Name, flag.ContinueOnError) 448 flags.SetOutput(ioutil.Discard) 449 flags.Usage = func() {} 450 if isRoot { 451 // If this is the root command, we must remember to undo the above changes 452 // on flag.CommandLine after the parse. We don't know the original settings 453 // of these values, so we just blindly set back to the default values. 454 defer func() { 455 flags.Init(cmd.Name, flag.ExitOnError) 456 flags.SetOutput(nil) 457 flags.Usage = func() { env.Usage(env, env.Stderr) } 458 }() 459 } 460 if err := flags.Parse(args); err != nil { 461 return nil, nil, err 462 } 463 cmd.ParsedFlags = flags 464 env.CommandFlags = make(map[string]string) 465 flags.Visit(func(f *flag.Flag) { 466 val := f.Value.String() 467 env.CommandFlags[f.Name] = val 468 }) 469 return flags.Args(), extractSetFlags(flags), nil 470 } 471 472 func mergeFlags(dst, src *flag.FlagSet) { 473 src.VisitAll(func(f *flag.Flag) { 474 // If there is a collision in flag names, the existing flag in dst wins. 475 // Note that flag.Var will panic if it sees a collision. 476 if dst.Lookup(f.Name) == nil { 477 dst.Var(f.Value, f.Name, f.Usage) 478 dst.Lookup(f.Name).DefValue = f.DefValue 479 } 480 }) 481 } 482 483 func copyFlags(flags *flag.FlagSet) *flag.FlagSet { 484 cp := new(flag.FlagSet) 485 mergeFlags(cp, flags) 486 return cp 487 } 488 489 // pathFlags returns the flags that are allowed for the last command in the 490 // path. Flags defined on ancestors are also allowed, except on "help". 491 func pathFlags(path []*Command) *flag.FlagSet { 492 cmd := path[len(path)-1] 493 flags := copyFlags(&cmd.Flags) 494 if cmd.Name != helpName && !cmd.DontInheritFlags { 495 // Walk backwards to merge flags up to the root command. If this takes too 496 // long, we could consider memoizing previous results. 497 for p := len(path) - 2; p >= 0; p-- { 498 if path[p].DontPropagateFlags { 499 break 500 } 501 mergeFlags(flags, &path[p].Flags) 502 if path[p].DontInheritFlags { 503 break 504 } 505 } 506 } 507 return flags 508 } 509 510 func extractSetFlags(flags *flag.FlagSet) map[string]string { 511 // Use FlagSet.Visit rather than VisitAll to restrict to flags that are set. 512 setFlags := make(map[string]string) 513 flags.Visit(func(f *flag.Flag) { 514 setFlags[f.Name] = f.Value.String() 515 }) 516 return setFlags 517 } 518 519 func flagsAsArgs(x map[string]string) []string { 520 var args []string 521 for key, val := range x { 522 args = append(args, "-"+key+"="+val) 523 } 524 sort.Strings(args) 525 return args 526 } 527 528 // subNames returns the sub names of c which should be ignored when using look 529 // path to find external binaries. 530 func (c *Command) subNames(prefix string) map[string]bool { 531 m := map[string]bool{prefix + "help": true} 532 for _, child := range c.Children { 533 m[prefix+child.Name] = true 534 } 535 return m 536 } 537 538 // ErrExitCode may be returned by Runner.Run to cause the program to exit with a 539 // specific error code. 540 type ErrExitCode int 541 542 // Error implements the error interface method. 543 func (x ErrExitCode) Error() string { 544 return fmt.Sprintf("exit code %d", x) 545 } 546 547 // ErrUsage indicates an error in command usage; e.g. unknown flags, subcommands 548 // or args. It corresponds to exit code 2. 549 const ErrUsage = ErrExitCode(2) 550 551 // ExitCode returns the exit code corresponding to err. 552 // 0: if err == nil 553 // code: if err is ErrExitCode(code) 554 // 1: all other errors 555 // Writes the error message for "all other errors" to w, if w is non-nil. 556 func ExitCode(err error, w io.Writer) int { 557 if err == nil { 558 return 0 559 } 560 if code, ok := err.(ErrExitCode); ok { 561 return int(code) 562 } 563 if w != nil { 564 // We don't print "ERROR: exit code N" above to avoid cluttering the output. 565 fmt.Fprintf(w, "ERROR: %v\n", err) 566 } 567 return 1 568 } 569 570 type binaryRunner struct { 571 subCmd string 572 cmdPath string 573 } 574 575 func (b binaryRunner) Run(env *Env, args []string) error { 576 env.TimerPush("run " + filepath.Base(b.subCmd)) 577 defer env.TimerPop() 578 vars := envvar.CopyMap(env.Vars) 579 vars["CMDLINE_PREFIX"] = b.cmdPath 580 cmd := exec.Command(b.subCmd, args...) 581 cmd.Stdin = env.Stdin 582 cmd.Stdout = env.Stdout 583 cmd.Stderr = env.Stderr 584 cmd.Env = envvar.MapToSlice(vars) 585 err := cmd.Run() 586 // Make sure we return the exit code from the binary, if it exited. 587 if exitError, ok := err.(*exec.ExitError); ok { 588 if status, ok := exitError.Sys().(syscall.WaitStatus); ok { 589 return ErrExitCode(status.ExitStatus()) 590 } 591 } 592 return err 593 }