diff --git a/README.md b/README.md index 17d0900583..46e934ee9f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ applications in an expressive way. * [Subcommands](#subcommands) * [Subcommands categories](#subcommands-categories) * [Exit code](#exit-code) + * [Combining short options](#combining-short-options) * [Bash Completion](#bash-completion) + [Enabling](#enabling) + [Distribution](#distribution) @@ -47,7 +48,6 @@ applications in an expressive way. * [Version Flag](#version-flag) + [Customization](#customization-2) + [Full API Example](#full-api-example) - * [Combining short Bool options](#combining-short-bool-options) - [Contribution Guidelines](#contribution-guidelines) @@ -921,6 +921,76 @@ func main() { } ``` +### Combining short options + +Traditional use of options using their shortnames look like this: + +``` +$ cmd -s -o -m "Some message" +``` + +Suppose you want users to be able to combine options with their shortnames. This +can be done using the `UseShortOptionHandling` bool in your app configuration, +or for individual commands by attaching it to the command configuration. For +example: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.UseShortOptionHandling = true + app.Commands = []cli.Command{ + { + Name: "short", + Usage: "complete a task on the list", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "serve, s"}, + cli.BoolFlag{Name: "option, o"}, + cli.StringFlag{Name: "message, m"}, + }, + Action: func(c *cli.Context) error { + fmt.Println("serve:", c.Bool("serve")) + fmt.Println("option:", c.Bool("option")) + fmt.Println("message:", c.String("message")) + return nil + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +If your program has any number of bool flags such as `serve` and `option`, and +optionally one non-bool flag `message`, with the short options of `-s`, `-o`, +and `-m` respectively, setting `UseShortOptionHandling` will also support the +following syntax: + +``` +$ cmd -som "Some message" +``` + +If you enable the `UseShortOptionHandling`, then you must not use any flags that +have a single leading `-` or this will result in failures. For example, +`-option` can no longer be used. Flags with two leading dashes (such as +`--options`) are still valid. + ### Bash Completion You can enable completion commands by setting the `EnableBashCompletion` @@ -1372,6 +1442,7 @@ func main() { cli.Uint64Flag{Name: "bigage"}, } app.EnableBashCompletion = true + app.UseShortOptionHandling = true app.HideHelp = false app.HideVersion = false app.BashComplete = func(c *cli.Context) { @@ -1502,26 +1573,6 @@ func wopAction(c *cli.Context) error { } ``` -### Combining short Bool options - -Traditional use of boolean options using their shortnames look like this: -``` -# cmd foobar -s -o -``` - -Suppose you want users to be able to combine your bool options with their shortname. This -can be done using the **UseShortOptionHandling** bool in your commands. Suppose your program -has a two bool flags such as *serve* and *option* with the short options of *-o* and -*-s* respectively. With **UseShortOptionHandling** set to *true*, a user can use a syntax -like: -``` -# cmd foobar -so -``` - -If you enable the **UseShortOptionHandling*, then you must not use any flags that have a single -leading *-* or this will result in failures. For example, **-option** can no longer be used. Flags -with two leading dashes (such as **--options**) are still valid. - ## Contribution Guidelines See [./CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/app.go b/app.go index 9ed492f775..93c123deef 100644 --- a/app.go +++ b/app.go @@ -1,9 +1,9 @@ package cli import ( + "flag" "fmt" "io" - "io/ioutil" "os" "path/filepath" "sort" @@ -94,6 +94,10 @@ type App struct { // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomAppHelpTemplate string + // Boolean to enable short-option handling so user can combine several + // single-character bool arguements into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool didSetup bool } @@ -173,6 +177,14 @@ func (a *App) Setup() { } } +func (a *App) newFlagSet() (*flag.FlagSet, error) { + return flagSet(a.Name, a.Flags) +} + +func (a *App) useShortOptionHandling() bool { + return a.UseShortOptionHandling +} + // Run is the entry point to the cli app. Parses the arguments slice and routes // to the proper flag/args combination func (a *App) Run(arguments []string) (err error) { @@ -186,14 +198,12 @@ func (a *App) Run(arguments []string) (err error) { // always appends the completion flag at the end of the command shellComplete, arguments := checkShellCompleteFlag(a, arguments) - // parse flags - set, err := flagSet(a.Name, a.Flags) + _, err = a.newFlagSet() if err != nil { return err } - set.SetOutput(ioutil.Discard) - err = set.Parse(arguments[1:]) + set, err := parseIter(a, arguments[1:]) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, nil) if nerr != nil { @@ -311,14 +321,12 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } a.Commands = newCmds - // parse flags - set, err := flagSet(a.Name, a.Flags) + _, err = a.newFlagSet() if err != nil { return err } - set.SetOutput(ioutil.Discard) - err = set.Parse(ctx.Args().Tail()) + set, err := parseIter(a, ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, ctx) diff --git a/app_test.go b/app_test.go index 69d1418d4c..6ebd217792 100644 --- a/app_test.go +++ b/app_test.go @@ -551,6 +551,94 @@ func TestApp_VisibleCommands(t *testing.T) { } } +func TestApp_UseShortOptionHandling(t *testing.T) { + var one, two bool + var name string + expected := "expectedName" + + app := NewApp() + app.UseShortOptionHandling = true + app.Flags = []Flag{ + BoolFlag{Name: "one, o"}, + BoolFlag{Name: "two, t"}, + StringFlag{Name: "name, n"}, + } + app.Action = func(c *Context) error { + one = c.Bool("one") + two = c.Bool("two") + name = c.String("name") + return nil + } + + app.Run([]string{"", "-on", expected}) + expect(t, one, true) + expect(t, two, false) + expect(t, name, expected) +} + +func TestApp_UseShortOptionHandlingCommand(t *testing.T) { + var one, two bool + var name string + expected := "expectedName" + + app := NewApp() + app.UseShortOptionHandling = true + command := Command{ + Name: "cmd", + Flags: []Flag{ + BoolFlag{Name: "one, o"}, + BoolFlag{Name: "two, t"}, + StringFlag{Name: "name, n"}, + }, + Action: func(c *Context) error { + one = c.Bool("one") + two = c.Bool("two") + name = c.String("name") + return nil + }, + } + app.Commands = []Command{command} + + app.Run([]string{"", "cmd", "-on", expected}) + expect(t, one, true) + expect(t, two, false) + expect(t, name, expected) +} + +func TestApp_UseShortOptionHandlingSubCommand(t *testing.T) { + var one, two bool + var name string + expected := "expectedName" + + app := NewApp() + app.UseShortOptionHandling = true + command := Command{ + Name: "cmd", + } + subCommand := Command{ + Name: "sub", + Flags: []Flag{ + BoolFlag{Name: "one, o"}, + BoolFlag{Name: "two, t"}, + StringFlag{Name: "name, n"}, + }, + Action: func(c *Context) error { + one = c.Bool("one") + two = c.Bool("two") + name = c.String("name") + return nil + }, + } + command.Subcommands = []Command{subCommand} + app.Commands = []Command{command} + + err := app.Run([]string{"", "cmd", "sub", "-on", expected}) + expect(t, err, nil) + expect(t, one, true) + expect(t, two, false) + expect(t, name, expected) +} + func TestApp_Float64Flag(t *testing.T) { var meters float64 diff --git a/command.go b/command.go index f1ca02d929..c74baeb477 100644 --- a/command.go +++ b/command.go @@ -3,7 +3,6 @@ package cli import ( "flag" "fmt" - "io/ioutil" "sort" "strings" ) @@ -111,6 +110,10 @@ func (c Command) Run(ctx *Context) (err error) { ) } + if ctx.App.UseShortOptionHandling { + c.UseShortOptionHandling = true + } + set, err := c.parseFlags(ctx.Args().Tail()) context := NewContext(ctx.App, set, ctx) @@ -177,13 +180,12 @@ func (c Command) Run(ctx *Context) (err error) { } func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { - set, err := flagSet(c.Name, c.Flags) - if err != nil { - return nil, err - } - set.SetOutput(ioutil.Discard) - if c.SkipFlagParsing { + set, err := c.newFlagSet() + if err != nil { + return nil, err + } + return set, set.Parse(append([]string{"--"}, args...)) } @@ -191,45 +193,8 @@ func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { args = reorderArgs(args) } -PARSE: - err = set.Parse(args) + set, err := parseIter(c, args) if err != nil { - if c.UseShortOptionHandling { - // To enable short-option handling (e.g., "-it" vs "-i -t") - // we have to iteratively catch parsing errors. This way - // we achieve LR parsing without transforming any arguments. - // Otherwise, there is no way we can discriminate combined - // short options from common arguments that should be left - // untouched. - errStr := err.Error() - trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: ") - if errStr == trimmed { - return nil, err - } - // regenerate the initial args with the split short opts - newArgs := Args{} - for i, arg := range args { - if arg != trimmed { - newArgs = append(newArgs, arg) - continue - } - shortOpts := translateShortOptions(set, Args{trimmed}) - if len(shortOpts) == 1 { - return nil, err - } - // add each short option and all remaining arguments - newArgs = append(newArgs, shortOpts...) - newArgs = append(newArgs, args[i+1:]...) - args = newArgs - // now reset the flagset parse again - set, err = flagSet(c.Name, c.Flags) - if err != nil { - return nil, err - } - set.SetOutput(ioutil.Discard) - goto PARSE - } - } return nil, err } @@ -241,6 +206,14 @@ PARSE: return set, nil } +func (c *Command) newFlagSet() (*flag.FlagSet, error) { + return flagSet(c.Name, c.Flags) +} + +func (c *Command) useShortOptionHandling() bool { + return c.UseShortOptionHandling +} + // reorderArgs moves all flags before arguments as this is what flag expects func reorderArgs(args []string) []string { var nonflags, flags []string @@ -271,35 +244,6 @@ func reorderArgs(args []string) []string { return append(flags, nonflags...) } -func translateShortOptions(set *flag.FlagSet, flagArgs Args) []string { - allCharsFlags := func (s string) bool { - for i := range s { - f := set.Lookup(string(s[i])) - if f == nil { - return false - } - } - return true - } - - // separate combined flags - var flagArgsSeparated []string - for _, flagArg := range flagArgs { - if strings.HasPrefix(flagArg, "-") && strings.HasPrefix(flagArg, "--") == false && len(flagArg) > 2 { - if !allCharsFlags(flagArg[1:]) { - flagArgsSeparated = append(flagArgsSeparated, flagArg) - continue - } - for _, flagChar := range flagArg[1:] { - flagArgsSeparated = append(flagArgsSeparated, "-"+string(flagChar)) - } - } else { - flagArgsSeparated = append(flagArgsSeparated, flagArg) - } - } - return flagArgsSeparated -} - // Names returns the names including short names and aliases. func (c Command) Names() []string { names := []string{c.Name} @@ -352,6 +296,7 @@ func (c Command) startApp(ctx *Context) error { app.Email = ctx.App.Email app.Writer = ctx.App.Writer app.ErrWriter = ctx.App.ErrWriter + app.UseShortOptionHandling = ctx.App.UseShortOptionHandling app.categories = CommandCategories{} for _, command := range c.Subcommands { diff --git a/flag.go b/flag.go index d98c808b47..29e6f7632f 100644 --- a/flag.go +++ b/flag.go @@ -105,6 +105,7 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { f.Apply(set) } } + set.SetOutput(ioutil.Discard) return set, nil } diff --git a/parse.go b/parse.go new file mode 100644 index 0000000000..865accf102 --- /dev/null +++ b/parse.go @@ -0,0 +1,80 @@ +package cli + +import ( + "flag" + "strings" +) + +type iterativeParser interface { + newFlagSet() (*flag.FlagSet, error) + useShortOptionHandling() bool +} + +// To enable short-option handling (e.g., "-it" vs "-i -t") we have to +// iteratively catch parsing errors. This way we achieve LR parsing without +// transforming any arguments. Otherwise, there is no way we can discriminate +// combined short options from common arguments that should be left untouched. +func parseIter(ip iterativeParser, args []string) (*flag.FlagSet, error) { + for { + set, err := ip.newFlagSet() + if err != nil { + return nil, err + } + + err = set.Parse(args) + if !ip.useShortOptionHandling() || err == nil { + return set, err + } + + errStr := err.Error() + trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: ") + if errStr == trimmed { + return nil, err + } + + // regenerate the initial args with the split short opts + newArgs := []string{} + for i, arg := range args { + if arg != trimmed { + newArgs = append(newArgs, arg) + continue + } + + shortOpts := splitShortOptions(set, trimmed) + if len(shortOpts) == 1 { + return nil, err + } + + // add each short option and all remaining arguments + newArgs = append(newArgs, shortOpts...) + newArgs = append(newArgs, args[i+1:]...) + args = newArgs + } + } +} + +func splitShortOptions(set *flag.FlagSet, arg string) []string { + shortFlagsExist := func(s string) bool { + for _, c := range s[1:] { + if f := set.Lookup(string(c)); f == nil { + return false + } + } + return true + } + + if !isSplittable(arg) || !shortFlagsExist(arg) { + return []string{arg} + } + + separated := make([]string, 0, len(arg)-1) + for _, flagChar := range arg[1:] { + separated = append(separated, "-"+string(flagChar)) + } + + return separated +} + +func isSplittable(flagArg string) bool { + return strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2 +}