Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 1 | // Package cmdline provides a data-driven framework to simplify writing |
| 2 | // command-line programs. It includes built-in support for formatted help. |
| 3 | // |
| 4 | // Commands may be linked together to form a command tree. Since commands may |
| 5 | // be arbitrarily nested within other commands, it's easy to create wrapper |
| 6 | // programs that invoke existing commands. |
| 7 | // |
| 8 | // The syntax for each command-line program is: |
| 9 | // |
| 10 | // command [flags] [subcommand [flags]]* [args] |
| 11 | // |
| 12 | // Each sequence of flags on the command-line is associated with the command |
| 13 | // that immediately precedes them. Global flags registered with the standard |
| 14 | // flags package are allowed anywhere a command-specific flag is allowed. |
| 15 | package cmdline |
| 16 | |
| 17 | import ( |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 18 | "flag" |
| 19 | "fmt" |
| 20 | "io" |
| 21 | "os" |
| 22 | "strings" |
| 23 | ) |
| 24 | |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 25 | // ErrExitCode may be returned by the Run function of a Command to cause the |
| 26 | // program to exit with a specific error code. |
| 27 | type ErrExitCode int |
| 28 | |
| 29 | func (x ErrExitCode) Error() string { |
| 30 | return fmt.Sprintf("exit code %d", x) |
| 31 | } |
| 32 | |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 33 | // ErrUsage is returned to indicate an error in command usage; e.g. unknown |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 34 | // flags, subcommands or args. It corresponds to exit code 1. |
| 35 | const ErrUsage = ErrExitCode(1) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 36 | |
| 37 | // Command represents a single command in a command-line program. A program |
| 38 | // with subcommands is represented as a root Command with children representing |
| 39 | // each subcommand. The command graph must be a tree; each command may either |
| 40 | // have exactly one parent (a sub-command), or no parent (the root), and cycles |
| 41 | // are not allowed. This makes it easier to display the usage for subcommands. |
| 42 | type Command struct { |
| 43 | Name string // Name of the command. |
| 44 | Short string // Short description, shown in help called on parent. |
| 45 | Long string // Long description, shown in help called on itself. |
| 46 | Flags flag.FlagSet // Flags for the command. |
| 47 | ArgsName string // Name of the args, shown in usage line. |
| 48 | ArgsLong string // Long description of the args, shown in help. |
| 49 | |
| 50 | // Children of the command. The framework will match args[0] against each |
| 51 | // child's name, and call Run on the first matching child. |
| 52 | Children []*Command |
| 53 | |
| 54 | // Run is a function that runs cmd with args. If both Children and Run are |
| 55 | // specified, Run will only be called if none of the children match. It is an |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 56 | // error if neither is specified. The special ErrExitCode error may be |
| 57 | // returned to indicate the command should exit with a specific exit code. |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 58 | Run func(cmd *Command, args []string) error |
| 59 | |
| 60 | // parent holds the parent of this Command, or nil if this is the root. |
| 61 | parent *Command |
| 62 | |
Todd Wang | a35aae2 | 2014-10-01 09:55:44 -0700 | [diff] [blame^] | 63 | // stdout and stderr are set through Init. |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 64 | stdout, stderr io.Writer |
| 65 | |
| 66 | // parseFlags holds the merged flags used for parsing. Each command starts |
| 67 | // with its own Flags, and we merge in all global flags. If the same flag is |
| 68 | // specified in both sets, the command's own flag wins. |
| 69 | parseFlags *flag.FlagSet |
| 70 | |
Todd Wang | a35aae2 | 2014-10-01 09:55:44 -0700 | [diff] [blame^] | 71 | // isDefaultHelp indicates whether this is the the default help command |
| 72 | // provided by the framework. |
Todd Wang | fcb72a5 | 2014-10-01 09:53:56 -0700 | [diff] [blame] | 73 | isDefaultHelp bool |
| 74 | |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 75 | // TODO(toddw): If necessary we can add alias support, e.g. for abbreviations. |
| 76 | // Alias map[string]string |
| 77 | } |
| 78 | |
| 79 | // style describes the formatting style for usage descriptions. |
| 80 | type style int |
| 81 | |
| 82 | const ( |
| 83 | styleText style = iota // Default style, good for cmdline output. |
| 84 | styleGoDoc // Style good for godoc processing. |
| 85 | ) |
| 86 | |
| 87 | // String returns the human-readable representation of the style. |
| 88 | func (s *style) String() string { |
| 89 | switch *s { |
| 90 | case styleText: |
| 91 | return "text" |
| 92 | case styleGoDoc: |
| 93 | return "godoc" |
| 94 | default: |
| 95 | panic(fmt.Errorf("Unhandled style %d", *s)) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | // Set implements the flag.Value interface method. |
| 100 | func (s *style) Set(value string) error { |
| 101 | switch value { |
| 102 | case "text": |
| 103 | *s = styleText |
| 104 | case "godoc": |
| 105 | *s = styleGoDoc |
| 106 | default: |
| 107 | return fmt.Errorf("Unknown style %q", value) |
| 108 | } |
| 109 | return nil |
| 110 | } |
| 111 | |
| 112 | // Stdout is where output goes. Typically os.Stdout. |
| 113 | func (cmd *Command) Stdout() io.Writer { |
| 114 | return cmd.stdout |
| 115 | } |
| 116 | |
| 117 | // Stderr is where error messages go. Typically os.Stderr |
| 118 | func (cmd *Command) Stderr() io.Writer { |
| 119 | return cmd.stderr |
| 120 | } |
| 121 | |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 122 | // UsageErrorf prints the error message represented by the printf-style format |
| 123 | // string and args, followed by the usage description of cmd. Returns ErrUsage |
| 124 | // to make it easy to use from within the cmd.Run function. |
| 125 | func (cmd *Command) UsageErrorf(format string, v ...interface{}) error { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 126 | fmt.Fprint(cmd.stderr, "ERROR: ") |
| 127 | fmt.Fprintf(cmd.stderr, format, v...) |
| 128 | fmt.Fprint(cmd.stderr, "\n\n") |
| 129 | cmd.usage(cmd.stderr, styleText, true) |
| 130 | return ErrUsage |
| 131 | } |
| 132 | |
| 133 | // usage prints the usage of cmd to the writer, with the given style. The |
| 134 | // firstCall boolean is set to false when printing usage for multiple commands, |
| 135 | // and is used to avoid printing redundant information (e.g. section headers, |
| 136 | // global flags). |
| 137 | func (cmd *Command) usage(w io.Writer, style style, firstCall bool) { |
| 138 | var names []string |
| 139 | for c := cmd; c != nil; c = c.parent { |
| 140 | names = append([]string{c.Name}, names...) |
| 141 | } |
| 142 | namestr := strings.Join(names, " ") |
| 143 | if !firstCall && style == styleGoDoc { |
| 144 | // Title-case names so that godoc recognizes it as a section header. |
| 145 | fmt.Fprintf(w, "%s\n\n", strings.Title(namestr)) |
| 146 | } |
| 147 | // Long description. |
| 148 | fmt.Fprint(w, strings.Trim(cmd.Long, "\n")) |
| 149 | fmt.Fprintln(w) |
| 150 | // Usage line. |
| 151 | hasFlags := false |
| 152 | cmd.Flags.VisitAll(func(*flag.Flag) { |
| 153 | hasFlags = true |
| 154 | }) |
| 155 | fmt.Fprintf(w, "\nUsage:\n") |
| 156 | nameflags := " " + namestr |
| 157 | if hasFlags { |
| 158 | nameflags += " [flags]" |
| 159 | } |
| 160 | if len(cmd.Children) > 0 { |
| 161 | fmt.Fprintf(w, "%s <command>\n", nameflags) |
| 162 | } |
| 163 | if cmd.Run != nil { |
| 164 | if cmd.ArgsName != "" { |
| 165 | fmt.Fprintf(w, "%s %s\n", nameflags, cmd.ArgsName) |
| 166 | } else { |
| 167 | fmt.Fprintf(w, "%s\n", nameflags) |
| 168 | } |
| 169 | } |
| 170 | if len(cmd.Children) == 0 && cmd.Run == nil { |
| 171 | // This is a specification error. |
| 172 | fmt.Fprintf(w, "%s [ERROR: neither Children nor Run is specified]\n", nameflags) |
| 173 | } |
| 174 | // Commands. |
| 175 | if len(cmd.Children) > 0 { |
| 176 | fmt.Fprintf(w, "\nThe %s commands are:\n", cmd.Name) |
| 177 | for _, child := range cmd.Children { |
Todd Wang | fcb72a5 | 2014-10-01 09:53:56 -0700 | [diff] [blame] | 178 | if !firstCall && child.isDefaultHelp { |
| 179 | continue // don't repeatedly list default help command |
| 180 | } |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 181 | fmt.Fprintf(w, " %-11s %s\n", child.Name, child.Short) |
| 182 | } |
| 183 | } |
| 184 | // Args. |
| 185 | if cmd.Run != nil && cmd.ArgsLong != "" { |
| 186 | fmt.Fprintf(w, "\n") |
| 187 | fmt.Fprint(w, strings.Trim(cmd.ArgsLong, "\n")) |
| 188 | fmt.Fprintf(w, "\n") |
| 189 | } |
| 190 | // Flags. |
| 191 | if hasFlags { |
| 192 | fmt.Fprintf(w, "\nThe %s flags are:\n", cmd.Name) |
| 193 | cmd.Flags.VisitAll(func(f *flag.Flag) { |
| 194 | fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage) |
| 195 | }) |
| 196 | } |
| 197 | // Global flags. |
| 198 | hasGlobalFlags := false |
| 199 | flag.VisitAll(func(*flag.Flag) { |
| 200 | hasGlobalFlags = true |
| 201 | }) |
| 202 | if firstCall && hasGlobalFlags { |
| 203 | fmt.Fprintf(w, "\nThe global flags are:\n") |
| 204 | flag.VisitAll(func(f *flag.Flag) { |
| 205 | fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage) |
| 206 | }) |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // newDefaultHelp creates a new default help command. We need to create new |
| 211 | // instances since the parent for each help command is different. |
| 212 | func newDefaultHelp() *Command { |
| 213 | helpStyle := styleText |
| 214 | help := &Command{ |
| 215 | Name: helpName, |
| 216 | Short: "Display help for commands", |
| 217 | Long: ` |
| 218 | Help displays usage descriptions for this command, or usage descriptions for |
| 219 | sub-commands. |
| 220 | `, |
Todd Wang | fcb72a5 | 2014-10-01 09:53:56 -0700 | [diff] [blame] | 221 | ArgsName: "[command ...]", |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 222 | ArgsLong: ` |
Todd Wang | fcb72a5 | 2014-10-01 09:53:56 -0700 | [diff] [blame] | 223 | [command ...] is an optional sequence of commands to display detailed usage. |
| 224 | The special-case "help ..." recursively displays help for all commands. |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 225 | `, |
| 226 | Run: func(cmd *Command, args []string) error { |
| 227 | // Help applies to its parent - e.g. "foo help" applies to the foo command. |
| 228 | return runHelp(cmd.parent, args, helpStyle) |
| 229 | }, |
Todd Wang | fcb72a5 | 2014-10-01 09:53:56 -0700 | [diff] [blame] | 230 | isDefaultHelp: true, |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 231 | } |
| 232 | help.Flags.Var(&helpStyle, "style", `The formatting style for help output, either "text" or "godoc".`) |
| 233 | return help |
| 234 | } |
| 235 | |
| 236 | const helpName = "help" |
| 237 | |
| 238 | // runHelp runs the "help" command. |
| 239 | func runHelp(cmd *Command, args []string, style style) error { |
| 240 | if len(args) == 0 { |
| 241 | cmd.usage(cmd.stdout, style, true) |
| 242 | return nil |
| 243 | } |
| 244 | if args[0] == "..." { |
| 245 | recursiveHelp(cmd, style, true) |
| 246 | return nil |
| 247 | } |
| 248 | // Find the subcommand to display help. |
| 249 | subName := args[0] |
| 250 | subArgs := args[1:] |
| 251 | for _, child := range cmd.Children { |
| 252 | if child.Name == subName { |
| 253 | return runHelp(child, subArgs, style) |
| 254 | } |
| 255 | } |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 256 | return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, subName) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 257 | } |
| 258 | |
| 259 | // recursiveHelp prints help recursively via DFS from this cmd onward. |
| 260 | func recursiveHelp(cmd *Command, style style, firstCall bool) { |
| 261 | cmd.usage(cmd.stdout, style, firstCall) |
| 262 | switch style { |
| 263 | case styleText: |
| 264 | fmt.Fprintln(cmd.stdout, strings.Repeat("=", 80)) |
| 265 | case styleGoDoc: |
| 266 | fmt.Fprintln(cmd.stdout) |
| 267 | } |
| 268 | for _, child := range cmd.Children { |
Todd Wang | fcb72a5 | 2014-10-01 09:53:56 -0700 | [diff] [blame] | 269 | if !firstCall && child.isDefaultHelp { |
| 270 | continue // don't repeatedly print default help command |
| 271 | } |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 272 | recursiveHelp(child, style, false) |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | // prefixErrorWriter simply wraps a regular io.Writer and adds an "ERROR: " |
| 277 | // prefix if Write is ever called. It's used to ensure errors are clearly |
| 278 | // marked when flag.FlagSet.Parse encounters errors. |
| 279 | type prefixErrorWriter struct { |
| 280 | writer io.Writer |
| 281 | prefixWritten bool |
| 282 | } |
| 283 | |
| 284 | func (p *prefixErrorWriter) Write(b []byte) (int, error) { |
| 285 | if !p.prefixWritten { |
| 286 | io.WriteString(p.writer, "ERROR: ") |
| 287 | p.prefixWritten = true |
| 288 | } |
| 289 | return p.writer.Write(b) |
| 290 | } |
| 291 | |
| 292 | // Init initializes all nodes in the command tree rooted at cmd. Init must be |
| 293 | // called before Execute. |
| 294 | func (cmd *Command) Init(parent *Command, stdout, stderr io.Writer) { |
| 295 | cmd.parent = parent |
| 296 | cmd.stdout = stdout |
| 297 | cmd.stderr = stderr |
| 298 | // Add help command, if it doesn't already exist. |
| 299 | hasHelp := false |
| 300 | for _, child := range cmd.Children { |
| 301 | if child.Name == helpName { |
| 302 | hasHelp = true |
| 303 | break |
| 304 | } |
| 305 | } |
| 306 | if !hasHelp && cmd.Name != helpName && len(cmd.Children) > 0 { |
| 307 | cmd.Children = append(cmd.Children, newDefaultHelp()) |
| 308 | } |
| 309 | // Merge command-specific and global flags into parseFlags. |
| 310 | cmd.parseFlags = flag.NewFlagSet(cmd.Name, flag.ContinueOnError) |
| 311 | cmd.parseFlags.SetOutput(&prefixErrorWriter{writer: stderr}) |
| 312 | cmd.parseFlags.Usage = func() { |
| 313 | cmd.usage(stderr, styleText, true) |
| 314 | } |
| 315 | flagMerger := func(f *flag.Flag) { |
| 316 | if cmd.parseFlags.Lookup(f.Name) == nil { |
| 317 | cmd.parseFlags.Var(f.Value, f.Name, f.Usage) |
| 318 | } |
| 319 | } |
| 320 | cmd.Flags.VisitAll(flagMerger) |
| 321 | flag.VisitAll(flagMerger) |
| 322 | // Call children recursively. |
| 323 | for _, child := range cmd.Children { |
| 324 | child.Init(cmd, stdout, stderr) |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | // Execute the command with the given args. The returned error is ErrUsage if |
| 329 | // there are usage errors, otherwise it is whatever the leaf command returns |
| 330 | // from its Run function. |
| 331 | func (cmd *Command) Execute(args []string) error { |
| 332 | // Parse the merged flags. |
| 333 | if err := cmd.parseFlags.Parse(args); err != nil { |
| 334 | return ErrUsage |
| 335 | } |
| 336 | args = cmd.parseFlags.Args() |
| 337 | // Look for matching children. |
| 338 | if len(args) > 0 { |
| 339 | subName := args[0] |
| 340 | subArgs := args[1:] |
| 341 | for _, child := range cmd.Children { |
| 342 | if child.Name == subName { |
| 343 | return child.Execute(subArgs) |
| 344 | } |
| 345 | } |
| 346 | } |
| 347 | // No matching children, try Run. |
| 348 | if cmd.Run != nil { |
| 349 | if cmd.ArgsName == "" && len(args) > 0 { |
| 350 | if len(cmd.Children) > 0 { |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 351 | return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0]) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 352 | } else { |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 353 | return cmd.UsageErrorf("%s doesn't take any arguments", cmd.Name) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 354 | } |
| 355 | } |
| 356 | return cmd.Run(cmd, args) |
| 357 | } |
| 358 | switch { |
| 359 | case len(cmd.Children) == 0: |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 360 | return cmd.UsageErrorf("%s: neither Children nor Run is specified", cmd.Name) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 361 | case len(args) > 0: |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 362 | return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0]) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 363 | default: |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 364 | return cmd.UsageErrorf("%s: no command specified", cmd.Name) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 365 | } |
| 366 | } |
| 367 | |
| 368 | // Main executes the command tree rooted at cmd, writing output to os.Stdout, |
| 369 | // writing errors to os.Stderr, and getting args from os.Args. We'll call |
| 370 | // os.Exit with a non-zero exit code on errors. It's meant as a simple |
| 371 | // one-liner for the main function of command-line tools. |
| 372 | func (cmd *Command) Main() { |
| 373 | cmd.Init(nil, os.Stdout, os.Stderr) |
| 374 | if err := cmd.Execute(os.Args[1:]); err != nil { |
Todd Wang | a615e4d | 2014-09-29 16:56:05 -0700 | [diff] [blame] | 375 | if code, ok := err.(ErrExitCode); ok { |
| 376 | os.Exit(int(code)) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 377 | } else { |
| 378 | fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) |
| 379 | os.Exit(2) |
| 380 | } |
| 381 | } |
| 382 | } |