Skip to content

Commit

Permalink
Add app-wide support for combining short flags
Browse files Browse the repository at this point in the history
  • Loading branch information
rliebz committed Apr 14, 2018
1 parent 8e01ec4 commit d08042b
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 56 deletions.
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 @@ -920,6 +920,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 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`
Expand Down Expand Up @@ -1371,6 +1441,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 @@ -1501,26 +1572,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)
72 changes: 39 additions & 33 deletions app.go
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 @@ -138,21 +142,7 @@ func (a *App) Setup() {
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
}

newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds

if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
a.updateCommands()

if !a.HideVersion {
a.appendFlag(VersionFlag)
Expand Down Expand Up @@ -192,8 +182,13 @@ func (a *App) Run(arguments []string) (err error) {
return err
}

arguments = arguments[1:]
if a.UseShortOptionHandling {
arguments = translateShortOptions(arguments)
}

set.SetOutput(ioutil.Discard)
err = set.Parse(arguments[1:])
err = set.Parse(arguments)
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, nil)
if nerr != nil {
Expand Down Expand Up @@ -286,33 +281,23 @@ func (a *App) RunAndExitOnError() {
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
// generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
}

newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
a.updateCommands()
}
a.Commands = newCmds

// parse flags
set, err := flagSet(a.Name, a.Flags)
if err != nil {
return err
}

arguments := ctx.Args().Tail()
if a.UseShortOptionHandling {
arguments = translateShortOptions(arguments)
}

set.SetOutput(ioutil.Discard)
err = set.Parse(ctx.Args().Tail())
err = set.Parse(arguments)
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx)

Expand Down Expand Up @@ -442,6 +427,27 @@ func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags)
}

func (a *App) updateCommands() {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}

newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
if a.UseShortOptionHandling {
c.UseShortOptionHandling = true
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
}

func (a *App) hasFlag(flag Flag) bool {
for _, f := range a.Flags {
if flag == f {
Expand Down
87 changes: 87 additions & 0 deletions app_test.go
Expand Up @@ -551,6 +551,93 @@ 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}

app.Run([]string{"", "cmd", "sub", "-on", expected})
expect(t, one, true)
expect(t, two, false)
expect(t, name, expected)
}

func TestApp_Float64Flag(t *testing.T) {
var meters float64

Expand Down
4 changes: 2 additions & 2 deletions command.go
Expand Up @@ -232,11 +232,11 @@ func reorderArgs(args []string) []string {
return append(flags, nonflags...)
}

// translateShortOptions separates combined flags
func translateShortOptions(flagArgs Args) []string {
// separate combined flags
var flagArgsSeparated []string
for _, flagArg := range flagArgs {
if strings.HasPrefix(flagArg, "-") && strings.HasPrefix(flagArg, "--") == false && len(flagArg) > 2 {
if strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2 {
for _, flagChar := range flagArg[1:] {
flagArgsSeparated = append(flagArgsSeparated, "-"+string(flagChar))
}
Expand Down
1 change: 1 addition & 0 deletions command_test.go
Expand Up @@ -315,5 +315,6 @@ func TestCommandSkipFlagParsing(t *testing.T) {
err := app.Run(c.testArgs)
expect(t, err, c.expectedErr)
expect(t, args, c.expectedArgs)
expect(t, value, "")
}
}

0 comments on commit d08042b

Please sign in to comment.