Merge branch 'master' into suggestions
This commit is contained in:
commit
4b238b8ff0
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@ -31,6 +31,7 @@ _(REQUIRED)_
|
||||
<!--
|
||||
If this PR fixes one of more issues, list them here.
|
||||
One line each, like so:
|
||||
|
||||
Fixes #123
|
||||
Fixes #39
|
||||
-->
|
||||
|
63
.github/stale.yml
vendored
63
.github/stale.yml
vendored
@ -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
|
29
.github/workflows/cli.yml
vendored
29
.github/workflows/cli.yml
vendored
@ -11,12 +11,11 @@ on:
|
||||
- v1
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
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 }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -27,10 +26,10 @@ jobs:
|
||||
|
||||
- name: Set GOPATH, PATH and ENV
|
||||
run: |
|
||||
echo "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)"
|
||||
echo "::set-env name=GO111MODULE::on"
|
||||
echo "::set-env name=GOPROXY::https://proxy.golang.org"
|
||||
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin"
|
||||
echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
|
||||
echo "GO111MODULE=on" >> $GITHUB_ENV
|
||||
echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
|
||||
echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Checkout Code
|
||||
@ -39,7 +38,7 @@ jobs:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- 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 .)
|
||||
|
||||
- name: vet
|
||||
@ -52,20 +51,20 @@ jobs:
|
||||
run: go run internal/build/build.go check-binary-size
|
||||
|
||||
- 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
|
||||
with:
|
||||
token: 0a8cc73b-bb7c-480b-8626-38a461643761
|
||||
fail_ci_if_error: true
|
||||
|
||||
test-docs:
|
||||
name: test-docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.13
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
# Currently fails on 1.16+
|
||||
go-version: 1.15
|
||||
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
@ -74,10 +73,10 @@ jobs:
|
||||
|
||||
- name: Set GOPATH, PATH and ENV
|
||||
run: |
|
||||
echo "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)"
|
||||
echo "::set-env name=GO111MODULE::on"
|
||||
echo "::set-env name=GOPROXY::https://proxy.golang.org"
|
||||
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin"
|
||||
echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
|
||||
echo "GO111MODULE=on" >> $GITHUB_ENV
|
||||
echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
|
||||
echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Checkout Code
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,3 +5,5 @@ vendor
|
||||
.idea
|
||||
internal/*/built-example
|
||||
coverage.txt
|
||||
|
||||
*.exe
|
||||
|
12
README.md
12
README.md
@ -1,7 +1,7 @@
|
||||
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)
|
||||
[![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)
|
||||
@ -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)
|
||||
- `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
|
||||
|
||||
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
|
||||
|
||||
@ -63,4 +67,4 @@ export PATH=$PATH:$GOPATH/bin
|
||||
|
||||
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
|
||||
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).
|
||||
|
6
altsrc/default_input_source.go
Normal file
6
altsrc/default_input_source.go
Normal file
@ -0,0 +1,6 @@
|
||||
package altsrc
|
||||
|
||||
// defaultInputSource creates a default InputSourceContext.
|
||||
func defaultInputSource() (InputSourceContext, error) {
|
||||
return &MapInputSource{file: "", valueMap: map[interface{}]interface{}{}}, nil
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -190,7 +191,13 @@ func TestPathApplyInputSourceMethodSet(t *testing.T) {
|
||||
|
||||
expected := "/path/to/source/hello"
|
||||
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"))
|
||||
}
|
||||
|
@ -17,8 +17,12 @@ import (
|
||||
// by the given flag.
|
||||
func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) {
|
||||
return func(context *cli.Context) (InputSourceContext, error) {
|
||||
if context.IsSet(flag) {
|
||||
return NewJSONSourceFromFile(context.String(flag))
|
||||
}
|
||||
|
||||
return defaultInputSource()
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSONSourceFromFile returns an InputSourceContext suitable for
|
||||
|
@ -16,6 +16,11 @@ type MapInputSource struct {
|
||||
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.
|
||||
// If so, it tries to traverse the tree by the '.' delimited sections to find
|
||||
// a nested value for the key.
|
||||
|
@ -6,14 +6,13 @@ import (
|
||||
)
|
||||
|
||||
func TestMapDuration(t *testing.T) {
|
||||
inputSource := &MapInputSource{
|
||||
file: "test",
|
||||
valueMap: map[interface{}]interface{}{
|
||||
inputSource := NewMapInputSource(
|
||||
"test",
|
||||
map[interface{}]interface{}{
|
||||
"duration_of_duration_type": time.Minute,
|
||||
"duration_of_string_type": "1m",
|
||||
"duration_of_int_type": 1000,
|
||||
},
|
||||
}
|
||||
})
|
||||
d, err := inputSource.Duration("duration_of_duration_type")
|
||||
expect(t, time.Minute, d)
|
||||
expect(t, nil, err)
|
||||
|
@ -87,9 +87,13 @@ func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
|
||||
// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
|
||||
func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
|
||||
return func(context *cli.Context) (InputSourceContext, error) {
|
||||
if context.IsSet(flagFileName) {
|
||||
filePath := context.String(flagFileName)
|
||||
return NewTomlSourceFromFile(filePath)
|
||||
}
|
||||
|
||||
return defaultInputSource()
|
||||
}
|
||||
}
|
||||
|
||||
func readCommandToml(filePath string, container interface{}) (err error) {
|
||||
|
@ -33,9 +33,13 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
|
||||
// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
|
||||
func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
|
||||
return func(context *cli.Context) (InputSourceContext, error) {
|
||||
if context.IsSet(flagFileName) {
|
||||
filePath := context.String(flagFileName)
|
||||
return NewYamlSourceFromFile(filePath)
|
||||
}
|
||||
|
||||
return defaultInputSource()
|
||||
}
|
||||
}
|
||||
|
||||
func readCommandYaml(filePath string, container interface{}) (err error) {
|
||||
|
63
app.go
63
app.go
@ -43,8 +43,11 @@ type App struct {
|
||||
Flags []Flag
|
||||
// Boolean to enable bash completion commands
|
||||
EnableBashCompletion bool
|
||||
// Boolean to hide built-in help command
|
||||
// Boolean to hide built-in help command and help flag
|
||||
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
|
||||
HideVersion bool
|
||||
// categories contains the categorized commands and is populated on app startup
|
||||
@ -61,7 +64,7 @@ type App struct {
|
||||
Action ActionFunc
|
||||
// Execute this function if the proper command cannot be found
|
||||
CommandNotFound CommandNotFoundFunc
|
||||
// Execute this function if an usage error occurs
|
||||
// Execute this function if a usage error occurs
|
||||
OnUsageError OnUsageErrorFunc
|
||||
// Compilation date
|
||||
Compiled time.Time
|
||||
@ -69,12 +72,15 @@ type App struct {
|
||||
Authors []*Author
|
||||
// Copyright of the binary if any
|
||||
Copyright string
|
||||
// Reader reader to write input to (useful for tests)
|
||||
Reader io.Reader
|
||||
// Writer writer to write output to
|
||||
Writer io.Writer
|
||||
// ErrWriter writes error output
|
||||
ErrWriter io.Writer
|
||||
// Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to
|
||||
// function as a default, so this is optional.
|
||||
// ExitErrHandler processes any error encountered while running an App before
|
||||
// it is returned to the caller. If no function is provided, HandleExitCoder
|
||||
// is used as the default behavior.
|
||||
ExitErrHandler ExitErrHandlerFunc
|
||||
// Other custom info
|
||||
Metadata map[string]interface{}
|
||||
@ -115,7 +121,9 @@ func NewApp() *App {
|
||||
BashComplete: DefaultAppComplete,
|
||||
Action: helpCommand.Action,
|
||||
Compiled: compileTime(),
|
||||
Reader: os.Stdin,
|
||||
Writer: os.Stdout,
|
||||
ErrWriter: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,7 +142,7 @@ func (a *App) Setup() {
|
||||
}
|
||||
|
||||
if a.HelpName == "" {
|
||||
a.HelpName = filepath.Base(os.Args[0])
|
||||
a.HelpName = a.Name
|
||||
}
|
||||
|
||||
if a.Usage == "" {
|
||||
@ -157,10 +165,18 @@ func (a *App) Setup() {
|
||||
a.Compiled = compileTime()
|
||||
}
|
||||
|
||||
if a.Reader == nil {
|
||||
a.Reader = os.Stdin
|
||||
}
|
||||
|
||||
if a.Writer == nil {
|
||||
a.Writer = os.Stdout
|
||||
}
|
||||
|
||||
if a.ErrWriter == nil {
|
||||
a.ErrWriter = os.Stderr
|
||||
}
|
||||
|
||||
var newCommands []*Command
|
||||
|
||||
for _, c := range a.Commands {
|
||||
@ -172,7 +188,9 @@ func (a *App) Setup() {
|
||||
a.Commands = newCommands
|
||||
|
||||
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
|
||||
if !a.HideHelpCommand {
|
||||
a.appendCommand(helpCommand)
|
||||
}
|
||||
|
||||
if HelpFlag != nil {
|
||||
a.appendFlag(HelpFlag)
|
||||
@ -192,10 +210,6 @@ func (a *App) Setup() {
|
||||
if a.Metadata == nil {
|
||||
a.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if a.Writer == nil {
|
||||
a.Writer = os.Stdout
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) newFlagSet() (*flag.FlagSet, error) {
|
||||
@ -271,7 +285,7 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
cerr := checkRequiredFlags(a.Flags, context)
|
||||
cerr := context.checkRequiredFlags(a.Flags)
|
||||
if cerr != nil {
|
||||
_ = ShowAppHelp(context)
|
||||
return cerr
|
||||
@ -292,8 +306,6 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
|
||||
if a.Before != nil {
|
||||
beforeErr := a.Before(context)
|
||||
if beforeErr != nil {
|
||||
_, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
|
||||
_ = ShowAppHelp(context)
|
||||
a.handleExitCoder(context, beforeErr)
|
||||
err = beforeErr
|
||||
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
|
||||
//
|
||||
// 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
|
||||
func (a *App) RunAndExitOnError() {
|
||||
if err := a.Run(os.Args); err != nil {
|
||||
_, _ = fmt.Fprintln(a.errWriter(), err)
|
||||
_, _ = fmt.Fprintln(a.ErrWriter, err)
|
||||
OsExiter(1)
|
||||
}
|
||||
}
|
||||
@ -335,19 +347,9 @@ func (a *App) RunAndExitOnError() {
|
||||
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
|
||||
// generate command-specific flags
|
||||
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
|
||||
// Setup also handles HideHelp and HideHelpCommand
|
||||
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
|
||||
for _, c := range a.Commands {
|
||||
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 {
|
||||
_ = ShowSubcommandHelp(context)
|
||||
return cerr
|
||||
@ -496,15 +498,6 @@ func (a *App) VisibleFlags() []Flag {
|
||||
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) {
|
||||
if !hasFlag(a.Flags, fl) {
|
||||
a.Flags = append(a.Flags, fl)
|
||||
|
109
app_test.go
109
app_test.go
@ -315,7 +315,6 @@ func ExampleApp_Run_bashComplete_withMultipleLongFlag() {
|
||||
}
|
||||
|
||||
func ExampleApp_Run_bashComplete() {
|
||||
// set args for examples sake
|
||||
// set args for examples sake
|
||||
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) {
|
||||
app := &App{}
|
||||
app.Setup()
|
||||
@ -471,18 +476,18 @@ func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) {
|
||||
a := App{
|
||||
Name: "cmd",
|
||||
Flags: []Flag{
|
||||
&StringFlag{Name: "--foo"},
|
||||
&StringFlag{Name: "foo"},
|
||||
},
|
||||
Writer: bytes.NewBufferString(""),
|
||||
}
|
||||
|
||||
set := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
_ = set.Parse([]string{"", "---foo"})
|
||||
_ = set.Parse([]string{"", "-bar"})
|
||||
c := &Context{flagSet: set}
|
||||
|
||||
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) {
|
||||
@ -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) {
|
||||
app := &App{}
|
||||
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) {
|
||||
var w bytes.Buffer
|
||||
|
||||
@ -2152,3 +2222,34 @@ func newTestApp() *App {
|
||||
a.Writer = ioutil.Discard
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
9
autocomplete/powershell_autocomplete.ps1
Normal file
9
autocomplete/powershell_autocomplete.ps1
Normal 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', $_)
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ _cli_zsh_autocomplete() {
|
||||
|
||||
if [[ "${opts[1]}" != "" ]]; then
|
||||
_describe 'values' opts
|
||||
else
|
||||
_files
|
||||
fi
|
||||
|
||||
return
|
||||
|
13
command.go
13
command.go
@ -41,8 +41,11 @@ type Command struct {
|
||||
Flags []Flag
|
||||
// Treat all flags as normal arguments if true
|
||||
SkipFlagParsing bool
|
||||
// Boolean to hide built-in help command
|
||||
// Boolean to hide built-in help command and help flag
|
||||
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
|
||||
Hidden bool
|
||||
// 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
|
||||
}
|
||||
|
||||
cerr := checkRequiredFlags(c.Flags, context)
|
||||
cerr := context.checkRequiredFlags(c.Flags)
|
||||
if cerr != nil {
|
||||
_ = ShowCommandHelp(context, c.Name)
|
||||
return cerr
|
||||
@ -152,7 +155,6 @@ func (c *Command) Run(ctx *Context) (err error) {
|
||||
if c.Before != nil {
|
||||
err = c.Before(context)
|
||||
if err != nil {
|
||||
_ = ShowCommandHelp(context, c.Name)
|
||||
context.App.handleExitCoder(context, err)
|
||||
return err
|
||||
}
|
||||
@ -230,6 +232,7 @@ func (c *Command) startApp(ctx *Context) error {
|
||||
}
|
||||
|
||||
app.Usage = c.Usage
|
||||
app.UsageText = c.UsageText
|
||||
app.Description = c.Description
|
||||
app.ArgsUsage = c.ArgsUsage
|
||||
|
||||
@ -241,10 +244,12 @@ func (c *Command) startApp(ctx *Context) error {
|
||||
app.Commands = c.Subcommands
|
||||
app.Flags = c.Flags
|
||||
app.HideHelp = c.HideHelp
|
||||
app.HideHelpCommand = c.HideHelpCommand
|
||||
|
||||
app.Version = ctx.App.Version
|
||||
app.HideVersion = ctx.App.HideVersion
|
||||
app.HideVersion = true
|
||||
app.Compiled = ctx.App.Compiled
|
||||
app.Reader = ctx.App.Reader
|
||||
app.Writer = ctx.App.Writer
|
||||
app.ErrWriter = ctx.App.ErrWriter
|
||||
app.ExitErrHandler = ctx.App.ExitErrHandler
|
||||
|
@ -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)
|
||||
}
|
||||
|
122
context.go
122
context.go
@ -2,9 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -53,8 +51,7 @@ func (c *Context) Set(name, value string) error {
|
||||
|
||||
// IsSet determines if the flag was actually set
|
||||
func (c *Context) IsSet(name string) bool {
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
if fs := c.lookupFlagSet(name); fs != nil {
|
||||
isSet := false
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == name {
|
||||
@ -64,9 +61,8 @@ func (c *Context) IsSet(name string) bool {
|
||||
if isSet {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
f := lookupFlag(name, c)
|
||||
f := c.lookupFlag(name)
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
@ -108,7 +104,10 @@ func (c *Context) Lineage() []*Context {
|
||||
|
||||
// Value returns the value of the flag corresponding to `name`
|
||||
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.
|
||||
@ -122,7 +121,7 @@ func (c *Context) NArg() int {
|
||||
return c.Args().Len()
|
||||
}
|
||||
|
||||
func lookupFlag(name string, ctx *Context) Flag {
|
||||
func (ctx *Context) lookupFlag(name string) Flag {
|
||||
for _, c := range ctx.Lineage() {
|
||||
if c.Command == nil {
|
||||
continue
|
||||
@ -150,8 +149,11 @@ func lookupFlag(name string, ctx *Context) Flag {
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupFlagSet(name string, ctx *Context) *flag.FlagSet {
|
||||
func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet {
|
||||
for _, c := range ctx.Lineage() {
|
||||
if c.flagSet == nil {
|
||||
continue
|
||||
}
|
||||
if f := c.flagSet.Lookup(name); f != nil {
|
||||
return c.flagSet
|
||||
}
|
||||
@ -160,89 +162,7 @@ func lookupFlagSet(name string, ctx *Context) *flag.FlagSet {
|
||||
return 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 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 {
|
||||
func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
|
||||
var missingFlags []string
|
||||
for _, f := range flags {
|
||||
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
|
||||
@ -271,3 +191,21 @@ func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,8 @@ func TestContext_String(t *testing.T) {
|
||||
c := NewContext(nil, set, parentCtx)
|
||||
expect(t, c.String("myflag"), "hello world")
|
||||
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) {
|
||||
@ -136,6 +138,18 @@ func TestContext_Bool(t *testing.T) {
|
||||
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) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
@ -183,6 +197,7 @@ func TestContext_IsSet_fromEnv(t *testing.T) {
|
||||
unparsableIsSet, uIsSet bool
|
||||
)
|
||||
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||
_ = os.Setenv("APP_PASSWORD", "")
|
||||
@ -303,13 +318,13 @@ func TestContext_lookupFlagSet(t *testing.T) {
|
||||
_ = set.Parse([]string{"--local-flag"})
|
||||
_ = parentSet.Parse([]string{"--top-flag"})
|
||||
|
||||
fs := lookupFlagSet("top-flag", ctx)
|
||||
fs := ctx.lookupFlagSet("top-flag")
|
||||
expect(t, fs, parentCtx.flagSet)
|
||||
|
||||
fs = lookupFlagSet("local-flag", ctx)
|
||||
fs = ctx.lookupFlagSet("local-flag")
|
||||
expect(t, fs, ctx.flagSet)
|
||||
|
||||
if fs := lookupFlagSet("frob", ctx); fs != nil {
|
||||
if fs := ctx.lookupFlagSet("frob"); fs != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@ -533,12 +548,21 @@ func TestCheckRequiredFlags(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
t.Run(test.testCase, func(t *testing.T) {
|
||||
// setup
|
||||
if test.envVarInput[0] != "" {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv(test.envVarInput[0], test.envVarInput[1])
|
||||
}
|
||||
@ -554,7 +578,7 @@ func TestCheckRequiredFlags(t *testing.T) {
|
||||
ctx.Command.Flags = test.flags
|
||||
|
||||
// logic under test
|
||||
err := checkRequiredFlags(test.flags, ctx)
|
||||
err := ctx.checkRequiredFlags(test.flags)
|
||||
|
||||
// assertions
|
||||
if test.expectedAnError && err == nil {
|
||||
|
67
docs.go
67
docs.go
@ -15,31 +15,39 @@ import (
|
||||
// The function errors if either parsing or writing of the string fails.
|
||||
func (a *App) ToMarkdown() (string, error) {
|
||||
var w bytes.Buffer
|
||||
if err := a.writeDocTemplate(&w); err != nil {
|
||||
if err := a.writeDocTemplate(&w, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
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.
|
||||
func (a *App) ToMan() (string, error) {
|
||||
func (a *App) ToManWithSection(sectionNumber int) (string, error) {
|
||||
var w bytes.Buffer
|
||||
if err := a.writeDocTemplate(&w); err != nil {
|
||||
if err := a.writeDocTemplate(&w, sectionNumber); err != nil {
|
||||
return "", err
|
||||
}
|
||||
man := md2man.Render(w.Bytes())
|
||||
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 {
|
||||
App *App
|
||||
SectionNum int
|
||||
Commands []string
|
||||
GlobalArgs []string
|
||||
SynopsisArgs []string
|
||||
}
|
||||
|
||||
func (a *App) writeDocTemplate(w io.Writer) error {
|
||||
func (a *App) writeDocTemplate(w io.Writer, sectionNum int) error {
|
||||
const name = "cli"
|
||||
t, err := template.New(name).Parse(MarkdownDocTemplate)
|
||||
if err != nil {
|
||||
@ -47,6 +55,7 @@ func (a *App) writeDocTemplate(w io.Writer) error {
|
||||
}
|
||||
return t.ExecuteTemplate(w, name, &cliTemplate{
|
||||
App: a,
|
||||
SectionNum: sectionNum,
|
||||
Commands: prepareCommands(a.Commands, 0),
|
||||
GlobalArgs: prepareArgsWithValues(a.VisibleFlags()),
|
||||
SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()),
|
||||
@ -59,15 +68,16 @@ func prepareCommands(commands []*Command, level int) []string {
|
||||
if command.Hidden {
|
||||
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.Join(command.Names(), ", "),
|
||||
usage,
|
||||
usageText,
|
||||
)
|
||||
|
||||
flags := prepareArgsWithValues(command.Flags)
|
||||
@ -146,3 +156,40 @@ func flagDetails(flag DocGenerationFlag) string {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -6,6 +6,29 @@
|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
@ -555,12 +584,14 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
|
||||
### Added
|
||||
- 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.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
|
||||
|
||||
[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.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
|
||||
|
245
docs/migrate-v1-to-v2.md
Normal file
245
docs/migrate-v1-to-v2.md
Normal 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.
|
@ -27,6 +27,7 @@ cli v1 manual
|
||||
* [Version Flag](#version-flag)
|
||||
+ [Customization](#customization-2)
|
||||
+ [Full API Example](#full-api-example)
|
||||
* [Migrating to V2](#migrating-to-v2)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
@ -611,7 +612,7 @@ given sources.
|
||||
Here is a more complete sample of a command using YAML support:
|
||||
|
||||
<!-- {
|
||||
"args": ["test-cmd", "--help"],
|
||||
"args": ["--help"],
|
||||
"output": "--test value.*default: 0"
|
||||
} -->
|
||||
``` go
|
||||
@ -1476,3 +1477,10 @@ func wopAction(c *cli.Context) error {
|
||||
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).
|
||||
|
@ -3,6 +3,7 @@ cli v2 manual
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Migrating From Older Releases](#migrating-from-older-releases)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Examples](#examples)
|
||||
* [Arguments](#arguments)
|
||||
@ -29,6 +30,7 @@ cli v2 manual
|
||||
+ [ZSH Support](#zsh-support)
|
||||
+ [ZSH default auto-complete example](#zsh-default-auto-complete-example)
|
||||
+ [ZSH custom auto-complete example](#zsh-custom-auto-complete-example)
|
||||
+ [PowerShell Support](#powershell-support)
|
||||
* [Generated Help Text](#generated-help-text)
|
||||
+ [Customization](#customization-1)
|
||||
* [Version Flag](#version-flag)
|
||||
@ -39,6 +41,13 @@ cli v2 manual
|
||||
|
||||
<!-- 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
|
||||
|
||||
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
|
||||
|
||||
@ -419,12 +428,14 @@ func main() {
|
||||
app := &cli.App{
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "lang, l",
|
||||
Name: "lang",
|
||||
Aliases: []string{"l"},
|
||||
Value: "english",
|
||||
Usage: "Language for the greeting",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config, c",
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "Load configuration from `FILE`",
|
||||
},
|
||||
},
|
||||
@ -504,7 +515,7 @@ func main() {
|
||||
```
|
||||
|
||||
If `EnvVars` contains more than one string, the first environment variable that
|
||||
resolves is used as the default.
|
||||
resolves is used.
|
||||
|
||||
<!-- {
|
||||
"args": ["--help"],
|
||||
@ -563,7 +574,8 @@ func main() {
|
||||
|
||||
app.Flags = []cli.Flag {
|
||||
&cli.StringFlag{
|
||||
Name: "password, p",
|
||||
Name: "password",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "password for the mysql database",
|
||||
FilePath: "/etc/mysql/password",
|
||||
},
|
||||
@ -616,7 +628,7 @@ given sources.
|
||||
Here is a more complete sample of a command using YAML support:
|
||||
|
||||
<!-- {
|
||||
"args": ["test-cmd", "--help"],
|
||||
"args": ["--help"],
|
||||
"output": "--test value.*default: 0"
|
||||
} -->
|
||||
``` go
|
||||
@ -638,7 +650,7 @@ func main() {
|
||||
|
||||
app := &cli.App{
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("yaml ist rad")
|
||||
fmt.Println("--test value.*default: 0")
|
||||
return nil
|
||||
},
|
||||
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
|
||||
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"
|
||||
@ -665,9 +677,7 @@ package main
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -1013,12 +1023,12 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.EnableBashCompletion = true
|
||||
app.Commands = []cli.Command{
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Aliases: []string{"a"},
|
||||
@ -1041,7 +1051,7 @@ func main() {
|
||||
Name: "template",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "options for task templates",
|
||||
Subcommands: []cli.Command{
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new template",
|
||||
@ -1217,6 +1227,23 @@ source path/to/autocomplete/zsh_autocomplete
|
||||
#### ZSH custom auto-complete example
|
||||
![](/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
|
||||
|
||||
The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked
|
||||
@ -1304,7 +1331,8 @@ import (
|
||||
|
||||
func main() {
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "haaaaalp", Aliases: []string{"halp"},
|
||||
Name: "haaaaalp",
|
||||
Aliases: []string{"halp"},
|
||||
Usage: "HALP",
|
||||
EnvVars: []string{"SHOW_HALP", "HALPPLZ"},
|
||||
}
|
||||
@ -1339,7 +1367,8 @@ import (
|
||||
|
||||
func main() {
|
||||
cli.VersionFlag = &cli.BoolFlag{
|
||||
Name: "print-version", Aliases: []string{"V"},
|
||||
Name: "print-version",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "print only the version",
|
||||
}
|
||||
|
||||
|
194
docs_test.go
194
docs_test.go
@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
@ -66,8 +67,50 @@ func testApp() *App {
|
||||
}, {
|
||||
Name: "hidden-command",
|
||||
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.Description = `Description of the application.`
|
||||
app.Usage = "Some app"
|
||||
app.Authors = []*Author{
|
||||
{Name: "Harrison", Email: "harrison@lolwut.com"},
|
||||
@ -76,13 +119,13 @@ func testApp() *App {
|
||||
return app
|
||||
}
|
||||
|
||||
func expectFileContent(t *testing.T, file, expected string) {
|
||||
func expectFileContent(t *testing.T, file, got string) {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
// Ignore windows line endings
|
||||
// TODO: Replace with bytes.ReplaceAll when support for Go 1.11 is dropped
|
||||
data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1)
|
||||
expect(t, err, nil)
|
||||
expect(t, string(data), expected)
|
||||
expect(t, got, string(data))
|
||||
}
|
||||
|
||||
func TestToMarkdownFull(t *testing.T) {
|
||||
@ -136,6 +179,19 @@ func TestToMarkdownNoAuthors(t *testing.T) {
|
||||
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) {
|
||||
// Given
|
||||
app := testApp()
|
||||
@ -147,3 +203,137 @@ func TestToMan(t *testing.T) {
|
||||
expect(t, err, nil)
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
50
errors.go
50
errors.go
@ -17,11 +17,10 @@ var ErrWriter io.Writer = os.Stderr
|
||||
// MultiError is an error that wraps multiple errors.
|
||||
type MultiError interface {
|
||||
error
|
||||
// Errors returns a copy of the errors slice
|
||||
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 {
|
||||
ret := multiError(err)
|
||||
return &ret
|
||||
@ -48,6 +47,28 @@ func (m *multiError) Errors() []error {
|
||||
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
|
||||
type ErrorFormatter interface {
|
||||
Format(s fmt.State, verb rune)
|
||||
@ -65,13 +86,20 @@ type exitError struct {
|
||||
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 {
|
||||
return Exit(message, exitCode)
|
||||
}
|
||||
|
||||
// Exit wraps a message and exit code into an ExitCoder suitable for handling by
|
||||
// HandleExitCoder
|
||||
// Exit wraps a message and exit code into an error, which by default is
|
||||
// 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 {
|
||||
return &exitError{
|
||||
message: message,
|
||||
@ -87,10 +115,14 @@ func (ee *exitError) ExitCode() int {
|
||||
return ee.exitCode
|
||||
}
|
||||
|
||||
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
|
||||
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
|
||||
// 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.
|
||||
// HandleExitCoder handles errors implementing ExitCoder by printing their
|
||||
// message and calling OsExiter with the given 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) {
|
||||
if err == nil {
|
||||
return
|
||||
|
@ -67,6 +67,26 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) {
|
||||
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
|
||||
type ErrorWithFormat struct {
|
||||
error
|
||||
|
4
fish.go
4
fish.go
@ -171,6 +171,10 @@ func fishAddFileFlag(flag Flag, completion *strings.Builder) {
|
||||
if f.TakesFile {
|
||||
return
|
||||
}
|
||||
case *PathFlag:
|
||||
if f.TakesFile {
|
||||
return
|
||||
}
|
||||
}
|
||||
completion.WriteString(" -f")
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import (
|
||||
func TestFishCompletion(t *testing.T) {
|
||||
// Given
|
||||
app := testApp()
|
||||
app.Flags = append(app.Flags, &PathFlag{
|
||||
Name: "logfile",
|
||||
TakesFile: true,
|
||||
})
|
||||
|
||||
// When
|
||||
res, err := app.ToFishCompletion()
|
||||
|
78
flag.go
78
flag.go
@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -36,7 +37,7 @@ var VersionFlag Flag = &BoolFlag{
|
||||
|
||||
// HelpFlag prints the help for all commands and subcommands.
|
||||
// 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{
|
||||
Name: "help",
|
||||
Aliases: []string{"h"},
|
||||
@ -118,6 +119,14 @@ type DocGenerationFlag interface {
|
||||
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) {
|
||||
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
|
||||
@ -130,11 +139,52 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
|
||||
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 {
|
||||
var visible []Flag
|
||||
for _, f := range fl {
|
||||
field := flagValue(f).FieldByName("Hidden")
|
||||
if !field.IsValid() || !field.Bool() {
|
||||
if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() {
|
||||
visible = append(visible, f)
|
||||
}
|
||||
}
|
||||
@ -244,6 +294,10 @@ func flagValue(f Flag) reflect.Value {
|
||||
return fv
|
||||
}
|
||||
|
||||
func formatDefault(format string) string {
|
||||
return " (default: " + format + ")"
|
||||
}
|
||||
|
||||
func stringifyFlag(f Flag) string {
|
||||
fv := flagValue(f)
|
||||
|
||||
@ -269,20 +323,20 @@ func stringifyFlag(f Flag) string {
|
||||
val := fv.FieldByName("Value")
|
||||
if val.IsValid() {
|
||||
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() != "" {
|
||||
defaultValueString = fmt.Sprintf(" (default: %q)", val.String())
|
||||
defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String())
|
||||
}
|
||||
}
|
||||
|
||||
helpText := fv.FieldByName("DefaultText")
|
||||
if helpText.IsValid() && helpText.String() != "" {
|
||||
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 = ""
|
||||
}
|
||||
|
||||
@ -351,11 +405,15 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string {
|
||||
|
||||
defaultVal := ""
|
||||
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))
|
||||
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 {
|
||||
@ -376,9 +434,11 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool)
|
||||
}
|
||||
}
|
||||
for _, fileVar := range strings.Split(filePath, ",") {
|
||||
if fileVar != "" {
|
||||
if data, err := ioutil.ReadFile(fileVar); err == nil {
|
||||
return string(data), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
@ -58,6 +58,11 @@ func (f *BoolFlag) GetValue() string {
|
||||
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
|
||||
func (f *BoolFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// false if not found
|
||||
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 false
|
||||
|
@ -58,6 +58,11 @@ func (f *DurationFlag) GetValue() 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
|
||||
func (f *DurationFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// 0 if not found
|
||||
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 0
|
||||
|
@ -58,12 +58,16 @@ func (f *Float64Flag) GetValue() string {
|
||||
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
|
||||
func (f *Float64Flag) Apply(set *flag.FlagSet) error {
|
||||
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
|
||||
if val != "" {
|
||||
valFloat, err := strconv.ParseFloat(val, 10)
|
||||
|
||||
valFloat, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
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
|
||||
// 0 if not found
|
||||
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 0
|
||||
|
@ -19,6 +19,16 @@ func NewFloat64Slice(defaults ...float64) *Float64Slice {
|
||||
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
|
||||
func (f *Float64Slice) Set(value string) error {
|
||||
if !f.hasBeenSet {
|
||||
@ -117,6 +127,11 @@ func (f *Float64SliceFlag) GetValue() string {
|
||||
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
|
||||
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range f.Names() {
|
||||
if f.Value == nil {
|
||||
f.Value = &Float64Slice{}
|
||||
}
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
copyValue := f.Value.clone()
|
||||
for _, name := range f.Names() {
|
||||
set.Var(copyValue, name, f.Usage)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -146,7 +165,7 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// Float64Slice looks up the value of a local Float64SliceFlag, returns
|
||||
// nil if not found
|
||||
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 nil
|
||||
|
@ -66,6 +66,11 @@ func (f *GenericFlag) GetValue() string {
|
||||
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
|
||||
// provided by the user for parsing by the flag
|
||||
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
|
||||
// nil if not found
|
||||
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 nil
|
||||
|
@ -58,6 +58,11 @@ func (f *IntFlag) GetValue() string {
|
||||
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
|
||||
func (f *IntFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// 0 if not found
|
||||
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 0
|
||||
|
@ -58,6 +58,11 @@ func (f *Int64Flag) GetValue() string {
|
||||
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
|
||||
func (f *Int64Flag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// 0 if not found
|
||||
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 0
|
||||
|
@ -19,6 +19,16 @@ func NewInt64Slice(defaults ...int64) *Int64Slice {
|
||||
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
|
||||
func (i *Int64Slice) Set(value string) error {
|
||||
if !i.hasBeenSet {
|
||||
@ -118,6 +128,11 @@ func (f *Int64SliceFlag) GetValue() string {
|
||||
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
|
||||
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
}
|
||||
|
||||
for _, name := range f.Names() {
|
||||
if f.Value == nil {
|
||||
f.Value = &Int64Slice{}
|
||||
}
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
copyValue := f.Value.clone()
|
||||
for _, name := range f.Names() {
|
||||
set.Var(copyValue, name, f.Usage)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -145,7 +164,10 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// Int64Slice looks up the value of a local Int64SliceFlag, returns
|
||||
// nil if not found
|
||||
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 {
|
||||
|
@ -19,6 +19,16 @@ func NewIntSlice(defaults ...int) *IntSlice {
|
||||
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 ?
|
||||
// SetInt directly adds an integer to the list of values
|
||||
func (i *IntSlice) SetInt(value int) {
|
||||
@ -129,6 +139,11 @@ func (f *IntSliceFlag) GetValue() string {
|
||||
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
|
||||
func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
}
|
||||
|
||||
for _, name := range f.Names() {
|
||||
if f.Value == nil {
|
||||
f.Value = &IntSlice{}
|
||||
}
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
copyValue := f.Value.clone()
|
||||
for _, name := range f.Names() {
|
||||
set.Var(copyValue, name, f.Usage)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -156,8 +175,8 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// IntSlice looks up the value of a local IntSliceFlag, returns
|
||||
// nil if not found
|
||||
func (c *Context) IntSlice(name string) []int {
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupIntSlice(name, c.flagSet)
|
||||
if fs := c.lookupFlagSet(name); fs != nil {
|
||||
return lookupIntSlice(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -54,6 +54,11 @@ func (f *PathFlag) GetValue() string {
|
||||
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
|
||||
func (f *PathFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// "" if not found
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,11 @@ func (f *StringFlag) GetValue() string {
|
||||
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
|
||||
func (f *StringFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// "" if not found
|
||||
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 ""
|
||||
@ -85,10 +90,7 @@ func (c *Context) String(name string) string {
|
||||
func lookupString(name string, set *flag.FlagSet) string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := f.Value.String(), error(nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
parsed := f.Value.String()
|
||||
return parsed
|
||||
}
|
||||
return ""
|
||||
|
@ -18,6 +18,16 @@ func NewStringSlice(defaults ...string) *StringSlice {
|
||||
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
|
||||
func (s *StringSlice) Set(value string) error {
|
||||
if !s.hasBeenSet {
|
||||
@ -114,10 +124,24 @@ func (f *StringSliceFlag) GetValue() string {
|
||||
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
|
||||
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 f.Value == nil {
|
||||
f.Value = &StringSlice{}
|
||||
}
|
||||
destination := f.Value
|
||||
if f.Destination != nil {
|
||||
destination = f.Destination
|
||||
@ -135,17 +159,15 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
f.HasBeenSet = true
|
||||
}
|
||||
|
||||
for _, name := range f.Names() {
|
||||
if f.Value == nil {
|
||||
f.Value = &StringSlice{}
|
||||
}
|
||||
|
||||
if f.Destination != nil {
|
||||
set.Var(f.Destination, name, f.Usage)
|
||||
continue
|
||||
setValue := f.Destination
|
||||
if f.Destination == nil {
|
||||
setValue = f.Value.clone()
|
||||
}
|
||||
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
for _, name := range f.Names() {
|
||||
set.Var(setValue, name, f.Usage)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -154,7 +176,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// StringSlice looks up the value of a local StringSliceFlag, returns
|
||||
// nil if not found
|
||||
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 nil
|
||||
|
302
flag_test.go
302
flag_test.go
@ -22,6 +22,13 @@ var boolFlagTests = []struct {
|
||||
{"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) {
|
||||
for _, test := range boolFlagTests {
|
||||
fl := &BoolFlag{Name: test.name}
|
||||
@ -45,15 +52,21 @@ func TestBoolFlagApply_SetsAllNames(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 {
|
||||
s := NewIntSlice(defaults...)
|
||||
s.hasBeenSet = true
|
||||
s.hasBeenSet = false
|
||||
return *s
|
||||
}
|
||||
|
||||
newSetInt64Slice := func(defaults ...int64) Int64Slice {
|
||||
s := NewInt64Slice(defaults...)
|
||||
s.hasBeenSet = true
|
||||
s.hasBeenSet = false
|
||||
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: .*`},
|
||||
{"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,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: .*`},
|
||||
@ -114,6 +130,7 @@ func TestFlagsFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range flagTests {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1)
|
||||
_ = os.Setenv(envVarSlice.Index(0).String(), test.input)
|
||||
@ -183,7 +200,7 @@ func TestStringFlagDefaultText(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_FOO", "derp")
|
||||
|
||||
@ -263,6 +280,7 @@ func TestPathFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPathFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_PATH", "/path/to/file")
|
||||
for _, test := range pathFlagTests {
|
||||
@ -331,11 +349,11 @@ var stringSliceFlagTests = []struct {
|
||||
value *StringSlice
|
||||
expected string
|
||||
}{
|
||||
{"foo", nil, NewStringSlice(""), "--foo value\t"},
|
||||
{"f", nil, NewStringSlice(""), "-f value\t"},
|
||||
{"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")"},
|
||||
{"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")"},
|
||||
{"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")"},
|
||||
{"foo", nil, NewStringSlice(""), "--foo value\t(accepts multiple inputs)"},
|
||||
{"f", nil, NewStringSlice(""), "-f value\t(accepts multiple inputs)"},
|
||||
{"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")\t(accepts multiple inputs)"},
|
||||
{"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\")\t(accepts multiple inputs)"},
|
||||
}
|
||||
|
||||
func TestStringSliceFlagHelpOutput(t *testing.T) {
|
||||
@ -350,6 +368,7 @@ func TestStringSliceFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_QWWX", "11,4")
|
||||
|
||||
@ -376,6 +395,32 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) {
|
||||
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 {
|
||||
name string
|
||||
expected string
|
||||
@ -396,6 +441,7 @@ func TestIntFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIntFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_BAR", "2")
|
||||
|
||||
@ -444,6 +490,7 @@ func TestInt64FlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_BAR", "2")
|
||||
|
||||
@ -481,6 +528,7 @@ func TestUintFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUintFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_BAR", "2")
|
||||
|
||||
@ -518,6 +566,7 @@ func TestUint64FlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_BAR", "2")
|
||||
|
||||
@ -555,6 +604,7 @@ func TestDurationFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_BAR", "2h3m6s")
|
||||
|
||||
@ -589,9 +639,9 @@ var intSliceFlagTests = []struct {
|
||||
value *IntSlice
|
||||
expected string
|
||||
}{
|
||||
{"heads", nil, NewIntSlice(), "--heads value\t"},
|
||||
{"H", nil, NewIntSlice(), "-H value\t"},
|
||||
{"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)"},
|
||||
{"heads", nil, NewIntSlice(), "--heads value\t(accepts multiple inputs)"},
|
||||
{"H", nil, NewIntSlice(), "-H value\t(accepts multiple inputs)"},
|
||||
{"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)\t(accepts multiple inputs)"},
|
||||
}
|
||||
|
||||
func TestIntSliceFlagHelpOutput(t *testing.T) {
|
||||
@ -606,6 +656,7 @@ func TestIntSliceFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_SMURF", "42,3")
|
||||
|
||||
@ -632,16 +683,55 @@ func TestIntSliceFlagApply_SetsAllNames(t *testing.T) {
|
||||
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 {
|
||||
name string
|
||||
aliases []string
|
||||
value *Int64Slice
|
||||
expected string
|
||||
}{
|
||||
{"heads", nil, NewInt64Slice(), "--heads value\t"},
|
||||
{"H", nil, NewInt64Slice(), "-H value\t"},
|
||||
{"heads", nil, NewInt64Slice(), "--heads value\t(accepts multiple inputs)"},
|
||||
{"H", nil, NewInt64Slice(), "-H value\t(accepts multiple inputs)"},
|
||||
{"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) {
|
||||
@ -656,6 +746,7 @@ func TestInt64SliceFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = 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 {
|
||||
name string
|
||||
expected string
|
||||
@ -693,6 +838,7 @@ func TestFloat64FlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_BAZ", "99.4")
|
||||
|
||||
@ -727,10 +873,10 @@ var float64SliceFlagTests = []struct {
|
||||
value *Float64Slice
|
||||
expected string
|
||||
}{
|
||||
{"heads", nil, NewFloat64Slice(), "--heads value\t"},
|
||||
{"H", nil, NewFloat64Slice(), "-H value\t"},
|
||||
{"heads", nil, NewFloat64Slice(), "--heads value\t(accepts multiple inputs)"},
|
||||
{"H", nil, NewFloat64Slice(), "-H value\t(accepts multiple inputs)"},
|
||||
{"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) {
|
||||
@ -745,6 +891,7 @@ func TestFloat64SliceFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_SMURF", "0.1234,-10.5")
|
||||
for _, test := range float64SliceFlagTests {
|
||||
@ -782,6 +929,7 @@ func TestGenericFlagHelpOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_ZAP", "3")
|
||||
|
||||
@ -844,6 +992,7 @@ func TestParseDestinationString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_COUNT", "20")
|
||||
_ = (&App{
|
||||
@ -863,6 +1012,7 @@ func TestParseMultiStringFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_COUNT", "20")
|
||||
_ = (&App{
|
||||
@ -937,6 +1087,7 @@ func TestParseMultiStringSliceWithDestination(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -976,6 +1127,7 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -996,6 +1148,7 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1016,6 +1169,7 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1036,6 +1190,7 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1056,6 +1211,7 @@ func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiStringSliceFromEnvWithDestination(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1112,6 +1268,7 @@ func TestParseDestinationInt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_TIMEOUT_SECONDS", "10")
|
||||
_ = (&App{
|
||||
@ -1131,6 +1288,7 @@ func TestParseMultiIntFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_TIMEOUT_SECONDS", "10")
|
||||
_ = (&App{
|
||||
@ -1201,6 +1359,7 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1221,6 +1380,7 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1241,6 +1401,7 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiIntSliceFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,40")
|
||||
|
||||
@ -1278,6 +1439,7 @@ func TestParseMultiInt64Slice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiInt64SliceFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,17179869184")
|
||||
|
||||
@ -1298,6 +1460,7 @@ func TestParseMultiInt64SliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "20,30,17179869184")
|
||||
|
||||
@ -1353,6 +1516,7 @@ func TestParseDestinationFloat64(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64FromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||
_ = (&App{
|
||||
@ -1372,6 +1536,7 @@ func TestParseMultiFloat64FromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5")
|
||||
_ = (&App{
|
||||
@ -1391,6 +1556,7 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64SliceFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "0.1,-10.5")
|
||||
|
||||
@ -1411,6 +1577,7 @@ func TestParseMultiFloat64SliceFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_INTERVALS", "0.1234,-10.5")
|
||||
|
||||
@ -1490,6 +1657,7 @@ func TestParseDestinationBool(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiBoolFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_DEBUG", "1")
|
||||
_ = (&App{
|
||||
@ -1509,6 +1677,7 @@ func TestParseMultiBoolFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseMultiBoolFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_DEBUG", "1")
|
||||
_ = (&App{
|
||||
@ -1539,6 +1708,7 @@ func TestParseBoolFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range boolFlagTests {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("DEBUG", test.input)
|
||||
_ = (&App{
|
||||
@ -1615,6 +1785,7 @@ func TestParseGeneric(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseGenericFromEnv(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_SERVE", "20,30")
|
||||
_ = (&App{
|
||||
@ -1639,6 +1810,7 @@ func TestParseGenericFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseGenericFromEnvCascade(t *testing.T) {
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
_ = os.Setenv("APP_FOO", "99,2000")
|
||||
_ = (&App{
|
||||
@ -1659,14 +1831,16 @@ func TestParseGenericFromEnvCascade(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlagFromFile(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_FOO", "123")
|
||||
|
||||
temp, err := ioutil.TempFile("", "urfave_cli_test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer resetEnv(os.Environ())
|
||||
os.Clearenv()
|
||||
os.Setenv("APP_FOO", "123")
|
||||
|
||||
_, _ = io.WriteString(temp, "abc")
|
||||
_ = temp.Close()
|
||||
defer func() {
|
||||
@ -1778,6 +1952,17 @@ func TestTimestampFlagApply(t *testing.T) {
|
||||
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) {
|
||||
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: "randomlayout"}
|
||||
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"})
|
||||
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)
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ type TimestampFlag struct {
|
||||
Value *Timestamp
|
||||
DefaultText string
|
||||
HasBeenSet bool
|
||||
Destination *Timestamp
|
||||
}
|
||||
|
||||
// IsSet returns whether or not the flag has been set through env or file
|
||||
@ -113,14 +114,25 @@ func (f *TimestampFlag) GetValue() string {
|
||||
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
|
||||
func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
|
||||
if f.Layout == "" {
|
||||
return fmt.Errorf("timestamp Layout is required")
|
||||
}
|
||||
if f.Value == nil {
|
||||
f.Value = &Timestamp{}
|
||||
}
|
||||
f.Value.SetLayout(f.Layout)
|
||||
|
||||
if f.Destination != nil {
|
||||
f.Destination.SetLayout(f.Layout)
|
||||
}
|
||||
|
||||
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
|
||||
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)
|
||||
@ -129,6 +141,11 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
|
||||
}
|
||||
|
||||
for _, name := range f.Names() {
|
||||
if f.Destination != nil {
|
||||
set.Var(f.Destination, name, f.Usage)
|
||||
continue
|
||||
}
|
||||
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
}
|
||||
return nil
|
||||
@ -136,7 +153,7 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
|
||||
|
||||
// Timestamp gets the timestamp from a flag name
|
||||
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 nil
|
||||
|
@ -52,6 +52,11 @@ func (f *UintFlag) GetUsage() string {
|
||||
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
|
||||
func (f *UintFlag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// 0 if not found
|
||||
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 0
|
||||
|
@ -52,6 +52,11 @@ func (f *Uint64Flag) GetUsage() string {
|
||||
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
|
||||
func (f *Uint64Flag) Apply(set *flag.FlagSet) error {
|
||||
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
|
||||
// 0 if not found
|
||||
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 0
|
||||
|
2
funcs.go
2
funcs.go
@ -17,7 +17,7 @@ type ActionFunc func(*Context) error
|
||||
// CommandNotFoundFunc is executed if the proper command cannot be found
|
||||
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
|
||||
// original error messages. If this function is not set, the "Incorrect usage"
|
||||
// is displayed and the execution is interrupted.
|
||||
|
4
go.mod
4
go.mod
@ -5,6 +5,6 @@ go 1.11
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/antzucaro/matchr v0.0.0-20180616170659-cbc221335f3c
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
||||
|
12
go.sum
12
go.sum
@ -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/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/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/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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
28
help.go
28
help.go
@ -77,13 +77,13 @@ func ShowAppHelpAndExit(c *Context, exitCode int) {
|
||||
|
||||
// ShowAppHelp is an action that displays the help.
|
||||
func ShowAppHelp(c *Context) error {
|
||||
template := c.App.CustomAppHelpTemplate
|
||||
if template == "" {
|
||||
template = AppHelpTemplate
|
||||
tpl := c.App.CustomAppHelpTemplate
|
||||
if tpl == "" {
|
||||
tpl = AppHelpTemplate
|
||||
}
|
||||
|
||||
if c.App.ExtraInfo == nil {
|
||||
HelpPrinter(c.App.Writer, template, c.App)
|
||||
HelpPrinter(c.App.Writer, tpl, c.App)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ func ShowAppHelp(c *Context) error {
|
||||
"ExtraInfo": c.App.ExtraInfo,
|
||||
}
|
||||
}
|
||||
HelpPrinterCustom(c.App.Writer, template, c.App, customAppData())
|
||||
HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData())
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -225,6 +225,12 @@ func ShowCommandHelp(ctx *Context, command string) error {
|
||||
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
|
||||
func ShowSubcommandHelp(c *Context) error {
|
||||
if c == nil {
|
||||
@ -275,6 +281,9 @@ func ShowCommandCompletions(ctx *Context, command string) {
|
||||
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
|
||||
funcMap := template.FuncMap{
|
||||
"join": strings.Join,
|
||||
"indent": indent,
|
||||
"nindent": nindent,
|
||||
"trim": strings.TrimSpace,
|
||||
}
|
||||
for key, value := range customFuncs {
|
||||
funcMap[key] = value
|
||||
@ -377,3 +386,12 @@ func checkCommandCompletions(c *Context, name string) bool {
|
||||
ShowCommandCompletions(c, name)
|
||||
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)
|
||||
}
|
||||
|
275
help_test.go
275
help_test.go
@ -5,6 +5,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"strings"
|
||||
"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) {
|
||||
oldFlag := HelpFlag
|
||||
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) {
|
||||
app := &App{
|
||||
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) {
|
||||
app := &App{
|
||||
Commands: []*Command{
|
||||
@ -762,3 +843,197 @@ VERSION:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -3,24 +3,17 @@ package cli
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
wd, _ = os.Getwd()
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = os.Setenv("CLI_TEMPLATE_REPANIC", "1")
|
||||
}
|
||||
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
_, fn, line, _ := runtime.Caller(1)
|
||||
fn = strings.Replace(fn, wd+"/", "", -1)
|
||||
t.Helper()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -193,8 +193,8 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
|
||||
cliBuiltFilePath = "./internal/example-cli/built-example"
|
||||
helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go"
|
||||
helloBuiltFilePath = "./internal/example-hello-world/built-example"
|
||||
desiredMinBinarySize = 2.0
|
||||
desiredMaxBinarySize = 2.1
|
||||
desiredMinBinarySize = 1.9
|
||||
desiredMaxBinarySize = 2.2
|
||||
badNewsEmoji = "🚨"
|
||||
goodNewsEmoji = "✨"
|
||||
checksPassedEmoji = "✅"
|
||||
|
26
template.go
26
template.go
@ -7,13 +7,13 @@ var AppHelpTemplate = `NAME:
|
||||
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
|
||||
|
||||
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}}{{end}}{{end}}{{if .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}}:
|
||||
{{range $index, $author := .Authors}}{{if $index}}
|
||||
@ -39,13 +39,13 @@ var CommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.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}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlags}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
@ -59,10 +59,10 @@ var SubcommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.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}}{{end}}
|
||||
{{.Description | nindent 3 | trim}}{{end}}
|
||||
|
||||
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||
{{.Name}}:{{range .VisibleCommands}}
|
||||
@ -74,9 +74,9 @@ OPTIONS:
|
||||
{{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 }}
|
||||
|
||||
@ -86,16 +86,18 @@ var MarkdownDocTemplate = `% {{ .App.Name }} 8
|
||||
{{ if .SynopsisArgs }}
|
||||
` + "```" + `
|
||||
{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + `
|
||||
{{ end }}{{ if .App.UsageText }}
|
||||
{{ end }}{{ if .App.Description }}
|
||||
# DESCRIPTION
|
||||
|
||||
{{ .App.UsageText }}
|
||||
{{ .App.Description }}
|
||||
{{ end }}
|
||||
**Usage**:
|
||||
|
||||
` + "```" + `
|
||||
` + "```" + `{{ if .App.UsageText }}
|
||||
{{ .App.UsageText }}
|
||||
{{ else }}
|
||||
{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
` + "```" + `
|
||||
{{ end }}` + "```" + `
|
||||
{{ if .GlobalArgs }}
|
||||
# GLOBAL OPTIONS
|
||||
{{ range $v := .GlobalArgs }}
|
||||
|
72
testdata/expected-doc-full.man
vendored
72
testdata/expected-doc-full.man
vendored
@ -3,7 +3,7 @@
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
greet \- Some app
|
||||
greet - Some app
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
@ -14,9 +14,9 @@ greet
|
||||
.RS
|
||||
|
||||
.nf
|
||||
[\-\-another\-flag|\-b]
|
||||
[\-\-flag|\-\-fl|\-f]=[value]
|
||||
[\-\-socket|\-s]=[value]
|
||||
[--another-flag|-b]
|
||||
[--flag|--fl|-f]=[value]
|
||||
[--socket|-s]=[value]
|
||||
|
||||
.fi
|
||||
.RE
|
||||
@ -24,7 +24,7 @@ greet
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
app [first\_arg] [second\_arg]
|
||||
Description of the application.
|
||||
|
||||
.PP
|
||||
\fBUsage\fP:
|
||||
@ -33,7 +33,7 @@ app [first\_arg] [second\_arg]
|
||||
.RS
|
||||
|
||||
.nf
|
||||
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
app [first_arg] [second_arg]
|
||||
|
||||
.fi
|
||||
.RE
|
||||
@ -41,13 +41,13 @@ greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
|
||||
.SH GLOBAL OPTIONS
|
||||
.PP
|
||||
\fB\-\-another\-flag, \-b\fP: another usage text
|
||||
\fB--another-flag, -b\fP: another usage text
|
||||
|
||||
.PP
|
||||
\fB\-\-flag, \-\-fl, \-f\fP="":
|
||||
\fB--flag, --fl, -f\fP="":
|
||||
|
||||
.PP
|
||||
\fB\-\-socket, \-s\fP="": some 'usage' text (default: value)
|
||||
\fB--socket, -s\fP="": some 'usage' text (default: value)
|
||||
|
||||
|
||||
.SH COMMANDS
|
||||
@ -56,23 +56,65 @@ greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
another usage test
|
||||
|
||||
.PP
|
||||
\fB\-\-another\-flag, \-b\fP: another usage text
|
||||
\fB--another-flag, -b\fP: another usage text
|
||||
|
||||
.PP
|
||||
\fB\-\-flag, \-\-fl, \-f\fP="":
|
||||
\fB--flag, --fl, -f\fP="":
|
||||
|
||||
.SS sub\-config, s, ss
|
||||
.SS sub-config, s, ss
|
||||
.PP
|
||||
another usage test
|
||||
|
||||
.PP
|
||||
\fB\-\-sub\-command\-flag, \-s\fP: some usage text
|
||||
\fB--sub-command-flag, -s\fP: some usage text
|
||||
|
||||
.PP
|
||||
\fB\-\-sub\-flag, \-\-sub\-fl, \-s\fP="":
|
||||
\fB--sub-flag, --sub-fl, -s\fP="":
|
||||
|
||||
.SH info, i, in
|
||||
.PP
|
||||
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
|
||||
|
32
testdata/expected-doc-full.md
vendored
32
testdata/expected-doc-full.md
vendored
@ -1,5 +1,3 @@
|
||||
% greet 8
|
||||
|
||||
# NAME
|
||||
|
||||
greet - Some app
|
||||
@ -16,12 +14,12 @@ greet
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
app [first_arg] [second_arg]
|
||||
Description of the application.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```
|
||||
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
app [first_arg] [second_arg]
|
||||
```
|
||||
|
||||
# GLOBAL OPTIONS
|
||||
@ -58,3 +56,29 @@ 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
|
||||
|
32
testdata/expected-doc-no-authors.md
vendored
32
testdata/expected-doc-no-authors.md
vendored
@ -1,5 +1,3 @@
|
||||
% greet 8
|
||||
|
||||
# NAME
|
||||
|
||||
greet - Some app
|
||||
@ -16,12 +14,12 @@ greet
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
app [first_arg] [second_arg]
|
||||
Description of the application.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```
|
||||
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
app [first_arg] [second_arg]
|
||||
```
|
||||
|
||||
# GLOBAL OPTIONS
|
||||
@ -58,3 +56,29 @@ 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
|
||||
|
6
testdata/expected-doc-no-commands.md
vendored
6
testdata/expected-doc-no-commands.md
vendored
@ -1,5 +1,3 @@
|
||||
% greet 8
|
||||
|
||||
# NAME
|
||||
|
||||
greet - Some app
|
||||
@ -16,12 +14,12 @@ greet
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
app [first_arg] [second_arg]
|
||||
Description of the application.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```
|
||||
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
app [first_arg] [second_arg]
|
||||
```
|
||||
|
||||
# GLOBAL OPTIONS
|
||||
|
32
testdata/expected-doc-no-flags.md
vendored
32
testdata/expected-doc-no-flags.md
vendored
@ -1,5 +1,3 @@
|
||||
% greet 8
|
||||
|
||||
# NAME
|
||||
|
||||
greet - Some app
|
||||
@ -10,12 +8,12 @@ greet
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
app [first_arg] [second_arg]
|
||||
Description of the application.
|
||||
|
||||
**Usage**:
|
||||
|
||||
```
|
||||
greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
||||
app [first_arg] [second_arg]
|
||||
```
|
||||
|
||||
# COMMANDS
|
||||
@ -43,3 +41,29 @@ 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
|
||||
|
84
testdata/expected-doc-no-usagetext.md
vendored
Normal file
84
testdata/expected-doc-no-usagetext.md
vendored
Normal 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
|
10
testdata/expected-fish-full.fish
vendored
10
testdata/expected-fish-full.fish
vendored
@ -2,7 +2,7 @@
|
||||
|
||||
function __fish_greet_no_subcommand --description 'Test if there has been any subcommand yet'
|
||||
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
|
||||
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' -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' -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 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'
|
||||
@ -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 -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 -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'
|
||||
|
Loading…
Reference in New Issue
Block a user