Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add app-wide support for combining short flags #735

Merged
merged 5 commits into from Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 72 additions & 21 deletions README.md
Expand Up @@ -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)
Expand All @@ -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 -->
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
26 changes: 17 additions & 9 deletions app.go
@@ -1,9 +1,9 @@
package cli

import (
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down
88 changes: 88 additions & 0 deletions app_test.go
Expand Up @@ -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

Expand Down