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