Merge pull request #735 from rliebz/combined

Add app-wide support for combining short flags
main
Audrius Butkevicius 5 years ago committed by GitHub
commit 946f918365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)

@ -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)

@ -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

@ -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 {

@ -120,6 +120,7 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
f.Apply(set)
}
}
set.SetOutput(ioutil.Discard)
return set, nil
}

@ -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
}
Loading…
Cancel
Save