Repository: urfave/cli
Pull request #735 (2018-04-14 15:47:49)

Add app-wide support for combining short flags
This change allows an entire application to support short-flag combining, rather than just individual commands. Setting the following will enable on the application's root action, as well as all commands and sub commands:

```go
app := cli.NewApp()
app.UseShortOptionHandling = true
```

I also added a bit of context to the documentation, and cleaned up a couple of related functions.

rliebz: @jszwedko @meatballhat — Let me know if there's anything I can do to help get this PR through.

Patch:
diff --git a/README.md b/README.md
index bd456b1..29ed171 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)
 
 <!-- tocstop -->
@@ -923,6 +923,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:
+
+<!-- {
+  "args": ["short", "&#45;som", "Some message"],
+  "output": "serve: true\noption: true\nmessage: Some message\n"
+} -->
+``` 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 `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`
@@ -1374,6 +1444,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) {
@@ -1504,26 +1575,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 9ed492f..93c123d 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/command.go b/command.go
index f1ca02d..c74baeb 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 5b6db0c..04f463a 100644
--- a/flag.go
+++ b/flag.go
@@ -120,6 +120,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 0000000..865accf
--- /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
+}


Test patch:
diff --git a/app_test.go b/app_test.go
index f7f53b7..48adf5e 100644
--- a/app_test.go
+++ b/app_test.go
@@ -634,6 +634,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