blob: 82c032c7d76e86d3c7d457a5cae5b683f0347efa [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
Todd Wanga35aae22014-10-01 09:55:44 -070063 // stdout and stderr are set through Init.
Jiri Simsa5293dcb2014-05-10 09:56:38 -070064 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 Wanga35aae22014-10-01 09:55:44 -070071 // isDefaultHelp indicates whether this is the the default help command
72 // provided by the framework.
Todd Wangfcb72a52014-10-01 09:53:56 -070073 isDefaultHelp bool
74
Jiri Simsa5293dcb2014-05-10 09:56:38 -070075 // 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.
80type style int
81
82const (
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.
88func (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.
100func (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.
113func (cmd *Command) Stdout() io.Writer {
114 return cmd.stdout
115}
116
117// Stderr is where error messages go. Typically os.Stderr
118func (cmd *Command) Stderr() io.Writer {
119 return cmd.stderr
120}
121
Todd Wanga615e4d2014-09-29 16:56:05 -0700122// 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.
125func (cmd *Command) UsageErrorf(format string, v ...interface{}) error {
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700126 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).
137func (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 Wangfcb72a52014-10-01 09:53:56 -0700178 if !firstCall && child.isDefaultHelp {
179 continue // don't repeatedly list default help command
180 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700181 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.
212func newDefaultHelp() *Command {
213 helpStyle := styleText
214 help := &Command{
215 Name: helpName,
216 Short: "Display help for commands",
217 Long: `
218Help displays usage descriptions for this command, or usage descriptions for
219sub-commands.
220`,
Todd Wangfcb72a52014-10-01 09:53:56 -0700221 ArgsName: "[command ...]",
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700222 ArgsLong: `
Todd Wangfcb72a52014-10-01 09:53:56 -0700223[command ...] is an optional sequence of commands to display detailed usage.
224The special-case "help ..." recursively displays help for all commands.
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700225`,
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 Wangfcb72a52014-10-01 09:53:56 -0700230 isDefaultHelp: true,
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700231 }
232 help.Flags.Var(&helpStyle, "style", `The formatting style for help output, either "text" or "godoc".`)
233 return help
234}
235
236const helpName = "help"
237
238// runHelp runs the "help" command.
239func 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 Wanga615e4d2014-09-29 16:56:05 -0700256 return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, subName)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700257}
258
259// recursiveHelp prints help recursively via DFS from this cmd onward.
260func 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 Wangfcb72a52014-10-01 09:53:56 -0700269 if !firstCall && child.isDefaultHelp {
270 continue // don't repeatedly print default help command
271 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700272 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.
279type prefixErrorWriter struct {
280 writer io.Writer
281 prefixWritten bool
282}
283
284func (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.
294func (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.
331func (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 Wanga615e4d2014-09-29 16:56:05 -0700351 return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0])
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700352 } else {
Todd Wanga615e4d2014-09-29 16:56:05 -0700353 return cmd.UsageErrorf("%s doesn't take any arguments", cmd.Name)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700354 }
355 }
356 return cmd.Run(cmd, args)
357 }
358 switch {
359 case len(cmd.Children) == 0:
Todd Wanga615e4d2014-09-29 16:56:05 -0700360 return cmd.UsageErrorf("%s: neither Children nor Run is specified", cmd.Name)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700361 case len(args) > 0:
Todd Wanga615e4d2014-09-29 16:56:05 -0700362 return cmd.UsageErrorf("%s: unknown command %q", cmd.Name, args[0])
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700363 default:
Todd Wanga615e4d2014-09-29 16:56:05 -0700364 return cmd.UsageErrorf("%s: no command specified", cmd.Name)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700365 }
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.
372func (cmd *Command) Main() {
373 cmd.Init(nil, os.Stdout, os.Stderr)
374 if err := cmd.Execute(os.Args[1:]); err != nil {
Todd Wanga615e4d2014-09-29 16:56:05 -0700375 if code, ok := err.(ErrExitCode); ok {
376 os.Exit(int(code))
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700377 } else {
378 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
379 os.Exit(2)
380 }
381 }
382}