Merge pull request #4 from codegangsta/master

merge
This commit is contained in:
Chris Robinson 2016-02-08 13:17:54 -08:00
commit 9001f738f4
10 changed files with 386 additions and 132 deletions

View File

@ -1,16 +1,19 @@
[![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli) [![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli)
[![Build Status](https://travis-ci.org/codegangsta/cli.png?branch=master)](https://travis-ci.org/codegangsta/cli) [![Build Status](https://travis-ci.org/codegangsta/cli.svg?branch=master)](https://travis-ci.org/codegangsta/cli)
[![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) [![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli)
# cli.go # cli.go
`cli.go` is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. `cli.go` is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.
## Overview ## Overview
Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.
**This is where `cli.go` comes into play.** `cli.go` makes command line programming fun, organized, and expressive! **This is where `cli.go` comes into play.** `cli.go` makes command line programming fun, organized, and expressive!
## Installation ## Installation
Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html).
To install `cli.go`, simply run: To install `cli.go`, simply run:
@ -24,6 +27,7 @@ export PATH=$PATH:$GOPATH/bin
``` ```
## Getting Started ## Getting Started
One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`.
``` go ``` go
@ -123,6 +127,7 @@ GLOBAL OPTIONS
``` ```
### Arguments ### Arguments
You can lookup arguments by calling the `Args` function on `cli.Context`. You can lookup arguments by calling the `Args` function on `cli.Context`.
``` go ``` go
@ -134,7 +139,9 @@ app.Action = func(c *cli.Context) {
``` ```
### Flags ### Flags
Setting and querying flags is simple. Setting and querying flags is simple.
``` go ``` go
... ...
app.Flags = []cli.Flag { app.Flags = []cli.Flag {
@ -159,6 +166,7 @@ app.Action = func(c *cli.Context) {
``` ```
You can also set a destination variable for a flag, to which the content will be scanned. You can also set a destination variable for a flag, to which the content will be scanned.
``` go ``` go
... ...
var language string var language string
@ -233,6 +241,7 @@ app.Flags = []cli.Flag {
### Subcommands ### Subcommands
Subcommands can be defined for a more git-like command line app. Subcommands can be defined for a more git-like command line app.
```go ```go
... ...
app.Commands = []cli.Command{ app.Commands = []cli.Command{
@ -283,6 +292,7 @@ You can enable completion commands by setting the `EnableBashCompletion`
flag on the `App` object. By default, this setting will only auto-complete to flag on the `App` object. By default, this setting will only auto-complete to
show an app's subcommands, but you can write your own completion methods for show an app's subcommands, but you can write your own completion methods for
the App or its subcommands. the App or its subcommands.
```go ```go
... ...
var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"}
@ -334,6 +344,7 @@ Alternatively, you can just document that users should source the generic
to the name of their program (as above). to the name of their program (as above).
## Contribution Guidelines ## Contribution Guidelines
Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch.
If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together.

56
app.go
View File

@ -5,18 +5,21 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"time" "time"
) )
// App is the main structure of a cli application. It is recomended that // App is the main structure of a cli application. It is recomended that
// an app be created with the cli.NewApp() function // an app be created with the cli.NewApp() function
type App struct { type App struct {
// The name of the program. Defaults to os.Args[0] // The name of the program. Defaults to path.Base(os.Args[0])
Name string Name string
// Full name of command for help, defaults to Name // Full name of command for help, defaults to Name
HelpName string HelpName string
// Description of the program. // Description of the program.
Usage string Usage string
// Text to override the USAGE section of help
UsageText string
// Description of the program argument format. // Description of the program argument format.
ArgsUsage string ArgsUsage string
// Version of the program // Version of the program
@ -43,6 +46,10 @@ type App struct {
Action func(context *Context) Action func(context *Context)
// Execute this function if the proper command cannot be found // Execute this function if the proper command cannot be found
CommandNotFound func(context *Context, command string) CommandNotFound func(context *Context, command string)
// Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages.
// This function is able to replace the original error messages.
// If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted.
OnUsageError func(context *Context, err error, isSubcommand bool) error
// Compilation date // Compilation date
Compiled time.Time Compiled time.Time
// List of all authors who contributed // List of all authors who contributed
@ -70,9 +77,10 @@ func compileTime() time.Time {
// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. // Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action.
func NewApp() *App { func NewApp() *App {
return &App{ return &App{
Name: os.Args[0], Name: path.Base(os.Args[0]),
HelpName: os.Args[0], HelpName: path.Base(os.Args[0]),
Usage: "A new cli application", Usage: "A new cli application",
UsageText: "",
Version: "0.0.0", Version: "0.0.0",
BashComplete: DefaultAppComplete, BashComplete: DefaultAppComplete,
Action: helpCommand.Action, Action: helpCommand.Action,
@ -118,25 +126,28 @@ func (a *App) Run(arguments []string) (err error) {
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
err = set.Parse(arguments[1:]) err = set.Parse(arguments[1:])
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, nil)
if nerr != nil { if nerr != nil {
fmt.Fprintln(a.Writer, nerr) fmt.Fprintln(a.Writer, nerr)
context := NewContext(a, set, nil)
ShowAppHelp(context) ShowAppHelp(context)
return nerr return nerr
} }
context := NewContext(a, set, nil)
if err != nil {
fmt.Fprintln(a.Writer, "Incorrect Usage.")
fmt.Fprintln(a.Writer)
ShowAppHelp(context)
return err
}
if checkCompletions(context) { if checkCompletions(context) {
return nil return nil
} }
if err != nil {
if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false)
return err
} else {
fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.")
ShowAppHelp(context)
return err
}
}
if !a.HideHelp && checkHelp(context) { if !a.HideHelp && checkHelp(context) {
ShowAppHelp(context) ShowAppHelp(context)
return nil return nil
@ -149,8 +160,7 @@ func (a *App) Run(arguments []string) (err error) {
if a.After != nil { if a.After != nil {
defer func() { defer func() {
afterErr := a.After(context) if afterErr := a.After(context); afterErr != nil {
if afterErr != nil {
if err != nil { if err != nil {
err = NewMultiError(err, afterErr) err = NewMultiError(err, afterErr)
} else { } else {
@ -161,8 +171,10 @@ func (a *App) Run(arguments []string) (err error) {
} }
if a.Before != nil { if a.Before != nil {
err := a.Before(context) err = a.Before(context)
if err != nil { if err != nil {
fmt.Fprintf(a.Writer, "%v\n\n", err)
ShowAppHelp(context)
return err return err
} }
} }
@ -233,15 +245,19 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
return nerr return nerr
} }
if checkCompletions(context) {
return nil
}
if err != nil { if err != nil {
fmt.Fprintln(a.Writer, "Incorrect Usage.") if a.OnUsageError != nil {
fmt.Fprintln(a.Writer) err = a.OnUsageError(context, err, true)
return err
} else {
fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.")
ShowSubcommandHelp(context) ShowSubcommandHelp(context)
return err return err
} }
if checkCompletions(context) {
return nil
} }
if len(a.Commands) > 0 { if len(a.Commands) > 0 {

View File

@ -5,9 +5,11 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"strings" "strings"
"testing" "testing"
"errors"
) )
func ExampleApp_Run() { func ExampleApp_Run() {
@ -22,6 +24,7 @@ func ExampleApp_Run() {
app.Action = func(c *Context) { app.Action = func(c *Context) {
fmt.Printf("Hello %v\n", c.String("name")) fmt.Printf("Hello %v\n", c.String("name"))
} }
app.UsageText = "app [first_arg] [second_arg]"
app.Author = "Harrison" app.Author = "Harrison"
app.Email = "harrison@lolwut.com" app.Email = "harrison@lolwut.com"
app.Authors = []Author{Author{Name: "Oliver Allen", Email: "oliver@toyshop.com"}} app.Authors = []Author{Author{Name: "Oliver Allen", Email: "oliver@toyshop.com"}}
@ -248,6 +251,24 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
expect(t, args[2], "--notARealFlag") expect(t, args[2], "--notARealFlag")
} }
func TestApp_CommandWithDash(t *testing.T) {
var args []string
app := NewApp()
command := Command{
Name: "cmd",
Action: func(c *Context) {
args = c.Args()
},
}
app.Commands = []Command{command}
app.Run([]string{"", "cmd", "my-arg", "-"})
expect(t, args[0], "my-arg")
expect(t, args[1], "-")
}
func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) {
var args []string var args []string
@ -556,6 +577,7 @@ func TestAppNoHelpFlag(t *testing.T) {
HelpFlag = BoolFlag{} HelpFlag = BoolFlag{}
app := NewApp() app := NewApp()
app.Writer = ioutil.Discard
err := app.Run([]string{"test", "-h"}) err := app.Run([]string{"test", "-h"})
if err != flag.ErrHelp { if err != flag.ErrHelp {
@ -940,6 +962,11 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) {
app := NewApp() app := NewApp()
app.Commands = []Command{ app.Commands = []Command{
Command{ Command{
Subcommands: []Command{
Command{
Name: "sub",
},
},
Name: "bar", Name: "bar",
Before: func(c *Context) error { return fmt.Errorf("before error") }, Before: func(c *Context) error { return fmt.Errorf("before error") },
After: func(c *Context) error { return fmt.Errorf("after error") }, After: func(c *Context) error { return fmt.Errorf("after error") },
@ -958,3 +985,63 @@ func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) {
t.Errorf("expected text of error from After method, but got none in \"%v\"", err) t.Errorf("expected text of error from After method, but got none in \"%v\"", err)
} }
} }
func TestApp_OnUsageError_WithWrongFlagValue(t *testing.T) {
app := NewApp()
app.Flags = []Flag{
IntFlag{Name: "flag"},
}
app.OnUsageError = func(c *Context, err error, isSubcommand bool) error {
if isSubcommand {
t.Errorf("Expect no subcommand")
}
if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") {
t.Errorf("Expect an invalid value error, but got \"%v\"", err)
}
return errors.New("intercepted: " + err.Error())
}
app.Commands = []Command{
Command{
Name: "bar",
},
}
err := app.Run([]string{"foo", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted: invalid value") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}
func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) {
app := NewApp()
app.Flags = []Flag{
IntFlag{Name: "flag"},
}
app.OnUsageError = func(c *Context, err error, isSubcommand bool) error {
if isSubcommand {
t.Errorf("Expect subcommand")
}
if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") {
t.Errorf("Expect an invalid value error, but got \"%v\"", err)
}
return errors.New("intercepted: " + err.Error())
}
app.Commands = []Command{
Command{
Name: "bar",
},
}
err := app.Run([]string{"foo", "--flag=wrong", "bar"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted: invalid value") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
}

View File

@ -16,6 +16,8 @@ type Command struct {
Aliases []string Aliases []string
// A short description of the usage of this command // A short description of the usage of this command
Usage string Usage string
// Custom text to show on USAGE section of help
UsageText string
// A longer explanation of how the command works // A longer explanation of how the command works
Description string Description string
// A short description of the arguments of this command // A short description of the arguments of this command
@ -30,6 +32,10 @@ type Command struct {
After func(context *Context) error After func(context *Context) error
// The function to call when this command is invoked // The function to call when this command is invoked
Action func(context *Context) Action func(context *Context)
// Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages.
// This function is able to replace the original error messages.
// If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted.
OnUsageError func(context *Context, err error) error
// List of child commands // List of child commands
Subcommands []Command Subcommands []Command
// List of flags to parse // List of flags to parse
@ -54,8 +60,8 @@ func (c Command) FullName() string {
} }
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags // Invokes the command given the context, parses ctx.Args() to generate command-specific flags
func (c Command) Run(ctx *Context) error { func (c Command) Run(ctx *Context) (err error) {
if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil { if len(c.Subcommands) > 0 {
return c.startApp(ctx) return c.startApp(ctx)
} }
@ -74,19 +80,22 @@ func (c Command) Run(ctx *Context) error {
set := flagSet(c.Name, c.Flags) set := flagSet(c.Name, c.Flags)
set.SetOutput(ioutil.Discard) set.SetOutput(ioutil.Discard)
if !c.SkipFlagParsing {
firstFlagIndex := -1 firstFlagIndex := -1
terminatorIndex := -1 terminatorIndex := -1
for index, arg := range ctx.Args() { for index, arg := range ctx.Args() {
if arg == "--" { if arg == "--" {
terminatorIndex = index terminatorIndex = index
break break
} else if arg == "-" {
// Do nothing. A dash alone is not really a flag.
continue
} else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 {
firstFlagIndex = index firstFlagIndex = index
} }
} }
var err error if firstFlagIndex > -1 {
if firstFlagIndex > -1 && !c.SkipFlagParsing {
args := ctx.Args() args := ctx.Args()
regularArgs := make([]string, len(args[1:firstFlagIndex])) regularArgs := make([]string, len(args[1:firstFlagIndex]))
copy(regularArgs, args[1:firstFlagIndex]) copy(regularArgs, args[1:firstFlagIndex])
@ -103,13 +112,23 @@ func (c Command) Run(ctx *Context) error {
} else { } else {
err = set.Parse(ctx.Args().Tail()) err = set.Parse(ctx.Args().Tail())
} }
} else {
if c.SkipFlagParsing {
err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...))
}
}
if err != nil { if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(ctx, err)
return err
} else {
fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.")
fmt.Fprintln(ctx.App.Writer) fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name) ShowCommandHelp(ctx, c.Name)
return err return err
} }
}
nerr := normalizeFlags(c.Flags, set) nerr := normalizeFlags(c.Flags, set)
if nerr != nil { if nerr != nil {
@ -127,6 +146,30 @@ func (c Command) Run(ctx *Context) error {
if checkCommandHelp(context, c.Name) { if checkCommandHelp(context, c.Name) {
return nil return nil
} }
if c.After != nil {
defer func() {
afterErr := c.After(context)
if afterErr != nil {
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if c.Before != nil {
err := c.Before(context)
if err != nil {
fmt.Fprintln(ctx.App.Writer, err)
fmt.Fprintln(ctx.App.Writer)
ShowCommandHelp(ctx, c.Name)
return err
}
}
context.Command = c context.Command = c
c.Action(context) c.Action(context)
return nil return nil
@ -160,7 +203,7 @@ func (c Command) startApp(ctx *Context) error {
if c.HelpName == "" { if c.HelpName == "" {
app.HelpName = c.HelpName app.HelpName = c.HelpName
} else { } else {
app.HelpName = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) app.HelpName = app.Name
} }
if c.Description != "" { if c.Description != "" {
@ -199,12 +242,9 @@ func (c Command) startApp(ctx *Context) error {
app.Action = helpSubcommand.Action app.Action = helpSubcommand.Action
} }
var newCmds []Command for index, cc := range app.Commands {
for _, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name}
cc.commandNamePath = []string{c.Name, cc.Name}
newCmds = append(newCmds, cc)
} }
app.Commands = newCmds
return app.RunAsSubcommand(ctx) return app.RunAsSubcommand(ctx)
} }

View File

@ -1,17 +1,33 @@
package cli package cli
import ( import (
"errors"
"flag" "flag"
"fmt"
"io/ioutil"
"strings"
"testing" "testing"
) )
func TestCommandDoNotIgnoreFlags(t *testing.T) { func TestCommandFlagParsing(t *testing.T) {
app := NewApp() cases := []struct {
set := flag.NewFlagSet("test", 0) testArgs []string
test := []string{"blah", "blah", "-break"} skipFlagParsing bool
set.Parse(test) expectedErr error
}{
{[]string{"blah", "blah", "-break"}, false, errors.New("flag provided but not defined: -break")}, // Test normal "not ignoring flags" flow
{[]string{"blah", "blah"}, true, nil}, // Test SkipFlagParsing without any args that look like flags
{[]string{"blah", "-break"}, true, nil}, // Test SkipFlagParsing with random flag arg
{[]string{"blah", "-help"}, true, nil}, // Test SkipFlagParsing with "special" help flag arg
}
c := NewContext(app, set, nil) for _, c := range cases {
app := NewApp()
app.Writer = ioutil.Discard
set := flag.NewFlagSet("test", 0)
set.Parse(c.testArgs)
context := NewContext(app, set, nil)
command := Command{ command := Command{
Name: "test-cmd", Name: "test-cmd",
@ -20,28 +36,62 @@ func TestCommandDoNotIgnoreFlags(t *testing.T) {
Description: "testing", Description: "testing",
Action: func(_ *Context) {}, Action: func(_ *Context) {},
} }
err := command.Run(c)
expect(t, err.Error(), "flag provided but not defined: -break") command.SkipFlagParsing = c.skipFlagParsing
err := command.Run(context)
expect(t, err, c.expectedErr)
expect(t, []string(context.Args()), c.testArgs)
}
} }
func TestCommandIgnoreFlags(t *testing.T) { func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) {
app := NewApp() app := NewApp()
set := flag.NewFlagSet("test", 0) app.Commands = []Command{
test := []string{"blah", "blah", "-break"} Command{
set.Parse(test) Name: "bar",
Before: func(c *Context) error { return fmt.Errorf("before error") },
c := NewContext(app, set, nil) After: func(c *Context) error { return fmt.Errorf("after error") },
},
command := Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(_ *Context) {},
SkipFlagParsing: true,
} }
err := command.Run(c)
expect(t, err, nil) err := app.Run([]string{"foo", "bar"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.Contains(err.Error(), "before error") {
t.Errorf("expected text of error from Before method, but got none in \"%v\"", err)
}
if !strings.Contains(err.Error(), "after error") {
t.Errorf("expected text of error from After method, but got none in \"%v\"", err)
}
}
func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) {
app := NewApp()
app.Commands = []Command{
Command{
Name: "bar",
Flags: []Flag{
IntFlag{Name: "flag"},
},
OnUsageError: func(c *Context, err error) error {
if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") {
t.Errorf("Expect an invalid value error, but got \"%v\"", err)
}
return errors.New("intercepted: " + err.Error())
},
},
}
err := app.Run([]string{"foo", "bar", "--flag=wrong"})
if err == nil {
t.Fatalf("expected to receive error from Run, got none")
}
if !strings.HasPrefix(err.Error(), "intercepted: invalid value") {
t.Errorf("Expect an intercepted error, but got \"%v\"", err)
}
} }

View File

@ -163,7 +163,7 @@ func (c *Context) GlobalIsSet(name string) bool {
// Returns a slice of flag names used in this context. // Returns a slice of flag names used in this context.
func (c *Context) FlagNames() (names []string) { func (c *Context) FlagNames() (names []string) {
for _, flag := range c.Command.Flags { for _, flag := range c.Command.Flags {
name := strings.Split(flag.getName(), ",")[0] name := strings.Split(flag.GetName(), ",")[0]
if name == "help" { if name == "help" {
continue continue
} }
@ -175,7 +175,7 @@ func (c *Context) FlagNames() (names []string) {
// Returns a slice of global flag names used by the app. // Returns a slice of global flag names used by the app.
func (c *Context) GlobalFlagNames() (names []string) { func (c *Context) GlobalFlagNames() (names []string) {
for _, flag := range c.App.Flags { for _, flag := range c.App.Flags {
name := strings.Split(flag.getName(), ",")[0] name := strings.Split(flag.GetName(), ",")[0]
if name == "help" || name == "version" { if name == "help" || name == "version" {
continue continue
} }
@ -360,7 +360,7 @@ func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited[f.Name] = true visited[f.Name] = true
}) })
for _, f := range flags { for _, f := range flags {
parts := strings.Split(f.getName(), ",") parts := strings.Split(f.GetName(), ",")
if len(parts) == 1 { if len(parts) == 1 {
continue continue
} }

59
flag.go
View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"runtime"
) )
// This flag enables bash-completion for all commands and subcommands // This flag enables bash-completion for all commands and subcommands
@ -35,7 +36,7 @@ type Flag interface {
fmt.Stringer fmt.Stringer
// Apply Flag settings to the given flag set // Apply Flag settings to the given flag set
Apply(*flag.FlagSet) Apply(*flag.FlagSet)
getName() string GetName() string
} }
func flagSet(name string, flags []Flag) *flag.FlagSet { func flagSet(name string, flags []Flag) *flag.FlagSet {
@ -73,7 +74,18 @@ type GenericFlag struct {
// help text to the user (uses the String() method of the generic flag to show // help text to the user (uses the String() method of the generic flag to show
// the value) // the value)
func (f GenericFlag) String() string { func (f GenericFlag) String() string {
return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s \"%v\"\t%v", prefixFor(f.Name), f.Name, f.Value, f.Usage)) return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name), f.FormatValueHelp(), f.Usage))
}
func (f GenericFlag) FormatValueHelp() string {
if f.Value == nil {
return ""
}
s := f.Value.String()
if len(s) == 0 {
return ""
}
return fmt.Sprintf("\"%s\"", s)
} }
// Apply takes the flagset and calls Set on the generic flag with the value // Apply takes the flagset and calls Set on the generic flag with the value
@ -95,7 +107,7 @@ func (f GenericFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f GenericFlag) getName() string { func (f GenericFlag) GetName() string {
return f.Name return f.Name
} }
@ -159,7 +171,7 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f StringSliceFlag) getName() string { func (f StringSliceFlag) GetName() string {
return f.Name return f.Name
} }
@ -231,7 +243,7 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f IntSliceFlag) getName() string { func (f IntSliceFlag) GetName() string {
return f.Name return f.Name
} }
@ -273,7 +285,7 @@ func (f BoolFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f BoolFlag) getName() string { func (f BoolFlag) GetName() string {
return f.Name return f.Name
} }
@ -316,7 +328,7 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f BoolTFlag) getName() string { func (f BoolTFlag) GetName() string {
return f.Name return f.Name
} }
@ -331,16 +343,15 @@ type StringFlag struct {
// String returns the usage // String returns the usage
func (f StringFlag) String() string { func (f StringFlag) String() string {
var fmtString string return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name), f.FormatValueHelp(), f.Usage))
fmtString = "%s %v\t%v"
if len(f.Value) > 0 {
fmtString = "%s \"%v\"\t%v"
} else {
fmtString = "%s %v\t%v"
} }
return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)) func (f StringFlag) FormatValueHelp() string {
s := f.Value
if len(s) == 0 {
return ""
}
return fmt.Sprintf("\"%s\"", s)
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -364,7 +375,7 @@ func (f StringFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f StringFlag) getName() string { func (f StringFlag) GetName() string {
return f.Name return f.Name
} }
@ -407,7 +418,7 @@ func (f IntFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f IntFlag) getName() string { func (f IntFlag) GetName() string {
return f.Name return f.Name
} }
@ -450,7 +461,7 @@ func (f DurationFlag) Apply(set *flag.FlagSet) {
}) })
} }
func (f DurationFlag) getName() string { func (f DurationFlag) GetName() string {
return f.Name return f.Name
} }
@ -492,7 +503,7 @@ func (f Float64Flag) Apply(set *flag.FlagSet) {
}) })
} }
func (f Float64Flag) getName() string { func (f Float64Flag) GetName() string {
return f.Name return f.Name
} }
@ -521,7 +532,15 @@ func prefixedNames(fullName string) (prefixed string) {
func withEnvHint(envVar, str string) string { func withEnvHint(envVar, str string) string {
envText := "" envText := ""
if envVar != "" { if envVar != "" {
envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $")) prefix := "$"
suffix := ""
sep := ", $"
if runtime.GOOS == "windows" {
prefix = "%"
suffix = "%"
sep = "%, %"
}
envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix)
} }
return str + envText return str + envText
} }

View File

@ -6,6 +6,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"runtime"
) )
var boolFlagTests = []struct { var boolFlagTests = []struct {
@ -58,8 +59,12 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
flag := StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"} flag := StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_FOO]") { expectedSuffix := " [$APP_FOO]"
t.Errorf("%s does not end with [$APP_FOO]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_FOO%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with" + expectedSuffix, output)
} }
} }
} }
@ -110,8 +115,12 @@ func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
flag := StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"} flag := StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_QWWX]") { expectedSuffix := " [$APP_QWWX]"
t.Errorf("%q does not end with [$APP_QWWX]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_QWWX%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%q does not end with" + expectedSuffix, output)
} }
} }
} }
@ -143,8 +152,12 @@ func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
flag := IntFlag{Name: test.name, EnvVar: "APP_BAR"} flag := IntFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_BAR]") { expectedSuffix := " [$APP_BAR]"
t.Errorf("%s does not end with [$APP_BAR]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_BAR%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with" + expectedSuffix, output)
} }
} }
} }
@ -176,8 +189,12 @@ func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
flag := DurationFlag{Name: test.name, EnvVar: "APP_BAR"} flag := DurationFlag{Name: test.name, EnvVar: "APP_BAR"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_BAR]") { expectedSuffix := " [$APP_BAR]"
t.Errorf("%s does not end with [$APP_BAR]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_BAR%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with" + expectedSuffix, output)
} }
} }
} }
@ -216,8 +233,12 @@ func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
flag := IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"} flag := IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_SMURF]") { expectedSuffix := " [$APP_SMURF]"
t.Errorf("%q does not end with [$APP_SMURF]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_SMURF%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%q does not end with" + expectedSuffix, output)
} }
} }
} }
@ -249,8 +270,12 @@ func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
flag := Float64Flag{Name: test.name, EnvVar: "APP_BAZ"} flag := Float64Flag{Name: test.name, EnvVar: "APP_BAZ"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_BAZ]") { expectedSuffix := " [$APP_BAZ]"
t.Errorf("%s does not end with [$APP_BAZ]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_BAZ%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with" + expectedSuffix, output)
} }
} }
} }
@ -283,8 +308,12 @@ func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
flag := GenericFlag{Name: test.name, EnvVar: "APP_ZAP"} flag := GenericFlag{Name: test.name, EnvVar: "APP_ZAP"}
output := flag.String() output := flag.String()
if !strings.HasSuffix(output, " [$APP_ZAP]") { expectedSuffix := " [$APP_ZAP]"
t.Errorf("%s does not end with [$APP_ZAP]", output) if runtime.GOOS == "windows" {
expectedSuffix = " [%APP_ZAP%]"
}
if !strings.HasSuffix(output, expectedSuffix) {
t.Errorf("%s does not end with" + expectedSuffix, output)
} }
} }
} }

View File

@ -15,7 +15,7 @@ var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}} {{.Name}} - {{.Usage}}
USAGE: USAGE:
{{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
{{if .Version}} {{if .Version}}
VERSION: VERSION:
{{.Version}} {{.Version}}
@ -180,7 +180,9 @@ func printHelp(out io.Writer, templ string, data interface{}) {
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
err := t.Execute(w, data) err := t.Execute(w, data)
if err != nil { if err != nil {
panic(err) // If the writer is closed, t.Execute will fail, and there's nothing
// we can do to recover. We could send this to os.Stderr if we need.
return
} }
w.Flush() w.Flush()
} }

View File

@ -7,13 +7,13 @@ import (
/* Test Helpers */ /* Test Helpers */
func expect(t *testing.T, a interface{}, b interface{}) { func expect(t *testing.T, a interface{}, b interface{}) {
if a != b { if !reflect.DeepEqual(a, b) {
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
} }
} }
func refute(t *testing.T, a interface{}, b interface{}) { func refute(t *testing.T, a interface{}, b interface{}) {
if a == b { if reflect.DeepEqual(a, b) {
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
} }
} }