Merge branch 'master' into suggestions

This commit is contained in:
Dan Buch 2022-04-19 16:45:20 -04:00 committed by GitHub
commit 4b238b8ff0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2097 additions and 439 deletions

View File

@ -29,10 +29,11 @@ _(REQUIRED)_
_(REQUIRED)_ _(REQUIRED)_
<!-- <!--
If this PR fixes one of more issues, list them here. If this PR fixes one of more issues, list them here.
One line each, like so: One line each, like so:
Fixes #123
Fixes #39 Fixes #123
Fixes #39
--> -->
## Special notes for your reviewer: ## Special notes for your reviewer:
@ -66,4 +67,4 @@ _(REQUIRED)_
```release-note ```release-note
``` ```

63
.github/stale.yml vendored
View File

@ -1,63 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 30
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- pinned
- security
- "kind/maintenance"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: "status/stale"
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue or PR has been automatically marked as stale because it has not had
recent activity. Please add a comment bumping this if you're still
interested in it's resolution! Thanks for your help, please let us know
if you need anything else.
# Comment to post when removing the stale label.
unmarkComment: >
This issue or PR has been bumped and is no longer marked as stale! Feel free
to bump it again in the future, if it's still relevant.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
Closing this as it has become stale.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed

View File

@ -11,12 +11,11 @@ on:
- v1 - v1
jobs: jobs:
test: test:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
go: [1.11, 1.12, 1.13] go: [1.15, 1.16, 1.17]
name: ${{ matrix.os }} @ Go ${{ matrix.go }} name: ${{ matrix.os }} @ Go ${{ matrix.go }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -27,10 +26,10 @@ jobs:
- name: Set GOPATH, PATH and ENV - name: Set GOPATH, PATH and ENV
run: | run: |
echo "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)" echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
echo "::set-env name=GO111MODULE::on" echo "GO111MODULE=on" >> $GITHUB_ENV
echo "::set-env name=GOPROXY::https://proxy.golang.org" echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin" echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
shell: bash shell: bash
- name: Checkout Code - name: Checkout Code
@ -39,7 +38,7 @@ jobs:
ref: ${{ github.ref }} ref: ${{ github.ref }}
- name: GOFMT Check - name: GOFMT Check
if: matrix.go == 1.13 && matrix.os == 'ubuntu-latest' if: matrix.go == 1.17 && matrix.os == 'ubuntu-latest'
run: test -z $(gofmt -l .) run: test -z $(gofmt -l .)
- name: vet - name: vet
@ -52,20 +51,20 @@ jobs:
run: go run internal/build/build.go check-binary-size run: go run internal/build/build.go check-binary-size
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == 1.13 && matrix.os == 'ubuntu-latest' if: success() && matrix.go == 1.17 && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
with: with:
token: 0a8cc73b-bb7c-480b-8626-38a461643761
fail_ci_if_error: true fail_ci_if_error: true
test-docs: test-docs:
name: test-docs name: test-docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.13 - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v1
with: with:
go-version: 1.13 # Currently fails on 1.16+
go-version: 1.15
- name: Use Node.js 12.x - name: Use Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
@ -74,10 +73,10 @@ jobs:
- name: Set GOPATH, PATH and ENV - name: Set GOPATH, PATH and ENV
run: | run: |
echo "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)" echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
echo "::set-env name=GO111MODULE::on" echo "GO111MODULE=on" >> $GITHUB_ENV
echo "::set-env name=GOPROXY::https://proxy.golang.org" echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin" echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
shell: bash shell: bash
- name: Checkout Code - name: Checkout Code

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ vendor
.idea .idea
internal/*/built-example internal/*/built-example
coverage.txt coverage.txt
*.exe

View File

@ -1,7 +1,7 @@
cli cli
=== ===
[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) [![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://pkg.go.dev/github.com/urfave/cli/v2)
[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli)
[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) [![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli)
[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) [![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli)
@ -17,11 +17,15 @@ Usage documentation exists for each major version. Don't know what version you'r
- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) - `v2` - [./docs/v2/manual.md](./docs/v2/manual.md)
- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md) - `v1` - [./docs/v1/manual.md](./docs/v1/manual.md)
Guides for migrating to newer versions:
- `v1-to-v2` - [./docs/migrate-v1-to-v2.md](./docs/migrate-v1-to-v2.md)
## Installation ## Installation
Make sure you have a working Go environment. Go version 1.11+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html). Using this package requires a working Go environment. [See the install instructions for Go](http://golang.org/doc/install.html).
Go Modules are strongly recommended when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). Go Modules are required when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules).
### Using `v2` releases ### Using `v2` releases
@ -63,4 +67,4 @@ export PATH=$PATH:$GOPATH/bin
cli is tested against multiple versions of Go on Linux, and against the latest cli is tested against multiple versions of Go on Linux, and against the latest
released version of Go on OS X and Windows. This project uses Github Actions for released version of Go on OS X and Windows. This project uses Github Actions for
builds. For more build info, please look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml). builds. To see our currently supported go versions and platforms, look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml).

View File

@ -0,0 +1,6 @@
package altsrc
// defaultInputSource creates a default InputSourceContext.
func defaultInputSource() (InputSourceContext, error) {
return &MapInputSource{file: "", valueMap: map[interface{}]interface{}{}}, nil
}

View File

@ -4,6 +4,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
@ -190,7 +191,13 @@ func TestPathApplyInputSourceMethodSet(t *testing.T) {
expected := "/path/to/source/hello" expected := "/path/to/source/hello"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
expected = `D:\path\to\source\hello` var err error
// Prepend the corresponding drive letter (or UNC path?), and change
// to windows-style path:
expected, err = filepath.Abs(expected)
if err != nil {
t.Fatal(err)
}
} }
expect(t, expected, c.String("test")) expect(t, expected, c.String("test"))
} }

View File

@ -17,7 +17,11 @@ import (
// by the given flag. // by the given flag.
func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) { func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) { return func(context *cli.Context) (InputSourceContext, error) {
return NewJSONSourceFromFile(context.String(flag)) if context.IsSet(flag) {
return NewJSONSourceFromFile(context.String(flag))
}
return defaultInputSource()
} }
} }

View File

@ -16,6 +16,11 @@ type MapInputSource struct {
valueMap map[interface{}]interface{} valueMap map[interface{}]interface{}
} }
// NewMapInputSource creates a new MapInputSource for implementing custom input sources.
func NewMapInputSource(file string, valueMap map[interface{}]interface{}) *MapInputSource {
return &MapInputSource{file: file, valueMap: valueMap}
}
// nestedVal checks if the name has '.' delimiters. // nestedVal checks if the name has '.' delimiters.
// If so, it tries to traverse the tree by the '.' delimited sections to find // If so, it tries to traverse the tree by the '.' delimited sections to find
// a nested value for the key. // a nested value for the key.

View File

@ -6,14 +6,13 @@ import (
) )
func TestMapDuration(t *testing.T) { func TestMapDuration(t *testing.T) {
inputSource := &MapInputSource{ inputSource := NewMapInputSource(
file: "test", "test",
valueMap: map[interface{}]interface{}{ map[interface{}]interface{}{
"duration_of_duration_type": time.Minute, "duration_of_duration_type": time.Minute,
"duration_of_string_type": "1m", "duration_of_string_type": "1m",
"duration_of_int_type": 1000, "duration_of_int_type": 1000,
}, })
}
d, err := inputSource.Duration("duration_of_duration_type") d, err := inputSource.Duration("duration_of_duration_type")
expect(t, time.Minute, d) expect(t, time.Minute, d)
expect(t, nil, err) expect(t, nil, err)

View File

@ -87,8 +87,12 @@ func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context. // NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) { func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) { return func(context *cli.Context) (InputSourceContext, error) {
filePath := context.String(flagFileName) if context.IsSet(flagFileName) {
return NewTomlSourceFromFile(filePath) filePath := context.String(flagFileName)
return NewTomlSourceFromFile(filePath)
}
return defaultInputSource()
} }
} }

View File

@ -33,8 +33,12 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context. // NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) { func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) { return func(context *cli.Context) (InputSourceContext, error) {
filePath := context.String(flagFileName) if context.IsSet(flagFileName) {
return NewYamlSourceFromFile(filePath) filePath := context.String(flagFileName)
return NewYamlSourceFromFile(filePath)
}
return defaultInputSource()
} }
} }

65
app.go
View File

