Merge remote-tracking branch 'origin/main' into merging-main-to-v3-dev-main

This commit is contained in:
Dan Buch 2022-07-16 07:44:00 -04:00
commit a82c9b1433
Signed by: meatballhat
GPG Key ID: A12F782281063434
86 changed files with 12211 additions and 2455 deletions

17
.github/stale.yml vendored
View File

@ -1,17 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 90
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue 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.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -34,20 +34,26 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: GOFMT Check - name: GOFMT Check
if: matrix.go == '1.17.x' && matrix.os == 'ubuntu-latest' if: matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest'
run: test -z $(gofmt -l .) run: test -z $(gofmt -l .)
- name: vet - name: vet
run: go run internal/build/build.go vet run: go run internal/build/build.go vet
- name: test with urfave_cli_no_docs tag
run: go run internal/build/build.go -tags urfave_cli_no_docs test
- name: test - name: test
run: go run internal/build/build.go test run: go run internal/build/build.go test
- name: check-binary-size - name: check-binary-size
run: go run internal/build/build.go check-binary-size run: go run internal/build/build.go check-binary-size
- name: check-binary-size with tags (informational only)
run: go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == '1.17.x' && matrix.os == 'ubuntu-latest' if: success() && matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
with: with:
fail_ci_if_error: true fail_ci_if_error: true
@ -73,14 +79,37 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Dependencies - name: Install Dependencies
run: run: |
mkdir -p "${GITHUB_WORKSPACE}/.local/bin" && mkdir -p "${GITHUB_WORKSPACE}/.local/bin"
curl -fsSL -o "${GITHUB_WORKSPACE}/.local/bin/gfmrun" "https://github.com/urfave/gfmrun/releases/download/v1.3.0/gfmrun-$(go env GOOS)-$(go env GOARCH)-v1.3.0" && curl -fsSL -o "${GITHUB_WORKSPACE}/.local/bin/gfmrun" "https://github.com/urfave/gfmrun/releases/download/v1.3.0/gfmrun-$(go env GOOS)-$(go env GOARCH)-v1.3.0"
chmod +x "${GITHUB_WORKSPACE}/.local/bin/gfmrun" && chmod +x "${GITHUB_WORKSPACE}/.local/bin/gfmrun"
npm install -g markdown-toc@1.2.0
- name: gfmrun - name: gfmrun
run: go run internal/build/build.go gfmrun docs/v2/manual.md run: go run internal/build/build.go gfmrun docs/v2/manual.md
- name: toc - name: diff check
run: go run internal/build/build.go toc docs/v2/manual.md run: |
git diff --exit-code
git diff --cached --exit-code
publish:
if: startswith(github.ref, 'refs/tags/')
name: publish
needs: [test-docs]
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup mkdocs
run: |
pip install -U pip
pip install -r mkdocs-requirements.txt
git remote rm origin
git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/urfave/cli.git
- name: Publish Docs
run: |
mkdocs gh-deploy --force

2
.gitignore vendored
View File

@ -1,10 +1,10 @@
*.coverprofile *.coverprofile
*.orig *.orig
node_modules/
vendor vendor
.idea .idea
internal/*/built-example internal/*/built-example
coverage.txt coverage.txt
/.local/ /.local/
/site/
*.exe *.exe

View File

@ -55,11 +55,12 @@ further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be reported by contacting urfave-governance@googlegroups.com, a members-only group
reviewed and investigated and will result in a response that is deemed necessary that is world-postable. All complaints will be reviewed and investigated and
and appropriate to the circumstances. The project team is obligated to maintain will result in a response that is deemed necessary and appropriate to the
confidentiality with regard to the reporter of an incident. Further details of circumstances. The project team is obligated to maintain confidentiality with
specific enforcement policies may be posted separately. regard to the reporter of an incident. Further details of specific enforcement
policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other faith may face temporary or permanent repercussions as determined by other

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2016 Jeremy Saenz & Contributors Copyright (c) 2022 urfave/cli maintainers
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

40
Makefile Normal file
View File

@ -0,0 +1,40 @@
# NOTE: this Makefile is meant to provide a simplified entry point for humans to
# run all of the critical steps to verify one's changes are harmonious in
# nature. Keeping target bodies to one line each and abstaining from make magic
# are very important so that maintainers and contributors can focus their
# attention on files that are primarily Go.
.PHONY: all
all: generate vet tag-test test check-binary-size tag-check-binary-size gfmrun v2diff
# NOTE: this is a special catch-all rule to run any of the commands
# defined in internal/build/build.go with optional arguments passed
# via GFLAGS (global flags) and FLAGS (command-specific flags), e.g.:
#
# $ make test GFLAGS='--packages cli'
%:
go run internal/build/build.go $(GFLAGS) $* $(FLAGS)
.PHONY: tag-test
tag-test:
go run internal/build/build.go -tags urfave_cli_no_docs test
.PHONY: tag-check-binary-size
tag-check-binary-size:
go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size
.PHONY: gfmrun
gfmrun:
go run internal/build/build.go gfmrun docs/v2/manual.md
.PHONY: docs
docs:
mkdocs build
.PHONY: docs-deps
docs-deps:
pip install -r mkdocs-requirements.txt
.PHONY: serve-docs
serve-docs:
mkdocs serve

View File

@ -1,5 +1,4 @@
cli # cli
===
[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://pkg.go.dev/github.com/urfave/cli/v2) [![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://pkg.go.dev/github.com/urfave/cli/v2)
[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli)
@ -10,61 +9,11 @@ cli is a simple, fast, and fun package for building command line apps in Go. The
goal is to enable developers to write fast and distributable command line goal is to enable developers to write fast and distributable command line
applications in an expressive way. applications in an expressive way.
## Usage Documentation ## Documentation
Usage documentation exists for each major version. Don't know what version you're on? You're probably using the version from the `main` branch, which is currently `v2`. More documentation is available in [`./docs`](./docs) or the hosted
documentation site at <https://cli.urfave.org>.
- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) ## License
- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md)
Guides for migrating to newer versions: See [`LICENSE`](./LICENSE)
- `v1-to-v2` - [./docs/migrate-v1-to-v2.md](./docs/migrate-v1-to-v2.md)
## Installation
Using this package requires a working Go environment. [See the install instructions for Go](http://golang.org/doc/install.html).
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
```
$ go get github.com/urfave/cli/v2
```
```go
...
import (
"github.com/urfave/cli/v2" // imports as package "cli"
)
...
```
### Using `v1` releases
```
$ go get github.com/urfave/cli
```
```go
...
import (
"github.com/urfave/cli"
)
...
```
### GOPATH
Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can
be easily used:
```
export PATH=$PATH:$GOPATH/bin
```
### Supported platforms
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. To see our currently supported go versions and platforms, look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/main/.github/workflows/cli.yml).

View File

@ -13,18 +13,18 @@ import (
// allows a value to be set on the existing parsed flags. // allows a value to be set on the existing parsed flags.
type FlagInputSourceExtension interface { type FlagInputSourceExtension interface {
cli.Flag cli.Flag
ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error
} }
// ApplyInputSourceValues iterates over all provided flags and // ApplyInputSourceValues iterates over all provided flags and
// executes ApplyInputSourceValue on flags implementing the // executes ApplyInputSourceValue on flags implementing the
// FlagInputSourceExtension interface to initialize these flags // FlagInputSourceExtension interface to initialize these flags
// to an alternate input source. // to an alternate input source.
func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error { func ApplyInputSourceValues(cCtx *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error {
for _, f := range flags { for _, f := range flags {
inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension) inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension)
if isType { if isType {
err := inputSourceExtendedFlag.ApplyInputSourceValue(context, inputSourceContext) err := inputSourceExtendedFlag.ApplyInputSourceValue(cCtx, inputSourceContext)
if err != nil { if err != nil {
return err return err
} }
@ -38,34 +38,33 @@ func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSource
// input source based on the func provided. If there is no error it will then apply the new input source to any flags // input source based on the func provided. If there is no error it will then apply the new input source to any flags
// that are supported by the input source // that are supported by the input source
func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc { func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc {
return func(context *cli.Context) error { return func(cCtx *cli.Context) error {
inputSource, err := createInputSource() inputSource, err := createInputSource()
if err != nil { if err != nil {
return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error()) return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error())
} }
return ApplyInputSourceValues(context, inputSource, flags) return ApplyInputSourceValues(cCtx, inputSource, flags)
} }
} }
// InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new // InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new
// input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is // input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is
// no error it will then apply the new input source to any flags that are supported by the input source // no error it will then apply the new input source to any flags that are supported by the input source
func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) cli.BeforeFunc { func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *cli.Context) (InputSourceContext, error)) cli.BeforeFunc {
return func(context *cli.Context) error { return func(cCtx *cli.Context) error {
inputSource, err := createInputSource(context) inputSource, err := createInputSource(cCtx)
if err != nil { if err != nil {
return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error()) return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error())
} }
return ApplyInputSourceValues(context, inputSource, flags) return ApplyInputSourceValues(cCtx, inputSource, flags)
} }
} }
// ApplyInputSourceValue applies a generic value to the flagSet if required // ApplyInputSourceValue applies a generic value to the flagSet if required
func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.GenericFlag.Name) {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.Generic(f.GenericFlag.Name) value, err := isc.Generic(f.GenericFlag.Name)
if err != nil { if err != nil {
return err return err
@ -76,15 +75,13 @@ func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourc
} }
} }
} }
}
return nil return nil
} }
// ApplyInputSourceValue applies a StringSlice value to the flagSet if required // ApplyInputSourceValue applies a StringSlice value to the flagSet if required
func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.StringSliceFlag.Name) {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.StringSlice(f.StringSliceFlag.Name) value, err := isc.StringSlice(f.StringSliceFlag.Name)
if err != nil { if err != nil {
return err return err
@ -99,14 +96,12 @@ func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputS
} }
} }
} }
}
return nil return nil
} }
// ApplyInputSourceValue applies a IntSlice value if required // ApplyInputSourceValue applies a IntSlice value if required
func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.IntSliceFlag.Name) {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.IntSlice(f.IntSliceFlag.Name) value, err := isc.IntSlice(f.IntSliceFlag.Name)
if err != nil { if err != nil {
return err return err
@ -121,14 +116,12 @@ func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour
} }
} }
} }
}
return nil return nil
} }
// ApplyInputSourceValue applies a Bool value to the flagSet if required // ApplyInputSourceValue applies a Bool value to the flagSet if required
func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *BoolFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.BoolFlag.Name) {
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) {
value, err := isc.Bool(f.BoolFlag.Name) value, err := isc.Bool(f.BoolFlag.Name)
if err != nil { if err != nil {
return err return err
@ -139,14 +132,12 @@ func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
} }
} }
} }
}
return nil return nil
} }
// ApplyInputSourceValue applies a String value to the flagSet if required // ApplyInputSourceValue applies a String value to the flagSet if required
func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.StringFlag.Name) {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.String(f.StringFlag.Name) value, err := isc.String(f.StringFlag.Name)
if err != nil { if err != nil {
return err return err
@ -157,14 +148,12 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource
} }
} }
} }
}
return nil return nil
} }
// ApplyInputSourceValue applies a Path value to the flagSet if required // ApplyInputSourceValue applies a Path value to the flagSet if required
func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.PathFlag.Name) {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.String(f.PathFlag.Name) value, err := isc.String(f.PathFlag.Name)
if err != nil { if err != nil {
return err return err
@ -185,62 +174,49 @@ func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
} }
} }
} }
}
return nil return nil
} }
// ApplyInputSourceValue applies a int value to the flagSet if required // ApplyInputSourceValue applies a int value to the flagSet if required
func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.IntFlag.Name) {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.Int(f.IntFlag.Name) value, err := isc.Int(f.IntFlag.Name)
if err != nil { if err != nil {
return err return err
} }
if value > 0 {
for _, name := range f.Names() { for _, name := range f.Names() {
_ = f.set.Set(name, strconv.FormatInt(int64(value), 10)) _ = f.set.Set(name, strconv.FormatInt(int64(value), 10))
} }
} }
}
}
return nil return nil
} }
// ApplyInputSourceValue applies a Duration value to the flagSet if required // ApplyInputSourceValue applies a Duration value to the flagSet if required
func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *DurationFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.DurationFlag.Name) {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.Duration(f.DurationFlag.Name) value, err := isc.Duration(f.DurationFlag.Name)
if err != nil { if err != nil {
return err return err
} }
if value > 0 {
for _, name := range f.Names() { for _, name := range f.Names() {
_ = f.set.Set(name, value.String()) _ = f.set.Set(name, value.String())
} }
} }
}
}
return nil return nil
} }
// ApplyInputSourceValue applies a Float64 value to the flagSet if required // ApplyInputSourceValue applies a Float64 value to the flagSet if required
func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error { func (f *Float64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error {
if f.set != nil { if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.Float64Flag.Name) {
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) {
value, err := isc.Float64(f.Float64Flag.Name) value, err := isc.Float64(f.Float64Flag.Name)
if err != nil { if err != nil {
return err return err
} }
if value > 0 {
floatStr := float64ToString(value) floatStr := float64ToString(value)
for _, name := range f.Names() { for _, name := range f.Names() {
_ = f.set.Set(name, floatStr) _ = f.set.Set(name, floatStr)
} }
} }
}
}
return nil return nil
} }

View File

