blob: d6705e6338e721f9356776492267eb18ea1517f4 [file] [log] [blame]
// Package cmdline provides a data-driven framework to simplify writing
// command-line programs. It includes built-in support for formatted help.
//
// Commands may be linked together to form a command tree. Since commands may
// be arbitrarily nested within other commands, it's easy to create wrapper
// programs that invoke existing commands.
//
// The syntax for each command-line program is:
//
// command [flags] [subcommand [flags]]* [args]
//
// Each sequence of flags on the command-line is associated with the command
// that immediately precedes them. Global flags registered with the standard
// flags package are allowed anywhere a command-specific flag is allowed.
package cmdline
import (
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
)
// ErrUsage is returned to indicate an error in command usage; e.g. unknown
// flags, subcommands or args.
var ErrUsage = errors.New("usage error")
// Command represents a single command in a command-line program. A program
// with subcommands is represented as a root Command with children representing
// each subcommand. The command graph must be a tree; each command may either
// have exactly one parent (a sub-command), or no parent (the root), and cycles
// are not allowed. This makes it easier to display the usage for subcommands.
type Command struct {
Name string // Name of the command.
Short string // Short description, shown in help called on parent.
Long string // Long description, shown in help called on itself.
Flags flag.FlagSet // Flags for the command.
ArgsName string // Name of the args, shown in usage line.
ArgsLong string // Long description of the args, shown in help.
// Children of the command. The framework will match args[0] against each
// child's name, and call Run on the first matching child.
Children []*Command
// Run is a function that runs cmd with args. If both Children and Run are
// specified, Run will only be called if none of the children match. It is an
// error if neither is specified.
Run func(cmd *Command, args []string) error
// parent holds the parent of this Command, or nil if this is the root.
parent *Command
// Stdout and stderr are set through Init.
stdout, stderr io.Writer
// parseFlags holds the merged flags used for parsing. Each command starts
// with its own Flags, and we merge in all global flags. If the same flag is
// specified in both sets, the command's own flag wins.
parseFlags *flag.FlagSet
// TODO(toddw): If necessary we can add alias support, e.g. for abbreviations.
// Alias map[string]string
}
// style describes the formatting style for usage descriptions.
type style int
const (
styleText style = iota // Default style, good for cmdline output.
styleGoDoc // Style good for godoc processing.
)
// String returns the human-readable representation of the style.
func (s *style) String() string {
switch *s {
case styleText:
return "text"
case styleGoDoc:
return "godoc"
default:
panic(fmt.Errorf("Unhandled style %d", *s))
}
}
// Set implements the flag.Value interface method.
func (s *style) Set(value string) error {
switch value {
case "text":
*s = styleText
case "godoc":
*s = styleGoDoc
default:
return fmt.Errorf("Unknown style %q", value)
}
return nil
}
// Stdout is where output goes. Typically os.Stdout.
func (cmd *Command) Stdout() io.Writer {
return cmd.stdout
}
// Stderr is where error messages go. Typically os.Stderr
func (cmd *Command) Stderr() io.Writer {
return cmd.stderr
}
// Errorf should be called to signal an invalid usage of the command.
func (cmd *Command) Errorf(format string, v ...interface{}) error {
fmt.Fprint(cmd.stderr, "ERROR: ")
fmt.Fprintf(cmd.stderr, format, v...)
fmt.Fprint(cmd.stderr, "\n\n")
cmd.usage(cmd.stderr, styleText, true)
return ErrUsage
}
// usage prints the usage of cmd to the writer, with the given style. The
// firstCall boolean is set to false when printing usage for multiple commands,
// and is used to avoid printing redundant information (e.g. section headers,
// global flags).
func (cmd *Command) usage(w io.Writer, style style, firstCall bool) {
var names []string
for c := cmd; c != nil; c = c.parent {
names = append([]string{c.Name}, names...)
}
namestr := strings.Join(names, " ")
if !firstCall && style == styleGoDoc {
// Title-case names so that godoc recognizes it as a section header.
fmt.Fprintf(w, "%s\n\n", strings.Title(namestr))
}
// Long description.
fmt.Fprint(w, strings.Trim(cmd.Long, "\n"))
fmt.Fprintln(w)
// Usage line.
hasFlags := false
cmd.Flags.VisitAll(func(*flag.Flag) {
hasFlags = true
})
fmt.Fprintf(w, "\nUsage:\n")
nameflags := " " + namestr
if hasFlags {
nameflags += " [flags]"
}
if len(cmd.Children) > 0 {
fmt.Fprintf(w, "%s <command>\n", nameflags)
}
if cmd.Run != nil {
if cmd.ArgsName != "" {
fmt.Fprintf(w, "%s %s\n", nameflags, cmd.ArgsName)
} else {
fmt.Fprintf(w, "%s\n", nameflags)
}
}
if len(cmd.Children) == 0 && cmd.Run == nil {
// This is a specification error.
fmt.Fprintf(w, "%s [ERROR: neither Children nor Run is specified]\n", nameflags)
}
// Commands.
if len(cmd.Children) > 0 {
fmt.Fprintf(w, "\nThe %s commands are:\n", cmd.Name)
for _, child := range cmd.Children {
fmt.Fprintf(w, " %-11s %s\n", child.Name, child.Short)
}
}
// Args.
if cmd.Run != nil && cmd.ArgsLong != "" {
fmt.Fprintf(w, "\n")
fmt.Fprint(w, strings.Trim(cmd.ArgsLong, "\n"))
fmt.Fprintf(w, "\n")
}
// Flags.
if hasFlags {
fmt.Fprintf(w, "\nThe %s flags are:\n", cmd.Name)
cmd.Flags.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
})
}
// Global flags.
hasGlobalFlags := false
flag.VisitAll(func(*flag.Flag) {
hasGlobalFlags = true
})
if firstCall && hasGlobalFlags {
fmt.Fprintf(w, "\nThe global flags are:\n")
flag.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(w, " -%s=%s: %s\n", f.Name, f.DefValue, f.Usage)
})
}
}
// newDefaultHelp creates a new default help command. We need to create new
// instances since the parent for each help command is different.
func newDefaultHelp() *Command {
helpStyle := styleText
help := &Command{
Name: helpName,
Short: "Display help for commands",
Long: `
Help displays usage descriptions for this command, or usage descriptions for
sub-commands.
`,
ArgsName: "<command>",
ArgsLong: `
<command> is an optional sequence of commands to display detailed per-command
usage. The special-case "help ..." recursively displays help for this command
and all sub-commands.
`,
Run: func(cmd *Command, args []string) error {
// Help applies to its parent - e.g. "foo help" applies to the foo command.
return runHelp(cmd.parent, args, helpStyle)
},
}
help.Flags.Var(&helpStyle, "style", `The formatting style for help output, either "text" or "godoc".`)
return help
}
const helpName = "help"
// runHelp runs the "help" command.
func runHelp(cmd *Command, args []string, style style) error {
if len(args) == 0 {
cmd.usage(cmd.stdout, style, true)
return nil
}
if args[0] == "..." {
recursiveHelp(cmd, style, true)
return nil
}
// Find the subcommand to display help.
subName := args[0]
subArgs := args[1:]
for _, child := range cmd.Children {
if child.Name == subName {
return runHelp(child, subArgs, style)
}
}
return cmd.Errorf("%s: unknown command %q", cmd.Name, subName)
}
// recursiveHelp prints help recursively via DFS from this cmd onward.
func recursiveHelp(cmd *Command, style style, firstCall bool) {
cmd.usage(cmd.stdout, style, firstCall)
switch style {
case styleText:
fmt.Fprintln(cmd.stdout, strings.Repeat("=", 80))
case styleGoDoc:
fmt.Fprintln(cmd.stdout)
}
for _, child := range cmd.Children {
recursiveHelp(child, style, false)
}
}
// prefixErrorWriter simply wraps a regular io.Writer and adds an "ERROR: "
// prefix if Write is ever called. It's used to ensure errors are clearly
// marked when flag.FlagSet.Parse encounters errors.
type prefixErrorWriter struct {
writer io.Writer
prefixWritten bool
}
func (p *prefixErrorWriter) Write(b []byte) (int, error) {
if !p.prefixWritten {
io.WriteString(p.writer, "ERROR: ")
p.prefixWritten = true
}
return p.writer.Write(b)
}
// Init initializes all nodes in the command tree rooted at cmd. Init must be
// called before Execute.
func (cmd *Command) Init(parent *Command, stdout, stderr io.Writer) {
cmd.parent = parent
cmd.stdout = stdout
cmd.stderr = stderr
// Add help command, if it doesn't already exist.
hasHelp := false
for _, child := range cmd.Children {
if child.Name == helpName {
hasHelp = true
break
}
}
if !hasHelp && cmd.Name != helpName && len(cmd.Children) > 0 {
cmd.Children = append(cmd.Children, newDefaultHelp())
}
// Merge command-specific and global flags into parseFlags.
cmd.parseFlags = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
cmd.parseFlags.SetOutput(&prefixErrorWriter{writer: stderr})
cmd.parseFlags.Usage = func() {
cmd.usage(stderr, styleText, true)
}
flagMerger := func(f *flag.Flag) {
if cmd.parseFlags.Lookup(f.Name) == nil {
cmd.parseFlags.Var(f.Value, f.Name, f.Usage)
}
}
cmd.Flags.VisitAll(flagMerger)
flag.VisitAll(flagMerger)
// Call children recursively.
for _, child := range cmd.Children {
child.Init(cmd, stdout, stderr)
}
}
// Execute the command with the given args. The returned error is ErrUsage if
// there are usage errors, otherwise it is whatever the leaf command returns
// from its Run function.
func (cmd *Command) Execute(args []string) error {
// Parse the merged flags.
if err := cmd.parseFlags.Parse(args); err != nil {
return ErrUsage
}
args = cmd.parseFlags.Args()
// Look for matching children.
if len(args) > 0 {
subName := args[0]
subArgs := args[1:]
for _, child := range cmd.Children {
if child.Name == subName {
return child.Execute(subArgs)
}
}
}
// No matching children, try Run.
if cmd.Run != nil {
if cmd.ArgsName == "" && len(args) > 0 {
if len(cmd.Children) > 0 {
return cmd.Errorf("%s: unknown command %q", cmd.Name, args[0])
} else {
return cmd.Errorf("%s doesn't take any arguments", cmd.Name)
}
}
return cmd.Run(cmd, args)
}
switch {
case len(cmd.Children) == 0:
return cmd.Errorf("%s: neither Children nor Run is specified", cmd.Name)
case len(args) > 0:
return cmd.Errorf("%s: unknown command %q", cmd.Name, args[0])
default:
return cmd.Errorf("%s: no command specified", cmd.Name)
}
}
// Main executes the command tree rooted at cmd, writing output to os.Stdout,
// writing errors to os.Stderr, and getting args from os.Args. We'll call
// os.Exit with a non-zero exit code on errors. It's meant as a simple
// one-liner for the main function of command-line tools.
func (cmd *Command) Main() {
cmd.Init(nil, os.Stdout, os.Stderr)
if err := cmd.Execute(os.Args[1:]); err != nil {
if err == ErrUsage {
os.Exit(1)
} else {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
os.Exit(2)
}
}
}