@ -43,8 +43,11 @@ type App struct {
Flags []Flag Flags []Flag
// Boolean to enable bash completion commands // Boolean to enable bash completion commands
EnableBashCompletion bool EnableBashCompletion bool
// Boolean to hide built-in help command // Boolean to hide built-in help command and help flag
HideHelp bool HideHelp bool
// Boolean to hide built-in help command but keep help flag.
// Ignored if HideHelp is true.
HideHelpCommand bool
// Boolean to hide built-in version flag and the VERSION section of help // Boolean to hide built-in version flag and the VERSION section of help
HideVersion bool HideVersion bool
// categories contains the categorized commands and is populated on app startup // categories contains the categorized commands and is populated on app startup
@ -61,7 +64,7 @@ type App struct {
Action ActionFunc Action ActionFunc
// Execute this function if the proper command cannot be found // Execute this function if the proper command cannot be found
CommandNotFound CommandNotFoundFunc CommandNotFound CommandNotFoundFunc
// Execute this function if an usage error occurs // Execute this function if a usage error occurs
OnUsageError OnUsageErrorFunc OnUsageError OnUsageErrorFunc
// Compilation date // Compilation date
Compiled time.Time Compiled time.Time
@ -69,12 +72,15 @@ type App struct {
Authors []*Author Authors []*Author
// Copyright of the binary if any // Copyright of the binary if any
Copyright string Copyright string
// Reader reader to write input to (useful for tests)
Reader io.Reader
// Writer writer to write output to // Writer writer to write output to
Writer io.Writer Writer io.Writer
// ErrWriter writes error output // ErrWriter writes error output
ErrWriter io.Writer ErrWriter io.Writer
// Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to // ExitErrHandler processes any error encountered while running an App before
// function as a default, so this is optional. // it is returned to the caller. If no function is provided, HandleExitCoder
// is used as the default behavior.
ExitErrHandler ExitErrHandlerFunc ExitErrHandler ExitErrHandlerFunc
// Other custom info // Other custom info
Metadata map[string]interface{} Metadata map[string]interface{}
@ -115,7 +121,9 @@ func NewApp() *App {
BashComplete: DefaultAppComplete, BashComplete: DefaultAppComplete,
Action: helpCommand.Action, Action: helpCommand.Action,
Compiled: compileTime(), Compiled: compileTime(),
Reader: os.Stdin,
Writer: os.Stdout, Writer: os.Stdout,
ErrWriter: os.Stderr,
} }
} }
@ -134,7 +142,7 @@ func (a *App) Setup() {
} }
if a.HelpName == "" { if a.HelpName == "" {
a.HelpName = filepath.Base(os.Args[0]) a.HelpName = a.Name
} }
if a.Usage == "" { if a.Usage == "" {
@ -157,10 +165,18 @@ func (a *App) Setup() {
a.Compiled = compileTime() a.Compiled = compileTime()
} }
if a.Reader == nil {
a.Reader = os.Stdin
}
if a.Writer == nil { if a.Writer == nil {
a.Writer = os.Stdout a.Writer = os.Stdout
} }
if a.ErrWriter == nil {
a.ErrWriter = os.Stderr
}
var newCommands []*Command var newCommands []*Command
for _, c := range a.Commands { for _, c := range a.Commands {
@ -172,7 +188,9 @@ func (a *App) Setup() {
a.Commands = newCommands a.Commands = newCommands
if a.Command(helpCommand.Name) == nil && !a.HideHelp { if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.appendCommand(helpCommand) if !a.HideHelpCommand {
a.appendCommand(helpCommand)
}
if HelpFlag != nil { if HelpFlag != nil {
a.appendFlag(HelpFlag) a.appendFlag(HelpFlag)
@ -192,10 +210,6 @@ func (a *App) Setup() {
if a.Metadata == nil { if a.Metadata == nil {
a.Metadata = make(map[string]interface{}) a.Metadata = make(map[string]interface{})
} }
if a.Writer == nil {
a.Writer = os.Stdout
}
} }
func (a *App) newFlagSet() (*flag.FlagSet, error) { func (a *App) newFlagSet() (*flag.FlagSet, error) {
@ -271,7 +285,7 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
return nil return nil
} }
cerr := checkRequiredFlags(a.Flags, context) cerr := context.checkRequiredFlags(a.Flags)
if cerr != nil { if cerr != nil {
_ = ShowAppHelp(context) _ = ShowAppHelp(context)
return cerr return cerr
@ -292,8 +306,6 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
if a.Before != nil { if a.Before != nil {
beforeErr := a.Before(context) beforeErr := a.Before(context)
if beforeErr != nil { if beforeErr != nil {
_, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
_ = ShowAppHelp(context)
a.handleExitCoder(context, beforeErr) a.handleExitCoder(context, beforeErr)
err = beforeErr err = beforeErr
return err return err
@ -323,11 +335,11 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned // RunAndExitOnError calls .Run() and exits non-zero if an error was returned
// //
// Deprecated: instead you should return an error that fulfills cli.ExitCoder // Deprecated: instead you should return an error that fulfills cli.ExitCoder
// to cli.App.Run. This will cause the application to exit with the given eror // to cli.App.Run. This will cause the application to exit with the given error
// code in the cli.ExitCoder // code in the cli.ExitCoder
func (a *App) RunAndExitOnError() { func (a *App) RunAndExitOnError() {
if err := a.Run(os.Args); err != nil { if err := a.Run(os.Args); err != nil {
_, _ = fmt.Fprintln(a.errWriter(), err) _, _ = fmt.Fprintln(a.ErrWriter, err)
OsExiter(1) OsExiter(1)
} }
} }
@ -335,19 +347,9 @@ func (a *App) RunAndExitOnError() {
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to // RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
// generate command-specific flags // generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) (err error) { func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// Setup also handles HideHelp and HideHelpCommand
a.Setup() a.Setup()
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.appendCommand(helpCommand)
if HelpFlag != nil {
a.appendFlag(HelpFlag)
}
}
}
var newCmds []*Command var newCmds []*Command
for _, c := range a.Commands { for _, c := range a.Commands {
if c.HelpName == "" { if c.HelpName == "" {
@ -407,7 +409,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
} }
} }
cerr := checkRequiredFlags(a.Flags, context) cerr := context.checkRequiredFlags(a.Flags)
if cerr != nil { if cerr != nil {
_ = ShowSubcommandHelp(context) _ = ShowSubcommandHelp(context)
return cerr return cerr
@ -496,15 +498,6 @@ func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags) return visibleFlags(a.Flags)
} }
func (a *App) errWriter() io.Writer {
// When the app ErrWriter is nil use the package level one.
if a.ErrWriter == nil {
return ErrWriter
}
return a.ErrWriter
}
func (a *App) appendFlag(fl Flag) { func (a *App) appendFlag(fl Flag) {
if !hasFlag(a.Flags, fl) { if !hasFlag(a.Flags, fl) {
a.Flags = append(a.Flags, fl) a.Flags = append(a.Flags, fl)

View File

@ -315,7 +315,6 @@ func ExampleApp_Run_bashComplete_withMultipleLongFlag() {
} }
func ExampleApp_Run_bashComplete() { func ExampleApp_Run_bashComplete() {
// set args for examples sake
// set args for examples sake // set args for examples sake
os.Args = []string{"greet", "--generate-bash-completion"} os.Args = []string{"greet", "--generate-bash-completion"}
@ -433,6 +432,12 @@ func TestApp_Command(t *testing.T) {
} }
} }
func TestApp_Setup_defaultsReader(t *testing.T) {
app := &App{}
app.Setup()
expect(t, app.Reader, os.Stdin)
}
func TestApp_Setup_defaultsWriter(t *testing.T) { func TestApp_Setup_defaultsWriter(t *testing.T) {
app := &App{} app := &App{}
app.Setup() app.Setup()
@ -471,18 +476,18 @@ func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) {
a := App{ a := App{
Name: "cmd", Name: "cmd",
Flags: []Flag{ Flags: []Flag{
&StringFlag{Name: "--foo"}, &StringFlag{Name: "foo"},
}, },
Writer: bytes.NewBufferString(""), Writer: bytes.NewBufferString(""),
} }
set := flag.NewFlagSet("", flag.ContinueOnError) set := flag.NewFlagSet("", flag.ContinueOnError)
_ = set.Parse([]string{"", "---foo"}) _ = set.Parse([]string{"", "-bar"})
c := &Context{flagSet: set} c := &Context{flagSet: set}
err := a.RunAsSubcommand(c) err := a.RunAsSubcommand(c)
expect(t, err, errors.New("bad flag syntax: ---foo")) expect(t, err.Error(), "flag provided but not defined: -bar")
} }
func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {
@ -850,6 +855,15 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) {
} }
} }
func TestApp_DefaultStdin(t *testing.T) {
app := &App{}
app.Setup()
if app.Reader != os.Stdin {
t.Error("Default input reader not set.")
}
}
func TestApp_DefaultStdout(t *testing.T) { func TestApp_DefaultStdout(t *testing.T) {
app := &App{} app := &App{}
app.Setup() app.Setup()
@ -859,6 +873,62 @@ func TestApp_DefaultStdout(t *testing.T) {
} }
} }
func TestApp_SetStdin(t *testing.T) {
buf := make([]byte, 12)
app := &App{
Name: "test",
Reader: strings.NewReader("Hello World!"),
Action: func(c *Context) error {
_, err := c.App.Reader.Read(buf)
return err
},
}
err := app.Run([]string{"help"})
if err != nil {
t.Fatalf("Run error: %s", err)
}
if string(buf) != "Hello World!" {
t.Error("App did not read input from desired reader.")
}
}
func TestApp_SetStdin_Subcommand(t *testing.T) {
buf := make([]byte, 12)
app := &App{
Name: "test",
Reader: strings.NewReader("Hello World!"),
Commands: []*Command{
{
Name: "command",
Subcommands: []*Command{
{
Name: "subcommand",
Action: func(c *Context) error {
_, err := c.App.Reader.Read(buf)
return err
},
},
},
},
},
}
err := app.Run([]string{"test", "command", "subcommand"})
if err != nil {
t.Fatalf("Run error: %s", err)
}
if string(buf) != "Hello World!" {
t.Error("App did not read input from desired reader.")
}
}
func TestApp_SetStdout(t *testing.T) { func TestApp_SetStdout(t *testing.T) {
var w bytes.Buffer var w bytes.Buffer
@ -2152,3 +2222,34 @@ func newTestApp() *App {
a.Writer = ioutil.Discard a.Writer = ioutil.Discard
return a return a
} }
func TestSetupInitializesBothWriters(t *testing.T) {
a := &App{}
a.Setup()
if a.ErrWriter != os.Stderr {
t.Errorf("expected a.ErrWriter to be os.Stderr")
}
if a.Writer != os.Stdout {
t.Errorf("expected a.Writer to be os.Stdout")
}
}
func TestSetupInitializesOnlyNilWriters(t *testing.T) {
wr := &bytes.Buffer{}
a := &App{
ErrWriter: wr,
}
a.Setup()
if a.ErrWriter != wr {
t.Errorf("expected a.ErrWriter to be a *bytes.Buffer instance")
}
if a.Writer != os.Stdout {
t.Errorf("expected a.Writer to be os.Stdout")
}
}

View File

@ -0,0 +1,9 @@
$fn = $($MyInvocation.MyCommand.Name)
$name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-bash-completion"
Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

View File

@ -13,6 +13,8 @@ _cli_zsh_autocomplete() {
if [[ "${opts[1]}" != "" ]]; then if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts _describe 'values' opts
else
_files
fi fi
return return

View File

@ -41,8 +41,11 @@ type Command struct {
Flags []Flag Flags []Flag
// Treat all flags as normal arguments if true // Treat all flags as normal arguments if true
SkipFlagParsing bool SkipFlagParsing bool
// Boolean to hide built-in help command // Boolean to hide built-in help command and help flag
HideHelp bool HideHelp bool
// Boolean to hide built-in help command but keep help flag
// Ignored if HideHelp is true.
HideHelpCommand bool
// Boolean to hide this command from help or completion // Boolean to hide this command from help or completion
Hidden bool Hidden bool
// Boolean to enable short-option handling so user can combine several // Boolean to enable short-option handling so user can combine several
@ -129,7 +132,7 @@ func (c *Command) Run(ctx *Context) (err error) {
return nil return nil
} }
cerr := checkRequiredFlags(c.Flags, context) cerr := context.checkRequiredFlags(c.Flags)
if cerr != nil { if cerr != nil {
_ = ShowCommandHelp(context, c.Name) _ = ShowCommandHelp(context, c.Name)
return cerr return cerr
@ -152,7 +155,6 @@ func (c *Command) Run(ctx *Context) (err error) {
if c.Before != nil { if c.Before != nil {
err = c.Before(context) err = c.Before(context)
if err != nil { if err != nil {
_ = ShowCommandHelp(context, c.Name)
context.App.handleExitCoder(context, err) context.App.handleExitCoder(context, err)
return err return err
} }
@ -230,6 +232,7 @@ func (c *Command) startApp(ctx *Context) error {
} }
app.Usage = c.Usage app.Usage = c.Usage
app.UsageText = c.UsageText
app.Description = c.Description app.Description = c.Description
app.ArgsUsage = c.ArgsUsage app.ArgsUsage = c.ArgsUsage
@ -241,10 +244,12 @@ func (c *Command) startApp(ctx *Context) error {
app.Commands = c.Subcommands app.Commands = c.Subcommands
app.Flags = c.Flags app.Flags = c.Flags
app.HideHelp = c.HideHelp app.HideHelp = c.HideHelp
app.HideHelpCommand = c.HideHelpCommand
app.Version = ctx.App.Version app.Version = ctx.App.Version
app.HideVersion = ctx.App.HideVersion app.HideVersion = true
app.Compiled = ctx.App.Compiled app.Compiled = ctx.App.Compiled
app.Reader = ctx.App.Reader
app.Writer = ctx.App.Writer app.Writer = ctx.App.Writer
app.ErrWriter = ctx.App.ErrWriter app.ErrWriter = ctx.App.ErrWriter
app.ExitErrHandler = ctx.App.ExitErrHandler app.ExitErrHandler = ctx.App.ExitErrHandler

View File

@ -377,3 +377,48 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) {
} }
} }
func TestCommand_NoVersionFlagOnCommands(t *testing.T) {
app := &App{
Version: "some version",
Commands: []*Command{
{
Name: "bar",
Usage: "this is for testing",
Subcommands: []*Command{{}}, // some subcommand
HideHelp: true,
Action: func(c *Context) error {
if len(c.App.VisibleFlags()) != 0 {
t.Fatal("unexpected flag on command")
}
return nil
},
},
},
}
err := app.Run([]string{"foo", "bar"})
expect(t, err, nil)
}
func TestCommand_CanAddVFlagOnCommands(t *testing.T) {
app := &App{
Version: "some version",
Writer: ioutil.Discard,
Commands: []*Command{
{
Name: "bar",
Usage: "this is for testing",
Subcommands: []*Command{{}}, // some subcommand
Flags: []Flag{
&BoolFlag{
Name: "v",
},
},
},
},
}
err := app.Run([]string{"foo", "bar"})
expect(t, err, nil)
}

View File

@ -2,9 +2,7 @@ package cli
import ( import (
"context" "context"
"errors"
"flag" "flag"
"fmt"
"strings" "strings"
) )
@ -53,20 +51,18 @@ func (c *Context) Set(name, value string) error {
// IsSet determines if the flag was actually set // IsSet determines if the flag was actually set
func (c *Context) IsSet(name string) bool { func (c *Context) IsSet(name string) bool {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
if fs := lookupFlagSet(name, c); fs != nil { isSet := false
isSet := false fs.Visit(func(f *flag.Flag) {
fs.Visit(func(f *flag.Flag) { if f.Name == name {
if f.Name == name { isSet = true
isSet = true
}
})
if isSet {
return true
} }
})
if isSet {
return true
} }
f := lookupFlag(name, c) f := c.lookupFlag(name)
if f == nil { if f == nil {
return false return false
} }
@ -108,7 +104,10 @@ func (c *Context) Lineage() []*Context {
// Value returns the value of the flag corresponding to `name` // Value returns the value of the flag corresponding to `name`
func (c *Context) Value(name string) interface{} { func (c *Context) Value(name string) interface{} {
return c.flagSet.Lookup(name).Value.(flag.Getter).Get() if fs := c.lookupFlagSet(name); fs != nil {
return fs.Lookup(name).Value.(flag.Getter).Get()
}
return nil
} }
// Args returns the command line arguments associated with the context. // Args returns the command line arguments associated with the context.
@ -122,7 +121,7 @@ func (c *Context) NArg() int {
return c.Args().Len() return c.Args().Len()
} }
func lookupFlag(name string, ctx *Context) Flag { func (ctx *Context) lookupFlag(name string) Flag {
for _, c := range ctx.Lineage() { for _, c := range ctx.Lineage() {
if c.Command == nil { if c.Command == nil {
continue continue
@ -150,8 +149,11 @@ func lookupFlag(name string, ctx *Context) Flag {
return nil return nil
} }
func lookupFlagSet(name string, ctx *Context) *flag.FlagSet { func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet {
for _, c := range ctx.Lineage() { for _, c := range ctx.Lineage() {
if c.flagSet == nil {
continue
}
if f := c.flagSet.Lookup(name); f != nil { if f := c.flagSet.Lookup(name); f != nil {
return c.flagSet return c.flagSet
} }
@ -160,89 +162,7 @@ func lookupFlagSet(name string, ctx *Context) *flag.FlagSet {
return nil return nil
} }
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
switch ff.Value.(type) {
case Serializer:
_ = set.Set(name, ff.Value.(Serializer).Serialize())
default:
_ = set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := f.Names()
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}
func makeFlagNameVisitor(names *[]string) func(*flag.Flag) {
return func(f *flag.Flag) {
nameParts := strings.Split(f.Name, ",")
name := strings.TrimSpace(nameParts[0])
for _, part := range nameParts {
part = strings.TrimSpace(part)
if len(part) > len(name) {
name = part
}
}
if name != "" {
*names = append(*names, name)
}
}
}
type requiredFlagsErr interface {
error
getMissingFlags() []string
}
type errRequiredFlags struct {
missingFlags []string
}
func (e *errRequiredFlags) Error() string {
numberOfMissingFlags := len(e.missingFlags)
if numberOfMissingFlags == 1 {
return fmt.Sprintf("Required flag %q not set", e.missingFlags[0])
}
joinedMissingFlags := strings.Join(e.missingFlags, ", ")
return fmt.Sprintf("Required flags %q not set", joinedMissingFlags)
}
func (e *errRequiredFlags) getMissingFlags() []string {
return e.missingFlags
}
func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr {
var missingFlags []string var missingFlags []string
for _, f := range flags { for _, f := range flags {
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
@ -271,3 +191,21 @@ func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr {
return nil return nil
} }
func makeFlagNameVisitor(names *[]string) func(*flag.Flag) {
return func(f *flag.Flag) {
nameParts := strings.Split(f.Name, ",")
name := strings.TrimSpace(nameParts[0])
for _, part := range nameParts {
part = strings.TrimSpace(part)
if len(part) > len(name) {
name = part
}
}
if name != "" {
*names = append(*names, name)
}
}
}

View File

@ -112,6 +112,8 @@ func TestContext_String(t *testing.T) {
c := NewContext(nil, set, parentCtx) c := NewContext(nil, set, parentCtx)
expect(t, c.String("myflag"), "hello world") expect(t, c.String("myflag"), "hello world")
expect(t, c.String("top-flag"), "hai veld") expect(t, c.String("top-flag"), "hai veld")
c = NewContext(nil, nil, parentCtx)
expect(t, c.String("top-flag"), "hai veld")
} }
func TestContext_Path(t *testing.T) { func TestContext_Path(t *testing.T) {
@ -136,6 +138,18 @@ func TestContext_Bool(t *testing.T) {
expect(t, c.Bool("top-flag"), true) expect(t, c.Bool("top-flag"), true)
} }
func TestContext_Value(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
parentSet := flag.NewFlagSet("test", 0)
parentSet.Int("top-flag", 13, "doc")
parentCtx := NewContext(nil, parentSet, nil)
c := NewContext(nil, set, parentCtx)
expect(t, c.Value("myflag"), 12)
expect(t, c.Value("top-flag"), 13)
expect(t, c.Value("unknown-flag"), nil)
}
func TestContext_Args(t *testing.T) { func TestContext_Args(t *testing.T) {
set := flag.NewFlagSet("test", 0) set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc") set.Bool("myflag", false, "doc")
@ -183,6 +197,7 @@ func TestContext_IsSet_fromEnv(t *testing.T) {
unparsableIsSet, uIsSet bool unparsableIsSet, uIsSet bool
) )
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
_ = os.Setenv("APP_PASSWORD", "") _ = os.Setenv("APP_PASSWORD", "")
@ -303,13 +318,13 @@ func TestContext_lookupFlagSet(t *testing.T) {
_ = set.Parse([]string{"--local-flag"}) _ = set.Parse([]string{"--local-flag"})
_ = parentSet.Parse([]string{"--top-flag"}) _ = parentSet.Parse([]string{"--top-flag"})
fs := lookupFlagSet("top-flag", ctx) fs := ctx.lookupFlagSet("top-flag")
expect(t, fs, parentCtx.flagSet) expect(t, fs, parentCtx.flagSet)
fs = lookupFlagSet("local-flag", ctx) fs = ctx.lookupFlagSet("local-flag")
expect(t, fs, ctx.flagSet) expect(t, fs, ctx.flagSet)
if fs := lookupFlagSet("frob", ctx); fs != nil { if fs := ctx.lookupFlagSet("frob"); fs != nil {
t.Fail() t.Fail()
} }
} }
@ -533,12 +548,21 @@ func TestCheckRequiredFlags(t *testing.T) {
}, },
parseInput: []string{"-n", "asd", "-n", "qwe"}, parseInput: []string{"-n", "asd", "-n", "qwe"},
}, },
{
testCase: "required_flag_with_short_alias_not_printed_on_error",
expectedAnError: true,
expectedErrorContents: []string{"Required flag \"names\" not set"},
flags: []Flag{
&StringSliceFlag{Name: "names, n", Required: true},
},
},
} }
for _, test := range tdata { for _, test := range tdata {
t.Run(test.testCase, func(t *testing.T) { t.Run(test.testCase, func(t *testing.T) {
// setup // setup
if test.envVarInput[0] != "" { if test.envVarInput[0] != "" {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv(test.envVarInput[0], test.envVarInput[1]) _ = os.Setenv(test.envVarInput[0], test.envVarInput[1])
} }
@ -554,7 +578,7 @@ func TestCheckRequiredFlags(t *testing.T) {
ctx.Command.Flags = test.flags ctx.Command.Flags = test.flags
// logic under test // logic under test
err := checkRequiredFlags(test.flags, ctx) err := ctx.checkRequiredFlags(test.flags)
// assertions // assertions
if test.expectedAnError && err == nil { if test.expectedAnError && err == nil {

67
docs.go
View File

@ -15,31 +15,39 @@ import (
// The function errors if either parsing or writing of the string fails. // The function errors if either parsing or writing of the string fails.
func (a *App) ToMarkdown() (string, error) { func (a *App) ToMarkdown() (string, error) {
var w bytes.Buffer var w bytes.Buffer
if err := a.writeDocTemplate(&w); err != nil { if err := a.writeDocTemplate(&w, 0); err != nil {
return "", err return "", err
} }
return w.String(), nil return w.String(), nil
} }
// ToMan creates a man page string for the `*App` // ToMan creates a man page string with section number for the `*App`
// The function errors if either parsing or writing of the string fails. // The function errors if either parsing or writing of the string fails.
func (a *App) ToMan() (string, error) { func (a *App) ToManWithSection(sectionNumber int) (string, error) {
var w bytes.Buffer var w bytes.Buffer
if err := a.writeDocTemplate(&w); err != nil { if err := a.writeDocTemplate(&w, sectionNumber); err != nil {
return "", err return "", err
} }
man := md2man.Render(w.Bytes()) man := md2man.Render(w.Bytes())
return string(man), nil return string(man), nil
} }
// ToMan creates a man page string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToMan() (string, error) {
man, err := a.ToManWithSection(8)
return man, err
}
type cliTemplate struct { type cliTemplate struct {
App *App App *App
SectionNum int
Commands []string Commands []string
GlobalArgs []string GlobalArgs []string
SynopsisArgs []string SynopsisArgs []string
} }
func (a *App) writeDocTemplate(w io.Writer) error { func (a *App) writeDocTemplate(w io.Writer, sectionNum int) error {
const name = "cli" const name = "cli"
t, err := template.New(name).Parse(MarkdownDocTemplate) t, err := template.New(name).Parse(MarkdownDocTemplate)
if err != nil { if err != nil {
@ -47,6 +55,7 @@ func (a *App) writeDocTemplate(w io.Writer) error {
} }
return t.ExecuteTemplate(w, name, &cliTemplate{ return t.ExecuteTemplate(w, name, &cliTemplate{
App: a, App: a,
SectionNum: sectionNum,
Commands: prepareCommands(a.Commands, 0), Commands: prepareCommands(a.Commands, 0),
GlobalArgs: prepareArgsWithValues(a.VisibleFlags()), GlobalArgs: prepareArgsWithValues(a.VisibleFlags()),
SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()), SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()),
@ -59,15 +68,16 @@ func prepareCommands(commands []*Command, level int) []string {
if command.Hidden { if command.Hidden {
continue continue
} }
usage := ""
if command.Usage != "" {
usage = command.Usage
}
prepared := fmt.Sprintf("%s %s\n\n%s\n", usageText := prepareUsageText(command)
usage := prepareUsage(command, usageText)
prepared := fmt.Sprintf("%s %s\n\n%s%s",
strings.Repeat("#", level+2), strings.Repeat("#", level+2),
strings.Join(command.Names(), ", "), strings.Join(command.Names(), ", "),
usage, usage,
usageText,
) )
flags := prepareArgsWithValues(command.Flags) flags := prepareArgsWithValues(command.Flags)
@ -146,3 +156,40 @@ func flagDetails(flag DocGenerationFlag) string {
} }
return ": " + description return ": " + description
} }
func prepareUsageText(command *Command) string {
if command.UsageText == "" {
return ""
}
// Remove leading and trailing newlines
preparedUsageText := strings.Trim(command.UsageText, "\n")
var usageText string
if strings.Contains(preparedUsageText, "\n") {
// Format multi-line string as a code block using the 4 space schema to allow for embedded markdown such
// that it will not break the continuous code block.
for _, ln := range strings.Split(preparedUsageText, "\n") {
usageText += fmt.Sprintf(" %s\n", ln)
}
} else {
// Style a single line as a note
usageText = fmt.Sprintf(">%s\n", preparedUsageText)
}
return usageText
}
func prepareUsage(command *Command, usageText string) string {
if command.Usage == "" {
return ""
}
usage := command.Usage + "\n"
// Add a newline to the Usage IFF there is a UsageText
if usageText != "" {
usage += "\n"
}
return usage
}

View File

@ -6,6 +6,29 @@
View [unreleased 2.X] series changes. View [unreleased 2.X] series changes.
## [2.2.0] - 2020-03-08
These release notes were written for the git hash [d648edd48d89ef3a841b1ec75c2ebbd4de5f748f](https://github.com/urfave/cli/tree/d648edd48d89ef3a841b1ec75c2ebbd4de5f748f)
### Fixed
* Fixed zsh completion scripts in [urfave/cli/pull/1062](https://github.com/urfave/cli/pull/1062) via [@zhsj](https://github.com/zhsj)
* Fixed description of subcommand to be more consistent in [urfave/cli/pull/1054](https://github.com/urfave/cli/pull/1054) via [@itchyny](https://github.com/itchyny)
* Fixed possible runtime panic in slice parsing in [urfave/cli/pull/1049](https://github.com/urfave/cli/pull/1049) via [@saschagrunert](https://github.com/saschagrunert)
* Fixed invalid man page header generation in [urfave/cli/pull/1041](https://github.com/urfave/cli/pull/1041) via [@saschagrunert](https://github.com/saschagrunert)
### Changed
* Improved auto-completion instructions and added example gifs in [urfave/cli/pull/1059](https://github.com/urfave/cli/pull/1059) via [@masonj188](https://github.com/masonj188)
* Removed the author from generated man pages in [urfave/cli/pull/1041](https://github.com/urfave/cli/pull/1041) via [@saschagrunert](https://github.com/saschagrunert)
### Added
* Added destination field to `StringSliceFlag` in [urfave/cli/pull/1078](https://github.com/urfave/cli/pull/1078) via [@davidsbond](https://github.com/davidsbond)
* Added `HideHelpCommand`. While `HideHelp` hides both `help` command and `--help` flag, `HideHelpCommand` only hides `help` command and leave `--help` flag as-is in [urfave/cli/pull/1083](https://github.com/urfave/cli/pull/1083) via [@AkihiroSuda](https://github.com/AkihiroSuda)
* Added timestampFlag docs in [urfave/cli/pull/997](https://github.com/urfave/cli/pull/997) via [@drov0](https://github.com/drov0)
* Added required flags documentation in [urfave/cli/pull/1008](https://github.com/urfave/cli/pull/1008) via [@lynncyrin](https://github.com/lynncyrin), [@anberns](https://github.com/anberns)
## [2.1.1] - 2019-12-24 ## [2.1.1] - 2019-12-24
### Fixed ### Fixed
@ -67,6 +90,12 @@ The V2 changes were all shipped in [urfave/cli/pull/892](https://github.com/urfa
View [unreleased 1.22.X] series changes. View [unreleased 1.22.X] series changes.
## [1.22.4] - 2020-03-31
### Fixed
- Fixed a panic with flag completion in [urfave/cli/pull/1101](https://github.com/urfave/cli/pull/1101) via [@unRob](https://github.com/unRob), [@VirrageS](https://github.com/VirrageS)
## [1.22.3] - 2020-02-25 ## [1.22.3] - 2020-02-25
### Fixed ### Fixed
@ -555,12 +584,14 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
### Added ### Added
- Initial implementation. - Initial implementation.
[unreleased 2.X]: https://github.com/urfave/cli/compare/v2.1.1...HEAD [unreleased 2.X]: https://github.com/urfave/cli/compare/v2.2.0...HEAD
[2.2.0]: https://github.com/urfave/cli/compare/v2.1.1...v2.2.0
[2.1.1]: https://github.com/urfave/cli/compare/v2.1.0...v2.1.1 [2.1.1]: https://github.com/urfave/cli/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/urfave/cli/compare/v2.0.0...v2.1.0 [2.1.0]: https://github.com/urfave/cli/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/urfave/cli/compare/v1.22.2...v2.0.0 [2.0.0]: https://github.com/urfave/cli/compare/v1.22.2...v2.0.0
[unreleased 1.22.X]: https://github.com/urfave/cli/compare/v1.22.3...v1 [unreleased 1.22.X]: https://github.com/urfave/cli/compare/v1.22.4...v1
[1.22.4]: https://github.com/urfave/cli/compare/v1.22.3...v1.22.4
[1.22.3]: https://github.com/urfave/cli/compare/v1.22.2...v1.22.3 [1.22.3]: https://github.com/urfave/cli/compare/v1.22.2...v1.22.3
[1.22.2]: https://github.com/urfave/cli/compare/v1.22.1...v1.22.2 [1.22.2]: https://github.com/urfave/cli/compare/v1.22.1...v1.22.2
[1.22.1]: https://github.com/urfave/cli/compare/v1.22.0...v1.22.1 [1.22.1]: https://github.com/urfave/cli/compare/v1.22.0...v1.22.1

245
docs/migrate-v1-to-v2.md Normal file
View File

@ -0,0 +1,245 @@
Migration Guide: v1 to v2
===
v2 has a number of breaking changes but converting is relatively
straightforward: make the changes documented below then resolve any
compiler errors. We hope this will be sufficient for most typical
users.
If you find any issues not covered by this document, please post a
comment on [Issue 921](https://github.com/urfave/cli/issues/921) or
consider sending a PR to help improve this guide.
<!-- toc -->
* [Flags before args](#flags-before-args)
* [Import string changed](#import-string-changed)
* [Flag aliases are done differently](#flag-aliases-are-done-differently)
* [EnvVar is now a list (EnvVars)](#envvar-is-now-a-list-envvars)
* [Actions returns errors](#actions-returns-errors)
* [cli.Flag changed](#cliflag-changed)
* [Commands are now lists of pointers](#commands-are-now-lists-of-pointers)
* [Lists of commands should be pointers](#lists-of-commands-should-be-pointers)
* [Appending Commands](#appending-commands)
* [GlobalString, GlobalBool and its likes are deprecated](#globalstring-globalbool-and-its-likes-are-deprecated)
* [BoolTFlag and BoolT are deprecated](#booltflag-and-boolt-are-deprecated)
* [&cli.StringSlice{""} replaced with cli.NewStringSlice("")](#clistringslice-replaced-with-clinewstringslice)
* [Replace deprecated functions](#replace-deprecated-functions)
* [Everything else](#everything-else)
<!-- tocstop -->
# Flags before args
In v2 flags must come before args. This is more POSIX-compliant. You
may need to update scripts, user documentation, etc.
This will work:
```
cli hello --shout rick
```
This will not:
```
cli hello rick --shout
```
# Import string changed
* OLD: `import "github.com/urfave/cli"`
* NEW: `import "github.com/urfave/cli/v2"`
Check each file for this and make the change.
Shell command to find them all: `fgrep -rl github.com/urfave/cli *`
# Flag aliases are done differently
Change `Name: "foo, f"` to `Name: "foo", Aliases: []string{"f"}`
* OLD:
```go
cli.StringFlag{
Name: "config, cfg"
}
```
* NEW:
```go
cli.StringFlag{
Name: "config",
Aliases: []string{"cfg"},
}
```
Sadly v2 doesn't warn you if a comma is in the name.
(https://github.com/urfave/cli/issues/1103)
# EnvVar is now a list (EnvVars)
Change `EnvVar: "XXXXX"` to `EnvVars: []string{"XXXXX"}` (plural).
* OLD:
```go
cli.StringFlag{
EnvVar: "APP_LANG"
}
```
* NEW:
```go
cli.StringFlag{
EnvVars: []string{"APP_LANG"}
}
```
# Actions returns errors
A command's `Action:` now returns an `error`.
* OLD: `Action: func(c *cli.Context) {`
* NEW: `Action: func(c *cli.Context) error {`
Compiler messages you might see:
```
cannot use func literal (type func(*cli.Context)) as type cli.ActionFunc in field value
```
# cli.Flag changed
`cli.Flag` is now a list of pointers.
What this means to you:
If you make a list of flags, add a `&` in front of each
item. cli.BoolFlag, cli.StringFlag, etc.
* OLD:
```go
app.Flags = []cli.Flag{
cli.BoolFlag{
```
* NEW:
```go
app.Flags = []cli.Flag{
&cli.BoolFlag{
```
Compiler messages you might see:
```
cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver)
```
# Commands are now lists of pointers
Occurrences of `[]Command` have been changed to `[]*Command`.
What this means to you:
Look for `[]cli.Command{}` and change it to `[]*cli.Command{}`
Example:
* OLD: `var commands = []cli.Command{}`
* NEW: `var commands = []*cli.Command{}`
Compiler messages you might see:
```
cannot convert commands (type []cli.Command) to type cli.CommandsByName
cannot use commands (type []cli.Command) as type []*cli.Command in assignment
```
# Lists of commands should be pointers
If you are building up a list of commands, the individual items should
now be pointers.
* OLD: `cli.Command{`
* NEW: `&cli.Command{`
Compiler messages you might see:
```
cannot use cli.Command literal (type cli.Command) as type *cli.Command in argument to
```
# Appending Commands
Appending to a list of commands needs to be changed since the list is
now pointers.
* OLD: `commands = append(commands, *c)`
* NEW: `commands = append(commands, c)`
Compiler messages you might see:
```
cannot use c (type *cli.Command) as type cli.Command in append
```
# GlobalString, GlobalBool and its likes are deprecated
Use simply `String` instead of `GlobalString`, `Bool` instead of `GlobalBool`
# BoolTFlag and BoolT are deprecated
BoolTFlag was a Bool Flag with its default value set to true and BoolT was used to find any BoolTFlag used locally, so both are deprecated.
* OLD:
```go
cli.BoolTFlag{
Name: FlagName,
Usage: FlagUsage,
EnvVar: "FLAG_ENV_VAR",
}
```
* NEW:
```go
cli.BoolFlag{
Name: FlagName,
Value: true,
Usage: FlagUsage,
EnvVar: "FLAG_ENV_VAR",
}
```
# &cli.StringSlice{""} replaced with cli.NewStringSlice("")
Example:
* OLD:
```go
Value: &cli.StringSlice{""},
```
* NEW:
```go
Value: cli.NewStringSlice(""),
}
```
# Replace deprecated functions
`cli.NewExitError()` is deprecated. Use `cli.Exit()` instead. ([Staticcheck](https://staticcheck.io/) detects this automatically and recommends replacement code.)
# Everything else
Compile the code and work through any errors. Most should
relate to issues listed above.
Once it compiles, test the command. Review the output of `-h` or any
help messages to verify they match the intended flags and subcommands.
Then test the program itself.
If you find any issues not covered by this document please let us know
by submitting a comment on
[Issue 921](https://github.com/urfave/cli/issues/921)
so that others can benefit.

View File

@ -27,6 +27,7 @@ cli v1 manual
* [Version Flag](#version-flag) * [Version Flag](#version-flag)
+ [Customization](#customization-2) + [Customization](#customization-2)
+ [Full API Example](#full-api-example) + [Full API Example](#full-api-example)
* [Migrating to V2](#migrating-to-v2)
<!-- tocstop --> <!-- tocstop -->
@ -611,7 +612,7 @@ given sources.
Here is a more complete sample of a command using YAML support: Here is a more complete sample of a command using YAML support:
<!-- { <!-- {
"args": ["test-cmd", "&#45;&#45;help"], "args": ["&#45;&#45;help"],
"output": "&#45&#45;test value.*default: 0" "output": "&#45&#45;test value.*default: 0"
} --> } -->
``` go ``` go
@ -1476,3 +1477,10 @@ func wopAction(c *cli.Context) error {
return nil return nil
} }
``` ```
## Migrating to V2
There are a small set of breaking changes between v1 and v2.
Converting is relatively straightforward and typically takes less than
an hour. Specific steps are included in
[Migration Guide: v1 to v2](../migrate-v1-to-v2.md).

View File

@ -3,6 +3,7 @@ cli v2 manual
<!-- toc --> <!-- toc -->
- [Migrating From Older Releases](#migrating-from-older-releases)
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Examples](#examples) - [Examples](#examples)
* [Arguments](#arguments) * [Arguments](#arguments)
@ -29,6 +30,7 @@ cli v2 manual
+ [ZSH Support](#zsh-support) + [ZSH Support](#zsh-support)
+ [ZSH default auto-complete example](#zsh-default-auto-complete-example) + [ZSH default auto-complete example](#zsh-default-auto-complete-example)
+ [ZSH custom auto-complete example](#zsh-custom-auto-complete-example) + [ZSH custom auto-complete example](#zsh-custom-auto-complete-example)
+ [PowerShell Support](#powershell-support)
* [Generated Help Text](#generated-help-text) * [Generated Help Text](#generated-help-text)
+ [Customization](#customization-1) + [Customization](#customization-1)
* [Version Flag](#version-flag) * [Version Flag](#version-flag)
@ -39,6 +41,13 @@ cli v2 manual
<!-- tocstop --> <!-- tocstop -->
## Migrating From Older Releases
There are a small set of breaking changes between v1 and v2.
Converting is relatively straightforward and typically takes less than
an hour. Specific steps are included in
[Migration Guide: v1 to v2](../migrate-v1-to-v2.md). Also see the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v2) for v2 API documentation.
## Getting Started ## Getting Started
One of the philosophies behind cli is that an API should be playful and full of One of the philosophies behind cli is that an API should be playful and full of
@ -300,7 +309,7 @@ func main() {
} }
``` ```
See full list of flags at http://godoc.org/github.com/urfave/cli See full list of flags at https://pkg.go.dev/github.com/urfave/cli/v2
#### Placeholder Values #### Placeholder Values
@ -418,15 +427,17 @@ import (
func main() { func main() {
app := &cli.App{ app := &cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "lang, l", Name: "lang",
Value: "english", Aliases: []string{"l"},
Usage: "Language for the greeting", Value: "english",
}, Usage: "Language for the greeting",
&cli.StringFlag{ },
Name: "config, c", &cli.StringFlag{
Usage: "Load configuration from `FILE`", Name: "config",
}, Aliases: []string{"c"},
Usage: "Load configuration from `FILE`",
},
}, },
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
@ -504,7 +515,7 @@ func main() {
``` ```
If `EnvVars` contains more than one string, the first environment variable that If `EnvVars` contains more than one string, the first environment variable that
resolves is used as the default. resolves is used.
<!-- { <!-- {
"args": ["&#45;&#45;help"], "args": ["&#45;&#45;help"],
@ -563,7 +574,8 @@ func main() {
app.Flags = []cli.Flag { app.Flags = []cli.Flag {
&cli.StringFlag{ &cli.StringFlag{
Name: "password, p", Name: "password",
Aliases: []string{"p"},
Usage: "password for the mysql database", Usage: "password for the mysql database",
FilePath: "/etc/mysql/password", FilePath: "/etc/mysql/password",
}, },
@ -616,7 +628,7 @@ given sources.
Here is a more complete sample of a command using YAML support: Here is a more complete sample of a command using YAML support:
<!-- { <!-- {
"args": ["test-cmd", "&#45;&#45;help"], "args": ["&#45;&#45;help"],
"output": "&#45&#45;test value.*default: 0" "output": "&#45&#45;test value.*default: 0"
} --> } -->
``` go ``` go
@ -638,7 +650,7 @@ func main() {
app := &cli.App{ app := &cli.App{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
fmt.Println("yaml ist rad") fmt.Println("--test value.*default: 0")
return nil return nil
}, },
Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")),
@ -654,7 +666,7 @@ func main() {
You can make a flag required by setting the `Required` field to `true`. If a user You can make a flag required by setting the `Required` field to `true`. If a user
does not provide a required flag, they will be shown an error message. does not provide a required flag, they will be shown an error message.
Take for example this app that reqiures the `lang` flag: Take for example this app that requires the `lang` flag:
<!-- { <!-- {
"error": "Required flag \"lang\" not set" "error": "Required flag \"lang\" not set"
@ -665,9 +677,7 @@ package main
import ( import (
"log" "log"
"os" "os"
"strings" "github.com/urfave/cli/v2"
"github.com/urfave/cli"
) )
func main() { func main() {
@ -1013,12 +1023,12 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"github.com/urfave/cli" "github.com/urfave/cli/v2"
) )
func main() { func main() {
app := cli.NewApp() app := cli.NewApp()
app.EnableBashCompletion = true app.EnableBashCompletion = true
app.Commands = []cli.Command{ app.Commands = []*cli.Command{
{ {
Name: "add", Name: "add",
Aliases: []string{"a"}, Aliases: []string{"a"},
@ -1041,7 +1051,7 @@ func main() {
Name: "template", Name: "template",
Aliases: []string{"t"}, Aliases: []string{"t"},
Usage: "options for task templates", Usage: "options for task templates",
Subcommands: []cli.Command{ Subcommands: []*cli.Command{
{ {
Name: "add", Name: "add",
Usage: "add a new template", Usage: "add a new template",
@ -1217,6 +1227,23 @@ source path/to/autocomplete/zsh_autocomplete
#### ZSH custom auto-complete example #### ZSH custom auto-complete example
![](/docs/v2/images/custom-zsh-autocomplete.gif) ![](/docs/v2/images/custom-zsh-autocomplete.gif)
#### PowerShell Support
Auto-completion for PowerShell is also supported using the `autocomplete/powershell_autocomplete.ps1`
file included in this repo.
Rename the script to `<my program>.ps1` and move it anywhere in your file system.
The location of script does not matter, only the file name of the script has to match
the your program's binary name.
To activate it, enter `& path/to/autocomplete/<my program>.ps1`
To persist across new shells, open the PowerShell profile (with `code $profile` or `notepad $profile`)
and add the line:
```
& path/to/autocomplete/<my program>.ps1
```
### Generated Help Text ### Generated Help Text
The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked
@ -1304,7 +1331,8 @@ import (
func main() { func main() {
cli.HelpFlag = &cli.BoolFlag{ cli.HelpFlag = &cli.BoolFlag{
Name: "haaaaalp", Aliases: []string{"halp"}, Name: "haaaaalp",
Aliases: []string{"halp"},
Usage: "HALP", Usage: "HALP",
EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, EnvVars: []string{"SHOW_HALP", "HALPPLZ"},
} }
@ -1339,7 +1367,8 @@ import (
func main() { func main() {
cli.VersionFlag = &cli.BoolFlag{ cli.VersionFlag = &cli.BoolFlag{
Name: "print-version", Aliases: []string{"V"}, Name: "print-version",
Aliases: []string{"V"},
Usage: "print only the version", Usage: "print only the version",
} }

View File

@ -2,6 +2,7 @@ package cli
import ( import (
"bytes" "bytes"
"errors"
"io/ioutil" "io/ioutil"
"testing" "testing"
) )
@ -66,8 +67,50 @@ func testApp() *App {
}, { }, {
Name: "hidden-command", Name: "hidden-command",
Hidden: true, Hidden: true,
}, {
Aliases: []string{"u"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "usage",
Usage: "standard usage text",
UsageText: `
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
` + "```" + `
func() { ... }
` + "```" + `
Should be a part of the same code block
`,
Subcommands: []*Command{{
Aliases: []string{"su"},
Flags: []Flag{
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-usage",
Usage: "standard usage text",
UsageText: "Single line of UsageText",
}},
}} }}
app.UsageText = "app [first_arg] [second_arg]" app.UsageText = "app [first_arg] [second_arg]"
app.Description = `Description of the application.`
app.Usage = "Some app" app.Usage = "Some app"
app.Authors = []*Author{ app.Authors = []*Author{
{Name: "Harrison", Email: "harrison@lolwut.com"}, {Name: "Harrison", Email: "harrison@lolwut.com"},
@ -76,13 +119,13 @@ func testApp() *App {
return app return app
} }
func expectFileContent(t *testing.T, file, expected string) { func expectFileContent(t *testing.T, file, got string) {
data, err := ioutil.ReadFile(file) data, err := ioutil.ReadFile(file)
// Ignore windows line endings // Ignore windows line endings
// TODO: Replace with bytes.ReplaceAll when support for Go 1.11 is dropped // TODO: Replace with bytes.ReplaceAll when support for Go 1.11 is dropped
data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1) data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1)
expect(t, err, nil) expect(t, err, nil)
expect(t, string(data), expected) expect(t, got, string(data))
} }
func TestToMarkdownFull(t *testing.T) { func TestToMarkdownFull(t *testing.T) {
@ -136,6 +179,19 @@ func TestToMarkdownNoAuthors(t *testing.T) {
expectFileContent(t, "testdata/expected-doc-no-authors.md", res) expectFileContent(t, "testdata/expected-doc-no-authors.md", res)
} }
func TestToMarkdownNoUsageText(t *testing.T) {
// Given
app := testApp()
app.UsageText = ""
// When
res, err := app.ToMarkdown()
// Then
expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-no-usagetext.md", res)
}
func TestToMan(t *testing.T) { func TestToMan(t *testing.T) {
// Given // Given
app := testApp() app := testApp()
@ -147,3 +203,137 @@ func TestToMan(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-full.man", res) expectFileContent(t, "testdata/expected-doc-full.man", res)
} }
func TestToManParseError(t *testing.T) {
// Given
app := testApp()
// When
// temporarily change the global variable for testing
tmp := MarkdownDocTemplate
MarkdownDocTemplate = `{{ .App.Name`
_, err := app.ToMan()
MarkdownDocTemplate = tmp
// Then
expect(t, err, errors.New(`template: cli:1: unclosed action`))
}
func TestToManWithSection(t *testing.T) {
// Given
app := testApp()
// When
res, err := app.ToManWithSection(8)
// Then
expect(t, err, nil)
expectFileContent(t, "testdata/expected-doc-full.man", res)
}
func Test_prepareUsageText(t *testing.T) {
t.Run("no UsageText provided", func(t *testing.T) {
// Given
cmd := Command{}
// When
res := prepareUsageText(&cmd)
// Then
expect(t, res, "")
})
t.Run("single line UsageText", func(t *testing.T) {
// Given
cmd := Command{UsageText: "Single line usage text"}
// When
res := prepareUsageText(&cmd)
// Then
expect(t, res, ">Single line usage text\n")
})
t.Run("multiline UsageText", func(t *testing.T) {
// Given
cmd := Command{
UsageText: `
Usage for the usage text
- Should be a part of the same code block
`,
}
// When
res := prepareUsageText(&cmd)
// Then
test := ` Usage for the usage text
- Should be a part of the same code block
`
expect(t, res, test)
})
t.Run("multiline UsageText has formatted embedded markdown", func(t *testing.T) {
// Given
cmd := Command{
UsageText: `
Usage for the usage text
` + "```" + `
func() { ... }
` + "```" + `
Should be a part of the same code block
`,
}
// When
res := prepareUsageText(&cmd)
// Then
test := ` Usage for the usage text
` + "```" + `
func() { ... }
` + "```" + `
Should be a part of the same code block
`
expect(t, res, test)
})
}
func Test_prepareUsage(t *testing.T) {
t.Run("no Usage provided", func(t *testing.T) {
// Given
cmd := Command{}
// When
res := prepareUsage(&cmd, "")
// Then
expect(t, res, "")
})
t.Run("simple Usage", func(t *testing.T) {
// Given
cmd := Command{Usage: "simple usage text"}
// When
res := prepareUsage(&cmd, "")
// Then
expect(t, res, cmd.Usage+"\n")
})
t.Run("simple Usage with UsageText", func(t *testing.T) {
// Given
cmd := Command{Usage: "simple usage text"}
// When
res := prepareUsage(&cmd, "a non-empty string")
// Then
expect(t, res, cmd.Usage+"\n\n")
})
}

View File

@ -17,11 +17,10 @@ var ErrWriter io.Writer = os.Stderr
// MultiError is an error that wraps multiple errors. // MultiError is an error that wraps multiple errors.
type MultiError interface { type MultiError interface {
error error
// Errors returns a copy of the errors slice
Errors() []error Errors() []error
} }
// NewMultiError creates a new MultiError. Pass in one or more errors. // newMultiError creates a new MultiError. Pass in one or more errors.
func newMultiError(err ...error) MultiError { func newMultiError(err ...error) MultiError {
ret := multiError(err) ret := multiError(err)
return &ret return &ret
@ -48,6 +47,28 @@ func (m *multiError) Errors() []error {
return errs return errs
} }
type requiredFlagsErr interface {
error
getMissingFlags() []string
}
type errRequiredFlags struct {
missingFlags []string
}
func (e *errRequiredFlags) Error() string {
numberOfMissingFlags := len(e.missingFlags)
if numberOfMissingFlags == 1 {
return fmt.Sprintf("Required flag %q not set", e.missingFlags[0])
}
joinedMissingFlags := strings.Join(e.missingFlags, ", ")
return fmt.Sprintf("Required flags %q not set", joinedMissingFlags)
}
func (e *errRequiredFlags) getMissingFlags() []string {
return e.missingFlags
}
// ErrorFormatter is the interface that will suitably format the error output // ErrorFormatter is the interface that will suitably format the error output
type ErrorFormatter interface { type ErrorFormatter interface {
Format(s fmt.State, verb rune) Format(s fmt.State, verb rune)
@ -65,13 +86,20 @@ type exitError struct {
message interface{} message interface{}
} }
// NewExitError makes a new *exitError // NewExitError calls Exit to create a new ExitCoder.
//
// Deprecated: This function is a duplicate of Exit and will eventually be removed.
func NewExitError(message interface{}, exitCode int) ExitCoder { func NewExitError(message interface{}, exitCode int) ExitCoder {
return Exit(message, exitCode) return Exit(message, exitCode)
} }
// Exit wraps a message and exit code into an ExitCoder suitable for handling by // Exit wraps a message and exit code into an error, which by default is
// HandleExitCoder // handled with a call to os.Exit during default error handling.
//
// This is the simplest way to trigger a non-zero exit code for an App without
// having to call os.Exit manually. During testing, this behavior can be avoided
// by overiding the ExitErrHandler function on an App or the package-global
// OsExiter function.
func Exit(message interface{}, exitCode int) ExitCoder { func Exit(message interface{}, exitCode int) ExitCoder {
return &exitError{ return &exitError{
message: message, message: message,
@ -87,10 +115,14 @@ func (ee *exitError) ExitCode() int {
return ee.exitCode return ee.exitCode
} }
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if // HandleExitCoder handles errors implementing ExitCoder by printing their
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the // message and calling OsExiter with the given exit code.
// given exit code. If the given error is a MultiError, then this func is //
// called on all members of the Errors slice and calls OsExiter with the last exit code. // If the given error instead implements MultiError, each error will be checked
// for the ExitCoder interface, and OsExiter will be called with the last exit
// code found, or exit code 1 if no ExitCoder is found.
//
// This function is the default error-handling behavior for an App.
func HandleExitCoder(err error) { func HandleExitCoder(err error) {
if err == nil { if err == nil {
return return

View File

@ -67,6 +67,26 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) {
expect(t, called, true) expect(t, called, true)
} }
func TestHandleExitCoder_MultiErrorWithoutExitCoder(t *testing.T) {
exitCode := 0
called := false
OsExiter = func(rc int) {
if !called {
exitCode = rc
called = true
}
}
defer func() { OsExiter = fakeOsExiter }()
err := newMultiError(errors.New("wowsa"), errors.New("egad"))
HandleExitCoder(err)
expect(t, exitCode, 1)
expect(t, called, true)
}
// make a stub to not import pkg/errors // make a stub to not import pkg/errors
type ErrorWithFormat struct { type ErrorWithFormat struct {
error error

View File

@ -171,6 +171,10 @@ func fishAddFileFlag(flag Flag, completion *strings.Builder) {
if f.TakesFile { if f.TakesFile {
return return
} }
case *PathFlag:
if f.TakesFile {
return
}
} }
completion.WriteString(" -f") completion.WriteString(" -f")
} }

View File

@ -7,6 +7,10 @@ import (
func TestFishCompletion(t *testing.T) { func TestFishCompletion(t *testing.T) {
// Given // Given
app := testApp() app := testApp()
app.Flags = append(app.Flags, &PathFlag{
Name: "logfile",
TakesFile: true,
})
// When // When
res, err := app.ToFishCompletion() res, err := app.ToFishCompletion()

82
flag.go
View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -36,7 +37,7 @@ var VersionFlag Flag = &BoolFlag{
// HelpFlag prints the help for all commands and subcommands. // HelpFlag prints the help for all commands and subcommands.
// Set to nil to disable the flag. The subcommand // Set to nil to disable the flag. The subcommand
// will still be added unless HideHelp is set to true. // will still be added unless HideHelp or HideHelpCommand is set to true.
var HelpFlag Flag = &BoolFlag{ var HelpFlag Flag = &BoolFlag{
Name: "help", Name: "help",
Aliases: []string{"h"}, Aliases: []string{"h"},
@ -118,6 +119,14 @@ type DocGenerationFlag interface {
GetValue() string GetValue() string
} }
// VisibleFlag is an interface that allows to check if a flag is visible
type VisibleFlag interface {
Flag
// IsVisible returns true if the flag is not hidden, otherwise false
IsVisible() bool
}
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError) set := flag.NewFlagSet(name, flag.ContinueOnError)
@ -130,11 +139,52 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
return set, nil return set, nil
} }
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case Serializer:
_ = set.Set(name, ff.Value.(Serializer).Serialize())
default:
_ = set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := f.Names()
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}
func visibleFlags(fl []Flag) []Flag { func visibleFlags(fl []Flag) []Flag {
var visible []Flag var visible []Flag
for _, f := range fl { for _, f := range fl {
field := flagValue(f).FieldByName("Hidden") if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() {
if !field.IsValid() || !field.Bool() {
visible = append(visible, f) visible = append(visible, f)
} }
} }
@ -244,6 +294,10 @@ func flagValue(f Flag) reflect.Value {
return fv return fv
} }
func formatDefault(format string) string {
return " (default: " + format + ")"
}
func stringifyFlag(f Flag) string { func stringifyFlag(f Flag) string {
fv := flagValue(f) fv := flagValue(f)
@ -269,20 +323,20 @@ func stringifyFlag(f Flag) string {
val := fv.FieldByName("Value") val := fv.FieldByName("Value")
if val.IsValid() { if val.IsValid() {
needsPlaceholder = val.Kind() != reflect.Bool needsPlaceholder = val.Kind() != reflect.Bool
defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface())
if val.Kind() == reflect.String && val.String() != "" { if val.Kind() == reflect.String && val.String() != "" {
defaultValueString = fmt.Sprintf(" (default: %q)", val.String()) defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String())
} }
} }
helpText := fv.FieldByName("DefaultText") helpText := fv.FieldByName("DefaultText")
if helpText.IsValid() && helpText.String() != "" { if helpText.IsValid() && helpText.String() != "" {
needsPlaceholder = val.Kind() != reflect.Bool needsPlaceholder = val.Kind() != reflect.Bool
defaultValueString = fmt.Sprintf(" (default: %s)", helpText.String()) defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String())
} }
if defaultValueString == " (default: )" { if defaultValueString == formatDefault("") {
defaultValueString = "" defaultValueString = ""
} }
@ -351,11 +405,15 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string {
defaultVal := "" defaultVal := ""
if len(defaultVals) > 0 { if len(defaultVals) > 0 {
defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) defaultVal = fmt.Sprintf(formatDefault("%s"), strings.Join(defaultVals, ", "))
} }
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault) multiInputString := "(accepts multiple inputs)"
if usageWithDefault != "" {
multiInputString = "\t" + multiInputString
}
return fmt.Sprintf("%s\t%s%s", prefixedNames(names, placeholder), usageWithDefault, multiInputString)
} }
func hasFlag(flags []Flag, fl Flag) bool { func hasFlag(flags []Flag, fl Flag) bool {
@ -376,8 +434,10 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool)
} }
} }
for _, fileVar := range strings.Split(filePath, ",") { for _, fileVar := range strings.Split(filePath, ",") {
if data, err := ioutil.ReadFile(fileVar); err == nil { if fileVar != "" {
return string(data), true if data, err := ioutil.ReadFile(fileVar); err == nil {
return string(data), true
}
} }
} }
return "", false return "", false

View File

@ -58,6 +58,11 @@ func (f *BoolFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *BoolFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *BoolFlag) Apply(set *flag.FlagSet) error { func (f *BoolFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -87,7 +92,7 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error {
// Bool looks up the value of a local BoolFlag, returns // Bool looks up the value of a local BoolFlag, returns
// false if not found // false if not found
func (c *Context) Bool(name string) bool { func (c *Context) Bool(name string) bool {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupBool(name, fs) return lookupBool(name, fs)
} }
return false return false

View File

@ -58,6 +58,11 @@ func (f *DurationFlag) GetValue() string {
return f.Value.String() return f.Value.String()
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *DurationFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *DurationFlag) Apply(set *flag.FlagSet) error { func (f *DurationFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -86,7 +91,7 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error {
// Duration looks up the value of a local DurationFlag, returns // Duration looks up the value of a local DurationFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Duration(name string) time.Duration { func (c *Context) Duration(name string) time.Duration {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupDuration(name, fs) return lookupDuration(name, fs)
} }
return 0 return 0

View File

@ -58,12 +58,16 @@ func (f *Float64Flag) GetValue() string {
return fmt.Sprintf("%f", f.Value) return fmt.Sprintf("%f", f.Value)
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Float64Flag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Float64Flag) Apply(set *flag.FlagSet) error { func (f *Float64Flag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
if val != "" { if val != "" {
valFloat, err := strconv.ParseFloat(val, 10) valFloat, err := strconv.ParseFloat(val, 64)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err)
} }
@ -87,7 +91,7 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error {
// Float64 looks up the value of a local Float64Flag, returns // Float64 looks up the value of a local Float64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Float64(name string) float64 { func (c *Context) Float64(name string) float64 {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupFloat64(name, fs) return lookupFloat64(name, fs)
} }
return 0 return 0

View File

@ -19,6 +19,16 @@ func NewFloat64Slice(defaults ...float64) *Float64Slice {
return &Float64Slice{slice: append([]float64{}, defaults...)} return &Float64Slice{slice: append([]float64{}, defaults...)}
} }
// clone allocate a copy of self object
func (f *Float64Slice) clone() *Float64Slice {
n := &Float64Slice{
slice: make([]float64, len(f.slice)),
hasBeenSet: f.hasBeenSet,
}
copy(n.slice, f.slice)
return n
}
// Set parses the value into a float64 and appends it to the list of values // Set parses the value into a float64 and appends it to the list of values
func (f *Float64Slice) Set(value string) error { func (f *Float64Slice) Set(value string) error {
if !f.hasBeenSet { if !f.hasBeenSet {
@ -117,6 +127,11 @@ func (f *Float64SliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Float64SliceFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -129,15 +144,19 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
} }
} }
// Set this to false so that we reset the slice if we then set values from
// flags that have already been set by the environment.
f.Value.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
} }
if f.Value == nil {
f.Value = &Float64Slice{}
}
copyValue := f.Value.clone()
for _, name := range f.Names() { for _, name := range f.Names() {
if f.Value == nil { set.Var(copyValue, name, f.Usage)
f.Value = &Float64Slice{}
}
set.Var(f.Value, name, f.Usage)
} }
return nil return nil
@ -146,7 +165,7 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
// Float64Slice looks up the value of a local Float64SliceFlag, returns // Float64Slice looks up the value of a local Float64SliceFlag, returns
// nil if not found // nil if not found
func (c *Context) Float64Slice(name string) []float64 { func (c *Context) Float64Slice(name string) []float64 {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupFloat64Slice(name, fs) return lookupFloat64Slice(name, fs)
} }
return nil return nil

View File

@ -66,6 +66,11 @@ func (f *GenericFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *GenericFlag) IsVisible() bool {
return !f.Hidden
}
// 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
// provided by the user for parsing by the flag // provided by the user for parsing by the flag
func (f GenericFlag) Apply(set *flag.FlagSet) error { func (f GenericFlag) Apply(set *flag.FlagSet) error {
@ -89,7 +94,7 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error {
// Generic looks up the value of a local GenericFlag, returns // Generic looks up the value of a local GenericFlag, returns
// nil if not found // nil if not found
func (c *Context) Generic(name string) interface{} { func (c *Context) Generic(name string) interface{} {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupGeneric(name, fs) return lookupGeneric(name, fs)
} }
return nil return nil

View File

@ -58,6 +58,11 @@ func (f *IntFlag) GetValue() string {
return fmt.Sprintf("%d", f.Value) return fmt.Sprintf("%d", f.Value)
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *IntFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *IntFlag) Apply(set *flag.FlagSet) error { func (f *IntFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -87,7 +92,7 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error {
// Int looks up the value of a local IntFlag, returns // Int looks up the value of a local IntFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Int(name string) int { func (c *Context) Int(name string) int {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupInt(name, fs) return lookupInt(name, fs)
} }
return 0 return 0

View File

@ -58,6 +58,11 @@ func (f *Int64Flag) GetValue() string {
return fmt.Sprintf("%d", f.Value) return fmt.Sprintf("%d", f.Value)
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Int64Flag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Int64Flag) Apply(set *flag.FlagSet) error { func (f *Int64Flag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -86,7 +91,7 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error {
// Int64 looks up the value of a local Int64Flag, returns // Int64 looks up the value of a local Int64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Int64(name string) int64 { func (c *Context) Int64(name string) int64 {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupInt64(name, fs) return lookupInt64(name, fs)
} }
return 0 return 0

View File

@ -19,6 +19,16 @@ func NewInt64Slice(defaults ...int64) *Int64Slice {
return &Int64Slice{slice: append([]int64{}, defaults...)} return &Int64Slice{slice: append([]int64{}, defaults...)}
} }
// clone allocate a copy of self object
func (i *Int64Slice) clone() *Int64Slice {
n := &Int64Slice{
slice: make([]int64, len(i.slice)),
hasBeenSet: i.hasBeenSet,
}
copy(n.slice, i.slice)
return n
}
// Set parses the value into an integer and appends it to the list of values // Set parses the value into an integer and appends it to the list of values
func (i *Int64Slice) Set(value string) error { func (i *Int64Slice) Set(value string) error {
if !i.hasBeenSet { if !i.hasBeenSet {
@ -118,6 +128,11 @@ func (f *Int64SliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Int64SliceFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -129,14 +144,18 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
} }
} }
// Set this to false so that we reset the slice if we then set values from
// flags that have already been set by the environment.
f.Value.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
if f.Value == nil {
f.Value = &Int64Slice{}
}
copyValue := f.Value.clone()
for _, name := range f.Names() { for _, name := range f.Names() {
if f.Value == nil { set.Var(copyValue, name, f.Usage)
f.Value = &Int64Slice{}
}
set.Var(f.Value, name, f.Usage)
} }
return nil return nil
@ -145,7 +164,10 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
// Int64Slice looks up the value of a local Int64SliceFlag, returns // Int64Slice looks up the value of a local Int64SliceFlag, returns
// nil if not found // nil if not found
func (c *Context) Int64Slice(name string) []int64 { func (c *Context) Int64Slice(name string) []int64 {
return lookupInt64Slice(name, c.flagSet) if fs := c.lookupFlagSet(name); fs != nil {
return lookupInt64Slice(name, fs)
}
return nil
} }
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {

View File

@ -19,6 +19,16 @@ func NewIntSlice(defaults ...int) *IntSlice {
return &IntSlice{slice: append([]int{}, defaults...)} return &IntSlice{slice: append([]int{}, defaults...)}
} }
// clone allocate a copy of self object
func (i *IntSlice) clone() *IntSlice {
n := &IntSlice{
slice: make([]int, len(i.slice)),
hasBeenSet: i.hasBeenSet,
}
copy(n.slice, i.slice)
return n
}
// TODO: Consistently have specific Set function for Int64 and Float64 ? // TODO: Consistently have specific Set function for Int64 and Float64 ?
// SetInt directly adds an integer to the list of values // SetInt directly adds an integer to the list of values
func (i *IntSlice) SetInt(value int) { func (i *IntSlice) SetInt(value int) {
@ -129,6 +139,11 @@ func (f *IntSliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *IntSliceFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -140,14 +155,18 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
} }
} }
// Set this to false so that we reset the slice if we then set values from
// flags that have already been set by the environment.
f.Value.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
if f.Value == nil {
f.Value = &IntSlice{}
}
copyValue := f.Value.clone()
for _, name := range f.Names() { for _, name := range f.Names() {
if f.Value == nil { set.Var(copyValue, name, f.Usage)
f.Value = &IntSlice{}
}
set.Var(f.Value, name, f.Usage)
} }
return nil return nil
@ -156,8 +175,8 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
// IntSlice looks up the value of a local IntSliceFlag, returns // IntSlice looks up the value of a local IntSliceFlag, returns
// nil if not found // nil if not found
func (c *Context) IntSlice(name string) []int { func (c *Context) IntSlice(name string) []int {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupIntSlice(name, c.flagSet) return lookupIntSlice(name, fs)
} }
return nil return nil
} }

View File

@ -54,6 +54,11 @@ func (f *PathFlag) GetValue() string {
return f.Value return f.Value
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *PathFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *PathFlag) Apply(set *flag.FlagSet) error { func (f *PathFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -75,7 +80,7 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error {
// Path looks up the value of a local PathFlag, returns // Path looks up the value of a local PathFlag, returns
// "" if not found // "" if not found
func (c *Context) Path(name string) string { func (c *Context) Path(name string) string {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupPath(name, fs) return lookupPath(name, fs)
} }

View File

@ -55,6 +55,11 @@ func (f *StringFlag) GetValue() string {
return f.Value return f.Value
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *StringFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *StringFlag) Apply(set *flag.FlagSet) error { func (f *StringFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -76,7 +81,7 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error {
// String looks up the value of a local StringFlag, returns // String looks up the value of a local StringFlag, returns
// "" if not found // "" if not found
func (c *Context) String(name string) string { func (c *Context) String(name string) string {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupString(name, fs) return lookupString(name, fs)
} }
return "" return ""
@ -85,10 +90,7 @@ func (c *Context) String(name string) string {
func lookupString(name string, set *flag.FlagSet) string { func lookupString(name string, set *flag.FlagSet) string {
f := set.Lookup(name) f := set.Lookup(name)
if f != nil { if f != nil {
parsed, err := f.Value.String(), error(nil) parsed := f.Value.String()
if err != nil {
return ""
}
return parsed return parsed
} }
return "" return ""

View File

@ -18,6 +18,16 @@ func NewStringSlice(defaults ...string) *StringSlice {
return &StringSlice{slice: append([]string{}, defaults...)} return &StringSlice{slice: append([]string{}, defaults...)}
} }
// clone allocate a copy of self object
func (s *StringSlice) clone() *StringSlice {
n := &StringSlice{
slice: make([]string, len(s.slice)),
hasBeenSet: s.hasBeenSet,
}
copy(n.slice, s.slice)
return n
}
// Set appends the string value to the list of values // Set appends the string value to the list of values
func (s *StringSlice) Set(value string) error { func (s *StringSlice) Set(value string) error {
if !s.hasBeenSet { if !s.hasBeenSet {
@ -114,10 +124,24 @@ func (f *StringSliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *StringSliceFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
if f.Destination != nil && f.Value != nil {
f.Destination.slice = make([]string, len(f.Value.slice))
copy(f.Destination.slice, f.Value.slice)
}
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
f.Value = &StringSlice{} if f.Value == nil {
f.Value = &StringSlice{}
}
destination := f.Value destination := f.Value
if f.Destination != nil { if f.Destination != nil {
destination = f.Destination destination = f.Destination
@ -135,17 +159,15 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
f.HasBeenSet = true f.HasBeenSet = true
} }
if f.Value == nil {
f.Value = &StringSlice{}
}
setValue := f.Destination
if f.Destination == nil {
setValue = f.Value.clone()
}
for _, name := range f.Names() { for _, name := range f.Names() {
if f.Value == nil { set.Var(setValue, name, f.Usage)
f.Value = &StringSlice{}
}
if f.Destination != nil {
set.Var(f.Destination, name, f.Usage)
continue
}
set.Var(f.Value, name, f.Usage)
} }
return nil return nil
@ -154,7 +176,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
// StringSlice looks up the value of a local StringSliceFlag, returns // StringSlice looks up the value of a local StringSliceFlag, returns
// nil if not found // nil if not found
func (c *Context) StringSlice(name string) []string { func (c *Context) StringSlice(name string) []string {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupStringSlice(name, fs) return lookupStringSlice(name, fs)
} }
return nil return nil

View File

@ -22,6 +22,13 @@ var boolFlagTests = []struct {
{"h", "-h\t(default: false)"}, {"h", "-h\t(default: false)"},
} }
func resetEnv(env []string) {
for _, e := range env {
fields := strings.SplitN(e, "=", 2)
os.Setenv(fields[0], fields[1])
}
}
func TestBoolFlagHelpOutput(t *testing.T) { func TestBoolFlagHelpOutput(t *testing.T) {
for _, test := range boolFlagTests { for _, test := range boolFlagTests {
fl := &BoolFlag{Name: test.name} fl := &BoolFlag{Name: test.name}
@ -45,15 +52,21 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) {
} }
func TestFlagsFromEnv(t *testing.T) { func TestFlagsFromEnv(t *testing.T) {
newSetFloat64Slice := func(defaults ...float64) Float64Slice {
s := NewFloat64Slice(defaults...)
s.hasBeenSet = false
return *s
}
newSetIntSlice := func(defaults ...int) IntSlice { newSetIntSlice := func(defaults ...int) IntSlice {
s := NewIntSlice(defaults...) s := NewIntSlice(defaults...)
s.hasBeenSet = true s.hasBeenSet = false
return *s return *s
} }
newSetInt64Slice := func(defaults ...int64) Int64Slice { newSetInt64Slice := func(defaults ...int64) Int64Slice {
s := NewInt64Slice(defaults...) s := NewInt64Slice(defaults...)
s.hasBeenSet = true s.hasBeenSet = false
return *s return *s
} }
@ -89,6 +102,9 @@ func TestFlagsFromEnv(t *testing.T) {
{"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, {"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`},
{"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`},
{"1.0,2", newSetFloat64Slice(1, 2), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "\[\]float64{}" as float64 slice value for flag seconds: .*`},
{"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`}, {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`},
{"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`}, {"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`},
@ -114,6 +130,7 @@ func TestFlagsFromEnv(t *testing.T) {
} }
for i, test := range flagTests { for i, test := range flagTests {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1) envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1)
_ = os.Setenv(envVarSlice.Index(0).String(), test.input) _ = os.Setenv(envVarSlice.Index(0).String(), test.input)
@ -183,7 +200,7 @@ func TestStringFlagDefaultText(t *testing.T) {
} }
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_FOO", "derp") _ = os.Setenv("APP_FOO", "derp")
@ -263,6 +280,7 @@ func TestPathFlagHelpOutput(t *testing.T) {
} }
func TestPathFlagWithEnvVarHelpOutput(t *testing.T) { func TestPathFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_PATH", "/path/to/file") _ = os.Setenv("APP_PATH", "/path/to/file")
for _, test := range pathFlagTests { for _, test := range pathFlagTests {
@ -331,11 +349,11 @@ var stringSliceFlagTests = []struct {
value *StringSlice value *StringSlice
expected string expected string
}{ }{
{"foo", nil, NewStringSlice(""), "--foo value\t"}, {"foo", nil, NewStringSlice(""), "--foo value\t(accepts multiple inputs)"},
{"f", nil, NewStringSlice(""), "-f value\t"}, {"f", nil, NewStringSlice(""), "-f value\t(accepts multiple inputs)"},
{"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")"}, {"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")\t(accepts multiple inputs)"},
{"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")"}, {"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")\t(accepts multiple inputs)"},
{"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")"}, {"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")\t(accepts multiple inputs)"},
} }
func TestStringSliceFlagHelpOutput(t *testing.T) { func TestStringSliceFlagHelpOutput(t *testing.T) {
@ -350,6 +368,7 @@ func TestStringSliceFlagHelpOutput(t *testing.T) {
} }
func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_QWWX", "11,4") _ = os.Setenv("APP_QWWX", "11,4")
@ -376,6 +395,32 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
} }
func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("MY_GOAT", "vincent van goat,scape goat")
var val StringSlice
fl := StringSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse(nil)
expect(t, err, nil)
expect(t, val.Value(), NewStringSlice("vincent van goat", "scape goat").Value())
}
func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) {
defValue := []string{"UA", "US"}
fl := StringSliceFlag{Name: "country", Value: NewStringSlice(defValue...), Destination: NewStringSlice("CA")}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse([]string{})
expect(t, err, nil)
expect(t, defValue, fl.Destination.Value())
}
var intFlagTests = []struct { var intFlagTests = []struct {
name string name string
expected string expected string
@ -396,6 +441,7 @@ func TestIntFlagHelpOutput(t *testing.T) {
} }
func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_BAR", "2") _ = os.Setenv("APP_BAR", "2")
@ -444,6 +490,7 @@ func TestInt64FlagHelpOutput(t *testing.T) {
} }
func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) { func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_BAR", "2") _ = os.Setenv("APP_BAR", "2")
@ -481,6 +528,7 @@ func TestUintFlagHelpOutput(t *testing.T) {
} }
func TestUintFlagWithEnvVarHelpOutput(t *testing.T) { func TestUintFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_BAR", "2") _ = os.Setenv("APP_BAR", "2")
@ -518,6 +566,7 @@ func TestUint64FlagHelpOutput(t *testing.T) {
} }
func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) { func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_BAR", "2") _ = os.Setenv("APP_BAR", "2")
@ -555,6 +604,7 @@ func TestDurationFlagHelpOutput(t *testing.T) {
} }
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_BAR", "2h3m6s") _ = os.Setenv("APP_BAR", "2h3m6s")
@ -589,9 +639,9 @@ var intSliceFlagTests = []struct {
value *IntSlice value *IntSlice
expected string expected string
}{ }{
{"heads", nil, NewIntSlice(), "--heads value\t"}, {"heads", nil, NewIntSlice(), "--heads value\t(accepts multiple inputs)"},
{"H", nil, NewIntSlice(), "-H value\t"}, {"H", nil, NewIntSlice(), "-H value\t(accepts multiple inputs)"},
{"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)"}, {"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)\t(accepts multiple inputs)"},
} }
func TestIntSliceFlagHelpOutput(t *testing.T) { func TestIntSliceFlagHelpOutput(t *testing.T) {
@ -606,6 +656,7 @@ func TestIntSliceFlagHelpOutput(t *testing.T) {
} }
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_SMURF", "42,3") _ = os.Setenv("APP_SMURF", "42,3")
@ -632,16 +683,55 @@ func TestIntSliceFlagApply_SetsAllNames(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
} }
func TestIntSliceFlagApply_ParentContext(t *testing.T) {
_ = (&App{
Flags: []Flag{
&IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewIntSlice(1, 2, 3)},
},
Commands: []*Command{
{
Name: "child",
Action: func(ctx *Context) error {
expected := []int{1, 2, 3}
if !reflect.DeepEqual(ctx.IntSlice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("numbers"))
}
if !reflect.DeepEqual(ctx.IntSlice("n"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("n"))
}
return nil
},
},
},
}).Run([]string{"run", "child"})
}
func TestIntSliceFlag_SetFromParentContext(t *testing.T) {
fl := &IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewIntSlice(1, 2, 3, 4)}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
ctx := &Context{
parentContext: &Context{
flagSet: set,
},
flagSet: flag.NewFlagSet("empty", 0),
}
expected := []int{1, 2, 3, 4}
if !reflect.DeepEqual(ctx.IntSlice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("numbers"))
}
}
var int64SliceFlagTests = []struct { var int64SliceFlagTests = []struct {
name string name string
aliases []string aliases []string
value *Int64Slice value *Int64Slice
expected string expected string
}{ }{
{"heads", nil, NewInt64Slice(), "--heads value\t"}, {"heads", nil, NewInt64Slice(), "--heads value\t(accepts multiple inputs)"},
{"H", nil, NewInt64Slice(), "-H value\t"}, {"H", nil, NewInt64Slice(), "-H value\t(accepts multiple inputs)"},
{"heads", []string{"H"}, NewInt64Slice(int64(2), int64(17179869184)), {"heads", []string{"H"}, NewInt64Slice(int64(2), int64(17179869184)),
"--heads value, -H value\t(default: 2, 17179869184)"}, "--heads value, -H value\t(default: 2, 17179869184)\t(accepts multiple inputs)"},
} }
func TestInt64SliceFlagHelpOutput(t *testing.T) { func TestInt64SliceFlagHelpOutput(t *testing.T) {
@ -656,6 +746,7 @@ func TestInt64SliceFlagHelpOutput(t *testing.T) {
} }
func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) { func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_SMURF", "42,17179869184") _ = os.Setenv("APP_SMURF", "42,17179869184")
@ -673,6 +764,60 @@ func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
func TestInt64SliceFlagApply_ParentContext(t *testing.T) {
_ = (&App{
Flags: []Flag{
&Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewInt64Slice(1, 2, 3)},
},
Commands: []*Command{
{
Name: "child",
Action: func(ctx *Context) error {
expected := []int64{1, 2, 3}
if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers"))
}
if !reflect.DeepEqual(ctx.Int64Slice("n"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("n"))
}
return nil
},
},
},
}).Run([]string{"run", "child"})
}
func TestInt64SliceFlag_SetFromParentContext(t *testing.T) {
fl := &Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewInt64Slice(1, 2, 3, 4)}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
ctx := &Context{
parentContext: &Context{
flagSet: set,
},
flagSet: flag.NewFlagSet("empty", 0),
}
expected := []int64{1, 2, 3, 4}
if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers"))
}
}
func TestInt64SliceFlag_ReturnNil(t *testing.T) {
fl := &Int64SliceFlag{}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
ctx := &Context{
parentContext: &Context{
flagSet: set,
},
flagSet: flag.NewFlagSet("empty", 0),
}
expected := []int64(nil)
if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers"))
}
}
var float64FlagTests = []struct { var float64FlagTests = []struct {
name string name string
expected string expected string
@ -693,6 +838,7 @@ func TestFloat64FlagHelpOutput(t *testing.T) {
} }
func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_BAZ", "99.4") _ = os.Setenv("APP_BAZ", "99.4")
@ -727,10 +873,10 @@ var float64SliceFlagTests = []struct {
value *Float64Slice value *Float64Slice
expected string expected string
}{ }{
{"heads", nil, NewFloat64Slice(), "--heads value\t"}, {"heads", nil, NewFloat64Slice(), "--heads value\t(accepts multiple inputs)"},
{"H", nil, NewFloat64Slice(), "-H value\t"}, {"H", nil, NewFloat64Slice(), "-H value\t(accepts multiple inputs)"},
{"heads", []string{"H"}, NewFloat64Slice(0.1234, -10.5), {"heads", []string{"H"}, NewFloat64Slice(0.1234, -10.5),
"--heads value, -H value\t(default: 0.1234, -10.5)"}, "--heads value, -H value\t(default: 0.1234, -10.5)\t(accepts multiple inputs)"},
} }
func TestFloat64SliceFlagHelpOutput(t *testing.T) { func TestFloat64SliceFlagHelpOutput(t *testing.T) {
@ -745,6 +891,7 @@ func TestFloat64SliceFlagHelpOutput(t *testing.T) {
} }
func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_SMURF", "0.1234,-10.5") _ = os.Setenv("APP_SMURF", "0.1234,-10.5")
for _, test := range float64SliceFlagTests { for _, test := range float64SliceFlagTests {
@ -782,6 +929,7 @@ func TestGenericFlagHelpOutput(t *testing.T) {
} }
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_ZAP", "3") _ = os.Setenv("APP_ZAP", "3")
@ -844,6 +992,7 @@ func TestParseDestinationString(t *testing.T) {
} }
func TestParseMultiStringFromEnv(t *testing.T) { func TestParseMultiStringFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_COUNT", "20") _ = os.Setenv("APP_COUNT", "20")
_ = (&App{ _ = (&App{
@ -863,6 +1012,7 @@ func TestParseMultiStringFromEnv(t *testing.T) {
} }
func TestParseMultiStringFromEnvCascade(t *testing.T) { func TestParseMultiStringFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_COUNT", "20") _ = os.Setenv("APP_COUNT", "20")
_ = (&App{ _ = (&App{
@ -937,6 +1087,7 @@ func TestParseMultiStringSliceWithDestination(t *testing.T) {
} }
func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) { func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -976,6 +1127,7 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) {
} }
func TestParseMultiStringSliceFromEnv(t *testing.T) { func TestParseMultiStringSliceFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -996,6 +1148,7 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) {
} }
func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1016,6 +1169,7 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
} }
func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1036,6 +1190,7 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
} }
func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1056,6 +1211,7 @@ func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
} }
func TestParseMultiStringSliceFromEnvWithDestination(t *testing.T) { func TestParseMultiStringSliceFromEnvWithDestination(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1112,6 +1268,7 @@ func TestParseDestinationInt(t *testing.T) {
} }
func TestParseMultiIntFromEnv(t *testing.T) { func TestParseMultiIntFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_TIMEOUT_SECONDS", "10") _ = os.Setenv("APP_TIMEOUT_SECONDS", "10")
_ = (&App{ _ = (&App{
@ -1131,6 +1288,7 @@ func TestParseMultiIntFromEnv(t *testing.T) {
} }
func TestParseMultiIntFromEnvCascade(t *testing.T) { func TestParseMultiIntFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_TIMEOUT_SECONDS", "10") _ = os.Setenv("APP_TIMEOUT_SECONDS", "10")
_ = (&App{ _ = (&App{
@ -1201,6 +1359,7 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) {
} }
func TestParseMultiIntSliceFromEnv(t *testing.T) { func TestParseMultiIntSliceFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1221,6 +1380,7 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) {
} }
func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1241,6 +1401,7 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
} }
func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { func TestParseMultiIntSliceFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40") _ = os.Setenv("APP_INTERVALS", "20,30,40")
@ -1278,6 +1439,7 @@ func TestParseMultiInt64Slice(t *testing.T) {
} }
func TestParseMultiInt64SliceFromEnv(t *testing.T) { func TestParseMultiInt64SliceFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,17179869184") _ = os.Setenv("APP_INTERVALS", "20,30,17179869184")
@ -1298,6 +1460,7 @@ func TestParseMultiInt64SliceFromEnv(t *testing.T) {
} }
func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) { func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,17179869184") _ = os.Setenv("APP_INTERVALS", "20,30,17179869184")
@ -1353,6 +1516,7 @@ func TestParseDestinationFloat64(t *testing.T) {
} }
func TestParseMultiFloat64FromEnv(t *testing.T) { func TestParseMultiFloat64FromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
_ = (&App{ _ = (&App{
@ -1372,6 +1536,7 @@ func TestParseMultiFloat64FromEnv(t *testing.T) {
} }
func TestParseMultiFloat64FromEnvCascade(t *testing.T) { func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
_ = (&App{ _ = (&App{
@ -1391,6 +1556,7 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
} }
func TestParseMultiFloat64SliceFromEnv(t *testing.T) { func TestParseMultiFloat64SliceFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "0.1,-10.5") _ = os.Setenv("APP_INTERVALS", "0.1,-10.5")
@ -1411,6 +1577,7 @@ func TestParseMultiFloat64SliceFromEnv(t *testing.T) {
} }
func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "0.1234,-10.5") _ = os.Setenv("APP_INTERVALS", "0.1234,-10.5")
@ -1490,6 +1657,7 @@ func TestParseDestinationBool(t *testing.T) {
} }
func TestParseMultiBoolFromEnv(t *testing.T) { func TestParseMultiBoolFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_DEBUG", "1") _ = os.Setenv("APP_DEBUG", "1")
_ = (&App{ _ = (&App{
@ -1509,6 +1677,7 @@ func TestParseMultiBoolFromEnv(t *testing.T) {
} }
func TestParseMultiBoolFromEnvCascade(t *testing.T) { func TestParseMultiBoolFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_DEBUG", "1") _ = os.Setenv("APP_DEBUG", "1")
_ = (&App{ _ = (&App{
@ -1539,6 +1708,7 @@ func TestParseBoolFromEnv(t *testing.T) {
} }
for _, test := range boolFlagTests { for _, test := range boolFlagTests {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("DEBUG", test.input) _ = os.Setenv("DEBUG", test.input)
_ = (&App{ _ = (&App{
@ -1615,6 +1785,7 @@ func TestParseGeneric(t *testing.T) {
} }
func TestParseGenericFromEnv(t *testing.T) { func TestParseGenericFromEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_SERVE", "20,30") _ = os.Setenv("APP_SERVE", "20,30")
_ = (&App{ _ = (&App{
@ -1639,6 +1810,7 @@ func TestParseGenericFromEnv(t *testing.T) {
} }
func TestParseGenericFromEnvCascade(t *testing.T) { func TestParseGenericFromEnvCascade(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("APP_FOO", "99,2000") _ = os.Setenv("APP_FOO", "99,2000")
_ = (&App{ _ = (&App{
@ -1659,14 +1831,16 @@ func TestParseGenericFromEnvCascade(t *testing.T) {
} }
func TestFlagFromFile(t *testing.T) { func TestFlagFromFile(t *testing.T) {
os.Clearenv()
os.Setenv("APP_FOO", "123")
temp, err := ioutil.TempFile("", "urfave_cli_test") temp, err := ioutil.TempFile("", "urfave_cli_test")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
defer resetEnv(os.Environ())
os.Clearenv()
os.Setenv("APP_FOO", "123")
_, _ = io.WriteString(temp, "abc") _, _ = io.WriteString(temp, "abc")
_ = temp.Close() _ = temp.Close()
defer func() { defer func() {
@ -1778,6 +1952,17 @@ func TestTimestampFlagApply(t *testing.T) {
expect(t, *fl.Value.timestamp, expectedResult) expect(t, *fl.Value.timestamp, expectedResult)
} }
func TestTimestampFlagApplyValue(t *testing.T) {
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: time.RFC3339, Value: NewTimestamp(expectedResult)}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse([]string{""})
expect(t, err, nil)
expect(t, *fl.Value.timestamp, expectedResult)
}
func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) { func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) {
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: "randomlayout"} fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: "randomlayout"}
set := flag.NewFlagSet("test", 0) set := flag.NewFlagSet("test", 0)
@ -1797,3 +1982,80 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"}) err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"})
expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\"")) expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\""))
} }
type flagDefaultTestCase struct {
name string
flag Flag
toParse []string
expect string
}
func TestFlagDefaultValue(t *testing.T) {
cases := []*flagDefaultTestCase{
&flagDefaultTestCase{
name: "stringSclice",
flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "float64Sclice",
flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
toParse: []string{"--flag", "13.3"},
expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "int64Sclice",
flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "intSclice",
flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "string",
flag: &StringFlag{Name: "flag", Value: "default"},
toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default")`,
},
&flagDefaultTestCase{
name: "bool",
flag: &BoolFlag{Name: "flag", Value: true},
toParse: []string{"--flag", "false"},
expect: `--flag (default: true)`,
},
&flagDefaultTestCase{
name: "uint64",
flag: &Uint64Flag{Name: "flag", Value: 1},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1)`,
},
}
for i, v := range cases {
set := flag.NewFlagSet("test", 0)
set.SetOutput(ioutil.Discard)
_ = v.flag.Apply(set)
if err := set.Parse(v.toParse); err != nil {
t.Error(err)
}
if got := v.flag.String(); got != v.expect {
t.Errorf("TestFlagDefaultValue %d %s\nexpect:%s\ngot:%s", i, v.name, v.expect, got)
}
}
}
func TestTimestampFlagApply_WithDestination(t *testing.T) {
var destination Timestamp
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: time.RFC3339, Destination: &destination}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"})
expect(t, err, nil)
expect(t, *fl.Destination.timestamp, expectedResult)
}

View File

@ -71,6 +71,7 @@ type TimestampFlag struct {
Value *Timestamp Value *Timestamp
DefaultText string DefaultText string
HasBeenSet bool HasBeenSet bool
Destination *Timestamp
} }
// IsSet returns whether or not the flag has been set through env or file // IsSet returns whether or not the flag has been set through env or file
@ -113,14 +114,25 @@ func (f *TimestampFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *TimestampFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *TimestampFlag) Apply(set *flag.FlagSet) error { func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
if f.Layout == "" { if f.Layout == "" {
return fmt.Errorf("timestamp Layout is required") return fmt.Errorf("timestamp Layout is required")
} }
f.Value = &Timestamp{} if f.Value == nil {
f.Value = &Timestamp{}
}
f.Value.SetLayout(f.Layout) f.Value.SetLayout(f.Layout)
if f.Destination != nil {
f.Destination.SetLayout(f.Layout)
}
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
if err := f.Value.Set(val); err != nil { if err := f.Value.Set(val); err != nil {
return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err)
@ -129,6 +141,11 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
} }
for _, name := range f.Names() { for _, name := range f.Names() {
if f.Destination != nil {
set.Var(f.Destination, name, f.Usage)
continue
}
set.Var(f.Value, name, f.Usage) set.Var(f.Value, name, f.Usage)
} }
return nil return nil
@ -136,7 +153,7 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
// Timestamp gets the timestamp from a flag name // Timestamp gets the timestamp from a flag name
func (c *Context) Timestamp(name string) *time.Time { func (c *Context) Timestamp(name string) *time.Time {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupTimestamp(name, fs) return lookupTimestamp(name, fs)
} }
return nil return nil

View File

@ -52,6 +52,11 @@ func (f *UintFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *UintFlag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *UintFlag) Apply(set *flag.FlagSet) error { func (f *UintFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -86,7 +91,7 @@ func (f *UintFlag) GetValue() string {
// Uint looks up the value of a local UintFlag, returns // Uint looks up the value of a local UintFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Uint(name string) uint { func (c *Context) Uint(name string) uint {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupUint(name, fs) return lookupUint(name, fs)
} }
return 0 return 0

View File

@ -52,6 +52,11 @@ func (f *Uint64Flag) GetUsage() string {
return f.Usage return f.Usage
} }
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Uint64Flag) IsVisible() bool {
return !f.Hidden
}
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Uint64Flag) Apply(set *flag.FlagSet) error { func (f *Uint64Flag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -86,7 +91,7 @@ func (f *Uint64Flag) GetValue() string {
// Uint64 looks up the value of a local Uint64Flag, returns // Uint64 looks up the value of a local Uint64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Uint64(name string) uint64 { func (c *Context) Uint64(name string) uint64 {
if fs := lookupFlagSet(name, c); fs != nil { if fs := c.lookupFlagSet(name); fs != nil {
return lookupUint64(name, fs) return lookupUint64(name, fs)
} }
return 0 return 0

View File

@ -17,7 +17,7 @@ type ActionFunc func(*Context) error
// CommandNotFoundFunc is executed if the proper command cannot be found // CommandNotFoundFunc is executed if the proper command cannot be found
type CommandNotFoundFunc func(*Context, string) type CommandNotFoundFunc func(*Context, string)
// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying // OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying
// customized usage error messages. This function is able to replace the // customized usage error messages. This function is able to replace the
// original error messages. If this function is not set, the "Incorrect usage" // original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted. // is displayed and the execution is interrupted.

4
go.mod
View File

@ -5,6 +5,6 @@ go 1.11
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d github.com/cpuguy83/go-md2man/v2 v2.0.1
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.8
) )

12
go.sum
View File

@ -2,15 +2,15 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c h1:CucViv7orgFBMkehuFFdkCVF5ERovbkRRyhvaYaHu/k= github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c h1:CucViv7orgFBMkehuFFdkCVF5ERovbkRRyhvaYaHu/k=
github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c/go.mod h1:bV/CkX4+ANGDaBwbHkt9kK287al/i9BsB18PRBvyqYo= github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c/go.mod h1:bV/CkX4+ANGDaBwbHkt9kK287al/i9BsB18PRBvyqYo=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

30
help.go
View File

@ -77,13 +77,13 @@ func ShowAppHelpAndExit(c *Context, exitCode int) {
// ShowAppHelp is an action that displays the help. // ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) error { func ShowAppHelp(c *Context) error {
template := c.App.CustomAppHelpTemplate tpl := c.App.CustomAppHelpTemplate
if template == "" { if tpl == "" {
template = AppHelpTemplate tpl = AppHelpTemplate
} }
if c.App.ExtraInfo == nil { if c.App.ExtraInfo == nil {
HelpPrinter(c.App.Writer, template, c.App) HelpPrinter(c.App.Writer, tpl, c.App)
return nil return nil
} }
@ -92,7 +92,7 @@ func ShowAppHelp(c *Context) error {
"ExtraInfo": c.App.ExtraInfo, "ExtraInfo": c.App.ExtraInfo,
} }
} }
HelpPrinterCustom(c.App.Writer, template, c.App, customAppData()) HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData())
return nil return nil
} }
@ -225,6 +225,12 @@ func ShowCommandHelp(ctx *Context, command string) error {
return nil return nil
} }
// ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code.
func ShowSubcommandHelpAndExit(c *Context, exitCode int) {
_ = ShowSubcommandHelp(c)
os.Exit(exitCode)
}
// ShowSubcommandHelp prints help for the given subcommand // ShowSubcommandHelp prints help for the given subcommand
func ShowSubcommandHelp(c *Context) error { func ShowSubcommandHelp(c *Context) error {
if c == nil { if c == nil {
@ -274,7 +280,10 @@ func ShowCommandCompletions(ctx *Context, command string) {
// allow using arbitrary functions in template rendering. // allow using arbitrary functions in template rendering.
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
funcMap := template.FuncMap{ funcMap := template.FuncMap{
"join": strings.Join, "join": strings.Join,
"indent": indent,
"nindent": nindent,
"trim": strings.TrimSpace,
} }
for key, value := range customFuncs { for key, value := range customFuncs {
funcMap[key] = value funcMap[key] = value
@ -377,3 +386,12 @@ func checkCommandCompletions(c *Context, name string) bool {
ShowCommandCompletions(c, name) ShowCommandCompletions(c, name)
return true return true
} }
func indent(spaces int, v string) string {
pad := strings.Repeat(" ", spaces)
return pad + strings.Replace(v, "\n", "\n"+pad, -1)
}
func nindent(spaces int, v string) string {
return "\n" + indent(spaces, v)
}

View File

@ -5,6 +5,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
@ -53,6 +54,22 @@ func Test_ShowAppHelp_HideVersion(t *testing.T) {
} }
} }
func Test_ShowAppHelp_MultiLineDescription(t *testing.T) {
output := new(bytes.Buffer)
app := &App{Writer: output}
app.HideVersion = true
app.Description = "multi\n line"
c := NewContext(app, nil, nil)
_ = ShowAppHelp(c)
if !bytes.Contains(output.Bytes(), []byte("DESCRIPTION:\n multi\n line")) {
t.Errorf("expected\n%s\nto include\n%s", output.String(), "DESCRIPTION:\n multi\n line")
}
}
func Test_Help_Custom_Flags(t *testing.T) { func Test_Help_Custom_Flags(t *testing.T) {
oldFlag := HelpFlag oldFlag := HelpFlag
defer func() { defer func() {
@ -494,6 +511,36 @@ func TestShowSubcommandHelp_CommandUsageText(t *testing.T) {
} }
} }
func TestShowSubcommandHelp_MultiLine_CommandUsageText(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
UsageText: `This is a
multi
line
UsageText`,
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo", "frobbly", "--help"})
expected := `USAGE:
This is a
multi
line
UsageText
`
if !strings.Contains(output.String(), expected) {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) {
app := &App{ app := &App{
Commands: []*Command{ Commands: []*Command{
@ -518,6 +565,40 @@ func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) {
} }
} }
func TestShowSubcommandHelp_MultiLine_SubcommandUsageText(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
Subcommands: []*Command{
{
Name: "bobbly",
UsageText: `This is a
multi
line
UsageText`,
},
},
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo", "frobbly", "bobbly", "--help"})
expected := `USAGE:
This is a
multi
line
UsageText
`
if !strings.Contains(output.String(), expected) {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowAppHelp_HiddenCommand(t *testing.T) { func TestShowAppHelp_HiddenCommand(t *testing.T) {
app := &App{ app := &App{
Commands: []*Command{ Commands: []*Command{
@ -762,3 +843,197 @@ VERSION:
t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String()) t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String())
} }
} }
func TestShowAppHelp_UsageText(t *testing.T) {
app := &App{
UsageText: "This is a sinlge line of UsageText",
Commands: []*Command{
{
Name: "frobbly",
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo"})
if !strings.Contains(output.String(), "This is a sinlge line of UsageText") {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowAppHelp_MultiLine_UsageText(t *testing.T) {
app := &App{
UsageText: `This is a
multi
line
App UsageText`,
Commands: []*Command{
{
Name: "frobbly",
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo"})
expected := `USAGE:
This is a
multi
line
App UsageText
`
if !strings.Contains(output.String(), expected) {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestHideHelpCommand(t *testing.T) {
app := &App{
HideHelpCommand: true,
Writer: ioutil.Discard,
}
err := app.Run([]string{"foo", "help"})
if err == nil {
t.Fatalf("expected a non-nil error")
}
if !strings.Contains(err.Error(), "No help topic for 'help'") {
t.Errorf("Run returned unexpected error: %v", err)
}
err = app.Run([]string{"foo", "--help"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestHideHelpCommand_False(t *testing.T) {
app := &App{
HideHelpCommand: false,
Writer: ioutil.Discard,
}
err := app.Run([]string{"foo", "help"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
err = app.Run([]string{"foo", "--help"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestHideHelpCommand_WithHideHelp(t *testing.T) {
app := &App{
HideHelp: true, // effective (hides both command and flag)
HideHelpCommand: true, // ignored
Writer: ioutil.Discard,
}
err := app.Run([]string{"foo", "help"})
if err == nil {
t.Fatalf("expected a non-nil error")
}
if !strings.Contains(err.Error(), "No help topic for 'help'") {
t.Errorf("Run returned unexpected error: %v", err)
}
err = app.Run([]string{"foo", "--help"})
if err == nil {
t.Fatalf("expected a non-nil error")
}
if !strings.Contains(err.Error(), "flag: help requested") {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func newContextFromStringSlice(ss []string) *Context {
set := flag.NewFlagSet("", flag.ContinueOnError)
_ = set.Parse(ss)
return &Context{flagSet: set}
}
func TestHideHelpCommand_RunAsSubcommand(t *testing.T) {
app := &App{
HideHelpCommand: true,
Writer: ioutil.Discard,
Commands: []*Command{
{
Name: "dummy",
},
},
}
err := app.RunAsSubcommand(newContextFromStringSlice([]string{"", "help"}))
if err == nil {
t.Fatalf("expected a non-nil error")
}
if !strings.Contains(err.Error(), "No help topic for 'help'") {
t.Errorf("Run returned unexpected error: %v", err)
}
err = app.RunAsSubcommand(newContextFromStringSlice([]string{"", "--help"}))
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestHideHelpCommand_RunAsSubcommand_False(t *testing.T) {
app := &App{
HideHelpCommand: false,
Writer: ioutil.Discard,
Commands: []*Command{
{
Name: "dummy",
},
},
}
err := app.RunAsSubcommand(newContextFromStringSlice([]string{"", "help"}))
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
err = app.RunAsSubcommand(newContextFromStringSlice([]string{"", "--help"}))
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}
func TestHideHelpCommand_WithSubcommands(t *testing.T) {
app := &App{
Writer: ioutil.Discard,
Commands: []*Command{
{
Name: "dummy",
Subcommands: []*Command{
{
Name: "dummy2",
},
},
HideHelpCommand: true,
},
},
}
err := app.Run([]string{"foo", "dummy", "help"})
if err == nil {
t.Fatalf("expected a non-nil error")
}
if !strings.Contains(err.Error(), "No help topic for 'help'") {
t.Errorf("Run returned unexpected error: %v", err)
}
err = app.Run([]string{"foo", "dummy", "--help"})
if err != nil {
t.Errorf("Run returned unexpected error: %v", err)
}
}

View File

@ -3,24 +3,17 @@ package cli
import ( import (
"os" "os"
"reflect" "reflect"
"runtime"
"strings"
"testing" "testing"
) )
var (
wd, _ = os.Getwd()
)
func init() { func init() {
_ = os.Setenv("CLI_TEMPLATE_REPANIC", "1") _ = os.Setenv("CLI_TEMPLATE_REPANIC", "1")
} }
func expect(t *testing.T, a interface{}, b interface{}) { func expect(t *testing.T, a interface{}, b interface{}) {
_, fn, line, _ := runtime.Caller(1) t.Helper()
fn = strings.Replace(fn, wd+"/", "", -1)
if !reflect.DeepEqual(a, b) { if !reflect.DeepEqual(a, b) {
t.Errorf("(%s:%d) Expected %v (type %v) - Got %v (type %v)", fn, line, 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))
} }
} }

View File

@ -193,8 +193,8 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
cliBuiltFilePath = "./internal/example-cli/built-example" cliBuiltFilePath = "./internal/example-cli/built-example"
helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go" helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go"
helloBuiltFilePath = "./internal/example-hello-world/built-example" helloBuiltFilePath = "./internal/example-hello-world/built-example"
desiredMinBinarySize = 2.0 desiredMinBinarySize = 1.9
desiredMaxBinarySize = 2.1 desiredMaxBinarySize = 2.2
badNewsEmoji = "🚨" badNewsEmoji = "🚨"
goodNewsEmoji = "✨" goodNewsEmoji = "✨"
checksPassedEmoji = "✅" checksPassedEmoji = "✅"

View File

@ -7,13 +7,13 @@ var AppHelpTemplate = `NAME:
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}} {{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
USAGE: USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
VERSION: VERSION:
{{.Version}}{{end}}{{end}}{{if .Description}} {{.Version}}{{end}}{{end}}{{if .Description}}
DESCRIPTION: DESCRIPTION:
{{.Description}}{{end}}{{if len .Authors}} {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}}
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{range $index, $author := .Authors}}{{if $index}} {{range $index, $author := .Authors}}{{if $index}}
@ -39,13 +39,13 @@ var CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}} {{.HelpName}} - {{.Usage}}
USAGE: USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY: CATEGORY:
{{.Category}}{{end}}{{if .Description}} {{.Category}}{{end}}{{if .Description}}
DESCRIPTION: DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}} {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlags}}
OPTIONS: OPTIONS:
{{range .VisibleFlags}}{{.}} {{range .VisibleFlags}}{{.}}
@ -59,10 +59,10 @@ var SubcommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}} {{.HelpName}} - {{.Usage}}
USAGE: USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
DESCRIPTION: DESCRIPTION:
{{.Description}}{{end}} {{.Description | nindent 3 | trim}}{{end}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}} COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}} {{.Name}}:{{range .VisibleCommands}}
@ -74,9 +74,9 @@ OPTIONS:
{{end}}{{end}} {{end}}{{end}}
` `
var MarkdownDocTemplate = `% {{ .App.Name }} 8 var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionNum }}
# NAME {{end}}# NAME
{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} {{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }}
@ -86,16 +86,18 @@ var MarkdownDocTemplate = `% {{ .App.Name }} 8
{{ if .SynopsisArgs }} {{ if .SynopsisArgs }}
` + "```" + ` ` + "```" + `
{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + ` {{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + `
{{ end }}{{ if .App.UsageText }} {{ end }}{{ if .App.Description }}
# DESCRIPTION # DESCRIPTION
{{ .App.UsageText }} {{ .App.Description }}
{{ end }} {{ end }}
**Usage**: **Usage**:
` + "```" + ` ` + "```" + `{{ if .App.UsageText }}
{{ .App.UsageText }}
{{ else }}
{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] {{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
` + "```" + ` {{ end }}` + "```" + `
{{ if .GlobalArgs }} {{ if .GlobalArgs }}
# GLOBAL OPTIONS # GLOBAL OPTIONS
{{ range $v := .GlobalArgs }} {{ range $v := .GlobalArgs }}

View File

@ -3,7 +3,7 @@
.SH NAME .SH NAME
.PP .PP
greet \- Some app greet - Some app
.SH SYNOPSIS .SH SYNOPSIS
@ -14,9 +14,9 @@ greet
.RS .RS
.nf .nf
[\-\-another\-flag|\-b] [--another-flag|-b]
[\-\-flag|\-\-fl|\-f]=[value] [--flag|--fl|-f]=[value]
[\-\-socket|\-s]=[value] [--socket|-s]=[value]
.fi .fi
.RE .RE
@ -24,7 +24,7 @@ greet
.SH DESCRIPTION .SH DESCRIPTION
.PP .PP
app [first\_arg] [second\_arg] Description of the application.
.PP .PP
\fBUsage\fP: \fBUsage\fP:
@ -33,7 +33,7 @@ app [first\_arg] [second\_arg]
.RS .RS
.nf .nf
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] app [first_arg] [second_arg]
.fi .fi
.RE .RE
@ -41,13 +41,13 @@ greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
.SH GLOBAL OPTIONS .SH GLOBAL OPTIONS
.PP .PP
\fB\-\-another\-flag, \-b\fP: another usage text \fB--another-flag, -b\fP: another usage text
.PP .PP
\fB\-\-flag, \-\-fl, \-f\fP="": \fB--flag, --fl, -f\fP="":
.PP .PP
\fB\-\-socket, \-s\fP="": some 'usage' text (default: value) \fB--socket, -s\fP="": some 'usage' text (default: value)
.SH COMMANDS .SH COMMANDS
@ -56,23 +56,65 @@ greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
another usage test another usage test
.PP .PP
\fB\-\-another\-flag, \-b\fP: another usage text \fB--another-flag, -b\fP: another usage text
.PP .PP
\fB\-\-flag, \-\-fl, \-f\fP="": \fB--flag, --fl, -f\fP="":
.SS sub\-config, s, ss .SS sub-config, s, ss
.PP .PP
another usage test another usage test
.PP .PP
\fB\-\-sub\-command\-flag, \-s\fP: some usage text \fB--sub-command-flag, -s\fP: some usage text
.PP .PP
\fB\-\-sub\-flag, \-\-sub\-fl, \-s\fP="": \fB--sub-flag, --sub-fl, -s\fP="":
.SH info, i, in .SH info, i, in
.PP .PP
retrieve generic information retrieve generic information
.SH some\-command .SH some-command
.SH usage, u
.PP
standard usage text
.PP
.RS
.nf
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
```
func() { ... }
```
Should be a part of the same code block
.fi
.RE
.PP
\fB--another-flag, -b\fP: another usage text
.PP
\fB--flag, --fl, -f\fP="":
.SS sub-usage, su
.PP
standard usage text
.PP
.RS
.PP
Single line of UsageText
.RE
.PP
\fB--sub-command-flag, -s\fP: some usage text

View File

@ -1,5 +1,3 @@
% greet 8
# NAME # NAME
greet - Some app greet - Some app
@ -16,12 +14,12 @@ greet
# DESCRIPTION # DESCRIPTION
app [first_arg] [second_arg] Description of the application.
**Usage**: **Usage**:
``` ```
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] app [first_arg] [second_arg]
``` ```
# GLOBAL OPTIONS # GLOBAL OPTIONS
@ -58,3 +56,29 @@ retrieve generic information
## some-command ## some-command
## usage, u
standard usage text
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
```
func() { ... }
```
Should be a part of the same code block
**--another-flag, -b**: another usage text
**--flag, --fl, -f**="":
### sub-usage, su
standard usage text
>Single line of UsageText
**--sub-command-flag, -s**: some usage text

View File

@ -1,5 +1,3 @@
% greet 8
# NAME # NAME
greet - Some app greet - Some app
@ -16,12 +14,12 @@ greet
# DESCRIPTION # DESCRIPTION
app [first_arg] [second_arg] Description of the application.
**Usage**: **Usage**:
``` ```
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] app [first_arg] [second_arg]
``` ```
# GLOBAL OPTIONS # GLOBAL OPTIONS
@ -58,3 +56,29 @@ retrieve generic information
## some-command ## some-command
## usage, u
standard usage text
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
```
func() { ... }
```
Should be a part of the same code block
**--another-flag, -b**: another usage text
**--flag, --fl, -f**="":
### sub-usage, su
standard usage text
>Single line of UsageText
**--sub-command-flag, -s**: some usage text

View File

@ -1,5 +1,3 @@
% greet 8
# NAME # NAME
greet - Some app greet - Some app
@ -16,12 +14,12 @@ greet
# DESCRIPTION # DESCRIPTION
app [first_arg] [second_arg] Description of the application.
**Usage**: **Usage**:
``` ```
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] app [first_arg] [second_arg]
``` ```
# GLOBAL OPTIONS # GLOBAL OPTIONS

View File

@ -1,5 +1,3 @@
% greet 8
# NAME # NAME
greet - Some app greet - Some app
@ -10,12 +8,12 @@ greet
# DESCRIPTION # DESCRIPTION
app [first_arg] [second_arg] Description of the application.
**Usage**: **Usage**:
``` ```
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] app [first_arg] [second_arg]
``` ```
# COMMANDS # COMMANDS
@ -43,3 +41,29 @@ retrieve generic information
## some-command ## some-command
## usage, u
standard usage text
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
```
func() { ... }
```
Should be a part of the same code block
**--another-flag, -b**: another usage text
**--flag, --fl, -f**="":
### sub-usage, su
standard usage text
>Single line of UsageText
**--sub-command-flag, -s**: some usage text

84
testdata/expected-doc-no-usagetext.md vendored Normal file
View File

@ -0,0 +1,84 @@
# NAME
greet - Some app
# SYNOPSIS
greet
```
[--another-flag|-b]
[--flag|--fl|-f]=[value]
[--socket|-s]=[value]
```
# DESCRIPTION
Description of the application.
**Usage**:
```
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
```
# GLOBAL OPTIONS
**--another-flag, -b**: another usage text
**--flag, --fl, -f**="":
**--socket, -s**="": some 'usage' text (default: value)
# COMMANDS
## config, c
another usage test
**--another-flag, -b**: another usage text
**--flag, --fl, -f**="":
### sub-config, s, ss
another usage test
**--sub-command-flag, -s**: some usage text
**--sub-flag, --sub-fl, -s**="":
## info, i, in
retrieve generic information
## some-command
## usage, u
standard usage text
Usage for the usage text
- formatted: Based on the specified ConfigMap and summon secrets.yml
- list: Inspect the environment for a specific process running on a Pod
- for_effect: Compare 'namespace' environment with 'local'
```
func() { ... }
```
Should be a part of the same code block
**--another-flag, -b**: another usage text
**--flag, --fl, -f**="":
### sub-usage, su
standard usage text
>Single line of UsageText
**--sub-command-flag, -s**: some usage text

View File

@ -2,7 +2,7 @@
function __fish_greet_no_subcommand --description 'Test if there has been any subcommand yet' function __fish_greet_no_subcommand --description 'Test if there has been any subcommand yet'
for i in (commandline -opc) for i in (commandline -opc)
if contains -- $i config c sub-config s ss info i in some-command if contains -- $i config c sub-config s ss info i in some-command usage u sub-usage su
return 1 return 1
end end
end end
@ -12,6 +12,7 @@ end
complete -c greet -n '__fish_greet_no_subcommand' -l socket -s s -r -d 'some \'usage\' text' complete -c greet -n '__fish_greet_no_subcommand' -l socket -s s -r -d 'some \'usage\' text'
complete -c greet -n '__fish_greet_no_subcommand' -f -l flag -s fl -s f -r complete -c greet -n '__fish_greet_no_subcommand' -f -l flag -s fl -s f -r
complete -c greet -n '__fish_greet_no_subcommand' -f -l another-flag -s b -d 'another usage text' complete -c greet -n '__fish_greet_no_subcommand' -f -l another-flag -s b -d 'another usage text'
complete -c greet -n '__fish_greet_no_subcommand' -l logfile -r
complete -c greet -n '__fish_greet_no_subcommand' -f -l help -s h -d 'show help' complete -c greet -n '__fish_greet_no_subcommand' -f -l help -s h -d 'show help'
complete -c greet -n '__fish_greet_no_subcommand' -f -l version -s v -d 'print the version' complete -c greet -n '__fish_greet_no_subcommand' -f -l version -s v -d 'print the version'
complete -c greet -n '__fish_seen_subcommand_from config c' -f -l help -s h -d 'show help' complete -c greet -n '__fish_seen_subcommand_from config c' -f -l help -s h -d 'show help'
@ -26,3 +27,10 @@ complete -c greet -n '__fish_seen_subcommand_from info i in' -f -l help -s h -d
complete -r -c greet -n '__fish_greet_no_subcommand' -a 'info i in' -d 'retrieve generic information' complete -r -c greet -n '__fish_greet_no_subcommand' -a 'info i in' -d 'retrieve generic information'
complete -c greet -n '__fish_seen_subcommand_from some-command' -f -l help -s h -d 'show help' complete -c greet -n '__fish_seen_subcommand_from some-command' -f -l help -s h -d 'show help'
complete -r -c greet -n '__fish_greet_no_subcommand' -a 'some-command' complete -r -c greet -n '__fish_greet_no_subcommand' -a 'some-command'
complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l help -s h -d 'show help'
complete -r -c greet -n '__fish_greet_no_subcommand' -a 'usage u' -d 'standard usage text'
complete -c greet -n '__fish_seen_subcommand_from usage u' -l flag -s fl -s f -r
complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l another-flag -s b -d 'another usage text'
complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l help -s h -d 'show help'
complete -r -c greet -n '__fish_seen_subcommand_from usage u' -a 'sub-usage su' -d 'standard usage text'
complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l sub-command-flag -s s -d 'some usage text'