Merge branch 'master' into lint-fixes
This commit is contained in:
commit
aed704a9d0
93
README.md
93
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", "-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)
|
||||
|
26
app.go
26
app.go
@ -1,9 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -95,6 +95,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
|
||||
}
|
||||
@ -174,6 +178,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) {
|
||||
@ -187,14 +199,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 {
|
||||
@ -312,14 +322,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)
|
||||
|
||||
|
88
app_test.go
88
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
|
||||
|
||||
|
93
command.go
93
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, "--") && 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 {
|
||||
|
1
flag.go
1
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
|
||||
}
|
||||
|
||||
|
80
parse.go
Normal file
80
parse.go
Normal file
@ -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…
Reference in New Issue
Block a user