blob: afaf4855f1e8891cae074e79e80b2b912e1c8f8c [file] [log] [blame]
Jiri Simsa5293dcb2014-05-10 09:56:38 -07001// 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.
15package cmdline
16
17import (
Jiri Simsa5293dcb2014-05-10 09:56:38 -070018 "flag"
19 "fmt"
20 "io"
21 "os"
22 "strings"
23)
24
Todd Wanga615e4d2014-09-29 16:56:05 -070025// ErrExitCode may be returned by the Run function of a Command to cause the
26// program to exit with a specific error code.
27type ErrExitCode int
28
29func (x ErrExitCode) Error() string {
30 return fmt.Sprintf("exit code %d", x)
31}
32
Jiri Simsa5293dcb2014-05-10 09:56:38 -070033// ErrUsage is returned to indicate an error in command usage; e.g. unknown
Todd Wanga615e4d2014-09-29 16:56:05 -070034// flags, subcommands or args. It corresponds to exit code 1.
35const ErrUsage = ErrExitCode(1)
Jiri Simsa5293dcb2014-05-10 09:56:38 -070036
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.
42type 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 Wanga615e4d2014-09-29 16:56:05 -070056 // 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 Simsa5293dcb2014-05-10 09:56:38 -070058 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
63 // Stdout and stderr are set through Init.
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 Wangfcb72a52014-10-01 09:53:56 -070071 // Is this the default help command provided by the framework?
72 isDefaultHelp bool
73
Jiri Simsa5293dcb2014-05-10 09:56:38 -070074 // TODO(toddw): If necessary we can add alias support, e.g. for abbreviations.
75 // Alias map[string]string
76}
77
78// style describes the formatting style for usage descriptions.
79type style int
80
81const (
82 styleText style = iota // Default style, good for cmdline output.
83 styleGoDoc // Style good for godoc processing.
84)
85
86// String returns the human-readable representation of the style.
87func (s *style) String() string {
88 switch *s {
89 case styleText:
90 return "text"
91 case styleGoDoc:
92 return "godoc"
93 default:
94 panic(fmt.Errorf("Unhandled style %d", *s))
95 }
96}
97
98// Set implements the flag.Value interface method.
99func (s *style) Set(value string) error {
100 switch value {
101 case "text":
102 *s = styleText
103 case "godoc":
104 *s = styleGoDoc
105 default:
106 return fmt.Errorf("Unknown style %q", value)
107 }
108 return nil
109}
110
111// Stdout is where output goes. Typically os.Stdout.
112func (cmd *Command) Stdout() io.Writer {
113 return cmd.stdout
114}
115
116// Stderr is where error messages go. Typically os.Stderr
117func (cmd *Command) Stderr() io.Writer {
118 return cmd.stderr
119}
120
Todd Wanga615e4d2014-09-29 16:56:05 -0700121// UsageErrorf prints the error message represented by the printf-style format
122// string and args, followed by the usage description of cmd. Returns ErrUsage
123// to make it easy to use from within the cmd.Run function.
124func (cmd *Command) UsageErrorf(format string, v ...interface{}) error {
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700125 fmt.Fprint(cmd.stderr, "ERROR: ")
126 fmt.Fprintf(cmd.stderr, format, v...)
127 fmt.Fprint(cmd.stderr, "\n\n")
128 cmd.usage(cmd.stderr, styleText, true)
129 return ErrUsage
130}
131
132// usage prints the usage of cmd to the writer, with the given style. The
133// firstCall boolean is set to false when printing usage for multiple commands,
134// and is used to avoid printing redundant information (e.g. section headers,
135// global flags).
136func (cmd *Command) usage(w io.Writer, style style, firstCall bool) {
137 var names []string
138 for c := cmd; c != nil; c = c.parent {
139 names = append([]string{c.Name}, names...)
140 }
141 namestr := strings.Join(names, " ")
142 if !firstCall && style == styleGoDoc {
143 // Title-case names so that godoc recognizes it as a section header.
144 fmt.Fprintf(w, "%s\n\n", strings.Title(namestr))
145 }
146 // Long description.
147 fmt.Fprint(w, strings.Trim(cmd.Long, "\n"))
148 fmt.Fprintln(w)
149 // Usage line.
150 hasFlags := false
151 cmd.Flags.VisitAll(func(*flag.Flag) {
152 hasFlags = true
153 })
154 fmt.Fprintf(w, "\nUsage:\n")
155 nameflags := " " + namestr
156 if hasFlags {
157 nameflags += " [flags]"
158 }
159 if len(cmd.Children) > 0 {
160 fmt.Fprintf(w, "%s <command>\n", nameflags)
161 }
162 if cmd.Run != nil {
163 if cmd.ArgsName != "" {
164 fmt.Fprintf(w, "%s %s\n", nameflags, cmd.ArgsName)
165 } else {
166 fmt.Fprintf(w, "%s\n", nameflags)
167 }
168 }
169 if len(cmd.Children) == 0 && cmd.Run == nil {
170 // This is a specification error.
171 fmt.Fprintf(w, "%s [ERROR: neither Children nor Run is specified]\n", nameflags)
172 }
173 // Commands.
174 if len(cmd.Children) > 0 {
175 fmt.Fprintf(w, "\nThe %s commands are:\n", cmd.Name)
176 for _, child := range cmd.Children {
Todd Wangfcb72a52014-10-01 09:53:56 -0700177 if !firstCall && child.isDefaultHelp {
178 continue // don't repeatedly list default help command
179 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700180 fmt.Fprintf(w, " %-11s %s\n", child.Name, child.Short)
181 }
182 }
183 // Args.
184 if cmd.Run != nil && cmd.ArgsLong != "" {
185 fmt.Fprintf(w, "\n")
186 fmt.Fprint(w, strings.Trim(cmd.ArgsLong, "\n"))
187 fmt.Fprintf(w, "\n")
188 }
189 // Flags.
190 if hasFlags {
191 fmt.Fprintf(w, "\nThe %s flags are:\n", cmd.Name)
192 cmd.Flags.VisitAll(func(f *flag.Flag) {
193 fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
194 })
195 }
196 // Global flags.
197 hasGlobalFlags := false
198 flag.VisitAll(func(*flag.Flag) {
199 hasGlobalFlags = true
200 })
201 if firstCall && hasGlobalFlags {
202 fmt.Fprintf(w, "\nThe global flags are:\n")
203 flag.VisitAll(func(f *flag.Flag) {
204 fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
205 })
206 }
207}
208
209// newDefaultHelp creates a new default help command. We need to create new
210// instances since the parent for each help command is different.
211func newDefaultHelp() *Command {
212 helpStyle := styleText
213 help := &Command{
214 Name: helpName,
215 Short: "Display help for commands",
216 Long: `
217Help displays usage descriptions for this command, or usage descriptions for
218sub-commands.
219`,
Todd Wangfcb72a52014-10-01 09:53:56 -0700220 ArgsName: "[command ...]",
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700221 ArgsLong: `
Todd Wangfcb72a52014-10-01 09:53:56 -0700222[command ...] is an optional sequence of commands to display detailed usage.
223The special-case "help ..." recursively displays help for all commands.
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700224`,
225 Run: func(cmd *Command, args []string) error {
226 // Help applies to its parent - e.g. "foo help" applies to the foo command.
227 return runHelp(cmd.parent, args, helpStyle)
228 },
Todd Wangfcb72a52014-10-01 09:53:56 -0700229 isDefaultHelp: true,
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700230 }
231 help.Flags.Var(&helpStyle, "style", `The formatting style for help output, either "text" or "godoc".`)
232 return help
233}
234
235const helpName = "help"
236
237// runHelp runs the "help" command.
238func runHelp(cmd *Command, args []string, style style) error {
239 if len(args) == 0 {
240 cmd.usage(cmd.stdout, style, true)
241 return nil
242 }
243 if args[0] == "..." {
244 recursiveHelp(cmd, style, true)
245 return nil
246 }
247 // Find the subcommand to display help.
248 subName := args[0]
249 subArgs := args[1:]
250 for _, child := range cmd.Children {
251 if child.Name == subName {
252 return runHelp(child, subArgs, style)
253 }
254 }
Todd Wanga615e4d2014-09-29 16:56:05 -0700255 return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, subName)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700256}
257
258// recursiveHelp prints help recursively via DFS from this cmd onward.
259func recursiveHelp(cmd *Command, style style, firstCall bool) {
260 cmd.usage(cmd.stdout, style, firstCall)
261 switch style {
262 case styleText:
263 fmt.Fprintln(cmd.stdout, strings.Repeat("=", 80))
264 case styleGoDoc:
265 fmt.Fprintln(cmd.stdout)
266 }
267 for _, child := range cmd.Children {
Todd Wangfcb72a52014-10-01 09:53:56 -0700268 if !firstCall && child.isDefaultHelp {
269 continue // don't repeatedly print default help command
270 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700271 recursiveHelp(child, style, false)
272 }
273}
274
275// prefixErrorWriter simply wraps a regular io.Writer and adds an "ERROR: "
276// prefix if Write is ever called. It's used to ensure errors are clearly
277// marked when flag.FlagSet.Parse encounters errors.
278type prefixErrorWriter struct {
279 writer io.Writer
280 prefixWritten bool
281}
282
283func (p *prefixErrorWriter) Write(b []byte) (int, error) {
284 if !p.prefixWritten {
285 io.WriteString(p.writer, "ERROR: ")
286 p.prefixWritten = true
287 }
288 return p.writer.Write(b)
289}
290
291// Init initializes all nodes in the command tree rooted at cmd. Init must be
292// called before Execute.
293func (cmd *Command) Init(parent *Command, stdout, stderr io.Writer) {
294 cmd.parent = parent
295 cmd.stdout = stdout
296 cmd.stderr = stderr
297 // Add help command, if it doesn't already exist.
298 hasHelp := false
299 for _, child := range cmd.Children {
300 if child.Name == helpName {
301 hasHelp = true
302 break
303 }
304 }
305 if !hasHelp && cmd.Name != helpName && len(cmd.Children) > 0 {
306 cmd.Children = append(cmd.Children, newDefaultHelp())
307 }
308 // Merge command-specific and global flags into parseFlags.
309 cmd.parseFlags = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
310 cmd.parseFlags.SetOutput(&prefixErrorWriter{writer: stderr})
311 cmd.parseFlags.Usage = func() {
312 cmd.usage(stderr, styleText, true)
313 }
314 flagMerger := func(f *flag.Flag) {
315 if cmd.parseFlags.Lookup(f.Name) == nil {
316 cmd.parseFlags.Var(f.Value, f.Name, f.Usage)
317 }
318 }
319 cmd.Flags.VisitAll(flagMerger)
320 flag.VisitAll(flagMerger)
321 // Call children recursively.
322 for _, child := range cmd.Children {
323 child.Init(cmd, stdout, stderr)
324 }
325}
326
327// Execute the command with the given args. The returned error is ErrUsage if
328// there are usage errors, otherwise it is whatever the leaf command returns
329// from its Run function.
330func (cmd *Command) Execute(args []string) error {
331 // Parse the merged flags.
332 if err := cmd.parseFlags.Parse(args); err != nil {
333 return ErrUsage
334 }
335 args = cmd.parseFlags.Args()
336 // Look for matching children.
337 if len(args) > 0 {
338 subName := args[0]
339 subArgs := args[1:]
340 for _, child := range cmd.Children {
341 if child.Name == subName {
342 return child.Execute(subArgs)
343 }
344 }
345 }
346 // No matching children, try Run.
347 if cmd.Run != nil {
348 if cmd.ArgsName == "" && len(args) > 0 {
349 if len(cmd.Children) > 0 {
Todd Wanga615e4d2014-09-29 16:56:05 -0700350 return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0])
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700351 } else {
Todd Wanga615e4d2014-09-29 16:56:05 -0700352 return cmd.UsageErrorf("%s doesn't take any arguments", cmd.Name)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700353 }
354 }
355 return cmd.Run(cmd, args)
356 }
357 switch {
358 case len(cmd.Children) == 0:
Todd Wanga615e4d2014-09-29 16:56:05 -0700359 return cmd.UsageErrorf("%s: neither Children nor Run is specified", cmd.Name)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700360 case len(args) > 0:
Todd Wanga615e4d2014-09-29 16:56:05 -0700361 return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0])
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700362 default:
Todd Wanga615e4d2014-09-29 16:56:05 -0700363 return cmd.UsageErrorf("%s: no command specified", cmd.Name)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700364 }
365}
366
367// Main executes the command tree rooted at cmd, writing output to os.Stdout,
368// writing errors to os.Stderr, and getting args from os.Args. We'll call
369// os.Exit with a non-zero exit code on errors. It's meant as a simple
370// one-liner for the main function of command-line tools.
371func (cmd *Command) Main() {
372 cmd.Init(nil, os.Stdout, os.Stderr)
373 if err := cmd.Execute(os.Args[1:]); err != nil {
Todd Wanga615e4d2014-09-29 16:56:05 -0700374 if code, ok := err.(ErrExitCode); ok {
375 os.Exit(int(code))
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700376 } else {
377 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
378 os.Exit(2)
379 }
380 }
381}