Merge remote-tracking branch 'origin/main' into applying-pr1218

This commit is contained in:
Dan Buch 2022-04-30 14:15:48 -04:00
commit 56837b07d3
Signed by: meatballhat
GPG Key ID: A12F782281063434
56 changed files with 1655 additions and 703 deletions

View File

@ -1,12 +1,10 @@
--- ---
name: ask a question name: ask a question
about: ask us question - assume stackoverflow's guidelines apply here about: ask a question - assume stackoverflow's guidelines apply here
title: 'q: ( your question title goes here )' title: your question title goes here
labels: 'kind/question, status/triage, area/v2' labels: 'kind/question, status/triage, area/v2'
assignees: '' assignees: ''
--- ---
## my question is... my question is...
_**( Put the question text here )**_

View File

@ -1,28 +1,32 @@
--- ---
name: v1 bug report name: v1 bug report
about: Create a report to help us fix v1 bugs about: Create a report to help us fix v1 bugs
title: 'v1 bug: ( your bug title goes here )' title: 'your bug title goes here'
labels: 'kind/bug, status/triage, area/v1' labels: 'kind/bug, status/triage, area/v1'
assignees: '' assignees: ''
--- ---
## my urfave/cli version is ## My urfave/cli version is
_**( Put the version of urfave/cli that you are using here )**_ _**( Put the version of urfave/cli that you are using here )**_
## Checklist ## Checklist
* [ ] Are you running the latest v1 release? The list of releases is [here](https://github.com/urfave/cli/releases). - [ ] Are you running the latest v1 release? The list of releases is [here](https://github.com/urfave/cli/releases).
* [ ] Did you check the manual for your release? The v1 manual is [here](https://github.com/urfave/cli/blob/master/docs/v1/manual.md) - [ ] Did you check the manual for your release? The v1 manual is [here](https://github.com/urfave/cli/blob/main/docs/v1/manual.md).
* [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. - [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching.
## Dependency Management ## Dependency Management
- [ ] My project is using go modules. <!--
- [ ] My project is using vendoring. Delete any of the following that do not apply:
- [ ] My project is automatically downloading the latest version. -->
- [ ] I am unsure of what my dependency management setup is.
- My project is using go modules.
- My project is using vendoring.
- My project is automatically downloading the latest version.
- I am unsure of what my dependency management setup is.
## Describe the bug ## Describe the bug
@ -34,23 +38,30 @@ Describe the steps or code required to reproduce the behavior
## Observed behavior ## Observed behavior
What did you see happen immediately after the reproduction steps above? What did you see happen immediately after the reproduction steps
above?
## Expected behavior ## Expected behavior
What would you have expected to happen immediately after the reproduction steps above? What would you have expected to happen immediately after the
reproduction steps above?
## Additional context ## Additional context
Add any other context about the problem here. Add any other context about the problem here.
If the issue relates to a specific open source Github repo, please link that repo here. If the issue relates to a specific open source Github repo, please
link that repo here.
If you can reproduce this issue with a public CI system, please link a failing build here. If you can reproduce this issue with a public CI system, please
link a failing build here.
## Want to fix this yourself? ## Want to fix this yourself?
We'd love to have more contributors on this project! If the fix for this bug is easily explained and very small, free free to create a pull request for it. You'll want to base the PR off the `v1` branch, all `v1` bug fix releases will be made from that branch. We'd love to have more contributors on this project! If the fix for
this bug is easily explained and very small, free free to create a
pull request for it. You'll want to base the PR off the `v1`
branch, all `v1` bug fix releases will be made from that branch.
## Run `go version` and paste its output here ## Run `go version` and paste its output here

View File

@ -1,28 +1,32 @@
--- ---
name: v2 bug report name: v2 bug report
about: Create a report to help us fix v2 bugs about: Create a report to help us fix v2 bugs
title: 'v2 bug: ( your bug title goes here )' title: 'your bug title goes here'
labels: 'kind/bug, area/v2, status/triage' labels: 'kind/bug, area/v2, status/triage'
assignees: '' assignees: ''
--- ---
## my urfave/cli version is ## My urfave/cli version is
_**( Put the version of urfave/cli that you are using here )**_ _**( Put the version of urfave/cli that you are using here )**_
## Checklist ## Checklist
* [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases). - [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases).
* [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/master/docs/v2/manual.md) - [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/main/docs/v2/manual.md)
* [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. - [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching.
## Dependency Management ## Dependency Management
- [ ] My project is using go modules. <!--
- [ ] My project is using vendoring. Delete any of the following that do not apply:
- [ ] My project is automatically downloading the latest version. -->
- [ ] I am unsure of what my dependency management setup is.
- My project is using go modules.
- My project is using vendoring.
- My project is automatically downloading the latest version.
- I am unsure of what my dependency management setup is.
## Describe the bug ## Describe the bug
@ -34,23 +38,30 @@ Describe the steps or code required to reproduce the behavior
## Observed behavior ## Observed behavior
What did you see happen immediately after the reproduction steps above? What did you see happen immediately after the reproduction steps
above?
## Expected behavior ## Expected behavior
What would you have expected to happen immediately after the reproduction steps above? What would you have expected to happen immediately after the
reproduction steps above?
## Additional context ## Additional context
Add any other context about the problem here. Add any other context about the problem here.
If the issue relates to a specific open source Github repo, please link that repo here. If the issue relates to a specific open source Github repo, please
link that repo here.
If you can reproduce this issue with a public CI system, please link a failing build here. If you can reproduce this issue with a public CI system, please
link a failing build here.
## Want to fix this yourself? ## Want to fix this yourself?
We'd love to have more contributors on this project! If the fix for this bug is easily explained and very small, free free to create a pull request for it. We'd love to have more contributors on this project! If the fix for
this bug is easily explained and very small, free free to create a
pull request for it.
## Run `go version` and paste its output here ## Run `go version` and paste its output here
``` ```

View File

@ -1,7 +1,7 @@
--- ---
name: v2 feature request name: v2 feature request
about: Suggest an improvement for v2 about: Suggest an improvement for v2
title: 'v2 feature: ( your feature title goes here )' title: 'your feature title goes here'
labels: 'type/feature, area/v2, status/triage' labels: 'type/feature, area/v2, status/triage'
assignees: '' assignees: ''
@ -10,16 +10,19 @@ assignees: ''
## Checklist ## Checklist
* [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases). * [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases).
* [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/master/docs/v2/manual.md) * [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/main/docs/v2/manual.md).
* [ ] Did you perform a search about this feature? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. * [ ] Did you perform a search about this feature? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching.
## What problem does this solve? ## What problem does this solve?
A clear and concise description of what problem this feature would solve. For example: A clear and concise description of what problem this feature would solve. For example:
- needing to type out the full flag name takes a long time, so I would like to suggest adding auto-complete - needing to type out the full flag name takes a long time, so I
- I use (osx, windows, linux) and would like support for (some existing feature) to be extended to my platform would like to suggest adding auto-complete
- the terminal output for a particular error case is confusing, and I think it could be improved - I use (osx, windows, linux) and would like support for (some
existing feature) to be extended to my platform
- the terminal output for a particular error case is confusing, and
I think it could be improved
## Solution description ## Solution description
@ -27,4 +30,5 @@ A detailed description of what you want to happen.
## Describe alternatives you've considered ## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative solutions or
features you've considered.

View File

@ -8,10 +8,14 @@
_(REQUIRED)_ _(REQUIRED)_
- [ ] bug <!--
- [ ] cleanup Delete any of the following that do not apply:
- [ ] documentation -->
- [ ] feature
- bug
- cleanup
- documentation
- feature
## What this PR does / why we need it: ## What this PR does / why we need it:
@ -28,6 +32,7 @@ _(REQUIRED)_
## Which issue(s) this PR fixes: ## Which issue(s) this PR fixes:
_(REQUIRED)_ _(REQUIRED)_
<!-- <!--
If this PR fixes one of more issues, list them here. If this PR fixes one of more issues, list them here.
One line each, like so: One line each, like so:

View File

@ -3,56 +3,55 @@ name: Run Tests
on: on:
push: push:
branches: branches:
- master - main
- v1 tags:
- v2.*
pull_request: pull_request:
branches: branches:
- master - main
- v1
jobs: jobs:
test: test:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
go: [1.15, 1.16, 1.17] go: [1.16.x, 1.17.x, 1.18.x]
name: ${{ matrix.os }} @ Go ${{ matrix.go }} name: ${{ matrix.os }} @ Go ${{ matrix.go }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v1 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Set GOPATH, PATH and ENV - name: Set PATH
run: | run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}"
echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
echo "GO111MODULE=on" >> $GITHUB_ENV
echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
shell: bash
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v1 uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- name: GOFMT Check - name: GOFMT Check
if: matrix.go == 1.17 && 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 tags
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 || true
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == 1.17 && matrix.os == 'ubuntu-latest' if: success() && matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v2
with: with:
fail_ci_if_error: true fail_ci_if_error: true
@ -61,44 +60,30 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v3
with: with:
# Currently fails on 1.16+ go-version: 1.18.x
go-version: 1.15
- name: Use Node.js 12.x - name: Use Node.js 16
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 12.x node-version: '16'
- name: Set GOPATH, PATH and ENV - name: Set PATH
run: | run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}"
echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
echo "GO111MODULE=on" >> $GITHUB_ENV
echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
shell: bash
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v1 uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- name: Install Dependencies - name: Install Dependencies
run: | run:
mkdir -p $GOPATH/bin mkdir -p "${GITHUB_WORKSPACE}/.local/bin" &&
curl -L -o $GOPATH/bin/gfmrun "https://github.com/urfave/gfmrun/releases/download/v1.2.14/gfmrun-$(go env GOOS)-amd64-v1.2.14" 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 $GOPATH/bin/gfmrun chmod +x "${GITHUB_WORKSPACE}/.local/bin/gfmrun" &&
npm install -g markdown-toc@1.2.0 npm install -g markdown-toc@1.2.0
- name: Run Tests (v1) - name: gfmrun
if: contains(github.base_ref, 'v1') run: go run internal/build/build.go gfmrun docs/v2/manual.md
run: |
go run internal/build/build.go gfmrun docs/v1/manual.md
go run internal/build/build.go toc docs/v1/manual.md
- name: Run Tests (v2) - name: toc
if: contains(github.base_ref, 'master') run: go run internal/build/build.go toc docs/v2/manual.md
run: |
go run internal/build/build.go gfmrun docs/v2/manual.md
go run internal/build/build.go toc docs/v2/manual.md

1
.gitignore vendored
View File

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

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

View File

@ -4,7 +4,7 @@ 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)
[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) [![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli)
[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) [![codecov](https://codecov.io/gh/urfave/cli/branch/main/graph/badge.svg)](https://codecov.io/gh/urfave/cli)
cli is a simple, fast, and fun package for building command line apps in Go. The 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
@ -12,7 +12,7 @@ applications in an expressive way.
## Usage Documentation ## Usage Documentation
Usage documentation exists for each major version. Don't know what version you're on? You're probably using the version from the `master` branch, which is currently `v2`. 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`.
- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) - `v2` - [./docs/v2/manual.md](./docs/v2/manual.md)
- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md) - `v1` - [./docs/v1/manual.md](./docs/v1/manual.md)
@ -30,7 +30,7 @@ Go Modules are required when using this package. [See the go blog guide on using
### Using `v2` releases ### Using `v2` releases
``` ```
$ GO111MODULE=on go get github.com/urfave/cli/v2 $ go get github.com/urfave/cli/v2
``` ```
```go ```go
@ -44,7 +44,7 @@ import (
### Using `v1` releases ### Using `v1` releases
``` ```
$ GO111MODULE=on go get github.com/urfave/cli $ go get github.com/urfave/cli
``` ```
```go ```go
@ -55,6 +55,16 @@ import (
... ...
``` ```
### 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 less dependencies.
### GOPATH ### GOPATH
Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can
@ -67,4 +77,8 @@ export PATH=$PATH:$GOPATH/bin
cli is tested against multiple versions of Go on Linux, and against the latest cli is tested against multiple versions of Go on Linux, and against the latest
released version of Go on OS X and Windows. This project uses Github Actions for released version of Go on OS X and Windows. This project uses Github Actions for
builds. To see our currently supported go versions and platforms, look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml). 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).
## License
See [`LICENSE`](./LICENSE)

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,42 +38,40 @@ 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 }
} if value != nil {
if value != nil { for _, name := range f.Names() {
for _, name := range f.Names() { _ = f.set.Set(name, value.String())
_ = f.set.Set(name, value.String())
}
} }
} }
} }
@ -82,20 +80,18 @@ func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourc
} }
// 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 }
} if value != nil {
if value != nil { var sliceValue cli.StringSlice = *(cli.NewStringSlice(value...))
var sliceValue cli.StringSlice = *(cli.NewStringSlice(value...)) for _, name := range f.Names() {
for _, name := range f.Names() { underlyingFlag := f.set.Lookup(name)
underlyingFlag := f.set.Lookup(name) if underlyingFlag != nil {
if underlyingFlag != nil { underlyingFlag.Value = &sliceValue
underlyingFlag.Value = &sliceValue
}
} }
} }
} }
@ -104,20 +100,18 @@ func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputS
} }
// 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 }
} if value != nil {
if value != nil { var sliceValue cli.IntSlice = *(cli.NewIntSlice(value...))
var sliceValue cli.IntSlice = *(cli.NewIntSlice(value...)) for _, name := range f.Names() {
for _, name := range f.Names() { underlyingFlag := f.set.Lookup(name)
underlyingFlag := f.set.Lookup(name) if underlyingFlag != nil {
if underlyingFlag != nil { underlyingFlag.Value = &sliceValue
underlyingFlag.Value = &sliceValue
}
} }
} }
} }
@ -126,17 +120,15 @@ func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour
} }
// 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 }
} if value {
if value { for _, name := range f.Names() {
for _, name := range f.Names() { _ = f.set.Set(name, strconv.FormatBool(value))
_ = f.set.Set(name, strconv.FormatBool(value))
}
} }
} }
} }
@ -144,17 +136,15 @@ func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
} }
// 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 }
} if value != "" {
if value != "" { for _, name := range f.Names() {
for _, name := range f.Names() { _ = f.set.Set(name, value)
_ = f.set.Set(name, value)
}
} }
} }
} }
@ -162,27 +152,25 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource
} }
// 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 }
} if value != "" {
if value != "" { for _, name := range f.Names() {
for _, name := range f.Names() {
if !filepath.IsAbs(value) && isc.Source() != "" { if !filepath.IsAbs(value) && isc.Source() != "" {
basePathAbs, err := filepath.Abs(isc.Source()) basePathAbs, err := filepath.Abs(isc.Source())
if err != nil { if err != nil {
return err return err
}
value = filepath.Join(filepath.Dir(basePathAbs), value)
} }
_ = f.set.Set(name, value) value = filepath.Join(filepath.Dir(basePathAbs), value)
} }
_ = f.set.Set(name, value)
} }
} }
} }
@ -190,55 +178,43 @@ func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
} }
// 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 }
} for _, name := range f.Names() {
if value > 0 { _ = f.set.Set(name, strconv.FormatInt(int64(value), 10))
for _, name := range f.Names() {
_ = 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 }
} for _, name := range f.Names() {
if value > 0 { _ = f.set.Set(name, value.String())
for _, name := range f.Names() {
_ = 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 }
} floatStr := float64ToString(value)
if value > 0 { for _, name := range f.Names() {
floatStr := float64ToString(value) _ = f.set.Set(name, floatStr)
for _, name := range f.Names() {
_ = 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",
}) }
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 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

@ -244,6 +244,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

@ -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
}

92
app.go
View File

@ -12,7 +12,7 @@ import (
) )
var ( var (
changeLogURL = "https://github.com/urfave/cli/blob/master/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)
contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
errInvalidActionType = NewExitError("ERROR invalid Action type. "+ errInvalidActionType = NewExitError("ERROR invalid Action type. "+
@ -245,48 +245,48 @@ 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) _ = 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,20 +297,20 @@ 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() 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)
} }
} }
@ -319,9 +319,9 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
} }
// 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
} }
@ -359,55 +359,55 @@ 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) _ = 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 +418,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
} }
@ -498,9 +498,9 @@ 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)
} }
@ -525,14 +525,14 @@ 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
} }

