Merge branch 'main' into master

This commit is contained in:
Dan Buch 2022-04-21 19:31:22 -04:00 committed by GitHub
commit 7a231c5eb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1536 additions and 455 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:

72
.github/stale.yml vendored
View File

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

View File

@ -3,43 +3,35 @@ 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.12, 1.13, 1.14] 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 "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)"
echo "::set-env name=GO111MODULE::on"
echo "::set-env name=GOPROXY::https://proxy.golang.org"
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin"
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.14 && 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
@ -52,54 +44,40 @@ jobs:
run: go run internal/build/build.go check-binary-size run: go run internal/build/build.go check-binary-size
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == 1.14 && 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:
token: 0a8cc73b-bb7c-480b-8626-38a461643761
fail_ci_if_error: true fail_ci_if_error: true
test-docs: test-docs:
name: test-docs name: test-docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.14 - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v3
with: with:
go-version: 1.14 go-version: 1.18.x
- 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 "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)"
echo "::set-env name=GO111MODULE::on"
echo "::set-env name=GOPROXY::https://proxy.golang.org"
echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin"
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

3
.gitignore vendored
View File

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

View File

@ -1,10 +1,10 @@
cli cli
=== ===
[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) [![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://pkg.go.dev/github.com/urfave/cli/v2)
[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli)
[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) [![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli)
[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) [![codecov](https://codecov.io/gh/urfave/cli/branch/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
@ -67,4 +67,4 @@ export PATH=$PATH:$GOPATH/bin
cli is tested against multiple versions of Go on Linux, and against the latest cli is tested against multiple versions of Go on Linux, and against the latest
released version of Go on OS X and Windows. This project uses Github Actions for released version of Go on OS X and Windows. This project uses Github Actions for
builds. 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).

View File

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

View File

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

19
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. "+
@ -64,7 +64,7 @@ type App struct {
Action ActionFunc Action ActionFunc
// Execute this function if the proper command cannot be found // Execute this function if the proper command cannot be found
CommandNotFound CommandNotFoundFunc CommandNotFound CommandNotFoundFunc
// Execute this function if an usage error occurs // Execute this function if a usage error occurs
OnUsageError OnUsageErrorFunc OnUsageError OnUsageErrorFunc
// Compilation date // Compilation date
Compiled time.Time Compiled time.Time
@ -72,6 +72,8 @@ type App struct {
Authors []*Author Authors []*Author
// Copyright of the binary if any // Copyright of the binary if any
Copyright string Copyright string
// Reader reader to write input to (useful for tests)
Reader io.Reader
// Writer writer to write output to // Writer writer to write output to
Writer io.Writer Writer io.Writer
// ErrWriter writes error output // ErrWriter writes error output
@ -117,6 +119,7 @@ func NewApp() *App {
BashComplete: DefaultAppComplete, BashComplete: DefaultAppComplete,
Action: helpCommand.Action, Action: helpCommand.Action,
Compiled: compileTime(), Compiled: compileTime(),
Reader: os.Stdin,
Writer: os.Stdout, Writer: os.Stdout,
ErrWriter: os.Stderr, ErrWriter: os.Stderr,
} }
@ -137,7 +140,7 @@ func (a *App) Setup() {
} }
if a.HelpName == "" { if a.HelpName == "" {
a.HelpName = filepath.Base(os.Args[0]) a.HelpName = a.Name
} }
if a.Usage == "" { if a.Usage == "" {
@ -160,6 +163,10 @@ func (a *App) Setup() {
a.Compiled = compileTime() a.Compiled = compileTime()
} }
if a.Reader == nil {
a.Reader = os.Stdin
}
if a.Writer == nil { if a.Writer == nil {
a.Writer = os.Stdout a.Writer = os.Stdout
} }
@ -271,7 +278,7 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
return nil return nil
} }
cerr := checkRequiredFlags(a.Flags, context) cerr := context.checkRequiredFlags(a.Flags)
if cerr != nil { if cerr != nil {
_ = ShowAppHelp(context) _ = ShowAppHelp(context)
return cerr return cerr
@ -321,7 +328,7 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned // RunAndExitOnError calls .Run() and exits non-zero if an error was returned
// //
// Deprecated: instead you should return an error that fulfills cli.ExitCoder // Deprecated: instead you should return an error that fulfills cli.ExitCoder
// to cli.App.Run. This will cause the application to exit with the given eror // to cli.App.Run. This will cause the application to exit with the given error
// code in the cli.ExitCoder // code in the cli.ExitCoder
func (a *App) RunAndExitOnError() { func (a *App) RunAndExitOnError() {
if err := a.Run(os.Args); err != nil { if err := a.Run(os.Args); err != nil {
@ -390,7 +397,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
} }
} }
cerr := checkRequiredFlags(a.Flags, context) cerr := context.checkRequiredFlags(a.Flags)
if cerr != nil { if cerr != nil {
_ = ShowSubcommandHelp(context) _ = ShowSubcommandHelp(context)
return cerr return cerr

View File

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

View File

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

View File

@ -127,7 +127,7 @@ func (c *Command) Run(ctx *Context) (err error) {
return nil return nil
} }
cerr := checkRequiredFlags(c.Flags, context) cerr := context.checkRequiredFlags(c.Flags)
if cerr != nil { if cerr != nil {
_ = ShowCommandHelp(context, c.Name) _ = ShowCommandHelp(context, c.Name)
return cerr return cerr
@ -227,6 +227,7 @@ func (c *Command) startApp(ctx *Context) error {
} }
app.Usage = c.Usage app.Usage = c.Usage
app.UsageText = c.UsageText
app.Description = c.Description app.Description = c.Description
app.ArgsUsage = c.ArgsUsage app.ArgsUsage = c.ArgsUsage
@ -243,6 +244,7 @@ func (c *Command) startApp(ctx *Context) error {
app.Version = ctx.App.Version app.Version = ctx.App.Version
app.HideVersion = true app.HideVersion = true
app.Compiled = ctx.App.Compiled app.Compiled = ctx.App.Compiled
app.Reader = ctx.App.Reader
app.Writer = ctx.App.Writer app.Writer = ctx.App.Writer
app.ErrWriter = ctx.App.ErrWriter app.ErrWriter = ctx.App.ErrWriter
app.ExitErrHandler = ctx.App.ExitErrHandler app.ExitErrHandler = ctx.App.ExitErrHandler

View File

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

View File

@ -112,6 +112,8 @@ func TestContext_String(t *testing.T) {
c := NewContext(nil, set, parentCtx) c := NewContext(nil, set, parentCtx)
expect(t, c.String("myflag"), "hello world") expect(t, c.String("myflag"), "hello world")
expect(t, c.String("top-flag"), "hai veld") expect(t, c.String("top-flag"), "hai veld")
c = NewContext(nil, nil, parentCtx)
expect(t, c.String("top-flag"), "hai veld")
} }
func TestContext_Path(t *testing.T) { func TestContext_Path(t *testing.T) {
@ -136,6 +138,18 @@ func TestContext_Bool(t *testing.T) {
expect(t, c.Bool("top-flag"), true) expect(t, c.Bool("top-flag"), true)
} }
func TestContext_Value(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.Int("myflag", 12, "doc")
parentSet := flag.NewFlagSet("test", 0)
parentSet.Int("top-flag", 13, "doc")
parentCtx := NewContext(nil, parentSet, nil)
c := NewContext(nil, set, parentCtx)
expect(t, c.Value("myflag"), 12)
expect(t, c.Value("top-flag"), 13)
expect(t, c.Value("unknown-flag"), nil)
}
func TestContext_Args(t *testing.T) { func TestContext_Args(t *testing.T) {
set := flag.NewFlagSet("test", 0) set := flag.NewFlagSet("test", 0)
set.Bool("myflag", false, "doc") set.Bool("myflag", false, "doc")
@ -304,13 +318,13 @@ func TestContext_lookupFlagSet(t *testing.T) {
_ = set.Parse([]string{"--local-flag"}) _ = set.Parse([]string{"--local-flag"})
_ = parentSet.Parse([]string{"--top-flag"}) _ = parentSet.Parse([]string{"--top-flag"})
fs := lookupFlagSet("top-flag", ctx) fs := ctx.lookupFlagSet("top-flag")
expect(t, fs, parentCtx.flagSet) expect(t, fs, parentCtx.flagSet)
fs = lookupFlagSet("local-flag", ctx) fs = ctx.lookupFlagSet("local-flag")
expect(t, fs, ctx.flagSet) expect(t, fs, ctx.flagSet)
if fs := lookupFlagSet("frob", ctx); fs != nil { if fs := ctx.lookupFlagSet("frob"); fs != nil {
t.Fail() t.Fail()
} }
} }
@ -564,7 +578,7 @@ func TestCheckRequiredFlags(t *testing.T) {
ctx.Command.Flags = test.flags ctx.Command.Flags = test.flags
// logic under test // logic under test
err := checkRequiredFlags(test.flags, ctx) err := ctx.checkRequiredFlags(test.flags)
// assertions // assertions
if test.expectedAnError && err == nil { if test.expectedAnError && err == nil {

67
docs.go
View File

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

View File

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

View File

@ -15,15 +15,18 @@ consider sending a PR to help improve this guide.
* [Flags before args](#flags-before-args) * [Flags before args](#flags-before-args)
* [Import string changed](#import-string-changed) * [Import string changed](#import-string-changed)
* [Flag aliases are done differently.](#flag-aliases-are-done-differently) * [Flag aliases are done differently](#flag-aliases-are-done-differently)
* [EnvVar is now a list (EnvVars)](#envvar-is-now-a-list-envvars) * [EnvVar is now a list (EnvVars)](#envvar-is-now-a-list-envvars)
* [Actions returns errors](#actions-returns-errors)
* [cli.Flag changed](#cliflag-changed)
* [Commands are now lists of pointers](#commands-are-now-lists-of-pointers) * [Commands are now lists of pointers](#commands-are-now-lists-of-pointers)
* [Lists of commands should be pointers](#lists-of-commands-should-be-pointers) * [Lists of commands should be pointers](#lists-of-commands-should-be-pointers)
* [cli.Flag changed](#cliflag-changed)
* [Appending Commands](#appending-commands) * [Appending Commands](#appending-commands)
* [Actions returns errors](#actions-returns-errors) * [GlobalString, GlobalBool and its likes are deprecated](#globalstring-globalbool-and-its-likes-are-deprecated)
* [BoolTFlag and BoolT are deprecated](#booltflag-and-boolt-are-deprecated)
* [&cli.StringSlice{""} replaced with cli.NewStringSlice("")](#clistringslice-replaced-with-clinewstringslice)
* [Replace deprecated functions](#replace-deprecated-functions)
* [Everything else](#everything-else) * [Everything else](#everything-else)
* [Full API Example](#full-api-example)
<!-- tocstop --> <!-- tocstop -->
@ -53,7 +56,7 @@ Check each file for this and make the change.
Shell command to find them all: `fgrep -rl github.com/urfave/cli *` Shell command to find them all: `fgrep -rl github.com/urfave/cli *`
# Flag aliases are done differently. # Flag aliases are done differently
Change `Name: "foo, f"` to `Name: "foo", Aliases: []string{"f"}` Change `Name: "foo, f"` to `Name: "foo", Aliases: []string{"f"}`
@ -181,6 +184,52 @@ Compiler messages you might see:
cannot use c (type *cli.Command) as type cli.Command in append cannot use c (type *cli.Command) as type cli.Command in append
``` ```
# GlobalString, GlobalBool and its likes are deprecated
Use simply `String` instead of `GlobalString`, `Bool` instead of `GlobalBool`
# BoolTFlag and BoolT are deprecated
BoolTFlag was a Bool Flag with its default value set to true and BoolT was used to find any BoolTFlag used locally, so both are deprecated.
* OLD:
```go
cli.BoolTFlag{
Name: FlagName,
Usage: FlagUsage,
EnvVar: "FLAG_ENV_VAR",
}
```
* NEW:
```go
cli.BoolFlag{
Name: FlagName,
Value: true,
Usage: FlagUsage,
EnvVar: "FLAG_ENV_VAR",
}
```
# &cli.StringSlice{""} replaced with cli.NewStringSlice("")
Example:
* OLD:
```go
Value: &cli.StringSlice{""},
```
* NEW:
```go
Value: cli.NewStringSlice(""),
}
```
# Replace deprecated functions
`cli.NewExitError()` is deprecated. Use `cli.Exit()` instead. ([Staticcheck](https://staticcheck.io/) detects this automatically and recommends replacement code.)
# Everything else # Everything else
Compile the code and work through any errors. Most should Compile the code and work through any errors. Most should

View File

@ -612,7 +612,7 @@ given sources.
Here is a more complete sample of a command using YAML support: Here is a more complete sample of a command using YAML support:
<!-- { <!-- {
"args": ["test-cmd", "&#45;&#45;help"], "args": ["&#45;&#45;help"],
"output": "&#45&#45;test value.*default: 0" "output": "&#45&#45;test value.*default: 0"
} --> } -->
``` go ``` go

View File

@ -30,6 +30,7 @@ cli v2 manual
+ [ZSH Support](#zsh-support) + [ZSH Support](#zsh-support)
+ [ZSH default auto-complete example](#zsh-default-auto-complete-example) + [ZSH default auto-complete example](#zsh-default-auto-complete-example)
+ [ZSH custom auto-complete example](#zsh-custom-auto-complete-example) + [ZSH custom auto-complete example](#zsh-custom-auto-complete-example)
+ [PowerShell Support](#powershell-support)
* [Generated Help Text](#generated-help-text) * [Generated Help Text](#generated-help-text)
+ [Customization](#customization-1) + [Customization](#customization-1)
* [Version Flag](#version-flag) * [Version Flag](#version-flag)
@ -44,7 +45,7 @@ cli v2 manual
There are a small set of breaking changes between v1 and v2. There are a small set of breaking changes between v1 and v2.
Converting is relatively straightforward and typically takes less than Converting is relatively straightforward and typically takes less than
an hour. Specific steps are included in an hour. Specific steps are included in
[Migration Guide: v1 to v2](../migrate-v1-to-v2.md). [Migration Guide: v1 to v2](../migrate-v1-to-v2.md). Also see the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v2) for v2 API documentation.
## Getting Started ## Getting Started
@ -426,12 +427,14 @@ func main() {
app := &cli.App{ app := &cli.App{
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "lang, l", Name: "lang",
Aliases: []string{"l"},
Value: "english", Value: "english",
Usage: "Language for the greeting", Usage: "Language for the greeting",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "config, c", Name: "config",
Aliases: []string{"c"},
Usage: "Load configuration from `FILE`", Usage: "Load configuration from `FILE`",
}, },
}, },
@ -511,7 +514,7 @@ func main() {
``` ```
If `EnvVars` contains more than one string, the first environment variable that If `EnvVars` contains more than one string, the first environment variable that
resolves is used as the default. resolves is used.
<!-- { <!-- {
"args": ["&#45;&#45;help"], "args": ["&#45;&#45;help"],
@ -570,7 +573,8 @@ func main() {
app.Flags = []cli.Flag { app.Flags = []cli.Flag {
&cli.StringFlag{ &cli.StringFlag{
Name: "password, p", Name: "password",
Aliases: []string{"p"},
Usage: "password for the mysql database", Usage: "password for the mysql database",
FilePath: "/etc/mysql/password", FilePath: "/etc/mysql/password",
}, },
@ -623,7 +627,7 @@ given sources.
Here is a more complete sample of a command using YAML support: Here is a more complete sample of a command using YAML support:
<!-- { <!-- {
"args": ["test-cmd", "&#45;&#45;help"], "args": ["&#45;&#45;help"],
"output": "&#45&#45;test value.*default: 0" "output": "&#45&#45;test value.*default: 0"
} --> } -->
``` go ``` go
@ -645,7 +649,7 @@ func main() {
app := &cli.App{ app := &cli.App{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
fmt.Println("yaml ist rad") fmt.Println("--test value.*default: 0")
return nil return nil
}, },
Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")),
@ -670,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"
) )
@ -1222,6 +1228,23 @@ source path/to/autocomplete/zsh_autocomplete
#### ZSH custom auto-complete example #### ZSH custom auto-complete example
![](/docs/v2/images/custom-zsh-autocomplete.gif) ![](/docs/v2/images/custom-zsh-autocomplete.gif)
#### PowerShell Support
Auto-completion for PowerShell is also supported using the `autocomplete/powershell_autocomplete.ps1`
file included in this repo.
Rename the script to `<my program>.ps1` and move it anywhere in your file system.
The location of script does not matter, only the file name of the script has to match
the your program's binary name.
To activate it, enter `& path/to/autocomplete/<my program>.ps1`
To persist across new shells, open the PowerShell profile (with `code $profile` or `notepad $profile`)
and add the line:
```
& path/to/autocomplete/<my program>.ps1
```
### Generated Help Text ### Generated Help Text
The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked
@ -1309,7 +1332,8 @@ import (
func main() { func main() {
cli.HelpFlag = &cli.BoolFlag{ cli.HelpFlag = &cli.BoolFlag{
Name: "haaaaalp", Aliases: []string{"halp"}, Name: "haaaaalp",
Aliases: []string{"halp"},
Usage: "HALP", Usage: "HALP",
EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, EnvVars: []string{"SHOW_HALP", "HALPPLZ"},
} }
@ -1344,7 +1368,8 @@ import (
func main() { func main() {
cli.VersionFlag = &cli.BoolFlag{ cli.VersionFlag = &cli.BoolFlag{
Name: "print-version", Aliases: []string{"V"}, Name: "print-version",
Aliases: []string{"V"},
Usage: "print only the version", Usage: "print only the version",
} }

View File

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

View File

@ -47,6 +47,28 @@ func (m *multiError) Errors() []error {
return errs return errs
} }
type requiredFlagsErr interface {
error
getMissingFlags() []string
}
type errRequiredFlags struct {
missingFlags []string
}
func (e *errRequiredFlags) Error() string {
numberOfMissingFlags := len(e.missingFlags)
if numberOfMissingFlags == 1 {
return fmt.Sprintf("Required flag %q not set", e.missingFlags[0])
}
joinedMissingFlags := strings.Join(e.missingFlags, ", ")
return fmt.Sprintf("Required flags %q not set", joinedMissingFlags)
}
func (e *errRequiredFlags) getMissingFlags() []string {
return e.missingFlags
}
// ErrorFormatter is the interface that will suitably format the error output // ErrorFormatter is the interface that will suitably format the error output
type ErrorFormatter interface { type ErrorFormatter interface {
Format(s fmt.State, verb rune) Format(s fmt.State, verb rune)

View File

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

View File

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

62
flag.go
View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -118,6 +119,14 @@ type DocGenerationFlag interface {
GetValue() string GetValue() string
} }
// VisibleFlag is an interface that allows to check if a flag is visible
type VisibleFlag interface {
Flag
// IsVisible returns true if the flag is not hidden, otherwise false
IsVisible() bool
}
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError) set := flag.NewFlagSet(name, flag.ContinueOnError)
@ -130,11 +139,52 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
return set, nil return set, nil
} }
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case Serializer:
_ = set.Set(name, ff.Value.(Serializer).Serialize())
default:
_ = set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := f.Names()
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}
func visibleFlags(fl []Flag) []Flag { func visibleFlags(fl []Flag) []Flag {
var visible []Flag var visible []Flag
for _, f := range fl { for _, f := range fl {
field := flagValue(f).FieldByName("Hidden") if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() {
if !field.IsValid() || !field.Bool() {
visible = append(visible, f) visible = append(visible, f)
} }
} }
@ -359,7 +409,11 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string {
} }
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault) multiInputString := "(accepts multiple inputs)"
if usageWithDefault != "" {
multiInputString = "\t" + multiInputString
}
return fmt.Sprintf("%s\t%s%s", prefixedNames(names, placeholder), usageWithDefault, multiInputString)
} }
func hasFlag(flags []Flag, fl Flag) bool { func hasFlag(flags []Flag, fl Flag) bool {
@ -380,9 +434,11 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool)
} }
} }
for _, fileVar := range strings.Split(filePath, ",") { for _, fileVar := range strings.Split(filePath, ",") {
if fileVar != "" {
if data, err := ioutil.ReadFile(fileVar); err == nil { if data, err := ioutil.ReadFile(fileVar); err == nil {
return string(data), true return string(data), true
} }
} }
}
return "", false return "", false
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,15 +52,21 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) {
} }
func TestFlagsFromEnv(t *testing.T) { func TestFlagsFromEnv(t *testing.T) {
newSetFloat64Slice := func(defaults ...float64) Float64Slice {
s := NewFloat64Slice(defaults...)
s.hasBeenSet = false
return *s
}
newSetIntSlice := func(defaults ...int) IntSlice { newSetIntSlice := func(defaults ...int) IntSlice {
s := NewIntSlice(defaults...) s := NewIntSlice(defaults...)
s.hasBeenSet = true s.hasBeenSet = false
return *s return *s
} }
newSetInt64Slice := func(defaults ...int64) Int64Slice { newSetInt64Slice := func(defaults ...int64) Int64Slice {
s := NewInt64Slice(defaults...) s := NewInt64Slice(defaults...)
s.hasBeenSet = true s.hasBeenSet = false
return *s return *s
} }
@ -96,6 +102,9 @@ func TestFlagsFromEnv(t *testing.T) {
{"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, {"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`},
{"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`},
{"1.0,2", newSetFloat64Slice(1, 2), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "\[\]float64{}" as float64 slice value for flag seconds: .*`},
{"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""},
{"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`}, {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`},
{"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`}, {"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`},
@ -340,11 +349,11 @@ var stringSliceFlagTests = []struct {
value *StringSlice value *StringSlice
expected string expected string
}{ }{
{"foo", nil, NewStringSlice(""), "--foo value\t"}, {"foo", nil, NewStringSlice(""), "--foo value\t(accepts multiple inputs)"},
{"f", nil, NewStringSlice(""), "-f value\t"}, {"f", nil, NewStringSlice(""), "-f value\t(accepts multiple inputs)"},
{"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")"}, {"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")\t(accepts multiple inputs)"},
{"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")"}, {"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")\t(accepts multiple inputs)"},
{"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")"}, {"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")\t(accepts multiple inputs)"},
} }
func TestStringSliceFlagHelpOutput(t *testing.T) { func TestStringSliceFlagHelpOutput(t *testing.T) {
@ -386,6 +395,20 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
} }
func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) {
defer resetEnv(os.Environ())
os.Clearenv()
_ = os.Setenv("MY_GOAT", "vincent van goat,scape goat")
var val StringSlice
fl := StringSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse(nil)
expect(t, err, nil)
expect(t, val.Value(), NewStringSlice("vincent van goat", "scape goat").Value())
}
func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) {
defValue := []string{"UA", "US"} defValue := []string{"UA", "US"}
@ -616,9 +639,9 @@ var intSliceFlagTests = []struct {
value *IntSlice value *IntSlice
expected string expected string
}{ }{
{"heads", nil, NewIntSlice(), "--heads value\t"}, {"heads", nil, NewIntSlice(), "--heads value\t(accepts multiple inputs)"},
{"H", nil, NewIntSlice(), "-H value\t"}, {"H", nil, NewIntSlice(), "-H value\t(accepts multiple inputs)"},
{"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)"}, {"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)\t(accepts multiple inputs)"},
} }
func TestIntSliceFlagHelpOutput(t *testing.T) { func TestIntSliceFlagHelpOutput(t *testing.T) {
@ -660,16 +683,55 @@ func TestIntSliceFlagApply_SetsAllNames(t *testing.T) {
expect(t, err, nil) expect(t, err, nil)
} }
func TestIntSliceFlagApply_ParentContext(t *testing.T) {
_ = (&App{
Flags: []Flag{
&IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewIntSlice(1, 2, 3)},
},
Commands: []*Command{
{
Name: "child",
Action: func(ctx *Context) error {
expected := []int{1, 2, 3}
if !reflect.DeepEqual(ctx.IntSlice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("numbers"))
}
if !reflect.DeepEqual(ctx.IntSlice("n"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("n"))
}
return nil
},
},
},
}).Run([]string{"run", "child"})
}
func TestIntSliceFlag_SetFromParentContext(t *testing.T) {
fl := &IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewIntSlice(1, 2, 3, 4)}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
ctx := &Context{
parentContext: &Context{
flagSet: set,
},
flagSet: flag.NewFlagSet("empty", 0),
}
expected := []int{1, 2, 3, 4}
if !reflect.DeepEqual(ctx.IntSlice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("numbers"))
}
}
var int64SliceFlagTests = []struct { var int64SliceFlagTests = []struct {
name string name string
aliases []string aliases []string
value *Int64Slice value *Int64Slice
expected string expected string
}{ }{
{"heads", nil, NewInt64Slice(), "--heads value\t"}, {"heads", nil, NewInt64Slice(), "--heads value\t(accepts multiple inputs)"},
{"H", nil, NewInt64Slice(), "-H value\t"}, {"H", nil, NewInt64Slice(), "-H value\t(accepts multiple inputs)"},
{"heads", []string{"H"}, NewInt64Slice(int64(2), int64(17179869184)), {"heads", []string{"H"}, NewInt64Slice(int64(2), int64(17179869184)),
"--heads value, -H value\t(default: 2, 17179869184)"}, "--heads value, -H value\t(default: 2, 17179869184)\t(accepts multiple inputs)"},
} }
func TestInt64SliceFlagHelpOutput(t *testing.T) { func TestInt64SliceFlagHelpOutput(t *testing.T) {
@ -702,6 +764,60 @@ func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) {
} }
} }
func TestInt64SliceFlagApply_ParentContext(t *testing.T) {
_ = (&App{
Flags: []Flag{
&Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewInt64Slice(1, 2, 3)},
},
Commands: []*Command{
{
Name: "child",
Action: func(ctx *Context) error {
expected := []int64{1, 2, 3}
if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers"))
}
if !reflect.DeepEqual(ctx.Int64Slice("n"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("n"))
}
return nil
},
},
},
}).Run([]string{"run", "child"})
}
func TestInt64SliceFlag_SetFromParentContext(t *testing.T) {
fl := &Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewInt64Slice(1, 2, 3, 4)}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
ctx := &Context{
parentContext: &Context{
flagSet: set,
},
flagSet: flag.NewFlagSet("empty", 0),
}
expected := []int64{1, 2, 3, 4}
if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers"))
}
}
func TestInt64SliceFlag_ReturnNil(t *testing.T) {
fl := &Int64SliceFlag{}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
ctx := &Context{
parentContext: &Context{
flagSet: set,
},
flagSet: flag.NewFlagSet("empty", 0),
}
expected := []int64(nil)
if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) {
t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers"))
}
}
var float64FlagTests = []struct { var float64FlagTests = []struct {
name string name string
expected string expected string
@ -757,10 +873,10 @@ var float64SliceFlagTests = []struct {
value *Float64Slice value *Float64Slice
expected string expected string
}{ }{
{"heads", nil, NewFloat64Slice(), "--heads value\t"}, {"heads", nil, NewFloat64Slice(), "--heads value\t(accepts multiple inputs)"},
{"H", nil, NewFloat64Slice(), "-H value\t"}, {"H", nil, NewFloat64Slice(), "-H value\t(accepts multiple inputs)"},
{"heads", []string{"H"}, NewFloat64Slice(0.1234, -10.5), {"heads", []string{"H"}, NewFloat64Slice(0.1234, -10.5),
"--heads value, -H value\t(default: 0.1234, -10.5)"}, "--heads value, -H value\t(default: 0.1234, -10.5)\t(accepts multiple inputs)"},
} }
func TestFloat64SliceFlagHelpOutput(t *testing.T) { func TestFloat64SliceFlagHelpOutput(t *testing.T) {
@ -1866,3 +1982,80 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"}) err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"})
expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\"")) expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\""))
} }
type flagDefaultTestCase struct {
name string
flag Flag
toParse []string
expect string
}
func TestFlagDefaultValue(t *testing.T) {
cases := []*flagDefaultTestCase{
&flagDefaultTestCase{
name: "stringSclice",
flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")},
toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "float64Sclice",
flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)},
toParse: []string{"--flag", "13.3"},
expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "int64Sclice",
flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "intSclice",
flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1, 2) (accepts multiple inputs)`,
},
&flagDefaultTestCase{
name: "string",
flag: &StringFlag{Name: "flag", Value: "default"},
toParse: []string{"--flag", "parsed"},
expect: `--flag value (default: "default")`,
},
&flagDefaultTestCase{
name: "bool",
flag: &BoolFlag{Name: "flag", Value: true},
toParse: []string{"--flag", "false"},
expect: `--flag (default: true)`,
},
&flagDefaultTestCase{
name: "uint64",
flag: &Uint64Flag{Name: "flag", Value: 1},
toParse: []string{"--flag", "13"},
expect: `--flag value (default: 1)`,
},
}
for i, v := range cases {
set := flag.NewFlagSet("test", 0)
set.SetOutput(ioutil.Discard)
_ = v.flag.Apply(set)
if err := set.Parse(v.toParse); err != nil {
t.Error(err)
}
if got := v.flag.String(); got != v.expect {
t.Errorf("TestFlagDefaultValue %d %s\nexpect:%s\ngot:%s", i, v.name, v.expect, got)
}
}
}
func TestTimestampFlagApply_WithDestination(t *testing.T) {
var destination Timestamp
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: time.RFC3339, Destination: &destination}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)
err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"})
expect(t, err, nil)
expect(t, *fl.Destination.timestamp, expectedResult)
}

View File

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

View File

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

View File

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

View File

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

10
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.0-20190314233015-f79a8a8ca69d github.com/cpuguy83/go-md2man/v2 v2.0.1
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.4.0
) )
require github.com/russross/blackfriday/v2 v2.1.0 // indirect

18
go.sum
View File

@ -1,14 +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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

28
help.go
View File

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

View File

@ -54,6 +54,22 @@ func Test_ShowAppHelp_HideVersion(t *testing.T) {
} }
} }
func Test_ShowAppHelp_MultiLineDescription(t *testing.T) {
output := new(bytes.Buffer)
app := &App{Writer: output}
app.HideVersion = true
app.Description = "multi\n line"
c := NewContext(app, nil, nil)
_ = ShowAppHelp(c)
if !bytes.Contains(output.Bytes(), []byte("DESCRIPTION:\n multi\n line")) {
t.Errorf("expected\n%s\nto include\n%s", output.String(), "DESCRIPTION:\n multi\n line")
}
}
func Test_Help_Custom_Flags(t *testing.T) { func Test_Help_Custom_Flags(t *testing.T) {
oldFlag := HelpFlag oldFlag := HelpFlag
defer func() { defer func() {
@ -495,6 +511,36 @@ func TestShowSubcommandHelp_CommandUsageText(t *testing.T) {
} }
} }
func TestShowSubcommandHelp_MultiLine_CommandUsageText(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
UsageText: `This is a
multi
line
UsageText`,
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo", "frobbly", "--help"})
expected := `USAGE:
This is a
multi
line
UsageText
`
if !strings.Contains(output.String(), expected) {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) {
app := &App{ app := &App{
Commands: []*Command{ Commands: []*Command{
@ -519,6 +565,40 @@ func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) {
} }
} }
func TestShowSubcommandHelp_MultiLine_SubcommandUsageText(t *testing.T) {
app := &App{
Commands: []*Command{
{
Name: "frobbly",
Subcommands: []*Command{
{
Name: "bobbly",
UsageText: `This is a
multi
line
UsageText`,
},
},
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo", "frobbly", "bobbly", "--help"})
expected := `USAGE:
This is a
multi
line
UsageText
`
if !strings.Contains(output.String(), expected) {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowAppHelp_HiddenCommand(t *testing.T) { func TestShowAppHelp_HiddenCommand(t *testing.T) {
app := &App{ app := &App{
Commands: []*Command{ Commands: []*Command{
@ -764,6 +844,56 @@ VERSION:
} }
} }
func TestShowAppHelp_UsageText(t *testing.T) {
app := &App{
UsageText: "This is a sinlge line of UsageText",
Commands: []*Command{
{
Name: "frobbly",
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo"})
if !strings.Contains(output.String(), "This is a sinlge line of UsageText") {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestShowAppHelp_MultiLine_UsageText(t *testing.T) {
app := &App{
UsageText: `This is a
multi
line
App UsageText`,
Commands: []*Command{
{
Name: "frobbly",
},
},
}
output := &bytes.Buffer{}
app.Writer = output
_ = app.Run([]string{"foo"})
expected := `USAGE:
This is a
multi
line
App UsageText
`
if !strings.Contains(output.String(), expected) {
t.Errorf("expected output to include usage text; got: %q", output.String())
}
}
func TestHideHelpCommand(t *testing.T) { func TestHideHelpCommand(t *testing.T) {
app := &App{ app := &App{
HideHelpCommand: true, HideHelpCommand: true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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