@ -26,29 +26,48 @@ type testApplyInputSource struct {
MapValue interface{} MapValue interface{}
} }
type racyInputSource struct {
*MapInputSource
}
func (ris *racyInputSource) isSet(name string) bool {
if _, ok := ris.MapInputSource.valueMap[name]; ok {
ris.MapInputSource.valueMap[name] = bogus{0}
}
return true
}
func TestGenericApplyInputSourceValue(t *testing.T) { func TestGenericApplyInputSourceValue(t *testing.T) {
v := &Parser{"abc", "def"} v := &Parser{"abc", "def"}
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}), Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}),
FlagName: "test", FlagName: "test",
MapValue: v, MapValue: v,
}) }
c := runTest(t, tis)
expect(t, v, c.Generic("test")) expect(t, v, c.Generic("test"))
c = runRacyTest(t, tis)
refute(t, v, c.Generic("test"))
} }
func TestGenericApplyInputSourceMethodContextSet(t *testing.T) { func TestGenericApplyInputSourceMethodContextSet(t *testing.T) {
p := &Parser{"abc", "def"} p := &Parser{"abc", "def"}
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}), Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}),
FlagName: "test", FlagName: "test",
MapValue: &Parser{"efg", "hig"}, MapValue: &Parser{"efg", "hig"},
ContextValueString: p.String(), ContextValueString: p.String(),
}) }
c := runTest(t, tis)
expect(t, p, c.Generic("test")) expect(t, p, c.Generic("test"))
c = runRacyTest(t, tis)
refute(t, p, c.Generic("test"))
} }
func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewGenericFlag(&cli.GenericFlag{ Flag: NewGenericFlag(&cli.GenericFlag{
Name: "test", Name: "test",
Value: &Parser{}, Value: &Parser{},
@ -58,17 +77,25 @@ func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) {
MapValue: &Parser{"efg", "hij"}, MapValue: &Parser{"efg", "hij"},
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "abc,def", EnvVarValue: "abc,def",
}) }
c := runTest(t, tis)
expect(t, &Parser{"abc", "def"}, c.Generic("test")) expect(t, &Parser{"abc", "def"}, c.Generic("test"))
c = runRacyTest(t, tis)
refute(t, &Parser{"abc", "def"}, c.Generic("test"))
} }
func TestStringSliceApplyInputSourceValue(t *testing.T) { func TestStringSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}), Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: []interface{}{"hello", "world"}, MapValue: []interface{}{"hello", "world"},
}) }
c := runTest(t, tis)
expect(t, c.StringSlice("test"), []string{"hello", "world"}) expect(t, c.StringSlice("test"), []string{"hello", "world"})
c = runRacyTest(t, tis)
refute(t, c.StringSlice("test"), []string{"hello", "world"})
} }
func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
@ -82,112 +109,154 @@ func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
} }
func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: []interface{}{"hello", "world"}, MapValue: []interface{}{"hello", "world"},
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "oh,no", EnvVarValue: "oh,no",
}) }
c := runTest(t, tis)
expect(t, c.StringSlice("test"), []string{"oh", "no"}) expect(t, c.StringSlice("test"), []string{"oh", "no"})
c = runRacyTest(t, tis)
refute(t, c.StringSlice("test"), []string{"oh", "no"})
} }
func TestIntSliceApplyInputSourceValue(t *testing.T) { func TestIntSliceApplyInputSourceValue(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}), Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: []interface{}{1, 2}, MapValue: []interface{}{1, 2},
}) }
c := runTest(t, tis)
expect(t, c.IntSlice("test"), []int{1, 2}) expect(t, c.IntSlice("test"), []int{1, 2})
c = runRacyTest(t, tis)
refute(t, c.IntSlice("test"), []int{1, 2})
} }
func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}), Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: []interface{}{1, 2}, MapValue: []interface{}{1, 2},
ContextValueString: "3", ContextValueString: "3",
}) }
c := runTest(t, tis)
expect(t, c.IntSlice("test"), []int{3}) expect(t, c.IntSlice("test"), []int{3})
c = runRacyTest(t, tis)
refute(t, c.IntSlice("test"), []int{3})
} }
func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: []interface{}{1, 2}, MapValue: []interface{}{1, 2},
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "3,4", EnvVarValue: "3,4",
}) }
c := runTest(t, tis)
expect(t, c.IntSlice("test"), []int{3, 4}) expect(t, c.IntSlice("test"), []int{3, 4})
c = runRacyTest(t, tis)
refute(t, c.IntSlice("test"), []int{3, 4})
} }
func TestBoolApplyInputSourceMethodSet(t *testing.T) { func TestBoolApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}), Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: true, MapValue: true,
}) }
c := runTest(t, tis)
expect(t, true, c.Bool("test")) expect(t, true, c.Bool("test"))
c = runRacyTest(t, tis)
refute(t, true, c.Bool("test"))
} }
func TestBoolApplyInputSourceMethodContextSet(t *testing.T) { func TestBoolApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}), Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: false, MapValue: false,
ContextValueString: "true", ContextValueString: "true",
}) }
c := runTest(t, tis)
expect(t, true, c.Bool("test")) expect(t, true, c.Bool("test"))
c = runRacyTest(t, tis)
refute(t, true, c.Bool("test"))
} }
func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewBoolFlag(&cli.BoolFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewBoolFlag(&cli.BoolFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: false, MapValue: false,
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "true", EnvVarValue: "true",
}) }
c := runTest(t, tis)
expect(t, true, c.Bool("test")) expect(t, true, c.Bool("test"))
c = runRacyTest(t, tis)
refute(t, true, c.Bool("test"))
} }
func TestStringApplyInputSourceMethodSet(t *testing.T) { func TestStringApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewStringFlag(&cli.StringFlag{Name: "test"}), Flag: NewStringFlag(&cli.StringFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: "hello", MapValue: "hello",
}) }
c := runTest(t, tis)
expect(t, "hello", c.String("test")) expect(t, "hello", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "hello", c.String("test"))
} }
func TestStringApplyInputSourceMethodContextSet(t *testing.T) { func TestStringApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewStringFlag(&cli.StringFlag{Name: "test"}), Flag: NewStringFlag(&cli.StringFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: "hello", MapValue: "hello",
ContextValueString: "goodbye", ContextValueString: "goodbye",
}) }
c := runTest(t, tis)
expect(t, "goodbye", c.String("test")) expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
} }
func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewStringFlag(&cli.StringFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewStringFlag(&cli.StringFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: "hello", MapValue: "hello",
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "goodbye", EnvVarValue: "goodbye",
})
expect(t, "goodbye", c.String("test"))
} }
c := runTest(t, tis)
expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
}
func TestPathApplyInputSourceMethodSet(t *testing.T) { func TestPathApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test"}), Flag: NewPathFlag(&cli.PathFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: "hello", MapValue: "hello",
SourcePath: "/path/to/source/file", SourcePath: "/path/to/source/file",
}) }
c := runTest(t, tis)
expected := "/path/to/source/hello" expected := "/path/to/source/hello"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -200,119 +269,214 @@ func TestPathApplyInputSourceMethodSet(t *testing.T) {
} }
} }
expect(t, expected, c.String("test")) expect(t, expected, c.String("test"))
c = runRacyTest(t, tis)
refute(t, expected, c.String("test"))
} }
func TestPathApplyInputSourceMethodContextSet(t *testing.T) { func TestPathApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test"}), Flag: NewPathFlag(&cli.PathFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: "hello", MapValue: "hello",
ContextValueString: "goodbye", ContextValueString: "goodbye",
SourcePath: "/path/to/source/file", SourcePath: "/path/to/source/file",
}) }
c := runTest(t, tis)
expect(t, "goodbye", c.String("test")) expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
} }
func TestPathApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestPathApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewPathFlag(&cli.PathFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewPathFlag(&cli.PathFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: "hello", MapValue: "hello",
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "goodbye", EnvVarValue: "goodbye",
SourcePath: "/path/to/source/file", SourcePath: "/path/to/source/file",
}) }
c := runTest(t, tis)
expect(t, "goodbye", c.String("test")) expect(t, "goodbye", c.String("test"))
c = runRacyTest(t, tis)
refute(t, "goodbye", c.String("test"))
} }
func TestIntApplyInputSourceMethodSet(t *testing.T) { func TestIntApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), Flag: NewIntFlag(&cli.IntFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: 15, MapValue: 15,
}) }
c := runTest(t, tis)
expect(t, 15, c.Int("test")) expect(t, 15, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, 15, c.Int("test"))
}
func TestIntApplyInputSourceMethodSetNegativeValue(t *testing.T) {
tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test"}),
FlagName: "test",
MapValue: -1,
}
c := runTest(t, tis)
expect(t, -1, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, -1, c.Int("test"))
} }
func TestIntApplyInputSourceMethodContextSet(t *testing.T) { func TestIntApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), Flag: NewIntFlag(&cli.IntFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: 15, MapValue: 15,
ContextValueString: "7", ContextValueString: "7",
}) }
c := runTest(t, tis)
expect(t, 7, c.Int("test")) expect(t, 7, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, 7, c.Int("test"))
} }
func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: 15, MapValue: 15,
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: "12", EnvVarValue: "12",
}) }
c := runTest(t, tis)
expect(t, 12, c.Int("test")) expect(t, 12, c.Int("test"))
c = runRacyTest(t, tis)
refute(t, 12, c.Int("test"))
} }
func TestDurationApplyInputSourceMethodSet(t *testing.T) { func TestDurationApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: 30 * time.Second, MapValue: 30 * time.Second,
}) }
c := runTest(t, tis)
expect(t, 30*time.Second, c.Duration("test")) expect(t, 30*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, 30*time.Second, c.Duration("test"))
}
func TestDurationApplyInputSourceMethodSetNegativeValue(t *testing.T) {
tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}),
FlagName: "test",
MapValue: -30 * time.Second,
}
c := runTest(t, tis)
expect(t, -30*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, -30*time.Second, c.Duration("test"))
} }
func TestDurationApplyInputSourceMethodContextSet(t *testing.T) { func TestDurationApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: 30 * time.Second, MapValue: 30 * time.Second,
ContextValueString: (15 * time.Second).String(), ContextValueString: (15 * time.Second).String(),
}) }
c := runTest(t, tis)
expect(t, 15*time.Second, c.Duration("test")) expect(t, 15*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, 15*time.Second, c.Duration("test"))
} }
func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: 30 * time.Second, MapValue: 30 * time.Second,
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: (15 * time.Second).String(), EnvVarValue: (15 * time.Second).String(),
}) }
c := runTest(t, tis)
expect(t, 15*time.Second, c.Duration("test")) expect(t, 15*time.Second, c.Duration("test"))
c = runRacyTest(t, tis)
refute(t, 15*time.Second, c.Duration("test"))
} }
func TestFloat64ApplyInputSourceMethodSet(t *testing.T) { func TestFloat64ApplyInputSourceMethodSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}), Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: 1.3, MapValue: 1.3,
}) }
c := runTest(t, tis)
expect(t, 1.3, c.Float64("test")) expect(t, 1.3, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, 1.3, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodSetNegativeValue(t *testing.T) {
tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}),
FlagName: "test",
MapValue: -1.3,
}
c := runTest(t, tis)
expect(t, -1.3, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, -1.3, c.Float64("test"))
}
func TestFloat64ApplyInputSourceMethodSetNegativeValueNotSet(t *testing.T) {
c := runTest(t, testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test1"}),
FlagName: "test1",
// dont set map value
})
expect(t, 0.0, c.Float64("test1"))
} }
func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) { func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}), Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}),
FlagName: "test", FlagName: "test",
MapValue: 1.3, MapValue: 1.3,
ContextValueString: fmt.Sprintf("%v", 1.4), ContextValueString: fmt.Sprintf("%v", 1.4),
}) }
c := runTest(t, tis)
expect(t, 1.4, c.Float64("test")) expect(t, 1.4, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, 1.4, c.Float64("test"))
} }
func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) { func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) {
c := runTest(t, testApplyInputSource{ tis := testApplyInputSource{
Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test", EnvVars: []string{"TEST"}}), Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test", EnvVars: []string{"TEST"}}),
FlagName: "test", FlagName: "test",
MapValue: 1.3, MapValue: 1.3,
EnvVarName: "TEST", EnvVarName: "TEST",
EnvVarValue: fmt.Sprintf("%v", 1.4), EnvVarValue: fmt.Sprintf("%v", 1.4),
}) }
c := runTest(t, tis)
expect(t, 1.4, c.Float64("test")) expect(t, 1.4, c.Float64("test"))
c = runRacyTest(t, tis)
refute(t, 1.4, c.Float64("test"))
} }
func runTest(t *testing.T, test testApplyInputSource) *cli.Context { func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
@ -340,6 +504,19 @@ func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
return c return c
} }
func runRacyTest(t *testing.T, test testApplyInputSource) *cli.Context {
set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError)
c := cli.NewContext(nil, set, nil)
_ = test.Flag.ApplyInputSourceValue(c, &racyInputSource{
MapInputSource: &MapInputSource{
file: test.SourcePath,
valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue},
},
})
return c
}
type Parser [2]string type Parser [2]string
func (p *Parser) Set(value string) error { func (p *Parser) Set(value string) error {
@ -357,3 +534,5 @@ func (p *Parser) Set(value string) error {
func (p *Parser) String() string { func (p *Parser) String() string {
return fmt.Sprintf("%s,%s", p[0], p[1]) return fmt.Sprintf("%s,%s", p[0], p[1])
} }
type bogus [1]uint

View File

@ -22,7 +22,10 @@ func expect(t *testing.T, a interface{}, b interface{}) {
} }
func refute(t *testing.T, a interface{}, b interface{}) { func refute(t *testing.T, a interface{}, b interface{}) {
if a == b { _, fn, line, _ := runtime.Caller(1)
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) fn = strings.Replace(fn, wd+"/", "", -1)
if reflect.DeepEqual(a, b) {
t.Errorf("(%s:%d) Did not expect %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a))
} }
} }

View File

@ -22,4 +22,6 @@ type InputSourceContext interface {
IntSlice(name string) ([]int, error) IntSlice(name string) ([]int, error)
Generic(name string) (cli.Generic, error) Generic(name string) (cli.Generic, error)
Bool(name string) (bool, error) Bool(name string) (bool, error)
isSet(name string) bool
} }

View File

@ -16,9 +16,9 @@ import (
// variables from a file containing JSON data with the file name defined // variables from a file containing JSON data with the file name defined
// by the given flag. // by the given flag.
func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) { func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) { return func(cCtx *cli.Context) (InputSourceContext, error) {
if context.IsSet(flag) { if cCtx.IsSet(flag) {
return NewJSONSourceFromFile(context.String(flag)) return NewJSONSourceFromFile(cCtx.String(flag))
} }
return defaultInputSource() return defaultInputSource()
@ -184,6 +184,11 @@ func (x *jsonSource) Bool(name string) (bool, error) {
return v, nil return v, nil
} }
func (x *jsonSource) isSet(name string) bool {
_, err := x.getValue(name)
return err == nil
}
func (x *jsonSource) getValue(key string) (interface{}, error) { func (x *jsonSource) getValue(key string) (interface{}, error) {
return jsonGetValue(key, x.deserialized) return jsonGetValue(key, x.deserialized)
} }

View File

@ -32,11 +32,18 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool
if !ok { if !ok {
return nil, false return nil, false
} }
ctype, ok := child.(map[interface{}]interface{})
if !ok { switch child := child.(type) {
case map[string]interface{}:
node = make(map[interface{}]interface{}, len(child))
for k, v := range child {
node[k] = v
}
case map[interface{}]interface{}:
node = child
default:
return nil, false return nil, false
} }
node = ctype
} }
if val, ok := node[sections[len(sections)-1]]; ok { if val, ok := node[sections[len(sections)-1]]; ok {
return val, true return val, true
@ -244,6 +251,15 @@ func (fsm *MapInputSource) Bool(name string) (bool, error) {
return false, nil return false, nil
} }
func (fsm *MapInputSource) isSet(name string) bool {
if _, exists := fsm.valueMap[name]; exists {
return exists
}
_, exists := nestedVal(name, fsm.valueMap)
return exists
}
func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error { func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error {
valueType := reflect.TypeOf(value) valueType := reflect.TypeOf(value)
valueTypeName := "" valueTypeName := ""

View File

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

View File

@ -11,7 +11,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
type yamlSourceContext struct { type yamlSourceContext struct {
@ -31,10 +31,10 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
} }
// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context. // NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) { func NewYamlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) { return func(cCtx *cli.Context) (InputSourceContext, error) {
if context.IsSet(flagFileName) { if cCtx.IsSet(flagFileName) {
filePath := context.String(flagFileName) filePath := cCtx.String(flagFileName)
return NewYamlSourceFromFile(filePath) return NewYamlSourceFromFile(filePath)
} }

View File

@ -0,0 +1,87 @@
package altsrc_test
import (
"fmt"
"log"
"os"
"time"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
func ExampleApp_Run_yamlFileLoaderDuration() {
execServe := func(c *cli.Context) error {
keepaliveInterval := c.Duration("keepalive-interval")
fmt.Printf("keepalive %s\n", keepaliveInterval)
return nil
}
fileExists := func(filename string) bool {
stat, _ := os.Stat(filename)
return stat != nil
}
// initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
initConfigFileInputSource := func(configFlag string, flags []cli.Flag) cli.BeforeFunc {
return func(context *cli.Context) error {
configFile := context.String(configFlag)
if context.IsSet(configFlag) && !fileExists(configFile) {
return fmt.Errorf("config file %s does not exist", configFile)
} else if !context.IsSet(configFlag) && !fileExists(configFile) {
return nil
}
inputSource, err := altsrc.NewYamlSourceFromFile(configFile)
if err != nil {
return err
}
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
}
}
flagsServe := []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
EnvVars: []string{"CONFIG_FILE"},
Value: "../testdata/empty.yml",
DefaultText: "../testdata/empty.yml",
Usage: "config file",
},
altsrc.NewDurationFlag(
&cli.DurationFlag{
Name: "keepalive-interval",
Aliases: []string{"k"},
EnvVars: []string{"KEEPALIVE_INTERVAL"},
Value: 45 * time.Second,
Usage: "interval of keepalive messages",
},
),
}
cmdServe := &cli.Command{
Name: "serve",
Usage: "Run the server",
UsageText: "serve [OPTIONS..]",
Action: execServe,
Flags: flagsServe,
Before: initConfigFileInputSource("config", flagsServe),
}
c := &cli.App{
Name: "cmd",
HideVersion: true,
UseShortOptionHandling: true,
Commands: []*cli.Command{
cmdServe,
},
}
if err := c.Run([]string{"cmd", "serve", "--config", "../testdata/empty.yml"}); err != nil {
log.Fatal(err)
}
// Output:
// keepalive 45s
}

241
app.go
View File

@ -7,10 +7,13 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"sort" "sort"
"time" "time"
) )
const suggestDidYouMeanTemplate = "Did you mean %q?"
var ( var (
changeLogURL = "https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md" changeLogURL = "https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md"
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
@ -18,6 +21,10 @@ var (
errInvalidActionType = NewExitError("ERROR invalid Action type. "+ errInvalidActionType = NewExitError("ERROR invalid Action type. "+
fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2) fmt.Sprintf("See %s", appActionDeprecationURL), 2)
SuggestFlag SuggestFlagFunc = suggestFlag
SuggestCommand SuggestCommandFunc = suggestCommand
SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate
) )
// App is the main structure of a cli application. It is recommended that // App is the main structure of a cli application. It is recommended that
@ -37,6 +44,9 @@ type App struct {
Version string Version string
// Description of the program // Description of the program
Description string Description string
// DefaultCommand is the (optional) name of a command
// to run if no command names are passed as CLI arguments.
DefaultCommand string
// List of commands to execute // List of commands to execute
Commands []*Command Commands []*Command
// List of flags to parse // List of flags to parse
@ -52,6 +62,8 @@ type App struct {
HideVersion bool HideVersion bool
// categories contains the categorized commands and is populated on app startup // categories contains the categorized commands and is populated on app startup
categories CommandCategories categories CommandCategories
// flagCategories contains the categorized flags and is populated on app startup
flagCategories FlagCategories
// An action to execute when the shell completion flag is set // An action to execute when the shell completion flag is set
BashComplete BashCompleteFunc BashComplete BashCompleteFunc
// An action to execute before any subcommands are run, but after the context is ready // An action to execute before any subcommands are run, but after the context is ready
@ -94,10 +106,16 @@ type App struct {
// single-character bool arguments into one // single-character bool arguments into one
// i.e. foobar -o -v -> foobar -ov // i.e. foobar -o -v -> foobar -ov
UseShortOptionHandling bool UseShortOptionHandling bool
// Enable suggestions for commands and flags
Suggest bool
didSetup bool didSetup bool
} }
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
type SuggestCommandFunc func(commands []*Command, provided string) string
// Tries to find out when this binary was compiled. // Tries to find out when this binary was compiled.
// Returns the current time if it fails to find it. // Returns the current time if it fails to find it.
func compileTime() time.Time { func compileTime() time.Time {
@ -181,6 +199,8 @@ func (a *App) Setup() {
if c.HelpName == "" { if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
} }
c.flagCategories = newFlagCategoriesFromFlags(c.Flags)
newCommands = append(newCommands, c) newCommands = append(newCommands, c)
} }
a.Commands = newCommands a.Commands = newCommands
@ -205,6 +225,13 @@ func (a *App) Setup() {
} }
sort.Sort(a.categories.(*commandCategories)) sort.Sort(a.categories.(*commandCategories))
a.flagCategories = newFlagCategories()
for _, fl := range a.Flags {
if cf, ok := fl.(CategorizableFlag); ok {
a.flagCategories.AddFlag(cf.GetCategory(), cf)
}
}
if a.Metadata == nil { if a.Metadata == nil {
a.Metadata = make(map[string]interface{}) a.Metadata = make(map[string]interface{})
} }
@ -245,48 +272,53 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
err = parseIter(set, a, arguments[1:], shellComplete) err = parseIter(set, a, arguments[1:], shellComplete)
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, &Context{Context: ctx}) cCtx := NewContext(a, set, &Context{Context: ctx})
if nerr != nil { if nerr != nil {
_, _ = fmt.Fprintln(a.Writer, nerr) _, _ = fmt.Fprintln(a.Writer, nerr)
_ = ShowAppHelp(context) _ = ShowAppHelp(cCtx)
return nerr return nerr
} }
context.shellComplete = shellComplete cCtx.shellComplete = shellComplete
if checkCompletions(context) { if checkCompletions(cCtx) {
return nil return nil
} }
if err != nil { if err != nil {
if a.OnUsageError != nil { if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false) err := a.OnUsageError(cCtx, err, false)
a.handleExitCoder(context, err) a.handleExitCoder(cCtx, err)
return err return err
} }
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
_ = ShowAppHelp(context) if a.Suggest {
if suggestion, err := a.suggestFlagFromError(err, ""); err == nil {
fmt.Fprintf(a.Writer, suggestion)
}
}
_ = ShowAppHelp(cCtx)
return err return err
} }
if !a.HideHelp && checkHelp(context) { if !a.HideHelp && checkHelp(cCtx) {
_ = ShowAppHelp(context) _ = ShowAppHelp(cCtx)
return nil return nil
} }
if !a.HideVersion && checkVersion(context) { if !a.HideVersion && checkVersion(cCtx) {
ShowVersion(context) ShowVersion(cCtx)
return nil return nil
} }
cerr := context.checkRequiredFlags(a.Flags) cerr := cCtx.checkRequiredFlags(a.Flags)
if cerr != nil { if cerr != nil {
_ = ShowAppHelp(context) _ = ShowAppHelp(cCtx)
return cerr return cerr
} }
if a.After != nil { if a.After != nil {
defer func() { defer func() {
if afterErr := a.After(context); afterErr != nil { if afterErr := a.After(cCtx); afterErr != nil {
if err != nil { if err != nil {
err = newMultiError(err, afterErr) err = newMultiError(err, afterErr)
} else { } else {
@ -297,34 +329,89 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
} }
if a.Before != nil { if a.Before != nil {
beforeErr := a.Before(context) beforeErr := a.Before(cCtx)
if beforeErr != nil { if beforeErr != nil {
a.handleExitCoder(context, beforeErr) a.handleExitCoder(cCtx, beforeErr)
err = beforeErr err = beforeErr
return err return err
} }
} }
args := context.Args() var c *Command
args := cCtx.Args()
if args.Present() { if args.Present() {
name := args.First() name := args.First()
c := a.Command(name) if a.validCommandName(name) {
if c != nil { c = a.Command(name)
return c.Run(context) } else {
hasDefault := a.DefaultCommand != ""
isFlagName := checkStringSliceIncludes(name, cCtx.FlagNames())
var (
isDefaultSubcommand = false
defaultHasSubcommands = false
)
if hasDefault {
dc := a.Command(a.DefaultCommand)
defaultHasSubcommands = len(dc.Subcommands) > 0
for _, dcSub := range dc.Subcommands {
if checkStringSliceIncludes(name, dcSub.Names()) {
isDefaultSubcommand = true
break
} }
} }
}
if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) {
argsWithDefault := a.argsWithDefaultCommand(args)
if !reflect.DeepEqual(args, argsWithDefault) {
c = a.Command(argsWithDefault.First())
}
}
}
} else if a.DefaultCommand != "" {
c = a.Command(a.DefaultCommand)
}
if c != nil {
return c.Run(cCtx)
}
if a.Action == nil { if a.Action == nil {
a.Action = helpCommand.Action a.Action = helpCommand.Action
} }
// Run default Action // Run default Action
err = a.Action(context) err = a.Action(cCtx)
a.handleExitCoder(context, err) a.handleExitCoder(cCtx, err)
return err return err
} }
func (a *App) suggestFlagFromError(err error, command string) (string, error) {
flag, parseErr := flagFromError(err)
if parseErr != nil {
return "", err
}
flags := a.Flags
if command != "" {
cmd := a.Command(command)
if cmd == nil {
return "", err
}
flags = cmd.Flags
}
suggestion := SuggestFlag(flags, flag, a.HideHelp)
if len(suggestion) == 0 {
return "", err
}
return fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", suggestion), nil
}
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned // RunAndExitOnError calls .Run() and exits non-zero if an error was returned
// //
// Deprecated: instead you should return an error that fulfills cli.ExitCoder // Deprecated: instead you should return an error that fulfills cli.ExitCoder
@ -359,55 +446,60 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete) err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete)
nerr := normalizeFlags(a.Flags, set) nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx) cCtx := NewContext(a, set, ctx)
if nerr != nil { if nerr != nil {
_, _ = fmt.Fprintln(a.Writer, nerr) _, _ = fmt.Fprintln(a.Writer, nerr)
_, _ = fmt.Fprintln(a.Writer) _, _ = fmt.Fprintln(a.Writer)
if len(a.Commands) > 0 { if len(a.Commands) > 0 {
_ = ShowSubcommandHelp(context) _ = ShowSubcommandHelp(cCtx)
} else { } else {
_ = ShowCommandHelp(ctx, context.Args().First()) _ = ShowCommandHelp(ctx, cCtx.Args().First())
} }
return nerr return nerr
} }
if checkCompletions(context) { if checkCompletions(cCtx) {
return nil return nil
} }
if err != nil { if err != nil {
if a.OnUsageError != nil { if a.OnUsageError != nil {
err = a.OnUsageError(context, err, true) err = a.OnUsageError(cCtx, err, true)
a.handleExitCoder(context, err) a.handleExitCoder(cCtx, err)
return err return err
} }
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
_ = ShowSubcommandHelp(context) if a.Suggest {
if suggestion, err := a.suggestFlagFromError(err, cCtx.Command.Name); err == nil {
fmt.Fprintf(a.Writer, suggestion)
}
}
_ = ShowSubcommandHelp(cCtx)
return err return err
} }
if len(a.Commands) > 0 { if len(a.Commands) > 0 {
if checkSubcommandHelp(context) { if checkSubcommandHelp(cCtx) {
return nil return nil
} }
} else { } else {
if checkCommandHelp(ctx, context.Args().First()) { if checkCommandHelp(ctx, cCtx.Args().First()) {
return nil return nil
} }
} }
cerr := context.checkRequiredFlags(a.Flags) cerr := cCtx.checkRequiredFlags(a.Flags)
if cerr != nil { if cerr != nil {
_ = ShowSubcommandHelp(context) _ = ShowSubcommandHelp(cCtx)
return cerr return cerr
} }
if a.After != nil { if a.After != nil {
defer func() { defer func() {
afterErr := a.After(context) afterErr := a.After(cCtx)
if afterErr != nil { if afterErr != nil {
a.handleExitCoder(context, err) a.handleExitCoder(cCtx, err)
if err != nil { if err != nil {
err = newMultiError(err, afterErr) err = newMultiError(err, afterErr)
} else { } else {
@ -418,27 +510,27 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
} }
if a.Before != nil { if a.Before != nil {
beforeErr := a.Before(context) beforeErr := a.Before(cCtx)
if beforeErr != nil { if beforeErr != nil {
a.handleExitCoder(context, beforeErr) a.handleExitCoder(cCtx, beforeErr)
err = beforeErr err = beforeErr
return err return err
} }
} }
args := context.Args() args := cCtx.Args()
if args.Present() { if args.Present() {
name := args.First() name := args.First()
c := a.Command(name) c := a.Command(name)
if c != nil { if c != nil {
return c.Run(context) return c.Run(cCtx)
} }
} }
// Run default Action // Run default Action
err = a.Action(context) err = a.Action(cCtx)
a.handleExitCoder(context, err) a.handleExitCoder(cCtx, err)
return err return err
} }
@ -481,6 +573,14 @@ func (a *App) VisibleCommands() []*Command {
return ret return ret
} }
// VisibleFlagCategories returns a slice containing all the categories with the flags they contain
func (a *App) VisibleFlagCategories() []VisibleFlagCategory {
if a.flagCategories == nil {
return []VisibleFlagCategory{}
}
return a.flagCategories.VisibleCategories()
}
// VisibleFlags returns a slice of the Flags with Hidden=false // VisibleFlags returns a slice of the Flags with Hidden=false
func (a *App) VisibleFlags() []Flag { func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags) return visibleFlags(a.Flags)
@ -498,14 +598,49 @@ func (a *App) appendCommand(c *Command) {
} }
} }
func (a *App) handleExitCoder(context *Context, err error) { func (a *App) handleExitCoder(cCtx *Context, err error) {
if a.ExitErrHandler != nil { if a.ExitErrHandler != nil {
a.ExitErrHandler(context, err) a.ExitErrHandler(cCtx, err)
} else { } else {
HandleExitCoder(err) HandleExitCoder(err)
} }
} }
func (a *App) commandNames() []string {
var cmdNames []string
for _, cmd := range a.Commands {
cmdNames = append(cmdNames, cmd.Names()...)
}
return cmdNames
}
func (a *App) validCommandName(checkCmdName string) bool {
valid := false
allCommandNames := a.commandNames()
for _, cmdName := range allCommandNames {
if checkCmdName == cmdName {
valid = true
break
}
}
return valid
}
func (a *App) argsWithDefaultCommand(oldArgs Args) Args {
if a.DefaultCommand != "" {
rawArgs := append([]string{a.DefaultCommand}, oldArgs.Slice()...)
newArgs := args(rawArgs)
return &newArgs
}
return oldArgs
}
// Author represents someone who has contributed to a cli project. // Author represents someone who has contributed to a cli project.
type Author struct { type Author struct {
Name string // The Authors name Name string // The Authors name
@ -525,16 +660,28 @@ func (a *Author) String() string {
// HandleAction attempts to figure out which Action signature was used. If // HandleAction attempts to figure out which Action signature was used. If
// it's an ActionFunc or a func with the legacy signature for Action, the func // it's an ActionFunc or a func with the legacy signature for Action, the func
// is run! // is run!
func HandleAction(action interface{}, context *Context) (err error) { func HandleAction(action interface{}, cCtx *Context) (err error) {
switch a := action.(type) { switch a := action.(type) {
case ActionFunc: case ActionFunc:
return a(context) return a(cCtx)
case func(*Context) error: case func(*Context) error:
return a(context) return a(cCtx)
case func(*Context): // deprecated function signature case func(*Context): // deprecated function signature
a(context) a(cCtx)
return nil return nil
} }
return errInvalidActionType return errInvalidActionType
} }
func checkStringSliceIncludes(want string, sSlice []string) bool {
found := false
for _, s := range sSlice {
if want == s {
found = true
break
}
}
return found
}

View File

@ -142,8 +142,8 @@ func ExampleApp_Run_appHelp() {
// help, h Shows a list of commands or help for one command // help, h Shows a list of commands or help for one command
// //
// GLOBAL OPTIONS: // GLOBAL OPTIONS:
// --name value a name to say (default: "bob")
// --help, -h show help (default: false) // --help, -h show help (default: false)
// --name value a name to say (default: "bob")
// --version, -v print the version (default: false) // --version, -v print the version (default: false)
} }
@ -228,6 +228,7 @@ func ExampleApp_Run_subcommandNoAction() {
} }
func ExampleApp_Run_bashComplete_withShortFlag() { func ExampleApp_Run_bashComplete_withShortFlag() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "-", "--generate-bash-completion"} os.Args = []string{"greet", "-", "--generate-bash-completion"}
app := NewApp() app := NewApp()
@ -255,6 +256,7 @@ func ExampleApp_Run_bashComplete_withShortFlag() {
} }
func ExampleApp_Run_bashComplete_withLongFlag() { func ExampleApp_Run_bashComplete_withLongFlag() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "--s", "--generate-bash-completion"} os.Args = []string{"greet", "--s", "--generate-bash-completion"}
app := NewApp() app := NewApp()
@ -283,6 +285,7 @@ func ExampleApp_Run_bashComplete_withLongFlag() {
// --similar-flag // --similar-flag
} }
func ExampleApp_Run_bashComplete_withMultipleLongFlag() { func ExampleApp_Run_bashComplete_withMultipleLongFlag() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "--st", "--generate-bash-completion"} os.Args = []string{"greet", "--st", "--generate-bash-completion"}
app := NewApp() app := NewApp()
@ -315,7 +318,7 @@ func ExampleApp_Run_bashComplete_withMultipleLongFlag() {
} }
func ExampleApp_Run_bashComplete() { func ExampleApp_Run_bashComplete() {
// set args for examples sake os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "--generate-bash-completion"} os.Args = []string{"greet", "--generate-bash-completion"}
app := &App{ app := &App{
@ -355,7 +358,7 @@ func ExampleApp_Run_bashComplete() {
func ExampleApp_Run_zshComplete() { func ExampleApp_Run_zshComplete() {
// set args for examples sake // set args for examples sake
os.Args = []string{"greet", "--generate-bash-completion"} os.Args = []string{"greet", "--generate-bash-completion"}
_ = os.Setenv("_CLI_ZSH_AUTOCOMPLETE_HACK", "1") _ = os.Setenv("SHELL", "/usr/bin/zsh")
app := NewApp() app := NewApp()
app.Name = "greet" app.Name = "greet"
@ -390,6 +393,40 @@ func ExampleApp_Run_zshComplete() {
// h:Shows a list of commands or help for one command // h:Shows a list of commands or help for one command
} }
func ExampleApp_Run_sliceValues() {
// set args for examples sake
os.Args = []string{"multi_values",
"--stringSclice", "parsed1,parsed2", "--stringSclice", "parsed3,parsed4",
"--float64Sclice", "13.3,14.4", "--float64Sclice", "15.5,16.6",
"--int64Sclice", "13,14", "--int64Sclice", "15,16",
"--intSclice", "13,14", "--intSclice", "15,16",
}
app := NewApp()
app.Name = "multi_values"
app.Flags = []Flag{
&StringSliceFlag{Name: "stringSclice"},
&Float64SliceFlag{Name: "float64Sclice"},
&Int64SliceFlag{Name: "int64Sclice"},
&IntSliceFlag{Name: "intSclice"},
}
app.Action = func(ctx *Context) error {
for i, v := range ctx.FlagNames() {
fmt.Printf("%d-%s %#v\n", i, v, ctx.Value(v))
}
err := ctx.Err()
fmt.Println("error:", err)
return err
}
_ = app.Run(os.Args)
// Output:
// 0-float64Sclice cli.Float64Slice{slice:[]float64{13.3, 14.4, 15.5, 16.6}, hasBeenSet:true}
// 1-int64Sclice cli.Int64Slice{slice:[]int64{13, 14, 15, 16}, hasBeenSet:true}
// 2-intSclice cli.IntSlice{slice:[]int{13, 14, 15, 16}, hasBeenSet:true}
// 3-stringSclice cli.StringSlice{slice:[]string{"parsed1", "parsed2", "parsed3", "parsed4"}, hasBeenSet:true}
// error: <nil>
}
func TestApp_Run(t *testing.T) { func TestApp_Run(t *testing.T) {
s := "" s := ""
@ -432,6 +469,175 @@ func TestApp_Command(t *testing.T) {
} }
} }
var defaultCommandAppTests = []struct {
cmdName string
defaultCmd string
expected bool
}{
{"foobar", "foobar", true},
{"batbaz", "foobar", true},
{"b", "", true},
{"f", "", true},
{"", "foobar", true},
{"", "", true},
{" ", "", false},
{"bat", "batbaz", false},
{"nothing", "batbaz", false},
{"nothing", "", false},
}
func TestApp_RunDefaultCommand(t *testing.T) {
for _, test := range defaultCommandAppTests {
testTitle := fmt.Sprintf("command=%[1]s-default=%[2]s", test.cmdName, test.defaultCmd)
t.Run(testTitle, func(t *testing.T) {
app := &App{
DefaultCommand: test.defaultCmd,
Commands: []*Command{
{Name: "foobar", Aliases: []string{"f"}},
{Name: "batbaz", Aliases: []string{"b"}},
},
}
err := app.Run([]string{"c", test.cmdName})
expect(t, err == nil, test.expected)
})
}
}
var defaultCommandSubCmdAppTests = []struct {
cmdName string
subCmd string
defaultCmd string
expected bool
}{
{"foobar", "", "foobar", true},
{"foobar", "carly", "foobar", true},
{"batbaz", "", "foobar", true},
{"b", "", "", true},
{"f", "", "", true},
{"", "", "foobar", true},
{"", "", "", true},
{"", "jimbob", "foobar", true},
{"", "j", "foobar", true},
{"", "carly", "foobar", true},
{"", "jimmers", "foobar", true},
{"", "jimmers", "", true},
{" ", "jimmers", "foobar", false},
{"", "", "", true},
{" ", "", "", false},
{" ", "j", "", false},
{"bat", "", "batbaz", false},
{"nothing", "", "batbaz", false},
{"nothing", "", "", false},
{"nothing", "j", "batbaz", false},
{"nothing", "carly", "", false},
}
func TestApp_RunDefaultCommandWithSubCommand(t *testing.T) {
for _, test := range defaultCommandSubCmdAppTests {
testTitle := fmt.Sprintf("command=%[1]s-subcmd=%[2]s-default=%[3]s", test.cmdName, test.subCmd, test.defaultCmd)
t.Run(testTitle, func(t *testing.T) {
app := &App{
DefaultCommand: test.defaultCmd,
Commands: []*Command{
{
Name: "foobar",
Aliases: []string{"f"},
Subcommands: []*Command{
{Name: "jimbob", Aliases: []string{"j"}},
{Name: "carly"},
},
},
{Name: "batbaz", Aliases: []string{"b"}},
},
}
err := app.Run([]string{"c", test.cmdName, test.subCmd})
expect(t, err == nil, test.expected)
})
}
}
var defaultCommandFlagAppTests = []struct {
cmdName string
flag string
defaultCmd string
expected bool
}{
{"foobar", "", "foobar", true},
{"foobar", "-c derp", "foobar", true},
{"batbaz", "", "foobar", true},
{"b", "", "", true},
{"f", "", "", true},
{"", "", "foobar", true},
{"", "", "", true},
{"", "-j", "foobar", true},
{"", "-j", "foobar", true},
{"", "-c derp", "foobar", true},
{"", "--carly=derp", "foobar", true},
{"", "-j", "foobar", true},
{"", "-j", "", true},
{" ", "-j", "foobar", false},
{"", "", "", true},
{" ", "", "", false},
{" ", "-j", "", false},
{"bat", "", "batbaz", false},
{"nothing", "", "batbaz", false},
{"nothing", "", "", false},
{"nothing", "--jimbob", "batbaz", false},
{"nothing", "--carly", "", false},
}
func TestApp_RunDefaultCommandWithFlags(t *testing.T) {
for _, test := range defaultCommandFlagAppTests {
testTitle := fmt.Sprintf("command=%[1]s-flag=%[2]s-default=%[3]s", test.cmdName, test.flag, test.defaultCmd)
t.Run(testTitle, func(t *testing.T) {
app := &App{
DefaultCommand: test.defaultCmd,
Flags: []Flag{
&StringFlag{
Name: "carly",
Aliases: []string{"c"},
Required: false,
},
&BoolFlag{
Name: "jimbob",
Aliases: []string{"j"},
Required: false,
Value: true,
},
},
Commands: []*Command{
{
Name: "foobar",
Aliases: []string{"f"},
},
{Name: "batbaz", Aliases: []string{"b"}},
},
}
appArgs := []string{"c"}
if test.flag != "" {
flags := strings.Split(test.flag, " ")
if len(flags) > 1 {
appArgs = append(appArgs, flags...)
}
flags = strings.Split(test.flag, "=")
if len(flags) > 1 {
appArgs = append(appArgs, flags...)
}
}
appArgs = append(appArgs, test.cmdName)
err := app.Run(appArgs)
expect(t, err == nil, test.expected)
})
}
}
func TestApp_Setup_defaultsReader(t *testing.T) { func TestApp_Setup_defaultsReader(t *testing.T) {
app := &App{} app := &App{}
app.Setup() app.Setup()
@ -445,14 +651,14 @@ func TestApp_Setup_defaultsWriter(t *testing.T) {
} }
func TestApp_RunAsSubcommandParseFlags(t *testing.T) { func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
var context *Context var cCtx *Context
a := &App{ a := &App{
Commands: []*Command{ Commands: []*Command{
{ {
Name: "foo", Name: "foo",
Action: func(c *Context) error { Action: func(c *Context) error {
context = c cCtx = c
return nil return nil
}, },
Flags: []Flag{ Flags: []Flag{
@ -468,8 +674,8 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) {
} }
_ = a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) _ = a.Run([]string{"", "foo", "--lang", "spanish", "abcd"})
expect(t, context.Args().Get(0), "abcd") expect(t, cCtx.Args().Get(0), "abcd")
expect(t, context.String("lang"), "spanish") expect(t, cCtx.String("lang"), "spanish")
} }
func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) { func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) {
@ -1890,6 +2096,14 @@ func TestApp_VisibleCategories(t *testing.T) {
expect(t, []CommandCategory{}, app.VisibleCategories()) expect(t, []CommandCategory{}, app.VisibleCategories())
} }
func TestApp_VisibleFlagCategories(t *testing.T) {
app := &App{}
vfc := app.VisibleFlagCategories()
if len(vfc) != 0 {
t.Errorf("unexpected visible flag categories %+v", vfc)
}
}
func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) {
app := &App{ app := &App{
Action: func(c *Context) error { return nil }, Action: func(c *Context) error { return nil },

View File

@ -1,14 +1,13 @@
#compdef $PROG #compdef $PROG
_cli_zsh_autocomplete() { _cli_zsh_autocomplete() {
local -a opts local -a opts
local cur local cur
cur=${words[-1]} cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi fi
if [[ "${opts[1]}" != "" ]]; then if [[ "${opts[1]}" != "" ]]; then
@ -16,8 +15,6 @@ _cli_zsh_autocomplete() {
else else
_files _files
fi fi
return
} }
compdef _cli_zsh_autocomplete $PROG compdef _cli_zsh_autocomplete $PROG

View File

@ -1,10 +1,12 @@
package cli package cli
import "sort"
// CommandCategories interface allows for category manipulation // CommandCategories interface allows for category manipulation
type CommandCategories interface { type CommandCategories interface {
// AddCommand adds a command to a category, creating a new category if necessary. // AddCommand adds a command to a category, creating a new category if necessary.
AddCommand(category string, command *Command) AddCommand(category string, command *Command)
// categories returns a copy of the category slice // Categories returns a slice of categories sorted by name
Categories() []CommandCategory Categories() []CommandCategory
} }
@ -77,3 +79,93 @@ func (c *commandCategory) VisibleCommands() []*Command {
} }
return ret return ret
} }
// FlagCategories interface allows for category manipulation
type FlagCategories interface {
// AddFlags adds a flag to a category, creating a new category if necessary.
AddFlag(category string, fl Flag)
// VisibleCategories returns a slice of visible flag categories sorted by name
VisibleCategories() []VisibleFlagCategory
}
type defaultFlagCategories struct {
m map[string]*defaultVisibleFlagCategory
}
func newFlagCategories() FlagCategories {
return &defaultFlagCategories{
m: map[string]*defaultVisibleFlagCategory{},
}
}
func newFlagCategoriesFromFlags(fs []Flag) FlagCategories {
fc := newFlagCategories()
for _, fl := range fs {
if cf, ok := fl.(CategorizableFlag); ok {
fc.AddFlag(cf.GetCategory(), cf)
}
}
return fc
}
func (f *defaultFlagCategories) AddFlag(category string, fl Flag) {
if _, ok := f.m[category]; !ok {
f.m[category] = &defaultVisibleFlagCategory{name: category, m: map[string]Flag{}}
}
f.m[category].m[fl.String()] = fl
}
func (f *defaultFlagCategories) VisibleCategories() []VisibleFlagCategory {
catNames := []string{}
for name := range f.m {
catNames = append(catNames, name)
}
sort.Strings(catNames)
ret := make([]VisibleFlagCategory, len(catNames))
for i, name := range catNames {
ret[i] = f.m[name]
}
return ret
}
// VisibleFlagCategory is a category containing flags.
type VisibleFlagCategory interface {
// Name returns the category name string
Name() string
// Flags returns a slice of VisibleFlag sorted by name
Flags() []VisibleFlag
}
type defaultVisibleFlagCategory struct {
name string
m map[string]Flag
}
func (fc *defaultVisibleFlagCategory) Name() string {
return fc.name
}
func (fc *defaultVisibleFlagCategory) Flags() []VisibleFlag {
vfNames := []string{}
for flName, fl := range fc.m {
if vf, ok := fl.(VisibleFlag); ok {
if vf.IsVisible() {
vfNames = append(vfNames, flName)
}
}
}
sort.Strings(vfNames)
ret := make([]VisibleFlag, len(vfNames))
for i, flName := range vfNames {
ret[i] = fc.m[flName].(VisibleFlag)
}
return ret
}

2
cli.go
View File

@ -20,4 +20,4 @@
// } // }
package cli package cli
//go:generate go run flag-gen/main.go flag-gen/assets_vfsdata.go //go:generate go run internal/genflags/cmd/genflags/main.go

View File

@ -39,6 +39,7 @@ type Command struct {
Subcommands []*Command Subcommands []*Command
// List of flags to parse // List of flags to parse
Flags []Flag Flags []Flag
flagCategories FlagCategories
// Treat all flags as normal arguments if true // Treat all flags as normal arguments if true
SkipFlagParsing bool SkipFlagParsing bool
// Boolean to hide built-in help command and help flag // Boolean to hide built-in help command and help flag
@ -105,39 +106,44 @@ func (c *Command) Run(ctx *Context) (err error) {
set, err := c.parseFlags(ctx.Args(), ctx.shellComplete) set, err := c.parseFlags(ctx.Args(), ctx.shellComplete)
context := NewContext(ctx.App, set, ctx) cCtx := NewContext(ctx.App, set, ctx)
context.Command = c cCtx.Command = c
if checkCommandCompletions(context, c.Name) { if checkCommandCompletions(cCtx, c.Name) {
return nil return nil
} }
if err != nil { if err != nil {
if c.OnUsageError != nil { if c.OnUsageError != nil {
err = c.OnUsageError(context, err, false) err = c.OnUsageError(cCtx, err, false)
context.App.handleExitCoder(context, err) cCtx.App.handleExitCoder(cCtx, err)
return err return err
} }
_, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) _, _ = fmt.Fprintln(cCtx.App.Writer, "Incorrect Usage:", err.Error())
_, _ = fmt.Fprintln(context.App.Writer) _, _ = fmt.Fprintln(cCtx.App.Writer)
_ = ShowCommandHelp(context, c.Name) if ctx.App.Suggest {
if suggestion, err := ctx.App.suggestFlagFromError(err, c.Name); err == nil {
fmt.Fprintf(cCtx.App.Writer, suggestion)
}
}
_ = ShowCommandHelp(cCtx, c.Name)
return err return err
} }
if checkCommandHelp(context, c.Name) { if checkCommandHelp(cCtx, c.Name) {
return nil return nil
} }
cerr := context.checkRequiredFlags(c.Flags) cerr := cCtx.checkRequiredFlags(c.Flags)
if cerr != nil { if cerr != nil {
_ = ShowCommandHelp(context, c.Name) _ = ShowCommandHelp(cCtx, c.Name)
return cerr return cerr
} }
if c.After != nil { if c.After != nil {
defer func() { defer func() {
afterErr := c.After(context) afterErr := c.After(cCtx)
if afterErr != nil { if afterErr != nil {
context.App.handleExitCoder(context, err) cCtx.App.handleExitCoder(cCtx, err)
if err != nil { if err != nil {
err = newMultiError(err, afterErr) err = newMultiError(err, afterErr)
} else { } else {
@ -148,9 +154,9 @@ func (c *Command) Run(ctx *Context) (err error) {
} }
if c.Before != nil { if c.Before != nil {
err = c.Before(context) err = c.Before(cCtx)
if err != nil { if err != nil {
context.App.handleExitCoder(context, err) cCtx.App.handleExitCoder(cCtx, err)
return err return err
} }
} }
@ -159,11 +165,11 @@ func (c *Command) Run(ctx *Context) (err error) {
c.Action = helpSubcommand.Action c.Action = helpSubcommand.Action
} }
context.Command = c cCtx.Command = c
err = c.Action(context) err = c.Action(cCtx)
if err != nil { if err != nil {
context.App.handleExitCoder(context, err) cCtx.App.handleExitCoder(cCtx, err)
} }
return err return err
} }
@ -249,6 +255,7 @@ func (c *Command) startApp(ctx *Context) error {
app.ErrWriter = ctx.App.ErrWriter app.ErrWriter = ctx.App.ErrWriter
app.ExitErrHandler = ctx.App.ExitErrHandler app.ExitErrHandler = ctx.App.ExitErrHandler
app.UseShortOptionHandling = ctx.App.UseShortOptionHandling app.UseShortOptionHandling = ctx.App.UseShortOptionHandling
app.Suggest = ctx.App.Suggest
app.categories = newCommandCategories() app.categories = newCommandCategories()
for _, command := range c.Subcommands { for _, command := range c.Subcommands {
@ -280,6 +287,14 @@ func (c *Command) startApp(ctx *Context) error {
return app.RunAsSubcommand(ctx) return app.RunAsSubcommand(ctx)
} }
// VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain
func (c *Command) VisibleFlagCategories() []VisibleFlagCategory {
if c.flagCategories == nil {
return []VisibleFlagCategory{}
}
return c.flagCategories.VisibleCategories()
}
// VisibleFlags returns a slice of the Flags with Hidden=false // VisibleFlags returns a slice of the Flags with Hidden=false
func (c *Command) VisibleFlags() []Flag { func (c *Command) VisibleFlags() []Flag {
return visibleFlags(c.Flags) return visibleFlags(c.Flags)

View File

@ -30,7 +30,7 @@ func TestCommandFlagParsing(t *testing.T) {
set := flag.NewFlagSet("test", 0) set := flag.NewFlagSet("test", 0)
_ = set.Parse(c.testArgs) _ = set.Parse(c.testArgs)
context := NewContext(app, set, nil) cCtx := NewContext(app, set, nil)
command := Command{ command := Command{
Name: "test-cmd", Name: "test-cmd",
@ -41,10 +41,10 @@ func TestCommandFlagParsing(t *testing.T) {
SkipFlagParsing: c.skipFlagParsing, SkipFlagParsing: c.skipFlagParsing,
} }
err := command.Run(context) err := command.Run(cCtx)
expect(t, err, c.expectedErr) expect(t, err, c.expectedErr)
expect(t, context.Args().Slice(), c.testArgs) expect(t, cCtx.Args().Slice(), c.testArgs)
} }
} }

View File

@ -40,18 +40,18 @@ func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
} }
// NumFlags returns the number of flags set // NumFlags returns the number of flags set
func (c *Context) NumFlags() int { func (cCtx *Context) NumFlags() int {
return c.flagSet.NFlag() return cCtx.flagSet.NFlag()
} }
// Set sets a context flag to a value. // Set sets a context flag to a value.
func (c *Context) Set(name, value string) error { func (cCtx *Context) Set(name, value string) error {
return c.flagSet.Set(name, value) return cCtx.flagSet.Set(name, value)
} }
// IsSet determines if the flag was actually set // IsSet determines if the flag was actually set
func (c *Context) IsSet(name string) bool { func (cCtx *Context) IsSet(name string) bool {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
isSet := false isSet := false
fs.Visit(func(f *flag.Flag) { fs.Visit(func(f *flag.Flag) {
if f.Name == name { if f.Name == name {
@ -62,7 +62,7 @@ func (c *Context) IsSet(name string) bool {
return true return true
} }
f := c.lookupFlag(name) f := cCtx.lookupFlag(name)
if f == nil { if f == nil {
return false return false
} }
@ -74,28 +74,28 @@ func (c *Context) IsSet(name string) bool {
} }
// LocalFlagNames returns a slice of flag names used in this context. // LocalFlagNames returns a slice of flag names used in this context.
func (c *Context) LocalFlagNames() []string { func (cCtx *Context) LocalFlagNames() []string {
var names []string var names []string
c.flagSet.Visit(makeFlagNameVisitor(&names)) cCtx.flagSet.Visit(makeFlagNameVisitor(&names))
return names return names
} }
// FlagNames returns a slice of flag names used by the this context and all of // FlagNames returns a slice of flag names used by the this context and all of
// its parent contexts. // its parent contexts.
func (c *Context) FlagNames() []string { func (cCtx *Context) FlagNames() []string {
var names []string var names []string
for _, ctx := range c.Lineage() { for _, pCtx := range cCtx.Lineage() {
ctx.flagSet.Visit(makeFlagNameVisitor(&names)) pCtx.flagSet.Visit(makeFlagNameVisitor(&names))
} }
return names return names
} }
// Lineage returns *this* context and all of its ancestor contexts in order from // Lineage returns *this* context and all of its ancestor contexts in order from
// child to parent // child to parent
func (c *Context) Lineage() []*Context { func (cCtx *Context) Lineage() []*Context {
var lineage []*Context var lineage []*Context
for cur := c; cur != nil; cur = cur.parentContext { for cur := cCtx; cur != nil; cur = cur.parentContext {
lineage = append(lineage, cur) lineage = append(lineage, cur)
} }
@ -103,26 +103,26 @@ func (c *Context) Lineage() []*Context {
} }
// Value returns the value of the flag corresponding to `name` // Value returns the value of the flag corresponding to `name`
func (c *Context) Value(name string) interface{} { func (cCtx *Context) Value(name string) interface{} {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return fs.Lookup(name).Value.(flag.Getter).Get() return fs.Lookup(name).Value.(flag.Getter).Get()
} }
return nil return nil
} }
// Args returns the command line arguments associated with the context. // Args returns the command line arguments associated with the context.
func (c *Context) Args() Args { func (cCtx *Context) Args() Args {
ret := args(c.flagSet.Args()) ret := args(cCtx.flagSet.Args())
return &ret return &ret
} }
// NArg returns the number of the command line arguments. // NArg returns the number of the command line arguments.
func (c *Context) NArg() int { func (cCtx *Context) NArg() int {
return c.Args().Len() return cCtx.Args().Len()
} }
func (ctx *Context) lookupFlag(name string) Flag { func (cCtx *Context) lookupFlag(name string) Flag {
for _, c := range ctx.Lineage() { for _, c := range cCtx.Lineage() {
if c.Command == nil { if c.Command == nil {
continue continue
} }
@ -136,8 +136,8 @@ func (ctx *Context) lookupFlag(name string) Flag {
} }
} }
if ctx.App != nil { if cCtx.App != nil {
for _, f := range ctx.App.Flags { for _, f := range cCtx.App.Flags {
for _, n := range f.Names() { for _, n := range f.Names() {
if n == name { if n == name {
return f return f
@ -149,8 +149,8 @@ func (ctx *Context) lookupFlag(name string) Flag {
return nil return nil
} }
func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet { func (cCtx *Context) lookupFlagSet(name string) *flag.FlagSet {
for _, c := range ctx.Lineage() { for _, c := range cCtx.Lineage() {
if c.flagSet == nil { if c.flagSet == nil {
continue continue
} }
@ -162,7 +162,7 @@ func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet {
return nil return nil
} }
func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { func (cCtx *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
var missingFlags []string var missingFlags []string
for _, f := range flags { for _, f := range flags {
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
@ -174,7 +174,7 @@ func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr {
flagName = key flagName = key
} }
if context.IsSet(strings.TrimSpace(key)) { if cCtx.IsSet(strings.TrimSpace(key)) {
flagPresent = true flagPresent = true
} }
} }

View File

@ -1,3 +1,6 @@
//go:build !urfave_cli_no_docs
// +build !urfave_cli_no_docs
package cli package cli
import ( import (
@ -80,14 +83,14 @@ func prepareCommands(commands []*Command, level int) []string {
usageText, usageText,
) )
flags := prepareArgsWithValues(command.Flags) flags := prepareArgsWithValues(command.VisibleFlags())
if len(flags) > 0 { if len(flags) > 0 {
prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n")) prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
} }
coms = append(coms, prepared) coms = append(coms, prepared)
// recursevly iterate subcommands // recursively iterate subcommands
if len(command.Subcommands) > 0 { if len(command.Subcommands) > 0 {
coms = append( coms = append(
coms, coms,

View File

@ -1,3 +1,9 @@
> :warning: This document is no longer being actively maintained. Please see the
> [releases page](https://github.com/urfave/cli/releases) for all release notes
> and related hypermedia for releases `>= 1.22.5`, `>= 2.3.0`.
---
# Change Log # Change Log
**ATTN**: This project uses [semantic versioning](http://semver.org/). **ATTN**: This project uses [semantic versioning](http://semver.org/).

1
docs/CNAME Normal file
View File

@ -0,0 +1 @@
cli.urfave.org

1
docs/CODE_OF_CONDUCT.md Symbolic link
View File

@ -0,0 +1 @@
../CODE_OF_CONDUCT.md

View File

@ -1,18 +1,143 @@
## Contributing ## Contributing
Use @urfave/cli to ping the maintainers. Welcome to the `urfave/cli` contributor docs! This goal of this document is to help those
interested in joining the 200+ humans who have contributed to this project over the years.
Feel free to put up a pull request to fix a bug or maybe add a feature. We will > As a general guiding principle, the current maintainers may be notified via the
give it a code review and make sure that it does not break backwards > @urfave/cli GitHub team.
compatibility. If collaborators agree that it is in line with
the vision of the project, we will work with you to get the code into
a mergeable state and merge it into the main branch.
If you have contributed something significant to the project, we will most All of the current maintainers are *volunteers* who live in various timezones with
likely add you as a collaborator. As a collaborator you are given the ability different scheduling needs, so please understand that your contribution or question may
to merge others pull requests. It is very important that new code does not not get a response for many days.
break existing code, so be careful about what code you do choose to merge.
If you feel like you have contributed to the project but have not yet been added ### semantic versioning adherence
as a collaborator, we probably forgot to add you :sweat_smile:. Please open an
issue! The `urfave/cli` project strives to strictly adhere to [semantic
versioning](https://semver.org/spec/v2.0.0.html). The active development branches and the
milestones and import paths to which they correspond are:
#### `main` branch
<https://github.com/urfave/cli/tree/main>
The majority of active development and issue management is targeting the `main` branch,
which **MUST** *only* receive bug fixes and feature *additions*.
- :arrow_right: [`v2.x`](https://github.com/urfave/cli/milestone/16)
- :arrow_right: `github.com/urfave/cli/v2`
The `main` branch in particular includes tooling to help with keeping the `v2.x` series
backward compatible. More details on this process are in the development workflow section
below.
#### `v1` branch
<https://github.com/urfave/cli/tree/v1>
The `v1` branch **MUST** only receive bug fixes in the `v1.22.x` series. There is no
strict rule regarding bug fixes to the `v2.x` series being backported to the `v1.22.x`
series.
- :arrow_right: [`v1.22.x`](https://github.com/urfave/cli/milestone/11)
- :arrow_right: `github.com/urfave/cli`
#### `v3-dev-main` branch
<https://github.com/urfave/cli/tree/v3-dev-main>
The `v3-dev-branch` **MUST** receive all bug fixes and features added to the `main` branch
and **MAY** receive feature *removals* and other changes that are otherwise
*backward-incompatible* with the `v2.x` series.
- :arrow_right: [`v3.x`](https://github.com/urfave/cli/milestone/5)
- unreleased / unsupported
### development workflow
Most of the tooling around the development workflow strives for effective
[dogfooding](https://en.wikipedia.org/wiki/Eating_your_own_dog_food). There is a top-level
`Makefile` that is maintained strictly for the purpose of easing verification of one's
development environment and any changes one may have introduced:
```sh
make
```
Running the default `make` target (`all`) will ensure all of the critical steps are run to
verify one's changes are harmonious in nature. The same steps are also run during the
[continuous integration
phase](https://github.com/urfave/cli/blob/main/.github/workflows/cli.yml).
In the event that the `v2diff` target exits non-zero, this is a signal that the public API
surface area has changed. If the changes adhere to semantic versioning, meaning they are
*additions* or *bug fixes*, then manually running the approval step will "promote" the
current `go doc` output:
```sh
make v2approve
```
Because the `generate` step includes updating `godoc-current.txt` and
`testdata/godoc-v2.x.txt`, these changes *MUST* be part of any proposed pull request so
that reviewers have an opportunity to also make an informed decision about the "promotion"
step.
#### generated code
A significant portion of the project's source code is generated, with the goal being to
eliminate repetitive maintenance where other type-safe abstraction is impractical or
impossible with Go versions `< 1.18`. In a future where the eldest Go version supported is
`1.18.x`, there will likely be efforts to take advantage of
[generics](https://go.dev/doc/tutorial/generics).
The built-in `go generate` command is used to run the commands specified in
`//go:generate` directives. Each such command runs a file that also supports a command
line help system which may be consulted for further information, e.g.:
```sh
go run internal/genflags/cmd/genflags/main.go --help
```
#### docs output
The documentation in the `docs` directory is automatically built via `mkdocs` into a
static site and published when releases are pushed (see [RELEASING](./RELEASING/)). There
is no strict requirement to build the documentation when developing locally, but the
following `make` targets may be used if desired:
```sh
# install documentation dependencies with `pip`
make docs-deps
```
```sh
# build the static site in `./site`
make docs
```
```sh
# start an mkdocs development server
make serve-docs
```
### pull requests
Please feel free to open a pull request to fix a bug or add a feature. The @urfave/cli
team will review it as soon as possible, giving special attention to maintaining backward
compatibility. If the @urfave/cli team agrees that your contribution is in line with the
vision of the project, they will work with you to get the code into a mergeable state,
merged, and then released.
### granting of commit bit / admin mode
Those with a history of contributing to this project will likely be invited to join the
@urfave/cli team. As a member of the @urfave/cli team, you will have the ability to fully
administer pull requests, issues, and other repository bits.
If you feel that you should be a member of the @urfave/cli team but have not yet been
added, the most likely explanation is that this is an accidental oversight! :sweat_smile:.
Please open an issue!
<!--
vim:tw=90
-->

61
docs/RELEASING.md Normal file
View File

@ -0,0 +1,61 @@
# Releasing urfave/cli
Releasing small batches often is [backed by
research](https://itrevolution.com/accelerate-book/) as part of the
virtuous cycles that keep teams and products healthy.
To that end, the overall goal of the release process is to send
changes out into the world as close to the time the commits were
merged to the `main` branch as possible. In this way, the community
of humans depending on this library are able to make use of the
changes they need **quickly**, which means they shouldn't have to
maintain long-lived forks of the project, which means they can get
back to focusing on the work on which they want to focus. This also
means that the @urfave/cli team should be able to focus on
delivering a steadily improving product with significantly eased
ability to associate bugs and regressions with specific releases.
## Process
- Release versions follow [semantic versioning](https://semver.org/)
- Releases are associated with **signed, annotated git tags**[^1].
- Release notes are **automatically generated**[^2].
In the `main` or `v1` branch, the current version is always
available via:
```sh
git describe --always --dirty --tags
```
**NOTE**: if the version reported contains `-dirty`, this is
indicative of a "dirty" work tree, which is not a great state for
creating a new release tag. Seek help from @urfave/cli teammates.
For example, given a described version of `v2.4.7-3-g68da1cd` and a
diff of `v2.4.7...` that contains only bug fixes, the next version
should be `v2.4.8`:
```sh
git tag -a -s -m 'Release 2.4.8' v2.4.8
git push origin v2.4.8
```
The tag push will trigger a GitHub Actions workflow. The remaining
steps require human intervention through the GitHub web view
although [automated solutions
exist](https://github.com/softprops/action-gh-release) that may be
adopted in the future.
- Open the [the new release page](https://github.com/urfave/cli/releases/new)
- At the top of the form, click on the `Choose a tag` select control and select `v2.4.8`
- In the `Write` tab below, click the `Auto-generate release notes` button
- At the bottom of the form, click the `Publish release` button
- :white_check_mark: you're done!
[^1]: This was not always true. There are many **lightweight git
tags** present in the repository history.
[^2]: This was not always true. The
[`docs/CHANGELOG.md`](./CHANGELOG.md) document used to be
manually maintained.

27
docs/SECURITY.md Normal file
View File

@ -0,0 +1,27 @@
# Security Policy
Hello and thank you for your interest in the `urfave/cli` security
policy! :tada: :lock:
## Supported Versions
| Version | Supported |
| ------------ | ------------------------------------- |
| `>= v2.3.x` | :white_check_mark: |
| `< v2.3` | :x: |
| `>= v1.22.x` | :white_check_mark: :lady_beetle: [^1] |
| `< v1.22` | :x: |
## Reporting a Vulnerability
Please disclose any vulnerabilities by sending an email to:
[urfave-security@googlegroups.com](mailto:urfave-security@googlegroups.com)
You should expect a response within 48 hours and further
communications to be decided via email. The `urfave/cli` maintainer
team comprises volunteers who contribute when possible, so please
have patience :bow:
[^1]: The `v1.22.x` series will receive bug fixes and security
patches only.

73
docs/index.md Normal file
View File

@ -0,0 +1,73 @@
# Welcome to 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/main/graph/badge.svg)](https://codecov.io/gh/urfave/cli)
`urfave/cli` is a simple, fast, and fun package for building command line apps in Go. The
goal is to enable developers to write fast and distributable command line applications in
an expressive way.
These are the guides for each major supported version:
- [`v2`](./v2/)
- [`v1`](./v1/)
In addition to the version-specific guides, these other documents are available:
- [CONTRIBUTING](./CONTRIBUTING/)
- [CODE OF CONDUCT](./CODE_OF_CONDUCT/)
- [RELEASING](./RELEASING/)
## Installation
Using this package requires a working Go environment. [See the install instructions for Go](http://golang.org/doc/install.html).
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
```
$ go get github.com/urfave/cli/v2
```
```go
...
import (
"github.com/urfave/cli/v2" // imports as package "cli"
)
...
```
### Using `v1` releases
```
$ go get github.com/urfave/cli
```
```go
...
import (
"github.com/urfave/cli"
)
...
```
### Build tags
You can use the following build tags:
#### `urfave_cli_no_docs`
When set, this removes `ToMarkdown` and `ToMan` methods, so your application
won't be able to call those. This reduces the resulting binary size by about
300-400 KB (measured using Go 1.18.1 on Linux/amd64), due to fewer dependencies.
### Supported platforms
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. To see our currently supported go versions and platforms, look at
the [github workflow
configuration](https://github.com/urfave/cli/blob/main/.github/workflows/cli.yml).

View File

@ -1,6 +1,4 @@
Migration Guide: v1 to v2 # Migration Guide: v1 to v2
===
v2 has a number of breaking changes but converting is relatively v2 has a number of breaking changes but converting is relatively
straightforward: make the changes documented below then resolve any straightforward: make the changes documented below then resolve any
@ -11,25 +9,6 @@ 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 comment on [Issue 921](https://github.com/urfave/cli/issues/921) or
consider sending a PR to help improve this guide. 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 # Flags before args
In v2 flags must come before args. This is more POSIX-compliant. You In v2 flags must come before args. This is more POSIX-compliant. You

1
docs/v1/index.md Symbolic link
View File

@ -0,0 +1 @@
manual.md

View File

@ -1,35 +1,4 @@
cli v1 manual # v1 guide
===
<!-- toc -->
- [Getting Started](#getting-started)
- [Examples](#examples)
* [Arguments](#arguments)
* [Flags](#flags)
+ [Placeholder Values](#placeholder-values)
+ [Alternate Names](#alternate-names)
+ [Ordering](#ordering)
+ [Values from the Environment](#values-from-the-environment)
+ [Values from files](#values-from-files)
+ [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others)
+ [Precedence](#precedence)
* [Subcommands](#subcommands)
* [Subcommands categories](#subcommands-categories)
* [Exit code](#exit-code)
* [Combining short options](#combining-short-options)
* [Bash Completion](#bash-completion)
+ [Enabling](#enabling)
+ [Distribution](#distribution)
+ [Customization](#customization)
* [Generated Help Text](#generated-help-text)
+ [Customization](#customization-1)
* [Version Flag](#version-flag)
+ [Customization](#customization-2)
+ [Full API Example](#full-api-example)
* [Migrating to V2](#migrating-to-v2)
<!-- tocstop -->
## Getting Started ## Getting Started

1
docs/v2/index.md Symbolic link
View File

@ -0,0 +1 @@
manual.md

File diff suppressed because it is too large Load Diff

View File

@ -1,133 +1,13 @@
//go:build !urfave_cli_no_docs
// +build !urfave_cli_no_docs
package cli package cli
import ( import (
"bytes"
"errors" "errors"
"io/ioutil"
"testing" "testing"
) )
func testApp() *App {
app := newTestApp()
app.Name = "greet"
app.Flags = []Flag{
&StringFlag{
Name: "socket",
Aliases: []string{"s"},
Usage: "some 'usage' text",
Value: "value",
TakesFile: true,
},
&StringFlag{Name: "flag", Aliases: []string{"fl", "f"}},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
&BoolFlag{
Name: "hidden-flag",
Hidden: true,
},
}
app.Commands = []*Command{{
Aliases: []string{"c"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "config",
Usage: "another usage test",
Subcommands: []*Command{{
Aliases: []string{"s", "ss"},
Flags: []Flag{
&StringFlag{Name: "sub-flag", Aliases: []string{"sub-fl", "s"}},
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-config",
Usage: "another usage test",
}},
}, {
Aliases: []string{"i", "in"},
Name: "info",
Usage: "retrieve generic information",
}, {
Name: "some-command",
}, {
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"},
{Name: "Oliver Allen", Email: "oliver@toyshop.com"},
}
return app
}
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, got, string(data))
}
func TestToMarkdownFull(t *testing.T) { func TestToMarkdownFull(t *testing.T) {
// Given // Given
app := testApp() app := testApp()

View File

@ -95,7 +95,7 @@ func (a *App) prepareFishCommands(commands []*Command, allCommands *[]string, pr
completions = append(completions, completion.String()) completions = append(completions, completion.String())
completions = append( completions = append(
completions, completions,
a.prepareFishFlags(command.Flags, command.Names())..., a.prepareFishFlags(command.VisibleFlags(), command.Names())...,
) )
// recursevly iterate subcommands // recursevly iterate subcommands

View File

@ -1,6 +1,8 @@
package cli package cli
import ( import (
"bytes"
"io/ioutil"
"testing" "testing"
) )
@ -19,3 +21,124 @@ func TestFishCompletion(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
expectFileContent(t, "testdata/expected-fish-full.fish", res) expectFileContent(t, "testdata/expected-fish-full.fish", res)
} }
func testApp() *App {
app := newTestApp()
app.Name = "greet"
app.Flags = []Flag{
&StringFlag{
Name: "socket",
Aliases: []string{"s"},
Usage: "some 'usage' text",
Value: "value",
TakesFile: true,
},
&StringFlag{Name: "flag", Aliases: []string{"fl", "f"}},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
&BoolFlag{
Name: "hidden-flag",
Hidden: true,
},
}
app.Commands = []*Command{{
Aliases: []string{"c"},
Flags: []Flag{
&StringFlag{
Name: "flag",
Aliases: []string{"fl", "f"},
TakesFile: true,
},
&BoolFlag{
Name: "another-flag",
Aliases: []string{"b"},
Usage: "another usage text",
},
},
Name: "config",
Usage: "another usage test",
Subcommands: []*Command{{
Aliases: []string{"s", "ss"},
Flags: []Flag{
&StringFlag{Name: "sub-flag", Aliases: []string{"sub-fl", "s"}},
&BoolFlag{
Name: "sub-command-flag",
Aliases: []string{"s"},
Usage: "some usage text",
},
},
Name: "sub-config",
Usage: "another usage test",
}},
}, {
Aliases: []string{"i", "in"},
Name: "info",
Usage: "retrieve generic information",
}, {
Name: "some-command",
}, {
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"},
{Name: "Oliver Allen", Email: "oliver@toyshop.com"},
}
return app
}
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, got, string(data))
}

51
flag-spec.yaml Normal file
View File

@ -0,0 +1,51 @@
# NOTE: this file is used by the tool defined in
# ./internal/genflags/cmd/genflags/main.go which uses the
# `genflags.Spec` type that maps to this file structure.
flag_types:
bool: {}
float64: {}
int64: {}
int: {}
time.Duration: {}
uint64: {}
uint: {}
string:
struct_fields:
- { name: TakesFile, type: bool }
Generic:
struct_fields:
- { name: TakesFile, type: bool }
Path:
struct_fields:
- { name: TakesFile, type: bool }
Float64Slice:
value_pointer: true
skip_interfaces:
- fmt.Stringer
Int64Slice:
value_pointer: true
skip_interfaces:
- fmt.Stringer
IntSlice:
value_pointer: true
skip_interfaces:
- fmt.Stringer
StringSlice:
value_pointer: true
skip_interfaces:
- fmt.Stringer
struct_fields:
- { name: TakesFile, type: bool }
Timestamp:
value_pointer: true
struct_fields:
- { name: Layout, type: string }
- { name: Timezone, type: "*time.Location" }
# TODO: enable UintSlice
# UintSlice: {}
# TODO: enable Uint64Slice once #1334 lands
# Uint64Slice: {}

109
flag.go
View File

@ -5,7 +5,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"reflect"
"regexp" "regexp"
"runtime" "runtime"
"strconv" "strconv"
@ -117,6 +116,12 @@ type DocGenerationFlag interface {
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
GetValue() string GetValue() string
// GetDefaultText returns the default text for this flag
GetDefaultText() string
// GetEnvVars returns the env vars for this flag
GetEnvVars() []string
} }
// VisibleFlag is an interface that allows to check if a flag is visible // VisibleFlag is an interface that allows to check if a flag is visible
@ -127,6 +132,14 @@ type VisibleFlag interface {
IsVisible() bool IsVisible() bool
} }
// CategorizableFlag is an interface that allows us to potentially
// use a flag in a categorized representation.
type CategorizableFlag interface {
VisibleFlag
GetCategory() string
}
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError) set := flag.NewFlagSet(name, flag.ContinueOnError)
@ -238,7 +251,7 @@ func prefixedNames(names []string, placeholder string) string {
func withEnvHint(envVars []string, str string) string { func withEnvHint(envVars []string, str string) string {
envText := "" envText := ""
if envVars != nil && len(envVars) > 0 { if len(envVars) > 0 {
prefix := "$" prefix := "$"
suffix := "" suffix := ""
sep := ", $" sep := ", $"
@ -253,7 +266,7 @@ func withEnvHint(envVars []string, str string) string {
return str + envText return str + envText
} }
func flagNames(name string, aliases []string) []string { func FlagNames(name string, aliases []string) []string {
var ret []string var ret []string
for _, part := range append([]string{name}, aliases...) { for _, part := range append([]string{name}, aliases...) {
@ -267,17 +280,6 @@ func flagNames(name string, aliases []string) []string {
return ret return ret
} }
func flagStringSliceField(f Flag, name string) []string {
fv := flagValue(f)
field := fv.FieldByName(name)
if field.IsValid() {
return field.Interface().([]string)
}
return []string{}
}
func withFileHint(filePath, str string) string { func withFileHint(filePath, str string) string {
fileText := "" fileText := ""
if filePath != "" { if filePath != "" {
@ -286,68 +288,34 @@ func withFileHint(filePath, str string) string {
return str + fileText return str + fileText
} }
func flagValue(f Flag) reflect.Value {
fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr {
fv = reflect.Indirect(fv)
}
return fv
}
func formatDefault(format string) string { func formatDefault(format string) string {
return " (default: " + format + ")" return " (default: " + format + ")"
} }
func stringifyFlag(f Flag) string { func stringifyFlag(f Flag) string {
fv := flagValue(f) // enforce DocGeneration interface on flags to avoid reflection
df, ok := f.(DocGenerationFlag)
switch f := f.(type) { if !ok {
case *IntSliceFlag: return ""
return withEnvHint(flagStringSliceField(f, "EnvVars"),
stringifyIntSliceFlag(f))
case *Int64SliceFlag:
return withEnvHint(flagStringSliceField(f, "EnvVars"),
stringifyInt64SliceFlag(f))
case *Float64SliceFlag:
return withEnvHint(flagStringSliceField(f, "EnvVars"),
stringifyFloat64SliceFlag(f))
case *StringSliceFlag:
return withEnvHint(flagStringSliceField(f, "EnvVars"),
stringifyStringSliceFlag(f))
} }
placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) placeholder, usage := unquoteUsage(df.GetUsage())
needsPlaceholder := df.TakesValue()
needsPlaceholder := false
defaultValueString := ""
val := fv.FieldByName("Value")
if val.IsValid() {
needsPlaceholder = val.Kind() != reflect.Bool
defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface())
if val.Kind() == reflect.String && 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(formatDefault("%s"), helpText.String())
}
if defaultValueString == formatDefault("") {
defaultValueString = ""
}
if needsPlaceholder && placeholder == "" { if needsPlaceholder && placeholder == "" {
placeholder = defaultPlaceholder placeholder = defaultPlaceholder
} }
defaultValueString := ""
if s := df.GetDefaultText(); s != "" {
defaultValueString = fmt.Sprintf(formatDefault("%s"), s)
}
usageWithDefault := strings.TrimSpace(usage + defaultValueString) usageWithDefault := strings.TrimSpace(usage + defaultValueString)
return withEnvHint(flagStringSliceField(f, "EnvVars"), return withEnvHint(df.GetEnvVars(),
fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) fmt.Sprintf("%s\t%s", prefixedNames(df.Names(), placeholder), usageWithDefault))
} }
func stringifyIntSliceFlag(f *IntSliceFlag) string { func stringifyIntSliceFlag(f *IntSliceFlag) string {
@ -426,19 +394,26 @@ func hasFlag(flags []Flag, fl Flag) bool {
return false return false
} }
func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) { // Return the first value from a list of environment variables and files
// (which may or may not exist), a description of where the value was found,
// and a boolean which is true if a value was found.
func flagFromEnvOrFile(envVars []string, filePath string) (value string, fromWhere string, found bool) {
for _, envVar := range envVars { for _, envVar := range envVars {
envVar = strings.TrimSpace(envVar) envVar = strings.TrimSpace(envVar)
if val, ok := syscall.Getenv(envVar); ok { if value, found := syscall.Getenv(envVar); found {
return val, true return value, fmt.Sprintf("environment variable %q", envVar), true
} }
} }
for _, fileVar := range strings.Split(filePath, ",") { for _, fileVar := range strings.Split(filePath, ",") {
if fileVar != "" { if fileVar != "" {
if data, err := ioutil.ReadFile(fileVar); err == nil { if data, err := ioutil.ReadFile(fileVar); err == nil {
return string(data), true return string(data), fmt.Sprintf("file %q", filePath), true
} }
} }
} }
return "", false return "", "", false
}
func flagSplitMultiValues(val string) []string {
return strings.Split(val, ",")
} }

View File

@ -6,42 +6,6 @@ import (
"strconv" "strconv"
) )
// BoolFlag is a flag with type bool
type BoolFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value bool
DefaultText string
Destination *bool
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *BoolFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *BoolFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *BoolFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *BoolFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *BoolFlag) TakesValue() bool { func (f *BoolFlag) TakesValue() bool {
return false return false
@ -52,25 +16,38 @@ func (f *BoolFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *BoolFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *BoolFlag) GetValue() string { func (f *BoolFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *BoolFlag) IsVisible() bool { func (f *BoolFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return fmt.Sprintf("%v", f.Value)
}
// GetEnvVars returns the env vars for this flag
func (f *BoolFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *BoolFlag) Apply(set *flag.FlagSet) error { func (f *BoolFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valBool, err := strconv.ParseBool(val) valBool, err := strconv.ParseBool(val)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as bool value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as bool value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = valBool f.Value = valBool
@ -89,10 +66,15 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *BoolFlag) Get(ctx *Context) bool {
return ctx.Bool(f.Name)
}
// Bool looks up the value of a local BoolFlag, returns // Bool looks up the value of a local BoolFlag, returns
// false if not found // false if not found
func (c *Context) Bool(name string) bool { func (cCtx *Context) Bool(name string) bool {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupBool(name, fs) return lookupBool(name, fs)
} }
return false return false

View File

@ -6,42 +6,6 @@ import (
"time" "time"
) )
// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration)
type DurationFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value time.Duration
DefaultText string
Destination *time.Duration
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *DurationFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *DurationFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *DurationFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *DurationFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *DurationFlag) TakesValue() bool { func (f *DurationFlag) TakesValue() bool {
return true return true
@ -52,25 +16,38 @@ func (f *DurationFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *DurationFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *DurationFlag) GetValue() string { func (f *DurationFlag) GetValue() string {
return f.Value.String() return f.Value.String()
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *DurationFlag) IsVisible() bool { func (f *DurationFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *DurationFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *DurationFlag) Apply(set *flag.FlagSet) error { func (f *DurationFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valDuration, err := time.ParseDuration(val) valDuration, err := time.ParseDuration(val)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as duration value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as duration value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = valDuration f.Value = valDuration
@ -88,10 +65,15 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *DurationFlag) Get(ctx *Context) time.Duration {
return ctx.Duration(f.Name)
}
// Duration looks up the value of a local DurationFlag, returns // Duration looks up the value of a local DurationFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Duration(name string) time.Duration { func (cCtx *Context) Duration(name string) time.Duration {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupDuration(name, fs) return lookupDuration(name, fs)
} }
return 0 return 0

View File

@ -6,42 +6,6 @@ import (
"strconv" "strconv"
) )
// Float64Flag is a flag with type float64
type Float64Flag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value float64
DefaultText string
Destination *float64
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Float64Flag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *Float64Flag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *Float64Flag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Float64Flag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *Float64Flag) TakesValue() bool { func (f *Float64Flag) TakesValue() bool {
return true return true
@ -52,24 +16,37 @@ func (f *Float64Flag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *Float64Flag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *Float64Flag) GetValue() string { func (f *Float64Flag) GetValue() string {
return fmt.Sprintf("%f", f.Value) return fmt.Sprintf("%v", f.Value)
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *Float64Flag) IsVisible() bool { func (f *Float64Flag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *Float64Flag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Float64Flag) Apply(set *flag.FlagSet) error { func (f *Float64Flag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valFloat, err := strconv.ParseFloat(val, 64) valFloat, err := strconv.ParseFloat(val, 64)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as float64 value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = valFloat f.Value = valFloat
@ -88,10 +65,15 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *Float64Flag) Get(ctx *Context) float64 {
return ctx.Float64(f.Name)
}
// Float64 looks up the value of a local Float64Flag, returns // Float64 looks up the value of a local Float64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Float64(name string) float64 { func (cCtx *Context) Float64(name string) float64 {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupFloat64(name, fs) return lookupFloat64(name, fs)
} }
return 0 return 0

View File

@ -43,18 +43,25 @@ func (f *Float64Slice) Set(value string) error {
return nil return nil
} }
tmp, err := strconv.ParseFloat(value, 64) for _, s := range flagSplitMultiValues(value) {
tmp, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
if err != nil { if err != nil {
return err return err
} }
f.slice = append(f.slice, tmp) f.slice = append(f.slice, tmp)
}
return nil return nil
} }
// String returns a readable representation of this value (for usage defaults) // String returns a readable representation of this value (for usage defaults)
func (f *Float64Slice) String() string { func (f *Float64Slice) String() string {
return fmt.Sprintf("%#v", f.slice) v := f.slice
if v == nil {
// treat nil the same as zero length non-nil
v = make([]float64, 0)
}
return fmt.Sprintf("%#v", v)
} }
// Serialize allows Float64Slice to fulfill Serializer // Serialize allows Float64Slice to fulfill Serializer
@ -73,39 +80,10 @@ func (f *Float64Slice) Get() interface{} {
return *f return *f
} }
// Float64SliceFlag is a flag with type *Float64Slice
type Float64SliceFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value *Float64Slice
DefaultText string
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Float64SliceFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value // String returns a readable representation of this value
// (for usage defaults) // (for usage defaults)
func (f *Float64SliceFlag) String() string { func (f *Float64SliceFlag) String() string {
return FlagStringer(f) return withEnvHint(f.GetEnvVars(), stringifyFloat64SliceFlag(f))
}
// Names returns the names of the flag
func (f *Float64SliceFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Float64SliceFlag) IsRequired() bool {
return f.Required
} }
// TakesValue returns true if the flag takes a value, otherwise false // TakesValue returns true if the flag takes a value, otherwise false
@ -118,6 +96,11 @@ func (f *Float64SliceFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *Float64SliceFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *Float64SliceFlag) GetValue() string { func (f *Float64SliceFlag) GetValue() string {
@ -127,45 +110,69 @@ func (f *Float64SliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *Float64SliceFlag) IsVisible() bool { func (f *Float64SliceFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *Float64SliceFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { // apply any default
if val != "" { if f.Destination != nil && f.Value != nil {
f.Value = &Float64Slice{} f.Destination.slice = make([]float64, len(f.Value.slice))
copy(f.Destination.slice, f.Value.slice)
}
for _, s := range strings.Split(val, ",") { // resolve setValue (what we will assign to the set)
if err := f.Value.Set(strings.TrimSpace(s)); err != nil { var setValue *Float64Slice
return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err) switch {
case f.Destination != nil:
setValue = f.Destination
case f.Value != nil:
setValue = f.Value.clone()
default:
setValue = new(Float64Slice)
}
if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" {
for _, s := range flagSplitMultiValues(val) {
if err := setValue.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as float64 slice value from %s for flag %s: %s", val, source, f.Name, err)
} }
} }
// Set this to false so that we reset the slice if we then set values from // 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. // flags that have already been set by the environment.
f.Value.hasBeenSet = false setValue.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
} }
if f.Value == nil {
f.Value = &Float64Slice{}
}
copyValue := f.Value.clone()
for _, name := range f.Names() { for _, name := range f.Names() {
set.Var(copyValue, name, f.Usage) set.Var(setValue, name, f.Usage)
} }
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *Float64SliceFlag) Get(ctx *Context) []float64 {
return ctx.Float64Slice(f.Name)
}
// Float64Slice looks up the value of a local Float64SliceFlag, returns // Float64Slice looks up the value of a local Float64SliceFlag, returns
// nil if not found // nil if not found
func (c *Context) Float64Slice(name string) []float64 { func (cCtx *Context) Float64Slice(name string) []float64 {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupFloat64Slice(name, fs) return lookupFloat64Slice(name, fs)
} }
return nil return nil
@ -174,7 +181,7 @@ func (c *Context) Float64Slice(name string) []float64 {
func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 {
f := set.Lookup(name) f := set.Lookup(name)
if f != nil { if f != nil {
if slice, ok := f.Value.(*Float64Slice); ok { if slice, ok := unwrapFlagValue(f.Value).(*Float64Slice); ok {
return slice.Value() return slice.Value()
} }
} }

View File

@ -11,42 +11,6 @@ type Generic interface {
String() string String() string
} }
// GenericFlag is a flag with type Generic
type GenericFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value Generic
DefaultText string
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *GenericFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *GenericFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *GenericFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *GenericFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *GenericFlag) TakesValue() bool { func (f *GenericFlag) TakesValue() bool {
return true return true
@ -57,6 +21,11 @@ func (f *GenericFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *GenericFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *GenericFlag) GetValue() string { func (f *GenericFlag) GetValue() string {
@ -66,18 +35,26 @@ func (f *GenericFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *GenericFlag) IsVisible() bool { func (f *GenericFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *GenericFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply takes the flagset and calls Set on the generic flag with the value // Apply takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag // provided by the user for parsing by the flag
func (f GenericFlag) Apply(set *flag.FlagSet) error { func (f GenericFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
if err := f.Value.Set(val); err != nil { if err := f.Value.Set(val); err != nil {
return fmt.Errorf("could not parse %q as value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q from %s as value for flag %s: %s", val, source, f.Name, err)
} }
f.HasBeenSet = true f.HasBeenSet = true
@ -91,10 +68,15 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *GenericFlag) Get(ctx *Context) interface{} {
return ctx.Generic(f.Name)
}
// Generic looks up the value of a local GenericFlag, returns // Generic looks up the value of a local GenericFlag, returns
// nil if not found // nil if not found
func (c *Context) Generic(name string) interface{} { func (cCtx *Context) Generic(name string) interface{} {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupGeneric(name, fs) return lookupGeneric(name, fs)
} }
return nil return nil

View File

@ -6,42 +6,6 @@ import (
"strconv" "strconv"
) )
// IntFlag is a flag with type int
type IntFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value int
DefaultText string
Destination *int
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *IntFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *IntFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *IntFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *IntFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *IntFlag) TakesValue() bool { func (f *IntFlag) TakesValue() bool {
return true return true
@ -52,25 +16,38 @@ func (f *IntFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *IntFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *IntFlag) GetValue() string { func (f *IntFlag) GetValue() string {
return fmt.Sprintf("%d", f.Value) return fmt.Sprintf("%d", f.Value)
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *IntFlag) IsVisible() bool { func (f *IntFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *IntFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *IntFlag) Apply(set *flag.FlagSet) error { func (f *IntFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valInt, err := strconv.ParseInt(val, 0, 64) valInt, err := strconv.ParseInt(val, 0, 64)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as int value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = int(valInt) f.Value = int(valInt)
@ -89,10 +66,15 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *IntFlag) Get(ctx *Context) int {
return ctx.Int(f.Name)
}
// Int looks up the value of a local IntFlag, returns // Int looks up the value of a local IntFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Int(name string) int { func (cCtx *Context) Int(name string) int {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupInt(name, fs) return lookupInt(name, fs)
} }
return 0 return 0

View File

@ -6,42 +6,6 @@ import (
"strconv" "strconv"
) )
// Int64Flag is a flag with type int64
type Int64Flag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value int64
DefaultText string
Destination *int64
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Int64Flag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *Int64Flag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *Int64Flag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Int64Flag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *Int64Flag) TakesValue() bool { func (f *Int64Flag) TakesValue() bool {
return true return true
@ -52,25 +16,38 @@ func (f *Int64Flag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *Int64Flag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *Int64Flag) GetValue() string { func (f *Int64Flag) GetValue() string {
return fmt.Sprintf("%d", f.Value) return fmt.Sprintf("%d", f.Value)
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *Int64Flag) IsVisible() bool { func (f *Int64Flag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *Int64Flag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Int64Flag) Apply(set *flag.FlagSet) error { func (f *Int64Flag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valInt, err := strconv.ParseInt(val, 0, 64) valInt, err := strconv.ParseInt(val, 0, 64)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as int value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = valInt f.Value = valInt
@ -88,10 +65,15 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *Int64Flag) Get(ctx *Context) int64 {
return ctx.Int64(f.Name)
}
// Int64 looks up the value of a local Int64Flag, returns // Int64 looks up the value of a local Int64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Int64(name string) int64 { func (cCtx *Context) Int64(name string) int64 {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupInt64(name, fs) return lookupInt64(name, fs)
} }
return 0 return 0

View File

@ -43,19 +43,26 @@ func (i *Int64Slice) Set(value string) error {
return nil return nil
} }
tmp, err := strconv.ParseInt(value, 0, 64) for _, s := range flagSplitMultiValues(value) {
tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
if err != nil { if err != nil {
return err return err
} }
i.slice = append(i.slice, tmp) i.slice = append(i.slice, tmp)
}
return nil return nil
} }
// String returns a readable representation of this value (for usage defaults) // String returns a readable representation of this value (for usage defaults)
func (i *Int64Slice) String() string { func (i *Int64Slice) String() string {
return fmt.Sprintf("%#v", i.slice) v := i.slice
if v == nil {
// treat nil the same as zero length non-nil
v = make([]int64, 0)
}
return fmt.Sprintf("%#v", v)
} }
// Serialize allows Int64Slice to fulfill Serializer // Serialize allows Int64Slice to fulfill Serializer
@ -74,39 +81,10 @@ func (i *Int64Slice) Get() interface{} {
return *i return *i
} }
// Int64SliceFlag is a flag with type *Int64Slice
type Int64SliceFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value *Int64Slice
DefaultText string
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Int64SliceFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value // String returns a readable representation of this value
// (for usage defaults) // (for usage defaults)
func (f *Int64SliceFlag) String() string { func (f *Int64SliceFlag) String() string {
return FlagStringer(f) return withEnvHint(f.GetEnvVars(), stringifyInt64SliceFlag(f))
}
// Names returns the names of the flag
func (f *Int64SliceFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Int64SliceFlag) IsRequired() bool {
return f.Required
} }
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
@ -115,10 +93,15 @@ func (f *Int64SliceFlag) TakesValue() bool {
} }
// GetUsage returns the usage string for the flag // GetUsage returns the usage string for the flag
func (f Int64SliceFlag) GetUsage() string { func (f *Int64SliceFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *Int64SliceFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *Int64SliceFlag) GetValue() string { func (f *Int64SliceFlag) GetValue() string {
@ -128,43 +111,67 @@ func (f *Int64SliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *Int64SliceFlag) IsVisible() bool { func (f *Int64SliceFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *Int64SliceFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { // apply any default
f.Value = &Int64Slice{} if f.Destination != nil && f.Value != nil {
f.Destination.slice = make([]int64, len(f.Value.slice))
copy(f.Destination.slice, f.Value.slice)
}
for _, s := range strings.Split(val, ",") { // resolve setValue (what we will assign to the set)
if err := f.Value.Set(strings.TrimSpace(s)); err != nil { var setValue *Int64Slice
return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err) switch {
case f.Destination != nil:
setValue = f.Destination
case f.Value != nil:
setValue = f.Value.clone()
default:
setValue = new(Int64Slice)
}
if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" {
for _, s := range flagSplitMultiValues(val) {
if err := setValue.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as int64 slice value from %s for flag %s: %s", val, source, f.Name, err)
} }
} }
// Set this to false so that we reset the slice if we then set values from // 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. // flags that have already been set by the environment.
f.Value.hasBeenSet = false setValue.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
if f.Value == nil {
f.Value = &Int64Slice{}
}
copyValue := f.Value.clone()
for _, name := range f.Names() { for _, name := range f.Names() {
set.Var(copyValue, name, f.Usage) set.Var(setValue, name, f.Usage)
} }
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *Int64SliceFlag) Get(ctx *Context) []int64 {
return ctx.Int64Slice(f.Name)
}
// Int64Slice looks up the value of a local Int64SliceFlag, returns // Int64Slice looks up the value of a local Int64SliceFlag, returns
// nil if not found // nil if not found
func (c *Context) Int64Slice(name string) []int64 { func (cCtx *Context) Int64Slice(name string) []int64 {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupInt64Slice(name, fs) return lookupInt64Slice(name, fs)
} }
return nil return nil
@ -173,7 +180,7 @@ func (c *Context) Int64Slice(name string) []int64 {
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
f := set.Lookup(name) f := set.Lookup(name)
if f != nil { if f != nil {
if slice, ok := f.Value.(*Int64Slice); ok { if slice, ok := unwrapFlagValue(f.Value).(*Int64Slice); ok {
return slice.Value() return slice.Value()
} }
} }

View File

@ -54,19 +54,26 @@ func (i *IntSlice) Set(value string) error {
return nil return nil
} }
tmp, err := strconv.ParseInt(value, 0, 64) for _, s := range flagSplitMultiValues(value) {
tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
if err != nil { if err != nil {
return err return err
} }
i.slice = append(i.slice, int(tmp)) i.slice = append(i.slice, int(tmp))
}
return nil return nil
} }
// String returns a readable representation of this value (for usage defaults) // String returns a readable representation of this value (for usage defaults)
func (i *IntSlice) String() string { func (i *IntSlice) String() string {
return fmt.Sprintf("%#v", i.slice) v := i.slice
if v == nil {
// treat nil the same as zero length non-nil
v = make([]int, 0)
}
return fmt.Sprintf("%#v", v)
} }
// Serialize allows IntSlice to fulfill Serializer // Serialize allows IntSlice to fulfill Serializer
@ -85,39 +92,10 @@ func (i *IntSlice) Get() interface{} {
return *i return *i
} }
// IntSliceFlag is a flag with type *IntSlice
type IntSliceFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value *IntSlice
DefaultText string
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *IntSliceFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value // String returns a readable representation of this value
// (for usage defaults) // (for usage defaults)
func (f *IntSliceFlag) String() string { func (f *IntSliceFlag) String() string {
return FlagStringer(f) return withEnvHint(f.GetEnvVars(), stringifyIntSliceFlag(f))
}
// Names returns the names of the flag
func (f *IntSliceFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *IntSliceFlag) IsRequired() bool {
return f.Required
} }
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
@ -126,10 +104,15 @@ func (f *IntSliceFlag) TakesValue() bool {
} }
// GetUsage returns the usage string for the flag // GetUsage returns the usage string for the flag
func (f IntSliceFlag) GetUsage() string { func (f *IntSliceFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *IntSliceFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *IntSliceFlag) GetValue() string { func (f *IntSliceFlag) GetValue() string {
@ -139,43 +122,67 @@ func (f *IntSliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *IntSliceFlag) IsVisible() bool { func (f *IntSliceFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *IntSliceFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { // apply any default
f.Value = &IntSlice{} if f.Destination != nil && f.Value != nil {
f.Destination.slice = make([]int, len(f.Value.slice))
copy(f.Destination.slice, f.Value.slice)
}
for _, s := range strings.Split(val, ",") { // resolve setValue (what we will assign to the set)
if err := f.Value.Set(strings.TrimSpace(s)); err != nil { var setValue *IntSlice
return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err) switch {
case f.Destination != nil:
setValue = f.Destination
case f.Value != nil:
setValue = f.Value.clone()
default:
setValue = new(IntSlice)
}
if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" {
for _, s := range flagSplitMultiValues(val) {
if err := setValue.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as int slice value from %s for flag %s: %s", val, source, f.Name, err)
} }
} }
// Set this to false so that we reset the slice if we then set values from // 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. // flags that have already been set by the environment.
f.Value.hasBeenSet = false setValue.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
if f.Value == nil {
f.Value = &IntSlice{}
}
copyValue := f.Value.clone()
for _, name := range f.Names() { for _, name := range f.Names() {
set.Var(copyValue, name, f.Usage) set.Var(setValue, name, f.Usage)
} }
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *IntSliceFlag) Get(ctx *Context) []int {
return ctx.IntSlice(f.Name)
}
// IntSlice looks up the value of a local IntSliceFlag, returns // IntSlice looks up the value of a local IntSliceFlag, returns
// nil if not found // nil if not found
func (c *Context) IntSlice(name string) []int { func (cCtx *Context) IntSlice(name string) []int {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupIntSlice(name, fs) return lookupIntSlice(name, fs)
} }
return nil return nil
@ -184,7 +191,7 @@ func (c *Context) IntSlice(name string) []int {
func lookupIntSlice(name string, set *flag.FlagSet) []int { func lookupIntSlice(name string, set *flag.FlagSet) []int {
f := set.Lookup(name) f := set.Lookup(name)
if f != nil { if f != nil {
if slice, ok := f.Value.(*IntSlice); ok { if slice, ok := unwrapFlagValue(f.Value).(*IntSlice); ok {
return slice.Value() return slice.Value()
} }
} }

View File

@ -1,42 +1,11 @@
package cli package cli
import "flag" import (
"flag"
"fmt"
)
type PathFlag struct { type Path = string
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value string
DefaultText string
Destination *string
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *PathFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *PathFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *PathFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *PathFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *PathFlag) TakesValue() bool { func (f *PathFlag) TakesValue() bool {
@ -48,20 +17,36 @@ func (f *PathFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *PathFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *PathFlag) GetValue() string { func (f *PathFlag) GetValue() string {
return f.Value return f.Value
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *PathFlag) IsVisible() bool { func (f *PathFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
if f.Value == "" {
return f.Value
}
return fmt.Sprintf("%q", f.Value)
}
// GetEnvVars returns the env vars for this flag
func (f *PathFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *PathFlag) Apply(set *flag.FlagSet) error { func (f *PathFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, _, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
f.Value = val f.Value = val
f.HasBeenSet = true f.HasBeenSet = true
} }
@ -77,10 +62,15 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *PathFlag) Get(ctx *Context) string {
return ctx.Path(f.Name)
}
// Path looks up the value of a local PathFlag, returns // Path looks up the value of a local PathFlag, returns
// "" if not found // "" if not found
func (c *Context) Path(name string) string { func (cCtx *Context) Path(name string) string {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupPath(name, fs) return lookupPath(name, fs)
} }

View File

@ -1,43 +1,9 @@
package cli package cli
import "flag" import (
"flag"
// StringFlag is a flag with type string "fmt"
type StringFlag struct { )
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value string
DefaultText string
Destination *string
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *StringFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *StringFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *StringFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *StringFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *StringFlag) TakesValue() bool { func (f *StringFlag) TakesValue() bool {
@ -49,20 +15,36 @@ func (f *StringFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *StringFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *StringFlag) GetValue() string { func (f *StringFlag) GetValue() string {
return f.Value return f.Value
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *StringFlag) IsVisible() bool { func (f *StringFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
if f.Value == "" {
return f.Value
}
return fmt.Sprintf("%q", f.Value)
}
// GetEnvVars returns the env vars for this flag
func (f *StringFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *StringFlag) Apply(set *flag.FlagSet) error { func (f *StringFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, _, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
f.Value = val f.Value = val
f.HasBeenSet = true f.HasBeenSet = true
} }
@ -78,10 +60,15 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *StringFlag) Get(ctx *Context) string {
return ctx.String(f.Name)
}
// String looks up the value of a local StringFlag, returns // String looks up the value of a local StringFlag, returns
// "" if not found // "" if not found
func (c *Context) String(name string) string { func (cCtx *Context) String(name string) string {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupString(name, fs) return lookupString(name, fs)
} }
return "" return ""

View File

@ -42,7 +42,9 @@ func (s *StringSlice) Set(value string) error {
return nil return nil
} }
s.slice = append(s.slice, value) for _, t := range flagSplitMultiValues(value) {
s.slice = append(s.slice, strings.TrimSpace(t))
}
return nil return nil
} }
@ -68,41 +70,10 @@ func (s *StringSlice) Get() interface{} {
return *s return *s
} }
// StringSliceFlag is a flag with type *StringSlice
type StringSliceFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value *StringSlice
DefaultText string
HasBeenSet bool
Destination *StringSlice
}
// IsSet returns whether or not the flag has been set through env or file
func (f *StringSliceFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value // String returns a readable representation of this value
// (for usage defaults) // (for usage defaults)
func (f *StringSliceFlag) String() string { func (f *StringSliceFlag) String() string {
return FlagStringer(f) return withEnvHint(f.GetEnvVars(), stringifyStringSliceFlag(f))
}
// Names returns the names of the flag
func (f *StringSliceFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *StringSliceFlag) IsRequired() bool {
return f.Required
} }
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
@ -115,6 +86,11 @@ func (f *StringSliceFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *StringSliceFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *StringSliceFlag) GetValue() string { func (f *StringSliceFlag) GetValue() string {
@ -124,48 +100,51 @@ func (f *StringSliceFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *StringSliceFlag) IsVisible() bool { func (f *StringSliceFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *StringSliceFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
// apply any default
if f.Destination != nil && f.Value != nil { if f.Destination != nil && f.Value != nil {
f.Destination.slice = make([]string, len(f.Value.slice)) f.Destination.slice = make([]string, len(f.Value.slice))
copy(f.Destination.slice, f.Value.slice) copy(f.Destination.slice, f.Value.slice)
} }
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { // resolve setValue (what we will assign to the set)
if f.Value == nil { var setValue *StringSlice
f.Value = &StringSlice{} switch {
} case f.Destination != nil:
destination := f.Value setValue = f.Destination
if f.Destination != nil { case f.Value != nil:
destination = f.Destination setValue = f.Value.clone()
default:
setValue = new(StringSlice)
} }
for _, s := range strings.Split(val, ",") { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if err := destination.Set(strings.TrimSpace(s)); err != nil { for _, s := range flagSplitMultiValues(val) {
return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err) if err := setValue.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as string value from %s for flag %s: %s", val, source, f.Name, err)
} }
} }
// Set this to false so that we reset the slice if we then set values from // 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. // flags that have already been set by the environment.
destination.hasBeenSet = false setValue.hasBeenSet = false
f.HasBeenSet = true f.HasBeenSet = true
} }
if f.Value == nil {
f.Value = &StringSlice{}
}
setValue := f.Destination
if f.Destination == nil {
setValue = f.Value.clone()
}
for _, name := range f.Names() { for _, name := range f.Names() {
set.Var(setValue, name, f.Usage) set.Var(setValue, name, f.Usage)
} }
@ -173,10 +152,15 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *StringSliceFlag) Get(ctx *Context) []string {
return ctx.StringSlice(f.Name)
}
// StringSlice looks up the value of a local StringSliceFlag, returns // StringSlice looks up the value of a local StringSliceFlag, returns
// nil if not found // nil if not found
func (c *Context) StringSlice(name string) []string { func (cCtx *Context) StringSlice(name string) []string {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupStringSlice(name, fs) return lookupStringSlice(name, fs)
} }
return nil return nil
@ -185,7 +169,7 @@ func (c *Context) StringSlice(name string) []string {
func lookupStringSlice(name string, set *flag.FlagSet) []string { func lookupStringSlice(name string, set *flag.FlagSet) []string {
f := set.Lookup(name) f := set.Lookup(name)
if f != nil { if f != nil {
if slice, ok := f.Value.(*StringSlice); ok { if slice, ok := unwrapFlagValue(f.Value).(*StringSlice); ok {
return slice.Value() return slice.Value()
} }
} }

View File

@ -51,6 +51,17 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, true) expect(t, v, true)
} }
func TestBoolFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Bool("trueflag", true, "doc")
set.Bool("falseflag", false, "doc")
ctx := NewContext(nil, set, nil)
tf := &BoolFlag{Name: "trueflag"}
ff := &BoolFlag{Name: "falseflag"}
expect(t, tf.Get(ctx), true)
expect(t, ff.Get(ctx), false)
}
func TestFlagsFromEnv(t *testing.T) { func TestFlagsFromEnv(t *testing.T) {
newSetFloat64Slice := func(defaults ...float64) Float64Slice { newSetFloat64Slice := func(defaults ...float64) Float64Slice {
s := NewFloat64Slice(defaults...) s := NewFloat64Slice(defaults...)
@ -85,33 +96,33 @@ func TestFlagsFromEnv(t *testing.T) {
{"", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""}, {"", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""},
{"1", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""}, {"1", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""},
{"false", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""}, {"false", false, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, ""},
{"foobar", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, `could not parse "foobar" as bool value for flag debug: .*`}, {"foobar", true, &BoolFlag{Name: "debug", EnvVars: []string{"DEBUG"}}, `could not parse "foobar" as bool value from environment variable "DEBUG" for flag debug: .*`},
{"1s", 1 * time.Second, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, ""}, {"1s", 1 * time.Second, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, ""},
{"foobar", false, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, `could not parse "foobar" as duration value for flag time: .*`}, {"foobar", false, &DurationFlag{Name: "time", EnvVars: []string{"TIME"}}, `could not parse "foobar" as duration value from environment variable "TIME" for flag time: .*`},
{"1.2", 1.2, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1.2", 1.2, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1", 1.0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1", 1.0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"foobar", 0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 value for flag seconds: .*`}, {"foobar", 0, &Float64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 value from environment variable "SECONDS" for flag seconds: .*`},
{"1", int64(1), &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1", int64(1), &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, {"1.2", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value from environment variable "SECONDS" for flag seconds: .*`},
{"foobar", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, {"foobar", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value from environment variable "SECONDS" for flag seconds: .*`},
{"1", 1, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1", 1, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, {"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value from environment variable "SECONDS" for flag seconds: .*`},
{"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value from environment variable "SECONDS" for flag seconds: .*`},
{"1.0,2", newSetFloat64Slice(1, 2), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"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: .*`}, {"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as float64 slice value from environment variable "SECONDS" for flag seconds: .*`},
{"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`}, {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value from environment variable "SECONDS" for flag seconds: .*`},
{"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`}, {"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value from environment variable "SECONDS" for flag seconds: .*`},
{"1,2", newSetInt64Slice(1, 2), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1,2", newSetInt64Slice(1, 2), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2,2", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int64 slice value for flag seconds: .*`}, {"1.2,2", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int64 slice value from environment variable "SECONDS" for flag seconds: .*`},
{"foobar", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int64 slice value for flag seconds: .*`}, {"foobar", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int64 slice value from environment variable "SECONDS" for flag seconds: .*`},
{"foo", "foo", &StringFlag{Name: "name", EnvVars: []string{"NAME"}}, ""}, {"foo", "foo", &StringFlag{Name: "name", EnvVars: []string{"NAME"}}, ""},
{"path", "path", &PathFlag{Name: "path", EnvVars: []string{"PATH"}}, ""}, {"path", "path", &PathFlag{Name: "path", EnvVars: []string{"PATH"}}, ""},
@ -119,12 +130,12 @@ func TestFlagsFromEnv(t *testing.T) {
{"foo,bar", newSetStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""}, {"foo,bar", newSetStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""},
{"1", uint(1), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1", uint(1), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint value for flag seconds: .*`}, {"1.2", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint value from environment variable "SECONDS" for flag seconds: .*`},
{"foobar", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint value for flag seconds: .*`}, {"foobar", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint value from environment variable "SECONDS" for flag seconds: .*`},
{"1", uint64(1), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1", uint64(1), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint64 value for flag seconds: .*`}, {"1.2", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint64 value from environment variable "SECONDS" for flag seconds: .*`},
{"foobar", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint64 value for flag seconds: .*`}, {"foobar", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint64 value from environment variable "SECONDS" for flag seconds: .*`},
{"foo,bar", &Parser{"foo", "bar"}, &GenericFlag{Name: "names", Value: &Parser{}, EnvVars: []string{"NAMES"}}, ""}, {"foo,bar", &Parser{"foo", "bar"}, &GenericFlag{Name: "names", Value: &Parser{}, EnvVars: []string{"NAMES"}}, ""},
} }
@ -132,8 +143,13 @@ func TestFlagsFromEnv(t *testing.T) {
for i, test := range flagTests { for i, test := range flagTests {
defer resetEnv(os.Environ()) defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1)
_ = os.Setenv(envVarSlice.Index(0).String(), test.input) f, ok := test.flag.(DocGenerationFlag)
if !ok {
t.Errorf("flag %v needs to implement DocGenerationFlag to retrieve env vars", test.flag)
}
envVarSlice := f.GetEnvVars()
_ = os.Setenv(envVarSlice[0], test.input)
a := App{ a := App{
Flags: []Flag{test.flag}, Flags: []Flag{test.flag},
@ -163,6 +179,183 @@ func TestFlagsFromEnv(t *testing.T) {
} }
} }
type nodocFlag struct {
Flag
Name string
}
func TestFlagStringifying(t *testing.T) {
for _, tc := range []struct {
name string
fl Flag
expected string
}{
{
name: "bool-flag",
fl: &BoolFlag{Name: "vividly"},
expected: "--vividly\t(default: false)",
},
{
name: "bool-flag-with-default-text",
fl: &BoolFlag{Name: "wildly", DefaultText: "scrambled"},
expected: "--wildly\t(default: scrambled)",
},
{
name: "duration-flag",
fl: &DurationFlag{Name: "scream-for"},
expected: "--scream-for value\t(default: 0s)",
},
{
name: "duration-flag-with-default-text",
fl: &DurationFlag{Name: "feels-about", DefaultText: "whimsically"},
expected: "--feels-about value\t(default: whimsically)",
},
{
name: "float64-flag",
fl: &Float64Flag{Name: "arduous"},
expected: "--arduous value\t(default: 0)",
},
{
name: "float64-flag-with-default-text",
fl: &Float64Flag{Name: "filibuster", DefaultText: "42"},
expected: "--filibuster value\t(default: 42)",
},
{
name: "float64-slice-flag",
fl: &Float64SliceFlag{Name: "pizzas"},
expected: "--pizzas value\t",
},
{
name: "float64-slice-flag-with-default-text",
fl: &Float64SliceFlag{Name: "pepperonis", DefaultText: "shaved"},
expected: "--pepperonis value\t(default: shaved)",
},
{
name: "generic-flag",
fl: &GenericFlag{Name: "yogurt"},
expected: "--yogurt value\t",
},
{
name: "generic-flag-with-default-text",
fl: &GenericFlag{Name: "ricotta", DefaultText: "plops"},
expected: "--ricotta value\t(default: plops)",
},
{
name: "int-flag",
fl: &IntFlag{Name: "grubs"},
expected: "--grubs value\t(default: 0)",
},
{
name: "int-flag-with-default-text",
fl: &IntFlag{Name: "poisons", DefaultText: "11ty"},
expected: "--poisons value\t(default: 11ty)",
},
{
name: "int-slice-flag",
fl: &IntSliceFlag{Name: "pencils"},
expected: "--pencils value\t",
},
{
name: "int-slice-flag-with-default-text",
fl: &IntFlag{Name: "pens", DefaultText: "-19"},
expected: "--pens value\t(default: -19)",
},
{
name: "int64-flag",
fl: &Int64Flag{Name: "flume"},
expected: "--flume value\t(default: 0)",
},
{
name: "int64-flag-with-default-text",
fl: &Int64Flag{Name: "shattering", DefaultText: "22"},
expected: "--shattering value\t(default: 22)",
},
{
name: "int64-slice-flag",
fl: &Int64SliceFlag{Name: "drawers"},
expected: "--drawers value\t",
},
{
name: "int64-slice-flag-with-default-text",
fl: &Int64SliceFlag{Name: "handles", DefaultText: "-2"},
expected: "--handles value\t(default: -2)",
},
{
name: "path-flag",
fl: &PathFlag{Name: "soup"},
expected: "--soup value\t",
},
{
name: "path-flag-with-default-text",
fl: &PathFlag{Name: "stew", DefaultText: "charred/beans"},
expected: "--stew value\t(default: charred/beans)",
},
{
name: "string-flag",
fl: &StringFlag{Name: "arf-sound"},
expected: "--arf-sound value\t",
},
{
name: "string-flag-with-default-text",
fl: &StringFlag{Name: "woof-sound", DefaultText: "urp"},
expected: "--woof-sound value\t(default: urp)",
},
{
name: "string-slice-flag",
fl: &StringSliceFlag{Name: "meow-sounds"},
expected: "--meow-sounds value\t",
},
{
name: "string-slice-flag-with-default-text",
fl: &StringSliceFlag{Name: "moo-sounds", DefaultText: "awoo"},
expected: "--moo-sounds value\t(default: awoo)",
},
{
name: "timestamp-flag",
fl: &TimestampFlag{Name: "eating"},
expected: "--eating value\t",
},
{
name: "timestamp-flag-with-default-text",
fl: &TimestampFlag{Name: "sleeping", DefaultText: "earlier"},
expected: "--sleeping value\t(default: earlier)",
},
{
name: "uint-flag",
fl: &UintFlag{Name: "jars"},
expected: "--jars value\t(default: 0)",
},
{
name: "uint-flag-with-default-text",
fl: &UintFlag{Name: "bottles", DefaultText: "99"},
expected: "--bottles value\t(default: 99)",
},
{
name: "uint64-flag",
fl: &Uint64Flag{Name: "cans"},
expected: "--cans value\t(default: 0)",
},
{
name: "uint64-flag-with-default-text",
fl: &UintFlag{Name: "tubes", DefaultText: "13"},
expected: "--tubes value\t(default: 13)",
},
{
name: "nodoc-flag",
fl: &nodocFlag{Name: "scarecrow"},
expected: "",
},
} {
t.Run(tc.name, func(ct *testing.T) {
s := stringifyFlag(tc.fl)
if s != tc.expected {
ct.Errorf("stringified flag %q does not match expected %q", s, tc.expected)
}
})
}
}
var stringFlagTests = []struct { var stringFlagTests = []struct {
name string name string
aliases []string aliases []string
@ -218,7 +411,7 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
var prefixStringFlagTests = []struct { var _ = []struct {
name string name string
aliases []string aliases []string
usage string usage string
@ -257,6 +450,14 @@ func TestStringFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, "YUUUU") expect(t, v, "YUUUU")
} }
func TestStringFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("myflag", "foobar", "doc")
ctx := NewContext(nil, set, nil)
f := &StringFlag{Name: "myflag"}
expect(t, f.Get(ctx), "foobar")
}
var pathFlagTests = []struct { var pathFlagTests = []struct {
name string name string
aliases []string aliases []string
@ -308,7 +509,15 @@ func TestPathFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, "/path/to/file/PATH") expect(t, v, "/path/to/file/PATH")
} }
var envHintFlagTests = []struct { func TestPathFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("myflag", "/my/path", "doc")
ctx := NewContext(nil, set, nil)
f := &PathFlag{Name: "myflag"}
expect(t, f.Get(ctx), "/my/path")
}
var _ = []struct {
name string name string
env string env string
hinter FlagEnvHintFunc hinter FlagEnvHintFunc
@ -395,7 +604,7 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
} }
func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) { func TestStringSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) {
defer resetEnv(os.Environ()) defer resetEnv(os.Environ())
os.Clearenv() os.Clearenv()
_ = os.Setenv("MY_GOAT", "vincent van goat,scape goat") _ = os.Setenv("MY_GOAT", "vincent van goat,scape goat")
@ -406,7 +615,22 @@ func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) {
err := set.Parse(nil) err := set.Parse(nil)
expect(t, err, nil) expect(t, err, nil)
expect(t, val.Value(), NewStringSlice("vincent van goat", "scape goat").Value()) expect(t, val.Value(), []string(nil))
expect(t, set.Lookup("goat").Value.(*StringSlice).Value(), []string{"vincent van goat", "scape goat"})
}
func TestStringSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("MY_GOAT", "vincent van goat,scape goat")
val := NewStringSlice(`some default`, `values here`)
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(), []string{`some default`, `values here`})
expect(t, set.Lookup("goat").Value.(*StringSlice).Value(), []string{"vincent van goat", "scape goat"})
} }
func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) {
@ -421,6 +645,14 @@ func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) {
expect(t, defValue, fl.Destination.Value()) expect(t, defValue, fl.Destination.Value())
} }
func TestStringSliceFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Var(NewStringSlice("a", "b", "c"), "myflag", "doc")
ctx := NewContext(nil, set, nil)
f := &StringSliceFlag{Name: "myflag"}
expect(t, f.Get(ctx), []string{"a", "b", "c"})
}
var intFlagTests = []struct { var intFlagTests = []struct {
name string name string
expected string expected string
@ -470,6 +702,14 @@ func TestIntFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, 5) expect(t, v, 5)
} }
func TestIntFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 42, "doc")
ctx := NewContext(nil, set, nil)
f := &IntFlag{Name: "myflag"}
expect(t, f.Get(ctx), 42)
}
var int64FlagTests = []struct { var int64FlagTests = []struct {
name string name string
expected string expected string
@ -508,6 +748,14 @@ func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
func TestInt64FlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int64("myflag", 42, "doc")
ctx := NewContext(nil, set, nil)
f := &Int64Flag{Name: "myflag"}
expect(t, f.Get(ctx), int64(42))
}
var uintFlagTests = []struct { var uintFlagTests = []struct {
name string name string
expected string expected string
@ -546,6 +794,14 @@ func TestUintFlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
func TestUintFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Uint("myflag", 42, "doc")
ctx := NewContext(nil, set, nil)
f := &UintFlag{Name: "myflag"}
expect(t, f.Get(ctx), uint(42))
}
var uint64FlagTests = []struct { var uint64FlagTests = []struct {
name string name string
expected string expected string
@ -584,6 +840,14 @@ func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
func TestUint64FlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Uint64("myflag", 42, "doc")
ctx := NewContext(nil, set, nil)
f := &Uint64Flag{Name: "myflag"}
expect(t, f.Get(ctx), uint64(42))
}
var durationFlagTests = []struct { var durationFlagTests = []struct {
name string name string
expected string expected string
@ -633,6 +897,14 @@ func TestDurationFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, time.Hour*30) expect(t, v, time.Hour*30)
} }
func TestDurationFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Duration("myflag", 42*time.Second, "doc")
ctx := NewContext(nil, set, nil)
f := &DurationFlag{Name: "myflag"}
expect(t, f.Get(ctx), 42*time.Second)
}
var intSliceFlagTests = []struct { var intSliceFlagTests = []struct {
name string name string
aliases []string aliases []string
@ -722,6 +994,14 @@ func TestIntSliceFlag_SetFromParentContext(t *testing.T) {
} }
} }
func TestIntSliceFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Var(NewIntSlice(1, 2, 3), "myflag", "doc")
ctx := NewContext(nil, set, nil)
f := &IntSliceFlag{Name: "myflag"}
expect(t, f.Get(ctx), []int{1, 2, 3})
}
var int64SliceFlagTests = []struct { var int64SliceFlagTests = []struct {
name string name string
aliases []string aliases []string
@ -818,6 +1098,14 @@ func TestInt64SliceFlag_ReturnNil(t *testing.T) {
} }
} }
func TestInt64SliceFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Var(NewInt64Slice(1, 2, 3), "myflag", "doc")
ctx := NewContext(nil, set, nil)
f := &Int64SliceFlag{Name: "myflag"}
expect(t, f.Get(ctx), []int64{1, 2, 3})
}
var float64FlagTests = []struct { var float64FlagTests = []struct {
name string name string
expected string expected string
@ -867,6 +1155,14 @@ func TestFloat64FlagApply_SetsAllNames(t *testing.T) {
expect(t, v, float64(43.33333)) expect(t, v, float64(43.33333))
} }
func TestFloat64FlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Float64("myflag", 1.23, "doc")
ctx := NewContext(nil, set, nil)
f := &Float64Flag{Name: "myflag"}
expect(t, f.Get(ctx), 1.23)
}
var float64SliceFlagTests = []struct { var float64SliceFlagTests = []struct {
name string name string
aliases []string aliases []string
@ -908,6 +1204,14 @@ func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
func TestFloat64SliceFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Var(NewFloat64Slice(1.23, 4.56), "myflag", "doc")
ctx := NewContext(nil, set, nil)
f := &Float64SliceFlag{Name: "myflag"}
expect(t, f.Get(ctx), []float64{1.23, 4.56})
}
var genericFlagTests = []struct { var genericFlagTests = []struct {
name string name string
value Generic value Generic
@ -956,6 +1260,14 @@ func TestGenericFlagApply_SetsAllNames(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
} }
func TestGenericFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Var(&Parser{"abc", "def"}, "myflag", "doc")
ctx := NewContext(nil, set, nil)
f := &GenericFlag{Name: "myflag"}
expect(t, f.Get(ctx), &Parser{"abc", "def"})
}
func TestParseMultiString(t *testing.T) { func TestParseMultiString(t *testing.T) {
_ = (&App{ _ = (&App{
Flags: []Flag{ Flags: []Flag{
@ -1109,6 +1421,75 @@ func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) {
}).Run([]string{"run", "-s", "10", "-s", "20"}) }).Run([]string{"run", "-s", "10", "-s", "20"})
} }
func TestParseMultiFloat64SliceWithDestinationAndEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40")
dest := &Float64Slice{}
_ = (&App{
Flags: []Flag{
&Float64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}},
},
Action: func(ctx *Context) error {
expected := []float64{10, 20}
if !reflect.DeepEqual(dest.slice, expected) {
t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve"))
}
if !reflect.DeepEqual(dest.slice, expected) {
t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s"))
}
return nil
},
}).Run([]string{"run", "-s", "10", "-s", "20"})
}
func TestParseMultiInt64SliceWithDestinationAndEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40")
dest := &Int64Slice{}
_ = (&App{
Flags: []Flag{
&Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}},
},
Action: func(ctx *Context) error {
expected := []int64{10, 20}
if !reflect.DeepEqual(dest.slice, expected) {
t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve"))
}
if !reflect.DeepEqual(dest.slice, expected) {
t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s"))
}
return nil
},
}).Run([]string{"run", "-s", "10", "-s", "20"})
}
func TestParseMultiIntSliceWithDestinationAndEnv(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("APP_INTERVALS", "20,30,40")
dest := &IntSlice{}
_ = (&App{
Flags: []Flag{
&IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: dest, EnvVars: []string{"APP_INTERVALS"}},
},
Action: func(ctx *Context) error {
expected := []int{10, 20}
if !reflect.DeepEqual(dest.slice, expected) {
t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve"))
}
if !reflect.DeepEqual(dest.slice, expected) {
t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s"))
}
return nil
},
}).Run([]string{"run", "-s", "10", "-s", "20"})
}
func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) {
_ = (&App{ _ = (&App{
Flags: []Flag{ Flags: []Flag{
@ -1860,7 +2241,7 @@ func TestFlagFromFile(t *testing.T) {
} }
for _, filePathTest := range filePathTests { for _, filePathTest := range filePathTests {
got, _ := flagFromEnvOrFile(filePathTest.name, filePathTest.path) got, _, _ := flagFromEnvOrFile(filePathTest.name, filePathTest.path)
if want := filePathTest.expected; got != want { if want := filePathTest.expected; got != want {
t.Errorf("Did not expect %v - Want %v", got, want) t.Errorf("Did not expect %v - Want %v", got, want)
} }
@ -1983,6 +2364,27 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\"")) expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\""))
} }
func TestTimestampFlagApply_Timezoned(t *testing.T) {
pdt := time.FixedZone("PDT", -7*60*60)
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: time.ANSIC, Timezone: pdt}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse([]string{"--time", "Mon Jan 2 08:04:05 2006"})
expect(t, err, nil)
expect(t, *fl.Value.timestamp, expectedResult.In(pdt))
}
func TestTimestampFlagValueFromContext(t *testing.T) {
set := flag.NewFlagSet("test", 0)
now := time.Now()
set.Var(NewTimestamp(now), "myflag", "doc")
ctx := NewContext(nil, set, nil)
f := &TimestampFlag{Name: "myflag"}
expect(t, f.Get(ctx), &now)
}
type flagDefaultTestCase struct { type flagDefaultTestCase struct {
name string name string
flag Flag flag Flag
@ -1992,43 +2394,43 @@ type flagDefaultTestCase struct {
func TestFlagDefaultValue(t *testing.T) { func TestFlagDefaultValue(t *testing.T) {
cases := []*flagDefaultTestCase{ cases := []*flagDefaultTestCase{
&flagDefaultTestCase{ {
name: "stringSclice", name: "stringSclice",
flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")}, flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
toParse: []string{"--flag", "parsed"}, toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`, expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`,
}, },
&flagDefaultTestCase{ {
name: "float64Sclice", name: "float64Sclice",
flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)}, flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
toParse: []string{"--flag", "13.3"}, toParse: []string{"--flag", "13.3"},
expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`, expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`,
}, },
&flagDefaultTestCase{ {
name: "int64Sclice", name: "int64Sclice",
flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)}, flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
toParse: []string{"--flag", "13"}, toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`, expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
}, },
&flagDefaultTestCase{ {
name: "intSclice", name: "intSclice",
flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)}, flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
toParse: []string{"--flag", "13"}, toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`, expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
}, },
&flagDefaultTestCase{ {
name: "string", name: "string",
flag: &StringFlag{Name: "flag", Value: "default"}, flag: &StringFlag{Name: "flag", Value: "default"},
toParse: []string{"--flag", "parsed"}, toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default")`, expect: `--flag value (default: "default")`,
}, },
&flagDefaultTestCase{ {
name: "bool", name: "bool",
flag: &BoolFlag{Name: "flag", Value: true}, flag: &BoolFlag{Name: "flag", Value: true},
toParse: []string{"--flag", "false"}, toParse: []string{"--flag", "false"},
expect: `--flag (default: true)`, expect: `--flag (default: true)`,
}, },
&flagDefaultTestCase{ {
name: "uint64", name: "uint64",
flag: &Uint64Flag{Name: "flag", Value: 1}, flag: &Uint64Flag{Name: "flag", Value: 1},
toParse: []string{"--flag", "13"}, toParse: []string{"--flag", "13"},
@ -2048,6 +2450,54 @@ func TestFlagDefaultValue(t *testing.T) {
} }
} }
type flagValueTestCase struct {
name string
flag Flag
toParse []string
expect string
}
func TestFlagValue(t *testing.T) {
cases := []*flagValueTestCase{
&flagValueTestCase{
name: "stringSclice",
flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
toParse: []string{"--flag", "parsed,parsed2", "--flag", "parsed3,parsed4"},
expect: `[parsed parsed2 parsed3 parsed4]`,
},
&flagValueTestCase{
name: "float64Sclice",
flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
toParse: []string{"--flag", "13.3,14.4", "--flag", "15.5,16.6"},
expect: `[]float64{13.3, 14.4, 15.5, 16.6}`,
},
&flagValueTestCase{
name: "int64Sclice",
flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
toParse: []string{"--flag", "13,14", "--flag", "15,16"},
expect: `[]int64{13, 14, 15, 16}`,
},
&flagValueTestCase{
name: "intSclice",
flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
toParse: []string{"--flag", "13,14", "--flag", "15,16"},
expect: `[]int{13, 14, 15, 16}`,
},
}
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)
}
f := set.Lookup("flag")
if got := f.Value.String(); got != v.expect {
t.Errorf("TestFlagValue %d-%s\nexpect:%s\ngot:%s", i, v.name, v.expect, got)
}
}
}
func TestTimestampFlagApply_WithDestination(t *testing.T) { func TestTimestampFlagApply_WithDestination(t *testing.T) {
var destination Timestamp var destination Timestamp
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
@ -2059,3 +2509,42 @@ func TestTimestampFlagApply_WithDestination(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
expect(t, *fl.Destination.timestamp, expectedResult) expect(t, *fl.Destination.timestamp, expectedResult)
} }
// Test issue #1254
// StringSlice() with UseShortOptionHandling causes duplicated entries, depending on the ordering of the flags
func TestSliceShortOptionHandle(t *testing.T) {
wasCalled := false
err := (&App{
Commands: []*Command{
{
Name: "foobar",
UseShortOptionHandling: true,
Action: func(ctx *Context) error {
wasCalled = true
if ctx.Bool("i") != true {
t.Error("bool i not set")
}
if ctx.Bool("t") != true {
t.Error("bool i not set")
}
ss := ctx.StringSlice("net")
if !reflect.DeepEqual(ss, []string{"foo"}) {
t.Errorf("Got different slice(%v) than expected", ss)
}
return nil
},
Flags: []Flag{
&StringSliceFlag{Name: "net"},
&BoolFlag{Name: "i"},
&BoolFlag{Name: "t"},
},
},
},
}).Run([]string{"run", "foobar", "--net=foo", "-it"})
if err != nil {
t.Fatal(err)
}
if !wasCalled {
t.Fatal("Action callback was never called")
}
}

View File

@ -11,6 +11,7 @@ type Timestamp struct {
timestamp *time.Time timestamp *time.Time
hasBeenSet bool hasBeenSet bool
layout string layout string
location *time.Location
} }
// Timestamp constructor // Timestamp constructor
@ -31,9 +32,22 @@ func (t *Timestamp) SetLayout(layout string) {
t.layout = layout t.layout = layout
} }
// Set perceived timezone of the to-be parsed time string
func (t *Timestamp) SetLocation(loc *time.Location) {
t.location = loc
}
// Parses the string value to timestamp // Parses the string value to timestamp
func (t *Timestamp) Set(value string) error { func (t *Timestamp) Set(value string) error {
timestamp, err := time.Parse(t.layout, value) var timestamp time.Time
var err error
if t.location != nil {
timestamp, err = time.ParseInLocation(t.layout, value, t.location)
} else {
timestamp, err = time.Parse(t.layout, value)
}
if err != nil { if err != nil {
return err return err
} }
@ -58,43 +72,6 @@ func (t *Timestamp) Get() interface{} {
return *t return *t
} }
// TimestampFlag is a flag with type time
type TimestampFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Layout string
Value *Timestamp
DefaultText string
HasBeenSet bool
Destination *Timestamp
}
// IsSet returns whether or not the flag has been set through env or file
func (f *TimestampFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *TimestampFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *TimestampFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *TimestampFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *TimestampFlag) TakesValue() bool { func (f *TimestampFlag) TakesValue() bool {
return true return true
@ -105,6 +82,11 @@ func (f *TimestampFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// GetCategory returns the category for the flag
func (f *TimestampFlag) GetCategory() string {
return f.Category
}
// GetValue returns the flags value as string representation and an empty // GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all. // string if the flag takes no value at all.
func (f *TimestampFlag) GetValue() string { func (f *TimestampFlag) GetValue() string {
@ -114,9 +96,17 @@ func (f *TimestampFlag) GetValue() string {
return "" return ""
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetDefaultText returns the default text for this flag
func (f *TimestampFlag) IsVisible() bool { func (f *TimestampFlag) GetDefaultText() string {
return !f.Hidden if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *TimestampFlag) GetEnvVars() []string {
return f.EnvVars
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
@ -128,14 +118,16 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
f.Value = &Timestamp{} f.Value = &Timestamp{}
} }
f.Value.SetLayout(f.Layout) f.Value.SetLayout(f.Layout)
f.Value.SetLocation(f.Timezone)
if f.Destination != nil { if f.Destination != nil {
f.Destination.SetLayout(f.Layout) f.Destination.SetLayout(f.Layout)
f.Destination.SetLocation(f.Timezone)
} }
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if err := f.Value.Set(val); err != nil { if err := f.Value.Set(val); err != nil {
return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as timestamp value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.HasBeenSet = true f.HasBeenSet = true
} }
@ -151,9 +143,14 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
return nil return nil
} }
// Get returns the flags value in the given Context.
func (f *TimestampFlag) Get(ctx *Context) *time.Time {
return ctx.Timestamp(f.Name)
}
// Timestamp gets the timestamp from a flag name // Timestamp gets the timestamp from a flag name
func (c *Context) Timestamp(name string) *time.Time { func (cCtx *Context) Timestamp(name string) *time.Time {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupTimestamp(name, fs) return lookupTimestamp(name, fs)
} }
return nil return nil

View File

@ -6,42 +6,6 @@ import (
"strconv" "strconv"
) )
// UintFlag is a flag with type uint
type UintFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value uint
DefaultText string
Destination *uint
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *UintFlag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *UintFlag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *UintFlag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *UintFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *UintFlag) TakesValue() bool { func (f *UintFlag) TakesValue() bool {
return true return true
@ -52,18 +16,18 @@ func (f *UintFlag) GetUsage() string {
return f.Usage return f.Usage
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetCategory returns the category for the flag
func (f *UintFlag) IsVisible() bool { func (f *UintFlag) GetCategory() string {
return !f.Hidden return f.Category
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *UintFlag) Apply(set *flag.FlagSet) error { func (f *UintFlag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valInt, err := strconv.ParseUint(val, 0, 64) valInt, err := strconv.ParseUint(val, 0, 64)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as uint value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as uint value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = uint(valInt) f.Value = uint(valInt)
@ -88,10 +52,28 @@ func (f *UintFlag) GetValue() string {
return fmt.Sprintf("%d", f.Value) return fmt.Sprintf("%d", f.Value)
} }
// GetDefaultText returns the default text for this flag
func (f *UintFlag) GetDefaultText() string {
if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *UintFlag) GetEnvVars() []string {
return f.EnvVars
}
// Get returns the flags value in the given Context.
func (f *UintFlag) Get(ctx *Context) uint {
return ctx.Uint(f.Name)
}
// Uint looks up the value of a local UintFlag, returns // Uint looks up the value of a local UintFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Uint(name string) uint { func (cCtx *Context) Uint(name string) uint {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupUint(name, fs) return lookupUint(name, fs)
} }
return 0 return 0

View File

@ -6,42 +6,6 @@ import (
"strconv" "strconv"
) )
// Uint64Flag is a flag with type uint64
type Uint64Flag struct {
Name string
Aliases []string
Usage string
EnvVars []string
FilePath string
Required bool
Hidden bool
Value uint64
DefaultText string
Destination *uint64
HasBeenSet bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Uint64Flag) IsSet() bool {
return f.HasBeenSet
}
// String returns a readable representation of this value
// (for usage defaults)
func (f *Uint64Flag) String() string {
return FlagStringer(f)
}
// Names returns the names of the flag
func (f *Uint64Flag) Names() []string {
return flagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Uint64Flag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false // TakesValue returns true of the flag takes a value, otherwise false
func (f *Uint64Flag) TakesValue() bool { func (f *Uint64Flag) TakesValue() bool {
return true return true
@ -52,18 +16,18 @@ func (f *Uint64Flag) GetUsage() string {
return f.Usage return f.Usage
} }
// IsVisible returns true if the flag is not hidden, otherwise false // GetCategory returns the category for the flag
func (f *Uint64Flag) IsVisible() bool { func (f *Uint64Flag) GetCategory() string {
return !f.Hidden return f.Category
} }
// Apply populates the flag given the flag set and environment // Apply populates the flag given the flag set and environment
func (f *Uint64Flag) Apply(set *flag.FlagSet) error { func (f *Uint64Flag) Apply(set *flag.FlagSet) error {
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found {
if val != "" { if val != "" {
valInt, err := strconv.ParseUint(val, 0, 64) valInt, err := strconv.ParseUint(val, 0, 64)
if err != nil { if err != nil {
return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as uint64 value from %s for flag %s: %s", val, source, f.Name, err)
} }
f.Value = valInt f.Value = valInt
@ -88,10 +52,28 @@ func (f *Uint64Flag) GetValue() string {
return fmt.Sprintf("%d", f.Value) return fmt.Sprintf("%d", f.Value)
} }
// GetDefaultText returns the default text for this flag
func (f *Uint64Flag) GetDefaultText() string {
if f.DefaultText != "" {
return f.DefaultText
}
return f.GetValue()
}
// GetEnvVars returns the env vars for this flag
func (f *Uint64Flag) GetEnvVars() []string {
return f.EnvVars
}
// Get returns the flags value in the given Context.
func (f *Uint64Flag) Get(ctx *Context) uint64 {
return ctx.Uint64(f.Name)
}
// Uint64 looks up the value of a local Uint64Flag, returns // Uint64 looks up the value of a local Uint64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Uint64(name string) uint64 { func (cCtx *Context) Uint64(name string) uint64 {
if fs := c.lookupFlagSet(name); fs != nil { if fs := cCtx.lookupFlagSet(name); fs != nil {
return lookupUint64(name, fs) return lookupUint64(name, fs)
} }
return 0 return 0

View File

@ -21,11 +21,11 @@ type CommandNotFoundFunc func(*Context, string)
// customized usage error messages. This function is able to replace the // customized usage error messages. This function is able to replace the
// original error messages. If this function is not set, the "Incorrect usage" // original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted. // is displayed and the execution is interrupted.
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error
// ExitErrHandlerFunc is executed if provided in order to handle exitError values // ExitErrHandlerFunc is executed if provided in order to handle exitError values
// returned by Actions and Before/After functions. // returned by Actions and Before/After functions.
type ExitErrHandlerFunc func(context *Context, err error) type ExitErrHandlerFunc func(cCtx *Context, err error)
// FlagStringFunc is used by the help generation to display a flag, which is // FlagStringFunc is used by the help generation to display a flag, which is
// expected to be a single line. // expected to be a single line.

12
go.mod
View File

@ -1,9 +1,13 @@
module github.com/urfave/cli/v2 module github.com/urfave/cli/v2
go 1.11 go 1.18
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v1.1.0
github.com/cpuguy83/go-md2man/v2 v2.0.1 github.com/cpuguy83/go-md2man/v2 v2.0.2
gopkg.in/yaml.v2 v2.2.8 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.1
) )
require github.com/russross/blackfriday/v2 v2.1.0 // indirect

16
go.sum
View File

@ -1,10 +1,18 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 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/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 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= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

2318
godoc-current.txt Normal file

File diff suppressed because it is too large Load Diff

198
help.go
View File

@ -10,34 +10,39 @@ import (
"unicode/utf8" "unicode/utf8"
) )
const (
helpName = "help"
helpAlias = "h"
)
var helpCommand = &Command{ var helpCommand = &Command{
Name: "help", Name: helpName,
Aliases: []string{"h"}, Aliases: []string{helpAlias},
Usage: "Shows a list of commands or help for one command", Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]", ArgsUsage: "[command]",
Action: func(c *Context) error { Action: func(cCtx *Context) error {
args := c.Args() args := cCtx.Args()
if args.Present() { if args.Present() {
return ShowCommandHelp(c, args.First()) return ShowCommandHelp(cCtx, args.First())
} }
_ = ShowAppHelp(c) _ = ShowAppHelp(cCtx)
return nil return nil
}, },
} }
var helpSubcommand = &Command{ var helpSubcommand = &Command{
Name: "help", Name: helpName,
Aliases: []string{"h"}, Aliases: []string{helpAlias},
Usage: "Shows a list of commands or help for one command", Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]", ArgsUsage: "[command]",
Action: func(c *Context) error { Action: func(cCtx *Context) error {
args := c.Args() args := cCtx.Args()
if args.Present() { if args.Present() {
return ShowCommandHelp(c, args.First()) return ShowCommandHelp(cCtx, args.First())
} }
return ShowSubcommandHelp(c) return ShowSubcommandHelp(cCtx)
}, },
} }
@ -59,6 +64,11 @@ var HelpPrinter helpPrinter = printHelp
// HelpPrinterCustom is a function that writes the help output. It is used as // HelpPrinterCustom is a function that writes the help output. It is used as
// the default implementation of HelpPrinter, and may be called directly if // the default implementation of HelpPrinter, and may be called directly if
// the ExtraInfo field is set on an App. // the ExtraInfo field is set on an App.
//
// In the default implementation, if the customFuncs argument contains a
// "wrapAt" key, which is a function which takes no arguments and returns
// an int, this int value will be used to produce a "wrap" function used
// by the default template to wrap long lines.
var HelpPrinterCustom helpPrinterCustom = printHelpCustom var HelpPrinterCustom helpPrinterCustom = printHelpCustom
// VersionPrinter prints the version for the App // VersionPrinter prints the version for the App
@ -71,30 +81,30 @@ func ShowAppHelpAndExit(c *Context, exitCode int) {
} }
// ShowAppHelp is an action that displays the help. // ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) error { func ShowAppHelp(cCtx *Context) error {
tpl := c.App.CustomAppHelpTemplate tpl := cCtx.App.CustomAppHelpTemplate
if tpl == "" { if tpl == "" {
tpl = AppHelpTemplate tpl = AppHelpTemplate
} }
if c.App.ExtraInfo == nil { if cCtx.App.ExtraInfo == nil {
HelpPrinter(c.App.Writer, tpl, c.App) HelpPrinter(cCtx.App.Writer, tpl, cCtx.App)
return nil return nil
} }
customAppData := func() map[string]interface{} { customAppData := func() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"ExtraInfo": c.App.ExtraInfo, "ExtraInfo": cCtx.App.ExtraInfo,
} }
} }
HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData()) HelpPrinterCustom(cCtx.App.Writer, tpl, cCtx.App, customAppData())
return nil return nil
} }
// DefaultAppComplete prints the list of subcommands as the default app completion method // DefaultAppComplete prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) { func DefaultAppComplete(cCtx *Context) {
DefaultCompleteWithFlags(nil)(c) DefaultCompleteWithFlags(nil)(cCtx)
} }
func printCommandSuggestions(commands []*Command, writer io.Writer) { func printCommandSuggestions(commands []*Command, writer io.Writer) {
@ -102,7 +112,7 @@ func printCommandSuggestions(commands []*Command, writer io.Writer) {
if command.Hidden { if command.Hidden {
continue continue
} }
if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { if strings.HasSuffix(os.Getenv("SHELL"), "zsh") {
for _, name := range command.Names() { for _, name := range command.Names() {
_, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage) _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
} }
@ -159,23 +169,30 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
} }
} }
func DefaultCompleteWithFlags(cmd *Command) func(c *Context) { func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) {
return func(c *Context) { return func(cCtx *Context) {
if len(os.Args) > 2 { if len(os.Args) > 2 {
lastArg := os.Args[len(os.Args)-2] lastArg := os.Args[len(os.Args)-2]
if strings.HasPrefix(lastArg, "-") { if strings.HasPrefix(lastArg, "-") {
printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer)
if cmd != nil { if cmd != nil {
printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer) printFlagSuggestions(lastArg, cmd.Flags, cCtx.App.Writer)
return
} }
printFlagSuggestions(lastArg, cCtx.App.Flags, cCtx.App.Writer)
return return
} }
} }
if cmd != nil { if cmd != nil {
printCommandSuggestions(cmd.Subcommands, c.App.Writer) printCommandSuggestions(cmd.Subcommands, cCtx.App.Writer)
} else { return
printCommandSuggestions(c.App.Commands, c.App.Writer)
} }
printCommandSuggestions(cCtx.App.Commands, cCtx.App.Writer)
} }
} }
@ -207,7 +224,13 @@ func ShowCommandHelp(ctx *Context, command string) error {
} }
if ctx.App.CommandNotFound == nil { if ctx.App.CommandNotFound == nil {
return Exit(fmt.Sprintf("No help topic for '%v'", command), 3) errMsg := fmt.Sprintf("No help topic for '%v'", command)
if ctx.App.Suggest {
if suggestion := SuggestCommand(ctx.App.Commands, command); suggestion != "" {
errMsg += ". " + suggestion
}
}
return Exit(errMsg, 3)
} }
ctx.App.CommandNotFound(ctx, command) ctx.App.CommandNotFound(ctx, command)
@ -221,32 +244,32 @@ func ShowSubcommandHelpAndExit(c *Context, exitCode int) {
} }
// ShowSubcommandHelp prints help for the given subcommand // ShowSubcommandHelp prints help for the given subcommand
func ShowSubcommandHelp(c *Context) error { func ShowSubcommandHelp(cCtx *Context) error {
if c == nil { if cCtx == nil {
return nil return nil
} }
if c.Command != nil { if cCtx.Command != nil {
return ShowCommandHelp(c, c.Command.Name) return ShowCommandHelp(cCtx, cCtx.Command.Name)
} }
return ShowCommandHelp(c, "") return ShowCommandHelp(cCtx, "")
} }
// ShowVersion prints the version number of the App // ShowVersion prints the version number of the App
func ShowVersion(c *Context) { func ShowVersion(cCtx *Context) {
VersionPrinter(c) VersionPrinter(cCtx)
} }
func printVersion(c *Context) { func printVersion(cCtx *Context) {
_, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) _, _ = fmt.Fprintf(cCtx.App.Writer, "%v version %v\n", cCtx.App.Name, cCtx.App.Version)
} }
// ShowCompletions prints the lists of commands within a given context // ShowCompletions prints the lists of commands within a given context
func ShowCompletions(c *Context) { func ShowCompletions(cCtx *Context) {
a := c.App a := cCtx.App
if a != nil && a.BashComplete != nil { if a != nil && a.BashComplete != nil {
a.BashComplete(c) a.BashComplete(cCtx)
} }
} }
@ -268,12 +291,29 @@ func ShowCommandCompletions(ctx *Context, command string) {
// The customFuncs map will be combined with a default template.FuncMap to // The customFuncs map will be combined with a default template.FuncMap to
// allow using arbitrary functions in template rendering. // allow using arbitrary functions in template rendering.
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
const maxLineLength = 10000
funcMap := template.FuncMap{ funcMap := template.FuncMap{
"join": strings.Join, "join": strings.Join,
"indent": indent, "indent": indent,
"nindent": nindent, "nindent": nindent,
"trim": strings.TrimSpace, "trim": strings.TrimSpace,
"wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) },
"offset": offset,
} }
if customFuncs["wrapAt"] != nil {
if wa, ok := customFuncs["wrapAt"]; ok {
if waf, ok := wa.(func() int); ok {
wrapAt := waf()
customFuncs["wrap"] = func(input string, offset int) string {
return wrap(input, offset, wrapAt)
}
}
}
}
for key, value := range customFuncs { for key, value := range customFuncs {
funcMap[key] = value funcMap[key] = value
} }
@ -297,20 +337,20 @@ func printHelp(out io.Writer, templ string, data interface{}) {
HelpPrinterCustom(out, templ, data, nil) HelpPrinterCustom(out, templ, data, nil)
} }
func checkVersion(c *Context) bool { func checkVersion(cCtx *Context) bool {
found := false found := false
for _, name := range VersionFlag.Names() { for _, name := range VersionFlag.Names() {
if c.Bool(name) { if cCtx.Bool(name) {
found = true found = true
} }
} }
return found return found
} }
func checkHelp(c *Context) bool { func checkHelp(cCtx *Context) bool {
found := false found := false
for _, name := range HelpFlag.Names() { for _, name := range HelpFlag.Names() {
if c.Bool(name) { if cCtx.Bool(name) {
found = true found = true
} }
} }
@ -326,9 +366,9 @@ func checkCommandHelp(c *Context, name string) bool {
return false return false
} }
func checkSubcommandHelp(c *Context) bool { func checkSubcommandHelp(cCtx *Context) bool {
if c.Bool("h") || c.Bool("help") { if cCtx.Bool("h") || cCtx.Bool("help") {
_ = ShowSubcommandHelp(c) _ = ShowSubcommandHelp(cCtx)
return true return true
} }
@ -350,20 +390,20 @@ func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
return true, arguments[:pos] return true, arguments[:pos]
} }
func checkCompletions(c *Context) bool { func checkCompletions(cCtx *Context) bool {
if !c.shellComplete { if !cCtx.shellComplete {
return false return false
} }
if args := c.Args(); args.Present() { if args := cCtx.Args(); args.Present() {
name := args.First() name := args.First()
if cmd := c.App.Command(name); cmd != nil { if cmd := cCtx.App.Command(name); cmd != nil {
// let the command handle the completion // let the command handle the completion
return false return false
} }
} }
ShowCompletions(c) ShowCompletions(cCtx)
return true return true
} }
@ -384,3 +424,55 @@ func indent(spaces int, v string) string {
func nindent(spaces int, v string) string { func nindent(spaces int, v string) string {
return "\n" + indent(spaces, v) return "\n" + indent(spaces, v)
} }
func wrap(input string, offset int, wrapAt int) string {
var sb strings.Builder
lines := strings.Split(input, "\n")
padding := strings.Repeat(" ", offset)
for i, line := range lines {
if i != 0 {
sb.WriteString(padding)
}
sb.WriteString(wrapLine(line, offset, wrapAt, padding))
if i != len(lines)-1 {
sb.WriteString("\n")
}
}
return sb.String()
}
func wrapLine(input string, offset int, wrapAt int, padding string) string {
if wrapAt <= offset || len(input) <= wrapAt-offset {
return input
}
lineWidth := wrapAt - offset
words := strings.Fields(input)
if len(words) == 0 {
return input
}
wrapped := words[0]
spaceLeft := lineWidth - len(wrapped)
for _, word := range words[1:] {
if len(word)+1 > spaceLeft {
wrapped += "\n" + padding + word
spaceLeft = lineWidth - len(word)
} else {
wrapped += " " + word
spaceLeft -= 1 + len(word)
}
}
return wrapped
}
func offset(input string, fixed int) int {
return len(input) + fixed
}

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
@ -1037,3 +1038,311 @@ func TestHideHelpCommand_WithSubcommands(t *testing.T) {
t.Errorf("Run returned unexpected error: %v", err) t.Errorf("Run returned unexpected error: %v", err)
} }
} }
func TestDefaultCompleteWithFlags(t *testing.T) {
origEnv := os.Environ()
origArgv := os.Args
t.Cleanup(func() {
os.Args = origArgv
resetEnv(origEnv)
})
os.Setenv("SHELL", "bash")
for _, tc := range []struct {
name string
c *Context
cmd *Command
argv []string
expected string
}{
{
name: "empty",
c: &Context{App: &App{}},
cmd: &Command{},
argv: []string{"prog", "cmd"},
expected: "",
},
{
name: "typical-flag-suggestion",
c: &Context{App: &App{
Name: "cmd",
Flags: []Flag{
&BoolFlag{Name: "happiness"},
&Int64Flag{Name: "everybody-jump-on"},
},
Commands: []*Command{
{Name: "putz"},
},
}},
cmd: &Command{
Flags: []Flag{
&BoolFlag{Name: "excitement"},
&StringFlag{Name: "hat-shape"},
},
},
argv: []string{"cmd", "--e", "--generate-bash-completion"},
expected: "--excitement\n",
},
{
name: "typical-command-suggestion",
c: &Context{App: &App{
Name: "cmd",
Flags: []Flag{
&BoolFlag{Name: "happiness"},
&Int64Flag{Name: "everybody-jump-on"},
},
}},
cmd: &Command{
Name: "putz",
Subcommands: []*Command{
{Name: "futz"},
},
Flags: []Flag{
&BoolFlag{Name: "excitement"},
&StringFlag{Name: "hat-shape"},
},
},
argv: []string{"cmd", "--generate-bash-completion"},
expected: "futz\n",
},
} {
t.Run(tc.name, func(ct *testing.T) {
writer := &bytes.Buffer{}
tc.c.App.Writer = writer
os.Args = tc.argv
f := DefaultCompleteWithFlags(tc.cmd)
f(tc.c)
written := writer.String()
if written != tc.expected {
ct.Errorf("written help does not match expected %q != %q", written, tc.expected)
}
})
}
}
func TestWrappedHelp(t *testing.T) {
// Reset HelpPrinter after this test.
defer func(old helpPrinter) {
HelpPrinter = old
}(HelpPrinter)
output := new(bytes.Buffer)
app := &App{
Writer: output,
Flags: []Flag{
&BoolFlag{Name: "foo",
Aliases: []string{"h"},
Usage: "here's a really long help text line, let's see where it wraps. blah blah blah and so on.",
},
},
Usage: "here's a sample App.Usage string long enough that it should be wrapped in this test",
UsageText: "i'm not sure how App.UsageText differs from App.Usage, but this should also be wrapped in this test",
// TODO: figure out how to make ArgsUsage appear in the help text, and test that
Description: `here's a sample App.Description string long enough that it should be wrapped in this test
with a newline
and an indented line`,
Copyright: `Here's a sample copyright text string long enough that it should be wrapped.
Including newlines.
And also indented lines.
And then another long line. Blah blah blah does anybody ever read these things?`,
}
c := NewContext(app, nil, nil)
HelpPrinter = func(w io.Writer, templ string, data interface{}) {
funcMap := map[string]interface{}{
"wrapAt": func() int {
return 30
},
}
HelpPrinterCustom(w, templ, data, funcMap)
}
_ = ShowAppHelp(c)
expected := `NAME:
- here's a sample
App.Usage string long
enough that it should be
wrapped in this test
USAGE:
i'm not sure how
App.UsageText differs from
App.Usage, but this should
also be wrapped in this
test
DESCRIPTION:
here's a sample
App.Description string long
enough that it should be
wrapped in this test
with a newline
and an indented line
GLOBAL OPTIONS:
--foo, -h here's a
really long help text
line, let's see where it
wraps. blah blah blah
and so on. (default:
false)
COPYRIGHT:
Here's a sample copyright
text string long enough
that it should be wrapped.
Including newlines.
And also indented lines.
And then another long line.
Blah blah blah does anybody
ever read these things?
`
if output.String() != expected {
t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s",
output.String(), expected)
}
}
func TestWrappedCommandHelp(t *testing.T) {
// Reset HelpPrinter after this test.
defer func(old helpPrinter) {
HelpPrinter = old
}(HelpPrinter)
output := new(bytes.Buffer)
app := &App{
Writer: output,
Commands: []*Command{
{
Name: "add",
Aliases: []string{"a"},
Usage: "add a task to the list",
UsageText: "this is an even longer way of describing adding a task to the list",
Description: "and a description long enough to wrap in this test case",
Action: func(c *Context) error {
return nil
},
},
},
}
c := NewContext(app, nil, nil)
HelpPrinter = func(w io.Writer, templ string, data interface{}) {
funcMap := map[string]interface{}{
"wrapAt": func() int {
return 30
},
}
HelpPrinterCustom(w, templ, data, funcMap)
}
_ = ShowCommandHelp(c, "add")
expected := `NAME:
- add a task to the list
USAGE:
this is an even longer way
of describing adding a task
to the list
DESCRIPTION:
and a description long
enough to wrap in this test
case
`
if output.String() != expected {
t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s",
output.String(), expected)
}
}
func TestWrappedSubcommandHelp(t *testing.T) {
// Reset HelpPrinter after this test.
defer func(old helpPrinter) {
HelpPrinter = old
}(HelpPrinter)
output := new(bytes.Buffer)
app := &App{
Name: "cli.test",
Writer: output,
Commands: []*Command{
{
Name: "bar",
Aliases: []string{"a"},
Usage: "add a task to the list",
UsageText: "this is an even longer way of describing adding a task to the list",
Description: "and a description long enough to wrap in this test case",
Action: func(c *Context) error {
return nil
},
Subcommands: []*Command{
{
Name: "grok",
Usage: "remove an existing template",
UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more",
Action: func(c *Context) error {
return nil
},
},
},
},
},
}
HelpPrinter = func(w io.Writer, templ string, data interface{}) {
funcMap := map[string]interface{}{
"wrapAt": func() int {
return 30
},
}
HelpPrinterCustom(w, templ, data, funcMap)
}
_ = app.Run([]string{"foo", "bar", "grok", "--help"})
expected := `NAME:
cli.test bar grok - remove
an
existing
template
USAGE:
longer usage text goes
here, la la la, hopefully
this is long enough to wrap
even more
OPTIONS:
--help, -h show help (default: false)
`
if output.String() != expected {
t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s",
output.String(), expected)
}
}

View File

@ -6,19 +6,45 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"math" "math"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var packages = []string{"cli", "altsrc"} const (
badNewsEmoji = "🚨"
goodNewsEmoji = "✨"
checksPassedEmoji = "✅"
v2diffWarning = `
# The unified diff above indicates that the public API surface area
# has changed. If you feel that the changes are acceptable and adhere
# to the semantic versioning promise of the v2.x series described in
# docs/CONTRIBUTING.md, please run the following command to promote
# the current go docs:
#
# make v2approve
#
`
)
func main() { func main() {
top, err := func() (string, error) {
if v, err := sh("git", "rev-parse", "--show-toplevel"); err == nil {
return strings.TrimSpace(v), nil
}
return os.Getwd()
}()
if err != nil {
log.Fatal(err)
}
app := cli.NewApp() app := cli.NewApp()
app.Name = "builder" app.Name = "builder"
@ -37,22 +63,56 @@ func main() {
Name: "gfmrun", Name: "gfmrun",
Action: GfmrunActionFunc, Action: GfmrunActionFunc,
}, },
{
Name: "toc",
Action: TocActionFunc,
},
{ {
Name: "check-binary-size", Name: "check-binary-size",
Action: checkBinarySizeActionFunc, Action: checkBinarySizeActionFunc,
}, },
{
Name: "generate",
Action: GenerateActionFunc,
},
{
Name: "v2diff",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "color", Value: false},
},
Action: V2Diff,
},
{
Name: "v2approve",
Action: V2Approve,
},
}
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "tags",
Usage: "set build tags",
},
&cli.PathFlag{
Name: "top",
Value: top,
},
&cli.StringSliceFlag{
Name: "packages",
Value: cli.NewStringSlice("cli", "altsrc", "internal/build", "internal/genflags"),
},
} }
err := app.Run(os.Args) if err := app.Run(os.Args); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func sh(exe string, args ...string) (string, error) {
cmd := exec.Command(exe, args...)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd)
outBytes, err := cmd.Output()
return string(outBytes), err
}
func runCmd(arg string, args ...string) error { func runCmd(arg string, args ...string) error {
cmd := exec.Command(arg, args...) cmd := exec.Command(arg, args...)
@ -60,80 +120,102 @@ func runCmd(arg string, args ...string) error {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd)
return cmd.Run() return cmd.Run()
} }
func VetActionFunc(_ *cli.Context) error { func VetActionFunc(cCtx *cli.Context) error {
return runCmd("go", "vet") return runCmd("go", "vet", cCtx.Path("top")+"/...")
} }
func TestActionFunc(c *cli.Context) error { func TestActionFunc(c *cli.Context) error {
for _, pkg := range packages { tags := c.String("tags")
var packageName string
if pkg == "cli" { for _, pkg := range c.StringSlice("packages") {
packageName = "github.com/urfave/cli/v2" packageName := "github.com/urfave/cli/v2"
} else {
if pkg != "cli" {
packageName = fmt.Sprintf("github.com/urfave/cli/v2/%s", pkg) packageName = fmt.Sprintf("github.com/urfave/cli/v2/%s", pkg)
} }
coverProfile := fmt.Sprintf("--coverprofile=%s.coverprofile", pkg) if err := runCmd(
"go", "test",
err := runCmd("go", "test", "-v", coverProfile, packageName) "-tags", tags,
if err != nil { "-v",
"--coverprofile", pkg+".coverprofile",
"--covermode", "count",
"--cover", packageName,
packageName,
); err != nil {
return err return err
} }
} }
return testCleanup() return testCleanup(c.StringSlice("packages"))
} }
func testCleanup() error { func testCleanup(packages []string) error {
var out bytes.Buffer out := &bytes.Buffer{}
fmt.Fprintf(out, "mode: count\n")
for _, pkg := range packages { for _, pkg := range packages {
file, err := os.Open(fmt.Sprintf("%s.coverprofile", pkg)) filename := pkg + ".coverprofile"
lineBytes, err := os.ReadFile(filename)
if err != nil { if err != nil {
return err return err
} }
b, err := ioutil.ReadAll(file) lines := strings.Split(string(lineBytes), "\n")
if err != nil {
return err
}
out.Write(b) fmt.Fprintf(out, strings.Join(lines[1:], "\n"))
err = file.Close()
if err != nil {
return err
}
err = os.Remove(fmt.Sprintf("%s.coverprofile", pkg)) if err := os.Remove(filename); err != nil {
if err != nil {
return err return err
} }
} }
outFile, err := os.Create("coverage.txt") return os.WriteFile("coverage.txt", out.Bytes(), 0644)
}
func GfmrunActionFunc(cCtx *cli.Context) error {
top := cCtx.Path("top")
bash, err := exec.LookPath("bash")
if err != nil { if err != nil {
return err return err
} }
_, err = out.WriteTo(outFile) os.Setenv("SHELL", bash)
tmpDir, err := os.MkdirTemp("", "urfave-cli*")
if err != nil { if err != nil {
return err return err
} }
err = outFile.Close() wd, err := os.Getwd()
if err != nil { if err != nil {
return err return err
} }
return nil if err := os.Chdir(tmpDir); err != nil {
return err
} }
func GfmrunActionFunc(c *cli.Context) error { fmt.Fprintf(cCtx.App.ErrWriter, "# ---> workspace/TMPDIR is %q\n", tmpDir)
filename := c.Args().Get(0)
if err := runCmd("go", "work", "init", top); err != nil {
return err
}
os.Setenv("TMPDIR", tmpDir)
if err := os.Chdir(wd); err != nil {
return err
}
filename := cCtx.Args().Get(0)
if filename == "" { if filename == "" {
filename = "README.md" filename = "README.md"
} }
@ -162,26 +244,11 @@ func GfmrunActionFunc(c *cli.Context) error {
return err return err
} }
return runCmd("gfmrun", "-c", fmt.Sprint(counter), "-s", filename) if err := runCmd("gfmrun", "-c", fmt.Sprint(counter), "-s", filename); err != nil {
}
func TocActionFunc(c *cli.Context) error {
filename := c.Args().Get(0)
if filename == "" {
filename = "README.md"
}
err := runCmd("markdown-toc", "-i", filename)
if err != nil {
return err return err
} }
err = runCmd("git", "diff", "--exit-code") return os.RemoveAll(tmpDir)
if err != nil {
return err
}
return nil
} }
// checkBinarySizeActionFunc checks the size of an example binary to ensure that we are keeping size down // checkBinarySizeActionFunc checks the size of an example binary to ensure that we are keeping size down
@ -193,22 +260,26 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
cliBuiltFilePath = "./internal/example-cli/built-example" cliBuiltFilePath = "./internal/example-cli/built-example"
helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go" helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go"
helloBuiltFilePath = "./internal/example-hello-world/built-example" helloBuiltFilePath = "./internal/example-hello-world/built-example"
desiredMinBinarySize = 1.9
desiredMaxBinarySize = 2.2 desiredMaxBinarySize = 2.2
badNewsEmoji = "🚨"
goodNewsEmoji = "✨"
checksPassedEmoji = "✅"
mbStringFormatter = "%.1fMB" mbStringFormatter = "%.1fMB"
) )
desiredMinBinarySize := 1.675
tags := c.String("tags")
if strings.Contains(tags, "urfave_cli_no_docs") {
desiredMinBinarySize = 1.39
}
// get cli example size // get cli example size
cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath) cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath, tags)
if err != nil { if err != nil {
return err return err
} }
// get hello world size // get hello world size
helloSize, err := getSize(helloSourceFilePath, helloBuiltFilePath) helloSize, err := getSize(helloSourceFilePath, helloBuiltFilePath, tags)
if err != nil { if err != nil {
return err return err
} }
@ -270,9 +341,72 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
return nil return nil
} }
func getSize(sourcePath string, builtPath string) (size int64, err error) { func GenerateActionFunc(cCtx *cli.Context) error {
top := cCtx.Path("top")
cliDocs, err := sh("go", "doc", "-all", top)
if err != nil {
return err
}
altsrcDocs, err := sh("go", "doc", "-all", filepath.Join(top, "altsrc"))
if err != nil {
return err
}
if err := os.WriteFile(
filepath.Join(top, "godoc-current.txt"),
[]byte(cliDocs+altsrcDocs),
0644,
); err != nil {
return err
}
return runCmd("go", "generate", cCtx.Path("top")+"/...")
}
func V2Diff(cCtx *cli.Context) error {
os.Chdir(cCtx.Path("top"))
err := runCmd(
"diff",
"--ignore-all-space",
"--minimal",
"--color="+func() string {
if cCtx.Bool("color") {
return "always"
}
return "auto"
}(),
"--unified",
"--label=a/godoc",
filepath.Join("testdata", "godoc-v2.x.txt"),
"--label=b/godoc",
"godoc-current.txt",
)
if err != nil {
fmt.Printf("# %v ---> Hey! <---\n", badNewsEmoji)
fmt.Println(strings.TrimSpace(v2diffWarning))
}
return err
}
func V2Approve(cCtx *cli.Context) error {
top := cCtx.Path("top")
return runCmd(
"cp",
"-v",
filepath.Join(top, "godoc-current.txt"),
filepath.Join(top, "testdata", "godoc-v2.x.txt"),
)
}
func getSize(sourcePath string, builtPath string, tags string) (size int64, err error) {
// build example binary // build example binary
err = runCmd("go", "build", "-o", builtPath, "-ldflags", "-s -w", sourcePath) err = runCmd("go", "build", "-tags", tags, "-o", builtPath, "-ldflags", "-s -w", sourcePath)
if err != nil { if err != nil {
fmt.Println("issue getting size for example binary") fmt.Println("issue getting size for example binary")
return 0, err return 0, err

View File

@ -7,5 +7,5 @@ import (
) )
func main() { func main() {
(&cli.App{}).Run([]string{}) (&cli.App{}).Run([]string{""})
} }

View File

@ -0,0 +1,163 @@
package main
import (
"bytes"
"context"
_ "embed"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"text/template"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/internal/genflags"
"gopkg.in/yaml.v3"
)
const (
defaultPackageName = "cli"
)
func sh(ctx context.Context, exe string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, exe, args...)
cmd.Stderr = os.Stderr
outBytes, err := cmd.Output()
return string(outBytes), err
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
top := "../../"
if v, err := sh(ctx, "git", "rev-parse", "--show-toplevel"); err == nil {
top = strings.TrimSpace(v)
}
app := &cli.App{
Name: "genflags",
Usage: "Generate flag types for urfave/cli",
Flags: []cli.Flag{
&cli.PathFlag{
Name: "flag-spec-yaml",
Aliases: []string{"f"},
Value: filepath.Join(top, "flag-spec.yaml"),
},
&cli.PathFlag{
Name: "generated-output",
Aliases: []string{"o"},
Value: filepath.Join(top, "zz_generated.flags.go"),
},
&cli.PathFlag{
Name: "generated-test-output",
Aliases: []string{"t"},
Value: filepath.Join(top, "zz_generated.flags_test.go"),
},
&cli.StringFlag{
Name: "generated-package-name",
Aliases: []string{"p"},
Value: defaultPackageName,
},
&cli.StringFlag{
Name: "generated-test-package-name",
Aliases: []string{"T"},
Value: defaultPackageName + "_test",
},
&cli.StringFlag{
Name: "urfave-cli-namespace",
Aliases: []string{"n"},
Value: "",
},
&cli.StringFlag{
Name: "urfave-cli-test-namespace",
Aliases: []string{"N"},
Value: "cli.",
},
},
Action: runGenFlags,
}
if err := app.RunContext(ctx, os.Args); err != nil {
log.Fatal(err)
}
}
func runGenFlags(cCtx *cli.Context) error {
specBytes, err := os.ReadFile(cCtx.Path("flag-spec-yaml"))
if err != nil {
return err
}
spec := &genflags.Spec{}
if err := yaml.Unmarshal(specBytes, spec); err != nil {
return err
}
if cCtx.IsSet("generated-package-name") {
spec.PackageName = strings.TrimSpace(cCtx.String("generated-package-name"))
}
if strings.TrimSpace(spec.PackageName) == "" {
spec.PackageName = defaultPackageName
}
if cCtx.IsSet("generated-test-package-name") {
spec.TestPackageName = strings.TrimSpace(cCtx.String("generated-test-package-name"))
}
if strings.TrimSpace(spec.TestPackageName) == "" {
spec.TestPackageName = defaultPackageName + "_test"
}
if cCtx.IsSet("urfave-cli-namespace") {
spec.UrfaveCLINamespace = strings.TrimSpace(cCtx.String("urfave-cli-namespace"))
}
if cCtx.IsSet("urfave-cli-test-namespace") {
spec.UrfaveCLITestNamespace = strings.TrimSpace(cCtx.String("urfave-cli-test-namespace"))
} else {
spec.UrfaveCLITestNamespace = "cli."
}
genTmpl, err := template.New("gen").Parse(genflags.TemplateString)
if err != nil {
return err
}
genTestTmpl, err := template.New("gen_test").Parse(genflags.TestTemplateString)
if err != nil {
return err
}
genBuf := &bytes.Buffer{}
if err := genTmpl.Execute(genBuf, spec); err != nil {
return err
}
genTestBuf := &bytes.Buffer{}
if err := genTestTmpl.Execute(genTestBuf, spec); err != nil {
return err
}
if err := os.WriteFile(cCtx.Path("generated-output"), genBuf.Bytes(), 0644); err != nil {
return err
}
if err := os.WriteFile(cCtx.Path("generated-test-output"), genTestBuf.Bytes(), 0644); err != nil {
return err
}
if _, err := sh(cCtx.Context, "goimports", "-w", cCtx.Path("generated-output")); err != nil {
return err
}
if _, err := sh(cCtx.Context, "goimports", "-w", cCtx.Path("generated-test-output")); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,68 @@
// WARNING: this file is generated. DO NOT EDIT
package {{.PackageName}}
{{range .SortedFlagTypes}}
// {{.TypeName}} is a flag with type {{if .ValuePointer}}*{{end}}{{.GoType}}
type {{.TypeName}} struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value {{if .ValuePointer}}*{{end}}{{.GoType}}
Destination *{{.GoType}}
Aliases []string
EnvVars []string
{{range .StructFields}}
{{.Name}} {{.Type}}
{{end}}
}
{{if .GenerateFmtStringerInterface}}
// String returns a readable representation of this value (for usage defaults)
func (f *{{.TypeName}}) String() string {
return {{$.UrfaveCLINamespace}}FlagStringer(f)
}
{{end}}{{/* /if .GenerateFmtStringerInterface */}}
{{if .GenerateFlagInterface}}
// IsSet returns whether or not the flag has been set through env or file
func (f *{{.TypeName}}) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *{{.TypeName}}) Names() []string {
return {{$.UrfaveCLINamespace}}FlagNames(f.Name, f.Aliases)
}
{{end}}{{/* /if .GenerateFlagInterface */}}
{{if .GenerateRequiredFlagInterface}}
// IsRequired returns whether or not the flag is required
func (f *{{.TypeName}}) IsRequired() bool {
return f.Required
}
{{end}}{{/* /if .GenerateRequiredFlagInterface */}}
{{if .GenerateVisibleFlagInterface}}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *{{.TypeName}}) IsVisible() bool {
return !f.Hidden
}
{{end}}{{/* /if .GenerateVisibleFlagInterface */}}
{{end}}{{/* /range .SortedFlagTypes */}}
// vim{{/* 👻 */}}:ro
{{/*
vim:filetype=gotexttmpl
*/}}

View File

@ -0,0 +1,43 @@
// WARNING: this file is generated. DO NOT EDIT
package {{.TestPackageName}}
{{range .SortedFlagTypes}}
{{if .GenerateFlagInterface}}
func Test{{.TypeName}}_SatisfiesFlagInterface(t *testing.T) {
var f {{$.UrfaveCLITestNamespace}}Flag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.IsSet()
_ = f.Names()
}
{{end}}
{{if .GenerateFmtStringerInterface}}
func Test{{.TypeName}}_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.String()
}
{{end}}
{{if .GenerateRequiredFlagInterface}}
func Test{{.TypeName}}_SatisfiesRequiredFlagInterface(t *testing.T) {
var f {{$.UrfaveCLITestNamespace}}RequiredFlag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.IsRequired()
}
{{end}}
{{if .GenerateVisibleFlagInterface}}
func Test{{.TypeName}}_SatisfiesVisibleFlagInterface(t *testing.T) {
var f {{$.UrfaveCLITestNamespace}}VisibleFlag = &{{$.UrfaveCLITestNamespace}}{{.TypeName}}{}
_ = f.IsVisible()
}
{{end}}
{{end}}
// vim{{/* 👻 */}}:ro
{{/*
vim:filetype=gotexttmpl
*/}}

View File

@ -0,0 +1,34 @@
package genflags
import (
_ "embed"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
//go:embed generated.gotmpl
TemplateString string
//go:embed generated_test.gotmpl
TestTemplateString string
titler = cases.Title(language.Und, cases.NoLower)
)
func TypeName(goType string, fc *FlagTypeConfig) string {
if fc != nil && strings.TrimSpace(fc.TypeName) != "" {
return strings.TrimSpace(fc.TypeName)
}
dotSplit := strings.Split(goType, ".")
goType = dotSplit[len(dotSplit)-1]
if strings.HasPrefix(goType, "[]") {
return titler.String(strings.TrimPrefix(goType, "[]")) + "SliceFlag"
}
return titler.String(goType) + "Flag"
}

View File

@ -0,0 +1,41 @@
package genflags_test
import (
"fmt"
"testing"
"github.com/urfave/cli/v2/internal/genflags"
)
func TestTypeName(t *testing.T) {
for _, tc := range []struct {
gt string
fc *genflags.FlagTypeConfig
expected string
}{
{gt: "int", fc: nil, expected: "IntFlag"},
{gt: "int", fc: &genflags.FlagTypeConfig{}, expected: "IntFlag"},
{gt: "int", fc: &genflags.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"},
{gt: "[]bool", fc: nil, expected: "BoolSliceFlag"},
{gt: "[]bool", fc: &genflags.FlagTypeConfig{}, expected: "BoolSliceFlag"},
{gt: "[]bool", fc: &genflags.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"},
{gt: "time.Rumination", fc: nil, expected: "RuminationFlag"},
{gt: "time.Rumination", fc: &genflags.FlagTypeConfig{}, expected: "RuminationFlag"},
{gt: "time.Rumination", fc: &genflags.FlagTypeConfig{TypeName: "PonderFlag"}, expected: "PonderFlag"},
} {
t.Run(
fmt.Sprintf("type=%s,cfg=%v", tc.gt, func() string {
if tc.fc != nil {
return tc.fc.TypeName
}
return "nil"
}()),
func(ct *testing.T) {
actual := genflags.TypeName(tc.gt, tc.fc)
if tc.expected != actual {
ct.Errorf("expected %q, got %q", tc.expected, actual)
}
},
)
}
}

108
internal/genflags/spec.go Normal file
View File

@ -0,0 +1,108 @@
package genflags
import (
"sort"
"strings"
)
type Spec struct {
FlagTypes map[string]*FlagTypeConfig `yaml:"flag_types"`
PackageName string `yaml:"package_name"`
TestPackageName string `yaml:"test_package_name"`
UrfaveCLINamespace string `yaml:"urfave_cli_namespace"`
UrfaveCLITestNamespace string `yaml:"urfave_cli_test_namespace"`
}
func (gfs *Spec) SortedFlagTypes() []*FlagType {
typeNames := []string{}
for name := range gfs.FlagTypes {
if strings.HasPrefix(name, "[]") {
name = strings.TrimPrefix(name, "[]") + "Slice"
}
typeNames = append(typeNames, name)
}
sort.Strings(typeNames)
ret := make([]*FlagType, len(typeNames))
for i, typeName := range typeNames {
ret[i] = &FlagType{
GoType: typeName,
Config: gfs.FlagTypes[typeName],
}
}
return ret
}
type FlagTypeConfig struct {
SkipInterfaces []string `yaml:"skip_interfaces"`
StructFields []*FlagStructField `yaml:"struct_fields"`
TypeName string `yaml:"type_name"`
ValuePointer bool `yaml:"value_pointer"`
}
type FlagStructField struct {
Name string
Type string
}
type FlagType struct {
GoType string
Config *FlagTypeConfig
}
func (ft *FlagType) StructFields() []*FlagStructField {
if ft.Config == nil || ft.Config.StructFields == nil {
return []*FlagStructField{}
}
return ft.Config.StructFields
}
func (ft *FlagType) ValuePointer() bool {
if ft.Config == nil {
return false
}
return ft.Config.ValuePointer
}
func (ft *FlagType) TypeName() string {
return TypeName(ft.GoType, ft.Config)
}
func (ft *FlagType) GenerateFmtStringerInterface() bool {
return ft.skipInterfaceNamed("fmt.Stringer")
}
func (ft *FlagType) GenerateFlagInterface() bool {
return ft.skipInterfaceNamed("Flag")
}
func (ft *FlagType) GenerateRequiredFlagInterface() bool {
return ft.skipInterfaceNamed("RequiredFlag")
}
func (ft *FlagType) GenerateVisibleFlagInterface() bool {
return ft.skipInterfaceNamed("VisibleFlag")
}
func (ft *FlagType) skipInterfaceNamed(name string) bool {
if ft.Config == nil {
return true
}
lowName := strings.ToLower(name)
for _, interfaceName := range ft.Config.SkipInterfaces {
if strings.ToLower(interfaceName) == lowName {
return false
}
}
return true
}

View File

@ -0,0 +1,112 @@
package genflags_test
import (
"reflect"
"testing"
"github.com/urfave/cli/v2/internal/genflags"
)
func TestSpec_SortedFlagTypes(t *testing.T) {
spec := &genflags.Spec{
FlagTypes: map[string]*genflags.FlagTypeConfig{
"nerf": &genflags.FlagTypeConfig{},
"gerf": nil,
},
}
actual := spec.SortedFlagTypes()
expected := []*genflags.FlagType{
{
GoType: "gerf",
Config: nil,
},
{
GoType: "nerf",
Config: &genflags.FlagTypeConfig{},
},
}
if !reflect.DeepEqual(expected, actual) {
t.Errorf("expected %#v, got %#v", expected, actual)
}
}
func genFlagType() *genflags.FlagType {
return &genflags.FlagType{
GoType: "blerf",
Config: &genflags.FlagTypeConfig{
SkipInterfaces: []string{"fmt.Stringer"},
StructFields: []*genflags.FlagStructField{
{
Name: "Foibles",
Type: "int",
},
{
Name: "Hoopled",
Type: "bool",
},
},
TypeName: "YeOldeBlerfFlag",
ValuePointer: true,
},
}
}
func TestFlagType_StructFields(t *testing.T) {
ft := genFlagType()
sf := ft.StructFields()
if 2 != len(sf) {
t.Errorf("expected 2 struct fields, got %v", len(sf))
return
}
if "Foibles" != sf[0].Name {
t.Errorf("expected struct field order to be retained")
}
}
func TestFlagType_ValuePointer(t *testing.T) {
ft := genFlagType()
if !ft.ValuePointer() {
t.Errorf("expected ValuePointer to be true")
return
}
ft.Config = nil
if ft.ValuePointer() {
t.Errorf("expected ValuePointer to be false")
}
}
func TestFlagType_GenerateFmtStringerInterface(t *testing.T) {
ft := genFlagType()
if ft.GenerateFmtStringerInterface() {
t.Errorf("expected GenerateFmtStringerInterface to be false")
return
}
ft.Config = nil
if !ft.GenerateFmtStringerInterface() {
t.Errorf("expected GenerateFmtStringerInterface to be true")
}
}
func TestFlagType_GenerateFlagInterface(t *testing.T) {
ft := genFlagType()
if !ft.GenerateFlagInterface() {
t.Errorf("expected GenerateFlagInterface to be true")
return
}
ft.Config = nil
if !ft.GenerateFlagInterface() {
t.Errorf("expected GenerateFlagInterface to be true")
}
}

5
mkdocs-requirements.txt Normal file
View File

@ -0,0 +1,5 @@
mkdocs-git-revision-date-localized-plugin~=1.0
mkdocs-material-extensions~=1.0
mkdocs-material~=8.2
mkdocs~=1.3
pygments~=2.12

62
mkdocs.yml Normal file
View File

@ -0,0 +1,62 @@
# NOTE: the mkdocs dependencies will need to be installed out of
# band until this whole thing gets more automated:
#
# pip install -r mkdocs-requirements.txt
#
site_name: urfave/cli
site_url: https://cli.urfave.org/
repo_url: https://github.com/urfave/cli
edit_uri: edit/main/docs/
nav:
- Home: index.md
- v2 Manual: v2/index.md
- v1 Manual: v1/index.md
theme:
name: material
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-4
name: dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/brightness-7
name: light mode
plugins:
- git-revision-date-localized
- search
# NOTE: this is the recommended configuration from
# https://squidfunk.github.io/mkdocs-material/setup/extensions/#recommended-configuration
markdown_extensions:
- abbr
- admonition
- attr_list
- def_list
- footnotes
- meta
- md_in_html
- toc:
permalink: true
- pymdownx.arithmatex:
generic: true
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.details
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.highlight
- pymdownx.inlinehilite
- pymdownx.keys
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde

View File

@ -26,9 +26,8 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple
return err return err
} }
errStr := err.Error() trimmed, trimErr := flagFromError(err)
trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -") if trimErr != nil {
if errStr == trimmed {
return err return err
} }
@ -67,6 +66,19 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple
} }
} }
const providedButNotDefinedErrMsg = "flag provided but not defined: -"
// flagFromError tries to parse a provided flag from an error message. If the
// parsing fials, it returns the input error and an empty string
func flagFromError(err error) (string, error) {
errStr := err.Error()
trimmed := strings.TrimPrefix(errStr, providedButNotDefinedErrMsg)
if errStr == trimmed {
return "", err
}
return trimmed, nil
}
func splitShortOptions(set *flag.FlagSet, arg string) []string { func splitShortOptions(set *flag.FlagSet, arg string) []string {
shortFlagsExist := func(s string) bool { shortFlagsExist := func(s string) bool {
for _, c := range s[1:] { for _, c := range s[1:] {

293
sliceflag.go Normal file
View File

@ -0,0 +1,293 @@
//go:build go1.18
// +build go1.18
package cli
import (
"flag"
"reflect"
)
type (
// SliceFlag extends implementations like StringSliceFlag and IntSliceFlag with support for using slices directly,
// as Value and/or Destination.
// See also SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, MultiIntFlag.
SliceFlag[T SliceFlagTarget[E], S ~[]E, E any] struct {
Target T
Value S
Destination *S
}
// SliceFlagTarget models a target implementation for use with SliceFlag.
// The three methods, SetValue, SetDestination, and GetDestination, are necessary to propagate Value and
// Destination, where Value is propagated inwards (initially), and Destination is propagated outwards (on every
// update).
SliceFlagTarget[E any] interface {
Flag
RequiredFlag
DocGenerationFlag
VisibleFlag
CategorizableFlag
// SetValue should propagate the given slice to the target, ideally as a new value.
// Note that a nil slice should nil/clear any existing value (modelled as ~[]E).
SetValue(slice []E)
// SetDestination should propagate the given slice to the target, ideally as a new value.
// Note that a nil slice should nil/clear any existing value (modelled as ~*[]E).
SetDestination(slice []E)
// GetDestination should return the current value referenced by any destination, or nil if nil/unset.
GetDestination() []E
}
// MultiStringFlag extends StringSliceFlag with support for using slices directly, as Value and/or Destination.
// See also SliceFlag.
MultiStringFlag = SliceFlag[*StringSliceFlag, []string, string]
// MultiFloat64Flag extends Float64SliceFlag with support for using slices directly, as Value and/or Destination.
// See also SliceFlag.
MultiFloat64Flag = SliceFlag[*Float64SliceFlag, []float64, float64]
// MultiInt64Flag extends Int64SliceFlag with support for using slices directly, as Value and/or Destination.
// See also SliceFlag.
MultiInt64Flag = SliceFlag[*Int64SliceFlag, []int64, int64]
// MultiIntFlag extends IntSliceFlag with support for using slices directly, as Value and/or Destination.
// See also SliceFlag.
MultiIntFlag = SliceFlag[*IntSliceFlag, []int, int]
flagValueHook struct {
value Generic
hook func()
}
)
var (
// compile time assertions
_ SliceFlagTarget[string] = (*StringSliceFlag)(nil)
_ SliceFlagTarget[string] = (*SliceFlag[*StringSliceFlag, []string, string])(nil)
_ SliceFlagTarget[string] = (*MultiStringFlag)(nil)
_ SliceFlagTarget[float64] = (*MultiFloat64Flag)(nil)
_ SliceFlagTarget[int64] = (*MultiInt64Flag)(nil)
_ SliceFlagTarget[int] = (*MultiIntFlag)(nil)
_ Generic = (*flagValueHook)(nil)
_ Serializer = (*flagValueHook)(nil)
)
func (x *SliceFlag[T, S, E]) Apply(set *flag.FlagSet) error {
x.Target.SetValue(x.convertSlice(x.Value))
destination := x.Destination
if destination == nil {
x.Target.SetDestination(nil)
return x.Target.Apply(set)
}
x.Target.SetDestination(x.convertSlice(*destination))
return applyFlagValueHook(set, x.Target.Apply, func() {
*destination = x.Target.GetDestination()
})
}
func (x *SliceFlag[T, S, E]) convertSlice(slice S) []E {
result := make([]E, len(slice))
copy(result, slice)
return result
}
func (x *SliceFlag[T, S, E]) SetValue(slice S) {
x.Value = slice
}
func (x *SliceFlag[T, S, E]) SetDestination(slice S) {
if slice != nil {
x.Destination = &slice
} else {
x.Destination = nil
}
}
func (x *SliceFlag[T, S, E]) GetDestination() S {
if destination := x.Destination; destination != nil {
return *destination
}
return nil
}
func (x *SliceFlag[T, S, E]) String() string { return x.Target.String() }
func (x *SliceFlag[T, S, E]) Names() []string { return x.Target.Names() }
func (x *SliceFlag[T, S, E]) IsSet() bool { return x.Target.IsSet() }
func (x *SliceFlag[T, S, E]) IsRequired() bool { return x.Target.IsRequired() }
func (x *SliceFlag[T, S, E]) TakesValue() bool { return x.Target.TakesValue() }
func (x *SliceFlag[T, S, E]) GetUsage() string { return x.Target.GetUsage() }
func (x *SliceFlag[T, S, E]) GetValue() string { return x.Target.GetValue() }
func (x *SliceFlag[T, S, E]) GetDefaultText() string { return x.Target.GetDefaultText() }
func (x *SliceFlag[T, S, E]) GetEnvVars() []string { return x.Target.GetEnvVars() }
func (x *SliceFlag[T, S, E]) IsVisible() bool { return x.Target.IsVisible() }
func (x *SliceFlag[T, S, E]) GetCategory() string { return x.Target.GetCategory() }
func (x *flagValueHook) Set(value string) error {
if err := x.value.Set(value); err != nil {
return err
}
x.hook()
return nil
}
func (x *flagValueHook) String() string {
// note: this is necessary due to the way Go's flag package handles defaults
isZeroValue := func(f flag.Value, v string) bool {
/*
https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/flag/flag.go;drc=2580d0e08d5e9f979b943758d3c49877fb2324cb;l=453
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Build a zero value of the flag's Value type, and see if the
// result of calling its String method equals the value passed in.
// This works unless the Value type is itself an interface type.
typ := reflect.TypeOf(f)
var z reflect.Value
if typ.Kind() == reflect.Pointer {
z = reflect.New(typ.Elem())
} else {
z = reflect.Zero(typ)
}
return v == z.Interface().(flag.Value).String()
}
if x.value != nil {
// only return non-empty if not the same string as returned by the zero value
if s := x.value.String(); !isZeroValue(x.value, s) {
return s
}
}
return ``
}
func (x *flagValueHook) Serialize() string {
if value, ok := x.value.(Serializer); ok {
return value.Serialize()
}
return x.String()
}
// applyFlagValueHook wraps calls apply then wraps flags to call a hook function on update and after initial apply.
func applyFlagValueHook(set *flag.FlagSet, apply func(set *flag.FlagSet) error, hook func()) error {
if apply == nil || set == nil || hook == nil {
panic(`invalid input`)
}
var tmp flag.FlagSet
if err := apply(&tmp); err != nil {
return err
}
tmp.VisitAll(func(f *flag.Flag) { set.Var(&flagValueHook{value: f.Value, hook: hook}, f.Name, f.Usage) })
hook()
return nil
}
// newSliceFlagValue is for implementing SliceFlagTarget.SetValue and SliceFlagTarget.SetDestination.
// It's e.g. as part of StringSliceFlag.SetValue, using the factory NewStringSlice.
func newSliceFlagValue[R any, S ~[]E, E any](factory func(defaults ...E) *R, defaults S) *R {
if defaults == nil {
return nil
}
return factory(defaults...)
}
// unwrapFlagValue strips any/all *flagValueHook wrappers.
func unwrapFlagValue(v flag.Value) flag.Value {
for {
h, ok := v.(*flagValueHook)
if !ok {
return v
}
v = h.value
}
}
// NOTE: the methods below are in this file to make use of the build constraint
func (f *Float64SliceFlag) SetValue(slice []float64) {
f.Value = newSliceFlagValue(NewFloat64Slice, slice)
}
func (f *Float64SliceFlag) SetDestination(slice []float64) {
f.Destination = newSliceFlagValue(NewFloat64Slice, slice)
}
func (f *Float64SliceFlag) GetDestination() []float64 {
if destination := f.Destination; destination != nil {
return destination.Value()
}
return nil
}
func (f *Int64SliceFlag) SetValue(slice []int64) {
f.Value = newSliceFlagValue(NewInt64Slice, slice)
}
func (f *Int64SliceFlag) SetDestination(slice []int64) {
f.Destination = newSliceFlagValue(NewInt64Slice, slice)
}
func (f *Int64SliceFlag) GetDestination() []int64 {
if destination := f.Destination; destination != nil {
return destination.Value()
}
return nil
}
func (f *IntSliceFlag) SetValue(slice []int) {
f.Value = newSliceFlagValue(NewIntSlice, slice)
}
func (f *IntSliceFlag) SetDestination(slice []int) {
f.Destination = newSliceFlagValue(NewIntSlice, slice)
}
func (f *IntSliceFlag) GetDestination() []int {
if destination := f.Destination; destination != nil {
return destination.Value()
}
return nil
}
func (f *StringSliceFlag) SetValue(slice []string) {
f.Value = newSliceFlagValue(NewStringSlice, slice)
}
func (f *StringSliceFlag) SetDestination(slice []string) {
f.Destination = newSliceFlagValue(NewStringSlice, slice)
}
func (f *StringSliceFlag) GetDestination() []string {
if destination := f.Destination; destination != nil {
return destination.Value()
}
return nil
}

10
sliceflag_pre18.go Normal file
View File

@ -0,0 +1,10 @@
//go:build !go1.18
// +build !go1.18
package cli
import (
"flag"
)
func unwrapFlagValue(v flag.Value) flag.Value { return v }

1005
sliceflag_test.go Normal file

File diff suppressed because it is too large Load Diff

60
suggestions.go Normal file
View File

@ -0,0 +1,60 @@
package cli
import (
"fmt"
"github.com/xrash/smetrics"
)
func jaroWinkler(a, b string) float64 {
// magic values are from https://github.com/xrash/smetrics/blob/039620a656736e6ad994090895784a7af15e0b80/jaro-winkler.go#L8
const (
boostThreshold = 0.7
prefixSize = 4
)
return smetrics.JaroWinkler(a, b, boostThreshold, prefixSize)
}
func suggestFlag(flags []Flag, provided string, hideHelp bool) string {
distance := 0.0
suggestion := ""
for _, flag := range flags {
flagNames := flag.Names()
if !hideHelp {
flagNames = append(flagNames, HelpFlag.Names()...)
}
for _, name := range flagNames {
newDistance := jaroWinkler(name, provided)
if newDistance > distance {
distance = newDistance
suggestion = name
}
}
}
if len(suggestion) == 1 {
suggestion = "-" + suggestion
} else if len(suggestion) > 1 {
suggestion = "--" + suggestion
}
return suggestion
}
// suggestCommand takes a list of commands and a provided string to suggest a
// command name
func suggestCommand(commands []*Command, provided string) (suggestion string) {
distance := 0.0
for _, command := range commands {
for _, name := range append(command.Names(), helpName, helpAlias) {
newDistance := jaroWinkler(name, provided)
if newDistance > distance {
distance = newDistance
suggestion = name
}
}
}
return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion)
}

187
suggestions_test.go Normal file
View File

@ -0,0 +1,187 @@
package cli
import (
"errors"
"fmt"
"testing"
)
func TestSuggestFlag(t *testing.T) {
// Given
app := testApp()
for _, testCase := range []struct {
provided, expected string
}{
{"", ""},
{"a", "--another-flag"},
{"hlp", "--help"},
{"k", ""},
{"s", "-s"},
} {
// When
res := suggestFlag(app.Flags, testCase.provided, false)
// Then
expect(t, res, testCase.expected)
}
}
func TestSuggestFlagHideHelp(t *testing.T) {
// Given
app := testApp()
// When
res := suggestFlag(app.Flags, "hlp", true)
// Then
expect(t, res, "--fl")
}
func TestSuggestFlagFromError(t *testing.T) {
// Given
app := testApp()
for _, testCase := range []struct {
command, provided, expected string
}{
{"", "hel", "--help"},
{"", "soccer", "--socket"},
{"config", "anot", "--another-flag"},
} {
// When
res, _ := app.suggestFlagFromError(
errors.New(providedButNotDefinedErrMsg+testCase.provided),
testCase.command,
)
// Then
expect(t, res, fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", testCase.expected))
}
}
func TestSuggestFlagFromErrorWrongError(t *testing.T) {
// Given
app := testApp()
// When
_, err := app.suggestFlagFromError(errors.New("invalid"), "")
// Then
expect(t, true, err != nil)
}
func TestSuggestFlagFromErrorWrongCommand(t *testing.T) {
// Given
app := testApp()
// When
_, err := app.suggestFlagFromError(
errors.New(providedButNotDefinedErrMsg+"flag"),
"invalid",
)
// Then
expect(t, true, err != nil)
}
func TestSuggestFlagFromErrorNoSuggestion(t *testing.T) {
// Given
app := testApp()
// When
_, err := app.suggestFlagFromError(
errors.New(providedButNotDefinedErrMsg+""),
"",
)
// Then
expect(t, true, err != nil)
}
func TestSuggestCommand(t *testing.T) {
// Given
app := testApp()
for _, testCase := range []struct {
provided, expected string
}{
{"", ""},
{"conf", "config"},
{"i", "i"},
{"information", "info"},
{"not-existing", "info"},
} {
// When
res := suggestCommand(app.Commands, testCase.provided)
// Then
expect(t, res, fmt.Sprintf(SuggestDidYouMeanTemplate, testCase.expected))
}
}
func ExampleApp_Suggest() {
app := &App{
Name: "greet",
Suggest: true,
HideHelp: true,
HideHelpCommand: true,
CustomAppHelpTemplate: "(this space intentionally left blank)\n",
Flags: []Flag{
&StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"},
},
Action: func(cCtx *Context) error {
fmt.Printf("Hello %v\n", cCtx.String("name"))
return nil
},
}
app.Run([]string{"greet", "--nema", "chipmunk"})
// Output:
// Incorrect Usage. flag provided but not defined: -nema
//
// Did you mean "--name"?
//
// (this space intentionally left blank)
}
func ExampleApp_Suggest_command() {
app := &App{
Name: "greet",
Suggest: true,
HideHelp: true,
HideHelpCommand: true,
CustomAppHelpTemplate: "(this space intentionally left blank)\n",
Flags: []Flag{
&StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"},
},
Action: func(cCtx *Context) error {
fmt.Printf("Hello %v\n", cCtx.String("name"))
return nil
},
Commands: []*Command{
{
Name: "neighbors",
CustomHelpTemplate: "(this space intentionally left blank)\n",
Flags: []Flag{
&BoolFlag{Name: "smiling"},
},
Action: func(cCtx *Context) error {
if cCtx.Bool("smiling") {
fmt.Println("😀")
}
fmt.Println("Hello, neighbors")
return nil
},
},
},
}
app.Run([]string{"greet", "neighbors", "--sliming"})
// Output:
// Incorrect Usage: flag provided but not defined: -sliming
//
// Did you mean "--smiling"?
//
// (this space intentionally left blank)
}

View File

@ -4,16 +4,16 @@ package cli
// cli.go uses text/template to render templates. You can // cli.go uses text/template to render templates. You can
// render custom help text by setting this variable. // render custom help text by setting this variable.
var AppHelpTemplate = `NAME: var AppHelpTemplate = `NAME:
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}} {{$v := offset .Name 6}}{{wrap .Name 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}
USAGE: USAGE:
{{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}} {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
VERSION: VERSION:
{{.Version}}{{end}}{{end}}{{if .Description}} {{.Version}}{{end}}{{end}}{{if .Description}}
DESCRIPTION: DESCRIPTION:
{{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} {{wrap .Description 3}}{{end}}{{if len .Authors}}
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{range $index, $author := .Authors}}{{if $index}} {{range $index, $author := .Authors}}{{if $index}}
@ -22,34 +22,44 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
COMMANDS:{{range .VisibleCategories}}{{if .Name}} COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}} {{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}}
GLOBAL OPTIONS:{{range .VisibleFlagCategories}}
{{if .Name}}{{.Name}}
{{end}}{{range .Flags}}{{.}}
{{end}}{{end}}{{else}}{{if .VisibleFlags}}
GLOBAL OPTIONS: GLOBAL OPTIONS:
{{range $index, $option := .VisibleFlags}}{{if $index}} {{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} {{end}}{{wrap $option.String 6}}{{end}}{{end}}{{end}}{{if .Copyright}}
COPYRIGHT: COPYRIGHT:
{{.Copyright}}{{end}} {{wrap .Copyright 3}}{{end}}
` `
// CommandHelpTemplate is the text template for the command help topic. // CommandHelpTemplate is the text template for the command help topic.
// cli.go uses text/template to render templates. You can // cli.go uses text/template to render templates. You can
// render custom help text by setting this variable. // render custom help text by setting this variable.
var CommandHelpTemplate = `NAME: var CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}} {{$v := offset .HelpName 6}}{{wrap .HelpName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}
USAGE: USAGE:
{{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY: CATEGORY:
{{.Category}}{{end}}{{if .Description}} {{.Category}}{{end}}{{if .Description}}
DESCRIPTION: DESCRIPTION:
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlags}} {{wrap .Description 3}}{{end}}{{if .VisibleFlagCategories}}
OPTIONS:{{range .VisibleFlagCategories}}
{{if .Name}}{{.Name}}
{{end}}{{range .Flags}}{{.}}
{{end}}{{end}}{{else}}{{if .VisibleFlags}}
OPTIONS: OPTIONS:
{{range .VisibleFlags}}{{.}} {{range .VisibleFlags}}{{.}}
{{end}}{{end}} {{end}}{{end}}{{end}}
` `
// SubcommandHelpTemplate is the text template for the subcommand help topic. // SubcommandHelpTemplate is the text template for the subcommand help topic.
@ -59,10 +69,10 @@ var SubcommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}} {{.HelpName}} - {{.Usage}}
USAGE: USAGE:
{{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
DESCRIPTION: DESCRIPTION:
{{.Description | nindent 3 | trim}}{{end}} {{wrap .Description 3}}{{end}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}} COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}} {{.Name}}:{{range .VisibleCommands}}

1
testdata/empty.yml vendored Normal file
View File

@ -0,0 +1 @@
# empty file

2318
testdata/godoc-v2.x.txt vendored Normal file

File diff suppressed because it is too large Load Diff

674
zz_generated.flags.go Normal file
View File

@ -0,0 +1,674 @@
// WARNING: this file is generated. DO NOT EDIT
package cli
import "time"
// Float64SliceFlag is a flag with type *Float64Slice
type Float64SliceFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value *Float64Slice
Destination *Float64Slice
Aliases []string
EnvVars []string
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Float64SliceFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *Float64SliceFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Float64SliceFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Float64SliceFlag) IsVisible() bool {
return !f.Hidden
}
// GenericFlag is a flag with type Generic
type GenericFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value Generic
Destination *Generic
Aliases []string
EnvVars []string
TakesFile bool
}
// String returns a readable representation of this value (for usage defaults)
func (f *GenericFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *GenericFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *GenericFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *GenericFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *GenericFlag) IsVisible() bool {
return !f.Hidden
}
// Int64SliceFlag is a flag with type *Int64Slice
type Int64SliceFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value *Int64Slice
Destination *Int64Slice
Aliases []string
EnvVars []string
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Int64SliceFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *Int64SliceFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Int64SliceFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Int64SliceFlag) IsVisible() bool {
return !f.Hidden
}
// IntSliceFlag is a flag with type *IntSlice
type IntSliceFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value *IntSlice
Destination *IntSlice
Aliases []string
EnvVars []string
}
// IsSet returns whether or not the flag has been set through env or file
func (f *IntSliceFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *IntSliceFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *IntSliceFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *IntSliceFlag) IsVisible() bool {
return !f.Hidden
}
// PathFlag is a flag with type Path
type PathFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value Path
Destination *Path
Aliases []string
EnvVars []string
TakesFile bool
}
// String returns a readable representation of this value (for usage defaults)
func (f *PathFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *PathFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *PathFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *PathFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *PathFlag) IsVisible() bool {
return !f.Hidden
}
// StringSliceFlag is a flag with type *StringSlice
type StringSliceFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value *StringSlice
Destination *StringSlice
Aliases []string
EnvVars []string
TakesFile bool
}
// IsSet returns whether or not the flag has been set through env or file
func (f *StringSliceFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *StringSliceFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *StringSliceFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *StringSliceFlag) IsVisible() bool {
return !f.Hidden
}
// TimestampFlag is a flag with type *Timestamp
type TimestampFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value *Timestamp
Destination *Timestamp
Aliases []string
EnvVars []string
Layout string
Timezone *time.Location
}
// String returns a readable representation of this value (for usage defaults)
func (f *TimestampFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *TimestampFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *TimestampFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *TimestampFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *TimestampFlag) IsVisible() bool {
return !f.Hidden
}
// BoolFlag is a flag with type bool
type BoolFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value bool
Destination *bool
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *BoolFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *BoolFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *BoolFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *BoolFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *BoolFlag) IsVisible() bool {
return !f.Hidden
}
// Float64Flag is a flag with type float64
type Float64Flag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value float64
Destination *float64
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *Float64Flag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Float64Flag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *Float64Flag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Float64Flag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Float64Flag) IsVisible() bool {
return !f.Hidden
}
// IntFlag is a flag with type int
type IntFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value int
Destination *int
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *IntFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *IntFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *IntFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *IntFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *IntFlag) IsVisible() bool {
return !f.Hidden
}
// Int64Flag is a flag with type int64
type Int64Flag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value int64
Destination *int64
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *Int64Flag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Int64Flag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *Int64Flag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Int64Flag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Int64Flag) IsVisible() bool {
return !f.Hidden
}
// StringFlag is a flag with type string
type StringFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value string
Destination *string
Aliases []string
EnvVars []string
TakesFile bool
}
// String returns a readable representation of this value (for usage defaults)
func (f *StringFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *StringFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *StringFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *StringFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *StringFlag) IsVisible() bool {
return !f.Hidden
}
// DurationFlag is a flag with type time.Duration
type DurationFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value time.Duration
Destination *time.Duration
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *DurationFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *DurationFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *DurationFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *DurationFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *DurationFlag) IsVisible() bool {
return !f.Hidden
}
// UintFlag is a flag with type uint
type UintFlag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value uint
Destination *uint
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *UintFlag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *UintFlag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *UintFlag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *UintFlag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *UintFlag) IsVisible() bool {
return !f.Hidden
}
// Uint64Flag is a flag with type uint64
type Uint64Flag struct {
Name string
Category string
DefaultText string
FilePath string
Usage string
Required bool
Hidden bool
HasBeenSet bool
Value uint64
Destination *uint64
Aliases []string
EnvVars []string
}
// String returns a readable representation of this value (for usage defaults)
func (f *Uint64Flag) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *Uint64Flag) IsSet() bool {
return f.HasBeenSet
}
// Names returns the names of the flag
func (f *Uint64Flag) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *Uint64Flag) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *Uint64Flag) IsVisible() bool {
return !f.Hidden
}
// vim:ro

363
zz_generated.flags_test.go Normal file
View File

@ -0,0 +1,363 @@
// WARNING: this file is generated. DO NOT EDIT
package cli_test
import (
"fmt"
"testing"
"github.com/urfave/cli/v2"
)
func TestFloat64SliceFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.Float64SliceFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestFloat64SliceFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.Float64SliceFlag{}
_ = f.IsRequired()
}
func TestFloat64SliceFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.Float64SliceFlag{}
_ = f.IsVisible()
}
func TestGenericFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.GenericFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestGenericFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.GenericFlag{}
_ = f.String()
}
func TestGenericFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.GenericFlag{}
_ = f.IsRequired()
}
func TestGenericFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.GenericFlag{}
_ = f.IsVisible()
}
func TestInt64SliceFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.Int64SliceFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestInt64SliceFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.Int64SliceFlag{}
_ = f.IsRequired()
}
func TestInt64SliceFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.Int64SliceFlag{}
_ = f.IsVisible()
}
func TestIntSliceFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.IntSliceFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestIntSliceFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.IntSliceFlag{}
_ = f.IsRequired()
}
func TestIntSliceFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.IntSliceFlag{}
_ = f.IsVisible()
}
func TestPathFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.PathFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestPathFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.PathFlag{}
_ = f.String()
}
func TestPathFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.PathFlag{}
_ = f.IsRequired()
}
func TestPathFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.PathFlag{}
_ = f.IsVisible()
}
func TestStringSliceFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.StringSliceFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestStringSliceFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.StringSliceFlag{}
_ = f.IsRequired()
}
func TestStringSliceFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.StringSliceFlag{}
_ = f.IsVisible()
}
func TestTimestampFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.TimestampFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestTimestampFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.TimestampFlag{}
_ = f.String()
}
func TestTimestampFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.TimestampFlag{}
_ = f.IsRequired()
}
func TestTimestampFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.TimestampFlag{}
_ = f.IsVisible()
}
func TestBoolFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.BoolFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestBoolFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.BoolFlag{}
_ = f.String()
}
func TestBoolFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.BoolFlag{}
_ = f.IsRequired()
}
func TestBoolFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.BoolFlag{}
_ = f.IsVisible()
}
func TestFloat64Flag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.Float64Flag{}
_ = f.IsSet()
_ = f.Names()
}
func TestFloat64Flag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.Float64Flag{}
_ = f.String()
}
func TestFloat64Flag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.Float64Flag{}
_ = f.IsRequired()
}
func TestFloat64Flag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.Float64Flag{}
_ = f.IsVisible()
}
func TestIntFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.IntFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestIntFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.IntFlag{}
_ = f.String()
}
func TestIntFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.IntFlag{}
_ = f.IsRequired()
}
func TestIntFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.IntFlag{}
_ = f.IsVisible()
}
func TestInt64Flag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.Int64Flag{}
_ = f.IsSet()
_ = f.Names()
}
func TestInt64Flag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.Int64Flag{}
_ = f.String()
}
func TestInt64Flag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.Int64Flag{}
_ = f.IsRequired()
}
func TestInt64Flag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.Int64Flag{}
_ = f.IsVisible()
}
func TestStringFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.StringFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestStringFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.StringFlag{}
_ = f.String()
}
func TestStringFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.StringFlag{}
_ = f.IsRequired()
}
func TestStringFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.StringFlag{}
_ = f.IsVisible()
}
func TestDurationFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.DurationFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestDurationFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.DurationFlag{}
_ = f.String()
}
func TestDurationFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.DurationFlag{}
_ = f.IsRequired()
}
func TestDurationFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.DurationFlag{}
_ = f.IsVisible()
}
func TestUintFlag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.UintFlag{}
_ = f.IsSet()
_ = f.Names()
}
func TestUintFlag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.UintFlag{}
_ = f.String()
}
func TestUintFlag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.UintFlag{}
_ = f.IsRequired()
}
func TestUintFlag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.UintFlag{}
_ = f.IsVisible()
}
func TestUint64Flag_SatisfiesFlagInterface(t *testing.T) {
var f cli.Flag = &cli.Uint64Flag{}
_ = f.IsSet()
_ = f.Names()
}
func TestUint64Flag_SatisfiesFmtStringerInterface(t *testing.T) {
var f fmt.Stringer = &cli.Uint64Flag{}
_ = f.String()
}
func TestUint64Flag_SatisfiesRequiredFlagInterface(t *testing.T) {
var f cli.RequiredFlag = &cli.Uint64Flag{}
_ = f.IsRequired()
}
func TestUint64Flag_SatisfiesVisibleFlagInterface(t *testing.T) {
var f cli.VisibleFlag = &cli.Uint64Flag{}
_ = f.IsVisible()
}
// vim:ro