diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/README.md b/README.md index fa9c9c5..e034366 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ A minimal, command-oriented CLI package. ## Features - Very small, simple API. -- Uses standard `flag` package as much as possible. -- No external dependencies. +- Support for POSIX flags. +- Only external dependency is [spf13/pflag](https://github.com/spf13/pflag). - Subcommands. - Auto-generated help. @@ -37,7 +37,7 @@ type cmd struct { verbose bool } -func (c *cmd) Run(fl *flag.FlagSet) { +func (c *cmd) Run(fl *pflag.FlagSet) { if c.verbose { fmt.Println("verbose enabled") } @@ -52,7 +52,7 @@ func (c *cmd) Spec() cli.CommandSpec { } } -func (c *cmd) RegisterFlags(fl *flag.FlagSet) { +func (c *cmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&c.verbose, "v", false, "sets verbose mode") } @@ -87,7 +87,7 @@ import ( type subcmd struct { } -func (c *subcmd) Run(fl *flag.FlagSet) { +func (c *subcmd) Run(fl *pflag.FlagSet) { fmt.Println("subcommand invoked") } @@ -102,7 +102,7 @@ func (c *subcmd) Spec() cli.CommandSpec { type cmd struct { } -func (c *cmd) Run(fl *flag.FlagSet) { +func (c *cmd) Run(fl *pflag.FlagSet) { // This root command has no default action, so print the help. fl.Usage() } diff --git a/command.go b/command.go index d99a365..e829dff 100644 --- a/command.go +++ b/command.go @@ -1,8 +1,9 @@ package cli import ( - "flag" "strings" + + "github.com/spf13/pflag" ) // CommandSpec describes a Command's usage. @@ -38,7 +39,7 @@ type Command interface { // Spec returns metadata about the command. Spec() CommandSpec // Run invokes the command's main routine with parsed flags. - Run(fl *flag.FlagSet) + Run(fl *pflag.FlagSet) } // ParentCommand is an optional interface for commands that have subcommands. @@ -52,5 +53,5 @@ type ParentCommand interface { // FlaggedCommand is an optional interface for commands that have flags. type FlaggedCommand interface { // RegisterFlags lets the command register flags which be sent to Handle. - RegisterFlags(fl *flag.FlagSet) + RegisterFlags(fl *pflag.FlagSet) } diff --git a/examples/simple/main.go b/examples/simple/main.go index c5d1afa..e8c1b7a 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -1,9 +1,9 @@ package main import ( - "flag" "fmt" + "github.com/spf13/pflag" "go.coder.com/cli" ) @@ -11,7 +11,7 @@ type cmd struct { verbose bool } -func (c *cmd) Run(fl *flag.FlagSet) { +func (c *cmd) Run(fl *pflag.FlagSet) { if c.verbose { fmt.Println("verbose enabled") } @@ -26,8 +26,8 @@ func (c *cmd) Spec() cli.CommandSpec { } } -func (c *cmd) RegisterFlags(fl *flag.FlagSet) { - fl.BoolVar(&c.verbose, "v", false, "sets verbose mode") +func (c *cmd) RegisterFlags(fl *pflag.FlagSet) { + fl.BoolVarP(&c.verbose, "verbose", "v", false, "sets verbose mode") } func main() { diff --git a/examples/subcommands/main.go b/examples/subcommands/main.go index a8157cd..3d39bea 100644 --- a/examples/subcommands/main.go +++ b/examples/subcommands/main.go @@ -1,16 +1,16 @@ package main import ( - "flag" "fmt" + "github.com/spf13/pflag" "go.coder.com/cli" ) type subcmd struct { } -func (c *subcmd) Run(fl *flag.FlagSet) { +func (c *subcmd) Run(fl *pflag.FlagSet) { fmt.Println("subcommand invoked") } @@ -25,7 +25,7 @@ func (c *subcmd) Spec() cli.CommandSpec { type cmd struct { } -func (c *cmd) Run(fl *flag.FlagSet) { +func (c *cmd) Run(fl *pflag.FlagSet) { // This root command has no default action, so print the help. fl.Usage() } diff --git a/go.mod b/go.mod index 8c80661..870002d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module go.coder.com/cli go 1.12 + +require github.com/spf13/pflag v1.0.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..edd0bcf --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= diff --git a/help.go b/help.go index 520bb01..3f679c8 100644 --- a/help.go +++ b/help.go @@ -1,13 +1,14 @@ package cli import ( - "flag" "fmt" "io" "strings" "text/tabwriter" "unicode" "unicode/utf8" + + "github.com/spf13/pflag" ) func flagDashes(name string) string { @@ -36,27 +37,18 @@ func fmtDefValue(value interface{}) string { // \xFF is used to escape a \t so tabwriter ignores it. const tabEscape = "\xFF" -func renderFlagHelp(fl *flag.FlagSet, w io.Writer) { - var count int - fl.VisitAll(func(f *flag.Flag) { - if count == 0 { - fmt.Fprintf(w, "\n%v flags:\n", fl.Name()) - } - - count++ - if f.DefValue == "" { - fmt.Fprintf(w, tabEscape+"\t%v%v\t%v\n", flagDashes(f.Name), f.Name, f.Usage) - } else { - fmt.Fprintf(w, tabEscape+"\t%v%v=%v\t%v\n", flagDashes(f.Name), f.Name, fmtDefValue(f.DefValue), f.Usage) - } - }) +func renderFlagHelp(fullName string, fl *pflag.FlagSet, w io.Writer) { + if fl.HasFlags() { + fmt.Fprintf(w, "\n%s flags:\n", fullName) + fmt.Fprint(w, fl.FlagUsages()) + } } // renderHelp generates a command's help page. -func renderHelp(cmd Command, fl *flag.FlagSet, w io.Writer) { +func renderHelp(fullName string, cmd Command, fl *pflag.FlagSet, w io.Writer) { // Render usage and description. fmt.Fprintf(w, "Usage: %v %v\n\n", - fl.Name(), cmd.Spec().Usage, + fullName, cmd.Spec().Usage, ) fmt.Fprintf(w, "%v\n", cmd.Spec().Desc) @@ -64,7 +56,7 @@ func renderHelp(cmd Command, fl *flag.FlagSet, w io.Writer) { defer tw.Flush() // Render flag help. - renderFlagHelp(fl, tw) + renderFlagHelp(fullName, fl, tw) // Render subcommand summaries. pc, ok := cmd.(ParentCommand) diff --git a/run.go b/run.go index c136531..e3a1ddb 100644 --- a/run.go +++ b/run.go @@ -1,17 +1,36 @@ package cli import ( - "flag" "os" + "strings" + + "github.com/spf13/pflag" ) func appendParent(parent string, add string) string { - if parent == "" { - return add + " " - } return parent + add + " " } +// splitArgs tries to split the args between the parent command's flags/args, and the subcommand's +// flags/args. If a subcommand is found, the parent args, subcommand args, and the subcommand will +// be returned. If a subcommand isn't found, the args will be returned as is, the subcommand args +// will be empty, and the subcommand will be nil. +func splitArgs(subCmds []Command, args []string) (cmdArgs, subArgs []string, subCmd Command) { + for i, arg := range args { + if strings.HasPrefix(arg, "-") { + continue + } + + for _, subCommand := range subCmds { + if subCommand.Spec().Name == arg { + return args[:i], args[i+1:], subCommand + } + } + } + + return args, []string{}, nil +} + // Run sets up flags, helps, and executes the command with the provided // arguments. // @@ -20,37 +39,51 @@ func appendParent(parent string, add string) string { // // Use RunRoot if this package is managing the entire CLI. func Run(cmd Command, args []string, parent string) { - fl := flag.NewFlagSet(parent+""+cmd.Spec().Name, flag.ExitOnError) + name := parent + cmd.Spec().Name + fl := pflag.NewFlagSet(name, pflag.ContinueOnError) + // Ensure pflag library doesn't print usage for us automatically, + // we'll override this below. + fl.Usage = func() {} if fc, ok := cmd.(FlaggedCommand); ok { fc.RegisterFlags(fl) } - fl.Usage = func() { - renderHelp(cmd, fl, os.Stderr) - } - if cmd.Spec().RawArgs { // Use `--` to return immediately when parsing the flags. args = append([]string{"--"}, args...) } - _ = fl.Parse(args) - subcommandArg := fl.Arg(0) + var ( + cmdArgs, subArgs []string + subCmd Command + ) + pc, isParentCmd := cmd.(ParentCommand) + if isParentCmd { + cmdArgs, subArgs, subCmd = splitArgs(pc.Subcommands(), args) + if subCmd != nil { + args = cmdArgs + } + } - // Route to subcommand. - if pc, ok := cmd.(ParentCommand); ok && subcommandArg != "" { - for _, subcommand := range pc.Subcommands() { - if subcommand.Spec().Name != subcommandArg { - continue - } + err := fl.Parse(args) + // Reassign the usage now that we've parsed the args + // so that we can render it manually. + fl.Usage = func() { + renderHelp(name, cmd, fl, os.Stderr) + } + if err != nil { + fl.Usage() + os.Exit(2) + } - Run( - subcommand, fl.Args()[1:], - appendParent(parent, cmd.Spec().Name), - ) - return - } + // Route to subcommand. + if isParentCmd && subCmd != nil { + Run( + subCmd, subArgs, + appendParent(parent, cmd.Spec().Name), + ) + return } cmd.Run(fl)