View File

@ -390,6 +390,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 := ""
@ -445,14 +479,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 +502,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) {

View File

@ -105,39 +105,39 @@ 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) _ = 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 +148,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 +159,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
} }

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/).

View File

@ -6,7 +6,7 @@ Feel free to put up a pull request to fix a bug or maybe add a feature. We will
give it a code review and make sure that it does not break backwards give it a code review and make sure that it does not break backwards
compatibility. If collaborators agree that it is in line with 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 the vision of the project, we will work with you to get the code into
a mergeable state and merge it into the master branch. a mergeable state and merge it into the main branch.
If you have contributed something significant to the project, we will most If you have contributed something significant to the project, we will most
likely add you as a collaborator. As a collaborator you are given the ability likely add you as a collaborator. As a collaborator you are given the ability

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.

View File

@ -674,8 +674,10 @@ Take for example this app that requires the `lang` flag:
package main package main
import ( import (
"fmt"
"log" "log"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )

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))
}

86
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
@ -238,7 +243,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 := ", $"
@ -267,17 +272,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 +280,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 {
@ -442,3 +402,7 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool)
} }
return "", false return "", false
} }
func flagSplitMultiValues(val string) []string {
return strings.Split(val, ",")
}

View File

@ -63,6 +63,19 @@ func (f *BoolFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *BoolFlag) GetDefaultText() string {
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, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -91,8 +104,8 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error {
// Bool looks up the value of a local BoolFlag, returns // Bool looks up the value of a local BoolFlag, returns
// false if not found // false if not found
func (c *Context) Bool(name string) bool { func (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

@ -63,6 +63,19 @@ func (f *DurationFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *DurationFlag) GetDefaultText() string {
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, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -90,8 +103,8 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error {
// Duration looks up the value of a local DurationFlag, returns // Duration looks up the value of a local DurationFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Duration(name string) time.Duration { func (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

@ -55,7 +55,20 @@ func (f *Float64Flag) GetUsage() string {
// 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)
}
// GetDefaultText returns the default text for this flag
func (f *Float64Flag) GetDefaultText() string {
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
} }
// IsVisible returns true if the flag is not hidden, otherwise false // IsVisible returns true if the flag is not hidden, otherwise false
@ -90,8 +103,8 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error {
// Float64 looks up the value of a local Float64Flag, returns // Float64 looks up the value of a local Float64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Float64(name string) float64 { func (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,12 +43,14 @@ func (f *Float64Slice) Set(value string) error {
return nil return nil
} }
tmp, err := strconv.ParseFloat(value, 64) for _, s := range flagSplitMultiValues(value) {
if err != nil { tmp, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
return err if err != nil {
} return err
}
f.slice = append(f.slice, tmp) f.slice = append(f.slice, tmp)
}
return nil return nil
} }
@ -95,7 +97,7 @@ func (f *Float64SliceFlag) IsSet() bool {
// 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 // Names returns the names of the flag
@ -132,13 +134,26 @@ func (f *Float64SliceFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *Float64SliceFlag) GetDefaultText() string {
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 { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
if val != "" { if val != "" {
f.Value = &Float64Slice{} f.Value = &Float64Slice{}
for _, s := range strings.Split(val, ",") { for _, s := range flagSplitMultiValues(val) {
if err := f.Value.Set(strings.TrimSpace(s)); err != nil { if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err) return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err)
} }
@ -164,8 +179,8 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
// Float64Slice looks up the value of a local Float64SliceFlag, returns // Float64Slice looks up the value of a local Float64SliceFlag, returns
// nil if not found // nil if not found
func (c *Context) Float64Slice(name string) []float64 { func (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

View File

@ -71,6 +71,19 @@ func (f *GenericFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *GenericFlag) GetDefaultText() string {
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 {
@ -93,8 +106,8 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error {
// Generic looks up the value of a local GenericFlag, returns // Generic looks up the value of a local GenericFlag, returns
// nil if not found // nil if not found
func (c *Context) Generic(name string) interface{} { func (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

@ -63,6 +63,19 @@ func (f *IntFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *IntFlag) GetDefaultText() string {
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, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -91,8 +104,8 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error {
// Int looks up the value of a local IntFlag, returns // Int looks up the value of a local IntFlag, returns
// 0 if not found // 0 if not found
func (c *Context) Int(name string) int { func (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

@ -63,6 +63,19 @@ func (f *Int64Flag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *Int64Flag) GetDefaultText() string {
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, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -90,8 +103,8 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error {
// Int64 looks up the value of a local Int64Flag, returns // Int64 looks up the value of a local Int64Flag, returns
// 0 if not found // 0 if not found
func (c *Context) Int64(name string) int64 { func (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,12 +43,14 @@ func (i *Int64Slice) Set(value string) error {
return nil return nil
} }
tmp, err := strconv.ParseInt(value, 0, 64) for _, s := range flagSplitMultiValues(value) {
if err != nil { tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
return err if err != nil {
} return err
}
i.slice = append(i.slice, tmp) i.slice = append(i.slice, tmp)
}
return nil return nil
} }
@ -96,7 +98,7 @@ func (f *Int64SliceFlag) IsSet() bool {
// 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 // Names returns the names of the flag
@ -133,12 +135,25 @@ func (f *Int64SliceFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *Int64SliceFlag) GetDefaultText() string {
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 { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
f.Value = &Int64Slice{} f.Value = &Int64Slice{}
for _, s := range strings.Split(val, ",") { for _, s := range flagSplitMultiValues(val) {
if err := f.Value.Set(strings.TrimSpace(s)); err != nil { if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err)
} }
@ -163,8 +178,8 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
// Int64Slice looks up the value of a local Int64SliceFlag, returns // Int64Slice looks up the value of a local Int64SliceFlag, returns
// nil if not found // nil if not found
func (c *Context) Int64Slice(name string) []int64 { func (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

View File

@ -54,12 +54,14 @@ func (i *IntSlice) Set(value string) error {
return nil return nil
} }
tmp, err := strconv.ParseInt(value, 0, 64) for _, s := range flagSplitMultiValues(value) {
if err != nil { tmp, err := strconv.ParseInt(strings.TrimSpace(s), 0, 64)
return err if err != nil {
} return err
}
i.slice = append(i.slice, int(tmp)) i.slice = append(i.slice, int(tmp))
}
return nil return nil
} }
@ -107,7 +109,7 @@ func (f *IntSliceFlag) IsSet() bool {
// 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 // Names returns the names of the flag
@ -144,12 +146,25 @@ func (f *IntSliceFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *IntSliceFlag) GetDefaultText() string {
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 { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
f.Value = &IntSlice{} f.Value = &IntSlice{}
for _, s := range strings.Split(val, ",") { for _, s := range flagSplitMultiValues(val) {
if err := f.Value.Set(strings.TrimSpace(s)); err != nil { if err := f.Value.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err)
} }
@ -174,8 +189,8 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
// IntSlice looks up the value of a local IntSliceFlag, returns // IntSlice looks up the value of a local IntSliceFlag, returns
// nil if not found // nil if not found
func (c *Context) IntSlice(name string) []int { func (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

View File

@ -1,6 +1,9 @@
package cli package cli
import "flag" import (
"flag"
"fmt"
)
type PathFlag struct { type PathFlag struct {
Name string Name string
@ -59,6 +62,22 @@ func (f *PathFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *PathFlag) GetDefaultText() string {
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, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -79,8 +98,8 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error {
// Path looks up the value of a local PathFlag, returns // Path looks up the value of a local PathFlag, returns
// "" if not found // "" if not found
func (c *Context) Path(name string) string { func (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,6 +1,9 @@
package cli package cli
import "flag" import (
"flag"
"fmt"
)
// StringFlag is a flag with type string // StringFlag is a flag with type string
type StringFlag struct { type StringFlag struct {
@ -60,6 +63,22 @@ func (f *StringFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *StringFlag) GetDefaultText() string {
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, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
@ -80,8 +99,8 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error {
// String looks up the value of a local StringFlag, returns // String looks up the value of a local StringFlag, returns
// "" if not found // "" if not found
func (c *Context) String(name string) string { func (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
} }
@ -92,7 +94,7 @@ func (f *StringSliceFlag) IsSet() bool {
// 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 // Names returns the names of the flag
@ -129,6 +131,19 @@ func (f *StringSliceFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *StringSliceFlag) GetDefaultText() string {
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 {
@ -147,7 +162,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
destination = f.Destination destination = f.Destination
} }
for _, s := range strings.Split(val, ",") { for _, s := range flagSplitMultiValues(val) {
if err := destination.Set(strings.TrimSpace(s)); err != nil { if err := destination.Set(strings.TrimSpace(s)); err != nil {
return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err) return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err)
} }
@ -175,8 +190,8 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
// StringSlice looks up the value of a local StringSliceFlag, returns // StringSlice looks up the value of a local StringSliceFlag, returns
// nil if not found // nil if not found
func (c *Context) StringSlice(name string) []string { func (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

View File

@ -132,8 +132,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 +168,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 +400,7 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
var prefixStringFlagTests = []struct { var _ = []struct {
name string name string
aliases []string aliases []string
usage string usage string
@ -308,7 +490,7 @@ func TestPathFlagApply_SetsAllNames(t *testing.T) {
expect(t, v, "/path/to/file/PATH") expect(t, v, "/path/to/file/PATH")
} }
var envHintFlagTests = []struct { var _ = []struct {
name string name string
env string env string
hinter FlagEnvHintFunc hinter FlagEnvHintFunc
@ -1992,43 +2174,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 +2230,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 +2289,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

@ -119,6 +119,19 @@ func (f *TimestampFlag) IsVisible() bool {
return !f.Hidden return !f.Hidden
} }
// GetDefaultText returns the default text for this flag
func (f *TimestampFlag) GetDefaultText() string {
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
func (f *TimestampFlag) Apply(set *flag.FlagSet) error { func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
if f.Layout == "" { if f.Layout == "" {
@ -152,8 +165,8 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
} }
// Timestamp gets the timestamp from a flag name // Timestamp gets the timestamp from a flag name
func (c *Context) Timestamp(name string) *time.Time { func (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

@ -88,10 +88,23 @@ 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
}
// 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

@ -88,10 +88,23 @@ 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
}
// 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.

8
go.mod
View File

@ -1,9 +1,11 @@
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.1
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.4.0
) )
require github.com/russross/blackfriday/v2 v2.1.0 // indirect

6
go.sum
View File

@ -1,10 +1,12 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
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/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=
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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

101
help.go
View File

@ -15,13 +15,13 @@ var helpCommand = &Command{
Aliases: []string{"h"}, Aliases: []string{"h"},
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
}, },
} }
@ -31,13 +31,13 @@ var helpSubcommand = &Command{
Aliases: []string{"h"}, Aliases: []string{"h"},
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)
}, },
} }
@ -71,30 +71,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) {
@ -159,23 +159,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)
} }
} }
@ -221,32 +228,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)
} }
} }
@ -297,20 +304,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 +333,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 +357,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
} }

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,85 @@ 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) {
origArgv := os.Args
t.Cleanup(func() {
os.Args = origArgv
})
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)
}
})
}
}

View File

@ -46,6 +46,12 @@ func main() {
Action: checkBinarySizeActionFunc, Action: checkBinarySizeActionFunc,
}, },
} }
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "tags",
Usage: "set build tags",
},
}
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
@ -68,6 +74,8 @@ func VetActionFunc(_ *cli.Context) error {
} }
func TestActionFunc(c *cli.Context) error { func TestActionFunc(c *cli.Context) error {
tags := c.String("tags")
for _, pkg := range packages { for _, pkg := range packages {
var packageName string var packageName string
@ -79,7 +87,7 @@ func TestActionFunc(c *cli.Context) error {
coverProfile := fmt.Sprintf("--coverprofile=%s.coverprofile", pkg) coverProfile := fmt.Sprintf("--coverprofile=%s.coverprofile", pkg)
err := runCmd("go", "test", "-v", coverProfile, packageName) err := runCmd("go", "test", "-tags", tags, "-v", coverProfile, packageName)
if err != nil { if err != nil {
return err return err
} }
@ -193,7 +201,7 @@ 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 desiredMinBinarySize = 1.675
desiredMaxBinarySize = 2.2 desiredMaxBinarySize = 2.2
badNewsEmoji = "🚨" badNewsEmoji = "🚨"
goodNewsEmoji = "✨" goodNewsEmoji = "✨"
@ -201,14 +209,16 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
mbStringFormatter = "%.1fMB" mbStringFormatter = "%.1fMB"
) )
tags := c.String("tags")
// 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 +280,9 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) {
return nil return nil
} }
func getSize(sourcePath string, builtPath string) (size int64, err error) { 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{""})
} }

1
testdata/empty.yml vendored Normal file
View File

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