Repository: urfave/cli Branch: main Commit: a107ee4e9d5a Files: 161 Total size: 890.6 KB Directory structure: gitextract_oit1zf28/ ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── question.md │ │ ├── v1-bug-report.md │ │ ├── v2-bug-report.md │ │ ├── v3-bug-report.md │ │ └── v3-feature-request.md │ ├── codecov.yml │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── lint.yml │ ├── publish-docs.yml │ └── test.yml ├── .gitignore ├── .golangci.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── args.go ├── args_test.go ├── autocomplete/ │ ├── bash_autocomplete │ ├── fish_autocomplete │ ├── powershell_autocomplete.ps1 │ └── zsh_autocomplete ├── category.go ├── cli.go ├── cli_test.go ├── command.go ├── command_parse.go ├── command_run.go ├── command_setup.go ├── command_stop_on_nth_arg_test.go ├── command_test.go ├── completion.go ├── completion_test.go ├── docs/ │ ├── CHANGELOG.md │ ├── CNAME │ ├── CONTRIBUTING.md │ ├── RELEASING.md │ ├── SECURITY.md │ ├── go.mod │ ├── go.sum │ ├── index.md │ ├── migrate-v1-to-v2.md │ ├── migrate-v2-to-v3.md │ ├── package.go │ ├── v1/ │ │ ├── examples/ │ │ │ ├── arguments.md │ │ │ ├── bash-completions.md │ │ │ ├── combining-short-options.md │ │ │ ├── exit-codes.md │ │ │ ├── flags.md │ │ │ ├── generated-help-text.md │ │ │ ├── greet.md │ │ │ ├── subcommands-categories.md │ │ │ ├── subcommands.md │ │ │ └── version-flag.md │ │ ├── getting-started.md │ │ └── migrating-to-v2.md │ ├── v2/ │ │ ├── examples/ │ │ │ ├── arguments.md │ │ │ ├── bash-completions.md │ │ │ ├── combining-short-options.md │ │ │ ├── exit-codes.md │ │ │ ├── flags.md │ │ │ ├── full-api-example.md │ │ │ ├── generated-help-text.md │ │ │ ├── greet.md │ │ │ ├── subcommands-categories.md │ │ │ ├── subcommands.md │ │ │ ├── suggestions.md │ │ │ ├── timestamp-flag.md │ │ │ └── version-flag.md │ │ ├── getting-started.md │ │ ├── migrating-from-older-releases.md │ │ └── migrating-to-v3.md │ └── v3/ │ ├── examples/ │ │ ├── arguments/ │ │ │ ├── advanced.md │ │ │ └── basics.md │ │ ├── completions/ │ │ │ ├── customizations.md │ │ │ └── shell-completions.md │ │ ├── exit-codes.md │ │ ├── flags/ │ │ │ ├── advanced.md │ │ │ ├── basics.md │ │ │ ├── short-options.md │ │ │ └── value-sources.md │ │ ├── full-api-example.md │ │ ├── greet.md │ │ ├── help/ │ │ │ ├── generated-help-text.md │ │ │ └── suggestions.md │ │ └── subcommands/ │ │ ├── basics.md │ │ └── categories.md │ ├── getting-started.md │ ├── index.md │ └── migrating-from-older-releases.md ├── docs.go ├── errors.go ├── errors_test.go ├── examples/ │ ├── example-cli/ │ │ └── example-cli.go │ └── example-hello-world/ │ └── example-hello-world.go ├── examples_test.go ├── fish.go ├── fish_test.go ├── flag.go ├── flag_bool.go ├── flag_bool_with_inverse.go ├── flag_bool_with_inverse_test.go ├── flag_duration.go ├── flag_ext.go ├── flag_float.go ├── flag_float_slice.go ├── flag_float_slice_test.go ├── flag_float_test.go ├── flag_generic.go ├── flag_impl.go ├── flag_int.go ├── flag_int_slice.go ├── flag_int_slice_test.go ├── flag_int_test.go ├── flag_map_impl.go ├── flag_mutex.go ├── flag_mutex_test.go ├── flag_number_slice.go ├── flag_number_slice_test.go ├── flag_slice_base.go ├── flag_string.go ├── flag_string_map.go ├── flag_string_slice.go ├── flag_test.go ├── flag_timestamp.go ├── flag_uint.go ├── flag_uint_slice.go ├── flag_uint_slice_test.go ├── flag_uint_test.go ├── flag_validation_test.go ├── funcs.go ├── go.mod ├── go.sum ├── godoc-current.txt ├── help.go ├── help_test.go ├── helpers_test.go ├── mkdocs-requirements.txt ├── mkdocs.yml ├── scripts/ │ └── build.go ├── sort.go ├── sort_test.go ├── staticcheck.conf ├── suggestions.go ├── suggestions_test.go ├── template.go ├── testdata/ │ ├── empty.yml │ ├── expected-doc-full.man │ ├── expected-doc-full.md │ ├── expected-doc-no-authors.md │ ├── expected-doc-no-commands.md │ ├── expected-doc-no-flags.md │ ├── expected-doc-no-usagetext.md │ ├── expected-fish-full.fish │ ├── expected-tabular-markdown-custom-app-path.md │ ├── expected-tabular-markdown-full.md │ └── godoc-v3.x.txt ├── value_source.go └── value_source_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # See https://help.github.com/articles/about-codeowners/ # for more info about CODEOWNERS file * @urfave/cli ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: ask a question about: ask a question - assume stackoverflow's guidelines apply here title: your question title goes here labels: 'kind/question, status/triage, area/v2' assignees: '' --- my question is... ================================================ FILE: .github/ISSUE_TEMPLATE/v1-bug-report.md ================================================ --- name: v1 bug report about: Create a report to help us fix v1 bugs title: 'your bug title goes here' labels: 'kind/bug, status/triage, area/v1' assignees: '' --- ## My urfave/cli version is _**( Put the version of urfave/cli that you are using here )**_ ## Checklist - [ ] 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://cli.urfave.org/v1/getting-started/). - [ ] 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 - 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 A clear and concise description of what the bug is. ## To reproduce Describe the steps or code required to reproduce the behavior ## Observed behavior What did you see happen immediately after the reproduction steps above? ## Expected behavior What would you have expected to happen immediately after the reproduction steps above? ## Additional context 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 you can reproduce this issue with a public CI system, please link a failing build here. ## 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, feel 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 ``` # paste `go version` output in here ``` ## Run `go env` and paste its output here ``` # paste `go env` output in here ``` ================================================ FILE: .github/ISSUE_TEMPLATE/v2-bug-report.md ================================================ --- name: v2 bug report about: Create a report to help us fix v2 bugs title: 'your bug title goes here' labels: 'kind/bug, area/v2, status/triage' assignees: '' --- ## My urfave/cli version is _**( Put the version of urfave/cli that you are using here )**_ ## Checklist - [ ] 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://cli.urfave.org/v2/getting-started/) - [ ] 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 - 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 A clear and concise description of what the bug is. ## To reproduce Describe the steps or code required to reproduce the behavior ## Observed behavior What did you see happen immediately after the reproduction steps above? ## Expected behavior What would you have expected to happen immediately after the reproduction steps above? ## Additional context 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 you can reproduce this issue with a public CI system, please link a failing build here. ## 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, feel free to create a pull request for it. ## Run `go version` and paste its output here ``` # paste `go version` output in here ``` ## Run `go env` and paste its output here ``` # paste `go env` output in here ``` ================================================ FILE: .github/ISSUE_TEMPLATE/v3-bug-report.md ================================================ --- name: v3 bug report about: Create a report to help us fix v3 bugs title: 'your bug title goes here' labels: 'kind/bug, area/v3, status/triage' assignees: '' --- ## My urfave/cli version is _**( Put the version of urfave/cli that you are using here )**_ ## Checklist - [ ] Are you running the latest v3 release? The list of releases is [here](https://github.com/urfave/cli/releases). - [ ] Did you check the manual for your release? The v3 manual is [here](https://cli.urfave.org/v3/getting-started/) - [ ] 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 - 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 A clear and concise description of what the bug is. ## To reproduce Describe the steps or code required to reproduce the behavior ## Observed behavior What did you see happen immediately after the reproduction steps above? ## Expected behavior What would you have expected to happen immediately after the reproduction steps above? ## Additional context 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 you can reproduce this issue with a public CI system, please link a failing build here. ## 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, feel free to create a pull request for it. ## Run `go version` and paste its output here ``` # paste `go version` output in here ``` ## Run `go env` and paste its output here ``` # paste `go env` output in here ``` ================================================ FILE: .github/ISSUE_TEMPLATE/v3-feature-request.md ================================================ --- name: v3 feature request about: Suggest an improvement to go into v3 title: 'your feature title goes here' labels: 'type/feature, area/v3, status/triage' assignees: '' --- ## Checklist * [ ] Are you running the latest v3 release? The list of releases is [here](https://github.com/urfave/cli/releases). * [ ] Did you check the manual for your release? The v3 manual is [here](https://github.com/urfave/cli/blob/main/docs/v3/index.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. ## What problem does this solve? 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 - 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 A detailed description of what you want to happen. ## Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. ================================================ FILE: .github/codecov.yml ================================================ comment: false coverage: status: project: default: threshold: 5% patch: default: threshold: 5% ignore: - examples - scripts ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: weekly - package-ecosystem: github-actions directory: / schedule: interval: weekly - package-ecosystem: pip directory: / schedule: interval: weekly groups: python-packages: patterns: ["*"] ================================================ FILE: .github/pull_request_template.md ================================================ ## What type of PR is this? _(REQUIRED)_ - bug - cleanup - documentation - feature ## What this PR does / why we need it: _(REQUIRED)_ ## Which issue(s) this PR fixes: _(REQUIRED)_ ## Special notes for your reviewer: _(fill-in or delete this section)_ ## Testing _(fill-in or delete this section)_ ## Release Notes _(REQUIRED)_ ```release-note ``` ================================================ FILE: .github/workflows/lint.yml ================================================ name: Run lints on: push: tags: - v3.* branches: - main pull_request: branches: - main permissions: contents: read jobs: golangci-lint: runs-on: ubuntu-24.04 steps: - name: Clone repository uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: stable - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: version: latest ================================================ FILE: .github/workflows/publish-docs.yml ================================================ name: publish docs on: push: branches: - main tags: - v3.* permissions: contents: read jobs: test-docs: name: test-docs runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: stable - name: Set PATH run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}" - run: make ensure-gfmrun - run: make gfmrun env: FLAGS: --walk docs/v3/ - run: make diffcheck publish: permissions: contents: write if: startswith(github.ref, 'refs/tags/') name: publish needs: [test-docs] runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 with: python-version: '3.13' cache: pip cache-dependency-path: mkdocs-requirements.txt - name: Ensure mkdocs is available run: make ensure-mkdocs - name: Set mkdocs remote run: make set-mkdocs-remote env: MKDOCS_REMOTE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy via mkdocs run: make deploy-mkdocs ================================================ FILE: .github/workflows/test.yml ================================================ name: Run tests on: push: branches: - main tags: - v3.* pull_request: branches: - main permissions: contents: read jobs: test: strategy: matrix: os: [ubuntu-24.04, macos-15, windows-2025] go: [stable, oldstable] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} - name: Set PATH run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}" - if: matrix.go == 'stable' && matrix.os == 'ubuntu-24.04' run: make ensure-goimports - if: matrix.go == 'stable' && matrix.os == 'ubuntu-24.04' run: make lint - run: make vet - run: make test - run: make check-binary-size - if: matrix.go == 'stable' && matrix.os == 'ubuntu-24.04' run: make generate - run: make diffcheck - if: matrix.go == 'stable' && matrix.os == 'ubuntu-24.04' run: make v3diff - if: success() && matrix.go == 'stable' && matrix.os == 'ubuntu-24.04' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true ================================================ FILE: .gitignore ================================================ *.coverprofile *.exe *.orig .*envrc .envrc .idea /.local/ /site/ coverage.txt examples/*/built-example vendor ================================================ FILE: .golangci.yaml ================================================ version: "2" formatters: enable: - gofumpt linters: enable: - makezero - misspell exclusions: presets: - std-error-handling ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting urfave-governance@googlegroups.com, a members-only group that is world-postable. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 urfave/cli maintainers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # NOTE: this Makefile is meant to provide a simplified entry point for humans to # run all of the critical steps to verify one's changes are harmonious in # nature. Keeping target bodies to one line each and abstaining from make magic # are very important so that maintainers and contributors can focus their # attention on files that are primarily Go. GO_RUN_BUILD := go run scripts/build.go .PHONY: all all: generate vet test check-binary-size gfmrun # NOTE: this is a special catch-all rule to run any of the commands # defined in scripts/build.go with optional arguments passed # via GFLAGS (global flags) and FLAGS (command-specific flags), e.g.: # # $ make test GFLAGS='--packages cli' %: $(GO_RUN_BUILD) $(GFLAGS) $* $(FLAGS) .PHONY: docs docs: mkdocs build .PHONY: serve-docs serve-docs: mkdocs serve ================================================ FILE: README.md ================================================ # Welcome to urfave/cli [![Go Reference][goreference_badge]][goreference_link] [![Go Report Card][goreportcard_badge]][goreportcard_link] [![codecov][codecov_badge]][codecov_link] [![Tests status][test_badge]][test_link] urfave/cli is a **declarative**, simple, fast, and fun package for building command line tools in Go featuring: - commands and subcommands with alias and prefix match support - flexible and permissive help system - dynamic shell completion for `bash`, `zsh`, `fish`, and `powershell` - no dependencies except Go standard library - input flags for simple types, slices of simple types, time, duration, and others - compound short flag support (`-a` `-b` `-c` can be shortened to `-abc`) - documentation generation in `man` and Markdown (supported via the [`urfave/cli-docs`][urfave/cli-docs] module) - input lookup from: - environment variables - plain text files - structured file formats (supported via the [`urfave/cli-altsrc`][urfave/cli-altsrc] module) ## Documentation See the hosted documentation website at . Contents of this website are built from the [`./docs`](./docs) directory. ## Support Check the [Q&A discussions]. If you don't find answer to your question, [create a new discussion]. If you found a bug or have a feature request, [create a new issue]. Please keep in mind that this project is run by unpaid volunteers. ### License See [`LICENSE`](./LICENSE). [test_badge]: https://github.com/urfave/cli/actions/workflows/test.yml/badge.svg [test_link]: https://github.com/urfave/cli/actions/workflows/test.yml [goreference_badge]: https://pkg.go.dev/badge/github.com/urfave/cli/v3.svg [goreference_link]: https://pkg.go.dev/github.com/urfave/cli/v3 [goreportcard_badge]: https://goreportcard.com/badge/github.com/urfave/cli/v3 [goreportcard_link]: https://goreportcard.com/report/github.com/urfave/cli/v3 [codecov_badge]: https://codecov.io/gh/urfave/cli/branch/main/graph/badge.svg?token=t9YGWLh05g [codecov_link]: https://codecov.io/gh/urfave/cli [Q&A discussions]: https://github.com/urfave/cli/discussions/categories/q-a [create a new discussion]: https://github.com/urfave/cli/discussions/new?category=q-a [urfave/cli-docs]: https://github.com/urfave/cli-docs [urfave/cli-altsrc]: https://github.com/urfave/cli-altsrc [create a new issue]: https://github.com/urfave/cli/issues/new/choose ================================================ FILE: args.go ================================================ package cli import ( "fmt" "time" ) type Args interface { // Get returns the nth argument, or else a blank string Get(n int) string // First returns the first argument, or else a blank string First() string // Tail returns the rest of the arguments (not the first one) // or else an empty string slice Tail() []string // Len returns the length of the wrapped slice Len() int // Present checks if there are any arguments present Present() bool // Slice returns a copy of the internal slice Slice() []string } type stringSliceArgs struct { v []string } func (a *stringSliceArgs) Get(n int) string { if len(a.v) > n { return a.v[n] } return "" } func (a *stringSliceArgs) First() string { return a.Get(0) } func (a *stringSliceArgs) Tail() []string { if a.Len() >= 2 { tail := a.v[1:] ret := make([]string, len(tail)) copy(ret, tail) return ret } return []string{} } func (a *stringSliceArgs) Len() int { return len(a.v) } func (a *stringSliceArgs) Present() bool { return a.Len() != 0 } func (a *stringSliceArgs) Slice() []string { ret := make([]string, len(a.v)) copy(ret, a.v) return ret } // Argument captures a positional argument that can // be parsed type Argument interface { // which this argument can be accessed using the given name HasName(string) bool // Parse the given args and return unparsed args and/or error Parse([]string) ([]string, error) // The usage template for this argument to use in help Usage() string // The Value of this Arg Get() any } // AnyArguments to differentiate between no arguments(nil) vs aleast one var AnyArguments = []Argument{ &StringArgs{ Max: -1, }, } type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // the name of this argument Value T `json:"value"` // the default value of this argument Destination *T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show Config C `json:"config"` // config for this argument similar to Flag Config value *T } func (a *ArgumentBase[T, C, VC]) HasName(s string) bool { return s == a.Name } func (a *ArgumentBase[T, C, VC]) Usage() string { if a.UsageText != "" { return a.UsageText } usageFormat := "%[1]s" return fmt.Sprintf(usageFormat, a.Name) } func (a *ArgumentBase[T, C, VC]) Parse(s []string) ([]string, error) { tracef("calling arg%[1] parse with args %[2]", a.Name, s) var vc VC var t T value := vc.Create(a.Value, &t, a.Config) a.value = &t tracef("attempting arg%[1] parse", &a.Name) if len(s) > 0 { if err := value.Set(s[0]); err != nil { return s, fmt.Errorf("invalid value %q for argument %s: %v", s[0], a.Name, err) } *a.value = value.Get().(T) tracef("set arg%[1] one value", a.Name, *a.value) } if a.Destination != nil { tracef("setting destination") *a.Destination = *a.value } if len(s) > 0 { return s[1:], nil } return s, nil } func (a *ArgumentBase[T, C, VC]) Get() any { if a.value != nil { return *a.value } return a.Value } // ArgumentsBase is a base type for slice arguments type ArgumentsBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // the name of this argument Value T `json:"value"` // the default value of this argument Destination *[]T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show Min int `json:"minTimes"` // the min num of occurrences of this argument Max int `json:"maxTimes"` // the max num of occurrences of this argument, set to -1 for unlimited Config C `json:"config"` // config for this argument similar to Flag Config values []T } func (a *ArgumentsBase[T, C, VC]) HasName(s string) bool { return s == a.Name } func (a *ArgumentsBase[T, C, VC]) Usage() string { if a.UsageText != "" { return a.UsageText } usageFormat := "" if a.Min == 0 { if a.Max == 1 { usageFormat = "[%[1]s]" } else { usageFormat = "[%[1]s ...]" } } else { usageFormat = "%[1]s [%[1]s ...]" } return fmt.Sprintf(usageFormat, a.Name) } func (a *ArgumentsBase[T, C, VC]) Parse(s []string) ([]string, error) { tracef("calling arg%[1] parse with args %[2]", &a.Name, s) if a.Max == 0 { return s, fmt.Errorf("args %s has max 0, not parsing argument", a.Name) } if a.Max != -1 && a.Min > a.Max { return s, fmt.Errorf("args %s has min[%d] > max[%d], not parsing argument", a.Name, a.Min, a.Max) } count := 0 var vc VC var t T value := vc.Create(a.Value, &t, a.Config) a.values = []T{} tracef("attempting arg%[1] parse", &a.Name) for _, arg := range s { if err := value.Set(arg); err != nil { return s, fmt.Errorf("invalid value %q for argument %s: %v", arg, a.Name, err) } tracef("set arg%[1] one value", &a.Name, value.Get().(T)) a.values = append(a.values, value.Get().(T)) count++ if count >= a.Max && a.Max > -1 { break } } if count < a.Min { return s, fmt.Errorf("sufficient count of arg %s not provided, given %d expected %d", a.Name, count, a.Min) } if a.Destination != nil { tracef("appending destination") *a.Destination = a.values // append(*a.Destination, a.values...) } return s[count:], nil } func (a *ArgumentsBase[T, C, VC]) Get() any { if a.values != nil { return a.values } return []T{} } type ( FloatArg = ArgumentBase[float64, NoConfig, floatValue[float64]] Float32Arg = ArgumentBase[float32, NoConfig, floatValue[float32]] Float64Arg = ArgumentBase[float64, NoConfig, floatValue[float64]] IntArg = ArgumentBase[int, IntegerConfig, intValue[int]] Int8Arg = ArgumentBase[int8, IntegerConfig, intValue[int8]] Int16Arg = ArgumentBase[int16, IntegerConfig, intValue[int16]] Int32Arg = ArgumentBase[int32, IntegerConfig, intValue[int32]] Int64Arg = ArgumentBase[int64, IntegerConfig, intValue[int64]] StringArg = ArgumentBase[string, StringConfig, stringValue] StringMapArgs = ArgumentBase[map[string]string, StringConfig, StringMap] TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue] UintArg = ArgumentBase[uint, IntegerConfig, uintValue[uint]] Uint8Arg = ArgumentBase[uint8, IntegerConfig, uintValue[uint8]] Uint16Arg = ArgumentBase[uint16, IntegerConfig, uintValue[uint16]] Uint32Arg = ArgumentBase[uint32, IntegerConfig, uintValue[uint32]] Uint64Arg = ArgumentBase[uint64, IntegerConfig, uintValue[uint64]] FloatArgs = ArgumentsBase[float64, NoConfig, floatValue[float64]] Float32Args = ArgumentsBase[float32, NoConfig, floatValue[float32]] Float64Args = ArgumentsBase[float64, NoConfig, floatValue[float64]] IntArgs = ArgumentsBase[int, IntegerConfig, intValue[int]] Int8Args = ArgumentsBase[int8, IntegerConfig, intValue[int8]] Int16Args = ArgumentsBase[int16, IntegerConfig, intValue[int16]] Int32Args = ArgumentsBase[int32, IntegerConfig, intValue[int32]] Int64Args = ArgumentsBase[int64, IntegerConfig, intValue[int64]] StringArgs = ArgumentsBase[string, StringConfig, stringValue] TimestampArgs = ArgumentsBase[time.Time, TimestampConfig, timestampValue] UintArgs = ArgumentsBase[uint, IntegerConfig, uintValue[uint]] Uint8Args = ArgumentsBase[uint8, IntegerConfig, uintValue[uint8]] Uint16Args = ArgumentsBase[uint16, IntegerConfig, uintValue[uint16]] Uint32Args = ArgumentsBase[uint32, IntegerConfig, uintValue[uint32]] Uint64Args = ArgumentsBase[uint64, IntegerConfig, uintValue[uint64]] ) func (c *Command) getArgValue(name string) any { tracef("command %s looking for args %s", c.Name, name) for _, arg := range c.Arguments { if arg.HasName(name) { tracef("command %s found args %s", c.Name, name) return arg.Get() } } tracef("command %s did not find args %s", c.Name, name) return nil } func arg[T any](name string, c *Command) T { val := c.getArgValue(name) if a, ok := val.(T); ok { return a } var zero T return zero } func (c *Command) StringArg(name string) string { return arg[string](name, c) } func (c *Command) StringArgs(name string) []string { return arg[[]string](name, c) } func (c *Command) FloatArg(name string) float64 { return arg[float64](name, c) } func (c *Command) FloatArgs(name string) []float64 { return arg[[]float64](name, c) } func (c *Command) Float32Arg(name string) float32 { return arg[float32](name, c) } func (c *Command) Float32Args(name string) []float32 { return arg[[]float32](name, c) } func (c *Command) Float64Arg(name string) float64 { return arg[float64](name, c) } func (c *Command) Float64Args(name string) []float64 { return arg[[]float64](name, c) } func (c *Command) IntArg(name string) int { return arg[int](name, c) } func (c *Command) IntArgs(name string) []int { return arg[[]int](name, c) } func (c *Command) Int8Arg(name string) int8 { return arg[int8](name, c) } func (c *Command) Int8Args(name string) []int8 { return arg[[]int8](name, c) } func (c *Command) Int16Arg(name string) int16 { return arg[int16](name, c) } func (c *Command) Int16Args(name string) []int16 { return arg[[]int16](name, c) } func (c *Command) Int32Arg(name string) int32 { return arg[int32](name, c) } func (c *Command) Int32Args(name string) []int32 { return arg[[]int32](name, c) } func (c *Command) Int64Arg(name string) int64 { return arg[int64](name, c) } func (c *Command) Int64Args(name string) []int64 { return arg[[]int64](name, c) } func (c *Command) UintArg(name string) uint { return arg[uint](name, c) } func (c *Command) Uint8Arg(name string) uint8 { return arg[uint8](name, c) } func (c *Command) Uint16Arg(name string) uint16 { return arg[uint16](name, c) } func (c *Command) Uint32Arg(name string) uint32 { return arg[uint32](name, c) } func (c *Command) Uint64Arg(name string) uint64 { return arg[uint64](name, c) } func (c *Command) UintArgs(name string) []uint { return arg[[]uint](name, c) } func (c *Command) Uint8Args(name string) []uint8 { return arg[[]uint8](name, c) } func (c *Command) Uint16Args(name string) []uint16 { return arg[[]uint16](name, c) } func (c *Command) Uint32Args(name string) []uint32 { return arg[[]uint32](name, c) } func (c *Command) Uint64Args(name string) []uint64 { return arg[[]uint64](name, c) } func (c *Command) TimestampArg(name string) time.Time { return arg[time.Time](name, c) } func (c *Command) TimestampArgs(name string) []time.Time { return arg[[]time.Time](name, c) } ================================================ FILE: args_test.go ================================================ package cli import ( "context" "testing" "time" "github.com/stretchr/testify/require" ) func TestArgNotSet(t *testing.T) { arg := &StringArg{ Name: "sa", Value: "foo", } require.Equal(t, "foo", arg.Get()) } func TestArgsMaxNotSet(t *testing.T) { arg := &StringArgs{ Name: "sa", Value: "foo", } cmd := buildMinimalTestCommand() cmd.Arguments = []Argument{arg} err := cmd.Run(buildTestContext(t), []string{"foo", "bar"}) require.ErrorContains(t, err, "args sa has max 0, not parsing argument") } func TestArgsMinGtMax(t *testing.T) { arg := &StringArgs{ Name: "sa", Value: "foo", Min: 2, Max: 1, } cmd := buildMinimalTestCommand() cmd.Arguments = []Argument{arg} err := cmd.Run(buildTestContext(t), []string{"foo", "bar"}) require.ErrorContains(t, err, "args sa has min[2] > max[1], not parsing argument") } func TestArgsFloatTypes(t *testing.T) { cmd := buildMinimalTestCommand() var fval float64 cmd.Arguments = []Argument{ &FloatArg{ Name: "ia", Destination: &fval, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "10"}) r := require.New(t) r.NoError(err) r.Equal(float64(10), fval) r.Equal(float64(10), cmd.FloatArg("ia")) r.Equal(float64(10), cmd.Float64Arg("ia")) r.Equal(float32(0), cmd.Float32Arg("ia")) r.Equal(float64(0), cmd.FloatArg("iab")) r.Equal(int8(0), cmd.Int8Arg("ia")) r.Equal(int16(0), cmd.Int16Arg("ia")) r.Equal(int32(0), cmd.Int32Arg("ia")) r.Equal(int64(0), cmd.Int64Arg("ia")) r.Empty(cmd.StringArg("ia")) r.Error(cmd.Run(buildTestContext(t), []string{"foo", "a"})) } func TestArgsIntTypes(t *testing.T) { cmd := buildMinimalTestCommand() var ival int cmd.Arguments = []Argument{ &IntArg{ Name: "ia", Destination: &ival, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "10"}) r := require.New(t) r.NoError(err) r.Equal(10, ival) r.Equal(10, cmd.IntArg("ia")) r.Equal(0, cmd.IntArg("iab")) r.Equal(int8(0), cmd.Int8Arg("ia")) r.Equal(int16(0), cmd.Int16Arg("ia")) r.Equal(int32(0), cmd.Int32Arg("ia")) r.Equal(int64(0), cmd.Int64Arg("ia")) r.Equal(float64(0), cmd.FloatArg("ia")) r.Empty(cmd.StringArg("ia")) r.Error(cmd.Run(buildTestContext(t), []string{"foo", "10.0"})) } func TestArgsFloatSliceTypes(t *testing.T) { cmd := buildMinimalTestCommand() var fval []float64 cmd.Arguments = []Argument{ &FloatArgs{ Name: "ia", Min: 1, Max: -1, Destination: &fval, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "10", "20", "30"}) r := require.New(t) r.NoError(err) r.Equal([]float64{10, 20, 30}, fval) r.Equal([]float64{10, 20, 30}, cmd.FloatArgs("ia")) r.Equal([]float64{10, 20, 30}, cmd.Float64Args("ia")) r.Nil(cmd.Float32Args("ia")) r.Error(cmd.Run(buildTestContext(t), []string{"foo", "10", "a"})) } func TestArgsIntSliceTypes(t *testing.T) { cmd := buildMinimalTestCommand() var ival []int cmd.Arguments = []Argument{ &IntArgs{ Name: "ia", Min: 1, Max: -1, Destination: &ival, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "10", "20", "30"}) r := require.New(t) r.NoError(err) r.Equal([]int{10, 20, 30}, ival) r.Equal([]int{10, 20, 30}, cmd.IntArgs("ia")) r.Nil(cmd.Int8Args("ia")) r.Nil(cmd.Int16Args("ia")) r.Nil(cmd.Int32Args("ia")) r.Nil(cmd.Int64Args("ia")) r.Error(cmd.Run(buildTestContext(t), []string{"foo", "10", "20.0"})) } func TestArgsUintTypes(t *testing.T) { cmd := buildMinimalTestCommand() var ival uint cmd.Arguments = []Argument{ &UintArg{ Name: "ia", Destination: &ival, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "10"}) r := require.New(t) r.NoError(err) r.Equal(uint(10), ival) r.Equal(uint(10), cmd.UintArg("ia")) r.Equal(uint(0), cmd.UintArg("iab")) r.Equal(uint8(0), cmd.Uint8Arg("ia")) r.Equal(uint16(0), cmd.Uint16Arg("ia")) r.Equal(uint32(0), cmd.Uint32Arg("ia")) r.Equal(uint64(0), cmd.Uint64Arg("ia")) r.Error(cmd.Run(buildTestContext(t), []string{"foo", "10.0"})) } func TestArgsUintSliceTypes(t *testing.T) { cmd := buildMinimalTestCommand() var ival []uint cmd.Arguments = []Argument{ &UintArgs{ Name: "ia", Min: 1, Max: -1, Destination: &ival, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "10", "20", "30"}) r := require.New(t) r.NoError(err) r.Equal([]uint{10, 20, 30}, ival) r.Equal([]uint{10, 20, 30}, cmd.UintArgs("ia")) r.Nil(cmd.Uint8Args("ia")) r.Nil(cmd.Uint16Args("ia")) r.Nil(cmd.Uint32Args("ia")) r.Nil(cmd.Uint64Args("ia")) r.Error(cmd.Run(buildTestContext(t), []string{"foo", "10", "20.0"})) } func TestArgumentsRootCommand(t *testing.T) { tests := []struct { name string args []string expectedIvals []int expectedUivals []uint expectedFvals []float64 errStr string }{ { name: "set ival", args: []string{"foo", "10"}, expectedIvals: []int{10}, expectedUivals: []uint{}, expectedFvals: []float64{}, }, { name: "set invalid ival", args: []string{"foo", "10.0"}, expectedIvals: []int{}, expectedUivals: []uint{}, expectedFvals: []float64{}, errStr: "strconv.ParseInt: parsing \"10.0\": invalid syntax", }, { name: "set ival uival", args: []string{"foo", "-10", "11"}, expectedIvals: []int{-10}, expectedUivals: []uint{11}, expectedFvals: []float64{}, }, { name: "set ival uival fval", args: []string{"foo", "-12", "14", "10.1"}, expectedIvals: []int{-12}, expectedUivals: []uint{14}, expectedFvals: []float64{10.1}, }, { name: "set ival uival multu fvals", args: []string{"foo", "-13", "12", "10.1", "11.09"}, expectedIvals: []int{-13}, expectedUivals: []uint{12}, expectedFvals: []float64{10.1, 11.09}, }, { name: "set fvals beyond max", args: []string{"foo", "13", "10", "10.1", "11.09", "12.1"}, expectedIvals: []int{13}, expectedUivals: []uint{10}, expectedFvals: []float64{10.1, 11.09}, errStr: "No help topic for '12.1'", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd := buildMinimalTestCommand() var ivals []int var uivals []uint var fvals []float64 cmd.Arguments = []Argument{ &IntArgs{ Name: "ia", Min: 1, Max: 1, Destination: &ivals, }, &UintArgs{ Name: "uia", Min: 1, Max: 1, Destination: &uivals, }, &FloatArgs{ Name: "fa", Min: 0, Max: 2, Destination: &fvals, }, } err := cmd.Run(buildTestContext(t), test.args) r := require.New(t) if test.errStr != "" { r.ErrorContains(err, test.errStr) } else { r.Equal(test.expectedIvals, ivals) } r.Equal(test.expectedIvals, cmd.IntArgs("ia")) r.Equal(test.expectedFvals, cmd.FloatArgs("fa")) r.Equal(test.expectedUivals, cmd.UintArgs("uia")) /*if test.expectedFvals != nil { r.Equal(test.expectedFvals, fvals) }*/ }) } } func TestArgumentsInvalidType(t *testing.T) { cmd := buildMinimalTestCommand() cmd.Arguments = []Argument{ &IntArgs{ Name: "ia", Min: 1, Max: 1, }, } r := require.New(t) r.Nil(cmd.StringArgs("ia")) r.Nil(cmd.FloatArgs("ia")) r.Nil(cmd.Int8Args("ia")) r.Nil(cmd.Int16Args("ia")) r.Nil(cmd.Int32Args("ia")) r.Nil(cmd.Int64Args("ia")) r.Equal(time.Time{}, cmd.TimestampArg("ia")) r.Nil(cmd.TimestampArgs("ia")) r.Nil(cmd.UintArgs("ia")) r.Nil(cmd.Uint8Args("ia")) r.Nil(cmd.Uint16Args("ia")) r.Nil(cmd.Uint32Args("ia")) r.Nil(cmd.Uint64Args("ia")) } func TestArgumentsSubcommand(t *testing.T) { tests := []struct { name string args []string expectedIval int expectedSvals []string expectedTVals []time.Time errStr string }{ { name: "insuff args", args: []string{"foo", "subcmd", "2006-01-02T15:04:05Z"}, errStr: "sufficient count of arg sa not provided, given 0 expected 1", }, { name: "set sval and tval", args: []string{"foo", "subcmd", "2006-01-02T15:04:05Z", "fubar"}, expectedIval: 10, expectedTVals: []time.Time{time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC)}, expectedSvals: []string{"fubar"}, }, { name: "set sval, tval and ival", args: []string{"foo", "subcmd", "--foo", "100", "2006-01-02T15:04:05Z", "fubar", "some"}, expectedIval: 100, expectedTVals: []time.Time{time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC)}, expectedSvals: []string{"fubar", "some"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd := buildMinimalTestCommand() var ival int var svals []string var tvals []time.Time cmd.Commands = []*Command{ { Name: "subcmd", Flags: []Flag{ &IntFlag{ Name: "foo", Value: 10, Destination: &ival, }, }, Arguments: []Argument{ &TimestampArgs{ Name: "ta", Min: 1, Max: 1, Destination: &tvals, Config: TimestampConfig{ Layouts: []string{time.RFC3339}, }, }, &StringArgs{ Name: "sa", Min: 1, Max: 3, Destination: &svals, }, }, }, } numUsageErrors := 0 cmd.Commands[0].OnUsageError = func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error { numUsageErrors++ return err } err := cmd.Run(buildTestContext(t), test.args) r := require.New(t) if test.errStr != "" { r.ErrorContains(err, test.errStr) r.Equal(1, numUsageErrors) } else { if test.expectedSvals != nil { r.Equal(test.expectedSvals, svals) r.Equal(test.expectedSvals, cmd.Commands[0].StringArgs("sa")) } if test.expectedTVals != nil { r.Equal(test.expectedTVals, tvals) r.Equal(test.expectedTVals, cmd.Commands[0].TimestampArgs("ta")) } r.Equal(test.expectedIval, ival) } }) } } func TestArgUsage(t *testing.T) { arg := &IntArg{ Name: "ia", } tests := []struct { name string usage string expected string }{ { name: "default", expected: "ia", }, { name: "usage", usage: "foo-usage", expected: "foo-usage", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { arg.UsageText = test.usage require.Equal(t, test.expected, arg.Usage()) }) } } func TestArgsUsage(t *testing.T) { arg := &IntArgs{ Name: "ia", Min: 0, Max: 1, } tests := []struct { name string min int max int usage string expected string }{ { name: "optional", min: 0, max: 1, expected: "[ia]", }, { name: "optional", min: 0, max: 1, usage: "[my optional usage]", expected: "[my optional usage]", }, { name: "zero or more", min: 0, max: 2, expected: "[ia ...]", }, { name: "one", min: 1, max: 1, expected: "ia [ia ...]", }, { name: "many", min: 2, max: 1, expected: "ia [ia ...]", }, { name: "many2", min: 2, max: 0, expected: "ia [ia ...]", }, { name: "unlimited", min: 2, max: -1, expected: "ia [ia ...]", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { arg.Min, arg.Max, arg.UsageText = test.min, test.max, test.usage require.Equal(t, test.expected, arg.Usage()) }) } } func TestSingleOptionalArg(t *testing.T) { tests := []struct { name string args []string argValue string exp string }{ { name: "no args", args: []string{"foo"}, exp: "", }, { name: "no arg with def value", args: []string{"foo"}, argValue: "bar", exp: "bar", }, { name: "one arg", args: []string{"foo", "zbar"}, exp: "zbar", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd := buildMinimalTestCommand() var s1 string arg := &StringArg{ Value: test.argValue, Destination: &s1, } cmd.Arguments = []Argument{ arg, } err := cmd.Run(buildTestContext(t), test.args) // r := require.New(t) r.NoError(err) r.Equal(test.exp, s1) }) } } func TestUnboundedArgs(t *testing.T) { arg := &StringArgs{ Min: 0, Max: -1, } tests := []struct { name string args []string defValues []string values []string expected []string }{ { name: "cmd accepts no args", args: []string{"foo"}, expected: []string{}, }, { name: "cmd uses given args", args: []string{"foo", "bar", "baz"}, expected: []string{"bar", "baz"}, }, { name: "cmd uses default values", args: []string{"foo"}, expected: []string{}, }, { name: "given args override default values", args: []string{"foo", "bar", "baz"}, values: []string{"zbar", "zbaz"}, expected: []string{"bar", "baz"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd := buildMinimalTestCommand() cmd.Arguments = []Argument{arg} arg.Destination = &test.values require.NoError(t, cmd.Run(context.Background(), test.args)) require.Equal(t, test.expected, *arg.Destination) }) } } ================================================ FILE: autocomplete/bash_autocomplete ================================================ #!/bin/bash # This is a shell completion script auto-generated by https://github.com/urfave/cli for bash. # Macs have bash3 for which the bash-completion package doesn't include # _init_completion. This is a minimal version of that function. __%[1]s_init_completion() { COMPREPLY=() _get_comp_words_by_ref "$@" cur prev words cword } __%[1]s_bash_autocomplete() { if [[ "${COMP_WORDS[0]}" != "source" ]]; then local cur opts base words COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" if declare -F _init_completion >/dev/null 2>&1; then _init_completion -n "=:" || return else __%[1]s_init_completion -n "=:" || return fi words=("${words[@]:0:$cword}") if [[ "$cur" == "-"* ]]; then requestComp="${words[*]} ${cur} --generate-shell-completion" else requestComp="${words[*]} --generate-shell-completion" fi opts=$(eval "${requestComp}" 2>/dev/null) COMPREPLY=($(compgen -W "${opts}" -- ${cur})) return 0 fi } complete -o bashdefault -o default -o nospace -F __%[1]s_bash_autocomplete %[1]s ================================================ FILE: autocomplete/fish_autocomplete ================================================ # This is a shell completion script auto-generated by https://github.com/urfave/cli for fish. function __%[1]_perform_completion # Extract all args except the last one set -l args (commandline -opc) # Extract the last arg (partial input) set -l lastArg (commandline -ct) set -l results ($args[1] $args[2..-1] $lastArg --generate-shell-completion 2> /dev/null) # Remove trailing empty lines for line in $results[-1..1] if test (string trim -- $line) = "" set results $results[1..-2] else break end end for line in $results if not string match -q -- "%[1]*" $line set -l parts (string split -m 1 ":" -- "$line") if test (count $parts) -eq 2 printf "%s\t%s\n" "$parts[1]" "$parts[2]" else printf "%s\n" "$line" end end end end # Clear existing completions for %[1] complete -c %[1] -e # Register completion function complete -c %[1] -f -a '(__%[1]_perform_completion)' ================================================ FILE: autocomplete/powershell_autocomplete.ps1 ================================================ $fn = $($MyInvocation.MyCommand.Name) $name = $fn -replace "(.*)\.ps1$", '$1' Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { param($commandName, $wordToComplete, $cursorPosition) $other = "$wordToComplete --generate-shell-completion" Invoke-Expression $other | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } ================================================ FILE: autocomplete/zsh_autocomplete ================================================ #compdef %[1]s compdef _%[1]s %[1]s # This is a shell completion script auto-generated by https://github.com/urfave/cli for zsh. _%[1]s() { local -a opts # Declare a local array local current current=${words[-1]} # -1 means "the last element" if [[ "$current" == "-"* ]]; then # Current word starts with a hyphen, so complete flags/options opts=("${(@f)$(${words[@]:0:#words[@]-1} ${current} --generate-shell-completion)}") else # Current word does not start with a hyphen, so complete subcommands opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}") fi if [[ "${opts[1]}" != "" ]]; then _describe 'values' opts else _files fi } # Don't run the completion function when being source-ed or eval-ed. # See https://github.com/urfave/cli/issues/1874 for discussion. if [ "$funcstack[1]" = "_%[1]s" ]; then _%[1]s fi ================================================ FILE: category.go ================================================ package cli import "sort" // CommandCategories interface allows for category manipulation type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command) // Categories returns a slice of categories sorted by name Categories() []CommandCategory } type commandCategories []*commandCategory func newCommandCategories() CommandCategories { ret := commandCategories([]*commandCategory{}) return &ret } func (c *commandCategories) Less(i, j int) bool { return lexicographicLess((*c)[i].Name(), (*c)[j].Name()) } func (c *commandCategories) Len() int { return len(*c) } func (c *commandCategories) Swap(i, j int) { (*c)[i], (*c)[j] = (*c)[j], (*c)[i] } func (c *commandCategories) AddCommand(category string, command *Command) { for _, commandCategory := range []*commandCategory(*c) { if commandCategory.name == category { commandCategory.commands = append(commandCategory.commands, command) return } } newVal := append(*c, &commandCategory{name: category, commands: []*Command{command}}) *c = newVal } func (c *commandCategories) Categories() []CommandCategory { ret := make([]CommandCategory, len(*c)) for i, cat := range *c { ret[i] = cat } return ret } // CommandCategory is a category containing commands. type CommandCategory interface { // Name returns the category name string Name() string // VisibleCommands returns a slice of the Commands with Hidden=false VisibleCommands() []*Command } type commandCategory struct { name string commands []*Command } func (c *commandCategory) Name() string { return c.name } func (c *commandCategory) VisibleCommands() []*Command { if c.commands == nil { c.commands = []*Command{} } var ret []*Command for _, command := range c.commands { if !command.Hidden { ret = append(ret, command) } } return ret } // FlagCategories interface allows for category manipulation type FlagCategories interface { // AddFlags adds a flag to a category, creating a new category if necessary. AddFlag(category string, fl Flag) // VisibleCategories returns a slice of visible flag categories sorted by name VisibleCategories() []VisibleFlagCategory } type defaultFlagCategories struct { m map[string]*defaultVisibleFlagCategory } func newFlagCategories() FlagCategories { return &defaultFlagCategories{ m: map[string]*defaultVisibleFlagCategory{}, } } func newFlagCategoriesFromFlags(fs []Flag) FlagCategories { fc := newFlagCategories() var categorized bool for _, fl := range fs { if cf, ok := fl.(CategorizableFlag); ok { visible := false if vf, ok := fl.(VisibleFlag); ok { visible = vf.IsVisible() } if cat := cf.GetCategory(); cat != "" && visible { fc.AddFlag(cat, fl) categorized = true } } } if categorized { for _, fl := range fs { if cf, ok := fl.(CategorizableFlag); ok { visible := false if vf, ok := fl.(VisibleFlag); ok { visible = vf.IsVisible() } if cf.GetCategory() == "" && visible { fc.AddFlag("", fl) } } } } return fc } func (f *defaultFlagCategories) AddFlag(category string, fl Flag) { if _, ok := f.m[category]; !ok { f.m[category] = &defaultVisibleFlagCategory{name: category, m: map[string]Flag{}} } f.m[category].m[fl.String()] = fl } func (f *defaultFlagCategories) VisibleCategories() []VisibleFlagCategory { catNames := []string{} for name := range f.m { catNames = append(catNames, name) } sort.Strings(catNames) ret := make([]VisibleFlagCategory, len(catNames)) for i, name := range catNames { ret[i] = f.m[name] } return ret } // VisibleFlagCategory is a category containing flags. type VisibleFlagCategory interface { // Name returns the category name string Name() string // Flags returns a slice of VisibleFlag sorted by name Flags() []Flag } type defaultVisibleFlagCategory struct { name string m map[string]Flag } func (fc *defaultVisibleFlagCategory) Name() string { return fc.name } func (fc *defaultVisibleFlagCategory) Flags() []Flag { vfNames := []string{} for flName, fl := range fc.m { if vf, ok := fl.(VisibleFlag); ok { if vf.IsVisible() { vfNames = append(vfNames, flName) } } } sort.Strings(vfNames) ret := make([]Flag, len(vfNames)) for i, flName := range vfNames { ret[i] = fc.m[flName] } return ret } ================================================ FILE: cli.go ================================================ // Package cli provides a minimal framework for creating and organizing command line // Go applications. cli is designed to be easy to understand and write, the most simple // cli application can be written as follows: // // func main() { // (&cli.Command{}).Run(context.Background(), os.Args) // } // // Of course this application does not do much, so let's make this an actual application: // // func main() { // cmd := &cli.Command{ // Name: "greet", // Usage: "say a greeting", // Action: func(c *cli.Context) error { // fmt.Println("Greetings") // return nil // }, // } // // cmd.Run(context.Background(), os.Args) // } package cli import ( "fmt" "os" "runtime" "strings" ) var isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on" func tracef(format string, a ...any) { if !isTracingOn { return } if !strings.HasSuffix(format, "\n") { format = format + "\n" } pc, file, line, _ := runtime.Caller(1) cf := runtime.FuncForPC(pc) fmt.Fprintf( os.Stderr, strings.Join([]string{ "## URFAVE CLI TRACE ", file, ":", fmt.Sprintf("%v", line), " ", fmt.Sprintf("(%s)", cf.Name()), " ", format, }, ""), a..., ) } ================================================ FILE: cli_test.go ================================================ package cli import ( "bytes" "context" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func expectFileContent(t *testing.T, file, got string) { data, err := os.ReadFile(file) // Ignore windows line endings data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) r := require.New(t) r.NoError(err) r.Equal(got, string(data)) } func buildTestContext(t *testing.T) context.Context { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) t.Cleanup(cancel) return ctx } func TestTracing(t *testing.T) { olderr := os.Stderr oldtracing := isTracingOn defer func() { os.Stderr = olderr isTracingOn = oldtracing }() file, err := os.CreateTemp(os.TempDir(), "cli*") assert.NoError(t, err) os.Stderr = file // Note we cant really set the env since the isTracingOn // is read at module startup so any changes mid code // wont take effect isTracingOn = false tracef("something") isTracingOn = true tracef("foothing") assert.NoError(t, file.Close()) b, err := os.ReadFile(file.Name()) assert.NoError(t, err) assert.Contains(t, string(b), "foothing") assert.NotContains(t, string(b), "something") } ================================================ FILE: command.go ================================================ package cli import ( "context" "fmt" "io" "slices" "strings" ) const ( // ignoreFlagPrefix is to ignore test flags when adding flags from other packages ignoreFlagPrefix = "test." commandContextKey = contextKey("cli.context") ) type contextKey string // Command contains everything needed to run an application that // accepts a string slice of arguments such as os.Args. A given // Command may contain Flags and sub-commands in Commands. type Command struct { // The name of the command Name string `json:"name"` // A list of aliases for the command Aliases []string `json:"aliases"` // A short description of the usage of this command Usage string `json:"usage"` // Text to override the USAGE section of help UsageText string `json:"usageText"` // A short description of the arguments of this command ArgsUsage string `json:"argsUsage"` // Version of the command Version string `json:"version"` // Longer explanation of how the command works Description string `json:"description"` // DefaultCommand is the (optional) name of a command // to run if no command names are passed as CLI arguments. DefaultCommand string `json:"defaultCommand"` // The category the command is part of Category string `json:"category"` // List of child commands Commands []*Command `json:"commands"` // List of flags to parse Flags []Flag `json:"flags"` // Boolean to hide built-in help command and help flag HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands EnableShellCompletion bool `json:"-"` // Shell Completion generation command name ShellCompletionCommandName string `json:"-"` // The function to call when checking for shell command completions ShellComplete ShellCompleteFunc `json:"-"` // The function to configure a shell completion command ConfigureShellCompletionCommand ConfigureShellCompletionCommand `json:"-"` // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc `json:"-"` // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc `json:"-"` // The function to call when this command is invoked Action ActionFunc `json:"-"` // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc `json:"-"` // Execute this function if a usage error occurs. OnUsageError OnUsageErrorFunc `json:"-"` // Execute this function when an invalid flag is accessed from the context InvalidFlagAccessHandler InvalidFlagAccessFunc `json:"-"` // Boolean to hide this command from help or completion Hidden bool `json:"hidden"` // List of all authors who contributed (string or fmt.Stringer) // TODO: ~string | fmt.Stringer when interface unions are available Authors []any `json:"authors"` // Copyright of the binary if any Copyright string `json:"copyright"` // Reader reader to write input to (useful for tests) Reader io.Reader `json:"-"` // Writer writer to write output to Writer io.Writer `json:"-"` // ErrWriter writes error output ErrWriter io.Writer `json:"-"` // ExitErrHandler processes any error encountered while running a Command before it is // returned to the caller. If no function is provided, HandleExitCoder is used as the // default behavior. ExitErrHandler ExitErrHandlerFunc `json:"-"` // Other custom info Metadata map[string]interface{} `json:"metadata"` // Carries a function which returns app specific info. ExtraInfo func() map[string]string `json:"-"` // CustomRootCommandHelpTemplate the text template for app help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomRootCommandHelpTemplate string `json:"-"` // SliceFlagSeparator is used to customize the separator for SliceFlag, the default is "," SliceFlagSeparator string `json:"sliceFlagSeparator"` // DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"` // MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "=" MapFlagKeyValueSeparator string `json:"mapFlagKeyValueSeparator"` // Boolean to enable short-option handling so user can combine several // single-character bool arguments into one // i.e. foobar -o -v -> foobar -ov UseShortOptionHandling bool `json:"useShortOptionHandling"` // Enable suggestions for commands and flags Suggest bool `json:"suggest"` // Allows global flags set by libraries which use flag.XXXVar(...) directly // to be parsed through this library AllowExtFlags bool `json:"allowExtFlags"` // Treat all flags as normal arguments if true SkipFlagParsing bool `json:"skipFlagParsing"` // CustomHelpTemplate the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string `json:"-"` // Use longest prefix match for commands PrefixMatchCommands bool `json:"prefixMatchCommands"` // Custom suggest command for matching SuggestCommandFunc SuggestCommandFunc `json:"-"` // Flag exclusion group MutuallyExclusiveFlags []MutuallyExclusiveFlags `json:"mutuallyExclusiveFlags"` // Arguments to parse for this command Arguments []Argument `json:"arguments"` // Whether to read arguments from stdin // applicable to root command only ReadArgsFromStdin bool `json:"readArgsFromStdin"` // StopOnNthArg provides v2-like behavior for specific commands by stopping // flag parsing after N positional arguments are encountered. When set to N, // all remaining arguments after the Nth positional argument will be treated // as arguments, not flags. // // A value of 0 means all arguments are treated as positional (no flag parsing). // A nil value means normal v3 flag parsing behavior (flags can appear anywhere). StopOnNthArg *int `json:"stopOnNthArg"` // categories contains the categorized commands and is populated on app startup categories CommandCategories // flagCategories contains the categorized flags and is populated on app startup flagCategories FlagCategories // flags that have been applied in current parse appliedFlags []Flag // flags that have been set setFlags map[Flag]struct{} // The parent of this command. This value will be nil for the // command at the root of the graph. parent *Command // parsed args parsedArgs Args // track state of error handling isInError bool // track state of defaults didSetupDefaults bool // whether in shell completion mode shellCompletion bool // whether global help flag was added globaHelpFlagAdded bool // whether global version flag was added globaVersionFlagAdded bool // whether this is a completion command isCompletionCommand bool } // FullName returns the full name of the command. // For commands with parents this ensures that the parent commands // are part of the command path. func (cmd *Command) FullName() string { namePath := []string{} if cmd.parent != nil { namePath = append(namePath, cmd.parent.FullName()) } return strings.Join(append(namePath, cmd.Name), " ") } func (cmd *Command) Command(name string) *Command { for _, subCmd := range cmd.Commands { if subCmd.HasName(name) { return subCmd } } return nil } func (cmd *Command) checkHelp() bool { tracef("checking if help is wanted (cmd=%[1]q)", cmd.Name) return HelpFlag != nil && slices.ContainsFunc(HelpFlag.Names(), cmd.Bool) } func (cmd *Command) allFlags() []Flag { var flags []Flag flags = append(flags, cmd.Flags...) for _, grpf := range cmd.MutuallyExclusiveFlags { for _, f1 := range grpf.Flags { flags = append(flags, f1...) } } return flags } // useShortOptionHandling traverses Lineage() for *any* ancestors // with UseShortOptionHandling func (cmd *Command) useShortOptionHandling() bool { for _, pCmd := range cmd.Lineage() { if pCmd.UseShortOptionHandling { return true } } return false } func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, error) { fl, parseErr := flagFromError(err) if parseErr != nil { return "", err } flags := cmd.Flags hideHelp := cmd.hideHelp() if commandName != "" { subCmd := cmd.Command(commandName) if subCmd == nil { return "", err } flags = subCmd.Flags hideHelp = hideHelp || subCmd.HideHelp } suggestion := SuggestFlag(flags, fl, hideHelp) if len(suggestion) == 0 { return "", err } return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil } // Names returns the names including short names and aliases. func (cmd *Command) Names() []string { return append([]string{cmd.Name}, cmd.Aliases...) } // HasName returns true if Command.Name matches given name func (cmd *Command) HasName(name string) bool { return slices.Contains(cmd.Names(), name) } // VisibleCategories returns a slice of categories and commands that are // Hidden=false func (cmd *Command) VisibleCategories() []CommandCategory { ret := []CommandCategory{} for _, category := range cmd.categories.Categories() { if visible := func() CommandCategory { if len(category.VisibleCommands()) > 0 { return category } return nil }(); visible != nil { ret = append(ret, visible) } } return ret } // VisibleCommands returns a slice of the Commands with Hidden=false func (cmd *Command) VisibleCommands() []*Command { var ret []*Command for _, command := range cmd.Commands { if command.Hidden || command.Name == helpName { continue } ret = append(ret, command) } return ret } // VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory { if cmd.flagCategories == nil { cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } return cmd.flagCategories.VisibleCategories() } // VisibleFlags returns a slice of the Flags with Hidden=false func (cmd *Command) VisibleFlags() []Flag { return visibleFlags(cmd.allFlags()) } func (cmd *Command) appendFlag(fl Flag) { if !hasFlag(cmd.Flags, fl) { cmd.Flags = append(cmd.Flags, fl) } } // VisiblePersistentFlags returns a slice of [LocalFlag] with Persistent=true and Hidden=false. func (cmd *Command) VisiblePersistentFlags() []Flag { var flags []Flag for _, fl := range cmd.Root().Flags { pfl, ok := fl.(LocalFlag) if !ok || pfl.IsLocal() { continue } flags = append(flags, fl) } return visibleFlags(flags) } func (cmd *Command) appendCommand(aCmd *Command) { if !slices.Contains(cmd.Commands, aCmd) { aCmd.parent = cmd cmd.Commands = append(cmd.Commands, aCmd) } } func (cmd *Command) handleExitCoder(ctx context.Context, err error) error { if cmd.parent != nil { return cmd.parent.handleExitCoder(ctx, err) } if cmd.ExitErrHandler != nil { cmd.ExitErrHandler(ctx, cmd, err) return err } HandleExitCoder(err) return err } func (cmd *Command) argsWithDefaultCommand(oldArgs Args) Args { rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...) newArgs := &stringSliceArgs{v: rawArgs} return newArgs } // Root returns the Command at the root of the graph func (cmd *Command) Root() *Command { if cmd.parent == nil { return cmd } return cmd.parent.Root() } func (cmd *Command) set(fName string, f Flag, val string) error { cmd.setFlags[f] = struct{}{} cmd.setMultiValueParsingConfig(f) if err := f.Set(fName, val); err != nil { return fmt.Errorf("invalid value %q for flag -%s: %v", val, fName, err) } return nil } func (cmd *Command) lFlag(name string) Flag { for _, f := range cmd.allFlags() { if slices.Contains(f.Names(), name) { tracef("flag found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return f } } return nil } func (cmd *Command) lookupFlag(name string) Flag { for _, pCmd := range cmd.Lineage() { if f := pCmd.lFlag(name); f != nil { return f } } tracef("flag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) cmd.onInvalidFlag(context.TODO(), name) return nil } // this looks up only allowed flags, i.e. local flags for current command // or persistent flags from ancestors func (cmd *Command) lookupAppliedFlag(name string) Flag { for _, f := range cmd.appliedFlags { if slices.Contains(f.Names(), name) { tracef("appliedFlag found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return f } } tracef("lookupAppliedflag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) cmd.onInvalidFlag(context.TODO(), name) return nil } func (cmd *Command) checkRequiredFlag(f Flag) (bool, string) { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { flagName := f.Names()[0] if !f.IsSet() { return false, flagName } } return true, "" } func (cmd *Command) checkAllRequiredFlags() requiredFlagsErr { for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent { if err := pCmd.checkRequiredFlags(); err != nil { return err } } return nil } func (cmd *Command) checkRequiredFlags() requiredFlagsErr { tracef("checking for required flags (cmd=%[1]q)", cmd.Name) missingFlags := []string{} for _, f := range cmd.appliedFlags { if ok, name := cmd.checkRequiredFlag(f); !ok { missingFlags = append(missingFlags, name) } } if len(missingFlags) != 0 { tracef("found missing required flags %[1]q (cmd=%[2]q)", missingFlags, cmd.Name) return &errRequiredFlags{missingFlags: missingFlags} } tracef("all required flags set (cmd=%[1]q)", cmd.Name) return nil } func (cmd *Command) onInvalidFlag(ctx context.Context, name string) { for cmd != nil { if cmd.InvalidFlagAccessHandler != nil { cmd.InvalidFlagAccessHandler(ctx, cmd, name) break } cmd = cmd.parent } } // NumFlags returns the number of flags set func (cmd *Command) NumFlags() int { tracef("numFlags numAppliedFlags %d", len(cmd.appliedFlags)) count := 0 for _, f := range cmd.appliedFlags { if f.IsSet() { count++ } } return count // cmd.flagSet.NFlag() } func (cmd *Command) setMultiValueParsingConfig(f Flag) { tracef("setMultiValueParsingConfig %T, %+v", f, f) if cf, ok := f.(multiValueParsingConfigSetter); ok { cf.setMultiValueParsingConfig(multiValueParsingConfig{ SliceFlagSeparator: cmd.SliceFlagSeparator, DisableSliceFlagSeparator: cmd.DisableSliceFlagSeparator, MapFlagKeyValueSeparator: cmd.MapFlagKeyValueSeparator, }) } } // Set sets a context flag to a value. func (cmd *Command) Set(name, value string) error { if f := cmd.lookupFlag(name); f != nil { cmd.setMultiValueParsingConfig(f) return f.Set(name, value) } return fmt.Errorf("no such flag -%s", name) } // IsSet determines if the flag was actually set func (cmd *Command) IsSet(name string) bool { fl := cmd.lookupFlag(name) if fl == nil { tracef("flag with name %[1]q NOT found; assuming not set (cmd=%[2]q)", name, cmd.Name) return false } isSet := fl.IsSet() if isSet { tracef("flag with name %[1]q is set (cmd=%[2]q)", name, cmd.Name) } else { tracef("flag with name %[1]q is no set (cmd=%[2]q)", name, cmd.Name) } return isSet } // LocalFlagNames returns a slice of flag names used in this // command. func (cmd *Command) LocalFlagNames() []string { names := []string{} // Check the flags which have been set via env or file for _, f := range cmd.allFlags() { if f.IsSet() { names = append(names, f.Names()...) } } // Sort out the duplicates since flag could be set via multiple // paths m := map[string]struct{}{} uniqNames := []string{} for _, name := range names { if _, ok := m[name]; !ok { m[name] = struct{}{} uniqNames = append(uniqNames, name) } } return uniqNames } // FlagNames returns a slice of flag names used by the this command // and all of its parent commands. func (cmd *Command) FlagNames() []string { names := cmd.LocalFlagNames() if cmd.parent != nil { names = append(cmd.parent.FlagNames(), names...) } return names } // Lineage returns *this* command and all of its ancestor commands // in order from child to parent func (cmd *Command) Lineage() []*Command { lineage := []*Command{cmd} if cmd.parent != nil { lineage = append(lineage, cmd.parent.Lineage()...) } return lineage } // Count returns the num of occurrences of this flag func (cmd *Command) Count(name string) int { if cf, ok := cmd.lookupFlag(name).(Countable); ok { return cf.Count() } return 0 } // Value returns the value of the flag corresponding to `name` func (cmd *Command) Value(name string) interface{} { if fs := cmd.lookupFlag(name); fs != nil { tracef("value found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return fs.Get() } tracef("value NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } // Args returns the command line arguments associated with the // command. func (cmd *Command) Args() Args { return cmd.parsedArgs } // NArg returns the number of the command line arguments. func (cmd *Command) NArg() int { return cmd.Args().Len() } func (cmd *Command) runFlagActions(ctx context.Context) error { tracef("runFlagActions") for fl := range cmd.setFlags { /*tracef("checking %v:%v", fl.Names(), fl.IsSet()) if !fl.IsSet() { continue }*/ //if pf, ok := fl.(LocalFlag); ok && !pf.IsLocal() { // continue //} if af, ok := fl.(ActionableFlag); ok { if err := af.RunAction(ctx, cmd); err != nil { return err } } } return nil } ================================================ FILE: command_parse.go ================================================ package cli import ( "fmt" "strings" "unicode" ) const ( providedButNotDefinedErrMsg = "flag provided but not defined: -" argumentNotProvidedErrMsg = "flag needs an argument: " ) // flagFromError tries to parse a provided flag from an error message. If the // parsing fails, it returns the input error and an empty string func flagFromError(err error) (string, error) { errStr := err.Error() trimmed := strings.TrimPrefix(errStr, providedButNotDefinedErrMsg) if errStr == trimmed { return "", err } return trimmed, nil } func (cmd *Command) parseFlags(args Args) (Args, error) { tracef("parsing flags from arguments %[1]q (cmd=%[2]q)", args, cmd.Name) cmd.setFlags = map[Flag]struct{}{} cmd.appliedFlags = cmd.allFlags() tracef("walking command lineage for persistent flags (cmd=%[1]q)", cmd.Name) for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent { tracef( "checking ancestor command=%[1]q for persistent flags (cmd=%[2]q)", pCmd.Name, cmd.Name, ) for _, fl := range pCmd.allFlags() { flNames := fl.Names() pfl, ok := fl.(LocalFlag) if !ok || pfl.IsLocal() { tracef("skipping non-persistent flag %[1]q (cmd=%[2]q)", flNames, cmd.Name) continue } tracef( "checking for applying persistent flag=%[1]q pCmd=%[2]q (cmd=%[3]q)", flNames, pCmd.Name, cmd.Name, ) applyPersistentFlag := true for _, name := range flNames { if cmd.lFlag(name) != nil { applyPersistentFlag = false break } } if !applyPersistentFlag { tracef("not applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) continue } tracef("applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) tracef("appending to applied flags flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) cmd.appliedFlags = append(cmd.appliedFlags, fl) } } tracef("parsing flags iteratively tail=%[1]q (cmd=%[2]q)", args.Tail(), cmd.Name) defer tracef("done parsing flags (cmd=%[1]q)", cmd.Name) posArgs := []string{} for rargs := args.Slice(); len(rargs) > 0; rargs = rargs[1:] { tracef("rearrange:1 (cmd=%[1]q) %[2]q", cmd.Name, rargs) firstArg := strings.TrimSpace(rargs[0]) if len(firstArg) == 0 { break } // stop parsing once we see a "--" if firstArg == "--" { posArgs = append(posArgs, rargs[1:]...) return &stringSliceArgs{posArgs}, nil } // Check if we've reached the Nth argument and should stop flag parsing if cmd.StopOnNthArg != nil && len(posArgs) == *cmd.StopOnNthArg { // Append current arg and all remaining args without parsing posArgs = append(posArgs, rargs[0:]...) return &stringSliceArgs{posArgs}, nil } // handle positional args if firstArg[0] != '-' { // positional argument probably tracef("rearrange-3 (cmd=%[1]q) check %[2]q", cmd.Name, firstArg) // if there is a command by that name let the command handle the // rest of the parsing if cmd.Command(firstArg) != nil { posArgs = append(posArgs, rargs...) return &stringSliceArgs{posArgs}, nil } posArgs = append(posArgs, firstArg) continue } numMinuses := 1 // this is same as firstArg == "-" if len(firstArg) == 1 { posArgs = append(posArgs, firstArg) break } shortOptionHandling := cmd.useShortOptionHandling() // stop parsing -- as short flags if firstArg[1] == '-' { numMinuses++ shortOptionHandling = false } else if !unicode.IsLetter(rune(firstArg[1])) { // this is not a flag tracef("parseFlags not a unicode letter. Stop parsing") posArgs = append(posArgs, rargs...) return &stringSliceArgs{posArgs}, nil } tracef("parseFlags (shortOptionHandling=%[1]q)", shortOptionHandling) flagName := firstArg[numMinuses:] flagVal := "" valFromEqual := false tracef("flagName:1 (fName=%[1]q)", flagName) if index := strings.Index(flagName, "="); index != -1 { flagVal = flagName[index+1:] flagName = flagName[:index] valFromEqual = true } tracef("flagName:2 (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal) f := cmd.lookupAppliedFlag(flagName) // found a flag matching given flagName if f != nil { tracef("Trying flag type (fName=%[1]q) (type=%[2]T)", flagName, f) if fb, ok := f.(boolFlag); ok && fb.IsBoolFlag() { if flagVal == "" { flagVal = "true" } tracef("parse Apply bool flag (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal) if err := cmd.set(flagName, f, flagVal); err != nil { return &stringSliceArgs{posArgs}, err } continue } tracef("processing non bool flag (fName=%[1]q)", flagName) // not a bool flag so need to get the next arg if flagVal == "" { if len(rargs) == 1 || valFromEqual { return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", argumentNotProvidedErrMsg, firstArg) } flagVal = rargs[1] rargs = rargs[1:] } tracef("setting non bool flag (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal) if err := cmd.set(flagName, f, flagVal); err != nil { return &stringSliceArgs{posArgs}, err } continue } // no flag lookup found and short handling is disabled if !shortOptionHandling { return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName) } // try to split the flags for index, c := range flagName { tracef("processing flag (fName=%[1]q)", string(c)) if sf := cmd.lookupFlag(string(c)); sf == nil { return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName) } else if fb, ok := sf.(boolFlag); ok && fb.IsBoolFlag() { fv := flagVal if index == (len(flagName)-1) && flagVal == "" { fv = "true" } if fv == "" { fv = "true" } if err := cmd.set(flagName, sf, fv); err != nil { tracef("processing flag.2 (fName=%[1]q)", string(c)) return &stringSliceArgs{posArgs}, err } } else if index == len(flagName)-1 { // last flag can take an arg if flagVal == "" { if len(rargs) == 1 { return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", argumentNotProvidedErrMsg, string(c)) } flagVal = rargs[1] rargs = rargs[1:] } tracef("parseFlags (flagName %[1]q) (flagVal %[2]q)", flagName, flagVal) if err := cmd.set(flagName, sf, flagVal); err != nil { tracef("processing flag.4 (fName=%[1]q)", string(c)) return &stringSliceArgs{posArgs}, err } } } } tracef("returning-2 (cmd=%[1]q) args %[2]q", cmd.Name, posArgs) return &stringSliceArgs{posArgs}, nil } ================================================ FILE: command_run.go ================================================ package cli import ( "bufio" "context" "fmt" "io" "slices" "unicode" ) func (cmd *Command) parseArgsFromStdin() ([]string, error) { type state int const ( stateSearchForToken state = -1 stateSearchForString state = 0 ) st := stateSearchForToken linenum := 1 token := "" args := []string{} breader := bufio.NewReader(cmd.Reader) outer: for { ch, _, err := breader.ReadRune() if err == io.EOF { switch st { case stateSearchForToken: if token != "--" { args = append(args, token) } case stateSearchForString: // make sure string is not empty for _, t := range token { if !unicode.IsSpace(t) { args = append(args, token) } } } break outer } if err != nil { return nil, err } switch st { case stateSearchForToken: if unicode.IsSpace(ch) || ch == '"' { if ch == '\n' { linenum++ } if token != "" { // end the processing here if token == "--" { break outer } args = append(args, token) token = "" } if ch == '"' { st = stateSearchForString } continue } token += string(ch) case stateSearchForString: if ch != '"' { token += string(ch) } else { if token != "" { args = append(args, token) token = "" } /*else { //TODO. Should we pass in empty strings ? }*/ st = stateSearchForToken } } } tracef("parsed stdin args as %v (cmd=%[2]q)", args, cmd.Name) return args, nil } // Run is the entry point to the command graph. The positional // arguments are parsed according to the Flag and Command // definitions and the matching Action functions are run. func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { _, deferErr = cmd.run(ctx, osArgs) return deferErr } func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context, deferErr error) { tracef("running with arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) cmd.setupDefaults(osArgs) // Validate StopOnNthArg if cmd.StopOnNthArg != nil && *cmd.StopOnNthArg < 0 { return ctx, fmt.Errorf("StopOnNthArg must be non-negative, got %d", *cmd.StopOnNthArg) } if v, ok := ctx.Value(commandContextKey).(*Command); ok { tracef("setting parent (cmd=%[1]q) command from context.Context value (cmd=%[2]q)", v.Name, cmd.Name) cmd.parent = v } if cmd.parent == nil { if cmd.ReadArgsFromStdin { if args, err := cmd.parseArgsFromStdin(); err != nil { return ctx, err } else { osArgs = append(osArgs, args...) } } // handle the completion flag separately from the flagset since // completion could be attempted after a flag, but before its value was put // on the command line. this causes the flagset to interpret the completion // flag name as the value of the flag before it which is undesirable // note that we can only do this because the shell autocomplete function // always appends the completion flag at the end of the command tracef("checking osArgs %v (cmd=%[2]q)", osArgs, cmd.Name) cmd.shellCompletion, osArgs = checkShellCompleteFlag(cmd, osArgs) tracef("setting cmd.shellCompletion=%[1]v from checkShellCompleteFlag (cmd=%[2]q)", cmd.shellCompletion && cmd.EnableShellCompletion, cmd.Name) cmd.shellCompletion = cmd.EnableShellCompletion && cmd.shellCompletion } tracef("using post-checkShellCompleteFlag arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) tracef("setting self as cmd in context (cmd=%[1]q)", cmd.Name) ctx = context.WithValue(ctx, commandContextKey, cmd) if cmd.parent == nil { cmd.setupCommandGraph() } var rargs Args = &stringSliceArgs{v: osArgs} var args Args = &stringSliceArgs{rargs.Tail()} if cmd.isCompletionCommand || cmd.Name == helpName { tracef("special command detected, skipping pre-parse (cmd=%[1]q)", cmd.Name) cmd.parsedArgs = args return ctx, cmd.Action(ctx, cmd) } for _, f := range cmd.allFlags() { if err := f.PreParse(); err != nil { return ctx, err } } var err error if cmd.SkipFlagParsing { tracef("skipping flag parsing (cmd=%[1]q)", cmd.Name) cmd.parsedArgs = args } else { cmd.parsedArgs, err = cmd.parseFlags(args) } tracef("using post-parse arguments %[1]q (cmd=%[2]q)", args, cmd.Name) if checkCompletions(ctx, cmd) { return ctx, nil } if err != nil { tracef("setting deferErr from %[1]q (cmd=%[2]q)", err, cmd.Name) deferErr = err cmd.isInError = true if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) err = cmd.handleExitCoder(ctx, err) return ctx, err } fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) if cmd.Suggest { if suggestion, err := cmd.suggestFlagFromError(err, ""); err == nil { fmt.Fprintf(cmd.Root().ErrWriter, "%s", suggestion) } } if !cmd.hideHelp() { if cmd.parent == nil { tracef("running ShowRootCommandHelp") if err := ShowRootCommandHelp(cmd); err != nil { tracef("SILENTLY IGNORING ERROR running ShowRootCommandHelp %[1]v (cmd=%[2]q)", err, cmd.Name) } } else { tracef("running ShowCommandHelp with %[1]q", cmd.Name) if err := ShowCommandHelp(ctx, cmd, cmd.Name); err != nil { tracef("SILENTLY IGNORING ERROR running ShowCommandHelp with %[1]q %[2]v", cmd.Name, err) } } } return ctx, err } if cmd.checkHelp() { return ctx, helpCommandAction(ctx, cmd) } else { tracef("no help is wanted (cmd=%[1]q)", cmd.Name) } if cmd.parent == nil && !cmd.HideVersion && checkVersion(cmd) { ShowVersion(cmd) return ctx, nil } for _, flag := range cmd.allFlags() { isSet := flag.IsSet() if err := flag.PostParse(); err != nil { return ctx, err } // add env set flags here if !isSet && flag.IsSet() { cmd.setFlags[flag] = struct{}{} } } if cmd.After != nil && !cmd.Root().shellCompletion { defer func() { if err := cmd.After(ctx, cmd); err != nil { err = cmd.handleExitCoder(ctx, err) if deferErr != nil { deferErr = newMultiError(deferErr, err) } else { deferErr = err } } }() } // Walk the parent chain to check mutually exclusive flag groups // defined on ancestor commands, since persistent flags are inherited. for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent { for _, grp := range pCmd.MutuallyExclusiveFlags { if err := grp.check(cmd); err != nil { if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) } else { _ = ShowSubcommandHelp(cmd) } return ctx, err } } } var subCmd *Command if cmd.parsedArgs.Present() { tracef("checking positional args %[1]q (cmd=%[2]q)", cmd.parsedArgs, cmd.Name) name := cmd.parsedArgs.First() tracef("using first positional argument as sub-command name=%[1]q (cmd=%[2]q)", name, cmd.Name) if cmd.SuggestCommandFunc != nil && name != "--" { name = cmd.SuggestCommandFunc(cmd.Commands, name) tracef("suggested command name=%1[q] (cmd=%[2]q)", name, cmd.Name) } subCmd = cmd.Command(name) if subCmd == nil { hasDefault := cmd.DefaultCommand != "" isFlagName := slices.Contains(cmd.FlagNames(), name) if hasDefault { tracef("using default command=%[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name) } if isFlagName || hasDefault { argsWithDefault := cmd.argsWithDefaultCommand(cmd.parsedArgs) tracef("using default command args=%[1]q (cmd=%[2]q)", argsWithDefault, cmd.Name) subCmd = cmd.Command(argsWithDefault.First()) cmd.parsedArgs = argsWithDefault } } } else if cmd.DefaultCommand != "" { tracef("no positional args present; checking default command %[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name) if dc := cmd.Command(cmd.DefaultCommand); dc != cmd { subCmd = dc } } // If a subcommand has been resolved, let it handle the remaining execution. if subCmd != nil { tracef("running sub-command %[1]q with arguments %[2]q (cmd=%[3]q)", subCmd.Name, cmd.Args(), cmd.Name) // It is important that we overwrite the ctx variable in the current // function so any defer'd functions use the new context returned // from the sub command. ctx, err = subCmd.run(ctx, cmd.Args().Slice()) return ctx, err } // This code path is the innermost command execution. Here we actually // perform the command action. // // First, resolve the chain of nested commands up to the parent. var cmdChain []*Command for p := cmd; p != nil; p = p.parent { cmdChain = append(cmdChain, p) } slices.Reverse(cmdChain) // Run Before actions in order. for _, cmd := range cmdChain { if cmd.Before == nil { continue } if bctx, err := cmd.Before(ctx, cmd); err != nil { deferErr = cmd.handleExitCoder(ctx, err) return ctx, deferErr } else if bctx != nil { ctx = bctx } } // Run flag actions in order. // These take a context, so this has to happen after Before actions. for _, cmd := range cmdChain { tracef("running flag actions (cmd=%[1]q)", cmd.Name) if err := cmd.runFlagActions(ctx); err != nil { deferErr = cmd.handleExitCoder(ctx, err) return ctx, deferErr } } if err := cmd.checkAllRequiredFlags(); err != nil { cmd.isInError = true if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) } else { _ = ShowSubcommandHelp(cmd) } return ctx, err } // Run the command action. if len(cmd.Arguments) > 0 { rargs := cmd.Args().Slice() tracef("calling argparse with %[1]v", rargs) for _, arg := range cmd.Arguments { var err error rargs, err = arg.Parse(rargs) if err != nil { tracef("calling with %[1]v (cmd=%[2]q)", err, cmd.Name) if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) } err = cmd.handleExitCoder(ctx, err) return ctx, err } } cmd.parsedArgs = &stringSliceArgs{v: rargs} } if err := cmd.Action(ctx, cmd); err != nil { tracef("calling handleExitCoder with %[1]v (cmd=%[2]q)", err, cmd.Name) deferErr = cmd.handleExitCoder(ctx, err) } tracef("returning deferErr (cmd=%[1]q) %[2]q", cmd.Name, deferErr) return ctx, deferErr } ================================================ FILE: command_setup.go ================================================ package cli import ( "flag" "os" "path/filepath" "sort" "strings" ) func (cmd *Command) setupDefaults(osArgs []string) { if cmd.didSetupDefaults { tracef("already did setup (cmd=%[1]q)", cmd.Name) return } cmd.didSetupDefaults = true isRoot := cmd.parent == nil tracef("isRoot? %[1]v (cmd=%[2]q)", isRoot, cmd.Name) if cmd.ShellComplete == nil { tracef("setting default ShellComplete (cmd=%[1]q)", cmd.Name) cmd.ShellComplete = DefaultCompleteWithFlags } if cmd.Name == "" && isRoot { name := filepath.Base(osArgs[0]) tracef("setting cmd.Name from first arg basename (cmd=%[1]q)", name) cmd.Name = name } if cmd.Usage == "" && isRoot { tracef("setting default Usage (cmd=%[1]q)", cmd.Name) cmd.Usage = "A new cli application" } if cmd.Version == "" { tracef("setting HideVersion=true due to empty Version (cmd=%[1]q)", cmd.Name) cmd.HideVersion = true } if cmd.Action == nil { tracef("setting default Action as help command action (cmd=%[1]q)", cmd.Name) cmd.Action = helpCommandAction } if cmd.Reader == nil { tracef("setting default Reader as os.Stdin (cmd=%[1]q)", cmd.Name) cmd.Reader = os.Stdin } if cmd.Writer == nil { tracef("setting default Writer as os.Stdout (cmd=%[1]q)", cmd.Name) cmd.Writer = os.Stdout } if cmd.ErrWriter == nil { tracef("setting default ErrWriter as os.Stderr (cmd=%[1]q)", cmd.Name) cmd.ErrWriter = os.Stderr } if cmd.AllowExtFlags { tracef("visiting all flags given AllowExtFlags=true (cmd=%[1]q)", cmd.Name) // add global flags added by other packages flag.VisitAll(func(f *flag.Flag) { // skip test flags if !strings.HasPrefix(f.Name, ignoreFlagPrefix) { cmd.Flags = append(cmd.Flags, &extFlag{f}) } }) } for _, subCmd := range cmd.Commands { tracef("setting sub-command (cmd=%[1]q) parent as self (cmd=%[2]q)", subCmd.Name, cmd.Name) subCmd.parent = cmd } cmd.ensureHelp() if !cmd.HideVersion && isRoot { tracef("appending version flag (cmd=%[1]q)", cmd.Name) if !cmd.globaVersionFlagAdded { var localVersionFlag Flag if globalVersionFlag, ok := VersionFlag.(*BoolFlag); ok { flag := *globalVersionFlag localVersionFlag = &flag } else { localVersionFlag = VersionFlag } cmd.appendFlag(localVersionFlag) cmd.globaVersionFlagAdded = true } } if cmd.PrefixMatchCommands && cmd.SuggestCommandFunc == nil { tracef("setting default SuggestCommandFunc (cmd=%[1]q)", cmd.Name) cmd.SuggestCommandFunc = suggestCommand } if isRoot && cmd.EnableShellCompletion || cmd.ConfigureShellCompletionCommand != nil { completionCommand := buildCompletionCommand(cmd.Name) if cmd.ShellCompletionCommandName != "" { tracef( "setting completion command name (%[1]q) from "+ "cmd.ShellCompletionCommandName (cmd=%[2]q)", cmd.ShellCompletionCommandName, cmd.Name, ) completionCommand.Name = cmd.ShellCompletionCommandName } tracef("appending completionCommand (cmd=%[1]q)", cmd.Name) cmd.appendCommand(completionCommand) if cmd.ConfigureShellCompletionCommand != nil { cmd.ConfigureShellCompletionCommand(completionCommand) } } tracef("setting command categories (cmd=%[1]q)", cmd.Name) cmd.categories = newCommandCategories() for _, subCmd := range cmd.Commands { cmd.categories.AddCommand(subCmd.Category, subCmd) } tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) for _, grp := range cmd.MutuallyExclusiveFlags { grp.propagateCategory() } tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) if cmd.Metadata == nil { tracef("setting default Metadata (cmd=%[1]q)", cmd.Name) cmd.Metadata = map[string]any{} } cmd.setFlags = map[Flag]struct{}{} } func (cmd *Command) setupCommandGraph() { tracef("setting up command graph (cmd=%[1]q)", cmd.Name) for _, subCmd := range cmd.Commands { subCmd.parent = cmd subCmd.setupSubcommand() subCmd.setupCommandGraph() } } func (cmd *Command) setupSubcommand() { tracef("setting up self as sub-command (cmd=%[1]q)", cmd.Name) cmd.ensureHelp() tracef("setting command categories (cmd=%[1]q)", cmd.Name) cmd.categories = newCommandCategories() for _, subCmd := range cmd.Commands { cmd.categories.AddCommand(subCmd.Category, subCmd) } tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) for _, grp := range cmd.MutuallyExclusiveFlags { grp.propagateCategory() } tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } func (cmd *Command) hideHelp() bool { tracef("hide help (cmd=%[1]q)", cmd.Name) for c := cmd; c != nil; c = c.parent { if c.HideHelp { return true } } return false } func (cmd *Command) ensureHelp() { tracef("ensuring help (cmd=%[1]q)", cmd.Name) helpCommand := buildHelpCommand(true) if !cmd.hideHelp() { if cmd.Command(helpCommand.Name) == nil { if !cmd.HideHelpCommand { tracef("appending helpCommand (cmd=%[1]q)", cmd.Name) cmd.appendCommand(helpCommand) } } if HelpFlag != nil { if !cmd.globaHelpFlagAdded { var localHelpFlag Flag if globalHelpFlag, ok := HelpFlag.(*BoolFlag); ok { flag := *globalHelpFlag localHelpFlag = &flag } else { localHelpFlag = HelpFlag } tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name) cmd.appendFlag(localHelpFlag) cmd.globaHelpFlagAdded = true } else { tracef("HelpFlag already added, skip (cmd=%[1]q)", cmd.Name) } } } } ================================================ FILE: command_stop_on_nth_arg_test.go ================================================ package cli import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCommand_StopOnNthArg(t *testing.T) { tests := []struct { name string stopOnNthArg *int testArgs []string expectedArgs []string expectedFlag string expectedBool bool }{ { name: "nil StopOnNthArg - normal parsing", stopOnNthArg: nil, testArgs: []string{"cmd", "--flag", "value", "arg1", "--bool", "arg2"}, expectedArgs: []string{"arg1", "arg2"}, expectedFlag: "value", expectedBool: true, }, { name: "stop after 0 args - all become args", stopOnNthArg: intPtr(0), testArgs: []string{"cmd", "--flag", "value", "arg1", "--bool", "arg2"}, expectedArgs: []string{"--flag", "value", "arg1", "--bool", "arg2"}, expectedFlag: "", expectedBool: false, }, { name: "stop after 1 arg", stopOnNthArg: intPtr(1), testArgs: []string{"cmd", "--flag", "value", "arg1", "--bool", "arg2"}, expectedArgs: []string{"arg1", "--bool", "arg2"}, expectedFlag: "value", expectedBool: false, }, { name: "stop after 2 args", stopOnNthArg: intPtr(2), testArgs: []string{"cmd", "--flag", "value", "arg1", "arg2", "--bool", "arg3"}, expectedArgs: []string{"arg1", "arg2", "--bool", "arg3"}, expectedFlag: "value", expectedBool: false, }, { name: "mixed flags and args - stop after 1", stopOnNthArg: intPtr(1), testArgs: []string{"cmd", "--flag", "value", "--bool", "arg1", "--flag2", "value2"}, expectedArgs: []string{"arg1", "--flag2", "value2"}, expectedFlag: "value", expectedBool: true, }, { name: "args before flags - stop after 1", stopOnNthArg: intPtr(1), testArgs: []string{"cmd", "arg1", "--flag", "value", "--bool"}, expectedArgs: []string{"arg1", "--flag", "value", "--bool"}, expectedFlag: "", expectedBool: false, }, { name: "ssh command example", stopOnNthArg: intPtr(1), testArgs: []string{"ssh", "machine-name", "ls", "-la"}, expectedArgs: []string{"machine-name", "ls", "-la"}, expectedFlag: "", expectedBool: false, }, { name: "with double dash terminator", stopOnNthArg: intPtr(1), testArgs: []string{"cmd", "--flag", "value", "--", "arg1", "--not-a-flag"}, expectedArgs: []string{"arg1", "--not-a-flag"}, expectedFlag: "value", expectedBool: false, }, { name: "stop after large number of args", stopOnNthArg: intPtr(100), testArgs: []string{"cmd", "--flag", "value", "arg1", "arg2", "--bool"}, expectedArgs: []string{"arg1", "arg2"}, expectedFlag: "value", expectedBool: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var args Args var flagValue string var boolValue bool cmd := &Command{ Name: "test", StopOnNthArg: tt.stopOnNthArg, Flags: []Flag{ &StringFlag{Name: "flag", Destination: &flagValue}, &StringFlag{Name: "flag2"}, &BoolFlag{Name: "bool", Destination: &boolValue}, }, Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, } require.NoError(t, cmd.Run(buildTestContext(t), tt.testArgs)) assert.Equal(t, tt.expectedArgs, args.Slice()) assert.Equal(t, tt.expectedFlag, flagValue) assert.Equal(t, tt.expectedBool, boolValue) }) } } func TestCommand_StopOnNthArg_WithSubcommands(t *testing.T) { tests := []struct { name string parentStopOnNthArg *int subStopOnNthArg *int testArgs []string expectedParentArgs []string expectedSubArgs []string expectedSubFlag string }{ { name: "parent normal, subcommand stops after 0", parentStopOnNthArg: nil, subStopOnNthArg: intPtr(0), testArgs: []string{"parent", "sub", "--subflag", "value", "subarg", "--not-parsed"}, expectedParentArgs: []string{}, expectedSubArgs: []string{"--subflag", "value", "subarg", "--not-parsed"}, expectedSubFlag: "", }, { name: "parent normal, subcommand stops after 1", parentStopOnNthArg: nil, subStopOnNthArg: intPtr(1), testArgs: []string{"parent", "sub", "--subflag", "value", "subarg", "--not-parsed"}, expectedParentArgs: []string{}, expectedSubArgs: []string{"subarg", "--not-parsed"}, expectedSubFlag: "value", }, { name: "parent normal, subcommand stops after 2", parentStopOnNthArg: nil, subStopOnNthArg: intPtr(2), testArgs: []string{"parent", "sub", "--subflag", "value", "subarg1", "subarg2", "--not-parsed"}, expectedParentArgs: []string{}, expectedSubArgs: []string{"subarg1", "subarg2", "--not-parsed"}, expectedSubFlag: "value", }, { name: "parent normal, subcommand never stops (high StopOnNthArg)", parentStopOnNthArg: nil, subStopOnNthArg: intPtr(100), testArgs: []string{"parent", "sub", "--subflag", "value1", "arg1", "arg2", "--subflag", "value2"}, expectedParentArgs: []string{}, expectedSubArgs: []string{"arg1", "arg2"}, expectedSubFlag: "value2", // Should parse the second --subflag since we never hit the stop limit }, { // Meaningless, but okay. name: "parent stops after 1, subcommand stops after 1", parentStopOnNthArg: intPtr(1), subStopOnNthArg: intPtr(1), testArgs: []string{"parent", "sub", "--subflag", "value", "subarg", "--not-parsed"}, expectedParentArgs: []string{}, expectedSubArgs: []string{"subarg", "--not-parsed"}, expectedSubFlag: "value", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var parentArgs, subArgs Args var subFlagValue string subCalled := false subCmd := &Command{ Name: "sub", StopOnNthArg: tt.subStopOnNthArg, Flags: []Flag{ &StringFlag{Name: "subflag", Destination: &subFlagValue}, }, Action: func(_ context.Context, cmd *Command) error { subCalled = true subArgs = cmd.Args() return nil }, } parentCmd := &Command{ Name: "parent", StopOnNthArg: tt.parentStopOnNthArg, Commands: []*Command{subCmd}, Flags: []Flag{ &StringFlag{Name: "parentflag"}, }, Action: func(_ context.Context, cmd *Command) error { parentArgs = cmd.Args() return nil }, } err := parentCmd.Run(buildTestContext(t), tt.testArgs) require.NoError(t, err) if tt.expectedSubArgs != nil { assert.True(t, subCalled, "subcommand should have been called") if len(tt.expectedSubArgs) > 0 { haveNonEmptySubArgsSlice := subArgs != nil && subArgs.Slice() != nil && len(subArgs.Slice()) > 0 assert.True(t, haveNonEmptySubArgsSlice, "subargs.Slice is not nil") if haveNonEmptySubArgsSlice { assert.Equal(t, tt.expectedSubArgs, subArgs.Slice()) } } else { assert.True(t, subArgs == nil || subArgs.Slice() == nil || len(subArgs.Slice()) == 0, "subargs.Slice is not nil") } assert.Equal(t, tt.expectedSubFlag, subFlagValue) } else { assert.False(t, subCalled, "subcommand should not have been called") assert.Equal(t, tt.expectedParentArgs, parentArgs.Slice()) } }) } } func TestCommand_StopOnNthArg_EdgeCases(t *testing.T) { t.Run("negative StopOnNthArg returns error", func(t *testing.T) { cmd := &Command{ Name: "test", StopOnNthArg: intPtr(-1), Action: func(_ context.Context, cmd *Command) error { return nil }, } // Negative value should return an error err := cmd.Run(buildTestContext(t), []string{"cmd", "arg1"}) require.Error(t, err) assert.Contains(t, err.Error(), "StopOnNthArg must be non-negative") }) t.Run("zero StopOnNthArg with no args", func(t *testing.T) { var args Args var flagValue string cmd := &Command{ Name: "test", StopOnNthArg: intPtr(0), Flags: []Flag{ &StringFlag{Name: "flag", Destination: &flagValue}, }, Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, } // All flags should become args require.NoError(t, cmd.Run(buildTestContext(t), []string{"cmd", "--flag", "value"})) assert.Equal(t, []string{"--flag", "value"}, args.Slice()) assert.Equal(t, "", flagValue) }) t.Run("StopOnNthArg with only flags", func(t *testing.T) { var args Args var flagValue string var boolValue bool cmd := &Command{ Name: "test", StopOnNthArg: intPtr(1), Flags: []Flag{ &StringFlag{Name: "flag", Destination: &flagValue}, &BoolFlag{Name: "bool", Destination: &boolValue}, }, Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, } // Should parse all flags since no args are encountered require.NoError(t, cmd.Run(buildTestContext(t), []string{"cmd", "--flag", "value", "--bool"})) assert.Equal(t, []string{}, args.Slice()) assert.Equal(t, "value", flagValue) assert.True(t, boolValue) }) } // Helper function to create int pointer func intPtr(i int) *int { return &i } ================================================ FILE: command_test.go ================================================ package cli import ( "bytes" "context" "encoding/json" "errors" "flag" "fmt" "io" "net/mail" "os" "sort" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( lastExitCode = 0 fakeOsExiter = func(rc int) { lastExitCode = rc } fakeErrWriter = &bytes.Buffer{} ) func init() { OsExiter = fakeOsExiter ErrWriter = fakeErrWriter } type opCounts struct { Total, ShellComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int } func buildExtendedTestCommand() *Command { cmd := buildMinimalTestCommand() cmd.Name = "greet" cmd.Flags = []Flag{ &StringFlag{ Name: "socket", Aliases: []string{"s"}, Usage: "some 'usage' text", Value: "value", TakesFile: true, }, &StringFlag{Name: "flag", Aliases: []string{"fl", "f"}}, &BoolFlag{ Name: "another-flag", Aliases: []string{"b"}, Usage: "another usage text", Sources: EnvVars("EXAMPLE_VARIABLE_NAME"), }, &BoolFlag{ Name: "hidden-flag", Hidden: true, }, } cmd.Commands = []*Command{{ Aliases: []string{"c"}, Flags: []Flag{ &StringFlag{ Name: "flag", Aliases: []string{"fl", "f"}, TakesFile: true, }, &BoolFlag{ Name: "another-flag", Aliases: []string{"b"}, Usage: "another usage text", }, }, Name: "config", Usage: "another usage test", Commands: []*Command{{ Aliases: []string{"s", "ss"}, Flags: []Flag{ &StringFlag{Name: "sub-flag", Aliases: []string{"sub-fl", "s"}}, &BoolFlag{ Name: "sub-command-flag", Aliases: []string{"s"}, Usage: "some usage text", }, }, Name: "sub-config", Usage: "another usage test", }}, }, { Aliases: []string{"i", "in"}, Name: "info", Usage: "retrieve generic information", }, { Name: "some-command", }, { Name: "hidden-command", Hidden: true, Flags: []Flag{ &BoolFlag{ Name: "completable", }, }, }, { 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 `, Commands: []*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", }}, }} cmd.UsageText = "app [first_arg] [second_arg]" cmd.Description = `Description of the application.` cmd.Usage = "Some app" cmd.Authors = []any{ "Harrison ", &mail.Address{Name: "Oliver Allen", Address: "oliver@toyshop.com"}, } return cmd } func TestCommandFlagParsing(t *testing.T) { cases := []struct { testArgs []string skipFlagParsing bool useShortOptionHandling bool expectedErr string }{ // Test normal "not ignoring flags" flow {testArgs: []string{"test-cmd", "-break", "blah", "blah"}, skipFlagParsing: false, useShortOptionHandling: false, expectedErr: "flag provided but not defined: -break"}, {testArgs: []string{"test-cmd", "blah", "blah"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing without any args that look like flags {testArgs: []string{"test-cmd", "blah", "-break"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with random flag arg {testArgs: []string{"test-cmd", "blah", "-help"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with "special" help flag arg {testArgs: []string{"test-cmd", "blah", "-h"}, skipFlagParsing: false, useShortOptionHandling: true, expectedErr: "No help topic for 'blah'"}, // Test UseShortOptionHandling } for _, c := range cases { t.Run(strings.Join(c.testArgs, " "), func(t *testing.T) { cmd := &Command{ Writer: io.Discard, Name: "test-cmd", Aliases: []string{"tc"}, Usage: "this is for testing", Description: "testing", Action: func(context.Context, *Command) error { return nil }, SkipFlagParsing: c.skipFlagParsing, } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) t.Cleanup(cancel) r := require.New(t) err := cmd.Run(ctx, c.testArgs) if c.expectedErr != "" { r.EqualError(err, c.expectedErr) } else { r.NoError(err) } }) } } func TestParseAndRunShortOpts(t *testing.T) { testCases := []struct { testArgs *stringSliceArgs expectedErr string expectedArgs Args }{ {testArgs: &stringSliceArgs{v: []string{"test", "-a"}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-c", "arg1", "arg2"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1", "arg2"}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-f"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-ac", "--fgh"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-af"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-cf"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-acf"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, {testArgs: &stringSliceArgs{v: []string{"test", "--acf"}}, expectedErr: "flag provided but not defined: -acf"}, {testArgs: &stringSliceArgs{v: []string{"test", "-invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "-invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "--invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "--invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "arg1", "-invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "arg1", "--invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "-acfi", "not-arg", "arg1", "-invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, {testArgs: &stringSliceArgs{v: []string{"test", "-i", "ivalue"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-i", "ivalue", "arg1"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1"}}}, {testArgs: &stringSliceArgs{v: []string{"test", "-i"}}, expectedErr: "flag needs an argument: -i"}, } for _, tc := range testCases { t.Run(strings.Join(tc.testArgs.v, " "), func(t *testing.T) { state := map[string]Args{"args": nil} cmd := &Command{ Name: "test", Usage: "this is for testing", Description: "testing", Action: func(_ context.Context, cmd *Command) error { state["args"] = cmd.Args() return nil }, UseShortOptionHandling: true, Writer: io.Discard, Flags: []Flag{ &BoolFlag{Name: "abc", Aliases: []string{"a"}}, &BoolFlag{Name: "cde", Aliases: []string{"c"}}, &BoolFlag{Name: "fgh", Aliases: []string{"f"}}, &StringFlag{Name: "ijk", Aliases: []string{"i"}}, }, } err := cmd.Run(buildTestContext(t), tc.testArgs.Slice()) r := require.New(t) if tc.expectedErr == "" { r.NoError(err) } else { r.ErrorContains(err, tc.expectedErr) } if tc.expectedArgs == nil { if state["args"] != nil { r.Len(state["args"].Slice(), 0) } else { r.Nil(state["args"]) } } else { r.Equal(tc.expectedArgs, state["args"]) } }) } } func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { cmd := &Command{ Name: "bar", Before: func(context.Context, *Command) (context.Context, error) { return nil, fmt.Errorf("before error") }, After: func(context.Context, *Command) error { return fmt.Errorf("after error") }, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"bar"}) require.ErrorContains(t, err, "before error") require.ErrorContains(t, err, "after error") } func TestCommand_Run_BeforeSavesMetadata(t *testing.T) { var receivedMsgFromAction string var receivedMsgFromAfter string cmd := &Command{ Name: "bar", Before: func(ctx context.Context, cmd *Command) (context.Context, error) { cmd.Metadata["msg"] = "hello world" return nil, nil }, Action: func(ctx context.Context, cmd *Command) error { msg, ok := cmd.Metadata["msg"] if !ok { return errors.New("msg not found") } receivedMsgFromAction = msg.(string) return nil }, After: func(_ context.Context, cmd *Command) error { msg, ok := cmd.Metadata["msg"] if !ok { return errors.New("msg not found") } receivedMsgFromAfter = msg.(string) return nil }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"foo", "bar"})) require.Equal(t, "hello world", receivedMsgFromAction) require.Equal(t, "hello world", receivedMsgFromAfter) } func TestCommand_Run_BeforeReturnNewContext(t *testing.T) { var receivedValFromAction, receivedValFromAfter string type key string bkey := key("bkey") cmd := &Command{ Name: "bar", Before: func(ctx context.Context, cmd *Command) (context.Context, error) { return context.WithValue(ctx, bkey, "bval"), nil }, Action: func(ctx context.Context, cmd *Command) error { if val := ctx.Value(bkey); val == nil { return errors.New("bkey value not found") } else { receivedValFromAction = val.(string) } return nil }, After: func(ctx context.Context, cmd *Command) error { if val := ctx.Value(bkey); val == nil { return errors.New("bkey value not found") } else { receivedValFromAfter = val.(string) } return nil }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"foo", "bar"})) require.Equal(t, "bval", receivedValFromAfter) require.Equal(t, "bval", receivedValFromAction) } type ctxKey string // ctxCollector is a small helper to collect context values. type ctxCollector struct { // keys are the keys to check the context for. keys []ctxKey // m maps from function name to context name to value. m map[string]map[ctxKey]string } func (cc *ctxCollector) collect(ctx context.Context, fnName string) { if cc.m == nil { cc.m = make(map[string]map[ctxKey]string) } if _, ok := cc.m[fnName]; !ok { cc.m[fnName] = make(map[ctxKey]string) } for _, k := range cc.keys { if val := ctx.Value(k); val != nil { cc.m[fnName][k] = val.(string) } } } func TestCommand_Run_BeforeReturnNewContextSubcommand(t *testing.T) { bkey := ctxKey("bkey") bkey2 := ctxKey("bkey2") cc := &ctxCollector{keys: []ctxKey{bkey, bkey2}} cmd := &Command{ Name: "bar", Before: func(ctx context.Context, cmd *Command) (context.Context, error) { return context.WithValue(ctx, bkey, "bval"), nil }, After: func(ctx context.Context, cmd *Command) error { cc.collect(ctx, "bar.After") return nil }, Commands: []*Command{ { Name: "baz", Before: func(ctx context.Context, cmd *Command) (context.Context, error) { return context.WithValue(ctx, bkey2, "bval2"), nil }, Action: func(ctx context.Context, cmd *Command) error { cc.collect(ctx, "baz.Action") return nil }, After: func(ctx context.Context, cmd *Command) error { cc.collect(ctx, "baz.After") return nil }, }, }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"bar", "baz"})) expected := map[string]map[ctxKey]string{ "bar.After": { bkey: "bval", bkey2: "bval2", }, "baz.Action": { bkey: "bval", bkey2: "bval2", }, "baz.After": { bkey: "bval", bkey2: "bval2", }, } require.Equal(t, expected, cc.m) } func TestCommand_Run_FlagActionContext(t *testing.T) { bkey := ctxKey("bkey") bkey2 := ctxKey("bkey2") cc := &ctxCollector{keys: []ctxKey{bkey, bkey2}} cmd := &Command{ Name: "bar", Before: func(ctx context.Context, cmd *Command) (context.Context, error) { return context.WithValue(ctx, bkey, "bval"), nil }, Flags: []Flag{ &StringFlag{ Name: "foo", Action: func(ctx context.Context, cmd *Command, _ string) error { cc.collect(ctx, "bar.foo.Action") return nil }, }, }, Commands: []*Command{ { Name: "baz", Before: func(ctx context.Context, cmd *Command) (context.Context, error) { return context.WithValue(ctx, bkey2, "bval2"), nil }, Flags: []Flag{ &StringFlag{ Name: "goo", Action: func(ctx context.Context, cmd *Command, _ string) error { cc.collect(ctx, "baz.goo.Action") return nil }, }, }, Action: func(ctx context.Context, cmd *Command) error { return nil }, }, }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"bar", "--foo", "value", "baz", "--goo", "value"})) expected := map[string]map[ctxKey]string{ "bar.foo.Action": { bkey: "bval", bkey2: "bval2", }, "baz.goo.Action": { bkey: "bval", bkey2: "bval2", }, } require.Equal(t, expected, cc.m) } func TestCommand_OnUsageError_hasCommandContext(t *testing.T) { cmd := &Command{ Name: "bar", Flags: []Flag{ &Int64Flag{Name: "flag"}, }, OnUsageError: func(_ context.Context, cmd *Command, err error, _ bool) error { return fmt.Errorf("intercepted in %s: %s", cmd.Name, err.Error()) }, } err := cmd.Run(buildTestContext(t), []string{"bar", "--flag=wrong"}) assert.ErrorContains(t, err, "intercepted in bar") } func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { cmd := &Command{ Name: "bar", Flags: []Flag{ &Int64Flag{Name: "flag"}, }, OnUsageError: func(_ context.Context, _ *Command, err error, _ bool) error { assert.ErrorContains(t, err, "strconv.ParseInt: parsing \"wrong\"") return errors.New("intercepted: " + err.Error()) }, } err := cmd.Run(buildTestContext(t), []string{"bar", "--flag=wrong"}) assert.ErrorContains(t, err, "intercepted: invalid value \"wrong\" for flag -flag: strconv.ParseInt: parsing \"wrong\"") } func TestCommand_OnUsageError_WithSubcommand(t *testing.T) { cmd := &Command{ Name: "bar", Commands: []*Command{ { Name: "baz", }, }, Flags: []Flag{ &Int64Flag{Name: "flag"}, }, OnUsageError: func(_ context.Context, _ *Command, err error, _ bool) error { assert.ErrorContains(t, err, "parsing \"wrong\": invalid syntax") return errors.New("intercepted: " + err.Error()) }, } require.ErrorContains(t, cmd.Run(buildTestContext(t), []string{"bar", "--flag=wrong"}), "intercepted: invalid value \"wrong\" for flag -flag: strconv.ParseInt: parsing \"wrong\": invalid syntax") } func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { cmd := &Command{ ErrWriter: io.Discard, Name: "bar", Usage: "this is for testing", Commands: []*Command{ { Name: "baz", Usage: "this is for testing", Action: func(_ context.Context, cmd *Command) error { require.Equal(t, io.Discard, cmd.Root().ErrWriter) return nil }, }, }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"bar", "baz"})) } func TestCommandSkipFlagParsing(t *testing.T) { cases := []struct { testArgs *stringSliceArgs expectedArgs *stringSliceArgs expectedErr error }{ {testArgs: &stringSliceArgs{v: []string{"some-command", "some-arg", "--flag", "foo"}}, expectedArgs: &stringSliceArgs{v: []string{"some-arg", "--flag", "foo"}}, expectedErr: nil}, {testArgs: &stringSliceArgs{v: []string{"some-command", "some-arg", "--flag=foo"}}, expectedArgs: &stringSliceArgs{v: []string{"some-arg", "--flag=foo"}}, expectedErr: nil}, } for _, c := range cases { t.Run(strings.Join(c.testArgs.Slice(), " "), func(t *testing.T) { var args Args cmd := &Command{ SkipFlagParsing: true, Name: "some-command", Flags: []Flag{ &StringFlag{Name: "flag"}, }, Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), c.testArgs.Slice()) assert.Equal(t, c.expectedErr, err) assert.Equal(t, c.expectedArgs, args) }) } } func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { cases := []struct { testArgs *stringSliceArgs expectedOut string }{ {testArgs: &stringSliceArgs{v: []string{"--undefined"}}, expectedOut: "found 0 args"}, {testArgs: &stringSliceArgs{v: []string{"--number"}}, expectedOut: "found 0 args"}, {testArgs: &stringSliceArgs{v: []string{"--number", "forty-two"}}, expectedOut: "found 0 args"}, {testArgs: &stringSliceArgs{v: []string{"--number", "42"}}, expectedOut: "found 0 args"}, {testArgs: &stringSliceArgs{v: []string{"--number", "42", "newArg"}}, expectedOut: "found 1 args"}, } for _, c := range cases { t.Run(strings.Join(c.testArgs.Slice(), " "), func(t *testing.T) { out := &bytes.Buffer{} cmd := &Command{ Writer: out, EnableShellCompletion: true, Name: "bar", Usage: "this is for testing", Flags: []Flag{ &Int64Flag{ Name: "number", Usage: "A number to parse", }, }, ShellComplete: func(_ context.Context, cmd *Command) { fmt.Fprintf(cmd.Root().Writer, "found %[1]d args", cmd.NArg()) }, } osArgs := &stringSliceArgs{v: []string{"bar"}} osArgs.v = append(osArgs.v, c.testArgs.Slice()...) osArgs.v = append(osArgs.v, completionFlag) r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), osArgs.Slice())) r.Equal(c.expectedOut, out.String()) }) } } func TestCommand_CanAddVFlagOnSubCommands(t *testing.T) { cmd := &Command{ Version: "some version", Writer: io.Discard, Name: "foo", Usage: "this is for testing", Commands: []*Command{ { Name: "bar", Flags: []Flag{ &BoolFlag{Name: "v"}, }, }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "bar"}) assert.NoError(t, err) } func TestCommand_VisibleSubcCommands(t *testing.T) { subc1 := &Command{ Name: "subc1", Usage: "subc1 command1", } subc3 := &Command{ Name: "subc3", Usage: "subc3 command2", } cmd := &Command{ Name: "bar", Usage: "this is for testing", Commands: []*Command{ subc1, { Name: "subc2", Usage: "subc2 command2", Hidden: true, }, subc3, }, } assert.Equal(t, cmd.VisibleCommands(), []*Command{subc1, subc3}) } func TestCommand_VisibleFlagCategories(t *testing.T) { cmd := &Command{ Name: "bar", Usage: "this is for testing", Flags: []Flag{ &StringFlag{ Name: "strd", // no category set }, &StringFlag{ Name: "strd1", // no category set and also hidden Hidden: true, }, &Int64Flag{ Name: "intd", Aliases: []string{"altd1", "altd2"}, Category: "cat1", }, &StringFlag{ Name: "sfd", Category: "cat2", // category set and hidden Hidden: true, }, }, MutuallyExclusiveFlags: []MutuallyExclusiveFlags{{ Category: "cat2", Flags: [][]Flag{ { &StringFlag{ Name: "mutex", }, }, }, }}, } cmd.MutuallyExclusiveFlags[0].propagateCategory() vfc := cmd.VisibleFlagCategories() require.Len(t, vfc, 3) assert.Equal(t, vfc[0].Name(), "", "expected category name to be empty") assert.Equal(t, vfc[0].Flags()[0].Names(), []string{"strd"}) assert.Equal(t, vfc[1].Name(), "cat1", "expected category name cat1") require.Len(t, vfc[1].Flags(), 1, "expected flag category to have one flag") assert.Equal(t, vfc[1].Flags()[0].Names(), []string{"intd", "altd1", "altd2"}) assert.Equal(t, vfc[2].Name(), "cat2", "expected category name cat2") require.Len(t, vfc[2].Flags(), 1, "expected flag category to have one flag") assert.Equal(t, vfc[2].Flags()[0].Names(), []string{"mutex"}) } func TestCommand_RunSubcommandWithDefault(t *testing.T) { cmd := &Command{ Version: "some version", Name: "app", DefaultCommand: "foo", Commands: []*Command{ { Name: "foo", Action: func(context.Context, *Command) error { return errors.New("should not run this subcommand") }, }, { Name: "bar", Usage: "this is for testing", Commands: []*Command{{}}, // some subcommand Action: func(context.Context, *Command) error { return nil }, }, }, } err := cmd.Run(buildTestContext(t), []string{"app", "bar"}) assert.NoError(t, err) err = cmd.Run(buildTestContext(t), []string{"app"}) assert.EqualError(t, err, "should not run this subcommand") } func TestCommand_Run(t *testing.T) { s := "" cmd := &Command{ Action: func(_ context.Context, cmd *Command) error { s = s + cmd.Args().First() return nil }, } err := cmd.Run(buildTestContext(t), []string{"command", "foo"}) assert.NoError(t, err) err = cmd.Run(buildTestContext(t), []string{"command", "bar"}) assert.NoError(t, err) assert.Equal(t, s, "foobar") } var commandTests = []struct { name string expected bool }{ {"foobar", true}, {"batbaz", true}, {"b", true}, {"f", true}, {"bat", false}, {"nothing", false}, } func TestCommand_Command(t *testing.T) { cmd := &Command{ Commands: []*Command{ {Name: "foobar", Aliases: []string{"f"}}, {Name: "batbaz", Aliases: []string{"b"}}, }, } for _, test := range commandTests { if test.expected { assert.NotEmpty(t, cmd.Command(test.name)) } else { assert.Empty(t, cmd.Command(test.name)) } } } var defaultCommandTests = []struct { cmdName string defaultCmd string args []string errNotExpected bool }{ {"foobar", "foobar", nil, true}, {"batbaz", "foobar", nil, true}, {"b", "", nil, true}, {"f", "", nil, true}, {"", "foobar", nil, true}, {"", "", nil, true}, {" ", "", nil, true}, {"bat", "batbaz", nil, true}, {"nothing", "batbaz", nil, true}, {"nothing", "", nil, false}, {"foobar", "foobar", []string{"xy", "zdf"}, true}, {"", "foobar", []string{"xy", "zdf"}, true}, } func TestCommand_RunDefaultCommand(t *testing.T) { for _, test := range defaultCommandTests { testTitle := fmt.Sprintf("command=%[1]s-default=%[2]s-args=%[3]v", test.cmdName, test.defaultCmd, test.args) t.Run(testTitle, func(t *testing.T) { fooCount := 0 var fooArgs Args barCount := 0 cmd := &Command{ DefaultCommand: test.defaultCmd, Commands: []*Command{ { Name: "foobar", Aliases: []string{"f"}, Action: func(ctx context.Context, c *Command) error { fooCount++ fooArgs = c.Args() return nil }, }, { Name: "batbaz", Aliases: []string{"b"}, Action: func(ctx context.Context, c *Command) error { barCount++ return nil }, }, }, } runArgs := []string{"c"} if test.cmdName != "" { runArgs = append(runArgs, test.cmdName) } if test.args != nil { runArgs = append(runArgs, test.args...) } err := cmd.Run(buildTestContext(t), runArgs) if test.errNotExpected { assert.NoError(t, err) if fooCount == 0 && barCount == 0 && test.defaultCmd != "" { t.Errorf("expected one of the commands to run") } if fooCount > 0 { expectedArgs := &stringSliceArgs{v: []string{}} if len(test.args) > 0 && (test.args[0] == "foobar" || test.args[0] == "f") { expectedArgs = &stringSliceArgs{v: test.args[1:]} } else if test.args != nil { expectedArgs = &stringSliceArgs{v: test.args} } assert.Equal(t, expectedArgs, fooArgs) } } else { if fooCount > 0 || barCount > 0 { t.Errorf("expected no commands to run") } assert.Error(t, err) } }) } } var defaultCommandSubCommandTests = []struct { cmdName string subCmd string defaultCmd string errNotExpected bool }{ {"foobar", "", "foobar", true}, {"foobar", "carly", "foobar", true}, {"batbaz", "", "foobar", true}, {"b", "", "", true}, {"f", "", "", true}, {"", "", "foobar", true}, {"", "", "", true}, {"", "jimbob", "foobar", true}, {"", "j", "foobar", true}, {"", "carly", "foobar", true}, {"", "jimmers", "foobar", false}, {"", "jimmers", "", false}, {" ", "jimmers", "foobar", true}, {"", "", "", true}, {" ", "", "", true}, {" ", "j", "", true}, {"bat", "", "batbaz", false}, {"nothing", "", "batbaz", false}, {"nothing", "", "", false}, {"nothing", "j", "batbaz", false}, {"nothing", "carly", "", false}, } func TestCommand_RunDefaultCommandWithSubCommand(t *testing.T) { for _, test := range defaultCommandSubCommandTests { testTitle := fmt.Sprintf("command=%[1]s-subcmd=%[2]s-default=%[3]s", test.cmdName, test.subCmd, test.defaultCmd) t.Run(testTitle, func(t *testing.T) { cmd := &Command{ DefaultCommand: test.defaultCmd, Commands: []*Command{ { Name: "foobar", Aliases: []string{"f"}, Commands: []*Command{ {Name: "jimbob", Aliases: []string{"j"}}, {Name: "carly"}, }, }, {Name: "batbaz", Aliases: []string{"b"}}, }, } runArgs := []string{"c"} if test.cmdName != "" { runArgs = append(runArgs, test.cmdName) } if test.subCmd != "" { runArgs = append(runArgs, test.subCmd) } err := cmd.Run(buildTestContext(t), runArgs) if test.errNotExpected { assert.NoError(t, err) } else { assert.Error(t, err) } }) } } var defaultCommandFlagTests = []struct { cmdName string flag string defaultCmd string errNotExpected bool }{ {"foobar", "", "foobar", true}, {"foobar", "-c derp", "foobar", true}, {"batbaz", "", "foobar", true}, {"b", "", "", true}, {"f", "", "", true}, {"", "", "foobar", true}, {"", "", "", true}, {"", "-j", "foobar", true}, {"", "-j", "foobar", true}, {"", "-c derp", "foobar", true}, {"", "--carly=derp", "foobar", true}, {"", "-j", "foobar", true}, {"", "-j", "", true}, {" ", "-j", "foobar", true}, {"", "", "", true}, {" ", "", "", true}, {" ", "-j", "", true}, {"bat", "", "batbaz", false}, {"nothing", "", "batbaz", false}, {"nothing", "", "", false}, {"nothing", "--jimbob", "batbaz", false}, {"nothing", "--carly", "", false}, } func TestCommand_RunDefaultCommandWithFlags(t *testing.T) { for _, test := range defaultCommandFlagTests { testTitle := fmt.Sprintf("command=%[1]s-flag=%[2]s-default=%[3]s", test.cmdName, test.flag, test.defaultCmd) t.Run(testTitle, func(t *testing.T) { cmd := &Command{ DefaultCommand: test.defaultCmd, Flags: []Flag{ &StringFlag{ Name: "carly", Aliases: []string{"c"}, Required: false, }, &BoolFlag{ Name: "jimbob", Aliases: []string{"j"}, Required: false, Value: true, }, }, Commands: []*Command{ { Name: "foobar", Aliases: []string{"f"}, }, {Name: "batbaz", Aliases: []string{"b"}}, }, } appArgs := []string{"c"} if test.flag != "" { flags := strings.Split(test.flag, " ") if len(flags) > 1 { appArgs = append(appArgs, flags...) } flags = strings.Split(test.flag, "=") if len(flags) > 1 { appArgs = append(appArgs, flags...) } } appArgs = append(appArgs, test.cmdName) err := cmd.Run(buildTestContext(t), appArgs) if test.errNotExpected { assert.NoError(t, err) } else { assert.Error(t, err) } }) } } func TestCommand_FlagsFromExtPackage(t *testing.T) { var someint int flag.IntVar(&someint, "epflag", 2, "ext package flag usage") // Based on source code we can reset the global flag parsing this way defer func() { flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) }() cmd := &Command{ AllowExtFlags: true, Flags: []Flag{ &StringFlag{ Name: "carly", Aliases: []string{"c"}, Required: false, }, &BoolFlag{ Name: "jimbob", Aliases: []string{"j"}, Required: false, Value: true, }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "-c", "cly", "--epflag", "10"}) assert.NoError(t, err) assert.Equal(t, int(10), someint) // this exercises the extFlag.Get() assert.Equal(t, int(10), cmd.Value("epflag")) cmd = &Command{ Flags: []Flag{ &StringFlag{ Name: "carly", Aliases: []string{"c"}, Required: false, }, &BoolFlag{ Name: "jimbob", Aliases: []string{"j"}, Required: false, Value: true, }, }, } // this should return an error since epflag shouldnt be registered err = cmd.Run(buildTestContext(t), []string{"foo", "-c", "cly", "--epflag", "10"}) assert.Error(t, err) } func TestCommand_Setup_defaultsReader(t *testing.T) { cmd := &Command{} cmd.setupDefaults([]string{"test"}) assert.Equal(t, cmd.Reader, os.Stdin) } func TestCommand_Setup_defaultsWriter(t *testing.T) { cmd := &Command{} cmd.setupDefaults([]string{"test"}) assert.Equal(t, cmd.Writer, os.Stdout) } func TestCommand_CommandWithFlagBeforeTerminator(t *testing.T) { var parsedOption string var args Args cmd := &Command{ Commands: []*Command{ { Name: "cmd", Flags: []Flag{ &StringFlag{Name: "option", Value: "", Usage: "some option"}, }, Action: func(_ context.Context, cmd *Command) error { parsedOption = cmd.String("option") args = cmd.Args() return nil }, }, }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"", "cmd", "--option", "my-option", "my-arg", "--", "--notARealFlag"})) require.Equal(t, "my-option", parsedOption) require.Equal(t, "my-arg", args.Get(0)) require.Equal(t, "--notARealFlag", args.Get(1)) } func TestCommand_CommandWithDash(t *testing.T) { var args Args cmd := &Command{ Commands: []*Command{ { Name: "cmd", Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, }, }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"", "cmd", "my-arg", "-"})) require.NotNil(t, args) require.Equal(t, "my-arg", args.Get(0)) require.Equal(t, "-", args.Get(1)) } func TestCommand_CommandWithNoFlagBeforeTerminator(t *testing.T) { var args Args cmd := &Command{ Commands: []*Command{ { Name: "cmd", Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, }, }, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"", "cmd", "my-arg", "--", "notAFlagAtAll"})) require.NotNil(t, args) require.Equal(t, "my-arg", args.Get(0)) require.Equal(t, "notAFlagAtAll", args.Get(1)) } func TestCommand_SkipFlagParsing(t *testing.T) { var args Args cmd := &Command{ SkipFlagParsing: true, Action: func(_ context.Context, cmd *Command) error { args = cmd.Args() return nil }, } _ = cmd.Run(buildTestContext(t), []string{"", "--", "my-arg", "notAFlagAtAll"}) assert.NotNil(t, args) assert.Equal(t, "--", args.Get(0)) assert.Equal(t, "my-arg", args.Get(1)) assert.Equal(t, "notAFlagAtAll", args.Get(2)) } func TestCommand_VisibleCommands(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frob", Action: func(context.Context, *Command) error { return nil }, }, { Name: "frib", Hidden: true, Action: func(context.Context, *Command) error { return nil }, }, }, } cmd.setupDefaults([]string{"test"}) expected := []*Command{ cmd.Commands[0], } actual := cmd.VisibleCommands() assert.Len(t, actual, len(expected)) for i, actualCommand := range actual { expectedCommand := expected[i] if expectedCommand.Action != nil { // comparing func addresses is OK! assert.Equal(t, fmt.Sprintf("%p", expectedCommand.Action), fmt.Sprintf("%p", actualCommand.Action)) } func() { // nil out funcs, as they cannot be compared // (https://github.com/golang/go/issues/8554) expectedAction := expectedCommand.Action actualAction := actualCommand.Action defer func() { expectedCommand.Action = expectedAction actualCommand.Action = actualAction }() expectedCommand.Action = nil actualCommand.Action = nil assert.Equal(t, expectedCommand, actualCommand) }() } } func TestCommand_UseShortOptionHandling(t *testing.T) { var one, two bool var name string expected := "expectedName" cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true cmd.Flags = []Flag{ &BoolFlag{Name: "one", Aliases: []string{"o"}}, &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, } cmd.Action = func(_ context.Context, cmd *Command) error { one = cmd.Bool("one") two = cmd.Bool("two") name = cmd.String("name") return nil } _ = cmd.Run(buildTestContext(t), []string{"", "-on", expected}) assert.True(t, one) assert.False(t, two) assert.Equal(t, name, expected) } func TestCommand_UseShortOptionHandling_missing_value(t *testing.T) { cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true cmd.Flags = []Flag{ &StringFlag{Name: "name", Aliases: []string{"n"}}, } err := cmd.Run(buildTestContext(t), []string{"", "-n"}) assert.EqualError(t, err, "flag needs an argument: -n") } func TestCommand_UseShortOptionHandlingCommand(t *testing.T) { var ( one, two bool name string expected = "expectedName" ) cmd := &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "one", Aliases: []string{"o"}}, &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, }, Action: func(_ context.Context, cmd *Command) error { one = cmd.Bool("one") two = cmd.Bool("two") name = cmd.String("name") return nil }, UseShortOptionHandling: true, Writer: io.Discard, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"cmd", "-on", expected})) require.True(t, one) require.False(t, two) require.Equal(t, expected, name) } func TestCommand_UseShortOptionHandlingCommand_missing_value(t *testing.T) { cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true command := &Command{ Name: "cmd", Flags: []Flag{ &StringFlag{Name: "name", Aliases: []string{"n"}}, }, } cmd.Commands = []*Command{command} require.EqualError( t, cmd.Run(buildTestContext(t), []string{"", "cmd", "-n"}), "flag needs an argument: -n", ) } func TestCommand_UseShortOptionHandlingSubCommand(t *testing.T) { var one, two bool var name string cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true cmd.Commands = []*Command{ { Name: "cmd", Commands: []*Command{ { Name: "sub", Flags: []Flag{ &BoolFlag{Name: "one", Aliases: []string{"o"}}, &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, }, Action: func(_ context.Context, cmd *Command) error { one = cmd.Bool("one") two = cmd.Bool("two") name = cmd.String("name") return nil }, }, }, }, } expected := "expectedName" require.NoError(t, cmd.Run(buildTestContext(t), []string{"", "cmd", "sub", "-on", expected})) require.True(t, one) require.False(t, two) require.Equal(t, expected, name) } func TestCommand_UseShortOptionHandlingSubCommand_missing_value(t *testing.T) { cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true command := &Command{ Name: "cmd", } subCommand := &Command{ Name: "sub", Flags: []Flag{ &StringFlag{Name: "name", Aliases: []string{"n"}}, }, } command.Commands = []*Command{subCommand} cmd.Commands = []*Command{command} err := cmd.Run(buildTestContext(t), []string{"", "cmd", "sub", "-n"}) assert.EqualError(t, err, "flag needs an argument: -n") } func TestCommand_UseShortOptionAfterSliceFlag(t *testing.T) { var one, two bool var name string var sliceValDest []string var sliceVal []string expected := "expectedName" cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true cmd.Flags = []Flag{ &StringSliceFlag{Name: "env", Aliases: []string{"e"}, Destination: &sliceValDest}, &BoolFlag{Name: "one", Aliases: []string{"o"}}, &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, } cmd.Action = func(_ context.Context, cmd *Command) error { sliceVal = cmd.StringSlice("env") one = cmd.Bool("one") two = cmd.Bool("two") name = cmd.String("name") return nil } _ = cmd.Run(buildTestContext(t), []string{"", "-e", "foo", "-on", expected}) assert.Equal(t, sliceVal, []string{"foo"}) assert.Equal(t, sliceValDest, []string{"foo"}) assert.True(t, one) assert.False(t, two) assert.Equal(t, expected, name) } func TestCommand_UseShortOptionWithArg(t *testing.T) { var rootPath string cmd := &Command{ UseShortOptionHandling: true, Commands: []*Command{ { Name: "short", Usage: "complete a task on the list", Arguments: []Argument{ &StringArg{Name: "root", UsageText: "Root path", Destination: &rootPath}, }, Flags: []Flag{ &BoolFlag{Name: "serve", Aliases: []string{"s"}}, &BoolFlag{Name: "option", Aliases: []string{"o"}}, &StringFlag{Name: "message", Aliases: []string{"m"}}, }, Action: func(ctx context.Context, cmd *Command) error { return nil }, }, }, } err := cmd.Run(buildTestContext(t), []string{"app", "short", "-som", "hello", "/path/to/root"}) require.NoError(t, err) require.Equal(t, "/path/to/root", rootPath) } func TestCommand_Float64Flag(t *testing.T) { var meters float64 cmd := &Command{ Flags: []Flag{ &FloatFlag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"}, }, Action: func(_ context.Context, cmd *Command) error { meters = cmd.Float("height") return nil }, } _ = cmd.Run(buildTestContext(t), []string{"", "--height", "1.93"}) assert.Equal(t, 1.93, meters) } func TestCommand_ParseSliceFlags(t *testing.T) { var parsedIntSlice []int64 var parsedStringSlice []string cmd := &Command{ Commands: []*Command{ { Name: "cmd", Flags: []Flag{ &Int64SliceFlag{Name: "p", Value: []int64{}, Usage: "set one or more ip addr"}, &StringSliceFlag{Name: "ip", Value: []string{}, Usage: "set one or more ports to open"}, }, Action: func(_ context.Context, cmd *Command) error { parsedIntSlice = cmd.Int64Slice("p") parsedStringSlice = cmd.StringSlice("ip") return nil }, }, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"", "cmd", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"})) r.Equal([]int64{22, 80}, parsedIntSlice) r.Equal([]string{"8.8.8.8", "8.8.4.4"}, parsedStringSlice) } func TestCommand_ParseSliceFlagsWithMissingValue(t *testing.T) { var parsedIntSlice []int64 var parsedStringSlice []string cmd := &Command{ Commands: []*Command{ { Name: "cmd", Flags: []Flag{ &Int64SliceFlag{Name: "a", Usage: "set numbers"}, &StringSliceFlag{Name: "str", Usage: "set strings"}, }, Action: func(_ context.Context, cmd *Command) error { parsedIntSlice = cmd.Int64Slice("a") parsedStringSlice = cmd.StringSlice("str") return nil }, }, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"", "cmd", "-a", "2", "-str", "A"})) r.Equal([]int64{2}, parsedIntSlice) r.Equal([]string{"A"}, parsedStringSlice) } func TestCommand_DefaultStdin(t *testing.T) { cmd := &Command{} cmd.setupDefaults([]string{"test"}) assert.Equal(t, cmd.Reader, os.Stdin, "Default input reader not set.") } func TestCommand_DefaultStdout(t *testing.T) { cmd := &Command{} cmd.setupDefaults([]string{"test"}) assert.Equal(t, cmd.Writer, os.Stdout, "Default output writer not set.") } func TestCommand_SetStdin(t *testing.T) { buf := make([]byte, 12) cmd := &Command{ Name: "test", Reader: strings.NewReader("Hello World!"), Action: func(_ context.Context, cmd *Command) error { _, err := cmd.Reader.Read(buf) return err }, } err := cmd.Run(buildTestContext(t), []string{"help"}) require.NoError(t, err) assert.Equal(t, "Hello World!", string(buf), "Command did not read input from desired reader.") } func TestCommand_SetStdin_Subcommand(t *testing.T) { buf := make([]byte, 12) cmd := &Command{ Name: "test", Reader: strings.NewReader("Hello World!"), Commands: []*Command{ { Name: "command", Commands: []*Command{ { Name: "subcommand", Action: func(_ context.Context, cmd *Command) error { _, err := cmd.Root().Reader.Read(buf) return err }, }, }, }, }, } err := cmd.Run(buildTestContext(t), []string{"test", "command", "subcommand"}) require.NoError(t, err) assert.Equal(t, "Hello World!", string(buf), "Command did not read input from desired reader.") } func TestCommand_SetStdout(t *testing.T) { var w bytes.Buffer cmd := &Command{ Name: "test", Writer: &w, } err := cmd.Run(buildTestContext(t), []string{"help"}) require.NoError(t, err) assert.NotZero(t, w.Len(), "Command did not write output to desired writer.") } func TestCommand_BeforeFunc(t *testing.T) { counts := &opCounts{} beforeError := fmt.Errorf("fail") var err error cmd := &Command{ Before: func(_ context.Context, cmd *Command) (context.Context, error) { counts.Total++ counts.Before = counts.Total s := cmd.String("opt") if s == "fail" { return nil, beforeError } return nil, nil }, Commands: []*Command{ { Name: "sub", Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, }, Flags: []Flag{ &StringFlag{Name: "opt"}, }, Writer: io.Discard, } // run with the Before() func succeeding err = cmd.Run(buildTestContext(t), []string{"command", "--opt", "succeed", "sub"}) require.NoError(t, err) assert.Equal(t, 1, counts.Before, "Before() not executed when expected") assert.Equal(t, 2, counts.SubCommand, "Subcommand not executed when expected") // reset counts = &opCounts{} // run with the Before() func failing err = cmd.Run(buildTestContext(t), []string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func assert.ErrorIs(t, err, beforeError, "Run error expected, but not received") assert.Equal(t, 1, counts.Before, "Before() not executed when expected") assert.Equal(t, 0, counts.SubCommand, "Subcommand executed when NOT expected") // reset counts = &opCounts{} afterError := errors.New("fail again") cmd.After = func(context.Context, *Command) error { return afterError } // run with the Before() func failing, wrapped by After() err = cmd.Run(buildTestContext(t), []string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if _, ok := err.(MultiError); !ok { t.Errorf("MultiError expected, but not received") } assert.Equal(t, 1, counts.Before, "Before() not executed when expected") assert.Zero(t, counts.SubCommand, "Subcommand executed when NOT expected") } func TestCommand_BeforeFuncPersistentFlag(t *testing.T) { counts := &opCounts{} beforeError := fmt.Errorf("fail") var err error cmd := &Command{ Before: func(_ context.Context, cmd *Command) (context.Context, error) { counts.Before++ s := cmd.String("opt") if s != "value" { return nil, beforeError } return nil, nil }, Commands: []*Command{ { Name: "sub", Action: func(context.Context, *Command) error { counts.SubCommand++ return nil }, }, }, Flags: []Flag{ &StringFlag{Name: "opt"}, }, Writer: io.Discard, } // Check that --opt value is available in root command Before hook, // even when it was set on the subcommand. err = cmd.Run(buildTestContext(t), []string{"command", "sub", "--opt", "value"}) require.NoError(t, err) assert.Equal(t, 1, counts.Before, "Before() not executed when expected") assert.Equal(t, 1, counts.SubCommand, "Subcommand not executed when expected") } func TestCommand_BeforeAfterFuncShellCompletion(t *testing.T) { t.Skip("TODO: is '--generate-shell-completion' (flag) still supported?") counts := &opCounts{} cmd := &Command{ EnableShellCompletion: true, Before: func(context.Context, *Command) (context.Context, error) { counts.Total++ counts.Before = counts.Total return nil, nil }, After: func(context.Context, *Command) error { counts.Total++ counts.After = counts.Total return nil }, Commands: []*Command{ { Name: "sub", Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, }, Flags: []Flag{ &StringFlag{Name: "opt"}, }, Writer: io.Discard, } r := require.New(t) // run with the Before() func succeeding r.NoError( cmd.Run( buildTestContext(t), []string{ "command", "--opt", "succeed", "sub", completionFlag, }, ), ) r.Equalf(0, counts.Before, "Before was run") r.Equal(0, counts.After, "After was run") r.Equal(0, counts.SubCommand, "SubCommand was run") } func TestCommand_AfterFunc(t *testing.T) { counts := &opCounts{} afterError := fmt.Errorf("fail") var err error cmd := &Command{ After: func(_ context.Context, cmd *Command) error { counts.Total++ counts.After = counts.Total s := cmd.String("opt") if s == "fail" { return afterError } return nil }, Commands: []*Command{ { Name: "sub", Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, }, Flags: []Flag{ &StringFlag{Name: "opt"}, }, } // run with the After() func succeeding err = cmd.Run(buildTestContext(t), []string{"command", "--opt", "succeed", "sub"}) require.NoError(t, err) assert.Equal(t, 2, counts.After, "After() not executed when expected") assert.Equal(t, 1, counts.SubCommand, "Subcommand not executed when expected") // reset counts = &opCounts{} // run with the Before() func failing err = cmd.Run(buildTestContext(t), []string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func assert.ErrorIs(t, err, afterError, "Run error expected, but not received") assert.Equal(t, 2, counts.After, "After() not executed when expected") assert.Equal(t, 1, counts.SubCommand, "Subcommand not executed when expected") /* reset */ counts = &opCounts{} // reset the flags since they are set previously cmd.Flags = []Flag{ &StringFlag{Name: "opt"}, } // run with none args err = cmd.Run(buildTestContext(t), []string{"command"}) // should be the same error produced by the Before func require.NoError(t, err) assert.Equal(t, 1, counts.After, "After() not executed when expected") assert.Equal(t, 0, counts.SubCommand, "Subcommand not executed when expected") } func TestCommandNoHelpFlag(t *testing.T) { oldFlag := HelpFlag defer func() { HelpFlag = oldFlag }() HelpFlag = nil cmd := &Command{Writer: io.Discard} err := cmd.Run(buildTestContext(t), []string{"test", "-h"}) assert.ErrorContains(t, err, providedButNotDefinedErrMsg, "expected error about missing help flag") } func TestRequiredFlagCommandRunBehavior(t *testing.T) { tdata := []struct { testCase string appFlags []Flag appRunInput []string appCommands []*Command expectedAnError bool }{ // assertion: empty input, when a required flag is present, errors { testCase: "error_case_empty_input_with_required_flag_on_app", appRunInput: []string{"myCLI"}, appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, expectedAnError: true, }, { testCase: "error_case_empty_input_with_required_flag_on_command", appRunInput: []string{"myCLI", "myCommand"}, appCommands: []*Command{{ Name: "myCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }}, expectedAnError: true, }, { testCase: "error_case_empty_input_with_required_flag_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand"}, appCommands: []*Command{{ Name: "myCommand", Commands: []*Command{{ Name: "mySubCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }}, }}, expectedAnError: true, }, // assertion: inputting --help, when a required flag is present, does not error { testCase: "valid_case_help_input_with_required_flag_on_app", appRunInput: []string{"myCLI", "--help"}, appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }, { testCase: "valid_case_help_input_with_required_flag_on_command", appRunInput: []string{"myCLI", "myCommand", "--help"}, appCommands: []*Command{{ Name: "myCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }}, }, { testCase: "valid_case_help_input_with_required_flag_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--help"}, appCommands: []*Command{{ Name: "myCommand", Commands: []*Command{{ Name: "mySubCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }}, }}, }, // assertion: giving optional input, when a required flag is present, errors { testCase: "error_case_optional_input_with_required_flag_on_app", appRunInput: []string{"myCLI", "--optional", "cats"}, appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optional"}}, expectedAnError: true, }, { testCase: "error_case_optional_input_with_required_flag_on_command", appRunInput: []string{"myCLI", "myCommand", "--optional", "cats"}, appCommands: []*Command{{ Name: "myCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optional"}}, }}, expectedAnError: true, }, { testCase: "error_case_optional_input_with_required_flag_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--optional", "cats"}, appCommands: []*Command{{ Name: "myCommand", Commands: []*Command{{ Name: "mySubCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optional"}}, }}, }}, expectedAnError: true, }, // assertion: when a required flag is present, inputting that required flag does not error { testCase: "valid_case_required_flag_input_on_app", appRunInput: []string{"myCLI", "--requiredFlag", "cats"}, appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }, { testCase: "valid_case_required_flag_input_on_command", appRunInput: []string{"myCLI", "myCommand", "--requiredFlag", "cats"}, appCommands: []*Command{{ Name: "myCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, }}, }, { testCase: "valid_case_required_flag_input_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--requiredFlag", "cats"}, appCommands: []*Command{{ Name: "myCommand", Commands: []*Command{{ Name: "mySubCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, Action: func(context.Context, *Command) error { return nil }, }}, }}, }, } for _, test := range tdata { t.Run(test.testCase, func(t *testing.T) { // setup cmd := buildMinimalTestCommand() cmd.Flags = test.appFlags cmd.Commands = test.appCommands // logic under test err := cmd.Run(buildTestContext(t), test.appRunInput) // assertions if test.expectedAnError { assert.Error(t, err) if _, ok := err.(requiredFlagsErr); test.expectedAnError && !ok { t.Errorf("expected a requiredFlagsErr, but got: %s", err) } } else { assert.NoError(t, err) } }) } } func TestCommandHelpPrinter(t *testing.T) { oldPrinter := HelpPrinter defer func() { HelpPrinter = oldPrinter }() wasCalled := false HelpPrinter = func(io.Writer, string, interface{}) { wasCalled = true } cmd := &Command{} _ = cmd.Run(buildTestContext(t), []string{"-h"}) assert.True(t, wasCalled, "Help printer expected to be called, but was not") } func TestCommand_VersionPrinter(t *testing.T) { oldPrinter := VersionPrinter defer func() { VersionPrinter = oldPrinter }() wasCalled := false VersionPrinter = func(*Command) { wasCalled = true } cmd := &Command{} ShowVersion(cmd) assert.True(t, wasCalled, "Version printer expected to be called, but was not") } func TestCommand_CommandNotFound(t *testing.T) { counts := &opCounts{} cmd := &Command{ CommandNotFound: func(context.Context, *Command, string) { counts.Total++ counts.CommandNotFound = counts.Total }, Commands: []*Command{ { Name: "bar", Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, }, } _ = cmd.Run(buildTestContext(t), []string{"command", "foo"}) assert.Equal(t, 1, counts.CommandNotFound) assert.Equal(t, 0, counts.SubCommand) assert.Equal(t, 1, counts.Total) } func TestCommand_OrderOfOperations(t *testing.T) { buildCmdCounts := func() (*Command, *opCounts) { counts := &opCounts{} cmd := &Command{ EnableShellCompletion: true, ShellComplete: func(context.Context, *Command) { counts.Total++ counts.ShellComplete = counts.Total }, OnUsageError: func(context.Context, *Command, error, bool) error { counts.Total++ counts.OnUsageError = counts.Total return errors.New("hay OnUsageError") }, Writer: io.Discard, } beforeNoError := func(context.Context, *Command) (context.Context, error) { counts.Total++ counts.Before = counts.Total return nil, nil } cmd.Before = beforeNoError cmd.CommandNotFound = func(context.Context, *Command, string) { counts.Total++ counts.CommandNotFound = counts.Total } afterNoError := func(context.Context, *Command) error { counts.Total++ counts.After = counts.Total return nil } cmd.After = afterNoError cmd.Commands = []*Command{ { Name: "bar", Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, } cmd.Action = func(context.Context, *Command) error { counts.Total++ counts.Action = counts.Total return nil } return cmd, counts } t.Run("on usage error", func(t *testing.T) { cmd, counts := buildCmdCounts() r := require.New(t) _ = cmd.Run(buildTestContext(t), []string{"command", "--nope"}) r.Equal(1, counts.OnUsageError) r.Equal(1, counts.Total) }) t.Run("shell complete", func(t *testing.T) { cmd, counts := buildCmdCounts() r := require.New(t) _ = cmd.Run(buildTestContext(t), []string{"command", completionFlag}) r.Equal(1, counts.ShellComplete) r.Equal(1, counts.Total) }) t.Run("nil on usage error", func(t *testing.T) { cmd, counts := buildCmdCounts() cmd.OnUsageError = nil _ = cmd.Run(buildTestContext(t), []string{"command", "--nope"}) require.Equal(t, 0, counts.Total) }) t.Run("before after action hooks", func(t *testing.T) { cmd, counts := buildCmdCounts() r := require.New(t) _ = cmd.Run(buildTestContext(t), []string{"command", "foo"}) r.Equal(0, counts.OnUsageError) r.Equal(1, counts.Before) r.Equal(0, counts.CommandNotFound) r.Equal(2, counts.Action) r.Equal(3, counts.After) r.Equal(3, counts.Total) }) t.Run("before with error", func(t *testing.T) { cmd, counts := buildCmdCounts() cmd.Before = func(context.Context, *Command) (context.Context, error) { counts.Total++ counts.Before = counts.Total return nil, errors.New("hay Before") } r := require.New(t) _ = cmd.Run(buildTestContext(t), []string{"command", "bar"}) r.Equal(0, counts.OnUsageError) r.Equal(1, counts.Before) r.Equal(2, counts.After) r.Equal(2, counts.Total) }) t.Run("nil after", func(t *testing.T) { cmd, counts := buildCmdCounts() cmd.After = nil r := require.New(t) _ = cmd.Run(buildTestContext(t), []string{"command", "bar"}) r.Equal(0, counts.OnUsageError) r.Equal(1, counts.Before) r.Equal(2, counts.SubCommand) r.Equal(2, counts.Total) }) t.Run("after errors", func(t *testing.T) { cmd, counts := buildCmdCounts() cmd.After = func(context.Context, *Command) error { counts.Total++ counts.After = counts.Total return errors.New("hay After") } r := require.New(t) err := cmd.Run(buildTestContext(t), []string{"command", "bar"}) r.Error(err) r.Equal(0, counts.OnUsageError) r.Equal(1, counts.Before) r.Equal(2, counts.SubCommand) r.Equal(3, counts.After) r.Equal(3, counts.Total) }) t.Run("nil commands", func(t *testing.T) { cmd, counts := buildCmdCounts() cmd.Commands = nil r := require.New(t) _ = cmd.Run(buildTestContext(t), []string{"command"}) r.Equal(0, counts.OnUsageError) r.Equal(1, counts.Before) r.Equal(2, counts.Action) r.Equal(3, counts.After) r.Equal(3, counts.Total) }) } func TestCommand_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) { subcommandHelpTopics := [][]string{ {"foo", "--help"}, {"foo", "-h"}, {"foo", "help"}, } for _, flagSet := range subcommandHelpTopics { t.Run(fmt.Sprintf("checking with flags %v", flagSet), func(t *testing.T) { buf := new(bytes.Buffer) subCmdBar := &Command{ Name: "bar", Usage: "does bar things", } subCmdBaz := &Command{ Name: "baz", Usage: "does baz things", } cmd := &Command{ Name: "foo", Description: "descriptive wall of text about how it does foo things", Commands: []*Command{subCmdBar, subCmdBaz}, Action: func(context.Context, *Command) error { return nil }, Writer: buf, } err := cmd.Run(buildTestContext(t), flagSet) assert.NoError(t, err) output := buf.String() assert.NotContains(t, output, "No help topic for", "expect a help topic, got none") for _, shouldContain := range []string{ cmd.Name, cmd.Description, subCmdBar.Name, subCmdBar.Usage, subCmdBaz.Name, subCmdBaz.Usage, } { assert.Contains(t, output, shouldContain, "want help to contain %q, did not: \n%q", shouldContain, output) } }) } } func TestCommand_Run_SubcommandFullPath(t *testing.T) { out := &bytes.Buffer{} subCmd := &Command{ Name: "bar", Usage: "does bar things", ArgsUsage: "[arguments...]", } cmd := &Command{ Name: "foo", Description: "foo commands", Commands: []*Command{subCmd}, Writer: out, } require.NoError(t, cmd.Run(buildTestContext(t), []string{"foo", "bar", "--help"})) outString := out.String() require.Contains(t, outString, "foo bar - does bar things") require.Contains(t, outString, "foo bar [options] [arguments...]") } func TestCommand_Run_Help(t *testing.T) { tests := []struct { helpArguments []string hideHelp bool wantContains string wantErr error }{ { helpArguments: []string{"boom", "--help"}, hideHelp: false, wantContains: "boom - make an explosive entrance", }, { helpArguments: []string{"boom", "-h"}, hideHelp: false, wantContains: "boom - make an explosive entrance", }, { helpArguments: []string{"boom", "help"}, hideHelp: false, wantContains: "boom - make an explosive entrance", }, { helpArguments: []string{"boom", "--help"}, hideHelp: true, wantErr: fmt.Errorf("flag provided but not defined: -help"), }, { helpArguments: []string{"boom", "-h"}, hideHelp: true, wantErr: fmt.Errorf("flag provided but not defined: -h"), }, { helpArguments: []string{"boom", "help"}, hideHelp: true, wantContains: "boom I say!", }, } for _, tt := range tests { t.Run(fmt.Sprintf("checking with arguments %v%v", tt.helpArguments, tt.hideHelp), func(t *testing.T) { buf := new(bytes.Buffer) cmd := &Command{ Name: "boom", Usage: "make an explosive entrance", Writer: buf, HideHelp: tt.hideHelp, Action: func(context.Context, *Command) error { buf.WriteString("boom I say!") return nil }, } err := cmd.Run(buildTestContext(t), tt.helpArguments) if tt.wantErr != nil { assert.ErrorContains(t, err, tt.wantErr.Error()) } output := buf.String() assert.Contains(t, output, tt.wantContains, "want help to contain %q, did not: \n%q", "boom - make an explosive entrance", output) }) } } func TestCommand_Run_Version(t *testing.T) { versionArguments := [][]string{{"boom", "--version"}, {"boom", "-v"}} for _, args := range versionArguments { t.Run(fmt.Sprintf("checking with arguments %v", args), func(t *testing.T) { buf := new(bytes.Buffer) cmd := &Command{ Name: "boom", Usage: "make an explosive entrance", Version: "0.1.0", Writer: buf, Action: func(context.Context, *Command) error { buf.WriteString("boom I say!") return nil }, } err := cmd.Run(buildTestContext(t), args) assert.NoError(t, err) assert.Contains(t, buf.String(), "0.1.0", "want version to contain 0.1.0") }) } } func TestCommand_Run_Categories(t *testing.T) { buf := new(bytes.Buffer) cmd := &Command{ Name: "categories", HideHelp: true, Commands: []*Command{ { Name: "command1", Category: "1", }, { Name: "command2", Category: "1", }, { Name: "command3", Category: "2", }, }, Writer: buf, } _ = cmd.Run(buildTestContext(t), []string{"categories"}) expect := commandCategories([]*commandCategory{ { name: "1", commands: []*Command{ cmd.Commands[0], cmd.Commands[1], }, }, { name: "2", commands: []*Command{ cmd.Commands[2], }, }, }) require.Equal(t, &expect, cmd.categories) output := buf.String() assert.Contains(t, output, "1:\n command1", "want buffer to include category %q, did not: \n%q", "1:\n command1", output) } func TestCommand_VisibleCategories(t *testing.T) { cmd := &Command{ Name: "visible-categories", HideHelp: true, Commands: []*Command{ { Name: "command1", Category: "1", Hidden: true, }, { Name: "command2", Category: "2", }, { Name: "command3", Category: "3", }, }, } expected := []CommandCategory{ &commandCategory{ name: "2", commands: []*Command{ cmd.Commands[1], }, }, &commandCategory{ name: "3", commands: []*Command{ cmd.Commands[2], }, }, } cmd.setupDefaults([]string{"test"}) assert.Equal(t, expected, cmd.VisibleCategories()) cmd = &Command{ Name: "visible-categories", HideHelp: true, Commands: []*Command{ { Name: "command1", Category: "1", Hidden: true, }, { Name: "command2", Category: "2", Hidden: true, }, { Name: "command3", Category: "3", }, }, } expected = []CommandCategory{ &commandCategory{ name: "3", commands: []*Command{ cmd.Commands[2], }, }, } cmd.setupDefaults([]string{"test"}) assert.Equal(t, expected, cmd.VisibleCategories()) cmd = &Command{ Name: "visible-categories", HideHelp: true, Commands: []*Command{ { Name: "command1", Category: "1", Hidden: true, }, { Name: "command2", Category: "2", Hidden: true, }, { Name: "command3", Category: "3", Hidden: true, }, }, } cmd.setupDefaults([]string{"test"}) assert.Empty(t, cmd.VisibleCategories()) } func TestCommand_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Commands: []*Command{ { Name: "sub", }, }, Name: "bar", Before: func(context.Context, *Command) (context.Context, error) { return nil, fmt.Errorf("before error") }, After: func(context.Context, *Command) error { return fmt.Errorf("after error") }, }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "bar"}) assert.ErrorContains(t, err, "before error") assert.ErrorContains(t, err, "after error") } func TestCommand_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { cmd := &Command{ Flags: []Flag{ &Int64Flag{Name: "flag"}, }, OnUsageError: func(_ context.Context, _ *Command, err error, isSubcommand bool) error { assert.False(t, isSubcommand, "Expect subcommand") assert.ErrorContains(t, err, "\"wrong\": invalid syntax") return errors.New("intercepted: " + err.Error()) }, Commands: []*Command{ { Name: "bar", }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "--flag=wrong", "bar"}) assert.ErrorContains(t, err, "parsing \"wrong\": invalid syntax", "Expect an intercepted error") } // A custom flag that conforms to the relevant interfaces, but has none of the // fields that the other flag types do. type customBoolFlag struct { Nombre string } // Don't use the normal FlagStringer func (c *customBoolFlag) String() string { return "***" + c.Nombre + "***" } func (c *customBoolFlag) Names() []string { return []string{c.Nombre} } func (c *customBoolFlag) TakesValue() bool { return false } func (c *customBoolFlag) GetValue() string { return "value" } func (c *customBoolFlag) GetUsage() string { return "usage" } func (c *customBoolFlag) PreParse() error { return nil } func (c *customBoolFlag) PostParse() error { return nil } func (c *customBoolFlag) Get() any { dest := false return &boolValue{ destination: &dest, } } func (c *customBoolFlag) Set(_, _ string) error { return nil } func (c *customBoolFlag) RunAction(context.Context, *Command) error { return nil } func (c *customBoolFlag) IsSet() bool { return false } func (c *customBoolFlag) IsRequired() bool { return false } func (c *customBoolFlag) IsVisible() bool { return false } func (c *customBoolFlag) GetCategory() string { return "" } func (c *customBoolFlag) GetEnvVars() []string { return nil } func (c *customBoolFlag) GetDefaultText() string { return "" } func TestCustomFlagsUnused(t *testing.T) { cmd := &Command{ Flags: []Flag{&customBoolFlag{"custom"}}, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"foo"}) assert.NoError(t, err, "Run returned unexpected error") } func TestCustomFlagsUsed(t *testing.T) { cmd := &Command{ Flags: []Flag{&customBoolFlag{"custom"}}, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"foo", "--custom=bar"}) assert.NoError(t, err, "Run returned unexpected error") } func TestCustomHelpVersionFlags(t *testing.T) { cmd := &Command{ Writer: io.Discard, } // Be sure to reset the global flags defer func(helpFlag Flag, versionFlag Flag) { HelpFlag = helpFlag.(*BoolFlag) VersionFlag = versionFlag.(*BoolFlag) }(HelpFlag, VersionFlag) HelpFlag = &customBoolFlag{"help-custom"} VersionFlag = &customBoolFlag{"version-custom"} err := cmd.Run(buildTestContext(t), []string{"foo", "--help-custom=bar"}) assert.NoError(t, err, "Run returned unexpected error") } func TestHandleExitCoder_Default(t *testing.T) { app := buildMinimalTestCommand() _ = app.handleExitCoder(context.Background(), Exit("Default Behavior Error", 42)) output := fakeErrWriter.String() assert.Contains(t, output, "Default", "Expected Default Behavior from Error Handler") } func TestHandleExitCoder_Custom(t *testing.T) { cmd := buildMinimalTestCommand() cmd.ExitErrHandler = func(context.Context, *Command, error) { _, _ = fmt.Fprintln(ErrWriter, "I'm a Custom error handler, I print what I want!") } _ = cmd.handleExitCoder(context.Background(), Exit("Default Behavior Error", 42)) output := fakeErrWriter.String() assert.Contains(t, output, "Custom", "Expected Custom Behavior from Error Handler") } func TestShellCompletionForIncompleteFlags(t *testing.T) { cmd := &Command{ Flags: []Flag{ &Int64Flag{ Name: "test-completion", }, }, EnableShellCompletion: true, ShellComplete: func(_ context.Context, cmd *Command) { for _, command := range cmd.Commands { if command.Hidden { continue } for _, name := range command.Names() { _, _ = fmt.Fprintln(cmd.Writer, name) } } for _, fl := range cmd.Flags { for _, name := range fl.Names() { if name == GenerateShellCompletionFlag.Names()[0] { continue } switch name = strings.TrimSpace(name); len(name) { case 0: case 1: _, _ = fmt.Fprintln(cmd.Writer, "-"+name) default: _, _ = fmt.Fprintln(cmd.Writer, "--"+name) } } } }, Action: func(context.Context, *Command) error { return fmt.Errorf("should not get here") }, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"", "--test-completion", completionFlag}) assert.NoError(t, err, "app should not return an error") } func TestWhenExitSubCommandWithCodeThenCommandQuitUnexpectedly(t *testing.T) { testCode := 104 cmd := buildMinimalTestCommand() cmd.Commands = []*Command{ { Name: "cmd", Commands: []*Command{ { Name: "subcmd", Action: func(context.Context, *Command) error { return Exit("exit error", testCode) }, }, }, }, } // set user function as ExitErrHandler exitCodeFromExitErrHandler := int(0) cmd.ExitErrHandler = func(_ context.Context, _ *Command, err error) { if exitErr, ok := err.(ExitCoder); ok { exitCodeFromExitErrHandler = exitErr.ExitCode() } } // keep and restore original OsExiter origExiter := OsExiter t.Cleanup(func() { OsExiter = origExiter }) // set user function as OsExiter exitCodeFromOsExiter := int(0) OsExiter = func(exitCode int) { exitCodeFromOsExiter = exitCode } r := require.New(t) r.Error(cmd.Run(buildTestContext(t), []string{ "myapp", "cmd", "subcmd", })) r.Equal(0, exitCodeFromOsExiter) r.Equal(testCode, exitCodeFromExitErrHandler) } func buildMinimalTestCommand() *Command { // reset the help flag because tests may have set it HelpFlag.(*BoolFlag).hasBeenSet = false return &Command{Writer: io.Discard} } func TestSetupInitializesBothWriters(t *testing.T) { cmd := &Command{} cmd.setupDefaults([]string{"test"}) assert.Equal(t, cmd.ErrWriter, os.Stderr, "expected a.ErrWriter to be os.Stderr") assert.Equal(t, cmd.Writer, os.Stdout, "expected a.Writer to be os.Stdout") } func TestSetupInitializesOnlyNilWriters(t *testing.T) { wr := &bytes.Buffer{} cmd := &Command{ ErrWriter: wr, } cmd.setupDefaults([]string{"test"}) assert.Equal(t, cmd.ErrWriter, wr, "expected a.ErrWriter to be a *bytes.Buffer instance") assert.Equal(t, cmd.Writer, os.Stdout, "expected a.Writer to be os.Stdout") } func TestFlagAction(t *testing.T) { now := time.Now().UTC().Truncate(time.Minute) testCases := []struct { name string args []string err string exp string }{ { name: "flag_string", args: []string{"app", "--f_string=string"}, exp: "string ", }, { name: "flag_string_error", args: []string{"app", "--f_string="}, err: "flag needs an argument: --f_string=", }, { name: "flag_string_error2", args: []string{"app", "--f_string=", "--f_bool"}, err: "flag needs an argument: --f_string=", }, { name: "flag_string_slice", args: []string{"app", "--f_string_slice=s1,s2,s3"}, exp: "[s1 s2 s3] ", }, { name: "flag_string_slice_error", args: []string{"app", "--f_string_slice=err"}, err: "error string slice", }, { name: "flag_bool", args: []string{"app", "--f_bool"}, exp: "true ", }, { name: "flag_bool_error", args: []string{"app", "--f_bool=false"}, err: "value is false", }, { name: "flag_duration", args: []string{"app", "--f_duration=1h30m20s"}, exp: "1h30m20s ", }, { name: "flag_duration_error", args: []string{"app", "--f_duration=0"}, err: "empty duration", }, { name: "flag_float64", args: []string{"app", "--f_float64=3.14159"}, exp: "3.14159 ", }, { name: "flag_float64_error", args: []string{"app", "--f_float64=-1"}, err: "negative float64", }, { name: "flag_float64_slice", args: []string{"app", "--f_float64_slice=1.1,2.2,3.3"}, exp: "[1.1 2.2 3.3] ", }, { name: "flag_float64_slice_error", args: []string{"app", "--f_float64_slice=-1"}, err: "invalid float64 slice", }, { name: "flag_int", args: []string{"app", "--f_int=1"}, exp: "1 ", }, { name: "flag_int_error", args: []string{"app", "--f_int=-1"}, err: "negative int", }, { name: "flag_int_slice", args: []string{"app", "--f_int_slice=1,2,3"}, exp: "[1 2 3] ", }, { name: "flag_int_slice_error", args: []string{"app", "--f_int_slice=-1"}, err: "invalid int slice", }, { name: "flag_timestamp", args: []string{"app", "--f_timestamp", now.Format(time.DateTime)}, exp: now.UTC().Format(time.RFC3339) + " ", }, { name: "flag_timestamp_error", args: []string{"app", "--f_timestamp", "0001-01-01 00:00:00"}, err: "zero timestamp", }, { name: "flag_uint", args: []string{"app", "--f_uint=1"}, exp: "1 ", }, { name: "flag_uint_error", args: []string{"app", "--f_uint=0"}, err: "zero uint64", }, { name: "flag_no_action", args: []string{"app", "--f_no_action=xx"}, exp: "", }, { name: "command_flag", args: []string{"app", "c1", "--f_string=c1"}, exp: "c1 ", }, { name: "subCommand_flag", args: []string{"app", "c1", "sub1", "--f_string=sub1"}, exp: "sub1 ", }, // TBD /* { name: "mixture", args: []string{"app", "--f_string=app", "--f_uint=1", "--f_int_slice=1,2,3", "--f_duration=1h30m20s", "c1", "--f_string=c1", "sub1", "--f_string=sub1"}, exp: "app 1 [1 2 3] 1h30m20s c1 sub1 ", },*/ { name: "flag_string_map", args: []string{"app", "--f_string_map=s1=s2,s3="}, exp: "map[s1:s2 s3:]", }, { name: "flag_string_map_error", args: []string{"app", "--f_string_map=err="}, err: "error string map", }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { out := &bytes.Buffer{} newStringFlag := func(local bool) *StringFlag { return &StringFlag{ Local: local, Name: "f_string", Action: func(_ context.Context, cmd *Command, v string) error { if v == "" { return fmt.Errorf("empty string") } _, err := cmd.Root().Writer.Write([]byte(v + " ")) return err }, } } cmd := &Command{ Writer: out, Name: "app", Commands: []*Command{ { Name: "c1", Flags: []Flag{newStringFlag(true)}, Action: func(_ context.Context, cmd *Command) error { return nil }, Commands: []*Command{ { Name: "sub1", Action: func(context.Context, *Command) error { return nil }, Flags: []Flag{newStringFlag(true)}, }, }, }, }, Flags: []Flag{ newStringFlag(true), &StringFlag{ Name: "f_no_action", }, &StringSliceFlag{ Local: true, Name: "f_string_slice", Action: func(_ context.Context, cmd *Command, v []string) error { if v[0] == "err" { return fmt.Errorf("error string slice") } _, err := fmt.Fprintf(cmd.Root().Writer, "%v ", v) return err }, }, &BoolFlag{ Name: "f_bool", Local: true, Action: func(_ context.Context, cmd *Command, v bool) error { if !v { return fmt.Errorf("value is false") } _, err := fmt.Fprintf(cmd.Root().Writer, "%t ", v) return err }, }, &DurationFlag{ Name: "f_duration", Local: true, Action: func(_ context.Context, cmd *Command, v time.Duration) error { if v == 0 { return fmt.Errorf("empty duration") } _, err := fmt.Fprintf(cmd.Root().Writer, v.String()+" ") return err }, }, &FloatFlag{ Name: "f_float64", Local: true, Action: func(_ context.Context, cmd *Command, v float64) error { if v < 0 { return fmt.Errorf("negative float64") } _, err := fmt.Fprintf(cmd.Root().Writer, strconv.FormatFloat(v, 'f', -1, 64)+" ") return err }, }, &FloatSliceFlag{ Name: "f_float64_slice", Local: true, Action: func(_ context.Context, cmd *Command, v []float64) error { if len(v) > 0 && v[0] < 0 { return fmt.Errorf("invalid float64 slice") } _, err := fmt.Fprintf(cmd.Root().Writer, "%v ", v) return err }, }, &Int64Flag{ Name: "f_int", Local: true, Action: func(_ context.Context, cmd *Command, v int64) error { if v < 0 { return fmt.Errorf("negative int") } _, err := fmt.Fprintf(cmd.Root().Writer, "%v ", v) return err }, }, &Int64SliceFlag{ Name: "f_int_slice", Local: true, Action: func(_ context.Context, cmd *Command, v []int64) error { if len(v) > 0 && v[0] < 0 { return fmt.Errorf("invalid int slice") } _, err := fmt.Fprintf(cmd.Root().Writer, "%v ", v) return err }, }, &TimestampFlag{ Name: "f_timestamp", Local: true, Config: TimestampConfig{ Timezone: time.UTC, Layouts: []string{time.DateTime}, }, Action: func(_ context.Context, cmd *Command, v time.Time) error { if v.IsZero() { return fmt.Errorf("zero timestamp") } _, err := cmd.Root().Writer.Write([]byte(v.Format(time.RFC3339) + " ")) return err }, }, &Uint64Flag{ Name: "f_uint", Local: true, Action: func(_ context.Context, cmd *Command, v uint64) error { if v == 0 { return fmt.Errorf("zero uint64") } _, err := fmt.Fprintf(cmd.Root().Writer, "%v ", v) return err }, }, &StringMapFlag{ Name: "f_string_map", Local: true, Action: func(_ context.Context, cmd *Command, v map[string]string) error { if _, ok := v["err"]; ok { return fmt.Errorf("error string map") } _, err := fmt.Fprintf(cmd.Root().Writer, "%v", v) return err }, }, }, Action: func(context.Context, *Command) error { return nil }, } err := cmd.Run(buildTestContext(t), test.args) r := require.New(t) if test.err != "" { r.EqualError(err, test.err) return } r.NoError(err) r.Equal(test.exp, out.String()) }) } } func TestLocalFlagError(t *testing.T) { var topInt int64 cmd := &Command{ Flags: []Flag{ &Int64Flag{ Name: "cmdFlag", Destination: &topInt, Local: true, }, }, Commands: []*Command{ { Name: "subcmd", }, }, } err := cmd.Run(buildTestContext(t), []string{ "app", "subcmd", "--cmdFlag", "11", }) assert.Error(t, err) assert.Contains(t, err.Error(), "flag provided but not defined: -cmdFlag") } func TestPersistentFlag(t *testing.T) { var topInt, topPersistentInt, subCommandInt, appOverrideInt int64 var appFlag string var appRequiredFlag string var appOverrideCmdInt int64 var appSliceFloat64 []float64 var persistentCommandSliceInt []int64 var persistentFlagActionCount int64 cmd := &Command{ Flags: []Flag{ &StringFlag{ Name: "persistentCommandFlag", Destination: &appFlag, Action: func(context.Context, *Command, string) error { persistentFlagActionCount++ return nil }, }, &Int64SliceFlag{ Name: "persistentCommandSliceFlag", Destination: &persistentCommandSliceInt, }, &FloatSliceFlag{ Name: "persistentCommandFloatSliceFlag", Value: []float64{11.3, 12.5}, }, &Int64Flag{ Name: "persistentCommandOverrideFlag", Destination: &appOverrideInt, }, &StringFlag{ Name: "persistentRequiredCommandFlag", Required: true, Destination: &appRequiredFlag, }, }, Commands: []*Command{ { Name: "cmd", Flags: []Flag{ &Int64Flag{ Name: "cmdFlag", Destination: &topInt, Local: true, }, &Int64Flag{ Name: "cmdPersistentFlag", Destination: &topPersistentInt, }, &Int64Flag{ Name: "paof", Aliases: []string{"persistentCommandOverrideFlag"}, Destination: &appOverrideCmdInt, Local: true, }, }, Commands: []*Command{ { Name: "subcmd", Flags: []Flag{ &Int64Flag{ Name: "cmdFlag", Destination: &subCommandInt, Local: true, }, }, Action: func(_ context.Context, cmd *Command) error { appSliceFloat64 = cmd.FloatSlice("persistentCommandFloatSliceFlag") return nil }, }, }, }, }, } err := cmd.Run(buildTestContext(t), []string{ "app", "--persistentCommandFlag", "hello", "--persistentCommandSliceFlag", "100", "--persistentCommandOverrideFlag", "102", "cmd", "--cmdFlag", "12", "--persistentCommandSliceFlag", "102", "--persistentCommandFloatSliceFlag", "102.455", "--paof", "105", "--persistentRequiredCommandFlag", "hellor", "subcmd", "--cmdPersistentFlag", "20", "--cmdFlag", "11", "--persistentCommandFlag", "bar", "--persistentCommandSliceFlag", "130", "--persistentCommandFloatSliceFlag", "3.1445", }) require.NoError(t, err) assert.Equal(t, "bar", appFlag) assert.Equal(t, "hellor", appRequiredFlag) assert.Equal(t, int64(12), topInt) assert.Equal(t, int64(20), topPersistentInt) // this should be changed from app since // cmd overrides it assert.Equal(t, int64(102), appOverrideInt) assert.Equal(t, int64(11), subCommandInt) assert.Equal(t, int64(105), appOverrideCmdInt) assert.Equal(t, []int64{100, 102, 130}, persistentCommandSliceInt) assert.Equal(t, []float64{102.455, 3.1445}, appSliceFloat64) assert.Equal(t, int64(2), persistentFlagActionCount, "Expected persistent flag action to be called 2 times") } func TestPersistentFlagIsSet(t *testing.T) { result := "" resultIsSet := false app := &Command{ Name: "root", Flags: []Flag{ &StringFlag{ Name: "result", }, }, Commands: []*Command{ { Name: "sub", Action: func(_ context.Context, cmd *Command) error { result = cmd.String("result") resultIsSet = cmd.IsSet("result") return nil }, }, }, } err := app.Run(context.Background(), []string{"root", "--result", "before", "sub"}) require.NoError(t, err) require.Equal(t, "before", result) require.True(t, resultIsSet) err = app.Run(context.Background(), []string{"root", "sub", "--result", "after"}) require.NoError(t, err) require.Equal(t, "after", result) require.True(t, resultIsSet) } func TestRequiredFlagDelayed(t *testing.T) { sf := &StringFlag{ Name: "result", Required: true, } expectedErr := &errRequiredFlags{ missingFlags: []string{sf.Name}, } tests := []struct { name string args []string errExpected error }{ { name: "leaf help", args: []string{"root", "sub", "-h"}, errExpected: nil, }, { name: "leaf action", args: []string{"root", "sub"}, errExpected: expectedErr, }, { name: "leaf flags set", args: []string{"root", "sub", "--if", "10"}, errExpected: expectedErr, }, { name: "leaf invalid flags set", args: []string{"root", "sub", "--xx"}, errExpected: expectedErr, }, } app := &Command{ Name: "root", Flags: []Flag{ sf, }, Commands: []*Command{ { Name: "sub", Flags: []Flag{ &Int64Flag{ Name: "if", Required: true, }, }, Action: func(ctx context.Context, c *Command) error { return nil }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := app.Run(context.Background(), test.args) if test.errExpected == nil { require.NoError(t, err) } else { require.ErrorAs(t, err, &test.errExpected) } }) } } func TestRequiredPersistentFlag(t *testing.T) { app := &Command{ Name: "root", Flags: []Flag{ &StringFlag{ Name: "result", Required: true, }, }, Commands: []*Command{ { Name: "sub", Action: func(ctx context.Context, c *Command) error { return nil }, }, }, } err := app.Run(context.Background(), []string{"root", "sub"}) require.Error(t, err) err = app.Run(context.Background(), []string{"root", "sub", "--result", "after"}) require.NoError(t, err) } func TestFlagDuplicates(t *testing.T) { tests := []struct { name string args []string errExpected bool }{ { name: "all args present once", args: []string{"foo", "--sflag", "hello", "--isflag", "1", "--isflag", "2", "--fsflag", "2.0", "--iflag", "10", "--bifflag"}, }, { name: "duplicate non slice flag(duplicatable)", args: []string{"foo", "--sflag", "hello", "--isflag", "1", "--isflag", "2", "--fsflag", "2.0", "--iflag", "10", "--iflag", "20"}, }, { name: "duplicate non slice flag(non duplicatable)", args: []string{"foo", "--sflag", "hello", "--isflag", "1", "--isflag", "2", "--fsflag", "2.0", "--iflag", "10", "--sflag", "trip"}, errExpected: true, }, { name: "duplicate slice flag(non duplicatable)", args: []string{"foo", "--sflag", "hello", "--isflag", "1", "--isflag", "2", "--fsflag", "2.0", "--fsflag", "3.0", "--iflag", "10"}, errExpected: true, }, { name: "duplicate bool inverse flag(non duplicatable)", args: []string{"foo", "--bifflag", "--bifflag"}, errExpected: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd := &Command{ Flags: []Flag{ &StringFlag{ Name: "sflag", OnlyOnce: true, }, &Int64SliceFlag{ Name: "isflag", }, &FloatSliceFlag{ Name: "fsflag", OnlyOnce: true, }, &BoolWithInverseFlag{ Name: "bifflag", OnlyOnce: true, }, &Int64Flag{ Name: "iflag", }, }, Action: func(context.Context, *Command) error { return nil }, } err := cmd.Run(buildTestContext(t), test.args) if test.errExpected { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestShorthandCommand(t *testing.T) { af := func(p *int) ActionFunc { return func(context.Context, *Command) error { *p = *p + 1 return nil } } var cmd1, cmd2 int cmd := &Command{ PrefixMatchCommands: true, Commands: []*Command{ { Name: "cthdisd", Aliases: []string{"cth"}, Action: af(&cmd1), }, { Name: "cthertoop", Aliases: []string{"cer"}, Action: af(&cmd2), }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "cth"}) assert.NoError(t, err) assert.True(t, cmd1 == 1 && cmd2 == 0, "Expected command1 to be triggered once") cmd1 = 0 cmd2 = 0 err = cmd.Run(buildTestContext(t), []string{"foo", "cthd"}) assert.NoError(t, err) assert.True(t, cmd1 == 1 && cmd2 == 0, "Expected command1 to be triggered once") cmd1 = 0 cmd2 = 0 err = cmd.Run(buildTestContext(t), []string{"foo", "cthe"}) assert.NoError(t, err) assert.True(t, cmd1 == 1 && cmd2 == 0, "Expected command1 to be triggered once") cmd1 = 0 cmd2 = 0 err = cmd.Run(buildTestContext(t), []string{"foo", "cthert"}) assert.NoError(t, err) assert.True(t, cmd1 == 0 && cmd2 == 1, "Expected command1 to be triggered once") cmd1 = 0 cmd2 = 0 err = cmd.Run(buildTestContext(t), []string{"foo", "cthet"}) assert.NoError(t, err) assert.True(t, cmd1 == 0 && cmd2 == 1, "Expected command1 to be triggered once") } func TestCommand_Int(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &Int64Flag{ Name: "myflag", Value: 12, }, }, } cmd := &Command{ Flags: []Flag{ &Int64Flag{ Name: "top-flag", Value: 13, }, }, parent: pCmd, } require.Equal(t, int64(12), cmd.Int64("myflag")) require.Equal(t, int64(13), cmd.Int64("top-flag")) } func TestCommand_Uint(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &Uint64Flag{ Name: "myflagUint", Value: 13, }, }, } cmd := &Command{ Flags: []Flag{ &Uint64Flag{ Name: "top-flag", Value: 14, }, }, parent: pCmd, } require.Equal(t, uint64(13), cmd.Uint64("myflagUint")) require.Equal(t, uint64(14), cmd.Uint64("top-flag")) } func TestCommand_Float64(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &FloatFlag{ Name: "myflag", Value: 17, }, }, } cmd := &Command{ Flags: []Flag{ &FloatFlag{ Name: "top-flag", Value: 18, }, }, parent: pCmd, } r := require.New(t) r.Equal(float64(17), cmd.Float("myflag")) r.Equal(float64(18), cmd.Float("top-flag")) } func TestCommand_Duration(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &DurationFlag{ Name: "myflag", Value: 12 * time.Second, }, }, } cmd := &Command{ Flags: []Flag{ &DurationFlag{ Name: "top-flag", Value: 13 * time.Second, }, }, parent: pCmd, } r := require.New(t) r.Equal(12*time.Second, cmd.Duration("myflag")) r.Equal(13*time.Second, cmd.Duration("top-flag")) } func TestCommand_Timestamp(t *testing.T) { t1 := time.Time{}.Add(12 * time.Second) t2 := time.Time{}.Add(13 * time.Second) cmd := &Command{ Name: "hello", Flags: []Flag{ &TimestampFlag{ Name: "myflag", Value: t1, }, }, Action: func(ctx context.Context, c *Command) error { return nil }, } pCmd := &Command{ Flags: []Flag{ &TimestampFlag{ Name: "top-flag", Value: t2, }, }, Commands: []*Command{ cmd, }, } err := pCmd.Run(context.Background(), []string{"foo", "hello"}) assert.NoError(t, err) r := require.New(t) r.Equal(t1, cmd.Timestamp("myflag")) r.Equal(t2, cmd.Timestamp("top-flag")) } func TestCommand_String(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &StringFlag{ Name: "myflag", Value: "hello world", }, }, } cmd := &Command{ Flags: []Flag{ &StringFlag{ Name: "top-flag", Value: "hai veld", }, }, parent: pCmd, } r := require.New(t) r.Equal("hello world", cmd.String("myflag")) r.Equal("hai veld", cmd.String("top-flag")) r.Equal("hai veld", cmd.String("top-flag")) } func TestCommand_Bool(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "myflag", }, }, } cmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "top-flag", Value: true, }, }, parent: pCmd, } r := require.New(t) r.False(cmd.Bool("myflag")) r.True(cmd.Bool("top-flag")) } func TestCommand_Value(t *testing.T) { subCmd := &Command{ Name: "test", Flags: []Flag{ &Int64Flag{ Name: "myflag", Usage: "doc", Aliases: []string{"m", "mf"}, }, }, Action: func(ctx context.Context, c *Command) error { return nil }, } cmd := &Command{ Flags: []Flag{ &Int64Flag{ Name: "top-flag", Usage: "doc", Aliases: []string{"t", "tf"}, }, }, Commands: []*Command{ subCmd, }, } t.Run("flag name", func(t *testing.T) { r := require.New(t) err := cmd.Run(buildTestContext(t), []string{"main", "--top-flag", "13", "test", "--myflag", "14"}) r.NoError(err) r.Equal(int64(13), cmd.Value("top-flag")) r.Equal(int64(13), cmd.Value("t")) r.Equal(int64(13), cmd.Value("tf")) r.Equal(int64(14), subCmd.Value("myflag")) r.Equal(int64(14), subCmd.Value("m")) r.Equal(int64(14), subCmd.Value("mf")) }) t.Run("flag aliases", func(t *testing.T) { r := require.New(t) err := cmd.Run(buildTestContext(t), []string{"main", "-tf", "15", "test", "-m", "16"}) r.NoError(err) r.Equal(int64(15), cmd.Value("top-flag")) r.Equal(int64(15), cmd.Value("t")) r.Equal(int64(15), cmd.Value("tf")) r.Equal(int64(16), subCmd.Value("myflag")) r.Equal(int64(16), subCmd.Value("m")) r.Equal(int64(16), subCmd.Value("mf")) r.Nil(cmd.Value("unknown-flag")) }) } func TestCommand_Value_InvalidFlagAccessHandler(t *testing.T) { var flagName string cmd := &Command{ InvalidFlagAccessHandler: func(_ context.Context, _ *Command, name string) { flagName = name }, Commands: []*Command{ { Name: "command", Commands: []*Command{ { Name: "subcommand", Action: func(_ context.Context, cmd *Command) error { cmd.Value("missing") return nil }, }, }, }, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"run", "command", "subcommand"})) r.Equal("missing", flagName) } func TestCommand_Args(t *testing.T) { cmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "myflag", }, }, } _ = cmd.Run(context.Background(), []string{"", "--myflag", "bat", "baz"}) r := require.New(t) r.Equal(2, cmd.Args().Len()) r.True(cmd.Bool("myflag")) r.Equal(2, cmd.NArg()) } func TestCommand_IsSet(t *testing.T) { cmd := &Command{ Name: "frob", Flags: []Flag{ &BoolFlag{ Name: "one-flag", }, &BoolFlag{ Name: "two-flag", }, &StringFlag{ Name: "three-flag", Value: "hello world", }, }, } pCmd := &Command{ Name: "root", Flags: []Flag{ &BoolFlag{ Name: "top-flag", Value: true, }, }, Commands: []*Command{ cmd, }, } r := require.New(t) r.NoError(pCmd.Run(context.Background(), []string{"foo", "frob", "--one-flag", "--top-flag", "--two-flag", "--three-flag", "dds"})) r.True(cmd.IsSet("one-flag")) r.True(cmd.IsSet("two-flag")) r.True(cmd.IsSet("three-flag")) r.True(cmd.IsSet("top-flag")) r.False(cmd.IsSet("bogus")) } // XXX Corresponds to hack in context.IsSet for flags with EnvVar field // Should be moved to `flag_test` in v2 func TestCommand_IsSet_fromEnv(t *testing.T) { var ( timeoutIsSet, tIsSet bool noEnvVarIsSet, nIsSet bool passwordIsSet, pIsSet bool unparsableIsSet, uIsSet bool ) t.Setenv("APP_TIMEOUT_SECONDS", "15.5") t.Setenv("APP_PASSWORD", "") cmd := &Command{ Flags: []Flag{ &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Local: true, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, &StringFlag{Name: "password", Aliases: []string{"p"}, Local: true, Sources: EnvVars("APP_PASSWORD")}, &FloatFlag{Name: "unparsable", Aliases: []string{"u"}, Local: true, Sources: EnvVars("APP_UNPARSABLE")}, &FloatFlag{Name: "no-env-var", Aliases: []string{"n"}, Local: true}, }, Action: func(_ context.Context, cmd *Command) error { timeoutIsSet = cmd.IsSet("timeout") tIsSet = cmd.IsSet("t") passwordIsSet = cmd.IsSet("password") pIsSet = cmd.IsSet("p") unparsableIsSet = cmd.IsSet("unparsable") uIsSet = cmd.IsSet("u") noEnvVarIsSet = cmd.IsSet("no-env-var") nIsSet = cmd.IsSet("n") return nil }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"run"})) r.True(timeoutIsSet) r.True(tIsSet) r.True(passwordIsSet) r.True(pIsSet) r.False(noEnvVarIsSet) r.False(nIsSet) t.Setenv("APP_UNPARSABLE", "foobar") r.Error(cmd.Run(buildTestContext(t), []string{"run"})) r.False(unparsableIsSet) r.False(uIsSet) } func TestCommand_NumFlags(t *testing.T) { rootCmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "myflagGlobal", Value: true, }, }, } cmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "myflag", }, &StringFlag{ Name: "otherflag", Value: "hello world", }, }, } _ = cmd.Run(context.Background(), []string{"", "--myflag", "--otherflag=foo"}) _ = rootCmd.Run(context.Background(), []string{"", "--myflagGlobal"}) require.Equal(t, 2, cmd.NumFlags()) actualFlags := cmd.LocalFlagNames() sort.Strings(actualFlags) require.Equal(t, []string{"myflag", "otherflag"}, actualFlags) actualFlags = cmd.FlagNames() sort.Strings(actualFlags) require.Equal(t, []string{"myflag", "otherflag"}, actualFlags) cmd.parent = rootCmd lineage := cmd.Lineage() r := require.New(t) r.Equal(2, len(lineage)) r.Equal(cmd, lineage[0]) r.Equal(rootCmd, lineage[1]) } func TestCommand_Set(t *testing.T) { cmd := &Command{ Flags: []Flag{ &Int64Flag{ Name: "int", Value: 5, }, }, } r := require.New(t) r.False(cmd.IsSet("int")) r.NoError(cmd.Set("int", "1")) r.Equal(int64(1), cmd.Int64("int")) r.True(cmd.IsSet("int")) } func TestCommand_Set_InvalidFlagAccessHandler(t *testing.T) { var flagName string cmd := &Command{ InvalidFlagAccessHandler: func(_ context.Context, _ *Command, name string) { flagName = name }, } r := require.New(t) r.True(cmd.Set("missing", "") != nil) r.Equal("missing", flagName) } func TestCommand_lookupFlag(t *testing.T) { pCmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "top-flag", Value: true, }, }, } cmd := &Command{ Flags: []Flag{ &BoolFlag{ Name: "local-flag", }, }, } _ = cmd.Run(context.Background(), []string{"--local-flag"}) pCmd.Commands = []*Command{cmd} _ = pCmd.Run(context.Background(), []string{"--top-flag"}) r := require.New(t) fs := cmd.lookupFlag("top-flag") r.Equal(pCmd.Flags[0], fs) fs = cmd.lookupFlag("local-flag") r.Equal(cmd.Flags[0], fs) r.Nil(cmd.lookupFlag("frob")) } func TestCommandAttributeAccessing(t *testing.T) { tdata := []struct { testCase string setBoolInput string ctxBoolInput string parent *Command }{ { testCase: "empty", setBoolInput: "", ctxBoolInput: "", }, { testCase: "empty_with_background_context", setBoolInput: "", ctxBoolInput: "", parent: &Command{}, }, { testCase: "empty_set_bool_and_present_ctx_bool", setBoolInput: "", ctxBoolInput: "ctx-bool", }, { testCase: "present_set_bool_and_present_ctx_bool_with_background_context", setBoolInput: "", ctxBoolInput: "ctx-bool", parent: &Command{}, }, { testCase: "present_set_bool_and_present_ctx_bool", setBoolInput: "ctx-bool", ctxBoolInput: "ctx-bool", }, { testCase: "present_set_bool_and_present_ctx_bool_with_background_context", setBoolInput: "ctx-bool", ctxBoolInput: "ctx-bool", parent: &Command{}, }, { testCase: "present_set_bool_and_different_ctx_bool", setBoolInput: "ctx-bool", ctxBoolInput: "not-ctx-bool", }, { testCase: "present_set_bool_and_different_ctx_bool_with_background_context", setBoolInput: "ctx-bool", ctxBoolInput: "not-ctx-bool", parent: &Command{}, }, } for _, test := range tdata { t.Run(test.testCase, func(t *testing.T) { cmd := &Command{parent: test.parent} require.False(t, cmd.Bool(test.ctxBoolInput)) }) } } func TestCheckRequiredFlags(t *testing.T) { tdata := []struct { testCase string parseInput []string envVarInput [2]string flags []Flag expectedAnError bool expectedErrorContents []string }{ { testCase: "empty", }, { testCase: "optional", flags: []Flag{ &StringFlag{Name: "optionalFlag"}, }, }, { testCase: "required", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, }, expectedAnError: true, expectedErrorContents: []string{"requiredFlag"}, }, { testCase: "required_and_present", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, }, parseInput: []string{"--requiredFlag", "myinput"}, }, { testCase: "required_and_present_via_env_var", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true, Sources: EnvVars("REQUIRED_FLAG")}, }, envVarInput: [2]string{"REQUIRED_FLAG", "true"}, }, { testCase: "required_and_optional", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optionalFlag"}, }, expectedAnError: true, }, { testCase: "required_and_optional_and_optional_present", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optionalFlag"}, }, parseInput: []string{"--optionalFlag", "myinput"}, expectedAnError: true, }, { testCase: "required_and_optional_and_optional_present_via_env_var", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optionalFlag", Sources: EnvVars("OPTIONAL_FLAG")}, }, envVarInput: [2]string{"OPTIONAL_FLAG", "true"}, expectedAnError: true, }, { testCase: "required_and_optional_and_required_present", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optionalFlag"}, }, parseInput: []string{"--requiredFlag", "myinput"}, }, { testCase: "two_required", flags: []Flag{ &StringFlag{Name: "requiredFlagOne", Required: true}, &StringFlag{Name: "requiredFlagTwo", Required: true}, }, expectedAnError: true, expectedErrorContents: []string{"requiredFlagOne", "requiredFlagTwo"}, }, { testCase: "two_required_and_one_present", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "requiredFlagTwo", Required: true}, }, parseInput: []string{"--requiredFlag", "myinput"}, expectedAnError: true, }, { testCase: "two_required_and_both_present", flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "requiredFlagTwo", Required: true}, }, parseInput: []string{"--requiredFlag", "myinput", "--requiredFlagTwo", "myinput"}, }, { testCase: "required_flag_with_short_name", flags: []Flag{ &StringSliceFlag{Name: "names", Aliases: []string{"N"}, Required: true}, }, parseInput: []string{"-N", "asd", "-N", "qwe"}, }, { testCase: "required_flag_with_multiple_short_names", flags: []Flag{ &StringSliceFlag{Name: "names", Aliases: []string{"N", "n"}, Required: true}, }, parseInput: []string{"-n", "asd", "-n", "qwe"}, }, { testCase: "required_flag_with_short_alias_not_printed_on_error", expectedAnError: true, expectedErrorContents: []string{"Required flag \"names\" not set"}, flags: []Flag{ &StringSliceFlag{Name: "names", Aliases: []string{"n"}, Required: true}, }, }, { testCase: "required_flag_with_one_character", expectedAnError: true, expectedErrorContents: []string{"Required flag \"n\" not set"}, flags: []Flag{ &StringFlag{Name: "n", Required: true}, }, }, } for _, test := range tdata { t.Run(test.testCase, func(t *testing.T) { // setup if test.envVarInput[0] != "" { t.Setenv(test.envVarInput[0], test.envVarInput[1]) } cmd := &Command{ Name: "foo", Flags: test.flags, } args := []string{"foo"} args = append(args, test.parseInput...) _ = cmd.Run(context.Background(), args) err := cmd.checkAllRequiredFlags() // assertions if test.expectedAnError { assert.NotNil(t, err) } else { assert.Nil(t, err) } for _, errString := range test.expectedErrorContents { if err != nil { assert.ErrorContains(t, err, errString) } } }) } } func TestCheckRequiredFlagsWithOnUsageError(t *testing.T) { expectedError := errors.New("OnUsageError") cmd := &Command{ Name: "foo", Flags: []Flag{ &StringFlag{Name: "requiredFlag", Required: true}, }, OnUsageError: func(_ context.Context, _ *Command, _ error, _ bool) error { return expectedError }, } actualError := cmd.Run(buildTestContext(t), []string{"requiredFlag"}) require.ErrorIs(t, actualError, expectedError) } func TestCommand_ParentCommand_Set(t *testing.T) { cmd := &Command{ parent: &Command{ Flags: []Flag{ &StringFlag{ Name: "Name", }, }, }, } err := cmd.Set("Name", "aaa") assert.NoError(t, err) } func TestCommandStringDashOption(t *testing.T) { tests := []struct { name string shortOptionHandling bool args []string }{ { name: "double dash separate value", args: []string{"foo", "--bar", "-", "test"}, }, { name: "single dash separate value", args: []string{"foo", "-bar", "-", "test"}, }, /*{ name: "single dash combined value", args: []string{"foo", "-b-", "test"}, shortOptionHandling: true, },*/ } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd := &Command{ Name: "foo", UseShortOptionHandling: test.shortOptionHandling, Flags: []Flag{ &StringFlag{ Name: "bar", Aliases: []string{"b"}, }, }, Action: func(ctx context.Context, c *Command) error { return nil }, } err := cmd.Run(buildTestContext(t), test.args) assert.NoError(t, err) assert.Equal(t, "-", cmd.String("b")) }) } } func TestCommandReadArgsFromStdIn(t *testing.T) { tests := []struct { name string input string args []string expectedInt int64 expectedFloat float64 expectedSlice []string expectError bool }{ { name: "empty", input: "", args: []string{"foo"}, expectedInt: 0, expectedFloat: 0.0, expectedSlice: []string{}, }, { name: "empty2", input: ` `, args: []string{"foo"}, expectedInt: 0, expectedFloat: 0.0, expectedSlice: []string{}, }, { name: "intflag-from-input", input: "--if=100", args: []string{"foo"}, expectedInt: 100, expectedFloat: 0.0, expectedSlice: []string{}, }, { name: "intflag-from-input2", input: ` --if 100`, args: []string{"foo"}, expectedInt: 100, expectedFloat: 0.0, expectedSlice: []string{}, }, { name: "multiflag-from-input", input: ` --if 100 --ff 100.1 --ssf hello --ssf "hello 123 44" `, args: []string{"foo"}, expectedInt: 100, expectedFloat: 100.1, expectedSlice: []string{"hello", "hello\n 123\n44"}, }, { name: "end-args", input: ` --if 100 -- --ff 100.1 --ssf hello --ssf hell02 `, args: []string{"foo"}, expectedInt: 100, expectedFloat: 0, expectedSlice: []string{}, }, { name: "invalid string", input: ` " `, args: []string{"foo"}, expectedInt: 0, expectedFloat: 0, expectedSlice: []string{}, }, { name: "invalid string2", input: ` --if " `, args: []string{"foo"}, expectError: true, }, { name: "incomplete string", input: ` --ssf " hello `, args: []string{"foo"}, expectedSlice: []string{"hello"}, }, } for _, tst := range tests { t.Run(tst.name, func(t *testing.T) { r := require.New(t) fp, err := os.CreateTemp("", "readargs") r.NoError(err) _, err = fp.Write([]byte(tst.input)) r.NoError(err) fp.Close() cmd := buildMinimalTestCommand() cmd.ReadArgsFromStdin = true cmd.Reader, err = os.Open(fp.Name()) r.NoError(err) cmd.Flags = []Flag{ &Int64Flag{ Name: "if", }, &FloatFlag{ Name: "ff", }, &StringSliceFlag{ Name: "ssf", Config: StringConfig{ TrimSpace: true, }, }, } actionCalled := false cmd.Action = func(ctx context.Context, c *Command) error { r.Equal(tst.expectedInt, c.Int64("if")) r.Equal(tst.expectedFloat, c.Float("ff")) r.Equal(tst.expectedSlice, c.StringSlice("ssf")) actionCalled = true return nil } err = cmd.Run(context.Background(), tst.args) if !tst.expectError { r.NoError(err) r.True(actionCalled) } else { r.Error(err) } }) } } func TestZeroValueCommand(t *testing.T) { var cmd Command assert.NoError(t, cmd.Run(context.Background(), []string{"foo"})) } func TestCommandInvalidName(t *testing.T) { var cmd Command assert.Equal(t, int64(0), cmd.Int64("foo")) assert.Equal(t, uint64(0), cmd.Uint64("foo")) assert.Equal(t, float64(0), cmd.Float("foo")) assert.Equal(t, "", cmd.String("foo")) assert.Equal(t, time.Time{}, cmd.Timestamp("foo")) assert.Equal(t, time.Duration(0), cmd.Duration("foo")) assert.Equal(t, []int64(nil), cmd.Int64Slice("foo")) assert.Equal(t, []uint64(nil), cmd.Uint64Slice("foo")) assert.Equal(t, []float64(nil), cmd.FloatSlice("foo")) assert.Equal(t, []string(nil), cmd.StringSlice("foo")) } func TestCommandCategories(t *testing.T) { var cc commandCategories = []*commandCategory{ { name: "foo", commands: []*Command{}, }, { name: "bar", commands: []*Command{}, }, { name: "goo", commands: nil, }, } sort.Sort(&cc) var prev *commandCategory for _, c := range cc { if prev != nil { assert.LessOrEqual(t, prev.name, c.name) } prev = c assert.Equal(t, []*Command(nil), c.VisibleCommands()) } } func TestCommandSliceFlagSeparator(t *testing.T) { cmd := &Command{ SliceFlagSeparator: ";", Flags: []Flag{ &StringSliceFlag{ Name: "foo", }, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"app", "--foo", "ff;dd;gg", "--foo", "t,u"})) r.Equal([]string{"ff", "dd", "gg", "t,u"}, cmd.Value("foo")) } func TestCommandMapKeyValueFlagSeparator(t *testing.T) { cmd := &Command{ MapFlagKeyValueSeparator: ":", Flags: []Flag{ &StringMapFlag{ Name: "f_string_map", }, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"app", "--f_string_map", "s1:s2,s3:", "--f_string_map", "s4:s5"})) exp := map[string]string{ "s1": "s2", "s3": "", "s4": "s5", } r.Equal(exp, cmd.Value("f_string_map")) } // TestStringFlagTerminator tests the string flag "--flag" with "--" terminator. func TestStringFlagTerminator(t *testing.T) { tests := []struct { name string input []string expectFlag string expectArgs []string expectErr bool errorContain string }{ { name: "flag and args after terminator", input: []string{"test", "--flag", "x", "--", "test", "a1", "a2", "a3"}, expectFlag: "x", expectArgs: []string{"test", "a1", "a2", "a3"}, }, /* { name: "missing flag value due to terminator", input: []string{"test", "--flag", "--", "x"}, expectErr: true, errorContain: "flag needs an argument", },*/ { name: "terminator with no trailing args", input: []string{"test", "--flag", "x", "--"}, expectFlag: "x", expectArgs: []string{}, }, { name: "no terminator, only flag", input: []string{"test", "--flag", "x"}, expectFlag: "x", expectArgs: []string{}, }, { name: "flag defined after --", input: []string{"test", "--", "x", "--flag=value"}, expectFlag: "", expectArgs: []string{"x", "--flag=value"}, }, { name: "flag and without --", input: []string{"test", "--flag", "value", "x"}, expectFlag: "value", expectArgs: []string{"x"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var flagVal string var argsVal []string // build minimal command with a StringFlag "flag" cmd := &Command{ Name: "test", Flags: []Flag{ &StringFlag{ Name: "flag", Usage: "a string flag", Destination: &flagVal, }, }, Action: func(ctx context.Context, c *Command) error { argsVal = c.Args().Slice() return nil }, } err := cmd.Run(context.Background(), tc.input) if tc.expectErr { assert.Error(t, err) if err != nil { assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.errorContain)) } } else { assert.NoError(t, err) assert.Equal(t, tc.expectFlag, flagVal) assert.Equal(t, tc.expectArgs, argsVal) } }) } } // TestBoolFlagTerminator tests the bool flag func TestBoolFlagTerminator(t *testing.T) { tests := []struct { name string input []string expectFlag bool expectArgs []string expectErr bool errorContain string }{ /*{ name: "bool flag with invalid non-bool value", input: []string{"test", "--flag", "x", "--", "test", "a1", "a2", "a3"}, expectErr: true, errorContain: "invalid syntax", },*/ { name: "bool flag omitted value defaults to true", input: []string{"test", "--flag", "--", "x"}, expectFlag: true, expectArgs: []string{"x"}, }, { name: "bool flag explicitly set to false", input: []string{"test", "--flag=false", "--", "x"}, expectFlag: false, expectArgs: []string{"x"}, }, { name: "bool flag defined after --", input: []string{"test", "--", "x", "--flag=true"}, expectFlag: false, expectArgs: []string{"x", "--flag=true"}, }, { name: "bool flag and without --", input: []string{"test", "--flag=true", "x"}, expectFlag: true, expectArgs: []string{"x"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var flagVal bool var argsVal []string // build minimal command with a BoolFlag "flag" cmd := &Command{ Name: "test", Flags: []Flag{ &BoolFlag{ Name: "flag", Usage: "a bool flag", Destination: &flagVal, }, }, Action: func(ctx context.Context, c *Command) error { argsVal = c.Args().Slice() return nil }, } err := cmd.Run(context.Background(), tc.input) if tc.expectErr { assert.Error(t, err) if err != nil { assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.errorContain)) } } else { assert.NoError(t, err) assert.Equal(t, tc.expectFlag, flagVal) assert.Equal(t, tc.expectArgs, argsVal) } }) } } // TestSliceStringFlagParsing tests the StringSliceFlag func TestSliceStringFlagParsing(t *testing.T) { var sliceVal []string cmdNoDelimiter := &Command{ Name: "test", Flags: []Flag{ &StringSliceFlag{ Name: "flag", Usage: "a string slice flag without delimiter", }, }, Action: func(ctx context.Context, c *Command) error { sliceVal = c.StringSlice("flag") return nil }, } /*cmdWithDelimiter := &Command{ Name: "test", Flags: []Flag{ &StringSliceFlag{ Name: "flag", Usage: "a string slice flag with delimiter", Delimiter: ':', }, }, Action: func(ctx context.Context, c *Command) error { sliceVal = c.StringSlice("flag") return nil }, }*/ tests := []struct { name string cmd *Command input []string expectSlice []string expectErr bool errorContain string }{ { name: "single value without delimiter (no split)", cmd: cmdNoDelimiter, input: []string{"test", "--flag", "x"}, expectSlice: []string{"x"}, }, { name: "multiple values with comma (default split)", cmd: cmdNoDelimiter, input: []string{"test", "--flag", "x,y"}, expectSlice: []string{"x", "y"}, }, /*{ name: "Case 10: with delimiter specified ':'", cmd: cmdWithDelimiter, input: []string{"test", "--flag", "x:y"}, expectSlice: []string{"x", "y"}, },*/ { name: "without delimiter specified, value remains unsplit", cmd: cmdNoDelimiter, input: []string{"test", "--flag", "x:y"}, expectSlice: []string{"x:y"}, }, } for _, tc := range tests { // Reset sliceVal sliceVal = nil t.Run(tc.name, func(t *testing.T) { err := tc.cmd.Run(context.Background(), tc.input) if tc.expectErr { assert.Error(t, err) if err != nil { assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.errorContain)) } } else { assert.NoError(t, err) assert.Equal(t, tc.expectSlice, sliceVal) } }) } } func TestJSONExportCommand(t *testing.T) { cmd := buildExtendedTestCommand() cmd.Arguments = []Argument{ &IntArgs{ Name: "fooi", }, } out, err := json.Marshal(cmd) require.NoError(t, err) expected := `{ "name": "greet", "aliases": null, "usage": "Some app", "usageText": "app [first_arg] [second_arg]", "argsUsage": "", "version": "", "description": "Description of the application.", "defaultCommand": "", "category": "", "commands": [ { "name": "config", "aliases": [ "c" ], "usage": "another usage test", "usageText": "", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": [ { "name": "sub-config", "aliases": [ "s", "ss" ], "usage": "another usage test", "usageText": "", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": null, "flags": [ { "name": "sub-flag", "category": "", "defaultText": "", "usage": "", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": "", "aliases": [ "sub-fl", "s" ], "takesFileArg": false, "config": { "TrimSpace": false }, "onlyOnce": false, "validateDefaults" : false }, { "name": "sub-command-flag", "category": "", "defaultText": "", "usage": "some usage text", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": false, "aliases": [ "s" ], "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults" : false } ], "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null } ], "flags": [ { "name": "flag", "category": "", "defaultText": "", "usage": "", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": "", "aliases": [ "fl", "f" ], "takesFileArg": true, "config": { "TrimSpace": false }, "onlyOnce": false, "validateDefaults" : false }, { "name": "another-flag", "category": "", "defaultText": "", "usage": "another usage text", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": false, "aliases": [ "b" ], "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults" : false } ], "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null }, { "name": "info", "aliases": [ "i", "in" ], "usage": "retrieve generic information", "usageText": "", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": null, "flags": null, "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null }, { "name": "some-command", "aliases": null, "usage": "", "usageText": "", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": null, "flags": null, "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null }, { "name": "hidden-command", "aliases": null, "usage": "", "usageText": "", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": null, "flags": [ { "name": "completable", "category": "", "defaultText": "", "usage": "", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": false, "aliases": null, "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults": false } ], "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": true, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null }, { "name": "usage", "aliases": [ "u" ], "usage": "standard usage text", "usageText": "\nUsage for the usage text\n- formatted: Based on the specified ConfigMap and summon secrets.yml\n- list: Inspect the environment for a specific process running on a Pod\n- for_effect: Compare 'namespace' environment with 'local'\n\n` + "```\\nfunc() { ... }\\n```" + `\n\nShould be a part of the same code block\n", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": [ { "name": "sub-usage", "aliases": [ "su" ], "usage": "standard usage text", "usageText": "Single line of UsageText", "argsUsage": "", "version": "", "description": "", "defaultCommand": "", "category": "", "commands": null, "flags": [ { "name": "sub-command-flag", "category": "", "defaultText": "", "usage": "some usage text", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": false, "aliases": [ "s" ], "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults" : false } ], "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null } ], "flags": [ { "name": "flag", "category": "", "defaultText": "", "usage": "", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": "", "aliases": [ "fl", "f" ], "takesFileArg": true, "config": { "TrimSpace": false }, "onlyOnce": false, "validateDefaults" : false }, { "name": "another-flag", "category": "", "defaultText": "", "usage": "another usage text", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": false, "aliases": [ "b" ], "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults" : false } ], "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": null, "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": null, "readArgsFromStdin": false, "stopOnNthArg": null } ], "flags": [ { "name": "socket", "category": "", "defaultText": "", "usage": "some 'usage' text", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": "value", "aliases": [ "s" ], "takesFileArg": true, "config": { "TrimSpace": false }, "onlyOnce": false, "validateDefaults" : false }, { "name": "flag", "category": "", "defaultText": "", "usage": "", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": "", "aliases": [ "fl", "f" ], "takesFileArg": false, "config": { "TrimSpace": false }, "onlyOnce": false, "validateDefaults" : false }, { "name": "another-flag", "category": "", "defaultText": "", "usage": "another usage text", "required": false, "hidden": false, "hideDefault": false, "local": false, "defaultValue": false, "aliases": [ "b" ], "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults" : false }, { "name": "hidden-flag", "category": "", "defaultText": "", "usage": "", "required": false, "hidden": true, "hideDefault": false, "local": false, "defaultValue": false, "aliases": null, "takesFileArg": false, "config": { "Count": null }, "onlyOnce": false, "validateDefaults" : false } ], "hideHelp": false, "hideHelpCommand": false, "hideVersion": false, "hidden": false, "authors": [ "Harrison ", { "Name": "Oliver Allen", "Address": "oliver@toyshop.com" } ], "copyright": "", "metadata": null, "sliceFlagSeparator": "", "disableSliceFlagSeparator": false, "mapFlagKeyValueSeparator": "", "useShortOptionHandling": false, "suggest": false, "allowExtFlags": false, "skipFlagParsing": false, "prefixMatchCommands": false, "mutuallyExclusiveFlags": null, "arguments": [ { "name": "fooi", "value": 0, "usageText": "", "minTimes": 0, "maxTimes": 0, "config": { "Base": 0 } } ], "readArgsFromStdin": false, "stopOnNthArg": null } ` assert.JSONEq(t, expected, string(out)) } func TestCommand_ExclusiveFlags(t *testing.T) { cmd := &Command{ Name: "bar", MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { Flags: [][]Flag{ { &StringFlag{ Name: "foo1", }, }, { &StringFlag{ Name: "foo2", }, }, }, }, }, } err := cmd.Run(buildTestContext(t), []string{"bar", "--foo1", "var1", "--foo2", "var2"}) require.Equal(t, "option foo1 cannot be set along with option foo2", err.Error()) } func TestCommand_ExclusiveFlagsWithOnUsageError(t *testing.T) { expectedErr := errors.New("my custom error") cmd := &Command{ Name: "bar", MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { Flags: [][]Flag{ { &StringFlag{ Name: "foo1", }, }, { &StringFlag{ Name: "foo2", }, }, }, }, }, OnUsageError: func(_ context.Context, _ *Command, _ error, _ bool) error { return expectedErr }, } actualErr := cmd.Run(buildTestContext(t), []string{"bar", "--foo1", "v1", "--foo2", "v2"}) require.ErrorIs(t, actualErr, expectedErr) } func TestCommand_ExclusiveFlagsWithAfter(t *testing.T) { var called bool cmd := &Command{ Name: "bar", MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { Category: "cat1", Flags: [][]Flag{ { &StringFlag{ Name: "foo", }, }, { &StringFlag{ Name: "foo2", }, }, }, }, }, After: func(ctx context.Context, cmd *Command) error { called = true return nil }, } require.Error(t, cmd.Run(buildTestContext(t), []string{ "bar", "--foo", "v1", "--foo2", "v2", })) require.True(t, called) } func TestCommand_ParallelRun(t *testing.T) { t.Parallel() for i := 0; i < 10; i++ { t.Run(fmt.Sprintf("run_%d", i), func(t *testing.T) { t.Parallel() defer func() { if r := recover(); r != nil { t.Errorf("unexpected panic - '%s'", r) } }() cmd := &Command{ Name: "debug", Usage: "make an explosive entrance", Action: func(_ context.Context, cmd *Command) error { return nil }, } if err := cmd.Run(context.Background(), nil); err != nil { fmt.Printf("%s\n", err) } }) } } func TestCommand_ExclusiveFlagsPersistent(t *testing.T) { exclusiveGroup := func(flags ...string) []MutuallyExclusiveFlags { grp := MutuallyExclusiveFlags{} for _, name := range flags { grp.Flags = append(grp.Flags, []Flag{&StringFlag{Name: name}}) } return []MutuallyExclusiveFlags{grp} } noop := func(_ context.Context, _ *Command) error { return nil } newBaseCmd := func() *Command { return &Command{ Name: "root", MutuallyExclusiveFlags: exclusiveGroup("alpha", "beta"), Commands: []*Command{{Name: "sub", Action: noop}}, } } tests := []struct { name string setup func() *Command args []string wantErr string }{ { name: "single flag propagated to subcommand", setup: newBaseCmd, args: []string{"root", "sub", "--alpha", "hello"}, }, { name: "both exclusive flags on subcommand errors", setup: newBaseCmd, args: []string{"root", "sub", "--alpha", "hello", "--beta", "world"}, wantErr: "cannot be set along with", }, { name: "neither flag set without required is ok", setup: newBaseCmd, args: []string{"root", "sub"}, }, { name: "exclusive flags checked on grandchild", setup: func() *Command { cmd := newBaseCmd() sub := cmd.Commands[0] sub.Name = "mid" sub.Action = nil sub.Commands = []*Command{{Name: "leaf", Action: noop}} return cmd }, args: []string{"root", "mid", "leaf", "--alpha", "hello", "--beta", "world"}, wantErr: "cannot be set along with", }, { name: "subcommand own group checked alongside parent group", setup: func() *Command { cmd := newBaseCmd() cmd.Commands[0].MutuallyExclusiveFlags = exclusiveGroup("gamma", "delta") return cmd }, args: []string{"root", "sub", "--gamma", "hello", "--delta", "world"}, wantErr: "cannot be set along with", }, { name: "parent group violation detected when subcommand has own group", setup: func() *Command { cmd := newBaseCmd() cmd.Commands[0].MutuallyExclusiveFlags = exclusiveGroup("gamma", "delta") return cmd }, args: []string{"root", "sub", "--alpha", "hello", "--beta", "world"}, wantErr: "cannot be set along with", }, { name: "parent and subcommand groups both pass independently", setup: func() *Command { cmd := newBaseCmd() cmd.Commands[0].MutuallyExclusiveFlags = exclusiveGroup("gamma", "delta") return cmd }, args: []string{"root", "sub", "--alpha", "hello", "--gamma", "world"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.setup().Run(buildTestContext(t), tt.args) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) } else { require.NoError(t, err) } }) } } ================================================ FILE: completion.go ================================================ package cli import ( "context" "embed" "fmt" "sort" "strings" ) const ( completionCommandName = "completion" // This flag is supposed to only be used by the completion script itself to generate completions on the fly. completionFlag = "--generate-shell-completion" ) type renderCompletion func(cmd *Command, appName string) (string, error) var ( //go:embed autocomplete autoCompleteFS embed.FS shellCompletions = map[string]renderCompletion{ "bash": func(c *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile("autocomplete/bash_autocomplete") return fmt.Sprintf(string(b), appName), err }, "zsh": func(c *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile("autocomplete/zsh_autocomplete") return fmt.Sprintf(string(b), appName), err }, "fish": func(c *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile("autocomplete/fish_autocomplete") return fmt.Sprintf(string(b), appName), err }, "pwsh": func(c *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile("autocomplete/powershell_autocomplete.ps1") return string(b), err }, } ) const completionDescription = `Output shell completion script for bash, zsh, fish, or Powershell. Source the output to enable completion. # .bashrc source <($COMMAND completion bash) # .zshrc source <($COMMAND completion zsh) # fish $COMMAND completion fish > ~/.config/fish/completions/$COMMAND.fish # Powershell Output the script to path/to/autocomplete/$COMMAND.ps1 an run it. ` func buildCompletionCommand(appName string) *Command { return &Command{ Name: completionCommandName, Hidden: true, Usage: "Output shell completion script for bash, zsh, fish, or Powershell", Description: strings.ReplaceAll(completionDescription, "$COMMAND", appName), Action: func(ctx context.Context, cmd *Command) error { return printShellCompletion(ctx, cmd, appName) }, isCompletionCommand: true, } } func printShellCompletion(_ context.Context, cmd *Command, appName string) error { var shells []string for k := range shellCompletions { shells = append(shells, k) } sort.Strings(shells) if cmd.Args().Len() == 0 { return Exit(fmt.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1) } s := cmd.Args().First() renderCompletion, ok := shellCompletions[s] if !ok { return Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1) } completionScript, err := renderCompletion(cmd, appName) if err != nil { return Exit(err, 1) } _, err = cmd.Writer.Write([]byte(completionScript)) if err != nil { return Exit(err, 1) } return nil } ================================================ FILE: completion_test.go ================================================ package cli import ( "bytes" "context" "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCompletionDisable(t *testing.T) { cmd := &Command{} err := cmd.Run(buildTestContext(t), []string{"foo", completionCommandName}) assert.Error(t, err, "Expected error for no help topic for completion") } func TestCompletionEnable(t *testing.T) { cmd := &Command{ EnableShellCompletion: true, Flags: []Flag{ &StringFlag{ Name: "goo", Required: true, }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", completionCommandName}) assert.ErrorContains(t, err, "no shell provided") } func TestCompletionEnableDiffCommandName(t *testing.T) { cmd := &Command{ EnableShellCompletion: true, ShellCompletionCommandName: "junky", } err := cmd.Run(buildTestContext(t), []string{"foo", "junky"}) assert.ErrorContains(t, err, "no shell provided") } func TestCompletionShell(t *testing.T) { for k := range shellCompletions { out := &bytes.Buffer{} t.Run(k, func(t *testing.T) { cmd := &Command{ EnableShellCompletion: true, Writer: out, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"foo", completionCommandName, k})) r.Containsf( k, out.String(), "Expected output to contain shell name %[1]q", k, ) }) } } func TestCompletionSubcommand(t *testing.T) { tests := []struct { name string args []string contains string msg string msgArgs []interface{} notContains bool }{ { name: "subcommand general completion", args: []string{"foo", "bar", completionFlag}, contains: "xyz", msg: "Expected output to contain shell name %[1]q", msgArgs: []interface{}{ "xyz", }, }, { name: "subcommand flag completion", args: []string{"foo", "bar", "-", completionFlag}, contains: "l1", msg: "Expected output to contain shell name %[1]q", msgArgs: []interface{}{ "l1", }, }, { name: "subcommand flag no completion", args: []string{"foo", "bar", "--", completionFlag}, contains: "l1", msg: "Expected output to contain shell name %[1]q", msgArgs: []interface{}{ "l1", }, notContains: true, }, { name: "sub sub command general completion", args: []string{"foo", "bar", "xyz", completionFlag}, contains: "-g", msg: "Expected output to contain flag %[1]q", msgArgs: []interface{}{ "-g", }, notContains: true, }, { name: "sub sub command flag completion", args: []string{"foo", "bar", "xyz", "-", completionFlag}, contains: "-g", msg: "Expected output to contain flag %[1]q", msgArgs: []interface{}{ "-g", }, }, { name: "sub sub command no completion", args: []string{"foo", "bar", "xyz", "--", completionFlag}, contains: "-g", msg: "Expected output to contain flag %[1]q", msgArgs: []interface{}{ "-g", }, notContains: true, }, { name: "sub sub command no completion extra args", args: []string{"foo", "bar", "xyz", "--", "sargs", completionFlag}, contains: "-g", msg: "Expected output to contain flag %[1]q", msgArgs: []interface{}{ "-g", }, notContains: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { out := &bytes.Buffer{} cmd := &Command{ EnableShellCompletion: true, Writer: out, Commands: []*Command{ { Name: "bar", Flags: []Flag{ &StringFlag{ Name: "l1", }, }, Action: func(ctx context.Context, c *Command) error { return nil }, Commands: []*Command{ { Name: "xyz", Flags: []Flag{ &StringFlag{ Name: "g", Aliases: []string{ "t", }, }, }, Action: func(ctx context.Context, c *Command) error { return nil }, }, }, }, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), test.args)) if test.notContains { r.NotContainsf(out.String(), test.contains, test.msg, test.msgArgs...) } else { r.Containsf(out.String(), test.contains, test.msg, test.msgArgs...) } }) } } type mockWriter struct { err error } func (mw *mockWriter) Write(p []byte) (int, error) { if mw.err != nil { return 0, mw.err } return len(p), nil } func TestCompletionInvalidShell(t *testing.T) { cmd := &Command{ EnableShellCompletion: true, } unknownShellName := "junky-sheell" err := cmd.Run(buildTestContext(t), []string{"foo", completionCommandName, unknownShellName}) assert.ErrorContains(t, err, "unknown shell junky-sheell") enableError := true shellCompletions[unknownShellName] = func(c *Command, appName string) (string, error) { if enableError { return "", fmt.Errorf("cant do completion") } return "something", nil } defer func() { delete(shellCompletions, unknownShellName) }() err = cmd.Run(buildTestContext(t), []string{"foo", completionCommandName, unknownShellName}) assert.ErrorContains(t, err, "cant do completion") // now disable shell completion error enableError = false c := cmd.Command(completionCommandName) assert.NotNil(t, c) c.Writer = &mockWriter{ err: fmt.Errorf("writer error"), } err = cmd.Run(buildTestContext(t), []string{"foo", completionCommandName, unknownShellName}) assert.ErrorContains(t, err, "writer error") } ================================================ FILE: docs/CHANGELOG.md ================================================ > :warning: This document is no longer being actively maintained. Please see the > [releases page](https://github.com/urfave/cli/releases) for all release notes > and related hypermedia for releases `>= 1.22.5`, `>= 2.3.0`. --- # Change Log **ATTN**: This project uses [semantic versioning](https://semver.org/). ## Unreleased - 2.X series View [unreleased 2.X] series changes. ## [2.2.0] - 2020-03-08 These release notes were written for the git hash [d648edd48d89ef3a841b1ec75c2ebbd4de5f748f](https://github.com/urfave/cli/tree/d648edd48d89ef3a841b1ec75c2ebbd4de5f748f) ### Fixed * Fixed zsh completion scripts in [urfave/cli/pull/1062](https://github.com/urfave/cli/pull/1062) via [@zhsj](https://github.com/zhsj) * Fixed description of subcommand to be more consistent in [urfave/cli/pull/1054](https://github.com/urfave/cli/pull/1054) via [@itchyny](https://github.com/itchyny) * Fixed possible runtime panic in slice parsing in [urfave/cli/pull/1049](https://github.com/urfave/cli/pull/1049) via [@saschagrunert](https://github.com/saschagrunert) * Fixed invalid man page header generation in [urfave/cli/pull/1041](https://github.com/urfave/cli/pull/1041) via [@saschagrunert](https://github.com/saschagrunert) ### Changed * Improved auto-completion instructions and added example gifs in [urfave/cli/pull/1059](https://github.com/urfave/cli/pull/1059) via [@masonj188](https://github.com/masonj188) * Removed the author from generated man pages in [urfave/cli/pull/1041](https://github.com/urfave/cli/pull/1041) via [@saschagrunert](https://github.com/saschagrunert) ### Added * Added destination field to `StringSliceFlag` in [urfave/cli/pull/1078](https://github.com/urfave/cli/pull/1078) via [@davidsbond](https://github.com/davidsbond) * Added `HideHelpCommand`. While `HideHelp` hides both `help` command and `--help` flag, `HideHelpCommand` only hides `help` command and leave `--help` flag as-is in [urfave/cli/pull/1083](https://github.com/urfave/cli/pull/1083) via [@AkihiroSuda](https://github.com/AkihiroSuda) * Added timestampFlag docs in [urfave/cli/pull/997](https://github.com/urfave/cli/pull/997) via [@drov0](https://github.com/drov0) * Added required flags documentation in [urfave/cli/pull/1008](https://github.com/urfave/cli/pull/1008) via [@lynncyrin](https://github.com/lynncyrin), [@anberns](https://github.com/anberns) ## [2.1.1] - 2019-12-24 ### Fixed * Fixed a `Context` regression introduced in `v2.1.0` in [urfave/cli/pull/1014](https://github.com/urfave/cli/pull/1014) via [@lynncyrin](https://github.com/lynncyrin) ## [2.1.0] - 2019-12-24 These release notes were written for the git hash [ae84df4cef4a2a6f1a0cb1d41ea0f3af8755e5a8](https://github.com/urfave/cli/tree/ae84df4cef4a2a6f1a0cb1d41ea0f3af8755e5a8) ### Fixed * Fixed some golint errors in [urfave/cli/pull/988](https://github.com/urfave/cli/pull/988) via [@liamchampton](https://github.com/liamchampton) * Fixed a panic with flag completion [urfave/cli/pull/946](https://github.com/urfave/cli/pull/946) via [@unRob](https://github.com/unRob) ### Changed * Changed docs generation to use visible flags in [urfave/cli/pull/999](https://github.com/urfave/cli/pull/999) via [@subpop](https://github.com/subpop) * Changed `App.Run` to use an optional context for timeouts and cancellation in [urfave/cli/pull/975](https://github.com/urfave/cli/pull/975) via [@marwan-at-work](https://github.com/marwan-at-work) * Changed version info to be hidden if the user has not defined a version in [urfave/cli/pull/955](https://github.com/urfave/cli/pull/955) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) * Changed docs generation to take into account multiple authors in [urfave/cli/pull/900](https://github.com/urfave/cli/pull/900) via [@saschagrunert](https://github.com/saschagrunert) * Changed context to expose a `Value` accessor in [urfave/cli/pull/741](https://github.com/urfave/cli/pull/741) via [@corruptmemory](https://github.com/corruptmemory) ### Added * Added timestamp flag in [urfave/cli/pull/987](https://github.com/urfave/cli/pull/987) via [@drov0](https://github.com/drov0) ## [2.0.0] - 2019-11-17 The V2 changes were all shipped in [urfave/cli/pull/892](https://github.com/urfave/cli/pull/892), which was created with the effort of over a dozen participants! They are: [@asahasrabuddhe](https://github.com/asahasrabuddhe), [@meatballhat](https://github.com/meatballhat), [@jszwedko](https://github.com/jszwedko), [@lynncyrin](https://github.com/lynncyrin), [@AudriusButkevicius](https://github.com/AudriusButkevicius), [@saschagrunert](https://github.com/saschagrunert), [@rliebz](https://github.com/rliebz), [@johnweldon](https://github.com/johnweldon), [@nlewo](https://github.com/nlewo), [@grubernaut](https://github.com/grubernaut), [@OneOfOne](https://github.com/OneOfOne), [@VMitov](https://github.com/VMitov), [@cbranch](https://github.com/cbranch), [@marwan-at-work](https://github.com/marwan-at-work), [@uudashr](https://github.com/uudashr), [@bfreis](https://github.com/bfreis) ### Added - Added `NewStringSlice` and `NewIntSlice` for creating their related types - Added `Float64SliceFlag` for unmarshaling a list of floats from the user - Added `Context.Lineage` to get all contexts from current up to global - Added `Context.LocalFlagNames` to get the flag names from *only* the current context - Added `BoolFlag.Value` to handle both default-false and default-true - Added `IsSet` method to the `Flag` interface which allows us to detect whether or not a flag has been set ### Changed - `Context.FlagNames` now returns all flags in the context lineage - `Context.IsSet` now considers the full context lineage ### Removed - Removed the ability to specify `&StringSlice{...string}` or `&IntSlice{...int}`. - Removed adapter code for deprecated `Action` func signature - Deprecated `App.Author`, `App.Email`, and `Command.ShortName` fields - Removed all `Context.Global*` methods, as the non-global versions now traverse up the context lineage automatically. - Removed `Context.Parent` method, as this is now available via `Context.Lineage` - Removed `BoolTFlag` and related code, as this is now available via `BoolFlag.Value` ## Unreleased - 1.22.X series View [unreleased 1.22.X] series changes. ## [1.22.4] - 2020-03-31 ### Fixed - Fixed a panic with flag completion in [urfave/cli/pull/1101](https://github.com/urfave/cli/pull/1101) via [@unRob](https://github.com/unRob), [@VirrageS](https://github.com/VirrageS) ## [1.22.3] - 2020-02-25 ### Fixed - String flag no longer persists the default value if the flag is explicitly initialized in [urfave/cli/pull/981](https://github.com/urfave/cli/pull/981) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) - `context.IsSet()` returns `true` or `false` correctly regardless of whether the short name or the full name of the flag is passed to it in [urfave/cli/pull/978](https://github.com/urfave/cli/pull/978) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) - Hide version if the version is not set by the user in [urfave/cli/pull/954](https://github.com/urfave/cli/pull/954) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) ## [1.22.2] - 2019-11-17 ### Fixed - Fix v1.21.0 pass through regression in [urfave/cli/pull/872](https://github.com/urfave/cli/pull/872) via [@lynncyrin](https://github.com/lynncyrin) - Fix infinite loop when parsing invalid flags for apps with short option handling in [urfave/cli/pull/911](https://github.com/urfave/cli/pull/911) via [@rliebz](https://github.com/rliebz) - Fix zsh autocomplete in [urfave/cli/pull/906](https://github.com/urfave/cli/pull/906) via [@gnowxilef](https://github.com/gnowxilef) - Fix typo in `DocGenerationFlag.TakesValue()` docstring in [urfave/cli/pull/902](https://github.com/urfave/cli/pull/902) via [@benmoose](https://github.com/benmoose) - Avoid panic for missing flag value in [urfave/cli/pull/893](https://github.com/urfave/cli/pull/893) via [@rliebz](https://github.com/rliebz) ### Changed - Simplify `HelpPrinter` and `CustomHelpPrinter` behaviors in [urfave/cli/pull/912](https://github.com/urfave/cli/pull/912) via [@rliebz](https://github.com/rliebz) ## [1.22.1] - 2019-09-11 ### Fixed * Hide output of hidden commands on man pages in [urfave/cli/pull/889](https://github.com/urfave/cli/pull/889) via [@crosbymichael](https://github.com/crosbymichael) * Don't generate fish completion for hidden commands [urfave/cli/pull/891](https://github.com/urfave/891) via [@saschagrunert](https://github.com/saschagrunert) * Using short flag names for required flags throws an error in [urfave/cli/pull/890](https://github.com/urfave/cli/pull/890) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) ### Changed * Remove flag code generation logic, legacy python test runner in [urfave/cli/pull/883](https://github.com/urfave/cli/pull/883) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) * Enable Go Modules support, drop support for `Go 1.10` add support for `Go 1.13` in [urfave/cli/pull/885](https://github.com/urfave/cli/pull/885) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) ## [1.22.0] - 2019-09-07 ### Fixed * Fix Subcommands not falling back to `app.ExitEventHandler` in [urfave/cli/pull/856](https://github.com/urfave/cli/pull/856) via [@FaranIdo](https://github.com/FaranIdo) ### Changed * Clarify that altsrc supports both TOML and JSON in [urfave/cli/pull/774](https://github.com/urfave/cli/pull/774) via [@whereswaldon](https://github.com/whereswaldon) * Made the exit code example more clear in [urfave/cli/pull/823](https://github.com/urfave/cli/pull/823) via [@xordspar0](https://github.com/xordspar0) * Removed the use of python for internal flag generation in [urfave/cli/pull/836](https://github.com/urfave/cli/pull/836) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) * Changed the supported go versions to `1.10`, `1.11`, `1.12` in [urfave/cli/pull/843](https://github.com/urfave/cli/pull/843) via [@lafriks](https://github.com/lafriks) * Changed the v1 releases section in the readme in [urfave/cli/pull/862](https://github.com/urfave/cli/pull/862) via [@russoj88](https://github.com/russoj88) * Cleaned up go modules in [urfave/cli/pull/874](https://github.com/urfave/cli/pull/874) via [@saschagrunert](https://github.com/saschagrunert) ### Added * Added `UseShortOptionHandling` for combining short flags in [urfave/cli/pull/735](https://github.com/urfave/cli/pull/735) via [@rliebz](https://github.com/rliebz) * Added support for flags bash completion in [urfave/cli/pull/808](https://github.com/urfave/cli/pull/808) via [@yogeshlonkar](https://github.com/yogeshlonkar) * Added the `TakesFile` indicator to flag in [urfave/cli/pull/851](https://github.com/urfave/cli/pull/851) via [@saschagrunert](https://github.com/saschagrunert) * Added fish shell completion support in [urfave/cli/pull/848](https://github.com/urfave/cli/pull/848) via [@saschagrunert](https://github.com/saschagrunert) ## [1.21.0] - 2019-08-02 ### Fixed * Fix using "slice" flag types with `EnvVar` in [urfave/cli/pull/687](https://github.com/urfave/cli/pull/687) via [@joshuarubin](https://github.com/joshuarubin) * Fix regression of `SkipFlagParsing` behavior in [urfave/cli/pull/697](https://github.com/urfave/cli/pull/697) via [@jszwedko](https://github.com/jszwedko) * Fix handling `ShortOptions` and `SkipArgReorder` in [urfave/cli/pull/686](https://github.com/urfave/cli/pull/686) via [@baude](https://github.com/baude) * Fix args reordering when bool flags are present in [urfave/cli/pull/712](https://github.com/urfave/cli/pull/712) via [@windler](https://github.com/windler) * Fix parsing of short options in [urfave/cli/pull/758](https://github.com/urfave/cli/pull/758) via [@vrothberg](https://github.com/vrothberg) * Fix unaligned indents for the command help messages in [urfave/cli/pull/806](https://github.com/urfave/cli/pull/806) via [@mingrammer](https://github.com/mingrammer) ### Changed * Cleaned up help output in [urfave/cli/pull/664](https://github.com/urfave/cli/pull/664) via [@maguro](https://github.com/maguro) * Remove redundant nil checks in [urfave/cli/pull/773](https://github.com/urfave/cli/pull/773) via [@teresy](https://github.com/teresy) * Case is now considered when sorting strings in [urfave/cli/pull/676](https://github.com/urfave/cli/pull/676) via [@rliebz](https://github.com/rliebz) ### Added * Added _"required flags"_ support in [urfave/cli/pull/819](https://github.com/urfave/cli/pull/819) via [@lynncyrin](https://github.com/lynncyrin/) * Backport JSON `InputSource` to v1 in [urfave/cli/pull/598](https://github.com/urfave/cli/pull/598) via [@jszwedko](https://github.com/jszwedko) * Allow more customization of flag help strings in [urfave/cli/pull/661](https://github.com/urfave/cli/pull/661) via [@rliebz](https://github.com/rliebz) * Allow custom `ExitError` handler function in [urfave/cli/pull/628](https://github.com/urfave/cli/pull/628) via [@phinnaeus](https://github.com/phinnaeus) * Allow loading a variable from a file in [urfave/cli/pull/675](https://github.com/urfave/cli/pull/675) via [@jmccann](https://github.com/jmccann) * Allow combining short bool names in [urfave/cli/pull/684](https://github.com/urfave/cli/pull/684) via [@baude](https://github.com/baude) * Added test coverage to context in [urfave/cli/pull/788](https://github.com/urfave/cli/pull/788) via [@benzvan](https://github.com/benzvan) * Added go module support in [urfave/cli/pull/831](https://github.com/urfave/cli/pull/831) via [@saschagrunert](https://github.com/saschagrunert) ## [1.20.0] - 2017-08-10 ### Fixed * `HandleExitCoder` is now correctly iterates over all errors in a `MultiError`. The exit code is the exit code of the last error or `1` if there are no `ExitCoder`s in the `MultiError`. * Fixed YAML file loading on Windows (previously would fail validate the file path) * Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly propagated * `ErrWriter` is now passed downwards through command structure to avoid the need to redefine it * Pass `Command` context into `OnUsageError` rather than parent context so that all fields are available * Errors occurring in `Before` funcs are no longer double printed * Use `UsageText` in the help templates for commands and subcommands if defined; otherwise build the usage as before (was previously ignoring this field) * `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if a program calls `Set` or `GlobalSet` directly after flag parsing (would previously only return `true` if the flag was set during parsing) ### Changed * No longer exit the program on command/subcommand error if the error raised is not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was determined to be a regression in functionality. See [the PR](https://github.com/urfave/cli/pull/595) for discussion. ### Added * `CommandsByName` type was added to make it easy to sort `Command`s by name, alphabetically * `altsrc` now handles loading of string and int arrays from TOML * Support for definition of custom help templates for `App` via `CustomAppHelpTemplate` * Support for arbitrary key/value fields on `App` to be used with `CustomAppHelpTemplate` via `ExtraInfo` * `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explicitly be `cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag` interface to be used. ## [1.19.1] - 2016-11-21 ### Fixed - Fixes regression introduced in 1.19.0 where using an `ActionFunc` as the `Action` for a command would cause it to error rather than calling the function. Should not have a affected declarative cases using `func(c *cli.Context) err)`. - Shell completion now handles the case where the user specifies `--generate-bash-completion` immediately after a flag that takes an argument. Previously it call the application with `--generate-bash-completion` as the flag value. ## [1.19.0] - 2016-11-19 ### Added - `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`) - A `Description` field was added to `App` for a more detailed description of the application (similar to the existing `Description` field on `Command`) - Flag type code generation via `go generate` - Write to stderr and exit 1 if action returns non-nil error - Added support for TOML to the `altsrc` loader - `SkipArgReorder` was added to allow users to skip the argument reordering. This is useful if you want to consider all "flags" after an argument as arguments rather than flags (the default behavior of the stdlib `flag` library). This is backported functionality from the [removal of the flag reordering](https://github.com/urfave/cli/pull/398) in the unreleased version 2 - For formatted errors (those implementing `ErrorFormatter`), the errors will be formatted during output. Compatible with `pkg/errors`. ### Changed - Raise minimum tested/supported Go version to 1.2+ ### Fixed - Consider empty environment variables as set (previously environment variables with the equivalent of `""` would be skipped rather than their value used). - Return an error if the value in a given environment variable cannot be parsed as the flag type. Previously these errors were silently swallowed. - Print full error when an invalid flag is specified (which includes the invalid flag) - `App.Writer` defaults to `stdout` when `nil` - If no action is specified on a command or app, the help is now printed instead of `panic`ing - `App.Metadata` is initialized automatically now (previously was `nil` unless initialized) - Correctly show help message if `-h` is provided to a subcommand - `context.(Global)IsSet` now respects environment variables. Previously it would return `false` if a flag was specified in the environment rather than as an argument - Removed deprecation warnings to STDERR to avoid them leaking to the end-user - `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well as `altsrc` where Go would complain that the types didn't match ## [1.18.1] - 2016-08-28 ### Fixed - Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported) ## [1.18.0] - 2016-06-27 ### Added - `./runtests` test runner with coverage tracking by default - testing on OS X - testing on Windows - `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code ### Changed - Use spaces for alignment in help/usage output instead of tabs, making the output alignment consistent regardless of tab width ### Fixed - Printing of command aliases in help text - Printing of visible flags for both struct and struct pointer flags - Display the `help` subcommand when using `CommandCategories` - No longer swallows `panic`s that occur within the `Action`s themselves when detecting the signature of the `Action` field ## [1.17.1] - 2016-08-28 ### Fixed - Removed deprecation warnings to STDERR to avoid them leaking to the end-user ## [1.17.0] - 2016-05-09 ### Added - Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` - `context.GlobalBoolT` was added as an analogue to `context.GlobalBool` - Support for hiding commands by setting `Hidden: true` -- this will hide the commands in help output ### Changed - `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer quoted in help text output. - All flag types now include `(default: {value})` strings following usage when a default value can be (reasonably) detected. - `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent with non-slice flag types - Apps now exit with a code of 3 if an unknown subcommand is specified (previously they printed "No help topic for...", but still exited 0. This makes it easier to script around apps built using `cli` since they can trust that a 0 exit code indicated a successful execution. - cleanups based on [Go Report Card feedback](https://goreportcard.com/report/github.com/urfave/cli) ## [1.16.1] - 2016-08-28 ### Fixed - Removed deprecation warnings to STDERR to avoid them leaking to the end-user ## [1.16.0] - 2016-05-02 ### Added - `Hidden` field on all flag struct types to omit from generated help text ### Changed - `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from generated help text via the `Hidden` field ### Fixed - handling of error values in `HandleAction` and `HandleExitCoder` ## [1.15.0] - 2016-04-30 ### Added - This file! - Support for placeholders in flag usage strings - `App.Metadata` map for arbitrary data/state management - `Set` and `GlobalSet` methods on `*cli.Context` for altering values after parsing. - Support for nested lookup of dot-delimited keys in structures loaded from YAML. ### Changed - The `App.Action` and `Command.Action` now prefer a return signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil `error` is returned, there may be two outcomes: - If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called automatically - Else the error is bubbled up and returned from `App.Run` - Specifying an `Action` with the legacy return signature of `func(*cli.Context)` will produce a deprecation message to stderr - Specifying an `Action` that is not a `func` type will produce a non-zero exit from `App.Run` - Specifying an `Action` func that has an invalid (input) signature will produce a non-zero exit from `App.Run` ### Deprecated - `cli.App.RunAndExitOnError`, which should now be done by returning an error that fulfills `cli.ExitCoder` to `cli.App.Run`. - the legacy signature for `cli.App.Action` of `func(*cli.Context)`, which should now have a return signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Fixed - Added missing `*cli.Context.GlobalFloat64` method ## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) ### Added - Codebeat badge - Support for categorization via `CategorizedHelp` and `Categories` on app. ### Changed - Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`. ### Fixed - Ensure version is not shown in help text when `HideVersion` set. ## [1.13.0] - 2016-03-06 (backfilled 2016-04-25) ### Added - YAML file input support. - `NArg` method on context. ## [1.12.0] - 2016-02-17 (backfilled 2016-04-25) ### Added - Custom usage error handling. - Custom text support in `USAGE` section of help output. - Improved help messages for empty strings. - AppVeyor CI configuration. ### Changed - Removed `panic` from default help printer func. - De-duping and optimizations. ### Fixed - Correctly handle `Before`/`After` at command level when no subcommands. - Case of literal `-` argument causing flag reordering. - Environment variable hints on Windows. - Docs updates. ## [1.11.1] - 2015-12-21 (backfilled 2016-04-25) ### Changed - Use `path.Base` in `Name` and `HelpName` - Export `GetName` on flag types. ### Fixed - Flag parsing when skipping is enabled. - Test output cleanup. - Move completion check to account for empty input case. ## [1.11.0] - 2015-11-15 (backfilled 2016-04-25) ### Added - Destination scan support for flags. - Testing against `tip` in Travis CI config. ### Changed - Go version in Travis CI config. ### Fixed - Removed redundant tests. - Use correct example naming in tests. ## [1.10.2] - 2015-10-29 (backfilled 2016-04-25) ### Fixed - Remove unused var in bash completion. ## [1.10.1] - 2015-10-21 (backfilled 2016-04-25) ### Added - Coverage and reference logos in README. ### Fixed - Use specified values in help and version parsing. - Only display app version and help message once. ## [1.10.0] - 2015-10-06 (backfilled 2016-04-25) ### Added - More tests for existing functionality. - `ArgsUsage` at app and command level for help text flexibility. ### Fixed - Honor `HideHelp` and `HideVersion` in `App.Run`. - Remove juvenile word from README. ## [1.9.0] - 2015-09-08 (backfilled 2016-04-25) ### Added - `FullName` on command with accompanying help output update. - Set default `$PROG` in bash completion. ### Changed - Docs formatting. ### Fixed - Removed self-referential imports in tests. ## [1.8.0] - 2015-06-30 (backfilled 2016-04-25) ### Added - Support for `Copyright` at app level. - `Parent` func at context level to walk up context lineage. ### Fixed - Global flag processing at top level. ## [1.7.1] - 2015-06-11 (backfilled 2016-04-25) ### Added - Aggregate errors from `Before`/`After` funcs. - Doc comments on flag structs. - Include non-global flags when checking version and help. - Travis CI config updates. ### Fixed - Ensure slice type flags have non-nil values. - Collect global flags from the full command hierarchy. - Docs prose. ## [1.7.0] - 2015-05-03 (backfilled 2016-04-25) ### Changed - `HelpPrinter` signature includes output writer. ### Fixed - Specify go 1.1+ in docs. - Set `Writer` when running command as app. ## [1.6.0] - 2015-03-23 (backfilled 2016-04-25) ### Added - Multiple author support. - `NumFlags` at context level. - `Aliases` at command level. ### Deprecated - `ShortName` at command level. ### Fixed - Subcommand help output. - Backward compatible support for deprecated `Author` and `Email` fields. - Docs regarding `Names`/`Aliases`. ## [1.5.0] - 2015-02-20 (backfilled 2016-04-25) ### Added - `After` hook func support at app and command level. ### Fixed - Use parsed context when running command as subcommand. - Docs prose. ## [1.4.1] - 2015-01-09 (backfilled 2016-04-25) ### Added - Support for hiding `-h / --help` flags, but not `help` subcommand. - Stop flag parsing after `--`. ### Fixed - Help text for generic flags to specify single value. - Use double quotes in output for defaults. - Use `ParseInt` instead of `ParseUint` for int environment var values. - Use `0` as base when parsing int environment var values. ## [1.4.0] - 2014-12-12 (backfilled 2016-04-25) ### Added - Support for environment variable lookup "cascade". - Support for `Stdout` on app for output redirection. ### Fixed - Print command help instead of app help in `ShowCommandHelp`. ## [1.3.1] - 2014-11-13 (backfilled 2016-04-25) ### Added - Docs and example code updates. ### Changed - Default `-v / --version` flag made optional. ## [1.3.0] - 2014-08-10 (backfilled 2016-04-25) ### Added - `FlagNames` at context level. - Exposed `VersionPrinter` var for more control over version output. - Zsh completion hook. - `AUTHOR` section in default app help template. - Contribution guidelines. - `DurationFlag` type. ## [1.2.0] - 2014-08-02 ### Added - Support for environment variable defaults on flags plus tests. ## [1.1.0] - 2014-07-15 ### Added - Bash completion. - Optional hiding of built-in help command. - Optional skipping of flag parsing at command level. - `Author`, `Email`, and `Compiled` metadata on app. - `Before` hook func support at app and command level. - `CommandNotFound` func support at app level. - Command reference available on context. - `GenericFlag` type. - `Float64Flag` type. - `BoolTFlag` type. - `IsSet` flag helper on context. - More flag lookup funcs at context level. - More tests & docs. ### Changed - Help template updates to account for presence/absence of flags. - Separated subcommand help template. - Exposed `HelpPrinter` var for more control over help output. ## [1.0.0] - 2013-11-01 ### Added - `help` flag in default app flag set and each command flag set. - Custom handling of argument parsing errors. - Command lookup by name at app level. - `StringSliceFlag` type and supporting `StringSlice` type. - `IntSliceFlag` type and supporting `IntSlice` type. - Slice type flag lookups by name at context level. - Export of app and command help functions. - More tests & docs. ## 0.1.0 - 2013-07-22 ### Added - Initial implementation. [unreleased 2.X]: https://github.com/urfave/cli/compare/v2.2.0...HEAD [2.2.0]: https://github.com/urfave/cli/compare/v2.1.1...v2.2.0 [2.1.1]: https://github.com/urfave/cli/compare/v2.1.0...v2.1.1 [2.1.0]: https://github.com/urfave/cli/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/urfave/cli/compare/v1.22.2...v2.0.0 [unreleased 1.22.X]: https://github.com/urfave/cli/compare/v1.22.4...v1 [1.22.4]: https://github.com/urfave/cli/compare/v1.22.3...v1.22.4 [1.22.3]: https://github.com/urfave/cli/compare/v1.22.2...v1.22.3 [1.22.2]: https://github.com/urfave/cli/compare/v1.22.1...v1.22.2 [1.22.1]: https://github.com/urfave/cli/compare/v1.22.0...v1.22.1 [1.22.0]: https://github.com/urfave/cli/compare/v1.21.0...v1.22.0 [1.21.0]: https://github.com/urfave/cli/compare/v1.20.0...v1.21.0 [1.20.0]: https://github.com/urfave/cli/compare/v1.19.1...v1.20.0 [1.19.1]: https://github.com/urfave/cli/compare/v1.19.0...v1.19.1 [1.19.0]: https://github.com/urfave/cli/compare/v1.18.0...v1.19.0 [1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0 [1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0 [1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0 [1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0 [1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1 [1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0 [1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2 [1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1 [1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0 [1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0 [1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0 [1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1 [1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0 [1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0 [1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0 [1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1 [1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0 [1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0 ================================================ FILE: docs/CNAME ================================================ cli.urfave.org ================================================ FILE: docs/CONTRIBUTING.md ================================================ ## Contributing Welcome to the `urfave/cli` contributor docs! This goal of this document is to help those interested in joining the 200+ humans who have contributed to this project over the years. > As a general guiding principle, the current maintainers may be notified via the > @urfave/cli GitHub team. All of the current maintainers are *volunteers* who live in various timezones with different scheduling needs, so please understand that your contribution or question may not get a response for many days. ### semantic versioning adherence The `urfave/cli` project strives to strictly adhere to [semantic versioning](https://semver.org/spec/v2.0.0.html). The active development branches and the milestones and import paths to which they correspond are: #### `main` branch The majority of active development and issue management is targeting the `main` branch. - :arrow_right: [`v3.x`](https://github.com/urfave/cli/milestone/5) - :arrow_right: `github.com/urfave/cli/v3` The `main` branch includes tooling to help with keeping track of `v3.x` series backward compatibility. More details on this process are in the development workflow section below. #### `v1-maint` branch The `v1-maint` branch **MUST** only receive bug fixes in the `v1.22.x` series. There is no strict rule regarding bug fixes to the `v3.x` or `v2.23.x` series being backported to the `v1.22.x` series. - :arrow_right: [`v1.22.x`](https://github.com/urfave/cli/milestone/11) - :arrow_right: `github.com/urfave/cli` #### `v2-maint` branch The `v2-maint` branch **MUST** only receive bug fixes in the `v2.23.x` series. There is no strict rule regarding bug fixes to the `v3.x` series being backported to the `v2.23.x` series. - :arrow_right: [`v2.23.x`](https://github.com/urfave/cli/milestone/16) - :arrow_right: `github.com/urfave/cli/v2` ### development workflow Most of the tooling around the development workflow strives for effective [dogfooding](https://en.wikipedia.org/wiki/Eating_your_own_dog_food). There is a top-level `Makefile` that is maintained strictly for the purpose of easing verification of one's development environment and any changes one may have introduced: ```sh make ``` Running the default `make` target (`all`) will ensure all of the critical steps are run to verify one's changes are harmonious in nature. The same steps are also run during the [continuous integration phase](https://github.com/urfave/cli/blob/main/.github/workflows/test.yml). `gfmrun` is required to run the examples, and without it `make all` will fail. You can find `gfmrun` here: - [urfave/gfmrun](https://github.com/urfave/gfmrun) To install `gfmrun`, you can use `go install`: ``` go install github.com/urfave/gfmrun/cmd/gfmrun@latest ``` In the event that the `v3diff` target exits non-zero, this is a signal that the public API surface area has changed. If the changes are acceptable, then manually running the approval step will "promote" the current `go doc` output: ```sh make v3approve ``` Because the `generate` step includes updating `godoc-current.txt` and `testdata/godoc-v3.x.txt`, these changes *MUST* be part of any proposed pull request so that reviewers have an opportunity to also make an informed decision about the "promotion" step. #### docs output The documentation in the `docs` directory is automatically built via `mkdocs` into a static site and published when releases are pushed (see [RELEASING](./RELEASING.md)). There is no strict requirement to build the documentation when developing locally, but the following `make` targets may be used if desired: ```sh # install documentation dependencies with `pip` make ensure-mkdocs ``` ```sh # build the static site in `./site` make docs ``` ```sh # start an mkdocs development server make serve-docs ``` ### pull requests Please feel free to open a pull request to fix a bug or add a feature. The @urfave/cli team will review it as soon as possible, giving special attention to maintaining backward compatibility. If the @urfave/cli team agrees that your contribution is in line with the vision of the project, they will work with you to get the code into a mergeable state, merged, and then released. ### granting of commit bit / admin mode Those with a history of contributing to this project will likely be invited to join the @urfave/cli team. As a member of the @urfave/cli team, you will have the ability to fully administer pull requests, issues, and other repository bits. If you feel that you should be a member of the @urfave/cli team but have not yet been added, the most likely explanation is that this is an accidental oversight! :sweat_smile:. Please open an issue! ================================================ FILE: docs/RELEASING.md ================================================ # Releasing urfave/cli Releasing small batches often is [backed by research](https://itrevolution.com/accelerate-book/) as part of the virtuous cycles that keep teams and products healthy. To that end, the overall goal of the release process is to send changes out into the world as close to the time the commits were merged to the `main` branch as possible. In this way, the community of humans depending on this library are able to make use of the changes they need **quickly**, which means they shouldn't have to maintain long-lived forks of the project, which means they can get back to focusing on the work on which they want to focus. This also means that the @urfave/cli team should be able to focus on delivering a steadily improving product with significantly eased ability to associate bugs and regressions with specific releases. ## Process - Release versions follow [semantic versioning](https://semver.org/) - Releases are associated with **signed, annotated git tags**[^1]. - Release notes are **automatically generated**[^2]. In the `main` or `v2-maint` branch, the current version is always available via: ```sh git describe --always --dirty --tags ``` **NOTE**: if the version reported contains `-dirty`, this is indicative of a "dirty" work tree, which is not a great state for creating a new release tag. Seek help from @urfave/cli teammates. For example, given a described version of `v2.4.7-3-g68da1cd` and a diff of `v2.4.7...` that contains only bug fixes, the next version should be `v2.4.8`: ```sh git tag -a -s -m 'Release 2.4.8' v2.4.8 git push origin v2.4.8 ``` The tag push will trigger a GitHub Actions workflow and will be **immediately available** to the [Go module mirror, index, and checksum database](https://proxy.golang.org/). The remaining steps require human intervention through the GitHub web view although [automated solutions exist](https://github.com/softprops/action-gh-release) that may be adopted in the future. - Open the [the new release page](https://github.com/urfave/cli/releases/new) - At the top of the form, click on the `Choose a tag` select control and select `v2.4.8` - In the `Write` tab below, click the `Auto-generate release notes` button - At the bottom of the form, click the `Publish release` button - :white_check_mark: you're done! [^1]: This was not always true. There are many **lightweight git tags** present in the repository history. [^2]: This was not always true. The [`docs/CHANGELOG.md`](./CHANGELOG.md) document used to be manually maintained. Relying on the automatic release notes generation requires the use of **merge commits** as opposed to squash merging or rebase merging. ================================================ FILE: docs/SECURITY.md ================================================ # Security Policy Hello and thank you for your interest in the `urfave/cli` security policy! :tada: :lock: ## Supported Versions | Version | Supported | | ------------ | ------------------------------------- | | `>= v2.3.x` | :white_check_mark: | | `< v2.3` | :x: | | `>= v1.22.x` | :white_check_mark: :lady_beetle: [^1] | | `< v1.22` | :x: | ## Reporting a Vulnerability Please disclose any vulnerabilities by sending an email to: [urfave-security@googlegroups.com](mailto:urfave-security@googlegroups.com) You should expect a response within 48 hours and further communications to be decided via email. The `urfave/cli` maintainer team comprises volunteers who contribute when possible, so please have patience :bow: [^1]: The `v1.22.x` series will receive bug fixes and security patches only. ================================================ FILE: docs/go.mod ================================================ module github.com/urfave/cli/docs/v3 go 1.23.2 replace github.com/urfave/cli/v3 => ../ require ( github.com/urfave/cli-altsrc/v3 v3.0.1 github.com/urfave/cli/v3 v3.1.1 ) require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/stretchr/testify v1.11.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: docs/go.sum ================================================ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli-altsrc/v3 v3.0.0-alpha2 h1:j4SaBpPB8++L0c0KuTnz/Yus3UQoWJ54hQjhIMW8rCM= github.com/urfave/cli-altsrc/v3 v3.0.0-alpha2/go.mod h1:Q79oyIY/z4jtzIrKEK6MUeWC7/szGr46x4QdOaOAIWc= github.com/urfave/cli-altsrc/v3 v3.0.1 h1:v+gHk59syLk8ao9rYybZs43+D5ut/gzj0omqQ1XYl8k= github.com/urfave/cli-altsrc/v3 v3.0.1/go.mod h1:8UtsKKcxFVzvaoySFPfvQOk413T+IXJhaCWyyoPW3yM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: docs/index.md ================================================ # Welcome to urfave/cli [![Run Tests](https://github.com/urfave/cli/actions/workflows/test.yml/badge.svg)](https://github.com/urfave/cli/actions/workflows/test.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/urfave/cli/v3.svg)](https://pkg.go.dev/github.com/urfave/cli/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/urfave/cli/v3)](https://goreportcard.com/report/github.com/urfave/cli/v3) [![codecov](https://codecov.io/gh/urfave/cli/branch/main/graph/badge.svg?token=t9YGWLh05g)](https://codecov.io/gh/urfave/cli) urfave/cli is a **declarative**, simple, fast, and fun package for building command line tools in Go featuring: - commands and subcommands with alias and prefix match support - flexible and permissive help system - dynamic shell completion for `bash`, `zsh`, `fish`, and `powershell` - `man` and markdown format documentation generation - input flags for simple types, slices of simple types, time, duration, and others - compound short flag support (`-a` `-b` `-c` :arrow_right: `-abc`) - input lookup from: - environment variables - plain text files - [structured file formats supported via the `urfave/cli-altsrc` package](https://github.com/urfave/cli-altsrc) These are the guides for each major version: - [`v3`](./v3/getting-started.md) - [`v2`](./v2/getting-started.md) - [`v1`](./v1/getting-started.md) In addition to the version-specific guides, these other documents are available: - [CONTRIBUTING](./CONTRIBUTING.md) - [CODE OF CONDUCT](./CODE_OF_CONDUCT.md) - [RELEASING](./RELEASING.md) ## Installation Using this package requires a working Go environment. [See the install instructions for Go](https://go.dev/doc/install). Go Modules are required when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). ### Using `v3` releases The latest `v3` release may be installed via the `/v3` suffix and is the recommended version for all new development. The state of the [`main` branch](https://github.com/urfave/cli/tree/main) at any given time **may** correspond to a `v3` series release or pre-release. Please see the [`v3` migration guide](./migrate-v2-to-v3.md) on using v3 if you are upgrading from v2. ```sh go get github.com/urfave/cli/v3@latest ``` ```go import ( "github.com/urfave/cli/v3" // imports as package "cli" ) ``` ### Using `v2` releases :warning: The `v2` series is receiving **security and bug fixes only** via the [`v2-maint` branch](https://github.com/urfave/cli/tree/v2-maint) and **should not** be used in new development. Please see the [`v3` migration guide](./migrate-v2-to-v3.md) and feel free to open an issue or discussion if you need help with the migration to `v3`. ```sh go get github.com/urfave/cli/v2@latest ``` ```go import ( "github.com/urfave/cli/v2" // imports as package "cli" ) ``` ### Using `v1` releases :warning: The `v1` series is receiving **security fixes only** via the [`v1-maint` branch](https://github.com/urfave/cli/tree/v1-maint) and **should not** be used in new development. Please see the [`v2` migration guide](./migrate-v1-to-v2.md) and feel free to open an issue or discussion if you need help with the migration to `v2`. ### Supported platforms cli is tested against multiple versions of Go on Linux, and against the latest released version of Go on OS X and Windows. This project uses GitHub Actions for builds. To see our currently supported go versions and platforms, look at the [github workflow configuration](https://github.com/urfave/cli/blob/main/.github/workflows/test.yml). ================================================ FILE: docs/migrate-v1-to-v2.md ================================================ # Migration Guide: v1 to v2 v2 has a number of breaking changes but converting is relatively straightforward: make the changes documented below then resolve any compiler errors. We hope this will be sufficient for most typical users. If you find any issues not covered by this document, please post a comment on [Issue 921](https://github.com/urfave/cli/issues/921) or consider sending a PR to help improve this guide. ## Flags before args In v2 flags must come before args. This is more POSIX-compliant. You may need to update scripts, user documentation, etc. This will work: ``` cli hello --shout rick ``` This will not: ``` cli hello rick --shout ``` ## Import string changed === "v1" `import "github.com/urfave/cli"` === "v2" `import "github.com/urfave/cli/v2"` Check each file for this and make the change. Shell command to find them all: `fgrep -rl github.com/urfave/cli *` ## Flag aliases are done differently Change `Name: "foo, f"` to `Name: "foo", Aliases: []string{"f"}` === "v1" ```go cli.StringFlag{ Name: "config, cfg" } ``` === "v2" ```go cli.StringFlag{ Name: "config", Aliases: []string{"cfg"}, } ``` Sadly v2 doesn't warn you if a comma is in the name. (https://github.com/urfave/cli/issues/1103) ## EnvVar is now a list (EnvVars) Change `EnvVar: "XXXXX"` to `EnvVars: []string{"XXXXX"}` (plural). === "v1" ```go cli.StringFlag{ EnvVar: "APP_LANG" } ``` === "v2" ```go cli.StringFlag{ EnvVars: []string{"APP_LANG"} } ``` ## Actions returns errors A command's `Action:` now returns an `error`. === "v1" `Action: func(c *cli.Context) {` === "v2" `Action: func(c *cli.Context) error {` Compiler messages you might see: ``` cannot use func literal (type func(*cli.Context)) as type cli.ActionFunc in field value ``` ## cli.Flag changed `cli.Flag` is now a list of pointers. What this means to you: If you make a list of flags, add a `&` in front of each item. cli.BoolFlag, cli.StringFlag, etc. === "v1" ```go app.Flags = []cli.Flag{ cli.BoolFlag{ ``` === "v2" ```go app.Flags = []cli.Flag{ &cli.BoolFlag{ ``` Compiler messages you might see: ``` cli.StringFlag does not implement cli.Flag (Apply method has pointer receiver) ``` ## Commands are now lists of pointers Occurrences of `[]Command` have been changed to `[]*Command`. What this means to you: Look for `[]cli.Command{}` and change it to `[]*cli.Command{}` Example: === "v1" `var commands = []cli.Command{}` === "v2" `var commands = []*cli.Command{}` Compiler messages you might see: ``` cannot convert commands (type []cli.Command) to type cli.CommandsByName cannot use commands (type []cli.Command) as type []*cli.Command in assignment ``` ## Lists of commands should be pointers If you are building up a list of commands, the individual items should now be pointers. === "v1" `cli.Command{` === "v2" `&cli.Command{` Compiler messages you might see: ``` cannot use cli.Command literal (type cli.Command) as type *cli.Command in argument to ``` ## Appending Commands Appending to a list of commands needs to be changed since the list is now pointers. === "v1" `commands = append(commands, *c)` === "v2" `commands = append(commands, c)` Compiler messages you might see: ``` cannot use c (type *cli.Command) as type cli.Command in append ``` ## GlobalString, GlobalBool and its likes are deprecated Use simply `String` instead of `GlobalString`, `Bool` instead of `GlobalBool` ## BoolTFlag and BoolT are deprecated BoolTFlag was a Bool Flag with its default value set to true and BoolT was used to find any BoolTFlag used locally, so both are deprecated. === "v1" ```go cli.BoolTFlag{ Name: FlagName, Usage: FlagUsage, EnvVar: "FLAG_ENV_VAR", } ``` === "v2" ```go cli.BoolFlag{ Name: FlagName, Value: true, Usage: FlagUsage, EnvVar: "FLAG_ENV_VAR", } ``` ## &cli.StringSlice{""} replaced with cli.NewStringSlice("") Example: === "v1" ```go Value: &cli.StringSlice{""}, ``` === "v2" ```go Value: cli.NewStringSlice(""), ``` ## Replace deprecated functions `cli.NewExitError()` is deprecated. Use `cli.Exit()` instead. ([Staticcheck](https://staticcheck.io/) detects this automatically and recommends replacement code.) ## Everything else Compile the code and work through any errors. Most should relate to issues listed above. Once it compiles, test the command. Review the output of `-h` or any help messages to verify they match the intended flags and subcommands. Then test the program itself. If you find any issues not covered by this document please let us know by submitting a comment on [Issue 921](https://github.com/urfave/cli/issues/921) so that others can benefit. ================================================ FILE: docs/migrate-v2-to-v3.md ================================================ # Migration Guide: v2 to v3 v3 has a number of breaking changes but converting is relatively straightforward: make the changes documented below then resolve any compiler errors. We hope this will be sufficient for most typical users. If you find any issues not covered by this document, please post a comment on [the discussion](https://github.com/urfave/cli/discussions/2084) or consider sending a PR to help improve this guide. ## New Import === "v2" `import "github.com/urfave/cli/v2"` === "v3" `import "github.com/urfave/cli/v3"` Check each file for this and make the change. Shell command to find them all: `fgrep -rl github.com/urfave/cli/v2 *` ## New Names ### cli.App === "v2" ```go cli.App{ // ... } ``` === "v3" ```go cli.Command{ // ... } ``` ### cli.App.EnableBashCompletion === "v2" ```go cli.App{ EnableBashCompletion: true, } ``` === "v3" ```go cli.Command{ EnableShellCompletion: true, } ``` ### cli.App.CustomAppHelpTemplate === "v2" ```go cli.App{ CustomAppHelpTemplate: "...", } ``` === "v3" ```go cli.Command{ CustomRootCommandHelpTemplate: "...", } ``` ### cli.App.RunContext === "v2" ```go (&cli.App{}).RunContext(context.Background(), os.Args) ``` === "v3" ```go (&cli.Command{}).Run(context.Background(), os.Args) ``` ### cli.App.BashComplete === "v2" ```go cli.App{ BashComplete: func(ctx *cli.Context) {}, } ``` === "v3" ```go cli.Command{ ShellComplete: func(ctx context.Context, cmd *cli.Command) {}, } ``` ### cli.Command.Subcommands === "v2" ```go cli.Command{ Subcommands: []*cli.Command{}, } ``` === "v3" ```go cli.Command{ Commands: []*cli.Command{}, } ``` ## Sources ### FilePath === "v2" ```go cli.StringFlag{ FilePath: "/path/to/foo", } ``` === "v3" ```go cli.StringFlag{ Sources: cli.Files("/path/to/foo"), } ``` or ```go cli.StringFlag{ Sources: cli.NewValueSourceChain( cli.File("/path/to/foo"), ), } ``` ### EnvVars === "v2" ```go cli.StringFlag{ EnvVars: []string{"APP_LANG"}, } ``` === "v3" ```go cli.StringFlag{ Sources: cli.EnvVars("APP_LANG"), } ``` or ```go cli.StringFlag{ Sources: cli.NewValueSourceChain( cli.EnvVar("APP_LANG"), ), } ``` ### Altsrc #### Altsrc is now a dedicated module === "v2" `import "github.com/urfave/cli/v2/altsrc"` === "v3" `import altsrc "github.com/urfave/cli-altsrc/v3"` #### Altsrc is now a value source for CLI === "v2" ```go altsrc.NewStringFlag( &cli.StringFlag{ Name: "key", Value: "/tmp/foo", }, ), ``` === "v3" Requires to use at least `github.com/urfave/cli-altsrc/v3@v3.0.0-alpha2.0.20250227140532-11fbec4d81a7` ```go cli.StringFlag{ Sources: cli.NewValueSourceChain(altsrcjson.JSON("key", altsrc.StringSourcer("/path/to/foo.json"))), } ``` ### Order of precedence of envvars, filepaths, altsrc now depends on the order in which they are defined === "v2" ```go altsrc.NewStringFlag( &cli.StringFlag{ Name: "key", EnvVars: []string{"APP_LANG"}, FilePath: "/path/to/foo", }, ), ``` === "v3" Requires to use at least `github.com/urfave/cli-altsrc/v3@v3.0.0-alpha2.0.20250227140532-11fbec4d81a7` ```go import altsrcjson "github.com/urfave/cli-altsrc/v3/json" // ... &cli.StringFlag{ Name: "key", Sources: cli.NewValueSourceChain( cli.EnvVar("APP_LANG"), cli.File("/path/to/foo"), altsrcjson.JSON("key", altsrc.StringSourcer("/path/to/foo.json")), ), }, ``` In the above case the Envs are checked first and if not found then files are looked at and then finally the `altsrc` ## cli.Context has been removed All functions handled previously by `cli.Context` have been incorporated into `cli.Command`: | v2 | v3 | |------------------------------|------------------------------| | `cli.Context.IsSet` | `cli.Command.IsSet` | | `cli.Context.NumFlags` | `cli.Command.NumFlags` | | `cli.Context.FlagNames` | `cli.Command.FlagNames` | | `cli.Context.LocalFlagNames` | `cli.Command.LocalFlagNames` | | `cli.Context.Lineage` | `cli.Command.Lineage` | | `cli.Context.Count` | `cli.Command.Count` | | `cli.Context.Value` | `cli.Command.Value` | | `cli.Context.Args` | `cli.Command.Args` | | `cli.Context.NArg` | `cli.Command.NArg` | ## Handler Function Signatures Changes All handler functions now take at least 2 arguments a `context.Context` and a pointer to `Cli.Command` in addition to other specific args. This allows handler functions to utilize `context.Context` for blocking/time-specific operations and so on. ### BeforeFunc === "v2" `type BeforeFunc func(*Context) error` === "v3" `type BeforeFunc func(context.Context, *cli.Command) (context.Context, error)` ### AfterFunc === "v2" `type AfterFunc func(*Context) error` === "v3" `type AfterFunc func(context.Context, *cli.Command) error` ### ActionFunc === "v2" `type ActionFunc func(*Context) error` === "v3" `type ActionFunc func(context.Context, *cli.Command) error` ### CommandNotFoundFunc === "v2" `type CommandNotFoundFunc func(*Context, string) error` === "v3" `type CommandNotFoundFunc func(context.Context, *cli.Command, string) error` ### OnUsageErrorFunc === "v2" `type OnUsageErrorFunc func(*Context, err error, isSubcommand bool) error` === "v3" `type OnUsageErrorFunc func(context.Context, *cli.Command, err error, isSubcommand bool) error` ### InvalidAccessFunc === "v2" `type InvalidAccessFunc func(*Context, string) error` === "v3" `type InvalidAccessFunc func(context.Context, *cli.Command, string) error` ### ExitErrHandlerFunc === "v2" `type ExitErrHandlerFunc func(*Context, err error) error` === "v3" `type ExitErrHandlerFunc func(context.Context, *cli.Command, err error) error` Compiler messages you might see(for ActionFunc): ``` cannot use func literal (type func(*cli.Context) error) as type cli.ActionFunc in field value ``` Similar messages would be shown for other funcs. ## TimestampFlag === "v2" ```go &cli.TimestampFlag{ Name: "foo", Layout: time.RFC3339, } ``` === "v3" ```go &cli.TimestampFlag{ Name: "foo", Config: cli.TimestampConfig{ Layouts: []string{time.RFC3339}, }, } ``` ## PathFlag === "v2" ```go &cli.PathFlag{ Name: "foo", } ``` === "v3" ```go &cli.StringFlag{ Name: "foo", TakesFile: true, } ``` ## Authors === "v2" ```go &cli.App{ Authors: []*cli.Author{ {Name: "Some Guy", Email: "someguy@example.com"}, }, } ``` === "v3" ```go // import "net/mail" &cli.Command{ Authors: []any{ mail.Address{Name: "Some Guy", Address: "someguy@example.com"}, }, } ``` ================================================ FILE: docs/package.go ================================================ // Package docs is an empty shell! This file is *only* meant to capture the dependencies // required by the `gfmrun` documentation tests. package docs import ( _ "github.com/urfave/cli-altsrc/v3" _ "github.com/urfave/cli/v3" ) ================================================ FILE: docs/v1/examples/arguments.md ================================================ --- tags: - v1 --- You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Action = func(c *cli.Context) error { fmt.Printf("Hello %q", c.Args().Get(0)) return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v1/examples/bash-completions.md ================================================ --- tags: - v1 --- You can enable completion commands by setting the `EnableBashCompletion` flag on the `App` object. By default, this setting will only auto-complete to show an app's subcommands, but you can write your own completion methods for the App or its subcommands. ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} app := cli.NewApp() app.EnableBashCompletion = true app.Commands = []cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) return nil }, BashComplete: func(c *cli.Context) { // This will complete if no args are passed if c.NArg() > 0 { return } for _, t := range tasks { fmt.Println(t) } }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` #### Enabling Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while setting the `PROG` variable to the name of your program: `PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` #### Distribution Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename it to the name of the program you wish to add autocomplete support for (or automatically install it there if you are distributing a package). Don't forget to source the file to make it active in the current shell. ``` sudo cp src/bash_autocomplete /etc/bash_completion.d/ source /etc/bash_completion.d/ ``` Alternatively, you can just document that users should source the generic `autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set to the name of their program (as above). #### Customization The default bash completion flag (`--generate-bash-completion`) is defined as `cli.BashCompletionFlag`, and may be redefined if desired, e.g.: ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { cli.BashCompletionFlag = cli.BoolFlag{ Name: "compgen", Hidden: true, } app := cli.NewApp() app.EnableBashCompletion = true app.Commands = []cli.Command{ { Name: "wat", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v1/examples/combining-short-options.md ================================================ --- tags: - v1 --- Traditional use of options using their shortnames look like this: ``` $ cmd -s -o -m "Some message" ``` Suppose you want users to be able to combine options with their shortnames. This can be done using the `UseShortOptionHandling` bool in your app configuration, or for individual commands by attaching it to the command configuration. For example: ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.UseShortOptionHandling = true app.Commands = []cli.Command{ { Name: "short", Usage: "complete a task on the list", Flags: []cli.Flag{ cli.BoolFlag{Name: "serve, s"}, cli.BoolFlag{Name: "option, o"}, cli.StringFlag{Name: "message, m"}, }, Action: func(c *cli.Context) error { fmt.Println("serve:", c.Bool("serve")) fmt.Println("option:", c.Bool("option")) fmt.Println("message:", c.String("message")) return nil }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` If your program has any number of bool flags such as `serve` and `option`, and optionally one non-bool flag `message`, with the short options of `-s`, `-o`, and `-m` respectively, setting `UseShortOptionHandling` will also support the following syntax: ``` $ cmd -som "Some message" ``` If you enable `UseShortOptionHandling`, then you must not use any flags that have a single leading `-` or this will result in failures. For example, `-option` can no longer be used. Flags with two leading dashes (such as `--options`) are still valid. ================================================ FILE: docs/v1/examples/exit-codes.md ================================================ --- tags: - v1 --- Calling `App.Run` will not automatically call `os.Exit`, which means that by default the exit code will "fall through" to being `0`. An explicit exit code may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a `cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag{ cli.BoolFlag{ Name: "ginger-crouton", Usage: "Add ginger croutons to the soup", }, } app.Action = func(ctx *cli.Context) error { if !ctx.Bool("ginger-crouton") { return cli.NewExitError("Ginger croutons are not in the soup", 86) } return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v1/examples/flags.md ================================================ --- tags: - v1 --- Setting and querying flags is simple. ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, } app.Action = func(c *cli.Context) error { name := "Nefertiti" if c.NArg() > 0 { name = c.Args().Get(0) } if c.String("lang") == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` You can also set a destination variable for a flag, to which the content will be scanned. ``` go package main import ( "log" "os" "fmt" "github.com/urfave/cli" ) func main() { var language string app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Destination: &language, }, } app.Action = func(c *cli.Context) error { name := "someone" if c.NArg() > 0 { name = c.Args()[0] } if language == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` See full list of flags at https://pkg.go.dev/github.com/urfave/cli #### Placeholder Values Sometimes it's useful to specify a flag's value within the usage string itself. Such placeholders are indicated with back quotes. For example this: ```go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag{ cli.StringFlag{ Name: "config, c", Usage: "Load configuration from `FILE`", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --config FILE, -c FILE Load configuration from FILE ``` Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. #### Alternate Names You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "lang, l", Value: "english", Usage: "language for the greeting", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. #### Ordering Flags for the application and commands are shown in the order they are defined. However, it's possible to sort them from outside this library by using `FlagsByName` or `CommandsByName` with `sort`. For example this: ``` go package main import ( "log" "os" "sort" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "lang, l", Value: "english", Usage: "Language for the greeting", }, cli.StringFlag{ Name: "config, c", Usage: "Load configuration from `FILE`", }, } app.Commands = []cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { return nil }, }, { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) error { return nil }, }, } sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --config FILE, -c FILE Load configuration from FILE --lang value, -l value Language for the greeting (default: "english") ``` #### Values from the Environment You can also have the default value set from the environment via `EnvVar`. e.g. ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "lang, l", Value: "english", Usage: "language for the greeting", EnvVar: "APP_LANG", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default. ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "lang, l", Value: "english", Usage: "language for the greeting", EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` #### Values from files You can also have the default value set from file via `FilePath`. e.g. ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag { cli.StringFlag{ Name: "password, p", Usage: "password for the mysql database", FilePath: "/etc/mysql/password", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Note that default values set from file (e.g. `FilePath`) take precedence over default values set from the environment (e.g. `EnvVar`). #### Values from alternate input sources (YAML, TOML, and others) There is a separate package altsrc that adds support for getting flag values from other file input sources. Currently supported input source formats: * YAML * JSON * TOML In order to get values for a flag from an alternate input source the following code would be added to wrap an existing cli.Flag like below: ``` go altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) ``` Initialization must also occur for these flags. Below is an example initializing getting data from a yaml file below. ``` go command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) ``` The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. It will then use that file name to initialize the yaml input source for any flags that are defined on that command. As a note the "load" flag used would also have to be defined on the command flags in order for this code snippet to work. Currently only YAML, JSON, and TOML files are supported but developers can add support for other input sources by implementing the altsrc.InputSourceContext for their given sources. Here is a more complete sample of a command using YAML support: ``` go package notmain import ( "fmt" "log" "os" "github.com/urfave/cli" "github.com/urfave/cli/altsrc" ) func main() { app := cli.NewApp() flags := []cli.Flag{ altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), cli.StringFlag{Name: "load"}, } app.Action = func(c *cli.Context) error { fmt.Println("yaml ist rad") return nil } app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) app.Flags = flags err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` #### Precedence The precedence for flag value sources is as follows (highest to lowest): 0. Command line flag value from user 0. Environment variable (if specified) 0. Configuration file (if specified) 0. Default defined on the flag ================================================ FILE: docs/v1/examples/generated-help-text.md ================================================ --- tags: - v1 --- The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked by the cli internals in order to print generated help text for the app, command, or subcommand, and break execution. #### Customization All of the help text generation may be customized, and at multiple levels. The templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and `SubcommandHelpTemplate` which may be reassigned or augmented, and full override is possible by assigning a compatible func to the `cli.HelpPrinter` variable, e.g.: ``` go package main import ( "fmt" "log" "io" "os" "github.com/urfave/cli" ) func main() { // EXAMPLE: Append to an existing template cli.AppHelpTemplate = fmt.Sprintf(`%s WEBSITE: http://awesometown.example.com SUPPORT: support@awesometown.example.com `, cli.AppHelpTemplate) // EXAMPLE: Override a template cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: {{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} COPYRIGHT: {{.Copyright}} {{end}}{{if .Version}} VERSION: {{.Version}} {{end}} ` // EXAMPLE: Replace the `HelpPrinter` func cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Println("Ha HA. I pwnd the help!!1") } err := cli.NewApp().Run(os.Args) if err != nil { log.Fatal(err) } } ``` The default flag may be customized to something other than `-h/--help` by setting `cli.HelpFlag`, e.g.: ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { cli.HelpFlag = cli.BoolFlag{ Name: "halp, haaaaalp", Usage: "HALP", EnvVar: "SHOW_HALP,HALPPLZ", } err := cli.NewApp().Run(os.Args) if err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v1/examples/greet.md ================================================ --- tags: - v1 --- Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness! Start by creating a directory named `greet`, and within it, add a file, `greet.go` with the following code in it: ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "greet" app.Usage = "fight the loneliness!" app.Action = func(c *cli.Context) error { fmt.Println("Hello friend!") return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Install our command to the `$GOPATH/bin` directory: ``` $ go install ``` Finally run our new command: ``` $ greet Hello friend! ``` cli also generates neat help text: ``` $ greet help NAME: greet - fight the loneliness! USAGE: greet [global options] command [command options] [arguments...] VERSION: 0.0.0 COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS --version Shows version information ``` ================================================ FILE: docs/v1/examples/subcommands-categories.md ================================================ --- tags: - v1 --- For additional organization in apps that have many subcommands, you can associate a category for each command to group them together in the help output. E.g. ```go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Commands = []cli.Command{ { Name: "noop", }, { Name: "add", Category: "Template actions", }, { Name: "remove", Category: "Template actions", }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Will include: ``` COMMANDS: noop Template actions: add remove ``` ================================================ FILE: docs/v1/examples/subcommands.md ================================================ --- tags: - v1 --- Subcommands can be defined for a more git-like command line app. ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Commands = []cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) error { fmt.Println("added task: ", c.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Subcommands: []cli.Command{ { Name: "add", Usage: "add a new template", Action: func(c *cli.Context) error { fmt.Println("new task template: ", c.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(c *cli.Context) error { fmt.Println("removed task template: ", c.Args().First()) return nil }, }, }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v1/examples/version-flag.md ================================================ --- tags: - v1 --- The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which is checked by the cli internals in order to print the `App.Version` via `cli.VersionPrinter` and break execution. #### Customization The default flag may be customized to something other than `-v/--version` by setting `cli.VersionFlag`, e.g.: ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { cli.VersionFlag = cli.BoolFlag{ Name: "print-version, V", Usage: "print only the version", } app := cli.NewApp() app.Name = "partay" app.Version = "19.99.0" err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) var ( Revision = "fafafaf" ) func main() { cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) } app := cli.NewApp() app.Name = "partay" app.Version = "19.99.0" err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` #### Full API Example **Notice**: This is a contrived (functioning) example meant strictly for API demonstration purposes. Use of one's imagination is encouraged. ``` go package main import ( "errors" "flag" "fmt" "io" "io/ioutil" "os" "time" "github.com/urfave/cli" ) func init() { cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" cli.CommandHelpTemplate += "\nYMMV\n" cli.SubcommandHelpTemplate += "\nor something\n" cli.HelpFlag = cli.BoolFlag{Name: "halp"} cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Fprintf(w, "best of luck to you\n") } cli.VersionPrinter = func(c *cli.Context) { fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) } cli.OsExiter = func(c int) { fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) } cli.ErrWriter = ioutil.Discard cli.FlagStringer = func(fl cli.Flag) string { return fmt.Sprintf("\t\t%s", fl.GetName()) } } type hexWriter struct{} func (w *hexWriter) Write(p []byte) (int, error) { for _, b := range p { fmt.Printf("%x", b) } fmt.Printf("\n") return len(p), nil } type genericType struct{ s string } func (g *genericType) Set(value string) error { g.s = value return nil } func (g *genericType) String() string { return g.s } func main() { app := cli.NewApp() app.Name = "kənˈtrīv" app.Version = "19.99.0" app.Compiled = time.Now() app.Authors = []cli.Author{ cli.Author{ Name: "Example Human", Email: "human@example.com", }, } app.Copyright = "(c) 1999 Serious Enterprise" app.HelpName = "contrive" app.Usage = "demonstrate available API" app.UsageText = "contrive - demonstrating the available API" app.ArgsUsage = "[args and such]" app.Commands = []cli.Command{ cli.Command{ Name: "doo", Aliases: []string{"do"}, Category: "motion", Usage: "do the doo", UsageText: "doo - does the dooing", Description: "no really, there is a lot of dooing to be done", ArgsUsage: "[arrgh]", Flags: []cli.Flag{ cli.BoolFlag{Name: "forever, forevvarr"}, }, Subcommands: cli.Commands{ cli.Command{ Name: "wop", Action: wopAction, }, }, SkipFlagParsing: false, HideHelp: false, Hidden: false, HelpName: "doo!", BashComplete: func(c *cli.Context) { fmt.Fprintf(c.App.Writer, "--better\n") }, Before: func(c *cli.Context) error { fmt.Fprintf(c.App.Writer, "brace for impact\n") return nil }, After: func(c *cli.Context) error { fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") return nil }, Action: func(c *cli.Context) error { c.Command.FullName() c.Command.HasName("wop") c.Command.Names() c.Command.VisibleFlags() fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") if c.Bool("forever") { c.Command.Run(c) } return nil }, OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { fmt.Fprintf(c.App.Writer, "for shame\n") return err }, }, } app.Flags = []cli.Flag{ cli.BoolFlag{Name: "fancy"}, cli.BoolTFlag{Name: "fancier"}, cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, cli.Float64Flag{Name: "howmuch"}, cli.GenericFlag{Name: "wat", Value: &genericType{}}, cli.Int64Flag{Name: "longdistance"}, cli.Int64SliceFlag{Name: "intervals"}, cli.IntFlag{Name: "distance"}, cli.IntSliceFlag{Name: "times"}, cli.StringFlag{Name: "dance-move, d"}, cli.StringSliceFlag{Name: "names, N"}, cli.UintFlag{Name: "age"}, cli.Uint64Flag{Name: "bigage"}, } app.EnableBashCompletion = true app.UseShortOptionHandling = true app.HideHelp = false app.HideVersion = false app.BashComplete = func(c *cli.Context) { fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") } app.Before = func(c *cli.Context) error { fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") return nil } app.After = func(c *cli.Context) error { fmt.Fprintf(c.App.Writer, "Phew!\n") return nil } app.CommandNotFound = func(c *cli.Context, command string) { fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) } app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { if isSubcommand { return err } fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) return nil } app.Action = func(c *cli.Context) error { cli.DefaultAppComplete(c) cli.HandleExitCoder(errors.New("not an exit coder, though")) cli.ShowAppHelp(c) cli.ShowCommandCompletions(c, "nope") cli.ShowCommandHelp(c, "also-nope") cli.ShowCompletions(c) cli.ShowSubcommandHelp(c) cli.ShowVersion(c) categories := c.App.Categories() categories.AddCommand("sounds", cli.Command{ Name: "bloop", }) for _, category := range c.App.Categories() { fmt.Fprintf(c.App.Writer, "%s\n", category.Name) fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) } fmt.Printf("%#v\n", c.App.Command("doo")) if c.Bool("infinite") { c.App.Run([]string{"app", "doo", "wop"}) } if c.Bool("forevar") { c.App.RunAsSubcommand(c) } c.App.Setup() fmt.Printf("%#v\n", c.App.VisibleCategories()) fmt.Printf("%#v\n", c.App.VisibleCommands()) fmt.Printf("%#v\n", c.App.VisibleFlags()) fmt.Printf("%#v\n", c.Args().First()) if len(c.Args()) > 0 { fmt.Printf("%#v\n", c.Args()[1]) } fmt.Printf("%#v\n", c.Args().Present()) fmt.Printf("%#v\n", c.Args().Tail()) set := flag.NewFlagSet("contrive", 0) nc := cli.NewContext(c.App, set, c) fmt.Printf("%#v\n", nc.Args()) fmt.Printf("%#v\n", nc.Bool("nope")) fmt.Printf("%#v\n", nc.BoolT("nerp")) fmt.Printf("%#v\n", nc.Duration("howlong")) fmt.Printf("%#v\n", nc.Float64("hay")) fmt.Printf("%#v\n", nc.Generic("bloop")) fmt.Printf("%#v\n", nc.Int64("bonk")) fmt.Printf("%#v\n", nc.Int64Slice("burnks")) fmt.Printf("%#v\n", nc.Int("bips")) fmt.Printf("%#v\n", nc.IntSlice("blups")) fmt.Printf("%#v\n", nc.String("snurt")) fmt.Printf("%#v\n", nc.StringSlice("snurkles")) fmt.Printf("%#v\n", nc.Uint("flub")) fmt.Printf("%#v\n", nc.Uint64("florb")) fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) fmt.Printf("%#v\n", nc.FlagNames()) fmt.Printf("%#v\n", nc.GlobalFlagNames()) fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) fmt.Printf("%#v\n", nc.NArg()) fmt.Printf("%#v\n", nc.NumFlags()) fmt.Printf("%#v\n", nc.Parent()) nc.Set("wat", "also-nope") ec := cli.NewExitError("ohwell", 86) fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) fmt.Printf("made it!\n") return nil } if os.Getenv("HEXY") != "" { app.Writer = &hexWriter{} app.ErrWriter = &hexWriter{} } app.Metadata = map[string]interface{}{ "layers": "many", "explicable": false, "whatever-values": 19.99, } // ignore error so we don't exit non-zero and break gfmrun README example tests _ = app.Run(os.Args) } func wopAction(c *cli.Context) error { fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") return nil } ``` ================================================ FILE: docs/v1/getting-started.md ================================================ --- tags: - v1 --- One of the philosophies behind cli is that an API should be playful and full of discovery. So a cli app can be as little as one line of code in `main()`. ``` go package main import ( "log" "os" "github.com/urfave/cli" ) func main() { err := cli.NewApp().Run(os.Args) if err != nil { log.Fatal(err) } } ``` This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation: ``` go package main import ( "fmt" "log" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "boom" app.Usage = "make an explosive entrance" app.Action = func(c *cli.Context) error { fmt.Println("boom! I say!") return nil } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } ``` Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below. ================================================ FILE: docs/v1/migrating-to-v2.md ================================================ --- tags: - v1 --- There are a small set of breaking changes between v1 and v2. Converting is relatively straightforward and typically takes less than an hour. Specific steps are included in [Migration Guide: v1 to v2](../migrate-v1-to-v2.md). ================================================ FILE: docs/v2/examples/arguments.md ================================================ --- tags: - v2 search: boost: 2 --- You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Action: func(cCtx *cli.Context) error { fmt.Printf("Hello %q", cCtx.Args().Get(0)) return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v2/examples/bash-completions.md ================================================ --- tags: - v2 search: boost: 2 --- You can enable completion commands by setting the `EnableBashCompletion` flag on the `App` object to `true`. By default, this setting will allow auto-completion for an app's subcommands, but you can write your own completion methods for the App or its subcommands as well. #### Default auto-completion ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ EnableBashCompletion: true, Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(cCtx *cli.Context) error { fmt.Println("added task: ", cCtx.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(cCtx *cli.Context) error { fmt.Println("completed task: ", cCtx.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Subcommands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(cCtx *cli.Context) error { fmt.Println("new task template: ", cCtx.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(cCtx *cli.Context) error { fmt.Println("removed task template: ", cCtx.Args().First()) return nil }, }, }, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` ![](../images/default-bash-autocomplete.gif) #### Custom auto-completion ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} app := &cli.App{ EnableBashCompletion: true, Commands: []*cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(cCtx *cli.Context) error { fmt.Println("completed task: ", cCtx.Args().First()) return nil }, BashComplete: func(cCtx *cli.Context) { // This will complete if no args are passed if cCtx.NArg() > 0 { return } for _, t := range tasks { fmt.Println(t) } }, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` ![](../images/custom-bash-autocomplete.gif) #### Enabling To enable auto-completion for the current shell session, a bash script, `autocomplete/bash_autocomplete` is included in this repo. To use `autocomplete/bash_autocomplete` set an environment variable named `PROG` to the name of your program and then `source` the `autocomplete/bash_autocomplete` file. For example, if your cli program is called `myprogram`: ```sh-session $ PROG=myprogram source path/to/cli/autocomplete/bash_autocomplete ``` Auto-completion is now enabled for the current shell, but will not persist into a new shell. #### Distribution and Persistent Autocompletion Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename it to the name of the program you wish to add autocomplete support for (or automatically install it there if you are distributing a package). Don't forget to source the file or restart your shell to activate the auto-completion. ```sh-session $ sudo cp path/to/autocomplete/bash_autocomplete /etc/bash_completion.d/ $ source /etc/bash_completion.d/ ``` Alternatively, you can just document that users should `source` the generic `autocomplete/bash_autocomplete` and set `$PROG` within their bash configuration file, adding these lines: ```sh-session $ PROG= $ source path/to/cli/autocomplete/bash_autocomplete ``` Keep in mind that if they are enabling auto-completion for more than one program, they will need to set `PROG` and source `autocomplete/bash_autocomplete` for each program, like so: ```sh-session $ PROG= $ source path/to/cli/autocomplete/bash_autocomplete $ PROG= $ source path/to/cli/autocomplete/bash_autocomplete ``` #### Customization The default shell completion flag (`--generate-bash-completion`) is defined as `cli.EnableBashCompletion`, and may be redefined if desired, e.g.: ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ EnableBashCompletion: true, Commands: []*cli.Command{ { Name: "wat", }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` #### ZSH Support Auto-completion for ZSH is also supported using the `autocomplete/zsh_autocomplete` file included in this repo. One environment variable is used, `PROG`. Set `PROG` to the program name as before, and then `source path/to/autocomplete/zsh_autocomplete`. Adding the following lines to your ZSH configuration file (usually `.zshrc`) will allow the auto-completion to persist across new shells: ```sh-session $ PROG= $ source path/to/autocomplete/zsh_autocomplete ``` #### ZSH default auto-complete example ![](../images/default-zsh-autocomplete.gif) #### ZSH custom auto-complete example ![](../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 `.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: ```powershell & path/to/autocomplete/.ps1 ``` To persist across new shells, open the PowerShell profile (with `code $profile` or `notepad $profile`) and add the line: ```powershell & path/to/autocomplete/.ps1 ``` ================================================ FILE: docs/v2/examples/combining-short-options.md ================================================ --- tags: - v2 search: boost: 2 --- Traditional use of options using their shortnames look like this: ```sh-session $ cmd -s -o -m "Some message" ``` Suppose you want users to be able to combine options with their shortnames. This can be done using the `UseShortOptionHandling` bool in your app configuration, or for individual commands by attaching it to the command configuration. For example: ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ UseShortOptionHandling: true, Commands: []*cli.Command{ { Name: "short", Usage: "complete a task on the list", Flags: []cli.Flag{ &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}}, &cli.BoolFlag{Name: "option", Aliases: []string{"o"}}, &cli.StringFlag{Name: "message", Aliases: []string{"m"}}, }, Action: func(cCtx *cli.Context) error { fmt.Println("serve:", cCtx.Bool("serve")) fmt.Println("option:", cCtx.Bool("option")) fmt.Println("message:", cCtx.String("message")) return nil }, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` If your program has any number of bool flags such as `serve` and `option`, and optionally one non-bool flag `message`, with the short options of `-s`, `-o`, and `-m` respectively, setting `UseShortOptionHandling` will also support the following syntax: ```sh-session $ cmd -som "Some message" ``` If you enable `UseShortOptionHandling`, then you must not use any flags that have a single leading `-` or this will result in failures. For example, `-option` can no longer be used. Flags with two leading dashes (such as `--options`) are still valid. ================================================ FILE: docs/v2/examples/exit-codes.md ================================================ --- tags: - v2 search: boost: 2 --- Calling `App.Run` will not automatically call `os.Exit`, which means that by default the exit code will "fall through" to being `0`. An explicit exit code may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a `cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.BoolFlag{ Name: "ginger-crouton", Usage: "is it in the soup?", }, }, Action: func(ctx *cli.Context) error { if !ctx.Bool("ginger-crouton") { return cli.Exit("Ginger croutons are not in the soup", 86) } return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v2/examples/flags.md ================================================ --- tags: - v2 search: boost: 2 --- Setting and querying flags is simple. ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, }, Action: func(cCtx *cli.Context) error { name := "Nefertiti" if cCtx.NArg() > 0 { name = cCtx.Args().Get(0) } if cCtx.String("lang") == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` You can also set a destination variable for a flag, to which the content will be scanned. Note that if the `Value` is set for the flag, it will be shown as default, and destination will be set to this value before parsing flag on the command line. ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { var language string app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Destination: &language, }, }, Action: func(cCtx *cli.Context) error { name := "someone" if cCtx.NArg() > 0 { name = cCtx.Args().Get(0) } if language == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` See full list of flags at https://pkg.go.dev/github.com/urfave/cli/v2 For bool flags you can specify the flag multiple times to get a count(e.g -v -v -v or -vvv) > If you want to support the `-vvv` flag, you need to set `App.UseShortOptionHandling`. ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { var count int app := &cli.App{ UseShortOptionHandling: true, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "foo", Usage: "foo greeting", Aliases: []string{"f"}, Count: &count, }, }, Action: func(cCtx *cli.Context) error { fmt.Println("count", count) return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` #### Placeholder Values Sometimes it's useful to specify a flag's value within the usage string itself. Such placeholders are indicated with back quotes. For example this: ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, Usage: "Load configuration from `FILE`", }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --config FILE, -c FILE Load configuration from FILE ``` Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. #### Alternate Names You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. #### Multiple Values per Single Flag Using a slice flag allows you to pass multiple values for a single flag; the values will be provided as a slice: - `Int64SliceFlag` - `IntSliceFlag` - `StringSliceFlag` ```go package main import ( "fmt" "log" "os" "strings" "github.com/urfave/cli/v3" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "greeting", Usage: "Pass multiple greetings", }, }, Action: func(cCtx *cli.Context) error { fmt.Println(strings.Join(cCtx.StringSlice("greeting"), `, `)) return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Multiple values need to be passed as separate, repeating flags, e.g. `--greeting Hello --greeting Hola`. #### Ordering Flags for the application and commands are shown in the order they are defined. However, it's possible to sort them from outside this library by using `FlagsByName` or `CommandsByName` with `sort`. For example this: ```go package main import ( "log" "os" "sort" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "Language for the greeting", }, &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, Usage: "Load configuration from `FILE`", }, }, Commands: []*cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(*cli.Context) error { return nil }, }, { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(*cli.Context) error { return nil }, }, }, } sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --config FILE, -c FILE Load configuration from FILE --lang value, -l value Language for the greeting (default: "english") ``` #### Values from the Environment You can also have the default value set from the environment via `EnvVars`. e.g. ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", EnvVars: []string{"APP_LANG"}, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` If `EnvVars` contains more than one string, the first environment variable that resolves is used. ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", EnvVars: []string{"LEGACY_COMPAT_LANG", "APP_LANG", "LANG"}, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` #### Values from files You can also have the default value set from file via `FilePath`. e.g. ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, Usage: "password for the mysql database", FilePath: "/etc/mysql/password", }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Note that default values set from file (e.g. `FilePath`) take precedence over default values set from the environment (e.g. `EnvVar`). #### Values from alternate input sources (YAML, TOML, and others) There is a separate package altsrc that adds support for getting flag values from other file input sources. Currently supported input source formats: - YAML - JSON - TOML In order to get values for a flag from an alternate input source the following code would be added to wrap an existing cli.Flag like below: ```go // --- >8 --- altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}) ``` Initialization must also occur for these flags. Below is an example initializing getting data from a yaml file below. ```go // --- >8 --- command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) ``` The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. It will then use that file name to initialize the yaml input source for any flags that are defined on that command. As a note the "load" flag used would also have to be defined on the command flags in order for this code snippet to work. Currently only YAML, JSON, and TOML files are supported but developers can add support for other input sources by implementing the altsrc.InputSourceContext for their given sources. Here is a more complete sample of a command using YAML support: ```go package main import ( "fmt" "os" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" ) func main() { flags := []cli.Flag{ altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}), &cli.StringFlag{Name: "load"}, } app := &cli.App{ Action: func(*cli.Context) error { fmt.Println("--test value.*default: 0") return nil }, Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), Flags: flags, } app.Run(os.Args) } ``` #### Required Flags You can make a flag required by setting the `Required` field to `true`. If a user does not provide a required flag, they will be shown an error message. Take for example this app that requires the `lang` flag: ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Required: true, }, }, Action: func(cCtx *cli.Context) error { output := "Hello" if cCtx.String("lang") == "spanish" { output = "Hola" } fmt.Println(output) return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` If the app is run without the `lang` flag, the user will see the following message ``` Required flag "lang" not set ``` #### Default Values for help output Sometimes it's useful to specify a flag's default help-text value within the flag declaration. This can be useful if the default value for a flag is a computed value. The default value can be set via the `DefaultText` struct field. For example this: ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Usage: "Use a randomized port", Value: 0, DefaultText: "random", }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --port value Use a randomized port (default: random) ``` #### Precedence The precedence for flag value sources is as follows (highest to lowest): 0. Command line flag value from user 0. Environment variable (if specified) 0. Configuration file (if specified) 0. Default defined on the flag #### Flag Actions Handlers can be registered per flag which are triggered after a flag has been processed. This can be used for a variety of purposes, one of which is flag validation ```go package main import ( "log" "os" "fmt" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Usage: "Use a randomized port", Value: 0, DefaultText: "random", Action: func(ctx *cli.Context, v int) error { if v >= 65536 { return fmt.Errorf("Flag port value %v out of range[0-65535]", v) } return nil }, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` Flag port value 70000 out of range[0-65535] ``` ================================================ FILE: docs/v2/examples/full-api-example.md ================================================ --- tags: - v2 search: boost: 2 --- **Notice**: This is a contrived (functioning) example meant strictly for API demonstration purposes. Use of one's imagination is encouraged. ```go package main import ( "errors" "flag" "fmt" "io" "io/ioutil" "os" "time" "github.com/urfave/cli/v2" ) func init() { cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" cli.CommandHelpTemplate += "\nYMMV\n" cli.SubcommandHelpTemplate += "\nor something\n" cli.HelpFlag = &cli.BoolFlag{Name: "halp"} cli.VersionFlag = &cli.BoolFlag{Name: "print-version", Aliases: []string{"V"}} cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Fprintf(w, "best of luck to you\n") } cli.VersionPrinter = func(cCtx *cli.Context) { fmt.Fprintf(cCtx.App.Writer, "version=%s\n", cCtx.App.Version) } cli.OsExiter = func(cCtx int) { fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", cCtx) } cli.ErrWriter = ioutil.Discard cli.FlagStringer = func(fl cli.Flag) string { return fmt.Sprintf("\t\t%s", fl.Names()[0]) } } type hexWriter struct{} func (w *hexWriter) Write(p []byte) (int, error) { for _, b := range p { fmt.Printf("%x", b) } fmt.Printf("\n") return len(p), nil } type genericType struct { s string } func (g *genericType) Set(value string) error { g.s = value return nil } func (g *genericType) String() string { return g.s } func main() { app := &cli.App{ Name: "kənˈtrīv", Version: "v19.99.0", Compiled: time.Now(), Authors: []*cli.Author{ &cli.Author{ Name: "Example Human", Email: "human@example.com", }, }, Copyright: "(c) 1999 Serious Enterprise", HelpName: "contrive", Usage: "demonstrate available API", UsageText: "contrive - demonstrating the available API", ArgsUsage: "[args and such]", Commands: []*cli.Command{ &cli.Command{ Name: "doo", Aliases: []string{"do"}, Category: "motion", Usage: "do the doo", UsageText: "doo - does the dooing", Description: "no really, there is a lot of dooing to be done", ArgsUsage: "[arrgh]", Flags: []cli.Flag{ &cli.BoolFlag{Name: "forever", Aliases: []string{"forevvarr"}}, }, Subcommands: []*cli.Command{ &cli.Command{ Name: "wop", Action: wopAction, }, }, SkipFlagParsing: false, HideHelp: false, HideHelpCommand: false, Hidden: false, HelpName: "doo!", BashComplete: func(cCtx *cli.Context) { fmt.Fprintf(cCtx.App.Writer, "--better\n") }, Before: func(cCtx *cli.Context) error { fmt.Fprintf(cCtx.App.Writer, "brace for impact\n") return nil }, After: func(cCtx *cli.Context) error { fmt.Fprintf(cCtx.App.Writer, "did we lose anyone?\n") return nil }, Action: func(cCtx *cli.Context) error { cCtx.Command.FullName() cCtx.Command.HasName("wop") cCtx.Command.Names() cCtx.Command.VisibleFlags() fmt.Fprintf(cCtx.App.Writer, "dodododododoodododddooooododododooo\n") if cCtx.Bool("forever") { cCtx.Command.Run(cCtx) } return nil }, OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { fmt.Fprintf(cCtx.App.Writer, "for shame\n") return err }, }, }, Flags: []cli.Flag{ &cli.BoolFlag{Name: "fancy"}, &cli.BoolFlag{Value: true, Name: "fancier"}, &cli.DurationFlag{Name: "howlong", Aliases: []string{"H"}, Value: time.Second * 3}, &cli.Float64Flag{Name: "howmuch"}, &cli.GenericFlag{Name: "wat", Value: &genericType{}}, &cli.Int64Flag{Name: "longdistance"}, &cli.Int64SliceFlag{Name: "intervals"}, &cli.IntFlag{Name: "distance"}, &cli.IntSliceFlag{Name: "times"}, &cli.StringFlag{Name: "dance-move", Aliases: []string{"d"}}, &cli.StringSliceFlag{Name: "names", Aliases: []string{"N"}}, &cli.UintFlag{Name: "age"}, &cli.Uint64Flag{Name: "bigage"}, }, EnableBashCompletion: true, HideHelp: false, HideHelpCommand: false, HideVersion: false, BashComplete: func(cCtx *cli.Context) { fmt.Fprintf(cCtx.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") }, Before: func(cCtx *cli.Context) error { fmt.Fprintf(cCtx.App.Writer, "HEEEERE GOES\n") return nil }, After: func(cCtx *cli.Context) error { fmt.Fprintf(cCtx.App.Writer, "Phew!\n") return nil }, CommandNotFound: func(cCtx *cli.Context, command string) { fmt.Fprintf(cCtx.App.Writer, "Thar be no %q here.\n", command) }, OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { if isSubcommand { return err } fmt.Fprintf(cCtx.App.Writer, "WRONG: %#v\n", err) return nil }, Action: func(cCtx *cli.Context) error { cli.DefaultAppComplete(cCtx) cli.HandleExitCoder(errors.New("not an exit coder, though")) cli.ShowAppHelp(cCtx) cli.ShowCommandCompletions(cCtx, "nope") cli.ShowCommandHelp(cCtx, "also-nope") cli.ShowCompletions(cCtx) cli.ShowSubcommandHelp(cCtx) cli.ShowVersion(cCtx) fmt.Printf("%#v\n", cCtx.App.Command("doo")) if cCtx.Bool("infinite") { cCtx.App.Run([]string{"app", "doo", "wop"}) } if cCtx.Bool("forevar") { cCtx.App.RunAsSubcommand(cCtx) } cCtx.App.Setup() fmt.Printf("%#v\n", cCtx.App.VisibleCategories()) fmt.Printf("%#v\n", cCtx.App.VisibleCommands()) fmt.Printf("%#v\n", cCtx.App.VisibleFlags()) fmt.Printf("%#v\n", cCtx.Args().First()) if cCtx.Args().Len() > 0 { fmt.Printf("%#v\n", cCtx.Args().Get(1)) } fmt.Printf("%#v\n", cCtx.Args().Present()) fmt.Printf("%#v\n", cCtx.Args().Tail()) set := flag.NewFlagSet("contrive", 0) nc := cli.NewContext(cCtx.App, set, cCtx) fmt.Printf("%#v\n", nc.Args()) fmt.Printf("%#v\n", nc.Bool("nope")) fmt.Printf("%#v\n", !nc.Bool("nerp")) fmt.Printf("%#v\n", nc.Duration("howlong")) fmt.Printf("%#v\n", nc.Float64("hay")) fmt.Printf("%#v\n", nc.Generic("bloop")) fmt.Printf("%#v\n", nc.Int64("bonk")) fmt.Printf("%#v\n", nc.Int64Slice("burnks")) fmt.Printf("%#v\n", nc.Int("bips")) fmt.Printf("%#v\n", nc.IntSlice("blups")) fmt.Printf("%#v\n", nc.String("snurt")) fmt.Printf("%#v\n", nc.StringSlice("snurkles")) fmt.Printf("%#v\n", nc.Uint("flub")) fmt.Printf("%#v\n", nc.Uint64("florb")) fmt.Printf("%#v\n", nc.FlagNames()) fmt.Printf("%#v\n", nc.IsSet("wat")) fmt.Printf("%#v\n", nc.Set("wat", "nope")) fmt.Printf("%#v\n", nc.NArg()) fmt.Printf("%#v\n", nc.NumFlags()) fmt.Printf("%#v\n", nc.Lineage()[1]) nc.Set("wat", "also-nope") ec := cli.Exit("ohwell", 86) fmt.Fprintf(cCtx.App.Writer, "%d", ec.ExitCode()) fmt.Printf("made it!\n") return ec }, Metadata: map[string]interface{}{ "layers": "many", "explicable": false, "whatever-values": 19.99, }, } if os.Getenv("HEXY") != "" { app.Writer = &hexWriter{} app.ErrWriter = &hexWriter{} } app.Run(os.Args) } func wopAction(cCtx *cli.Context) error { fmt.Fprintf(cCtx.App.Writer, ":wave: over here, eh\n") return nil } ``` ================================================ FILE: docs/v2/examples/generated-help-text.md ================================================ --- tags: - v2 search: boost: 2 --- The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked by the cli internals in order to print generated help text for the app, command, or subcommand, and break execution. #### Customization All of the help text generation may be customized, and at multiple levels. The templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and `SubcommandHelpTemplate` which may be reassigned or augmented, and full override is possible by assigning a compatible func to the `cli.HelpPrinter` variable, e.g.: ```go package main import ( "fmt" "io" "os" "github.com/urfave/cli/v2" ) func main() { // EXAMPLE: Append to an existing template cli.AppHelpTemplate = fmt.Sprintf(`%s WEBSITE: http://awesometown.example.com SUPPORT: support@awesometown.example.com `, cli.AppHelpTemplate) // EXAMPLE: Override a template cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: {{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} COPYRIGHT: {{.Copyright}} {{end}}{{if .Version}} VERSION: {{.Version}} {{end}} ` // EXAMPLE: Replace the `HelpPrinter` func cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Println("Ha HA. I pwnd the help!!1") } (&cli.App{}).Run(os.Args) } ``` The default flag may be customized to something other than `-h/--help` by setting `cli.HelpFlag`, e.g.: ```go package main import ( "os" "github.com/urfave/cli/v2" ) func main() { cli.HelpFlag = &cli.BoolFlag{ Name: "haaaaalp", Aliases: []string{"halp"}, Usage: "HALP", EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, } (&cli.App{}).Run(os.Args) } ``` ================================================ FILE: docs/v2/examples/greet.md ================================================ --- tags: - v2 search: boost: 2 --- Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness! Start by creating a directory named `greet`, and within it, add a file, `greet.go` with the following code in it: ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Name: "greet", Usage: "fight the loneliness!", Action: func(*cli.Context) error { fmt.Println("Hello friend!") return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Install our command to the `$GOPATH/bin` directory: ```sh-session $ go install ``` Finally run our new command: ```sh-session $ greet Hello friend! ``` cli also generates neat help text: ```sh-session $ greet help NAME: greet - fight the loneliness! USAGE: greet [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS --help, -h show help (default: false) ``` ================================================ FILE: docs/v2/examples/subcommands-categories.md ================================================ --- tags: - v2 search: boost: 2 --- For additional organization in apps that have many subcommands, you can associate a category for each command to group them together in the help output, e.g.: ```go package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "noop", }, { Name: "add", Category: "template", }, { Name: "remove", Category: "template", }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Will include: ``` COMMANDS: noop Template actions: add remove ``` ================================================ FILE: docs/v2/examples/subcommands.md ================================================ --- tags: - v2 search: boost: 2 --- Subcommands can be defined for a more git-like command line app. ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(cCtx *cli.Context) error { fmt.Println("added task: ", cCtx.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(cCtx *cli.Context) error { fmt.Println("completed task: ", cCtx.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Subcommands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(cCtx *cli.Context) error { fmt.Println("new task template: ", cCtx.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(cCtx *cli.Context) error { fmt.Println("removed task template: ", cCtx.Args().First()) return nil }, }, }, }, }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v2/examples/suggestions.md ================================================ --- tags: - v2 search: boost: 2 --- To enable flag and command suggestions, set `app.Suggest = true`. If the suggest feature is enabled, then the help output of the corresponding command will provide an appropriate suggestion for the provided flag or subcommand if available. ================================================ FILE: docs/v2/examples/timestamp-flag.md ================================================ --- tags: - v2 search: boost: 2 --- Using the timestamp flag is simple. Please refer to [`time.Parse`](https://golang.org/pkg/time/#example_Parse) to get possible formats. ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.TimestampFlag{Name: "meeting", Layout: "2006-01-02T15:04:05"}, }, Action: func(cCtx *cli.Context) error { fmt.Printf("%s", cCtx.Timestamp("meeting").String()) return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` In this example the flag could be used like this: ```sh-session $ myapp --meeting 2019-08-12T15:04:05 ``` When the layout doesn't contain timezones, timestamp will render with UTC. To change behavior, a default timezone can be provided with flag definition: ```go app := &cli.App{ Flags: []cli.Flag{ &cli.TimestampFlag{Name: "meeting", Layout: "2006-01-02T15:04:05", Timezone: time.Local}, }, } ``` (time.Local contains the system's local time zone.) Side note: quotes may be necessary around the date depending on your layout (if you have spaces for instance) ================================================ FILE: docs/v2/examples/version-flag.md ================================================ --- tags: - v2 search: boost: 2 --- The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which is checked by the cli internals in order to print the `App.Version` via `cli.VersionPrinter` and break execution. #### Customization The default flag may be customized to something other than `-v/--version` by setting `cli.VersionFlag`, e.g.: ```go package main import ( "os" "github.com/urfave/cli/v2" ) func main() { cli.VersionFlag = &cli.BoolFlag{ Name: "print-version", Aliases: []string{"V"}, Usage: "print only the version", } app := &cli.App{ Name: "partay", Version: "v19.99.0", } app.Run(os.Args) } ``` Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: ```go package main import ( "fmt" "os" "github.com/urfave/cli/v2" ) var ( Revision = "fafafaf" ) func main() { cli.VersionPrinter = func(cCtx *cli.Context) { fmt.Printf("version=%s revision=%s\n", cCtx.App.Version, Revision) } app := &cli.App{ Name: "partay", Version: "v19.99.0", } app.Run(os.Args) } ``` ================================================ FILE: docs/v2/getting-started.md ================================================ --- tags: - v2 search: boost: 2 --- One of the philosophies behind cli is that an API should be playful and full of discovery. So a cli app can be as little as one line of code in `main()`. ```go package main import ( "os" "github.com/urfave/cli/v2" ) func main() { (&cli.App{}).Run(os.Args) } ``` This app will run and show help text, but is not very useful. ``` $ wl-paste > hello.go $ go build hello.go $ ./hello NAME: hello - A new cli application USAGE: hello [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) ``` Let's add an action to execute and some help documentation: ```go package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Name: "boom", Usage: "make an explosive entrance", Action: func(*cli.Context) error { fmt.Println("boom! I say!") return nil }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } ``` Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below. ================================================ FILE: docs/v2/migrating-from-older-releases.md ================================================ --- tags: - v2 search: boost: 2 --- There are a small set of breaking changes between v1 and v2. Converting is relatively straightforward and typically takes less than an hour. Specific steps are included in [Migration Guide: v1 to v2](../migrate-v1-to-v2.md). Also see the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v2) for v2 API documentation. ================================================ FILE: docs/v2/migrating-to-v3.md ================================================ --- tags: - v2 --- There are a small set of breaking changes between v2 and v3. Converting is relatively straightforward and typically takes less than an hour. Specific steps are included in [Migration Guide: v2 to v3](../migrate-v2-to-v3.md). ================================================ FILE: docs/v3/examples/arguments/advanced.md ================================================ --- tags: - v3 search: boost: 2 --- The [Basics] showed how to access arguments for a command. They are all retrieved as strings which is fine but it we need to say get integers or timestamps the user would have to convert from string to desired type. To ease the burden on users the `cli` library offers predefined `{Type}Arg` and `{Type}Args` structure to faciliate this. The value of the argument can be retrieved using the `command.{Type}Arg()` function. For e.g ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Arguments: []cli.Argument{ &cli.IntArg{ Name: "someint", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Printf("We got %d", cmd.IntArg("someint")) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Running this program with an argument gives the following output ```sh-session $ greet 10 We got 10 ``` Instead of using the `cmd.{Type}Arg()` function to retrieve the argument value a destination for the argument can be set for e.g ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { var ival int cmd := &cli.Command{ Arguments: []cli.Argument{ &cli.IntArg{ Name: "someint", Destination: &ival, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Printf("We got %d", ival) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Some of the basic types arguments suported are - `FloatArg` - `IntArg` - `Int8Arg` - `Int16Arg` - `Int32Arg` - `Int64Arg` - `StringArg` - `UintArg` - `Uint8Arg` - `Uint16Arg` - `Uint32Arg` - `Uint64Arg` - `TimestampArg` This is ok for single value arguments. Any number of these single value arguments can be concatenated in the `Arguments` slice field of `Command`. The library also support multi value arguments for e.g ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Arguments: []cli.Argument{ &cli.IntArgs{ Name: "someint", Min: 0, Max: -1, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("We got ", cmd.IntArgs("someint")) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Some things to note about multi value arguments 1. They are of `{Type}Args` type rather than `{Type}Arg` to differentiate them from single value arguments. 2. The `Max` field needs to be defined to a non zero value without which it cannot be parsed. 3. `Max` field value needs to be greater than the `Min` field value. As with single value args the destination field can be set ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { var ivals []int cmd := &cli.Command{ Arguments: []cli.Argument{ &cli.IntArgs{ Name: "someint", Min: 0, Max: -1, Destination: &ivals, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("We got ", ivals) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Following multi value arguments are supported - `FloatArgs` - `IntArgs` - `Int8Args` - `Int16Args` - `Int32Args` - `Int64Args` - `StringArgs` - `UintArgs` - `Uint8Args` - `Uint16Args` - `Uint32Args` - `Uint64Args` - `TimestampArgs` It goes without saying that the chain of arguments set in the Arguments slice need to be consistent. Generally a glob argument(`max=-1`) should be set for the argument at the end of the slice. To glob args we arent interested in we coud add the following to the end of the Arguments slice and retrieve them as a slice ``` &StringArgs{ Max: -1, }, ``` ================================================ FILE: docs/v3/examples/arguments/basics.md ================================================ --- tags: - v3 search: boost: 2 --- Lets add some arguments to our greeter app. This allows you to change the behaviour of the app depending on what argument has been passed. You can lookup arguments by calling the `Args` function on `cli.Command`, e.g.: ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Printf("Hello %q", cmd.Args().Get(0)) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Running this program with an argument gives the following output ```sh-session $ greet friend Hello "Friend" ``` Any number of arguments can be passed to the greeter app. We can get the number of arguments and each argument using the `Args` ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Printf("Number of args : %d\n", cmd.Args().Len()) var out string for i := 0; i < cmd.Args().Len(); i++ { out = out + fmt.Sprintf(" %v", cmd.Args().Get(i)) } fmt.Printf("Hello%v", out) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Running this program with an argument gives the following output ```sh-session $ greet Friend 1 bar 2.0 Number of args : 4 Hello Friend 1 bar 2.0 ``` ================================================ FILE: docs/v3/examples/completions/customizations.md ================================================ --- tags: - v3 search: boost: 2 --- If default completion isn't sufficient additional customizations are available - custom auto-completion - customizing completion command #### Custom auto-completion ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} cmd := &cli.Command{ EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("completed task: ", cmd.Args().First()) return nil }, ShellComplete: func(ctx context.Context, cmd *cli.Command) { // This will complete if no args are passed if cmd.NArg() > 0 { return } for _, t := range tasks { fmt.Println(t) } }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` ![](../../images/custom-bash-autocomplete.gif) #### Customize a completion command By default, a completion command is hidden, meaning the command isn't included in the help message. You can customize it by setting root Command's `ConfigureShellCompletionCommand`. ```go package main import ( "context" "fmt" "log" "os" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "greet", // EnableShellCompletion is unnecessary ConfigureShellCompletionCommand: func(cmd *cli.Command) { // cmd is a completion command cmd.Hidden = false // Make a completion command public cmd.Usage = "..." // Customize Usage cmd.Description = "..." // Customize Description }, Commands: []*cli.Command{ { Name: "hello", Usage: "Say hello", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("Hello") return nil }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` #### Customization The default shell completion flag (`--generate-shell-completion`) is defined as `cli.EnableShellCompletion`, and may be redefined if desired, e.g.: ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "wat", }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v3/examples/completions/shell-completions.md ================================================ --- tags: - v3 search: boost: 2 --- The urfave/cli v3 library supports programmable completion for apps utilizing its framework. This means that the completion is generated dynamically at runtime by invokiong the app itself with a special hidden flag. The urfave/cli searches for this flag and activates a different flow for command paths than regular flow The following shells are supported - bash - zsh - fish - powershell Enabling auto complete requires 2 things - Setting the `EnableShellCompletion` field on root `Command` object to `true`. - Sourcing the completion script for that particular shell. The completion script for a particular shell can be retrieved by running the "completion" subcommand on the app after the `EnableShellCompletion` field on root `Command` object has been set to `true`. Consider the following program ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("added task: ", cmd.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("completed task: ", cmd.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Commands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("new task template: ", cmd.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("removed task template: ", cmd.Args().First()) return nil }, }, }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` After compiling this app as `greet` we can generate the autocompletion as following in bash script ```sh-session $ greet completion bash ``` This file can be saved to /etc/bash_completion.d/greet or $HOME/.bash_completion.d/greet where it will be automatically picked in new bash shells. For the current shell these can be sourced either using filename or from generation command directly ```sh-session $ source ~/.bash_completion.d/greet ``` ```sh-session $ source <(greet completion bash) ``` The procedure for other shells is similar to bash though the specific paths for each of the shells may vary. Some of the sections below detail the setup need for other shells as well as examples in those shells. #### Default auto-completion ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("added task: ", cmd.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("completed task: ", cmd.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Commands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("new task template: ", cmd.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("removed task template: ", cmd.Args().First()) return nil }, }, }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` ![](../../images/default-bash-autocomplete.gif) #### ZSH Support Adding the following lines to your ZSH configuration file (usually `.zshrc`) will allow the auto-completion to persist across new shells: ```sh-session $ PROG= $ source path/to/autocomplete/zsh_autocomplete ``` #### ZSH default auto-complete example ![](../../images/default-zsh-autocomplete.gif) #### PowerShell Support Generate the completion script as save it to `.ps1` . This file can be moved to 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: ```powershell & path/to/autocomplete/.ps1 ``` To persist across new shells, open the PowerShell profile (with `code $profile` or `notepad $profile`) and add the line: ```powershell & path/to/autocomplete/.ps1 ``` ================================================ FILE: docs/v3/examples/exit-codes.md ================================================ --- tags: - v3 search: boost: 2 --- Calling `Command.Run` will not automatically call `os.Exit`, which means that by default the exit code will "fall through" to being `0`. An explicit exit code may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a `cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.BoolFlag{ Name: "ginger-crouton", Usage: "is it in the soup?", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { if !cmd.Bool("ginger-crouton") { return cli.Exit("Ginger croutons are not in the soup", 86) } return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v3/examples/flags/advanced.md ================================================ --- tags: - v3 search: boost: 2 --- #### Alternate Names You can set alternate (or short) names for flags by providing a list of strings for `Aliases` e.g. ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. #### Multiple Values per Single Flag As noted in the basics for flag, the simple flags allow only one value per flag and only the last entered value on command line will be returned to user on query. `urfave/cli` also supports multi-value flags called slice flags. These flags can take multiple values of same type. In addition they can be invoked multiple times on the command line and values will be appended to original value of the flag and returned to the user as a slice - `IntSliceFlag` - `Int8SliceFlag` - `Int16SliceFlag` - `Int32SliceFlag` - `Int64SliceFlag` - `UintSliceFlag` - `Uint8SliceFlag` - `Uint16SliceFlag` - `Uint32SliceFlag` - `Uint64SliceFlag` - `StringSliceFlag` - `FloatSliceFlag` ```go package main import ( "fmt" "log" "os" "strings" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "greeting", Usage: "Pass multiple greetings", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println(strings.Join(cmd.StringSlice("greeting"), `, `)) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Multiple values need to be passed as separate, repeating flags, e.g. `--greeting Hello --greeting Hola`. #### Count for bool flag For bool flags you can specify the flag multiple times to get a count(e.g -v -v -v or -vvv) > If you want to support the `-vvv` flag, you need to set `Command.UseShortOptionHandling`. ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { var count int cmd := &cli.Command{ UseShortOptionHandling: true, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "foo", Usage: "foo greeting", Aliases: []string{"f"}, Config: cli.BoolConfig{ Count: &count, }, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("count", count) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` #### Placeholder Values Sometimes it's useful to specify a flag's value within the usage string itself. Such placeholders are indicated with back quotes. For example this: ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, Usage: "Load configuration from `FILE`", }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --config FILE, -c FILE Load configuration from FILE ``` Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. #### Ordering Flags for the application and commands are shown in the order they are defined. However, it's possible to sort them from outside this library by using `FlagsByName` or `CommandsByName` with `sort`. For example this: ```go package main import ( "log" "os" "sort" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "Language for the greeting", }, &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, Usage: "Load configuration from `FILE`", }, }, Commands: []*cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(ctx context.Context, cmd *cli.Command) error { return nil }, }, { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(ctx context.Context, cmd *cli.Command) error { return nil }, }, }, } sort.Sort(cli.FlagsByName(cmd.Flags)) if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --config FILE, -c FILE Load configuration from FILE --lang value, -l value Language for the greeting (default: "english") ``` #### Required Flags You can mark a flag as *required* by setting the `Required` field to `true`. If a user does not provide a required flag, they will be shown an error message. Take for example this app that requires the `lang` flag: ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Required: true, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { output := "Hello" if cmd.String("lang") == "spanish" { output = "Hola" } fmt.Println(output) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` If the command is run without the `lang` flag, the user will see the following message ``` Required flag "lang" not set ``` #### Flag Groups You can make groups of flags that are mutually exclusive of each other. This provides the ability to provide configuration options out of which only one can be defined on the command line. Take for example this app that looks up a user using one of multiple options: ```go package main import ( "context" "encoding/json" "fmt" "log" "os" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "authors", MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{ { Required: true, Flags: [][]cli.Flag{ { &cli.StringFlag{ Name: "login", Usage: "the username of the user", }, }, { &cli.StringFlag{ Name: "id", Usage: "the user id (defaults to 'me' for current user)", }, }, }, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { u, err := getUser(ctx, cmd) if err != nil { return err } data, err := json.Marshal(u) if err != nil { return err } fmt.Println(string(data)) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } type User struct { Id string `json:"id"` Login string `json:"login"` FirstName string `json:"firstName"` LastName string `json:"lastName"` } // Mock function that returns a static user value. // Would retrieve a user from an API or database with other functions. func getUser(ctx context.Context, cmd *cli.Command) (User, error) { u := User{ Id: "abc123", Login: "vwoolf@example.com", FirstName: "Virginia", LastName: "Woolf", } if login := cmd.String("login"); login != "" { fmt.Printf("Getting user by login: %s\n", login) u.Login = login } if id := cmd.String("id"); id != "" { fmt.Printf("Getting user by id: %s\n", id) u.Id = id } return u, nil } ``` If the command is run without either the `login` or `id` flag, the user will see the following message ``` one of these flags needs to be provided: login, id ``` #### Default Values for help output Sometimes it's useful to specify a flag's default help-text value within the flag declaration. This can be useful if the default value for a flag is a computed value. The default value can be set via the `DefaultText` struct field. For example this: ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Usage: "Use a randomized port", Value: 0, DefaultText: "random", }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` --port value Use a randomized port (default: random) ``` #### Flag Actions Handlers can be registered per flag which are triggered after a flag has been processed. This can be used for a variety of purposes, one of which is flag validation ```go package main import ( "log" "os" "fmt" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Usage: "Use a randomized port", Value: 0, DefaultText: "random", Action: func(ctx context.Context, cmd *cli.Command, v int) error { if v >= 65536 { return fmt.Errorf("Flag port value %v out of range[0-65535]", v) } return nil }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Will result in help output like: ``` Flag port value 70000 out of range[0-65535] ``` ================================================ FILE: docs/v3/examples/flags/basics.md ================================================ --- tags: - v3 search: boost: 2 --- Flags, also called options, can be used to control various behaviour of the app by turning on/off capabilities or setting some configuration and so on. Setting and querying flags is done using the ```cmd.()``` function Here is an example of using a StringFlag which accepts a string as its option value ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { name := "Nefertiti" if cmd.NArg() > 0 { name = cmd.Args().Get(0) } if cmd.String("lang") == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` This very simple program gives a lot of outputs depending on the value of the flag set. ```sh-session $ greet Hello Nefertiti ``` Note that the Value for the flag is the default value that will be used when the flag is not set on the command line. Since in the above invocation no flag was specified the value of the "lang" flag was default to "english". Now lets change the language ```sh-session $ greet --lang spanish Hola Nefertiti ``` Flag values can be provided with a space after the flag name or using the ```=``` sign ```sh-session $ greet --lang=spanish Hola Nefertiti $ greet --lang=spanish my-friend Hola my-friend ``` While the value of any flag can be retrieved using ```command.``` sometimes it is convenient to have the value of the flag automatically stored in a destination variable for a flag. If the `Value` is set for the flag, it will be shown as default, and destination will be set to this value before parsing flag on the command line. ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { var language string cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Destination: &language, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { name := "someone" if cmd.NArg() > 0 { name = cmd.Args().Get(0) } if language == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Note that most flag can be invoked multiple times but only the last value entered for the flag will be provided to the user(with some exceptions. See flags-advanced.md) The following basic flags are supported - `IntFlag` - `Int8Flag` - `Int16Flag` - `Int32Flag` - `Int64Flag` - `UintFlag` - `Uint8Flag` - `Uint16Flag` - `Uint32Flag` - `Uint64Flag` - `BoolFlag` - `DurationFlag` - `FloatFlag` - `Float32Flag` - `Float64Flag` - `StringFlag` - `TimestampFlag` For full list of flags see [`https://pkg.go.dev/github.com/urfave/cli/v3`](https://pkg.go.dev/github.com/urfave/cli/v3) ### Timestamp Flag ### Using the timestamp flag is similar to other flags but special attention is need for the format to be provided to the flag . Please refer to [`time.Parse`](https://golang.org/pkg/time/#example_Parse) to get possible formats. ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.TimestampFlag{ Name: "meeting", Config: cli.TimestampConfig{ Layouts: []string{"2006-01-02T15:04:05"}, }, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Printf("%s", cmd.Timestamp("meeting").String()) return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` In this example the flag could be used like this: ```sh-session $ myapp --meeting 2019-08-12T15:04:05 ``` When the layout doesn't contain timezones, timestamp will render with UTC. To change behavior, a default timezone can be provided with flag definition: ```go cmd := &cli.Command{ Flags: []cli.Flag{ &cli.TimestampFlag{ Name: "meeting", Config: cli.TimestampConfig{ Timezone: time.Local, AvailableLayouts: []string{"2006-01-02T15:04:05"}, }, }, }, } ``` (time.Local contains the system's local time zone.) Side note: quotes may be necessary around the date depending on your layout (if you have spaces for instance) ### Version Flags ### A default version flag (`-v/--version`) is provided as `cli.VersionFlag`, which is checked by the cli internals in order to print the `Command.Version` via `cli.VersionPrinter` and break execution. #### Customization The default flag may be customized to something other than `-v/--version` by setting fields of `cli.VersionFlag`, e.g.: ```go package main import ( "os" "context" "github.com/urfave/cli/v3" ) func main() { cli.VersionFlag = &cli.BoolFlag{ Name: "print-version", Aliases: []string{"V"}, Usage: "print only the version", } cmd := &cli.Command{ Name: "partay", Version: "v19.99.0", } cmd.Run(context.Background(), os.Args) } ``` Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: ```go package main import ( "fmt" "os" "context" "github.com/urfave/cli/v3" ) var ( Revision = "fafafaf" ) func main() { cli.VersionPrinter = func(cmd *cli.Command) { fmt.Printf("version=%s revision=%s\n", cmd.Root().Version, Revision) } cmd := &cli.Command{ Name: "partay", Version: "v19.99.0", } cmd.Run(context.Background(), os.Args) } ``` ================================================ FILE: docs/v3/examples/flags/short-options.md ================================================ --- tags: - v3 search: boost: 2 --- Traditional use of options using their shortnames look like this: ```sh-session $ cmd -s -o -m "Some message" ``` Suppose you want users to be able to combine options with their shortnames. This can be done using the `UseShortOptionHandling` bool in your app configuration, or for individual commands by attaching it to the command configuration. For example: ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ UseShortOptionHandling: true, Commands: []*cli.Command{ { Name: "short", Usage: "complete a task on the list", Flags: []cli.Flag{ &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}}, &cli.BoolFlag{Name: "option", Aliases: []string{"o"}}, &cli.StringFlag{Name: "message", Aliases: []string{"m"}}, }, Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("serve:", cmd.Bool("serve")) fmt.Println("option:", cmd.Bool("option")) fmt.Println("message:", cmd.String("message")) return nil }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` If your program has any number of bool flags such as `serve` and `option`, and optionally one non-bool flag `message`, with the short options of `-s`, `-o`, and `-m` respectively, setting `UseShortOptionHandling` will also support the following syntax: ```sh-session $ cmd -som "Some message" ``` If you enable `UseShortOptionHandling`, then you must not use any flags that have a single leading `-` or this will result in failures. For example, `-option` can no longer be used. Flags with two leading dashes (such as `--options`) are still valid. ================================================ FILE: docs/v3/examples/flags/value-sources.md ================================================ --- tags: - v3 search: boost: 2 --- Flags can have their default values set from different sources. The following sources are provided by default with `urfave/cli` - Environment - Text Files The library also provides a framework for users to plugin their own implementation of value sources to be fetched via other mechanisms(http and so on). In addition there is a `urfave/cli-altsrc` repo which hosts some common value sources to read from files or via http/https. - YAML - JSON - TOML #### Values from the Environment To set a value from the environment use `cli.EnvVars`. e.g. ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", Sources: cli.EnvVars("APP_LANG"), }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` If `cli.EnvVars` contains more than one string, the first environment variable that resolves is used. ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", Sources: cli.EnvVars("LEGACY_COMPAT_LANG", "APP_LANG", "LANG"), }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` #### Values from files You can also have the default value set from file via `cli.File`. e.g. ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, Usage: "password for the mysql database", Sources: cli.Files("/etc/mysql/password"), }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Note that default values are set in the same order as they are defined in the `Sources` param. This allows the user to choose order of priority #### Values from alternate input sources (YAML, TOML, and others) There is a separate package [altsrc](https://github.com/urfave/cli-altsrc) that adds support for getting flag values from other file input sources. Currently supported input source formats by that library are: - YAML - JSON - TOML A simple straight forward usage would be ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" "github.com/urfave/cli-altsrc/v3" yaml "github.com/urfave/cli-altsrc/v3/yaml" ) func main() { cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, Usage: "password for the mysql database", Sources: cli.NewValueSourceChain(yaml.YAML("somekey", altsrc.StringSourcer("/path/to/filename"))), }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Sometime the source name is itself provided by another CLI flag. To allow the library to "lazy-load" the file when needed we use the `altsrc.NewStringPtrSourcer` function to bind the value of the flag to a pointer that is set as a destination of another flag ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" "github.com/urfave/cli-altsrc/v3" yaml "github.com/urfave/cli-altsrc/v3/yaml" ) func main() { var filename string cmd := &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "file", Aliases: []string{"f"}, Value: "/path/to/default", Usage: "filename for mysql database", Destination: &filename, }, &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, Usage: "password for the mysql database", Sources: cli.NewValueSourceChain(yaml.YAML("somekey", altsrc.NewStringPtrSourcer(&filename))), }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v3/examples/full-api-example.md ================================================ --- tags: - v3 search: boost: 2 --- **Notice**: This is a contrived (functioning) example meant strictly for API demonstration purposes. Use of one's imagination is encouraged. ```go package main import ( "context" "errors" "fmt" "io" "io/ioutil" "os" "time" "slices" "github.com/urfave/cli/v3" ) func init() { cli.RootCommandHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" cli.CommandHelpTemplate += "\nYMMV\n" cli.SubcommandHelpTemplate += "\nor something\n" cli.HelpFlag = &cli.BoolFlag{Name: "halp"} cli.VersionFlag = &cli.BoolFlag{Name: "print-version", Aliases: []string{"V"}} cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Fprintf(w, "best of luck to you\n") } cli.VersionPrinter = func(cmd *cli.Command) { fmt.Fprintf(cmd.Root().Writer, "version=%s\n", cmd.Root().Version) } cli.OsExiter = func(cmd int) { fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", cmd) } cli.ErrWriter = ioutil.Discard cli.FlagStringer = func(fl cli.Flag) string { return fmt.Sprintf("\t\t%s", fl.Names()[0]) } } type hexWriter struct{} func (w *hexWriter) Write(p []byte) (int, error) { for _, b := range p { fmt.Printf("%x", b) } fmt.Printf("\n") return len(p), nil } type genericType struct { s string } func (g *genericType) Set(value string) error { g.s = value return nil } func (g *genericType) String() string { return g.s } func main() { cmd := &cli.Command{ Name: "kənˈtrīv", Version: "v19.99.0", /*Authors: []any{ &cli.Author{ Name: "Example Human", Email: "human@example.com", }, },*/ Copyright: "(c) 1999 Serious Enterprise", Usage: "demonstrate available API", UsageText: "contrive - demonstrating the available API", ArgsUsage: "[args and such]", Commands: []*cli.Command{ &cli.Command{ Name: "doo", Aliases: []string{"do"}, Category: "motion", Usage: "do the doo", UsageText: "doo - does the dooing", Description: "no really, there is a lot of dooing to be done", ArgsUsage: "[arrgh]", Flags: []cli.Flag{ &cli.BoolFlag{Name: "forever", Aliases: []string{"forevvarr"}}, }, Commands: []*cli.Command{ &cli.Command{ Name: "wop", Action: wopAction, }, }, SkipFlagParsing: false, HideHelp: false, Hidden: false, ShellComplete: func(ctx context.Context, cmd *cli.Command) { fmt.Fprintf(cmd.Root().Writer, "--better\n") }, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { fmt.Fprintf(cmd.Root().Writer, "brace for impact\n") return nil, nil }, After: func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Root().Writer, "did we lose anyone?\n") return nil }, Action: func(ctx context.Context, cmd *cli.Command) error { cmd.FullName() cmd.HasName("wop") cmd.Names() cmd.VisibleFlags() fmt.Fprintf(cmd.Root().Writer, "dodododododoodododddooooododododooo\n") if cmd.Bool("forever") { cmd.Run(ctx, nil) } return nil }, OnUsageError: func(ctx context.Context, cmd *cli.Command, err error, isSubcommand bool) error { fmt.Fprintf(cmd.Root().Writer, "for shame\n") return err }, }, }, Flags: []cli.Flag{ &cli.BoolFlag{Name: "fancy"}, &cli.BoolFlag{Value: true, Name: "fancier"}, &cli.DurationFlag{Name: "howlong", Aliases: []string{"H"}, Value: time.Second * 3}, &cli.FloatFlag{Name: "howmuch"}, &cli.IntFlag{Name: "longdistance", Validator: func (t int) error { if t < 10 { return fmt.Errorf("10 miles isn't long distance!!!!") } return nil }}, &cli.IntSliceFlag{Name: "intervals"}, &cli.StringFlag{Name: "dance-move", Aliases: []string{"d"}, Validator: func(move string) error { moves := []string{"salsa", "tap", "two-step", "lock-step"} if !slices.Contains(moves, move) { return fmt.Errorf("Havent learnt %s move yet", move) } return nil }}, &cli.StringSliceFlag{Name: "names", Aliases: []string{"N"}}, &cli.UintFlag{Name: "age"}, }, EnableShellCompletion: true, HideHelp: false, HideVersion: false, ShellComplete: func(ctx context.Context, cmd *cli.Command) { fmt.Fprintf(cmd.Root().Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") }, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { fmt.Fprintf(cmd.Root().Writer, "HEEEERE GOES\n") return nil, nil }, After: func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Root().Writer, "Phew!\n") return nil }, CommandNotFound: func(ctx context.Context, cmd *cli.Command, command string) { fmt.Fprintf(cmd.Root().Writer, "Thar be no %q here.\n", command) }, OnUsageError: func(ctx context.Context, cmd *cli.Command, err error, isSubcommand bool) error { if isSubcommand { return err } fmt.Fprintf(cmd.Root().Writer, "WRONG: %#v\n", err) return nil }, Action: func(ctx context.Context, cmd *cli.Command) error { cli.DefaultRootCommandComplete(ctx, cmd) cli.HandleExitCoder(errors.New("not an exit coder, though")) cli.ShowRootCommandHelp(cmd) cli.ShowCommandHelp(ctx, cmd, "also-nope") cli.ShowSubcommandHelp(cmd) cli.ShowVersion(cmd) fmt.Printf("%#v\n", cmd.Root().Command("doo")) if cmd.Bool("infinite") { cmd.Root().Run(ctx, []string{"app", "doo", "wop"}) } if cmd.Bool("forevar") { cmd.Root().Run(ctx, nil) } fmt.Printf("%#v\n", cmd.Root().VisibleCategories()) fmt.Printf("%#v\n", cmd.Root().VisibleCommands()) fmt.Printf("%#v\n", cmd.Root().VisibleFlags()) fmt.Printf("%#v\n", cmd.Args().First()) if cmd.Args().Len() > 0 { fmt.Printf("%#v\n", cmd.Args().Get(1)) } fmt.Printf("%#v\n", cmd.Args().Present()) fmt.Printf("%#v\n", cmd.Args().Tail()) ec := cli.Exit("ohwell", 86) fmt.Fprintf(cmd.Root().Writer, "%d", ec.ExitCode()) fmt.Printf("made it!\n") return ec }, Metadata: map[string]interface{}{ "layers": "many", "explicable": false, "whatever-values": 19.99, }, } if os.Getenv("HEXY") != "" { cmd.Writer = &hexWriter{} cmd.ErrWriter = &hexWriter{} } cmd.Run(context.Background(), os.Args) } func wopAction(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Root().Writer, ":wave: over here, eh\n") return nil } ``` ================================================ FILE: docs/v3/examples/greet.md ================================================ --- tags: - v3 search: boost: 2 --- Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness! Start by creating a directory named `greet`, and within it, add a file, `greet.go` with the following code in it: ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "greet", Usage: "fight the loneliness!", Action: func(context.Context, *cli.Command) error { fmt.Println("Hello friend!") return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Install our command to the `$GOPATH/bin` directory: ```sh-session $ go install ``` Finally run our new command: ```sh-session $ greet Hello friend! ``` cli also generates neat help text: ```sh-session $ greet help NAME: greet - fight the loneliness! USAGE: greet [global options] GLOBAL OPTIONS: --help, -h show help ``` In general a full help with flags and subcommands would give something like this ``` NAME: greet - fight the loneliness! USAGE: greet [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS --help, -h show help (default: false) ``` ================================================ FILE: docs/v3/examples/help/generated-help-text.md ================================================ --- tags: - v3 search: boost: 2 --- The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked by the cli internals in order to print generated help text for the app, command, or subcommand, and break execution. #### Customization All of the help text generation may be customized, and at multiple levels. The templates are exposed as variables `RootCommandHelpTemplate`, `CommandHelpTemplate`, and `SubcommandHelpTemplate` which may be reassigned or augmented, and full override is possible by assigning a compatible func to the `cli.HelpPrinter` variable, e.g.: ```go package main import ( "fmt" "io" "os" "context" "github.com/urfave/cli/v3" ) func main() { // EXAMPLE: Append to an existing template cli.RootCommandHelpTemplate = fmt.Sprintf(`%s WEBSITE: http://awesometown.example.com SUPPORT: support@awesometown.example.com `, cli.RootCommandHelpTemplate) // EXAMPLE: Override a template cli.RootCommandHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: {{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} COPYRIGHT: {{.Copyright}} {{end}}{{if .Version}} VERSION: {{.Version}} {{end}} ` // EXAMPLE: Replace the `HelpPrinter` func cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Println("Ha HA. I pwnd the help!!1") } (&cli.Command{}).Run(context.Background(), os.Args) } ``` The default flag may be customized to something other than `-h/--help` by setting `cli.HelpFlag`, e.g.: ```go package main import ( "os" "context" "github.com/urfave/cli/v3" ) func main() { cli.HelpFlag = &cli.BoolFlag{ Name: "haaaaalp", Aliases: []string{"halp"}, Usage: "HALP", Sources: cli.EnvVars("SHOW_HALP", "HALPPLZ"), } (&cli.Command{}).Run(context.Background(), os.Args) } ``` ================================================ FILE: docs/v3/examples/help/suggestions.md ================================================ --- tags: - v3 search: boost: 2 --- To enable flag and command suggestions, set `Command.Suggest = true`. If the suggest feature is enabled, then the help output of the corresponding command will provide an appropriate suggestion for the provided flag or subcommand if available. ================================================ FILE: docs/v3/examples/subcommands/basics.md ================================================ --- tags: - v3 search: boost: 2 --- Subcommands can be defined for a more git-like command line app. ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("added task: ", cmd.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("completed task: ", cmd.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Commands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("new task template: ", cmd.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(ctx context.Context, cmd *cli.Command) error { fmt.Println("removed task template: ", cmd.Args().First()) return nil }, }, }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` ================================================ FILE: docs/v3/examples/subcommands/categories.md ================================================ --- tags: - v3 search: boost: 2 --- For additional organization in apps that have many subcommands, you can associate a category for each command to group them together in the help output, e.g.: ```go package main import ( "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Commands: []*cli.Command{ { Name: "noop", }, { Name: "add", Category: "template", }, { Name: "remove", Category: "template", }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` Will include: ``` COMMANDS: noop template: add remove ``` ================================================ FILE: docs/v3/getting-started.md ================================================ --- tags: - v3 search: boost: 2 --- One of the philosophies behind cli is that an API should be playful and full of discovery. So a cli app can be as little as one line of code in `main()`. ```go package main import ( "os" "context" "github.com/urfave/cli/v3" ) func main() { (&cli.Command{}).Run(context.Background(), os.Args) } ``` This app will run and show help text, but is not very useful. ``` $ wl-paste > hello.go $ go build hello.go $ ./hello NAME: hello - A new cli application USAGE: hello [global options] GLOBAL OPTIONS: --help, -h show help ``` Let's add an action to execute and some help documentation: ```go package main import ( "fmt" "log" "os" "context" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "boom", Usage: "make an explosive entrance", Action: func(context.Context, *cli.Command) error { fmt.Println("boom! I say!") return nil }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } ``` The output of above code is ``` boom! I say! ``` Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered in a separate section. ================================================ FILE: docs/v3/index.md ================================================ # v3 guide ================================================ FILE: docs/v3/migrating-from-older-releases.md ================================================ --- tags: - v3 search: boost: 2 --- There are a small set of breaking changes between v1 and v3. Converting is relatively straightforward and typically takes less than an hour. Specific steps are included in [Migration Guide: v2 to v3](../migrate-v2-to-v3.md). Also see the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v3) for v3 API documentation. ================================================ FILE: docs.go ================================================ package cli import ( "fmt" "os" "runtime" "strings" ) func prefixFor(name string) (prefix string) { if len(name) == 1 { prefix = "-" } else { prefix = "--" } return prefix } // Returns the placeholder, if any, and the unquoted usage string. func unquoteUsage(usage string) (string, string) { for i := 0; i < len(usage); i++ { if usage[i] == '`' { for j := i + 1; j < len(usage); j++ { if usage[j] == '`' { name := usage[i+1 : j] usage = usage[:i] + name + usage[j+1:] return name, usage } } break } } return "", usage } func prefixedNames(names []string, placeholder string) string { var prefixed string for i, name := range names { if name == "" { continue } prefixed += prefixFor(name) + name if placeholder != "" { prefixed += " " + placeholder } if i < len(names)-1 { prefixed += ", " } } return prefixed } func envFormat(envVars []string, prefix, sep, suffix string) string { if len(envVars) > 0 { return fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix) } return "" } func defaultEnvFormat(envVars []string) string { return envFormat(envVars, "$", ", $", "") } func withEnvHint(envVars []string, str string) string { envText := "" if runtime.GOOS != "windows" || os.Getenv("PSHOME") != "" { envText = defaultEnvFormat(envVars) } else { envText = envFormat(envVars, "%", "%, %", "%") } return str + envText } func withFileHint(filePath, str string) string { fileText := "" if filePath != "" { fileText = fmt.Sprintf(" [%s]", filePath) } return str + fileText } func formatDefault(format string) string { return " (default: " + format + ")" } func stringifyFlag(f Flag) string { // enforce DocGeneration interface on flags to avoid reflection df, ok := f.(DocGenerationFlag) if !ok { return "" } placeholder, usage := unquoteUsage(df.GetUsage()) needsPlaceholder := df.TakesValue() // if needsPlaceholder is true, placeholder is empty if needsPlaceholder && placeholder == "" { // try to get type from flag if tname := df.TypeName(); tname != "" { placeholder = tname } else { placeholder = defaultPlaceholder } } defaultValueString := "" // don't print default text for required flags if rf, ok := f.(RequiredFlag); !ok || !rf.IsRequired() { if df.IsDefaultVisible() { if s := df.GetDefaultText(); s != "" { defaultValueString = fmt.Sprintf(formatDefault("%s"), s) } else if df.TakesValue() && df.GetValue() != "" { defaultValueString = fmt.Sprintf(formatDefault("%s"), df.GetValue()) } } } usageWithDefault := strings.TrimSpace(usage + defaultValueString) pn := prefixedNames(f.Names(), placeholder) sliceFlag, ok := f.(DocGenerationMultiValueFlag) if ok && sliceFlag.IsMultiValueFlag() { pn = pn + " [ " + pn + " ]" } return withEnvHint(df.GetEnvVars(), fmt.Sprintf("%s\t%s", pn, usageWithDefault)) } ================================================ FILE: errors.go ================================================ package cli import ( "fmt" "io" "os" "strings" ) // OsExiter is the function used when the app exits. If not set defaults to os.Exit. var OsExiter = os.Exit // ErrWriter is used to write errors to the user. This can be anything // implementing the io.Writer interface and defaults to os.Stderr. var ErrWriter io.Writer = os.Stderr // MultiError is an error that wraps multiple errors. type MultiError interface { error Errors() []error } // newMultiError creates a new MultiError. Pass in one or more errors. func newMultiError(err ...error) MultiError { ret := multiError(err) return &ret } type multiError []error // Error implements the error interface. func (m *multiError) Error() string { errs := make([]string, len(*m)) for i, err := range *m { errs[i] = err.Error() } return strings.Join(errs, "\n") } // Errors returns a copy of the errors slice func (m *multiError) Errors() []error { errs := make([]error, len(*m)) copy(errs, *m) return errs } type requiredFlagsErr interface { error } type errRequiredFlags struct { missingFlags []string } func (e *errRequiredFlags) Error() string { if len(e.missingFlags) == 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) } type mutuallyExclusiveGroup struct { flag1Name string flag2Name string } func (e *mutuallyExclusiveGroup) Error() string { return fmt.Sprintf("option %s cannot be set along with option %s", e.flag1Name, e.flag2Name) } type mutuallyExclusiveGroupRequiredFlag struct { flags *MutuallyExclusiveFlags } func (e *mutuallyExclusiveGroupRequiredFlag) Error() string { var missingFlags []string for _, grpf := range e.flags.Flags { var grpString []string for _, f := range grpf { grpString = append(grpString, f.Names()...) } missingFlags = append(missingFlags, strings.Join(grpString, " ")) } return fmt.Sprintf("one of these flags needs to be provided: %s", strings.Join(missingFlags, ", ")) } // ErrorFormatter is the interface that will suitably format the error output type ErrorFormatter interface { Format(s fmt.State, verb rune) } // ExitCoder is the interface checked by `Command` for a custom exit code. type ExitCoder interface { error ExitCode() int } type exitError struct { exitCode int err error } // Exit wraps a message and exit code into an error, which by default is // handled with a call to os.Exit during default error handling. // // This is the simplest way to trigger a non-zero exit code for a Command without // having to call os.Exit manually. During testing, this behavior can be avoided // by overriding the ExitErrHandler function on a Command or the package-global // OsExiter function. func Exit(message any, exitCode int) ExitCoder { var err error switch e := message.(type) { case ErrorFormatter: err = fmt.Errorf("%+v", message) case error: err = e default: err = fmt.Errorf("%+v", message) } return &exitError{ err: err, exitCode: exitCode, } } func (ee *exitError) Error() string { return ee.err.Error() } func (ee *exitError) ExitCode() int { return ee.exitCode } // HandleExitCoder handles errors implementing ExitCoder by printing their // message and calling OsExiter with the given exit code. // // If the given error instead implements MultiError, each error will be checked // for the ExitCoder interface, and OsExiter will be called with the last exit // code found, or exit code 1 if no ExitCoder is found. // // This function is the default error-handling behavior for a Command. func HandleExitCoder(err error) { if err == nil { return } if exitErr, ok := err.(ExitCoder); ok { if _, ok := exitErr.(ErrorFormatter); ok { _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) } else { _, _ = fmt.Fprintln(ErrWriter, err) } OsExiter(exitErr.ExitCode()) return } if multiErr, ok := err.(MultiError); ok { code := handleMultiError(multiErr) OsExiter(code) return } } func handleMultiError(multiErr MultiError) int { code := 1 for _, merr := range multiErr.Errors() { if multiErr2, ok := merr.(MultiError); ok { code = handleMultiError(multiErr2) } else if merr != nil { fmt.Fprintln(ErrWriter, merr) if exitErr, ok := merr.(ExitCoder); ok { code = exitErr.ExitCode() } } } return code } ================================================ FILE: errors_test.go ================================================ package cli import ( "bytes" "errors" "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestHandleExitCoder_nil(t *testing.T) { exitCode := 0 called := false OsExiter = func(rc int) { if !called { exitCode = rc called = true } } defer func() { OsExiter = fakeOsExiter }() HandleExitCoder(nil) assert.Equal(t, 0, exitCode) assert.False(t, called) } func TestHandleExitCoder_ExitCoder(t *testing.T) { exitCode := 0 called := false OsExiter = func(rc int) { if !called { exitCode = rc called = true } } defer func() { OsExiter = fakeOsExiter }() HandleExitCoder(Exit("galactic perimeter breach", 9)) assert.Equal(t, 9, exitCode) assert.True(t, called) } func TestHandleExitCoder_ErrorExitCoder(t *testing.T) { exitCode := 0 called := false OsExiter = func(rc int) { if !called { exitCode = rc called = true } } defer func() { OsExiter = fakeOsExiter }() HandleExitCoder(Exit(errors.New("galactic perimeter breach"), 9)) assert.Equal(t, 9, exitCode) assert.True(t, called) } func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { exitCode := 0 called := false OsExiter = func(rc int) { if !called { exitCode = rc called = true } } defer func() { OsExiter = fakeOsExiter }() exitErr := Exit("galactic perimeter breach", 9) exitErr2 := Exit("last ExitCoder", 11) err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr, exitErr2) HandleExitCoder(err) assert.Equal(t, 11, exitCode) assert.True(t, called) } type exitFormatter struct { code int } func (f *exitFormatter) Format(s fmt.State, verb rune) { _, _ = s.Write([]byte("some other special")) } func (f *exitFormatter) ExitCode() int { return f.code } func (f *exitFormatter) Error() string { return fmt.Sprintf("my special error code %d", f.code) } func TestHandleExitCoder_ErrorFormatter(t *testing.T) { exitCode := 0 called := false OsExiter = func(rc int) { if !called { exitCode = rc called = true } } oldWriter := ErrWriter var buf bytes.Buffer ErrWriter = &buf defer func() { OsExiter = fakeOsExiter ErrWriter = oldWriter }() exitErr := Exit("galactic perimeter breach", 9) exitErr2 := Exit("last ExitCoder", 11) exitErr3 := &exitFormatter{code: 12} // add some recursion for multi error to fix test coverage err := newMultiError(errors.New("wowsa"), errors.New("egad"), exitErr3, newMultiError(exitErr, exitErr2)) HandleExitCoder(err) assert.Equal(t, 11, exitCode) assert.True(t, called) assert.Contains(t, buf.String(), "some other special") } func TestHandleExitCoder_MultiErrorWithoutExitCoder(t *testing.T) { exitCode := 0 called := false OsExiter = func(rc int) { if !called { exitCode = rc called = true } } defer func() { OsExiter = fakeOsExiter }() err := newMultiError(errors.New("wowsa"), errors.New("egad")) HandleExitCoder(err) assert.Equal(t, 1, exitCode) assert.True(t, called) } // make a stub to not import pkg/errors type ErrorWithFormat struct { error } func NewErrorWithFormat(m string) *ErrorWithFormat { return &ErrorWithFormat{error: errors.New(m)} } func (f *ErrorWithFormat) Format(s fmt.State, verb rune) { fmt.Fprintf(s, "This the format: %v", f.error) } func TestHandleExitCoder_ErrorWithFormat(t *testing.T) { called := false OsExiter = func(int) { if !called { called = true } } ErrWriter = &bytes.Buffer{} defer func() { OsExiter = fakeOsExiter ErrWriter = fakeErrWriter }() err := Exit(NewErrorWithFormat("I am formatted"), 1) HandleExitCoder(err) assert.True(t, called) assert.Equal(t, ErrWriter.(*bytes.Buffer).String(), "This the format: I am formatted\n") } func TestHandleExitCoder_MultiErrorWithFormat(t *testing.T) { called := false OsExiter = func(int) { if !called { called = true } } ErrWriter = &bytes.Buffer{} defer func() { OsExiter = fakeOsExiter }() err := newMultiError(NewErrorWithFormat("err1"), NewErrorWithFormat("err2")) HandleExitCoder(err) assert.True(t, called) assert.Equal(t, ErrWriter.(*bytes.Buffer).String(), "This the format: err1\nThis the format: err2\n") } func TestMultiErrorErrorsCopy(t *testing.T) { errList := []error{ errors.New("foo"), errors.New("bar"), errors.New("baz"), } me := newMultiError(errList...) assert.Equal(t, errList, me.Errors()) } func TestErrRequiredFlags_Error(t *testing.T) { missingFlags := []string{"flag1", "flag2"} err := &errRequiredFlags{missingFlags: missingFlags} expectedMsg := "Required flags \"flag1, flag2\" not set" assert.Equal(t, expectedMsg, err.Error()) missingFlags = []string{"flag1"} err = &errRequiredFlags{missingFlags: missingFlags} expectedMsg = "Required flag \"flag1\" not set" assert.Equal(t, expectedMsg, err.Error()) } ================================================ FILE: examples/example-cli/example-cli.go ================================================ // minimal example CLI used for binary size checking package main import ( "context" "github.com/urfave/cli/v3" ) func main() { _ = (&cli.Command{}).Run(context.Background(), []string{""}) } ================================================ FILE: examples/example-hello-world/example-hello-world.go ================================================ // example hello world used for binary size checking package main import "fmt" func main() { fmt.Println("hello world") } ================================================ FILE: examples_test.go ================================================ package cli_test import ( "context" "fmt" "net/mail" "os" "time" // Alias the package import to make the examples runnable on pkg.go.dev. // // See issue #1811. cli "github.com/urfave/cli/v3" ) func ExampleCommand_Run() { // Declare a command cmd := &cli.Command{ Name: "greet", Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "pat", Usage: "a name to say"}, }, Action: func(_ context.Context, cmd *cli.Command) error { fmt.Printf("Hello %[1]v\n", cmd.String("name")) return nil }, Authors: []any{ &mail.Address{Name: "Oliver Allen", Address: "oliver@toyshop.example.com"}, "gruffalo@soup-world.example.org", }, Version: "v0.13.12", } // Simulate the command line arguments os.Args = []string{"greet", "--name", "Jeremy"} if err := cmd.Run(context.Background(), os.Args); err != nil { // do something with unhandled errors fmt.Fprintf(os.Stderr, "Unhandled error: %[1]v\n", err) os.Exit(86) } // Output: // Hello Jeremy } func ExampleCommand_Run_subcommand() { cmd := &cli.Command{ Name: "say", Commands: []*cli.Command{ { Name: "hello", Aliases: []string{"hi"}, Usage: "use it to see a description", Description: "This is how we describe hello the function", Commands: []*cli.Command{ { Name: "english", Aliases: []string{"en"}, Usage: "sends a greeting in english", Description: "greets someone in english", Flags: []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "Bob", Usage: "Name of the person to greet", }, }, Action: func(_ context.Context, cmd *cli.Command) error { fmt.Println("Hello,", cmd.String("name")) return nil }, }, }, }, }, } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() // Simulate the command line arguments os.Args = []string{"say", "hi", "english", "--name", "Jeremy"} _ = cmd.Run(ctx, os.Args) // Output: // Hello, Jeremy } func ExampleCommand_Run_appHelp() { cmd := &cli.Command{ Name: "greet", Version: "0.1.0", Description: "This is how we describe greet the app", Authors: []any{ &mail.Address{Name: "Harrison", Address: "harrison@lolwut.example.com"}, "Oliver Allen ", }, Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, }, Arguments: cli.AnyArguments, Commands: []*cli.Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", ArgsUsage: "[arguments...]", Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, }, }, } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() // Simulate the command line arguments os.Args = []string{"greet", "help"} _ = cmd.Run(ctx, os.Args) // Output: // NAME: // greet - A new cli application // // USAGE: // greet [global options] [command [command options]] [arguments...] // // VERSION: // 0.1.0 // // DESCRIPTION: // This is how we describe greet the app // // AUTHORS: // "Harrison" // Oliver Allen // // COMMANDS: // describeit, d use it to see a description // help, h Shows a list of commands or help for one command // // GLOBAL OPTIONS: // --name string a name to say (default: "bob") // --help, -h show help // --version, -v print the version } func ExampleCommand_Run_commandHelp() { cmd := &cli.Command{ Name: "greet", Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "pat", Usage: "a name to say"}, }, Action: func(_ context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Root().Writer, "hello to %[1]q\n", cmd.String("name")) return nil }, Commands: []*cli.Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", ArgsUsage: "[arguments...]", Action: func(context.Context, *cli.Command) error { fmt.Println("i like to describe things") return nil }, }, }, } // Simulate the command line arguments os.Args = []string{"greet", "h", "describeit"} _ = cmd.Run(context.Background(), os.Args) // Output: // NAME: // greet describeit - use it to see a description // // USAGE: // greet describeit [arguments...] // // DESCRIPTION: // This is how we describe describeit the function // // OPTIONS: // --help, -h show help } func ExampleCommand_Run_noAction() { cmd := &cli.Command{Name: "greet"} // Simulate the command line arguments os.Args = []string{"greet"} _ = cmd.Run(context.Background(), os.Args) // Output: // NAME: // greet - A new cli application // // USAGE: // greet [global options] // // GLOBAL OPTIONS: // --help, -h show help } func ExampleCommand_Run_subcommandNoAction() { cmd := &cli.Command{ Name: "greet", Commands: []*cli.Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", ArgsUsage: "[arguments...]", Description: "This is how we describe describeit the function", }, }, } // Simulate the command line arguments os.Args = []string{"greet", "describeit"} _ = cmd.Run(context.Background(), os.Args) // Output: // NAME: // greet describeit - use it to see a description // // USAGE: // greet describeit [options] [arguments...] // // DESCRIPTION: // This is how we describe describeit the function // // OPTIONS: // --help, -h show help } func ExampleCommand_Run_shellComplete_bash_withShortFlag() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Flags: []cli.Flag{ &cli.Int64Flag{ Name: "other", Aliases: []string{"o"}, }, &cli.StringFlag{ Name: "xyz", Aliases: []string{"x"}, }, }, } // Simulate a bash environment and command line arguments os.Setenv("SHELL", "bash") os.Args = []string{"greet", "-", "--generate-shell-completion"} _ = cmd.Run(context.Background(), os.Args) // Output: // --other // --xyz // --help } func ExampleCommand_Run_shellComplete_bash_withLongFlag() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Flags: []cli.Flag{ &cli.Int64Flag{ Name: "other", Aliases: []string{"o"}, }, &cli.StringFlag{ Name: "xyz", Aliases: []string{"x"}, }, &cli.StringFlag{ Name: "some-flag,s", }, &cli.StringFlag{ Name: "similar-flag", }, }, } // Simulate a bash environment and command line arguments os.Setenv("SHELL", "bash") os.Args = []string{"greet", "--s", "--generate-shell-completion"} _ = cmd.Run(context.Background(), os.Args) // Output: // --some-flag // --similar-flag } func ExampleCommand_Run_shellComplete_bash_withMultipleLongFlag() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Flags: []cli.Flag{ &cli.Int64Flag{ Name: "int-flag", Aliases: []string{"i"}, }, &cli.StringFlag{ Name: "string", Aliases: []string{"s"}, }, &cli.StringFlag{ Name: "string-flag-2", }, &cli.StringFlag{ Name: "similar-flag", }, &cli.StringFlag{ Name: "some-flag", }, }, } // Simulate a bash environment and command line arguments os.Setenv("SHELL", "bash") os.Args = []string{"greet", "--st", "--generate-shell-completion"} _ = cmd.Run(context.Background(), os.Args) // Output: // --string // --string-flag-2 } func ExampleCommand_Run_shellComplete_bash() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating shell completion", Action: func(context.Context, *cli.Command) error { fmt.Printf("the next example") return nil }, }, }, } // Simulate a bash environment and command line arguments os.Setenv("SHELL", "bash") os.Args = []string{"greet", "--generate-shell-completion"} _ = cmd.Run(context.Background(), os.Args) // Output: // describeit // next // help } func ExampleCommand_Run_shellComplete_zsh() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", Action: func(context.Context, *cli.Command) error { fmt.Printf("the next example") return nil }, }, }, } // Simulate a zsh environment and command line arguments os.Args = []string{"greet", "--generate-shell-completion"} os.Setenv("SHELL", "/usr/bin/zsh") _ = cmd.Run(context.Background(), os.Args) // Output: // describeit:use it to see a description // next:next example // help:Shows a list of commands or help for one command } func ExampleCommand_Run_shellComplete_fish() { cmd := &cli.Command{ Name: "greet", EnableShellCompletion: true, Commands: []*cli.Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", Action: func(context.Context, *cli.Command) error { fmt.Printf("the next example") return nil }, }, }, } // Simulate a fish environment and command line arguments os.Args = []string{"greet", "--generate-shell-completion"} os.Setenv("SHELL", "/usr/bin/fish") _ = cmd.Run(context.Background(), os.Args) // Output: // describeit:use it to see a description // next:next example // help:Shows a list of commands or help for one command } func ExampleCommand_Run_sliceValues() { cmd := &cli.Command{ Name: "multi_values", Flags: []cli.Flag{ &cli.StringSliceFlag{Name: "stringSlice"}, &cli.FloatSliceFlag{Name: "float64Slice"}, &cli.Int64SliceFlag{Name: "intSlice"}, }, HideHelp: true, Action: func(ctx context.Context, cmd *cli.Command) error { for i, v := range cmd.FlagNames() { fmt.Printf("%d-%s %#v\n", i, v, cmd.Value(v)) } err := ctx.Err() fmt.Println("error:", err) return err }, } // Simulate command line arguments os.Args = []string{ "multi_values", "--stringSlice", "parsed1,parsed2", "--stringSlice", "parsed3,parsed4", "--float64Slice", "13.3,14.4", "--float64Slice", "15.5,16.6", "--intSlice", "13,14", "--intSlice", "15,16", } _ = cmd.Run(context.Background(), os.Args) // Output: // 0-stringSlice []string{"parsed1", "parsed2", "parsed3", "parsed4"} // 1-float64Slice []float64{13.3, 14.4, 15.5, 16.6} // 2-intSlice []int64{13, 14, 15, 16} // error: } func ExampleCommand_Run_mapValues() { cmd := &cli.Command{ Name: "multi_values", Flags: []cli.Flag{ &cli.StringMapFlag{Name: "stringMap"}, }, HideHelp: true, Action: func(ctx context.Context, cmd *cli.Command) error { for i, v := range cmd.FlagNames() { fmt.Printf("%d-%s %#v\n", i, v, cmd.StringMap(v)) } fmt.Printf("notfound %#v\n", cmd.StringMap("notfound")) err := ctx.Err() fmt.Println("error:", err) return err }, } // Simulate command line arguments os.Args = []string{ "multi_values", "--stringMap", "parsed1=parsed two", "--stringMap", "parsed3=", } _ = cmd.Run(context.Background(), os.Args) // Output: // 0-stringMap map[string]string{"parsed1":"parsed two", "parsed3":""} // notfound map[string]string(nil) // error: } func ExampleBoolWithInverseFlag() { flagWithInverse := &cli.BoolWithInverseFlag{ Name: "env", } cmd := &cli.Command{ Flags: []cli.Flag{ flagWithInverse, }, Action: func(_ context.Context, cmd *cli.Command) error { if flagWithInverse.IsSet() { if cmd.Bool("env") { fmt.Println("env is set") } else { fmt.Println("no-env is set") } } return nil }, } _ = cmd.Run(context.Background(), []string{"prog", "--no-env"}) fmt.Println("flags:", len(flagWithInverse.Names())) // Output: // no-env is set // flags: 2 } func ExampleCommand_Suggest() { cmd := &cli.Command{ Name: "greet", ErrWriter: os.Stdout, Suggest: true, HideHelp: false, HideHelpCommand: true, CustomRootCommandHelpTemplate: "(this space intentionally left blank)\n", Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"}, }, Action: func(_ context.Context, cmd *cli.Command) error { fmt.Printf("Hello %v\n", cmd.String("name")) return nil }, } if cmd.Run(context.Background(), []string{"greet", "--nema", "chipmunk"}) == nil { fmt.Println("Expected error") } // Output: // Incorrect Usage: flag provided but not defined: -nema // // Did you mean "--name"? // // (this space intentionally left blank) } func ExampleCommand_Suggest_command() { cmd := &cli.Command{ ErrWriter: os.Stdout, Name: "greet", Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"}, }, Action: func(_ context.Context, cmd *cli.Command) error { fmt.Printf("Hello %v\n", cmd.String("name")) return nil }, Commands: []*cli.Command{ { Name: "neighbors", HideHelp: true, HideHelpCommand: true, Suggest: true, CustomHelpTemplate: "(this space intentionally left blank)\n", Flags: []cli.Flag{ &cli.BoolFlag{Name: "smiling"}, }, Action: func(_ context.Context, cmd *cli.Command) error { if cmd.Bool("smiling") { fmt.Println("😀") } fmt.Println("Hello, neighbors") return nil }, }, }, } if cmd.Run(context.Background(), []string{"greet", "neighbors", "--sliming"}) == nil { fmt.Println("Expected error") } // Output: // Incorrect Usage: flag provided but not defined: -sliming // // Did you mean "--smiling"? } ================================================ FILE: fish.go ================================================ package cli import ( "bytes" "fmt" "io" "strings" "text/template" ) // ToFishCompletion creates a fish completion string for the `*Command` // The function errors if either parsing or writing of the string fails. func (cmd *Command) ToFishCompletion() (string, error) { var w bytes.Buffer if err := cmd.writeFishCompletionTemplate(&w); err != nil { return "", err } return w.String(), nil } type fishCommandCompletionTemplate struct { Command *Command Completions []string AllCommands []string } func (cmd *Command) writeFishCompletionTemplate(w io.Writer) error { const name = "cli" t, err := template.New(name).Parse(FishCompletionTemplate) if err != nil { return err } // Add global flags completions := prepareFishFlags(cmd.Name, cmd) if cmd.ShellComplete != nil { var completion strings.Builder fmt.Fprintf(&completion, "complete -c %s -n '%s' -xa '(%s %s 2>/dev/null)'", cmd.Name, fishFlagHelper(cmd.Name, cmd), cmd.Name, completionFlag, ) completions = append(completions, completion.String()) } // Add commands and their flags completions = append( completions, prepareFishCommands(cmd.Name, cmd)..., ) toplevelCommandNames := []string{} for _, child := range cmd.Commands { toplevelCommandNames = append(toplevelCommandNames, child.Names()...) } return t.ExecuteTemplate(w, name, &fishCommandCompletionTemplate{ Command: cmd, Completions: completions, AllCommands: toplevelCommandNames, }) } func prepareFishCommands(binary string, parent *Command) []string { commands := parent.Commands completions := []string{} for _, command := range commands { if !command.Hidden { var completion strings.Builder fmt.Fprintf(&completion, "complete -x -c %s -n '%s' -a '%s'", binary, fishSubcommandHelper(binary, parent, commands), command.Name, ) if command.Usage != "" { fmt.Fprintf(&completion, " -d '%s'", escapeSingleQuotes(command.Usage)) } completions = append(completions, completion.String()) } if command.ShellComplete != nil { var completion strings.Builder var path []string lineage := command.Lineage() for i := len(lineage) - 2; i >= 0; i-- { path = append(path, lineage[i].Name) } fmt.Fprintf(&completion, "complete -c %s -n '%s' -xa '(%s %s %s 2>/dev/null)'", binary, fishFlagHelper(binary, command), binary, strings.Join(path, " "), completionFlag, ) completions = append(completions, completion.String()) } completions = append( completions, prepareFishFlags(binary, command)..., ) // recursively iterate subcommands completions = append( completions, prepareFishCommands(binary, command)..., ) } return completions } func prepareFishFlags(binary string, owner *Command) []string { flags := owner.VisibleFlags() completions := []string{} for _, f := range flags { completion := &strings.Builder{} fmt.Fprintf(completion, "complete -c %s -n '%s'", binary, fishFlagHelper(binary, owner), ) fishAddFileFlag(f, completion) for idx, opt := range f.Names() { if idx == 0 { fmt.Fprintf(completion, " -l %s", strings.TrimSpace(opt), ) } else { fmt.Fprintf(completion, " -s %s", strings.TrimSpace(opt), ) } } if flag, ok := f.(DocGenerationFlag); ok { if flag.TakesValue() { completion.WriteString(" -r") } if flag.GetUsage() != "" { fmt.Fprintf(completion, " -d '%s'", escapeSingleQuotes(flag.GetUsage())) } } completions = append(completions, completion.String()) } return completions } func fishAddFileFlag(flag Flag, completion *strings.Builder) { switch f := flag.(type) { case *StringFlag: if f.TakesFile { return } case *StringSliceFlag: if f.TakesFile { return } } completion.WriteString(" -f") } func fishSubcommandHelper(binary string, command *Command, siblings []*Command) string { fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary) if len(command.Lineage()) > 1 { var siblingNames []string for _, sibling := range siblings { siblingNames = append(siblingNames, sibling.Names()...) } ancestry := commandAncestry(command) fishHelper = fmt.Sprintf( "%s; and not __fish_seen_subcommand_from %s", ancestry, strings.Join(siblingNames, " "), ) } return fishHelper } func fishFlagHelper(binary string, command *Command) string { fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary) if len(command.Lineage()) > 1 { fishHelper = commandAncestry(command) } return fishHelper } func commandAncestry(command *Command) string { var ancestry []string ancestors := command.Lineage() for i := len(ancestors) - 2; i >= 0; i-- { ancestry = append( ancestry, fmt.Sprintf( "__fish_seen_subcommand_from %s", strings.Join(ancestors[i].Names(), " "), ), ) } return strings.Join(ancestry, "; and ") } func escapeSingleQuotes(input string) string { return strings.ReplaceAll(input, `'`, `\'`) } ================================================ FILE: fish_test.go ================================================ package cli import ( "context" "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFishCompletion(t *testing.T) { // Given cmd := buildExtendedTestCommand() cmd.Flags = append(cmd.Flags, &StringFlag{ Name: "logfile", TakesFile: true, }, &StringSliceFlag{ Name: "foofile", TakesFile: true, }) cmd.setupCommandGraph() oldTemplate := FishCompletionTemplate defer func() { FishCompletionTemplate = oldTemplate }() FishCompletionTemplate = "{{something" // test error case _, err1 := cmd.ToFishCompletion() assert.Error(t, err1) // reset the template FishCompletionTemplate = oldTemplate // When res, err := cmd.ToFishCompletion() // Then require.NoError(t, err) expectFileContent(t, "testdata/expected-fish-full.fish", res) } func TestFishCompletionShellComplete(t *testing.T) { cmd := buildExtendedTestCommand() cmd.ShellComplete = func(context.Context, *Command) {} configCmd := cmd.Command("config") configCmd.ShellComplete = func(context.Context, *Command) {} subConfigCmd := configCmd.Command("sub-config") subConfigCmd.ShellComplete = func(context.Context, *Command) {} cmd.setupCommandGraph() res, err := cmd.ToFishCompletion() require.NoError(t, err) assert.Contains(t, res, fmt.Sprintf("complete -c greet -n '__fish_greet_no_subcommand' -xa '(greet %s 2>/dev/null)'", completionFlag)) assert.Contains(t, res, fmt.Sprintf("complete -c greet -n '__fish_seen_subcommand_from config c' -xa '(greet config %s 2>/dev/null)'", completionFlag)) assert.Contains(t, res, fmt.Sprintf("complete -c greet -n '__fish_seen_subcommand_from config c; and __fish_seen_subcommand_from sub-config s ss' -xa '(greet config sub-config %s 2>/dev/null)'", completionFlag)) } ================================================ FILE: flag.go ================================================ package cli import ( "context" "fmt" "regexp" "strings" "time" ) const defaultPlaceholder = "value" const ( defaultSliceFlagSeparator = "," defaultMapFlagKeyValueSeparator = "=" disableSliceFlagSeparator = false ) var ( slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano()) commaWhitespace = regexp.MustCompile("[, ]+.*") ) // GenerateShellCompletionFlag enables shell completion var GenerateShellCompletionFlag Flag = &BoolFlag{ Name: "generate-shell-completion", Hidden: true, } // VersionFlag prints the version for the application var VersionFlag Flag = &BoolFlag{ Name: "version", Aliases: []string{"v"}, Usage: "print the version", HideDefault: true, Local: true, } // HelpFlag prints the help for all commands and subcommands. // Set to nil to disable the flag. The subcommand // will still be added unless HideHelp or HideHelpCommand is set to true. var HelpFlag Flag = &BoolFlag{ Name: "help", Aliases: []string{"h"}, Usage: "show help", HideDefault: true, Local: true, } // FlagStringer converts a flag definition to a string. This is used by help // to display a flag. var FlagStringer FlagStringFunc = stringifyFlag // Serializer is used to circumvent the limitations of flag.FlagSet.Set type Serializer interface { Serialize() string } // FlagNamePrefixer converts a full flag name and its placeholder into the help // message flag prefix. This is used by the default FlagStringer. var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames // FlagEnvHinter annotates flag help message with the environment variable // details. This is used by the default FlagStringer. var FlagEnvHinter FlagEnvHintFunc = withEnvHint // FlagFileHinter annotates flag help message with the environment variable // details. This is used by the default FlagStringer. var FlagFileHinter FlagFileHintFunc = withFileHint // FlagsByName is a slice of Flag. type FlagsByName []Flag func (f FlagsByName) Len() int { return len(f) } func (f FlagsByName) Less(i, j int) bool { return lexicographicLess(f[i].Names()[0], f[j].Names()[0]) } func (f FlagsByName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } // ActionableFlag is an interface that wraps Flag interface and RunAction operation. type ActionableFlag interface { RunAction(context.Context, *Command) error } // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recommended that // this interface be implemented. type Flag interface { fmt.Stringer // Retrieve the value of the Flag Get() any // Lifecycle methods. // flag callback prior to parsing PreParse() error // flag callback post parsing PostParse() error // Apply Flag settings to the given flag set Set(string, string) error // All possible names for this flag Names() []string // Whether the flag has been set or not IsSet() bool } // RequiredFlag is an interface that allows us to mark flags as required // it allows flags required flags to be backwards compatible with the Flag interface type RequiredFlag interface { // whether the flag is a required flag or not IsRequired() bool } // DocGenerationFlag is an interface that allows documentation generation for the flag type DocGenerationFlag interface { // TakesValue returns true if the flag takes a value, otherwise false TakesValue() bool // GetUsage returns the usage string for the flag GetUsage() string // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. GetValue() string // GetDefaultText returns the default text for this flag GetDefaultText() string // GetEnvVars returns the env vars for this flag GetEnvVars() []string // IsDefaultVisible returns whether the default value should be shown in // help text IsDefaultVisible() bool // TypeName to detect if a flag is a string, bool, etc. TypeName() string } // DocGenerationMultiValueFlag extends DocGenerationFlag for slice/map based flags. type DocGenerationMultiValueFlag interface { DocGenerationFlag // IsMultiValueFlag returns true for flags that can be given multiple times. IsMultiValueFlag() bool } // Countable is an interface to enable detection of flag values which support // repetitive flags type Countable interface { Count() int } // VisibleFlag is an interface that allows to check if a flag is visible type VisibleFlag interface { // IsVisible returns true if the flag is not hidden, otherwise false IsVisible() bool } // CategorizableFlag is an interface that allows us to potentially // use a flag in a categorized representation. type CategorizableFlag interface { // Returns the category of the flag GetCategory() string // Sets the category of the flag SetCategory(string) } // LocalFlag is an interface to enable detection of flags which are local // to current command type LocalFlag interface { IsLocal() bool } func visibleFlags(fl []Flag) []Flag { var visible []Flag for _, f := range fl { if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() { visible = append(visible, f) } } return visible } func FlagNames(name string, aliases []string) []string { var ret []string for _, part := range append([]string{name}, aliases...) { // v1 -> v2 migration warning zone: // Strip off anything after the first found comma or space, which // *hopefully* makes it a tiny bit more obvious that unexpected behavior is // caused by using the v1 form of stringly typed "Name". ret = append(ret, commaWhitespace.ReplaceAllString(part, "")) } return ret } func hasFlag(flags []Flag, fl Flag) bool { for _, existing := range flags { if fl == existing { return true } } return false } func flagSplitMultiValues(val string, sliceSeparator string, disableSliceSeparator bool) []string { if disableSliceSeparator { return []string{val} } if len(sliceSeparator) == 0 { sliceSeparator = defaultSliceFlagSeparator } return strings.Split(val, sliceSeparator) } ================================================ FILE: flag_bool.go ================================================ package cli import ( "errors" "strconv" ) type BoolFlag = FlagBase[bool, BoolConfig, boolValue] // BoolConfig defines the configuration for bool flags type BoolConfig struct { Count *int } // boolValue needs to implement the boolFlag internal interface in flag // to be able to capture bool fields and values // // type boolFlag interface { // Value // IsBoolFlag() bool // } type boolValue struct { destination *bool count *int } func (cmd *Command) Bool(name string) bool { if v, ok := cmd.Value(name).(bool); ok { tracef("bool available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("bool NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return false } // Below functions are to satisfy the ValueCreator interface // Create creates the bool value func (b boolValue) Create(val bool, p *bool, c BoolConfig) Value { *p = val if c.Count == nil { c.Count = new(int) } return &boolValue{ destination: p, count: c.Count, } } // ToString formats the bool value func (b boolValue) ToString(value bool) string { b.destination = &value return b.String() } // Below functions are to satisfy the flag.Value interface func (b *boolValue) Set(s string) error { v, err := strconv.ParseBool(s) if err != nil { err = errors.New("parse error") return err } *b.destination = v if b.count != nil { *b.count = *b.count + 1 } return err } func (b *boolValue) Get() interface{} { return *b.destination } func (b *boolValue) String() string { return strconv.FormatBool(*b.destination) } func (b *boolValue) IsBoolFlag() bool { return true } ================================================ FILE: flag_bool_with_inverse.go ================================================ package cli import ( "context" "fmt" "slices" "strings" ) var DefaultInverseBoolPrefix = "no-" type BoolWithInverseFlag struct { Name string `json:"name"` // name of the flag Category string `json:"category"` // category of the flag, if any DefaultText string `json:"defaultText"` // default text of the flag for usage purposes HideDefault bool `json:"hideDefault"` // whether to hide the default value in output Usage string `json:"usage"` // usage string for help output Sources ValueSourceChain `json:"-"` // sources to load flag value from Required bool `json:"required"` // whether the flag is required or not Hidden bool `json:"hidden"` // whether to hide the flag in help output Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well Value bool `json:"defaultValue"` // default value for this flag if not set by from any source Destination *bool `json:"-"` // destination pointer for value when set Aliases []string `json:"aliases"` // Aliases that are allowed for this flag TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes Action func(context.Context, *Command, bool) error `json:"-"` // Action callback to be called when flag is set OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line Validator func(bool) error `json:"-"` // custom function to validate this flag value ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not Config BoolConfig `json:"config"` // Additional/Custom configuration associated with this flag type InversePrefix string `json:"invPrefix"` // The prefix used to indicate a negative value. Default: `env` becomes `no-env` // unexported fields for internal use count int // number of times the flag has been set hasBeenSet bool // whether the flag has been set from env or file applied bool // whether the flag has been applied to a flag set already value Value // value representing this flag's value pset bool nset bool } func (bif *BoolWithInverseFlag) IsSet() bool { return bif.hasBeenSet } func (bif *BoolWithInverseFlag) Get() any { return bif.value.Get() } func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error { if bif.Action != nil { return bif.Action(ctx, cmd, bif.Get().(bool)) } return nil } func (bif *BoolWithInverseFlag) IsLocal() bool { return bif.Local } func (bif *BoolWithInverseFlag) inversePrefix() string { if bif.InversePrefix == "" { bif.InversePrefix = DefaultInverseBoolPrefix } return bif.InversePrefix } func (bif *BoolWithInverseFlag) PreParse() error { count := bif.Config.Count if count == nil { count = &bif.count } dest := bif.Destination if dest == nil { dest = new(bool) } *dest = bif.Value bif.value = &boolValue{ destination: dest, count: count, } // Validate the given default or values set from external sources as well if bif.Validator != nil && bif.ValidateDefaults { if err := bif.Validator(bif.value.Get().(bool)); err != nil { return err } } bif.applied = true return nil } func (bif *BoolWithInverseFlag) PostParse() error { tracef("postparse (flag=%[1]q)", bif.Name) if !bif.hasBeenSet { if val, source, found := bif.Sources.LookupWithSource(); found { if val == "" { val = "false" } if err := bif.Set(bif.Name, val); err != nil { return fmt.Errorf( "could not parse %[1]q as %[2]T value from %[3]s for flag %[4]s: %[5]s", val, bif.Value, source, bif.Name, err, ) } bif.hasBeenSet = true } } return nil } func (bif *BoolWithInverseFlag) Set(name, val string) error { if bif.count > 0 && bif.OnlyOnce { return fmt.Errorf("cant duplicate this flag") } bif.hasBeenSet = true if slices.Contains(append([]string{bif.Name}, bif.Aliases...), name) { if bif.nset { return fmt.Errorf("cannot set both flags `--%s` and `--%s`", bif.Name, bif.inversePrefix()+bif.Name) } if err := bif.value.Set(val); err != nil { return err } bif.pset = true } else { if bif.pset { return fmt.Errorf("cannot set both flags `--%s` and `--%s`", bif.Name, bif.inversePrefix()+bif.Name) } if err := bif.value.Set("false"); err != nil { return err } bif.nset = true } if bif.Validator != nil { return bif.Validator(bif.value.Get().(bool)) } return nil } func (bif *BoolWithInverseFlag) Names() []string { names := append([]string{bif.Name}, bif.Aliases...) for _, name := range names { names = append(names, bif.inversePrefix()+name) } return names } func (bif *BoolWithInverseFlag) IsRequired() bool { return bif.Required } func (bif *BoolWithInverseFlag) IsVisible() bool { return !bif.Hidden } // String implements the standard Stringer interface. // // Example for BoolFlag{Name: "env"} // --[no-]env (default: false) func (bif *BoolWithInverseFlag) String() string { out := FlagStringer(bif) i := strings.Index(out, "\t") prefix := "--" // single character flags are prefixed with `-` instead of `--` if len(bif.Name) == 1 { prefix = "-" } return fmt.Sprintf("%s[%s]%s%s", prefix, bif.inversePrefix(), bif.Name, out[i:]) } // IsBoolFlag returns whether the flag doesnt need to accept args func (bif *BoolWithInverseFlag) IsBoolFlag() bool { return true } // Count returns the number of times this flag has been invoked func (bif *BoolWithInverseFlag) Count() int { return bif.count } // GetDefaultText returns the default text for this flag func (bif *BoolWithInverseFlag) GetDefaultText() string { if bif.Required { return bif.DefaultText } return boolValue{}.ToString(bif.Value) } // GetCategory returns the category of the flag func (bif *BoolWithInverseFlag) GetCategory() string { return bif.Category } func (bif *BoolWithInverseFlag) SetCategory(c string) { bif.Category = c } // GetUsage returns the usage string for the flag func (bif *BoolWithInverseFlag) GetUsage() string { return bif.Usage } // GetEnvVars returns the env vars for this flag func (bif *BoolWithInverseFlag) GetEnvVars() []string { return bif.Sources.EnvKeys() } // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (bif *BoolWithInverseFlag) GetValue() string { return "" } func (bif *BoolWithInverseFlag) TakesValue() bool { return false } // IsDefaultVisible returns true if the flag is not hidden, otherwise false func (bif *BoolWithInverseFlag) IsDefaultVisible() bool { return !bif.HideDefault } // TypeName is used for stringify/docs. For bool its a no-op func (bif *BoolWithInverseFlag) TypeName() string { return "bool" } ================================================ FILE: flag_bool_with_inverse_test.go ================================================ package cli import ( "context" "fmt" "strings" "testing" "github.com/stretchr/testify/require" ) var errBothEnvFlagsAreSet = fmt.Errorf("cannot set both flags `--env` and `--no-env`") type boolWithInverseTestCase struct { args []string toBeSet bool value bool err error envVars map[string]string } func (tc *boolWithInverseTestCase) Run(t *testing.T, flagWithInverse *BoolWithInverseFlag) error { cmd := &Command{ Flags: []Flag{flagWithInverse}, Action: func(context.Context, *Command) error { return nil }, } for key, val := range tc.envVars { t.Setenv(key, val) } err := cmd.Run(buildTestContext(t), append([]string{"prog"}, tc.args...)) if err != nil { return err } if flagWithInverse.IsSet() != tc.toBeSet { return fmt.Errorf("flag should be set %t, but got %t", tc.toBeSet, flagWithInverse.IsSet()) } if flagWithInverse.Get() != tc.value { return fmt.Errorf("flag value should be %t, but got %t", tc.value, flagWithInverse.Get()) } return nil } func runBoolWithInverseFlagTests(t *testing.T, newFlagMethod func() *BoolWithInverseFlag, cases []*boolWithInverseTestCase) error { for _, tc := range cases { t.Run(strings.Join(tc.args, " ")+fmt.Sprintf("%[1]v %[2]v %[3]v", tc.value, tc.toBeSet, tc.err), func(t *testing.T) { r := require.New(t) fl := newFlagMethod() err := tc.Run(t, fl) if err != nil && tc.err == nil { r.NoError(err) } if err == nil && tc.err != nil { r.Error(err) } if err != nil && tc.err != nil { r.ErrorContains(err, tc.err.Error()) } }) } return nil } func TestBoolWithInverseBasic(t *testing.T) { flagMethod := func() *BoolWithInverseFlag { return &BoolWithInverseFlag{ Name: "env", } } testCases := []*boolWithInverseTestCase{ { args: []string{"--no-env"}, toBeSet: true, value: false, }, { args: []string{"--env"}, toBeSet: true, value: true, }, { toBeSet: false, value: false, }, { args: []string{"--env", "--no-env"}, err: errBothEnvFlagsAreSet, }, } err := runBoolWithInverseFlagTests(t, flagMethod, testCases) if err != nil { t.Error(err) return } } func TestBoolWithInverseAction(t *testing.T) { err := fmt.Errorf("action called") flagMethod := func() *BoolWithInverseFlag { bif := &BoolWithInverseFlag{ Name: "env", // Setting env to the opposite to test flag Action is working as intended Action: func(_ context.Context, cmd *Command, value bool) error { return err }, } return bif } testCases := []*boolWithInverseTestCase{ { args: []string{"--no-env"}, toBeSet: true, value: false, err: err, }, { args: []string{"--env"}, toBeSet: true, value: true, err: err, }, // This test is not inverse because the flag action is never called { toBeSet: false, value: false, }, { args: []string{"--env", "--no-env"}, err: errBothEnvFlagsAreSet, }, } errr := runBoolWithInverseFlagTests(t, flagMethod, testCases) if errr != nil { t.Error(errr) return } } func TestBoolWithInverseAlias(t *testing.T) { flagMethod := func() *BoolWithInverseFlag { return &BoolWithInverseFlag{ Name: "env", Aliases: []string{"e", "do-env"}, } } testCases := []*boolWithInverseTestCase{ { args: []string{"--no-e"}, toBeSet: true, value: false, }, { args: []string{"--e"}, toBeSet: true, value: true, }, { toBeSet: false, value: false, }, { args: []string{"--do-env", "--no-do-env"}, err: errBothEnvFlagsAreSet, }, } err := runBoolWithInverseFlagTests(t, flagMethod, testCases) if err != nil { t.Error(err) return } } func TestBoolWithInverseEnvVars(t *testing.T) { flagMethod := func() *BoolWithInverseFlag { return &BoolWithInverseFlag{ Name: "env", Sources: EnvVars("ENV", "NO-ENV"), Local: true, } } testCases := []*boolWithInverseTestCase{ { toBeSet: true, value: false, envVars: map[string]string{ "NO-ENV": "false", }, }, { toBeSet: true, value: true, envVars: map[string]string{ "ENV": "true", }, }, { toBeSet: true, value: false, envVars: map[string]string{ "ENV": "false", }, }, { toBeSet: false, value: false, }, // TODO /*{ err: errBothEnvFlagsAreSet, envVars: map[string]string{ "ENV": "true", "NO-ENV": "true", }, },*/ { err: fmt.Errorf("could not parse \"true_env\" as bool value from environment variable \"ENV\" for flag env: parse error"), envVars: map[string]string{ "ENV": "true_env", }, }, { err: fmt.Errorf("could not parse \"false_env\" as bool value from environment variable \"NO-ENV\" for flag env: parse error"), envVars: map[string]string{ "NO-ENV": "false_env", }, }, } err := runBoolWithInverseFlagTests(t, flagMethod, testCases) if err != nil { t.Error(err) return } } func TestBoolWithInverseWithPrefix(t *testing.T) { flagMethod := func() *BoolWithInverseFlag { return &BoolWithInverseFlag{ Name: "env", InversePrefix: "without-", } } testCases := []*boolWithInverseTestCase{ { args: []string{"--without-env"}, toBeSet: true, value: false, }, { args: []string{"--env"}, toBeSet: true, value: true, }, { toBeSet: false, value: false, }, { args: []string{"--env", "--without-env"}, err: fmt.Errorf("cannot set both flags `--env` and `--without-env`"), }, { args: []string{"--without-env", "--env"}, err: fmt.Errorf("cannot set both flags `--env` and `--without-env`"), }, } err := runBoolWithInverseFlagTests(t, flagMethod, testCases) if err != nil { t.Error(err) return } } func TestBoolWithInverseRequired(t *testing.T) { flagMethod := func() *BoolWithInverseFlag { return &BoolWithInverseFlag{ Name: "env", Required: true, } } testCases := []*boolWithInverseTestCase{ { args: []string{"--no-env"}, toBeSet: true, value: false, }, { args: []string{"--env"}, toBeSet: true, value: true, }, { args: []string{"--env", "--no-env"}, err: errBothEnvFlagsAreSet, }, } err := runBoolWithInverseFlagTests(t, flagMethod, testCases) if err != nil { t.Error(err) return } } func TestBoolWithInverseNames(t *testing.T) { flag := &BoolWithInverseFlag{ Name: "env", Required: true, } names := flag.Names() require.Len(t, names, 2) require.Equal(t, "env", names[0], "expected first name to be `env`") require.Equal(t, "no-env", names[1], "expected first name to be `no-env`") var d DocGenerationFlag = flag require.Equal(t, "bool", d.TypeName()) } func TestBoolWithInverseString(t *testing.T) { tcs := []struct { testName string flagName string required bool usage string inversePrefix string expected string }{ { testName: "empty inverse prefix no flag", flagName: "", required: true, expected: "--[no-]\t", }, { testName: "single-char flag name", flagName: "e", required: true, expected: "-[no-]e\t", }, { testName: "multi-char flag name", flagName: "env", required: true, expected: "--[no-]env\t", }, { testName: "required with usage", flagName: "env", required: true, usage: "env usage", expected: "--[no-]env\tenv usage", }, { testName: "required without usage", flagName: "env", required: true, expected: "--[no-]env\t", }, { testName: "not required with default usage", flagName: "env", required: false, expected: "--[no-]env\t(default: false)", }, { testName: "custom inverse prefix", flagName: "env", required: true, inversePrefix: "nope-", expected: "--[nope-]env\t", }, { testName: "empty inverse prefix", flagName: "env", required: true, expected: "--[no-]env\t", }, } for _, tc := range tcs { t.Run(tc.testName, func(t *testing.T) { flag := &BoolWithInverseFlag{ Name: tc.flagName, Usage: tc.usage, Required: tc.required, InversePrefix: tc.inversePrefix, } require.Equal(t, tc.expected, flag.String()) }) } } func TestBoolWithInverseDestination(t *testing.T) { destination := new(bool) count := new(int) flagMethod := func() *BoolWithInverseFlag { return &BoolWithInverseFlag{ Name: "env", Destination: destination, Config: BoolConfig{ Count: count, }, } } checkAndReset := func(expectedCount int, expectedValue bool) error { if *count != expectedCount { return fmt.Errorf("expected count to be %d, got %d", expectedCount, *count) } if *destination != expectedValue { return fmt.Errorf("expected destination to be %t, got %t", expectedValue, *destination) } *count = 0 *destination = false return nil } err := (&boolWithInverseTestCase{ args: []string{"--env"}, toBeSet: true, value: true, }).Run(t, flagMethod()) if err != nil { t.Error(err) return } err = checkAndReset(1, true) if err != nil { t.Error(err) return } err = (&boolWithInverseTestCase{ args: []string{"--no-env"}, toBeSet: true, value: false, }).Run(t, flagMethod()) if err != nil { t.Error(err) return } err = checkAndReset(1, false) if err != nil { t.Error(err) return } err = (&boolWithInverseTestCase{ args: []string{}, toBeSet: false, value: false, }).Run(t, flagMethod()) if err != nil { t.Error(err) return } err = checkAndReset(0, false) if err != nil { t.Error(err) return } f := flagMethod() f.Value = true err = (&boolWithInverseTestCase{ args: []string{}, toBeSet: false, value: true, }).Run(t, f) if err != nil { t.Error(err) return } err = checkAndReset(0, true) if err != nil { t.Error(err) return } } func TestBoolWithInverseFlag_SatisfiesRequiredFlagInterface(t *testing.T) { var f RequiredFlag = &BoolWithInverseFlag{} _ = f.IsRequired() } func TestBoolWithInverseFlag_SatisfiesVisibleFlagInterface(t *testing.T) { var f VisibleFlag = &BoolWithInverseFlag{} _ = f.IsVisible() } ================================================ FILE: flag_duration.go ================================================ package cli import ( "fmt" "time" ) type DurationFlag = FlagBase[time.Duration, NoConfig, durationValue] // -- time.Duration Value type durationValue time.Duration // Below functions are to satisfy the ValueCreator interface func (d durationValue) Create(val time.Duration, p *time.Duration, c NoConfig) Value { *p = val return (*durationValue)(p) } func (d durationValue) ToString(val time.Duration) string { d = durationValue(val) return d.String() } // Below functions are to satisfy the flag.Value interface func (d *durationValue) Set(s string) error { v, err := time.ParseDuration(s) if err != nil { return err } *d = durationValue(v) return err } func (d *durationValue) Get() any { return time.Duration(*d) } func (d *durationValue) String() string { return fmt.Sprintf("%v", time.Duration(*d)) } func (cmd *Command) Duration(name string) time.Duration { if v, ok := cmd.Value(name).(time.Duration); ok { tracef("duration available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("duration NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } ================================================ FILE: flag_ext.go ================================================ package cli import "flag" type extFlag struct { f *flag.Flag } func (e *extFlag) PreParse() error { if e.f.DefValue != "" { return e.Set("", e.f.DefValue) } return nil } func (e *extFlag) PostParse() error { return nil } func (e *extFlag) Set(_ string, val string) error { return e.f.Value.Set(val) } func (e *extFlag) Get() any { return e.f.Value.(flag.Getter).Get() } func (e *extFlag) Names() []string { return []string{e.f.Name} } func (e *extFlag) IsSet() bool { return false } func (e *extFlag) String() string { return FlagStringer(e) } func (e *extFlag) IsVisible() bool { return true } func (e *extFlag) TakesValue() bool { return false } func (e *extFlag) GetUsage() string { return e.f.Usage } func (e *extFlag) GetValue() string { return e.f.Value.String() } func (e *extFlag) GetDefaultText() string { return e.f.DefValue } func (e *extFlag) GetEnvVars() []string { return nil } ================================================ FILE: flag_float.go ================================================ package cli import ( "strconv" "unsafe" ) type ( FloatFlag = FlagBase[float64, NoConfig, floatValue[float64]] Float32Flag = FlagBase[float32, NoConfig, floatValue[float32]] Float64Flag = FlagBase[float64, NoConfig, floatValue[float64]] ) // -- float Value type floatValue[T float32 | float64] struct { val *T } // Below functions are to satisfy the ValueCreator interface func (f floatValue[T]) Create(val T, p *T, c NoConfig) Value { *p = val return &floatValue[T]{val: p} } func (f floatValue[T]) ToString(b T) string { f.val = &b return f.String() } // Below functions are to satisfy the flag.Value interface func (f *floatValue[T]) Set(s string) error { v, err := strconv.ParseFloat(s, int(unsafe.Sizeof(T(0))*8)) if err != nil { return err } *f.val = T(v) return nil } func (f *floatValue[T]) Get() any { return *f.val } func (f *floatValue[T]) String() string { return strconv.FormatFloat(float64(*f.val), 'g', -1, int(unsafe.Sizeof(T(0))*8)) } // Float looks up the value of a local FloatFlag, returns // 0 if not found func (cmd *Command) Float(name string) float64 { return getFloat[float64](cmd, name) } // Float32 looks up the value of a local Float32Flag, returns // 0 if not found func (cmd *Command) Float32(name string) float32 { return getFloat[float32](cmd, name) } // Float64 looks up the value of a local Float64Flag, returns // 0 if not found func (cmd *Command) Float64(name string) float64 { return getFloat[float64](cmd, name) } func getFloat[T float32 | float64](cmd *Command, name string) T { if v, ok := cmd.Value(name).(T); ok { tracef("float available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("float NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } ================================================ FILE: flag_float_slice.go ================================================ package cli type ( FloatSlice = SliceBase[float64, NoConfig, floatValue[float64]] Float32Slice = SliceBase[float32, NoConfig, floatValue[float32]] Float64Slice = SliceBase[float64, NoConfig, floatValue[float64]] FloatSliceFlag = FlagBase[[]float64, NoConfig, FloatSlice] Float32SliceFlag = FlagBase[[]float32, NoConfig, Float32Slice] Float64SliceFlag = FlagBase[[]float64, NoConfig, Float64Slice] ) var ( NewFloatSlice = NewSliceBase[float64, NoConfig, floatValue[float64]] NewFloat32Slice = NewSliceBase[float32, NoConfig, floatValue[float32]] NewFloat64Slice = NewSliceBase[float64, NoConfig, floatValue[float64]] ) // FloatSlice looks up the value of a local FloatSliceFlag, returns // nil if not found func (cmd *Command) FloatSlice(name string) []float64 { return getNumberSlice[float64](cmd, name) } // Float32Slice looks up the value of a local Float32Slice, returns // nil if not found func (cmd *Command) Float32Slice(name string) []float32 { return getNumberSlice[float32](cmd, name) } // Float64Slice looks up the value of a local Float64SliceFlag, returns // nil if not found func (cmd *Command) Float64Slice(name string) []float64 { return getNumberSlice[float64](cmd, name) } ================================================ FILE: flag_float_slice_test.go ================================================ package cli import ( "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCommand_FloatSlice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []float64 expectErr bool }{ { flag: &FloatSliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []float64{1, 2, 3, 4}, }, { flag: &FloatSliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []float64{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.FloatSlice(name), "FloatSlice(%v)", name) } }) } } func TestCommand_Float32Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []float32 expectErr bool }{ { flag: &Float32SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []float32{1, 2, 3, 4}, }, { flag: &Float32SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []float32{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Float32Slice(name), "Float32Slice(%v)", name) } }) } } func TestCommand_Float64Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []float64 expectErr bool }{ { flag: &Float64SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []float64{1, 2, 3, 4}, }, { flag: &Float64SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []float64{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Float64Slice(name), "Float64Slice(%v)", name) } }) } } ================================================ FILE: flag_float_test.go ================================================ package cli import ( "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_FloatFlag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue float64 expectErr bool }{ { name: "valid", flag: &FloatFlag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "-234567"}, expectedValue: -234567, }, { name: "invalid", flag: &FloatFlag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Float(name)) } }) } } func Test_Float32Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue float32 expectErr bool }{ { name: "valid", flag: &Float32Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "2147483647"}, expectedValue: 2147483647, }, { name: "invalid", flag: &Float32Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Float32(name)) } }) } } func Test_Float64Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue float64 expectErr bool }{ { name: "valid", flag: &Float64Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "-2147483648"}, expectedValue: -2147483648, }, { name: "invalid", flag: &Float64Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Float64(name)) } }) } } func Test_floatValue_String(t *testing.T) { var f float64 = 100 fv := floatValue[float64]{val: &f} assert.Equal(t, "100", fv.String()) } ================================================ FILE: flag_generic.go ================================================ package cli type GenericFlag = FlagBase[Value, NoConfig, genericValue] // -- Value Value type genericValue struct { val Value } // Below functions are to satisfy the ValueCreator interface func (f genericValue) Create(val Value, p *Value, c NoConfig) Value { *p = val return &genericValue{ val: *p, } } func (f genericValue) ToString(b Value) string { f.val = b return f.String() } // Below functions are to satisfy the flag.Value interface func (f *genericValue) Set(s string) error { if f.val != nil { return f.val.Set(s) } return nil } func (f *genericValue) Get() any { if f.val != nil { return f.val.Get() } return nil } func (f *genericValue) String() string { if f.val != nil { return f.val.String() } return "" } func (f *genericValue) IsBoolFlag() bool { if f.val == nil { return false } bf, ok := f.val.(boolFlag) return ok && bf.IsBoolFlag() } // Generic looks up the value of a local GenericFlag, returns // nil if not found func (cmd *Command) Generic(name string) Value { if v, ok := cmd.Value(name).(Value); ok { tracef("generic available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("generic NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } ================================================ FILE: flag_impl.go ================================================ package cli import ( "context" "flag" "fmt" "reflect" "strings" ) // Value represents a value as used by cli. // For now it implements the golang flag.Value interface type Value interface { flag.Value flag.Getter } type boolFlag interface { IsBoolFlag() bool } type multiValueParsingConfig struct { // SliceFlagSeparator is used to customize the separator for SliceFlag, the default is "," SliceFlagSeparator string // DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false DisableSliceFlagSeparator bool // MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "=" MapFlagKeyValueSeparator string } type multiValueParsingConfigSetter interface { // configuration of parsing setMultiValueParsingConfig(c multiValueParsingConfig) } // ValueCreator is responsible for creating a flag.Value emulation // as well as custom formatting // // T specifies the type // C specifies the config for the type type ValueCreator[T any, C any] interface { Create(T, *T, C) Value ToString(T) string } // NoConfig is for flags which dont need a custom configuration type NoConfig struct{} // FlagBase [T,C,VC] is a generic flag base which can be used // as a boilerplate to implement the most common interfaces // used by urfave/cli. // // T specifies the type // C specifies the configuration required(if any for that flag type) // VC specifies the value creator which creates the flag.Value emulation type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // name of the flag Category string `json:"category"` // category of the flag, if any DefaultText string `json:"defaultText"` // default text of the flag for usage purposes HideDefault bool `json:"hideDefault"` // whether to hide the default value in output Usage string `json:"usage"` // usage string for help output Sources ValueSourceChain `json:"-"` // sources to load flag value from Required bool `json:"required"` // whether the flag is required or not Hidden bool `json:"hidden"` // whether to hide the flag in help output Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well Value T `json:"defaultValue"` // default value for this flag if not set by from any source Destination *T `json:"-"` // destination pointer for value when set Aliases []string `json:"aliases"` // Aliases that are allowed for this flag TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes Action func(context.Context, *Command, T) error `json:"-"` // Action callback to be called when flag is set Config C `json:"config"` // Additional/Custom configuration associated with this flag type OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line Validator func(T) error `json:"-"` // custom function to validate this flag value ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not // unexported fields for internal use count int // number of times the flag has been set hasBeenSet bool // whether the flag has been set from env or file applied bool // whether the flag has been applied to a flag set already creator VC // value creator for this flag type value Value // value representing this flag's value } // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *FlagBase[T, C, V]) GetValue() string { var v V return v.ToString(f.Value) } // TypeName returns the type of the flag. func (f *FlagBase[T, C, V]) TypeName() string { ty := reflect.TypeOf(f.Value) if ty == nil { return "" } // convert the typename to generic type convertToGenericType := func(name string) string { prefixMap := map[string]string{ "float": "float", "int": "int", "uint": "uint", } for prefix, genericType := range prefixMap { if strings.HasPrefix(name, prefix) { return genericType } } return strings.ToLower(name) } switch ty.Kind() { // if it is a Slice, then return the slice's inner type. Will nested slices be used in the future? case reflect.Slice: elemType := ty.Elem() return convertToGenericType(elemType.Name()) // if it is a Map, then return the map's key and value types. case reflect.Map: keyType := ty.Key() valueType := ty.Elem() return fmt.Sprintf("%s=%s", convertToGenericType(keyType.Name()), convertToGenericType(valueType.Name())) default: return convertToGenericType(ty.Name()) } } // PostParse populates the flag given the flag set and environment func (f *FlagBase[T, C, V]) PostParse() error { tracef("postparse (flag=%[1]q)", f.Name) if !f.hasBeenSet { if val, source, found := f.Sources.LookupWithSource(); found { if val != "" || reflect.TypeOf(f.Value).Kind() == reflect.String { if err := f.Set(f.Name, val); err != nil { return fmt.Errorf( "could not parse %[1]q as %[2]T value from %[3]s for flag %[4]s: %[5]s", val, f.Value, source, f.Name, err, ) } } else if val == "" && reflect.TypeOf(f.Value).Kind() == reflect.Bool { _ = f.Set(f.Name, "false") } f.hasBeenSet = true } } return nil } // pass configuration of parsing to value func (f *FlagBase[T, C, V]) setMultiValueParsingConfig(c multiValueParsingConfig) { tracef("setMultiValueParsingConfig %T, %+v", f.value, f.value) if cf, ok := f.value.(multiValueParsingConfigSetter); ok { cf.setMultiValueParsingConfig(c) } } func (f *FlagBase[T, C, V]) PreParse() error { newVal := f.Value if f.Destination == nil { f.value = f.creator.Create(newVal, new(T), f.Config) } else { f.value = f.creator.Create(newVal, f.Destination, f.Config) } // Validate the given default or values set from external sources as well if f.Validator != nil && f.ValidateDefaults { if err := f.Validator(f.value.Get().(T)); err != nil { return err } } f.applied = true return nil } // Set applies given value from string func (f *FlagBase[T, C, V]) Set(_ string, val string) error { tracef("apply (flag=%[1]q)", f.Name) // TODO move this phase into a separate flag initialization function // if flag has been applied previously then it would have already been set // from env or file. So no need to apply the env set again. However // lots of units tests prior to persistent flags assumed that the // flag can be applied to different flag sets multiple times while still // keeping the env set. if !f.applied || f.Local { if err := f.PreParse(); err != nil { return err } f.applied = true } if f.count == 1 && f.OnlyOnce { return fmt.Errorf("cant duplicate this flag") } f.count++ if err := f.value.Set(val); err != nil { return err } f.hasBeenSet = true if f.Validator != nil { if err := f.Validator(f.value.Get().(T)); err != nil { return err } } return nil } func (f *FlagBase[T, C, V]) Get() any { if f.value != nil { return f.value.Get() } return f.Value } // IsDefaultVisible returns true if the flag is not hidden, otherwise false func (f *FlagBase[T, C, V]) IsDefaultVisible() bool { return !f.HideDefault } // String returns a readable representation of this value (for usage defaults) func (f *FlagBase[T, C, V]) String() string { return FlagStringer(f) } // IsSet returns whether or not the flag has been set through env or file func (f *FlagBase[T, C, V]) IsSet() bool { return f.hasBeenSet } // Names returns the names of the flag func (f *FlagBase[T, C, V]) Names() []string { return FlagNames(f.Name, f.Aliases) } // IsRequired returns whether or not the flag is required func (f *FlagBase[T, C, V]) IsRequired() bool { return f.Required } // IsVisible returns true if the flag is not hidden, otherwise false func (f *FlagBase[T, C, V]) IsVisible() bool { return !f.Hidden } // GetCategory returns the category of the flag func (f *FlagBase[T, C, V]) GetCategory() string { return f.Category } func (f *FlagBase[T, C, V]) SetCategory(c string) { f.Category = c } // GetUsage returns the usage string for the flag func (f *FlagBase[T, C, V]) GetUsage() string { return f.Usage } // GetEnvVars returns the env vars for this flag func (f *FlagBase[T, C, V]) GetEnvVars() []string { return f.Sources.EnvKeys() } // TakesValue returns true if the flag takes a value, otherwise false func (f *FlagBase[T, C, V]) TakesValue() bool { var t T return reflect.TypeOf(t) == nil || reflect.TypeOf(t).Kind() != reflect.Bool } // GetDefaultText returns the default text for this flag func (f *FlagBase[T, C, V]) GetDefaultText() string { return f.DefaultText } // RunAction executes flag action if set func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error { if f.Action != nil { return f.Action(ctx, cmd, f.value.Get().(T)) } return nil } // IsMultiValueFlag returns true if the value type T can take multiple // values from cmd line. This is true for slice and map type flags func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool { // TBD how to specify if reflect.TypeOf(f.Value) == nil { return false } kind := reflect.TypeOf(f.Value).Kind() return kind == reflect.Slice || kind == reflect.Map } // IsLocal returns false if flag needs to be persistent across subcommands func (f *FlagBase[T, C, VC]) IsLocal() bool { return f.Local } // IsBoolFlag returns whether the flag doesnt need to accept args func (f *FlagBase[T, C, VC]) IsBoolFlag() bool { bf, ok := f.value.(boolFlag) return ok && bf.IsBoolFlag() } // Count returns the number of times this flag has been invoked func (f *FlagBase[T, C, VC]) Count() int { return f.count } ================================================ FILE: flag_int.go ================================================ package cli import ( "strconv" "unsafe" ) type ( IntFlag = FlagBase[int, IntegerConfig, intValue[int]] Int8Flag = FlagBase[int8, IntegerConfig, intValue[int8]] Int16Flag = FlagBase[int16, IntegerConfig, intValue[int16]] Int32Flag = FlagBase[int32, IntegerConfig, intValue[int32]] Int64Flag = FlagBase[int64, IntegerConfig, intValue[int64]] ) // IntegerConfig is the configuration for all integer type flags type IntegerConfig struct { Base int } // -- int Value type intValue[T int | int8 | int16 | int32 | int64] struct { val *T base int } // Below functions are to satisfy the ValueCreator interface func (i intValue[T]) Create(val T, p *T, c IntegerConfig) Value { *p = val return &intValue[T]{ val: p, base: c.Base, } } func (i intValue[T]) ToString(b T) string { i.val = &b return i.String() } // Below functions are to satisfy the flag.Value interface func (i *intValue[T]) Set(s string) error { v, err := strconv.ParseInt(s, i.base, int(unsafe.Sizeof(T(0))*8)) if err != nil { return err } *i.val = T(v) return err } func (i *intValue[T]) Get() any { return *i.val } func (i *intValue[T]) String() string { base := i.base if base == 0 { base = 10 } return strconv.FormatInt(int64(*i.val), base) } // Int looks up the value of a local Int64Flag, returns // 0 if not found func (cmd *Command) Int(name string) int { return getInt[int](cmd, name) } // Int8 looks up the value of a local Int8Flag, returns // 0 if not found func (cmd *Command) Int8(name string) int8 { return getInt[int8](cmd, name) } // Int16 looks up the value of a local Int16Flag, returns // 0 if not found func (cmd *Command) Int16(name string) int16 { return getInt[int16](cmd, name) } // Int32 looks up the value of a local Int32Flag, returns // 0 if not found func (cmd *Command) Int32(name string) int32 { return getInt[int32](cmd, name) } // Int64 looks up the value of a local Int64Flag, returns // 0 if not found func (cmd *Command) Int64(name string) int64 { return getInt[int64](cmd, name) } func getInt[T int | int8 | int16 | int32 | int64](cmd *Command, name string) T { if v, ok := cmd.Value(name).(T); ok { tracef("int available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("int NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } ================================================ FILE: flag_int_slice.go ================================================ package cli type ( IntSlice = SliceBase[int, IntegerConfig, intValue[int]] Int8Slice = SliceBase[int8, IntegerConfig, intValue[int8]] Int16Slice = SliceBase[int16, IntegerConfig, intValue[int16]] Int32Slice = SliceBase[int32, IntegerConfig, intValue[int32]] Int64Slice = SliceBase[int64, IntegerConfig, intValue[int64]] IntSliceFlag = FlagBase[[]int, IntegerConfig, IntSlice] Int8SliceFlag = FlagBase[[]int8, IntegerConfig, Int8Slice] Int16SliceFlag = FlagBase[[]int16, IntegerConfig, Int16Slice] Int32SliceFlag = FlagBase[[]int32, IntegerConfig, Int32Slice] Int64SliceFlag = FlagBase[[]int64, IntegerConfig, Int64Slice] ) var ( NewIntSlice = NewSliceBase[int, IntegerConfig, intValue[int]] NewInt8Slice = NewSliceBase[int8, IntegerConfig, intValue[int8]] NewInt16Slice = NewSliceBase[int16, IntegerConfig, intValue[int16]] NewInt32Slice = NewSliceBase[int32, IntegerConfig, intValue[int32]] NewInt64Slice = NewSliceBase[int64, IntegerConfig, intValue[int64]] ) // IntSlice looks up the value of a local IntSliceFlag, returns // nil if not found func (cmd *Command) IntSlice(name string) []int { return getNumberSlice[int](cmd, name) } // Int8Slice looks up the value of a local Int8SliceFlag, returns // nil if not found func (cmd *Command) Int8Slice(name string) []int8 { return getNumberSlice[int8](cmd, name) } // Int16Slice looks up the value of a local Int16SliceFlag, returns // nil if not found func (cmd *Command) Int16Slice(name string) []int16 { return getNumberSlice[int16](cmd, name) } // Int32Slice looks up the value of a local Int32SliceFlag, returns // nil if not found func (cmd *Command) Int32Slice(name string) []int32 { return getNumberSlice[int32](cmd, name) } // Int64Slice looks up the value of a local Int64SliceFlag, returns // nil if not found func (cmd *Command) Int64Slice(name string) []int64 { return getNumberSlice[int64](cmd, name) } ================================================ FILE: flag_int_slice_test.go ================================================ package cli import ( "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCommand_IntSlice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []int expectErr bool }{ { flag: &IntSliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []int{1, 2, 3, 4}, }, { flag: &IntSliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []int{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.IntSlice(name), "IntSlice(%v)", name) } }) } } func TestCommand_Int8Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []int8 expectErr bool }{ { flag: &Int8SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []int8{1, 2, 3, 4}, }, { flag: &Int8SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []int8{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Int8Slice(name), "Int8Slice(%v)", name) } }) } } func TestCommand_Int16Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []int16 expectErr bool }{ { flag: &Int16SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []int16{1, 2, 3, 4}, }, { flag: &Int16SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []int16{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Int16Slice(name), "Int16Slice(%v)", name) } }) } } func TestCommand_Int32Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []int32 expectErr bool }{ { flag: &Int32SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []int32{1, 2, 3, 4}, }, { flag: &Int32SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []int32{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Int32Slice(name), "Int32Slice(%v)", name) } }) } } func TestCommand_Int64Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []int64 expectErr bool }{ { flag: &Int64SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []int64{1, 2, 3, 4}, }, { flag: &Int64SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []int64{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Int64Slice(name), "Int64Slice(%v)", name) } }) } } ================================================ FILE: flag_int_test.go ================================================ package cli import ( "flag" "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestIntFlag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue int expectErr bool }{ { name: "valid", flag: &IntFlag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "-234567"}, expectedValue: -234567, }, { name: "invalid", flag: &IntFlag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Int(name)) } }) } } func TestInt8Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue int8 expectErr bool }{ { name: "valid", flag: &Int8Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "127"}, expectedValue: 127, }, { name: "invalid", flag: &Int8Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Int8(name)) } }) } } func TestInt16Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue int16 expectErr bool }{ { name: "valid", flag: &Int16Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "32767"}, expectedValue: 32767, }, { name: "invalid", flag: &Int16Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, { name: "out of range", flag: &Int16Flag{ Name: "number", }, arguments: []string{"--number", "32768"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Int16(name)) } }) } } func TestInt32Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue int32 expectErr bool }{ { name: "valid", flag: &Int32Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "2147483647"}, expectedValue: 2147483647, }, { name: "invalid", flag: &Int32Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, { name: "out of range", flag: &Int32Flag{ Name: "number", }, arguments: []string{"--number", "2147483648"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Int32(name)) } }) } } func TestInt64Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue int64 expectErr bool }{ { name: "valid", flag: &Int64Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "-2147483648"}, expectedValue: -2147483648, }, { name: "invalid", flag: &Int64Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Int64(name)) } }) } } func TestIntFlagExt(t *testing.T) { tests := []struct { name string flag *flag.Flag config IntegerConfig arguments []string flagName string expectedValue string expectErr bool }{ { name: "valid", flag: &flag.Flag{ Name: "number", }, config: IntegerConfig{}, arguments: []string{"--number", "234567"}, flagName: "number", expectedValue: "234567", }, { name: "valid", flag: &flag.Flag{ Name: "number", }, config: IntegerConfig{Base: 10}, arguments: []string{"--number", "234567"}, flagName: "number", expectedValue: "234567", }, { name: "valid hex", flag: &flag.Flag{ Name: "number", DefValue: "FFFF", }, config: IntegerConfig{Base: 16}, arguments: []string{"--number", "39447"}, flagName: "number", expectedValue: "39447", }, { name: "valid hex default", flag: &flag.Flag{ Name: "number", DefValue: "FFFF", }, config: IntegerConfig{Base: 16}, expectedValue: "ffff", }, { name: "invalid", flag: &flag.Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var uValue intValue[int] var u int f := &extFlag{f: tt.flag} tt.flag.Value = uValue.Create(u, &u, tt.config) if tt.config.Base != 0 && tt.config.Base != 10 { t.Skipf("skipping %q with base %d, only base 10 is supported", tt.name, tt.config.Base) } cmd := &Command{ Name: "mock", Flags: []Flag{f}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tt.expectedValue, f.GetValue()) }) } } ================================================ FILE: flag_map_impl.go ================================================ package cli import ( "encoding/json" "fmt" "reflect" "sort" "strings" ) // MapBase wraps map[string]T to satisfy flag.Value type MapBase[T any, C any, VC ValueCreator[T, C]] struct { dict *map[string]T hasBeenSet bool value Value multiValueConfig multiValueParsingConfig } func (i MapBase[T, C, VC]) Create(val map[string]T, p *map[string]T, c C) Value { *p = map[string]T{} for k, v := range val { (*p)[k] = v } var t T np := new(T) var vc VC return &MapBase[T, C, VC]{ dict: p, value: vc.Create(t, np, c), } } // NewMapBase makes a *MapBase with default values func NewMapBase[T any, C any, VC ValueCreator[T, C]](defaults map[string]T) *MapBase[T, C, VC] { return &MapBase[T, C, VC]{ dict: &defaults, } } // configuration of slicing func (i *MapBase[T, C, VC]) setMultiValueParsingConfig(c multiValueParsingConfig) { i.multiValueConfig = c mvc := &i.multiValueConfig tracef( "set map parsing config - keyValueSeparator '%s', slice separator '%s', disable separator:%v", mvc.MapFlagKeyValueSeparator, mvc.SliceFlagSeparator, mvc.DisableSliceFlagSeparator, ) } // Set parses the value and appends it to the list of values func (i *MapBase[T, C, VC]) Set(value string) error { if !i.hasBeenSet { *i.dict = map[string]T{} i.hasBeenSet = true } if strings.HasPrefix(value, slPfx) { // Deserializing assumes overwrite _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.dict) i.hasBeenSet = true return nil } mvc := &i.multiValueConfig keyValueSeparator := mvc.MapFlagKeyValueSeparator if len(keyValueSeparator) == 0 { keyValueSeparator = defaultMapFlagKeyValueSeparator } tracef( "splitting map value '%s', keyValueSeparator '%s', slice separator '%s', disable separator:%v", value, keyValueSeparator, mvc.SliceFlagSeparator, mvc.DisableSliceFlagSeparator, ) for _, item := range flagSplitMultiValues(value, mvc.SliceFlagSeparator, mvc.DisableSliceFlagSeparator) { key, value, ok := strings.Cut(item, keyValueSeparator) if !ok { return fmt.Errorf("item %q is missing separator %q", item, keyValueSeparator) } if err := i.value.Set(value); err != nil { return err } (*i.dict)[key] = i.value.Get().(T) } return nil } // String returns a readable representation of this value (for usage defaults) func (i *MapBase[T, C, VC]) String() string { v := i.Value() var t T if reflect.TypeOf(t).Kind() == reflect.String { return fmt.Sprintf("%v", v) } return fmt.Sprintf("%T{%s}", v, i.ToString(v)) } // Serialize allows MapBase to fulfill Serializer func (i *MapBase[T, C, VC]) Serialize() string { jsonBytes, _ := json.Marshal(i.dict) return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) } // Value returns the mapping of values set by this flag func (i *MapBase[T, C, VC]) Value() map[string]T { if i.dict == nil { return map[string]T{} } return *i.dict } // Get returns the mapping of values set by this flag func (i *MapBase[T, C, VC]) Get() interface{} { return *i.dict } func (i MapBase[T, C, VC]) ToString(t map[string]T) string { var defaultVals []string var vc VC for _, k := range sortedKeys(t) { defaultVals = append(defaultVals, k+defaultMapFlagKeyValueSeparator+vc.ToString(t[k])) } return strings.Join(defaultVals, ", ") } func sortedKeys[T any](dict map[string]T) []string { keys := make([]string, 0, len(dict)) for k := range dict { keys = append(keys, k) } sort.Strings(keys) return keys } ================================================ FILE: flag_mutex.go ================================================ package cli // MutuallyExclusiveFlags defines a mutually exclusive flag group // Multiple option paths can be provided out of which // only one can be defined on cmdline // So for example // [ --foo | [ --bar something --darth somethingelse ] ] type MutuallyExclusiveFlags struct { // Flag list Flags [][]Flag // whether this group is required Required bool // Category to apply to all flags within group Category string } func (grp MutuallyExclusiveFlags) check(_ *Command) error { oneSet := false e := &mutuallyExclusiveGroup{} for _, grpf := range grp.Flags { for _, f := range grpf { if f.IsSet() { if oneSet { e.flag2Name = f.Names()[0] return e } e.flag1Name = f.Names()[0] oneSet = true break } if oneSet { break } } } if !oneSet && grp.Required { return &mutuallyExclusiveGroupRequiredFlag{flags: &grp} } return nil } func (grp MutuallyExclusiveFlags) propagateCategory() { for _, grpf := range grp.Flags { for _, f := range grpf { if cf, ok := f.(CategorizableFlag); ok { cf.SetCategory(grp.Category) } } } } ================================================ FILE: flag_mutex_test.go ================================================ package cli import ( "strings" "testing" "github.com/stretchr/testify/assert" ) func newCommand() *Command { return &Command{ MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { Flags: [][]Flag{ { &Int64Flag{ Name: "i", }, &StringFlag{ Name: "s", Sources: EnvVars("S_VAR"), }, &BoolWithInverseFlag{ Name: "b", }, }, { &Int64Flag{ Name: "t", Aliases: []string{"ai"}, Sources: EnvVars("T_VAR"), }, }, }, }, }, } } func TestFlagMutuallyExclusiveFlags(t *testing.T) { tests := []struct { name string args []string errStr string required bool envs map[string]string }{ { name: "simple", }, { name: "set one flag", args: []string{"--i", "10"}, }, { name: "set both flags", args: []string{"--i", "11", "--ai", "12"}, errStr: "option i cannot be set along with option ai", }, { name: "required none set", required: true, errStr: "one of these flags needs to be provided", }, { name: "required one set", args: []string{"--i", "10"}, required: true, }, { name: "required both set", args: []string{"--i", "11", "--ai", "12"}, errStr: "option i cannot be set along with option ai", required: true, }, { name: "set env var", required: true, envs: map[string]string{ "S_VAR": "some", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.envs != nil { for k, v := range test.envs { t.Setenv(k, v) } } cmd := newCommand() cmd.MutuallyExclusiveFlags[0].Required = test.required err := cmd.Run(buildTestContext(t), append([]string{"foo"}, test.args...)) if test.errStr == "" { assert.NoError(t, err) return } if err == nil { t.Error("Expected mutual exclusion error") return } switch err.(type) { case (*mutuallyExclusiveGroup), (*mutuallyExclusiveGroupRequiredFlag): if !strings.Contains(err.Error(), test.errStr) { t.Logf("Invalid error string %v", err) } default: t.Errorf("got invalid error type %T", err) } }) } } ================================================ FILE: flag_number_slice.go ================================================ package cli type numberType interface { int | int8 | int16 | int32 | int64 | float32 | float64 } func getNumberSlice[T numberType](cmd *Command, name string) []T { if v, ok := cmd.Value(name).([]T); ok { tracef("%T slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", *new(T), name, v, cmd.Name) return v } tracef("%T slice NOT available for flag name %[1]q (cmd=%[2]q)", *new(T), name, cmd.Name) return nil } ================================================ FILE: flag_number_slice_test.go ================================================ package cli import ( "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_getNumberSlice_int64(t *testing.T) { f := &Int64SliceFlag{Name: "numbers"} cmd := &Command{ Name: "mock", Flags: []Flag{f}, Writer: io.Discard, ErrWriter: io.Discard, } err := f.Set("", "1,2,3") require.NoError(t, err) expected := []int64{1, 2, 3} assert.Equal(t, expected, getNumberSlice[int64](cmd, "numbers")) } func Test_getNumberSlice_float64(t *testing.T) { f := &Float64SliceFlag{Name: "numbers"} cmd := &Command{ Name: "mock", Flags: []Flag{f}, Writer: io.Discard, ErrWriter: io.Discard, } err := f.Set("", "1,2,3") require.NoError(t, err) expected := []float64{1, 2, 3} assert.Equal(t, expected, getNumberSlice[float64](cmd, "numbers")) } ================================================ FILE: flag_slice_base.go ================================================ package cli import ( "encoding/json" "fmt" "reflect" "strings" ) // SliceBase wraps []T to satisfy flag.Value type SliceBase[T any, C any, VC ValueCreator[T, C]] struct { slice *[]T hasBeenSet bool value Value sliceSeparator string disableSliceSeparator bool } func (i SliceBase[T, C, VC]) Create(val []T, p *[]T, c C) Value { *p = []T{} *p = append(*p, val...) var t T np := new(T) var vc VC return &SliceBase[T, C, VC]{ slice: p, value: vc.Create(t, np, c), } } // NewSliceBase makes a *SliceBase with default values func NewSliceBase[T any, C any, VC ValueCreator[T, C]](defaults ...T) *SliceBase[T, C, VC] { return &SliceBase[T, C, VC]{ slice: &defaults, } } // configuration of slicing func (i *SliceBase[T, C, VC]) setMultiValueParsingConfig(c multiValueParsingConfig) { i.disableSliceSeparator = c.DisableSliceFlagSeparator i.sliceSeparator = c.SliceFlagSeparator tracef("set slice parsing config - slice separator '%s', disable separator:%v", i.sliceSeparator, i.disableSliceSeparator) } // Set parses the value and appends it to the list of values func (i *SliceBase[T, C, VC]) Set(value string) error { if !i.hasBeenSet { *i.slice = []T{} i.hasBeenSet = true } if strings.HasPrefix(value, slPfx) { // Deserializing assumes overwrite _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) i.hasBeenSet = true return nil } trimSpace := true // hack. How do we know if we should trim spaces? // it makes sense only for string slice flags which have // an option to not trim spaces. So by default we trim spaces // otherwise we let the underlying value type handle it. var t T if reflect.TypeOf(t).Kind() == reflect.String { trimSpace = false } tracef("splitting slice value '%s', separator '%s', disable separator:%v", value, i.sliceSeparator, i.disableSliceSeparator) for _, s := range flagSplitMultiValues(value, i.sliceSeparator, i.disableSliceSeparator) { if trimSpace { s = strings.TrimSpace(s) } if err := i.value.Set(s); err != nil { return err } *i.slice = append(*i.slice, i.value.Get().(T)) } return nil } // String returns a readable representation of this value (for usage defaults) func (i *SliceBase[T, C, VC]) String() string { var defaultVals []string var v VC for _, s := range *i.slice { defaultVals = append(defaultVals, v.ToString(s)) } return strings.Join(defaultVals, ", ") } // Serialize allows SliceBase to fulfill Serializer func (i *SliceBase[T, C, VC]) Serialize() string { jsonBytes, _ := json.Marshal(i.slice) return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) } // Value returns the slice of values set by this flag func (i *SliceBase[T, C, VC]) Value() []T { if i.slice == nil { return nil } return *i.slice } // Get returns the slice of values set by this flag func (i *SliceBase[T, C, VC]) Get() interface{} { return *i.slice } func (i SliceBase[T, C, VC]) ToString(t []T) string { i.slice = &t return i.String() } ================================================ FILE: flag_string.go ================================================ package cli import ( "fmt" "strings" ) type StringFlag = FlagBase[string, StringConfig, stringValue] // StringConfig defines the configuration for string flags type StringConfig struct { // Whether to trim whitespace of parsed value TrimSpace bool } // -- string Value type stringValue struct { destination *string trimSpace bool } // Below functions are to satisfy the ValueCreator interface func (s stringValue) Create(val string, p *string, c StringConfig) Value { *p = val return &stringValue{ destination: p, trimSpace: c.TrimSpace, } } func (s stringValue) ToString(val string) string { s.destination = &val return s.String() } // Below functions are to satisfy the flag.Value interface func (s *stringValue) Set(val string) error { if s.trimSpace { val = strings.TrimSpace(val) } *s.destination = val return nil } func (s *stringValue) Get() any { return *s.destination } func (s *stringValue) String() string { if s.destination != nil && *s.destination != "" { return fmt.Sprintf("%q", *s.destination) } return "" } func (cmd *Command) String(name string) string { if v, ok := cmd.Value(name).(string); ok { tracef("string available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("string NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return "" } ================================================ FILE: flag_string_map.go ================================================ package cli type ( StringMap = MapBase[string, StringConfig, stringValue] StringMapFlag = FlagBase[map[string]string, StringConfig, StringMap] ) var NewStringMap = NewMapBase[string, StringConfig, stringValue] // StringMap looks up the value of a local StringMapFlag, returns // nil if not found func (cmd *Command) StringMap(name string) map[string]string { if v, ok := cmd.Value(name).(map[string]string); ok { tracef("string map available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("string map NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } ================================================ FILE: flag_string_slice.go ================================================ package cli type ( StringSlice = SliceBase[string, StringConfig, stringValue] StringSliceFlag = FlagBase[[]string, StringConfig, StringSlice] ) var NewStringSlice = NewSliceBase[string, StringConfig, stringValue] // StringSlice looks up the value of a local StringSliceFlag, returns // nil if not found func (cmd *Command) StringSlice(name string) []string { if v, ok := cmd.Value(name).([]string); ok { tracef("string slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("string slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } ================================================ FILE: flag_test.go ================================================ package cli import ( "context" "errors" "flag" "fmt" "os" "reflect" "regexp" "runtime" "sort" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type Parser [2]string func (p *Parser) Set(value string) error { parts := strings.Split(value, ",") if len(parts) != 2 { return fmt.Errorf("invalid format") } (*p)[0] = parts[0] (*p)[1] = parts[1] return nil } func (p *Parser) String() string { return fmt.Sprintf("%s,%s", p[0], p[1]) } func (p *Parser) Get() interface{} { return p } var boolFlagTests = []struct { name string expected string }{ {"help", "--help\t"}, {"h", "-h\t"}, } func TestBoolFlagHelpOutput(t *testing.T) { for _, test := range boolFlagTests { fl := &BoolFlag{Name: test.name} output := fl.String() assert.Equal(t, test.expected, output) } } func TestBoolFlagApply_SetsAllNames(t *testing.T) { v := false cmd := buildMinimalTestCommand() cmd.Flags = []Flag{ &BoolFlag{Name: "wat", Aliases: []string{"W", "huh"}, Destination: &v}, } err := cmd.Run(buildTestContext(t), []string{"", "--wat", "-W", "--huh"}) assert.NoError(t, err) assert.True(t, v) } func TestBoolFlagValueFromCommand(t *testing.T) { tf := &BoolFlag{Name: "trueflag"} ff := &BoolFlag{Name: "falseflag"} cmd := buildMinimalTestCommand() cmd.Flags = []Flag{ tf, ff, } r := require.New(t) r.NoError(cmd.Set(tf.Name, "true")) r.NoError(cmd.Set(ff.Name, "false")) r.True(cmd.Bool(tf.Name)) r.False(cmd.Bool(ff.Name)) } func TestBoolFlagApply_SetsCount(t *testing.T) { v := false count := 0 cmd := buildMinimalTestCommand() cmd.Flags = []Flag{ &BoolFlag{Name: "wat", Aliases: []string{"W", "huh"}, Destination: &v, Config: BoolConfig{Count: &count}}, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--wat", "-W", "--huh"})) assert.True(t, v) assert.Equal(t, 3, count) } func TestBoolFlagCountFromCommand(t *testing.T) { boolCountTests := []struct { name string input []string expectedVal bool expectedCount int }{ { name: "3 count", input: []string{"main", "-tf", "-w", "-huh"}, expectedVal: true, expectedCount: 3, }, { name: "single count", input: []string{"main", "-huh"}, expectedVal: true, expectedCount: 1, }, { name: "zero count", input: []string{"main"}, expectedVal: false, expectedCount: 0, }, } flags := func() []Flag { return []Flag{ &BoolFlag{Name: "tf", Aliases: []string{"w", "huh"}}, &BoolWithInverseFlag{Name: "tf", Aliases: []string{"w", "huh"}}, } } for index := range flags() { for _, bct := range boolCountTests { t.Run(bct.name, func(t *testing.T) { bf := flags()[index] cmd := &Command{ Flags: []Flag{ bf, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), bct.input)) for _, alias := range bf.Names() { r.Equal(bct.expectedCount, cmd.Count(alias)) r.Equal(bct.expectedVal, cmd.Value(alias)) } }) } } } func TestFlagsFromEnv(t *testing.T) { testCases := []struct { name string input string output any fl Flag errContains string }{ { name: "BoolFlag valid true", input: "1", output: true, fl: &BoolFlag{Name: "debug", Sources: EnvVars("DEBUG")}, }, { name: "BoolFlag valid false", input: "false", output: false, fl: &BoolFlag{Name: "debug", Sources: EnvVars("DEBUG")}, }, { name: "BoolFlag invalid", input: "foobar", output: true, fl: &BoolFlag{Name: "debug", Sources: EnvVars("DEBUG")}, errContains: `could not parse "foobar" as bool value from environment variable ` + `"DEBUG" for flag debug:`, }, { name: "BoolInverse Empty", output: false, fl: &BoolWithInverseFlag{Name: "debug", Sources: EnvVars("DEBUG")}, }, { name: "DurationFlag valid", input: "1s", output: 1 * time.Second, fl: &DurationFlag{Name: "time", Sources: EnvVars("TIME")}, }, { name: "DurationFlag invalid", input: "foobar", output: false, fl: &DurationFlag{Name: "time", Sources: EnvVars("TIME")}, errContains: `could not parse "foobar" as time.Duration value from environment ` + `variable "TIME" for flag time:`, }, { name: "Float64Flag valid", input: "1.2", output: 1.2, fl: &FloatFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Float64Flag valid from int", input: "1", output: 1.0, fl: &FloatFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Float64Flag invalid", input: "foobar", output: 0, fl: &FloatFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "foobar" as float64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Int64Flag valid", input: "1", output: int64(1), fl: &Int64Flag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Int64Flag invalid from float", input: "1.2", output: 0, fl: &Int64Flag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "1.2" as int64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Int64Flag invalid", input: "foobar", output: 0, fl: &Int64Flag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "foobar" as int64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Int64Flag valid from hex", input: "deadBEEF", output: int64(3735928559), fl: &Int64Flag{Name: "seconds", Sources: EnvVars("SECONDS"), Config: IntegerConfig{Base: 16}}, }, { name: "Int64Flag invalid from octal", input: "08", output: 0, fl: &Int64Flag{Name: "seconds", Sources: EnvVars("SECONDS"), Config: IntegerConfig{Base: 8}}, errContains: `could not parse "08" as int64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Float64SliceFlag valid", input: "1.0,2", output: []float64{1, 2}, fl: &FloatSliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Float64SliceFlag invalid", input: "foobar", output: []float64{}, fl: &FloatSliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "foobar" as []float64 value from environment ` + `variable "SECONDS" for flag seconds:`, }, { name: "Generic", input: "foo,bar", output: &Parser{"foo", "bar"}, fl: &GenericFlag{Name: "names", Value: &Parser{}, Sources: EnvVars("NAMES")}, }, { name: "Int64SliceFlag valid", input: "1,2", output: []int64{1, 2}, fl: &Int64SliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Int64SliceFlag invalid from float", input: "1.2,2", output: []int64{}, fl: &Int64SliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "1.2,2" as []int64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Int64SliceFlag invalid", input: "foobar", output: []int64{}, fl: &Int64SliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "foobar" as []int64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Uint64SliceFlag valid", input: "1,2", output: []uint64{1, 2}, fl: &Uint64SliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Uint64SliceFlag invalid with float", input: "1.2,2", output: []uint64{}, fl: &Uint64SliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "1.2,2" as []uint64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Uint64SliceFlag invalid", input: "foobar", output: []uint64{}, fl: &Uint64SliceFlag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "foobar" as []uint64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "StringFlag valid", input: "foo", output: "foo", fl: &StringFlag{Name: "name", Sources: EnvVars("NAME")}, }, { name: "StringFlag valid with TrimSpace", input: " foo", output: "foo", fl: &StringFlag{Name: "names", Sources: EnvVars("NAMES"), Config: StringConfig{TrimSpace: true}}, }, { name: "StringSliceFlag valid", input: "foo,bar", output: []string{"foo", "bar"}, fl: &StringSliceFlag{Name: "names", Sources: EnvVars("NAMES")}, }, { name: "StringSliceFlag valid with TrimSpace", input: "foo , bar ", output: []string{"foo", "bar"}, fl: &StringSliceFlag{Name: "names", Sources: EnvVars("NAMES"), Config: StringConfig{TrimSpace: true}}, }, { name: "StringSliceFlag valid without TrimSpace", input: "foo , bar ", output: []string{"foo ", " bar "}, fl: &StringSliceFlag{Name: "names", Sources: EnvVars("NAMES")}, }, { name: "StringMapFlag valid", input: "foo=bar,empty=", output: map[string]string{"foo": "bar", "empty": ""}, fl: &StringMapFlag{Name: "names", Sources: EnvVars("NAMES")}, }, { name: "StringMapFlag valid with TrimSpace", input: "foo= bar ", output: map[string]string{"foo": "bar"}, fl: &StringMapFlag{Name: "names", Sources: EnvVars("NAMES"), Config: StringConfig{TrimSpace: true}}, }, { name: "Uint64Flag valid", input: "1", output: uint64(1), fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS")}, }, { name: "Uint64Flag valid leading zero", input: "08", output: uint64(8), fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS"), Config: IntegerConfig{Base: 10}}, }, { name: "Uint64Flag valid from octal", input: "755", output: uint64(493), fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS"), Config: IntegerConfig{Base: 8}}, }, { name: "Uint64Flag valid from hex", input: "deadBEEF", output: uint64(3735928559), fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS"), Config: IntegerConfig{Base: 16}}, }, { name: "Uint64Flag invalid octal", input: "08", output: 0, fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS"), Config: IntegerConfig{Base: 8}}, errContains: `could not parse "08" as uint64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Uint64Flag invalid float", input: "1.2", output: 0, fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "1.2" as uint64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, { name: "Uint64Flag invalid", input: "foobar", output: 0, fl: &Uint64Flag{Name: "seconds", Sources: EnvVars("SECONDS")}, errContains: `could not parse "foobar" as uint64 value from environment variable ` + `"SECONDS" for flag seconds:`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r := require.New(t) r.Implements((*DocGenerationFlag)(nil), tc.fl) f := tc.fl.(DocGenerationFlag) envVarSlice := f.GetEnvVars() t.Setenv(envVarSlice[0], tc.input) cmd := &Command{ Flags: []Flag{tc.fl}, Action: func(_ context.Context, cmd *Command) error { r.Equal(tc.output, cmd.Value(tc.fl.Names()[0])) r.True(tc.fl.IsSet()) r.Equal(tc.fl.Names(), cmd.FlagNames()) return nil }, } err := cmd.Run(buildTestContext(t), []string{"run"}) if tc.errContains != "" { r.NotNil(err) r.ErrorContains(err, tc.errContains) return } r.NoError(err) }) } } type nodocFlag struct { Flag Name string } func TestFlagStringifying(t *testing.T) { for _, tc := range []struct { name string fl Flag expected string }{ { name: "bool-flag", fl: &BoolFlag{Name: "vividly"}, expected: "--vividly\t", }, { name: "bool-flag-with-default-text", fl: &BoolFlag{Name: "wildly", DefaultText: "scrambled"}, expected: "--wildly\t(default: scrambled)", }, { name: "bool-inv-flag", fl: &BoolWithInverseFlag{Name: "vividly"}, expected: "--vividly, --no-vividly\t(default: false)", }, { name: "duration-flag", fl: &DurationFlag{Name: "scream-for"}, expected: "--scream-for duration\t(default: 0s)", }, { name: "duration-flag-with-default-text", fl: &DurationFlag{Name: "feels-about", DefaultText: "whimsically"}, expected: "--feels-about duration\t(default: whimsically)", }, { name: "float64-flag", fl: &FloatFlag{Name: "arduous"}, expected: "--arduous float\t(default: 0)", }, { name: "float64-flag-with-default-text", fl: &FloatFlag{Name: "filibuster", DefaultText: "42"}, expected: "--filibuster float\t(default: 42)", }, { name: "float64-slice-flag", fl: &FloatSliceFlag{Name: "pizzas"}, expected: "--pizzas float [ --pizzas float ]\t", }, { name: "float64-slice-flag-with-default-text", fl: &FloatSliceFlag{Name: "pepperonis", DefaultText: "shaved"}, expected: "--pepperonis float [ --pepperonis float ]\t(default: shaved)", }, { name: "generic-flag", fl: &GenericFlag{Name: "yogurt"}, expected: "--yogurt value\t", }, { name: "generic-flag-with-default-text", fl: &GenericFlag{Name: "ricotta", DefaultText: "plops"}, expected: "--ricotta value\t(default: plops)", }, { name: "int-flag", fl: &Int64Flag{Name: "grubs"}, expected: "--grubs int\t(default: 0)", }, { name: "int-flag-with-default-text", fl: &Int64Flag{Name: "poisons", DefaultText: "11ty"}, expected: "--poisons int\t(default: 11ty)", }, { name: "int-slice-flag", fl: &Int64SliceFlag{Name: "pencils"}, expected: "--pencils int [ --pencils int ]\t", }, { name: "int-slice-flag-with-default-text", fl: &Int64Flag{Name: "pens", DefaultText: "-19"}, expected: "--pens int\t(default: -19)", }, { name: "uint-slice-flag", fl: &Uint64SliceFlag{Name: "pencils"}, expected: "--pencils uint [ --pencils uint ]\t", }, { name: "uint-slice-flag-with-default-text", fl: &Uint64Flag{Name: "pens", DefaultText: "29"}, expected: "--pens uint\t(default: 29)", }, { name: "int64-flag", fl: &Int64Flag{Name: "flume"}, expected: "--flume int\t(default: 0)", }, { name: "int64-flag-with-default-text", fl: &Int64Flag{Name: "shattering", DefaultText: "22"}, expected: "--shattering int\t(default: 22)", }, { name: "uint64-slice-flag", fl: &Uint64SliceFlag{Name: "drawers"}, expected: "--drawers uint [ --drawers uint ]\t", }, { name: "uint64-slice-flag-with-default-text", fl: &Uint64SliceFlag{Name: "handles", DefaultText: "-2"}, expected: "--handles uint [ --handles uint ]\t(default: -2)", }, { name: "string-flag", fl: &StringFlag{Name: "arf-sound"}, expected: "--arf-sound string\t", }, { name: "string-flag-with-default-text", fl: &StringFlag{Name: "woof-sound", DefaultText: "urp"}, expected: "--woof-sound string\t(default: urp)", }, { name: "string-slice-flag", fl: &StringSliceFlag{Name: "meow-sounds"}, expected: "--meow-sounds string [ --meow-sounds string ]\t", }, { name: "string-slice-flag-with-default-text", fl: &StringSliceFlag{Name: "moo-sounds", DefaultText: "awoo"}, expected: "--moo-sounds string [ --moo-sounds string ]\t(default: awoo)", }, { name: "timestamp-flag", fl: &TimestampFlag{Name: "eating"}, expected: "--eating time\t", }, { name: "timestamp-flag-with-default-text", fl: &TimestampFlag{Name: "sleeping", DefaultText: "earlier"}, expected: "--sleeping time\t(default: earlier)", }, { name: "uint-flag", fl: &Uint64Flag{Name: "jars"}, expected: "--jars uint\t(default: 0)", }, { name: "uint-flag-with-default-text", fl: &Uint64Flag{Name: "bottles", DefaultText: "99"}, expected: "--bottles uint\t(default: 99)", }, { name: "uint64-flag", fl: &Uint64Flag{Name: "cans"}, expected: "--cans uint\t(default: 0)", }, { name: "uint64-flag-with-default-text", fl: &Uint64Flag{Name: "tubes", DefaultText: "13"}, expected: "--tubes uint\t(default: 13)", }, { name: "nodoc-flag", fl: &nodocFlag{Name: "scarecrow"}, expected: "", }, } { t.Run(tc.name, func(ct *testing.T) { s := stringifyFlag(tc.fl) assert.Equal(t, tc.expected, s, "stringified flag %q does not match expected", s) }) } } var stringFlagTests = []struct { name string aliases []string usage string value string expected string }{ {"foo", nil, "", "", "--foo string\t"}, {"f", nil, "", "", "-f string\t"}, {"f", nil, "The total `foo` desired", "all", "-f foo\tThe total foo desired (default: \"all\")"}, {"test", nil, "", "Something", "--test string\t(default: \"Something\")"}, {"config", []string{"c"}, "Load configuration from `FILE`", "", "--config FILE, -c FILE\tLoad configuration from FILE"}, {"config", []string{"c"}, "Load configuration from `CONFIG`", "config.json", "--config CONFIG, -c CONFIG\tLoad configuration from CONFIG (default: \"config.json\")"}, } func TestStringFlagHelpOutput(t *testing.T) { for _, test := range stringFlagTests { fl := &StringFlag{Name: test.name, Aliases: test.aliases, Usage: test.usage, Value: test.value} assert.Equal(t, test.expected, fl.String()) } } func TestStringFlagDefaultText(t *testing.T) { fl := &StringFlag{Name: "foo", Aliases: nil, Usage: "amount of `foo` requested", Value: "none", DefaultText: "all of it"} expected := "--foo foo\tamount of foo requested (default: all of it)" assert.Equal(t, expected, fl.String()) } func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_FOO", "derp") for _, test := range stringFlagTests { fl := &StringFlag{Name: test.name, Aliases: test.aliases, Value: test.value, Sources: EnvVars("APP_FOO")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_FOO"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } var _ = []struct { name string aliases []string usage string value string prefixer FlagNamePrefixFunc expected string }{ {name: "foo", usage: "", value: "", prefixer: func(a []string, b string) string { return fmt.Sprintf("name: %s, ph: %s", a, b) }, expected: "name: foo, ph: value\t"}, {name: "f", usage: "", value: "", prefixer: func(a []string, b string) string { return fmt.Sprintf("name: %s, ph: %s", a, b) }, expected: "name: f, ph: value\t"}, {name: "f", usage: "The total `foo` desired", value: "all", prefixer: func(a []string, b string) string { return fmt.Sprintf("name: %s, ph: %s", a, b) }, expected: "name: f, ph: foo\tThe total foo desired (default: \"all\")"}, {name: "test", usage: "", value: "Something", prefixer: func(a []string, b string) string { return fmt.Sprintf("name: %s, ph: %s", a, b) }, expected: "name: test, ph: value\t(default: \"Something\")"}, {name: "config", aliases: []string{"c"}, usage: "Load configuration from `FILE`", value: "", prefixer: func(a []string, b string) string { return fmt.Sprintf("name: %s, ph: %s", a, b) }, expected: "name: config,c, ph: FILE\tLoad configuration from FILE"}, {name: "config", aliases: []string{"c"}, usage: "Load configuration from `CONFIG`", value: "config.json", prefixer: func(a []string, b string) string { return fmt.Sprintf("name: %s, ph: %s", a, b) }, expected: "name: config,c, ph: CONFIG\tLoad configuration from CONFIG (default: \"config.json\")"}, } func TestStringFlagApply_SetsAllNames(t *testing.T) { v := "mmm" cmd := &Command{ Flags: []Flag{ &StringFlag{Name: "hay", Aliases: []string{"H", "hayyy"}, Destination: &v}, }, } err := cmd.Run(buildTestContext(t), []string{"", "--hay", "u", "-H", "yuu", "--hayyy", "YUUUU"}) assert.NoError(t, err) assert.Equal(t, "YUUUU", v) } func TestStringFlagValueFromCommand(t *testing.T) { f := &StringFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ f, }, } require.NoError(t, cmd.Set("myflag", "foobar")) require.Equal(t, "foobar", cmd.String(f.Name)) } var _ = []struct { name string env string hinter FlagEnvHintFunc expected string }{ {"foo", "", func(a []string, b string) string { return fmt.Sprintf("env: %s, str: %s", a, b) }, "env: , str: --foo value\t"}, {"f", "", func(a []string, b string) string { return fmt.Sprintf("env: %s, str: %s", a, b) }, "env: , str: -f value\t"}, {"foo", "ENV_VAR", func(a []string, b string) string { return fmt.Sprintf("env: %s, str: %s", a, b) }, "env: ENV_VAR, str: --foo value\t"}, {"f", "ENV_VAR", func(a []string, b string) string { return fmt.Sprintf("env: %s, str: %s", a, b) }, "env: ENV_VAR, str: -f value\t"}, } // func TestFlagEnvHinter(t *testing.T) { // defer func() { // FlagEnvHinter = withEnvHint // }() // // for _, test := range envHintFlagTests { // FlagEnvHinter = test.hinter // fl := StringFlag{Name: test.name, Sources: ValueSources{test.env}} // output := fl.String() // if output != test.expected { // t.Errorf("%q does not match %q", output, test.expected) // } // } // } var stringSliceFlagTests = []struct { name string aliases []string value []string expected string }{ {"foo", nil, []string{}, "--foo string [ --foo string ]\t"}, {"f", nil, []string{}, "-f string [ -f string ]\t"}, {"f", nil, []string{"Lipstick"}, "-f string [ -f string ]\t(default: \"Lipstick\")"}, {"test", nil, []string{"Something"}, "--test string [ --test string ]\t(default: \"Something\")"}, {"dee", []string{"d"}, []string{"Inka", "Dinka", "dooo"}, "--dee string, -d string [ --dee string, -d string ]\t(default: \"Inka\", \"Dinka\", \"dooo\")"}, } func TestStringSliceFlagHelpOutput(t *testing.T) { for _, test := range stringSliceFlagTests { f := &StringSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} assert.Equal(t, test.expected, f.String()) } } func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_QWWX", "11,4") for _, test := range stringSliceFlagTests { fl := &StringSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, Sources: EnvVars("APP_QWWX")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_QWWX"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%q does not end with"+expectedSuffix, output) } } } func TestStringSliceFlagApply_SetsAllNames(t *testing.T) { fl := &StringSliceFlag{Name: "goat", Aliases: []string{"G", "gooots"}} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{"", "--goat", "aaa", "-G", "bbb", "--gooots", "eeeee"}) assert.NoError(t, err) } func TestStringSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { t.Setenv("MY_GOAT", "vincent van goat,scape goat") fl := &StringSliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT")} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{""}) assert.NoError(t, err) assert.Equal(t, []string{"vincent van goat", "scape goat"}, cmd.Value("goat")) } func TestStringSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { t.Setenv("MY_GOAT", "vincent van goat,scape goat") val := []string{`some default`, `values here`} fl := &StringSliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{""}) assert.NoError(t, err) assert.Equal(t, []string{"vincent van goat", "scape goat"}, cmd.Value("goat")) } func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := []string{"UA", "US"} dest := []string{"CA"} fl := StringSliceFlag{Name: "country", Value: defValue, Destination: &dest} cmd := &Command{ Flags: []Flag{ &fl, }, } err := cmd.Run(buildTestContext(t), []string{""}) assert.NoError(t, err) assert.Equal(t, defValue, dest) } func TestStringSliceFlagValueFromCommand(t *testing.T) { f := &StringSliceFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ f, }, } require.NoError(t, cmd.Set("myflag", "a")) require.NoError(t, cmd.Set("myflag", "b")) require.NoError(t, cmd.Set("myflag", "c")) require.Equal(t, []string{"a", "b", "c"}, cmd.StringSlice(f.Name)) } var intFlagTests = []struct { name string expected string }{ {"hats", "--hats int\t(default: 9)"}, {"H", "-H int\t(default: 9)"}, } func TestIntFlagHelpOutput(t *testing.T) { for _, test := range intFlagTests { fl := &Int64Flag{Name: test.name, Value: 9} assert.Equal(t, test.expected, fl.String()) } } func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_BAR", "2") for _, test := range intFlagTests { fl := &Int64Flag{Name: test.name, Sources: EnvVars("APP_BAR")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_BAR"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } func TestIntFlagApply_SetsAllNames(t *testing.T) { v := int64(3) cmd := &Command{ Flags: []Flag{ &Int64Flag{Name: "banana", Aliases: []string{"B", "banannanana"}, Destination: &v}, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"", "--banana", "1", "-B", "2", "--banannanana", "5"})) r.Equal(int64(5), v) } func TestIntFlagValueFromCommand(t *testing.T) { fl := &Int64Flag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ fl, }, } require.NoError(t, cmd.Set("myflag", "42")) require.Equal(t, int64(42), cmd.Int64(fl.Name)) } var uintFlagTests = []struct { name string expected string }{ {"nerfs", "--nerfs uint\t(default: 41)"}, {"N", "-N uint\t(default: 41)"}, } func TestUintFlagHelpOutput(t *testing.T) { for _, test := range uintFlagTests { fl := &Uint64Flag{Name: test.name, Value: 41} assert.Equal(t, test.expected, fl.String()) } } func TestUintFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_BAR", "2") for _, test := range uintFlagTests { fl := &Uint64Flag{Name: test.name, Sources: EnvVars("APP_BAR")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_BAR"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } func TestUintFlagValueFromCommand(t *testing.T) { fl := &Uint64Flag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ fl, }, } require.NoError(t, cmd.Set("myflag", "42")) require.Equal(t, uint64(42), cmd.Uint64(fl.Name)) } var uint64FlagTests = []struct { name string expected string }{ {"gerfs", "--gerfs uint\t(default: 8589934582)"}, {"G", "-G uint\t(default: 8589934582)"}, } func TestUint64FlagHelpOutput(t *testing.T) { for _, test := range uint64FlagTests { fl := Uint64Flag{Name: test.name, Value: 8589934582} assert.Equal(t, test.expected, fl.String()) } } func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_BAR", "2") for _, test := range uint64FlagTests { fl := &Uint64Flag{Name: test.name, Sources: EnvVars("APP_BAR")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_BAR"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } func TestUint64FlagValueFromCommand(t *testing.T) { f := &Uint64Flag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ f, }, } require.NoError(t, cmd.Set("myflag", "42")) require.Equal(t, uint64(42), cmd.Uint64(f.Name)) } var durationFlagTests = []struct { name string expected string }{ {"hooting", "--hooting duration\t(default: 1s)"}, {"H", "-H duration\t(default: 1s)"}, } func TestDurationFlagHelpOutput(t *testing.T) { for _, test := range durationFlagTests { fl := &DurationFlag{Name: test.name, Value: 1 * time.Second} assert.Equal(t, test.expected, fl.String()) } } func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_BAR", "2h3m6s") for _, test := range durationFlagTests { fl := &DurationFlag{Name: test.name, Sources: EnvVars("APP_BAR")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_BAR"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } func TestDurationFlagApply_SetsAllNames(t *testing.T) { v := time.Second * 20 cmd := &Command{ Flags: []Flag{ &DurationFlag{Name: "howmuch", Aliases: []string{"H", "whyyy"}, Destination: &v}, }, } err := cmd.Run(buildTestContext(t), []string{"", "--howmuch", "30s", "-H", "5m", "--whyyy", "30h"}) assert.NoError(t, err) assert.Equal(t, time.Hour*30, v) } func TestDurationFlagValueFromCommand(t *testing.T) { f := &DurationFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ f, }, } require.NoError(t, cmd.Set("myflag", "42s")) require.Equal(t, 42*time.Second, cmd.Duration(f.Name)) } var intSliceFlagTests = []struct { name string aliases []string value []int64 expected string }{ {"heads", nil, []int64{}, "--heads int [ --heads int ]\t"}, {"H", nil, []int64{}, "-H int [ -H int ]\t"}, {"H", []string{"heads"}, []int64{9, 3}, "-H int, --heads int [ -H int, --heads int ]\t(default: 9, 3)"}, } func TestIntSliceFlagHelpOutput(t *testing.T) { for _, test := range intSliceFlagTests { fl := &Int64SliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} assert.Equal(t, test.expected, fl.String()) } } func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_SMURF", "42,3") for _, test := range intSliceFlagTests { fl := &Int64SliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, Sources: EnvVars("APP_SMURF")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_SMURF"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%q does not end with"+expectedSuffix, output) } } } func TestIntSliceFlagApply_SetsAllNames(t *testing.T) { fl := &Int64SliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{"", "--bits", "23", "-B", "3", "--bips", "99"}) assert.NoError(t, err) } func TestIntSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { t.Setenv("MY_GOAT", "1 , 2") fl := &Int64SliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT")} cmd := &Command{ Flags: []Flag{ fl, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{""})) r.NoError(fl.PostParse()) r.Equal([]int64{1, 2}, cmd.Value("goat")) } func TestIntSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { t.Setenv("MY_GOAT", "1 , 2") val := []int64{3, 4} fl := &Int64SliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val} cmd := &Command{ Flags: []Flag{ fl, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{""})) r.Equal([]int64{3, 4}, val) r.Equal([]int64{1, 2}, cmd.Value("goat")) } func TestIntSliceFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := []int64{1, 2} dest := []int64{3} fl := &Int64SliceFlag{Name: "country", Value: defValue, Destination: &dest} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{""}) assert.NoError(t, err) assert.Equal(t, defValue, dest) } func TestIntSliceFlagApply_ParentContext(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []int64{1, 2, 3}}, }, Commands: []*Command{ { Name: "child", Action: func(_ context.Context, cmd *Command) error { require.Equalf(t, []int64{1, 2, 3}, cmd.Int64Slice("numbers"), "child context unable to view parent flag") return nil }, }, }, }).Run(buildTestContext(t), []string{"run", "child"}) } func TestIntSliceFlag_SetFromParentCommand(t *testing.T) { fl := &Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []int64{1, 2, 3, 4}} cmd := &Command{ parent: &Command{ Flags: []Flag{ fl, }, }, } require.Equalf(t, []int64{1, 2, 3, 4}, cmd.Int64Slice("numbers"), "child context unable to view parent flag") } func TestIntSliceFlagValueFromCommand(t *testing.T) { f := &Int64SliceFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ f, }, } require.NoError(t, cmd.Set("myflag", "1")) require.NoError(t, cmd.Set("myflag", "2")) require.NoError(t, cmd.Set("myflag", "3")) require.Equal(t, []int64{1, 2, 3}, cmd.Int64Slice(f.Name)) } var uintSliceFlagTests = []struct { name string aliases []string value []uint64 expected string }{ {"heads", nil, []uint64{}, "--heads uint [ --heads uint ]\t"}, {"H", nil, []uint64{}, "-H uint [ -H uint ]\t"}, { "heads", []string{"H"}, []uint64{2, 17179869184}, "--heads uint, -H uint [ --heads uint, -H uint ]\t(default: 2, 17179869184)", }, } func TestUintSliceFlagHelpOutput(t *testing.T) { for _, test := range uintSliceFlagTests { t.Run(test.name, func(t *testing.T) { fl := &Uint64SliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} require.Equal(t, test.expected, fl.String()) }) } } func TestUintSliceFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_SMURF", "42,17179869184") for _, test := range uintSliceFlagTests { fl := &Uint64SliceFlag{Name: test.name, Value: test.value, Sources: EnvVars("APP_SMURF")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_SMURF"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%q does not end with"+expectedSuffix, output) } } } func TestUintSliceFlagApply_SetsAllNames(t *testing.T) { fl := &Uint64SliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{"", "--bits", "23", "-B", "3", "--bips", "99"}) assert.NoError(t, err) } func TestUintSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { t.Setenv("MY_GOAT", "1 , 2") fl := &Uint64SliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT")} cmd := &Command{ Flags: []Flag{ fl, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{""})) r.Equal([]uint64{1, 2}, cmd.Value("goat")) } func TestUintSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { t.Setenv("MY_GOAT", "1 , 2") val := NewUint64Slice(3, 4) fl := &Uint64SliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val.Value()} cmd := &Command{ Flags: []Flag{ fl, }, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{""})) r.Equal([]uint64{3, 4}, val.Value()) r.Equal([]uint64{1, 2}, cmd.Value("goat")) } func TestUintSliceFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := []uint64{1, 2} var dest []uint64 fl := &Uint64SliceFlag{Name: "country", Value: defValue, Destination: &dest} cmd := &Command{ Flags: []Flag{ fl, }, } err := cmd.Run(buildTestContext(t), []string{""}) assert.NoError(t, err) assert.Equal(t, defValue, dest) } func TestUint64SliceFlagApply_ParentContext(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Uint64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint64{1, 2, 3}}, }, Commands: []*Command{ { Name: "child", Action: func(_ context.Context, cmd *Command) error { require.Equalf( t, []uint64{1, 2, 3}, cmd.Uint64Slice("numbers"), "child context unable to view parent flag", ) return nil }, }, }, }).Run(buildTestContext(t), []string{"run", "child"}) } func TestUintSliceFlag_SetFromParentCommand(t *testing.T) { fl := &UintSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint{1, 2, 3, 4}} cmd := &Command{ parent: &Command{ Flags: []Flag{ fl, }, }, } r := require.New(t) r.Equalf( []uint{1, 2, 3, 4}, cmd.UintSlice("numbers"), "child context unable to view parent flag", ) } func TestUintSliceFlag_ReturnNil(t *testing.T) { fl := &Uint64SliceFlag{} r := require.New(t) cmd := &Command{ parent: &Command{ Flags: []Flag{ fl, }, }, } r.Equalf( []uint64(nil), cmd.Uint64Slice("numbers"), "child context unable to view parent flag", ) } var uint64SliceFlagTests = []struct { name string aliases []string value []uint64 expected string }{ {"heads", nil, []uint64{}, "--heads uint [ --heads uint ]\t"}, {"H", nil, []uint64{}, "-H uint [ -H uint ]\t"}, { "heads", []string{"H"}, []uint64{2, 17179869184}, "--heads uint, -H uint [ --heads uint, -H uint ]\t(default: 2, 17179869184)", }, } func TestUint64SliceFlagHelpOutput(t *testing.T) { for _, test := range uint64SliceFlagTests { fl := Uint64SliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} assert.Equal(t, test.expected, fl.String()) } } func TestUint64SliceFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_SMURF", "42,17179869184") for _, test := range uint64SliceFlagTests { fl := Uint64SliceFlag{Name: test.name, Value: test.value, Sources: EnvVars("APP_SMURF")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_SMURF"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%q does not end with"+expectedSuffix, output) } } } func TestUint64SliceFlagApply_SetsAllNames(t *testing.T) { fl := Uint64SliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} cmd := &Command{ Flags: []Flag{ &fl, }, } err := cmd.Run(buildTestContext(t), []string{"", "--bits", "23", "-B", "3", "--bips", "99"}) assert.NoError(t, err) } func TestUint64SliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { t.Setenv("MY_GOAT", "1 , 2") fl := Uint64SliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT")} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, []uint64{1, 2}, cmd.Value("goat")) } func TestUint64SliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { t.Setenv("MY_GOAT", "1 , 2") val := []uint64{3, 4} fl := Uint64SliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, []uint64{1, 2}, cmd.Value("goat")) } func TestUint64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := []uint64{1, 2} dest := []uint64{3} fl := Uint64SliceFlag{Name: "country", Value: defValue, Destination: &dest} cmd := &Command{ Flags: []Flag{ &fl, }, } err := cmd.Run(buildTestContext(t), []string{""}) assert.NoError(t, err) assert.Equal(t, defValue, dest) } func TestUint64SliceFlagApply_ParentCommand(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Uint64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint64{1, 2, 3}}, }, Commands: []*Command{ { Name: "child", Action: func(_ context.Context, cmd *Command) error { require.Equalf( t, []uint64{1, 2, 3}, cmd.Uint64Slice("numbers"), "child context unable to view parent flag", ) return nil }, }, }, }).Run(buildTestContext(t), []string{"run", "child"}) } func TestUint64SliceFlag_SetFromParentCommand(t *testing.T) { fl := &Uint64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint64{1, 2, 3, 4}} cmd := &Command{ parent: &Command{ Flags: []Flag{ fl, }, }, } r := require.New(t) r.Equalf( []uint64{1, 2, 3, 4}, cmd.Uint64Slice("numbers"), "child context unable to view parent flag", ) } func TestUint64SliceFlag_ReturnNil(t *testing.T) { fl := &Uint64SliceFlag{} cmd := &Command{ parent: &Command{ Flags: []Flag{ fl, }, }, } r := require.New(t) r.Equalf( []uint64(nil), cmd.Uint64Slice("numbers"), "child context unable to view parent flag", ) } var float64FlagTests = []struct { name string expected string }{ {"hooting", "--hooting float\t(default: 0.1)"}, {"H", "-H float\t(default: 0.1)"}, } func TestFloat64FlagHelpOutput(t *testing.T) { for _, test := range float64FlagTests { f := &FloatFlag{Name: test.name, Value: 0.1} assert.Equal(t, test.expected, f.String()) } } func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_BAZ", "99.4") for _, test := range float64FlagTests { fl := &FloatFlag{Name: test.name, Sources: EnvVars("APP_BAZ")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_BAZ"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } func TestFloat64FlagApply_SetsAllNames(t *testing.T) { v := 99.1 fl := FloatFlag{Name: "noodles", Aliases: []string{"N", "nurbles"}, Destination: &v} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--noodles", "1.3", "-N", "11", "--nurbles", "43.33333"})) assert.Equal(t, float64(43.33333), v) } func TestFloat64FlagValueFromCommand(t *testing.T) { fl := &FloatFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ fl, }, } require.NoError(t, cmd.Set("myflag", "1.23")) require.Equal(t, 1.23, cmd.Float(fl.Name)) } var float64SliceFlagTests = []struct { name string aliases []string value []float64 expected string }{ {"heads", nil, []float64{}, "--heads float [ --heads float ]\t"}, {"H", nil, []float64{}, "-H float [ -H float ]\t"}, { "heads", []string{"H"}, []float64{0.1234, -10.5}, "--heads float, -H float [ --heads float, -H float ]\t(default: 0.1234, -10.5)", }, } func TestFloat64SliceFlagHelpOutput(t *testing.T) { for _, test := range float64SliceFlagTests { fl := FloatSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} assert.Equal(t, test.expected, fl.String()) } } func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_SMURF", "0.1234,-10.5") for _, test := range float64SliceFlagTests { fl := FloatSliceFlag{Name: test.name, Value: test.value, Sources: EnvVars("APP_SMURF")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_SMURF"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%q does not end with"+expectedSuffix, output) } } } func TestFloat64SliceFlagApply_SetsAllNames(t *testing.T) { fl := FloatSliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--bits", "23", "-B", "3", "--bips", "99"})) } func TestFloat64SliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { t.Setenv("MY_GOAT", "1.0 , 2.0") fl := FloatSliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT")} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, []float64{1, 2}, cmd.Value("goat")) } func TestFloat64SliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { t.Setenv("MY_GOAT", "1.0 , 2.0") val := []float64{3.0, 4.0} fl := FloatSliceFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, []float64{1, 2}, cmd.Value("goat")) } func TestFloat64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := []float64{1.0, 2.0} dest := []float64{3} fl := FloatSliceFlag{Name: "country", Value: defValue, Destination: &dest} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, defValue, dest) } func TestFloat64SliceFlagValueFromCommand(t *testing.T) { fl := FloatSliceFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) require.NoError(t, cmd.Set("myflag", "1.23")) require.NoError(t, cmd.Set("myflag", "4.56")) require.Equal(t, []float64{1.23, 4.56}, cmd.FloatSlice(fl.Name)) } func TestFloat64SliceFlagApply_ParentCommand(t *testing.T) { _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []float64{1.0, 2.0, 3.0}}, }, Commands: []*Command{ { Name: "child", Action: func(_ context.Context, cmd *Command) error { require.Equalf(t, []float64{1.0, 2.0, 3.0}, cmd.FloatSlice("numbers"), "child context unable to view parent flag") return nil }, }, }, }).Run(buildTestContext(t), []string{"run", "child"}) } var genericFlagTests = []struct { name string value Value expected string }{ {"toads", &Parser{"abc", "def"}, "--toads value\ttest flag (default: abc,def)"}, {"t", &Parser{"abc", "def"}, "-t value\ttest flag (default: abc,def)"}, } func TestGenericFlagHelpOutput(t *testing.T) { for _, test := range genericFlagTests { fl := &GenericFlag{Name: test.name, Value: test.value, Usage: "test flag"} assert.Equal(t, test.expected, fl.String()) } } func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_ZAP", "3") for _, test := range genericFlagTests { fl := &GenericFlag{Name: test.name, Sources: EnvVars("APP_ZAP")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_ZAP"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%s does not end with"+expectedSuffix, output) } } } func TestGenericFlagApply_SetsAllNames(t *testing.T) { fl := GenericFlag{Name: "orbs", Aliases: []string{"O", "obrs"}, Value: &Parser{}} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--orbs", "eleventy,3", "-O", "4,bloop", "--obrs", "19,s"})) } func TestGenericFlagValueFromCommand(t *testing.T) { cmd := &Command{ Name: "foo", Flags: []Flag{ &GenericFlag{Name: "myflag", Value: &Parser{}}, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"foo", "--myflag", "abc,def"})) assert.Equal(t, &Parser{"abc", "def"}, cmd.Generic("myflag")) assert.Nil(t, cmd.Generic("someother")) } func TestParseGenericFromEnv(t *testing.T) { t.Setenv("APP_SERVE", "20,30") cmd := &Command{ Flags: []Flag{ &GenericFlag{ Name: "serve", Aliases: []string{"s"}, Value: &Parser{}, Sources: EnvVars("APP_SERVE"), }, }, Action: func(ctx context.Context, cmd *Command) error { if !reflect.DeepEqual(cmd.Generic("serve"), &Parser{"20", "30"}) { t.Errorf("main name not set from env") } if !reflect.DeepEqual(cmd.Generic("s"), &Parser{"20", "30"}) { t.Errorf("short name not set from env") } return nil }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"run"})) } func TestFlagActionFromEnv(t *testing.T) { t.Setenv("X", "42") x := 0 cmd := &Command{ Flags: []Flag{ &IntFlag{ Name: "x", Sources: EnvVars("X"), Action: func(ctx context.Context, cmd *Command, v int) error { x = v return nil }, }, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"run"})) assert.Equal(t, cmd.Int("x"), 42) assert.Equal(t, x, 42) } func TestParseShortOptionBoolError(t *testing.T) { cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true cmd.Flags = []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}}, &BoolFlag{Name: "verbose", Aliases: []string{"v"}}, } err := cmd.Run(buildTestContext(t), []string{"run", "-vd=notabool"}) assert.Error(t, err, "expected error parsing invalid bool") } func TestParseShortOptionIntError(t *testing.T) { cmd := buildMinimalTestCommand() cmd.Flags = []Flag{ &IntFlag{Name: "port", Aliases: []string{"p"}}, &BoolFlag{Name: "debug", Aliases: []string{"d"}}, } err := cmd.Run(buildTestContext(t), []string{"run", "-dp=notanint"}) assert.Error(t, err, "expected error parsing invalid int") } func TestParseMultiString(t *testing.T) { _ = (&Command{ Flags: []Flag{ &StringFlag{Name: "serve", Aliases: []string{"s"}}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, "10", cmd.String("serve"), "main name not set") assert.Equal(t, "10", cmd.String("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10"}) } func TestParseDestinationString(t *testing.T) { var dest string _ = (&Command{ Flags: []Flag{ &StringFlag{ Name: "dest", Destination: &dest, }, }, Action: func(context.Context, *Command) error { assert.Equal(t, "10", dest, "expected destination String 10") return nil }, }).Run(buildTestContext(t), []string{"run", "--dest", "10"}) } func TestParseMultiStringFromEnv(t *testing.T) { t.Setenv("APP_COUNT", "20") _ = (&Command{ Flags: []Flag{ &StringFlag{Name: "count", Aliases: []string{"c"}, Sources: EnvVars("APP_COUNT")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, "20", cmd.String("count"), "main name not set") assert.Equal(t, "20", cmd.String("c"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringFromEnvCascade(t *testing.T) { t.Setenv("APP_COUNT", "20") _ = (&Command{ Flags: []Flag{ &StringFlag{Name: "count", Aliases: []string{"c"}, Sources: EnvVars("COMPAT_COUNT", "APP_COUNT")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, "20", cmd.String("count"), "main name not set") assert.Equal(t, "20", cmd.String("c"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringSlice(t *testing.T) { _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []string{}}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} assert.Equal(t, expected, cmd.StringSlice("serve"), "main name not set") assert.Equal(t, expected, cmd.StringSlice("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiStringSliceWithDefaults(t *testing.T) { _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []string{"9", "2"}}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} assert.Equal(t, expected, cmd.StringSlice("serve"), "main name not set") assert.Equal(t, expected, cmd.StringSlice("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiStringSliceWithDestination(t *testing.T) { dest := []string{} _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} assert.Equal(t, expected, dest, "destination val not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") dest := []string{} _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} assert.Equal(t, expected, dest, "destination val not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiFloat64SliceWithDestinationAndEnv(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") dest := []float64{} _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { expected := []float64{10, 20} assert.Equal(t, expected, dest, "destination val not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiIntSliceWithDestinationAndEnv(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") dest := []int64{} _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(context.Context, *Command) error { require.Equalf(t, []int64{10, 20}, dest, "main name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []string{"9", "2"}}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"9", "2"} assert.Equal(t, expected, cmd.StringSlice("serve"), "main name not set") assert.Equal(t, expected, cmd.StringSlice("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringSliceFromEnv(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{}, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"20", "30", "40"} assert.Equal(t, expected, cmd.StringSlice("intervals"), "main name not set from env") assert.Equal(t, expected, cmd.StringSlice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{"1", "2", "5"}, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"20", "30", "40"} assert.Equal(t, expected, cmd.StringSlice("intervals"), "main name not set from env") assert.Equal(t, expected, cmd.StringSlice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"20", "30", "40"} assert.Equal(t, expected, cmd.StringSlice("intervals"), "main name not set from env") assert.Equal(t, expected, cmd.StringSlice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{"1", "2", "5"}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { expected := []string{"20", "30", "40"} assert.Equal(t, expected, cmd.StringSlice("intervals"), "main name not set from env") assert.Equal(t, expected, cmd.StringSlice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiStringSliceFromEnvWithDestination(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") dest := []string{} _ = (&Command{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(context.Context, *Command) error { assert.Equal(t, []string{"20", "30", "40"}, dest, "destination value not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiInt(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Int64Flag{Name: "serve", Aliases: []string{"s"}}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, int64(10), cmd.Int64("serve"), "main name not set") assert.Equal(t, int64(10), cmd.Int64("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10"}) } func TestParseDestinationInt(t *testing.T) { var dest int64 _ = (&Command{ Flags: []Flag{ &Int64Flag{ Name: "dest", Destination: &dest, }, }, Action: func(context.Context, *Command) error { assert.Equal(t, int64(10), dest, "expected destination Int64 10") return nil }, }).Run(buildTestContext(t), []string{"run", "--dest", "10"}) } func TestParseMultiIntFromEnv(t *testing.T) { t.Setenv("APP_TIMEOUT_SECONDS", "10") _ = (&Command{ Flags: []Flag{ &Int64Flag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, int64(10), cmd.Int64("timeout"), "main name not set") assert.Equal(t, int64(10), cmd.Int64("t"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiIntFromEnvCascade(t *testing.T) { t.Setenv("APP_TIMEOUT_SECONDS", "10") _ = (&Command{ Flags: []Flag{ &Int64Flag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("COMPAT_TIMEOUT_SECONDS", "APP_TIMEOUT_SECONDS")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, int64(10), cmd.Int64("timeout"), "main name not set") assert.Equal(t, int64(10), cmd.Int64("t"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiIntSlice(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []int64{}}, }, Action: func(_ context.Context, cmd *Command) error { r := require.New(t) r.Equalf([]int64{10, 20}, cmd.Int64Slice("serve"), "main name not set") r.Equalf([]int64{10, 20}, cmd.Int64Slice("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiIntSliceWithDefaults(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []int64{9, 2}}, }, Action: func(_ context.Context, cmd *Command) error { r := require.New(t) r.Equalf([]int64{10, 20}, cmd.Int64Slice("serve"), "main name not set") r.Equalf([]int64{10, 20}, cmd.Int64Slice("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10", "-s", "20"}) } func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) { _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []int64{9, 2}}, }, Action: func(_ context.Context, cmd *Command) error { expected := []int64{9, 2} assert.Equal(t, expected, cmd.Int64Slice("serve"), "main name not set") assert.Equal(t, expected, cmd.Int64Slice("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiIntSliceFromEnv(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []int64{}, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { r := require.New(t) r.Equalf([]int64{20, 30, 40}, cmd.Int64Slice("intervals"), "main name not set from env") r.Equalf([]int64{20, 30, 40}, cmd.Int64Slice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []int64{1, 2, 5}, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { r := require.New(t) r.Equalf([]int64{20, 30, 40}, cmd.Int64Slice("intervals"), "main name not set from env") r.Equalf([]int64{20, 30, 40}, cmd.Int64Slice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { t.Setenv("APP_INTERVALS", "20,30,40") _ = (&Command{ Flags: []Flag{ &Int64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []int64{}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { r := require.New(t) r.Equalf([]int64{20, 30, 40}, cmd.Int64Slice("intervals"), "main name not set from env") r.Equalf([]int64{20, 30, 40}, cmd.Int64Slice("i"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiFloat64(t *testing.T) { _ = (&Command{ Flags: []Flag{ &FloatFlag{Name: "serve", Aliases: []string{"s"}}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, 10.2, cmd.Float("serve"), "main name not set") assert.Equal(t, 10.2, cmd.Float("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "-s", "10.2"}) } func TestParseDestinationFloat64(t *testing.T) { var dest float64 _ = (&Command{ Flags: []Flag{ &FloatFlag{ Name: "dest", Destination: &dest, }, }, Action: func(context.Context, *Command) error { assert.Equal(t, 10.2, dest, "expected destination Float64 10.2") return nil }, }).Run(buildTestContext(t), []string{"run", "--dest", "10.2"}) } func TestParseMultiFloat64FromEnv(t *testing.T) { t.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = (&Command{ Flags: []Flag{ &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, 15.5, cmd.Float("timeout"), "main name not set") assert.Equal(t, 15.5, cmd.Float("t"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiFloat64FromEnvCascade(t *testing.T) { t.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = (&Command{ Flags: []Flag{ &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("COMPAT_TIMEOUT_SECONDS", "APP_TIMEOUT_SECONDS")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, 15.5, cmd.Float("timeout"), "main name not set") assert.Equal(t, 15.5, cmd.Float("t"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiFloat64SliceFromEnv(t *testing.T) { t.Setenv("APP_INTERVALS", "0.1,-10.5") _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []float64{}, Sources: EnvVars("APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { require.Equalf(t, []float64{0.1, -10.5}, cmd.FloatSlice("intervals"), "main name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { t.Setenv("APP_INTERVALS", "0.1234,-10.5") _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []float64{}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, Action: func(_ context.Context, cmd *Command) error { require.Equalf(t, []float64{0.1234, -10.5}, cmd.FloatSlice("intervals"), "main name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiBool(t *testing.T) { _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "serve", Aliases: []string{"s"}}, }, Action: func(_ context.Context, cmd *Command) error { assert.True(t, cmd.Bool("serve"), "main name not set") assert.True(t, cmd.Bool("s"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "--serve"}) } func TestParseBoolShortOptionHandle(t *testing.T) { _ = (&Command{ Commands: []*Command{ { Name: "foobar", UseShortOptionHandling: true, Action: func(_ context.Context, cmd *Command) error { assert.True(t, cmd.Bool("serve"), "main name not set") assert.True(t, cmd.Bool("option"), "short name not set") return nil }, Flags: []Flag{ &BoolFlag{Name: "serve", Aliases: []string{"s"}}, &BoolFlag{Name: "option", Aliases: []string{"o"}}, }, }, }, }).Run(buildTestContext(t), []string{"run", "foobar", "-so"}) } func TestParseDestinationBool(t *testing.T) { var dest bool _ = (&Command{ Flags: []Flag{ &BoolFlag{ Name: "dest", Destination: &dest, }, }, Action: func(context.Context, *Command) error { assert.True(t, dest, "expected destination Bool true") return nil }, }).Run(buildTestContext(t), []string{"run", "--dest"}) } func TestParseMultiBoolFromEnv(t *testing.T) { t.Setenv("APP_DEBUG", "1") _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("APP_DEBUG")}, }, Action: func(_ context.Context, cmd *Command) error { assert.True(t, cmd.Bool("debug"), "main name not set") assert.True(t, cmd.Bool("d"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiBoolFromEnvCascade(t *testing.T) { t.Setenv("APP_DEBUG", "1") _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("COMPAT_DEBUG", "APP_DEBUG")}, }, Action: func(_ context.Context, cmd *Command) error { assert.True(t, cmd.Bool("debug"), "main name not set from env") assert.True(t, cmd.Bool("d"), "short name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseBoolFromEnv(t *testing.T) { boolFlagTests := []struct { input string output bool }{ {"", false}, {"1", true}, {"false", false}, {"true", true}, } for _, test := range boolFlagTests { t.Run(fmt.Sprintf("%[1]q %[2]v", test.input, test.output), func(t *testing.T) { t.Setenv("DEBUG", test.input) _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("DEBUG")}, }, Action: func(_ context.Context, cmd *Command) error { assert.Equal(t, test.output, cmd.Bool("debug")) assert.Equal(t, test.output, cmd.Bool("d")) return nil }, }).Run(buildTestContext(t), []string{"run"}) }) } } func TestParseMultiBoolT(t *testing.T) { _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "implode", Aliases: []string{"i"}, Value: true}, }, Action: func(_ context.Context, cmd *Command) error { assert.False(t, cmd.Bool("implode"), "main name not set") assert.False(t, cmd.Bool("i"), "short name not set") return nil }, }).Run(buildTestContext(t), []string{"run", "--implode=false"}) } func TestStringSlice_Serialized_Set(t *testing.T) { sl0 := NewStringSlice("a", "b") ser0 := sl0.Serialize() require.GreaterOrEqual(t, len(ser0), len(slPfx), "serialized shorter than expected") sl1 := NewStringSlice("c", "d") _ = sl1.Set(ser0) require.Equal(t, sl0.String(), sl1.String(), "pre and post serialization do not match") } func TestIntSlice_Serialized_Set(t *testing.T) { sl0 := NewInt64Slice(1, 2) ser0 := sl0.Serialize() require.GreaterOrEqual(t, len(ser0), len(slPfx), "serialized shorter than expected") sl1 := NewInt64Slice(3, 4) _ = sl1.Set(ser0) require.Equal(t, sl0.String(), sl1.String(), "pre and post serialization do not match") } func TestUintSlice_Serialized_Set(t *testing.T) { sl0 := NewUint64Slice(1, 2) ser0 := sl0.Serialize() require.GreaterOrEqual(t, len(ser0), len(slPfx), "serialized shorter than expected") sl1 := NewUint64Slice(3, 4) _ = sl1.Set(ser0) require.Equal(t, sl0.String(), sl1.String(), "pre and post serialization do not match") } func TestUint64Slice_Serialized_Set(t *testing.T) { sl0 := NewUint64Slice(1, 2) ser0 := sl0.Serialize() require.GreaterOrEqual(t, len(ser0), len(slPfx), "serialized shorter than expected") sl1 := NewUint64Slice(3, 4) _ = sl1.Set(ser0) require.Equal(t, sl0.String(), sl1.String(), "pre and post serialization do not match") } func TestStringMap_Serialized_Set(t *testing.T) { m0 := NewStringMap(map[string]string{"a": "b"}) ser0 := m0.Serialize() require.GreaterOrEqual(t, len(ser0), len(slPfx), "serialized shorter than expected") m1 := NewStringMap(map[string]string{"c": "d"}) _ = m1.Set(ser0) require.Equal(t, m0.String(), m1.String(), "pre and post serialization do not match") } var timestampFlagTests = []struct { name string aliases []string usage string expected string }{ {"foo", nil, "", "--foo time\t(default: 2020-04-10 01:01:01.000000001 +0000 UTC)"}, {"f", nil, "all", "-f time\tall (default: 2020-04-10 01:01:01.000000001 +0000 UTC)"}, } func TestTimestampFlagHelpOutput(t *testing.T) { tl, err := time.LoadLocation("UTC") assert.NoError(t, err) for _, test := range timestampFlagTests { value := time.Date(2020, time.April, 10, 1, 1, 1, 1, tl) fl := &TimestampFlag{Name: test.name, Aliases: test.aliases, Usage: test.usage, Value: value} assert.Equal(t, test.expected, fl.String()) } } func TestTimestamp_set(t *testing.T) { ts := timestampValue{ timestamp: nil, hasBeenSet: false, layouts: []string{"Jan 2, 2006 at 3:04pm (MST)"}, } time1 := "Feb 3, 2013 at 7:54pm (PST)" require.NoError(t, ts.Set(time1), "Failed to parse time %s with layouts %v", time1, ts.layouts) require.True(t, ts.hasBeenSet, "hasBeenSet is not true after setting a time") ts.hasBeenSet = false ts.layouts = []string{time.RFC3339} time2 := "2006-01-02T15:04:05Z" require.NoError(t, ts.Set(time2), "Failed to parse time %s with layout %v", time2, ts.layouts) require.True(t, ts.hasBeenSet, "hasBeenSet is not true after setting a time") } func TestTimestampFlagApply_SingleFormat(t *testing.T) { expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.RFC3339}}} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--time", "2006-01-02T15:04:05Z"})) assert.Equal(t, expectedResult, cmd.Value("time")) } func TestTimestampFlagApply_MultipleFormats(t *testing.T) { now := time.Now().UTC() testCases := []struct { caseName string layoutsPrecisions map[string]time.Duration expRes time.Time expErrValidation func(err error) (validation error) }{ { caseName: "all_valid_layouts", layoutsPrecisions: map[string]time.Duration{ time.RFC3339: time.Second, time.DateTime: time.Second, time.RFC1123: time.Second, }, expRes: now.Truncate(time.Second), }, { caseName: "one_invalid_layout", layoutsPrecisions: map[string]time.Duration{ time.RFC3339: time.Second, time.DateTime: time.Second, "foo": 0, }, expRes: now.Truncate(time.Second), }, { caseName: "multiple_invalid_layouts", layoutsPrecisions: map[string]time.Duration{ time.RFC3339: time.Second, "foo": 0, time.DateTime: time.Second, "bar": 0, }, expRes: now.Truncate(time.Second), }, { caseName: "all_invalid_layouts", layoutsPrecisions: map[string]time.Duration{ "foo": 0, "2024-08-07 74:01:82Z-100": 0, "25:70": 0, "": 0, }, expErrValidation: func(err error) error { if err == nil { return errors.New("got nil err") } found := regexp.MustCompile(`(cannot parse ".+" as ".*")|(extra text: ".+")`).Match([]byte(err.Error())) if !found { return fmt.Errorf("given error does not satisfy pattern: %w", err) } return nil }, }, { caseName: "empty_layout", layoutsPrecisions: map[string]time.Duration{ "": 0, }, expErrValidation: func(err error) error { if err == nil { return errors.New("got nil err") } found := regexp.MustCompile(`extra text: ".+"`).Match([]byte(err.Error())) if !found { return fmt.Errorf("given error does not satisfy pattern: %w", err) } return nil }, }, { caseName: "nil_layouts_slice", expErrValidation: func(err error) error { if err == nil { return errors.New("got nil err") } found := regexp.MustCompile(`got nil/empty layouts slice`).Match([]byte(err.Error())) if !found { return fmt.Errorf("given error does not satisfy pattern: %w", err) } return nil }, }, { caseName: "empty_layouts_slice", layoutsPrecisions: map[string]time.Duration{}, expErrValidation: func(err error) error { if err == nil { return errors.New("got nil err") } found := regexp.MustCompile(`got nil/empty layouts slice`).Match([]byte(err.Error())) if !found { return fmt.Errorf("given error does not satisfy pattern: %w", err) } return nil }, }, } // TODO: replace with maps.Keys() (go >= ), lo.Keys() if acceptable getKeys := func(m map[string]time.Duration) []string { if m == nil { return nil } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } for idx := range testCases { testCase := testCases[idx] t.Run(testCase.caseName, func(t *testing.T) { // t.Parallel() fl := TimestampFlag{ Name: "time", Config: TimestampConfig{ Layouts: getKeys(testCase.layoutsPrecisions), }, } if len(testCase.layoutsPrecisions) == 0 { err := fl.Set(fl.Name, now.Format(time.RFC3339)) if testCase.expErrValidation != nil { assert.NoError(t, testCase.expErrValidation(err)) } } validLayouts := make([]string, 0, len(testCase.layoutsPrecisions)) invalidLayouts := make([]string, 0, len(testCase.layoutsPrecisions)) // TODO: replace with lo.Filter if acceptable for layout, prec := range testCase.layoutsPrecisions { v, err := time.Parse(layout, now.Format(layout)) if err != nil || prec == 0 || now.Truncate(prec).UnixNano() != v.Truncate(prec).UnixNano() { invalidLayouts = append(invalidLayouts, layout) continue } validLayouts = append(validLayouts, layout) } for _, layout := range validLayouts { err := fl.Set(fl.Name, now.Format(layout)) assert.NoError(t, err) if !testCase.expRes.IsZero() { assert.Equal(t, testCase.expRes, fl.value.Get()) } } for range invalidLayouts { err := fl.Set(fl.Name, now.Format(time.RFC3339)) if testCase.expErrValidation != nil { assert.NoError(t, testCase.expErrValidation(err)) } } }) } } func TestTimestampFlagApply_ShortenedLayouts(t *testing.T) { now := time.Now().UTC() shortenedLayoutsPrecisions := map[string]time.Duration{ time.Kitchen: time.Minute, time.Stamp: time.Second, time.StampMilli: time.Millisecond, time.StampMicro: time.Microsecond, time.StampNano: time.Nanosecond, time.TimeOnly: time.Second, "15:04": time.Minute, } // TODO: replace with maps.Keys() (go >= ), lo.Keys() if acceptable getKeys := func(m map[string]time.Duration) []string { if m == nil { return nil } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } fl := TimestampFlag{ Name: "time", Config: TimestampConfig{ Layouts: getKeys(shortenedLayoutsPrecisions), }, } for layout, prec := range shortenedLayoutsPrecisions { err := fl.Set(fl.Name, now.Format(layout)) assert.NoError(t, err) assert.Equal(t, now.Truncate(prec), fl.value.Get()) } } func TestTimestampFlagApplyValue(t *testing.T) { expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Value: expectedResult} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, expectedResult, cmd.Value("time")) } func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) { fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{"randomlayout"}}} cmd := &Command{ Flags: []Flag{ &fl, }, } err := cmd.Run(buildTestContext(t), []string{"", "--time", "2006-01-02T15:04:05Z"}) assert.EqualError(t, err, "invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"randomlayout\": cannot parse \"2006-01-02T15:04:05Z\" as \"randomlayout\"") } func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) { fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{"Jan 2, 2006 at 3:04pm (MST)"}}} cmd := &Command{ Flags: []Flag{ &fl, }, } err := cmd.Set("time", "2006-01-02T15:04:05Z") assert.EqualError(t, err, "parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\"") } func TestTimestampFlagApply_Timezoned(t *testing.T) { pdt := time.FixedZone("PDT", -7*60*60) expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.ANSIC}, Timezone: pdt}} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--time", "Mon Jan 2 08:04:05 2006"})) assert.Equal(t, expectedResult.In(pdt), cmd.Value("time")) } func TestTimestampFlagValueFromCommand(t *testing.T) { now := time.Now() cmd := &Command{ Flags: []Flag{ &TimestampFlag{ Name: "myflag", Value: now, }, }, } f := &TimestampFlag{Name: "myflag"} require.Equal(t, now, cmd.Timestamp(f.Name)) } type flagDefaultTestCase struct { name string flag Flag toParse []string expect string } func TestFlagDefaultValue(t *testing.T) { cases := []*flagDefaultTestCase{ { name: "stringSlice", flag: &StringSliceFlag{Name: "flag", Value: []string{"default1", "default2"}}, toParse: []string{"--flag", "parsed"}, expect: `--flag string [ --flag string ] (default: "default1", "default2")`, }, { name: "floatSlice", flag: &FloatSliceFlag{Name: "flag", Value: []float64{1.1, 2.2}}, toParse: []string{"--flag", "13.3"}, expect: `--flag float [ --flag float ] (default: 1.1, 2.2)`, }, { name: "float32Slice", flag: &Float32SliceFlag{Name: "flag", Value: []float32{1.1, 2.2}}, toParse: []string{"--flag", "13.3"}, expect: `--flag float [ --flag float ] (default: 1.1, 2.2)`, }, { name: "float64Slice", flag: &Float64SliceFlag{Name: "flag", Value: []float64{1.1, 2.2}}, toParse: []string{"--flag", "13.3"}, expect: `--flag float [ --flag float ] (default: 1.1, 2.2)`, }, { name: "intSlice", flag: &Int64SliceFlag{Name: "flag", Value: []int64{1, 2}}, toParse: []string{"--flag", "13"}, expect: `--flag int [ --flag int ] (default: 1, 2)`, }, { name: "uintSlice", flag: &Uint64SliceFlag{Name: "flag", Value: []uint64{1, 2}}, toParse: []string{"--flag", "13"}, expect: `--flag uint [ --flag uint ] (default: 1, 2)`, }, { name: "string", flag: &StringFlag{Name: "flag", Value: "default"}, toParse: []string{"--flag", "parsed"}, expect: `--flag string (default: "default")`, }, { name: "bool", flag: &BoolFlag{Name: "flag", Value: true}, toParse: []string{"--flag=false"}, expect: `--flag `, }, { name: "uint", flag: &UintFlag{Name: "flag", Value: 1}, toParse: []string{"--flag", "13"}, expect: `--flag uint (default: 1)`, }, { name: "uint64", flag: &Uint64Flag{Name: "flag", Value: 1}, toParse: []string{"--flag", "13"}, expect: `--flag uint (default: 1)`, }, { name: "stringMap", flag: &StringMapFlag{Name: "flag", Value: map[string]string{"default1": "default2"}}, toParse: []string{"--flag", "parsed="}, expect: `--flag string=string [ --flag string=string ] (default: default1="default2")`, }, } for _, v := range cases { t.Run(v.name, func(t *testing.T) { cmd := &Command{ Flags: []Flag{ v.flag, }, } assert.NoError(t, cmd.Run(buildTestContext(t), append([]string{""}, v.toParse...))) assert.Equal(t, v.expect, v.flag.String()) }) } } type flagDefaultTestCaseWithEnv struct { name string flag Flag toParse []string expect string environ map[string]string } func TestFlagDefaultValueWithEnv(t *testing.T) { ts, err := time.Parse(time.RFC3339, "2005-01-02T15:04:05Z") require.NoError(t, err) cases := []*flagDefaultTestCaseWithEnv{ { name: "stringSlice", flag: &StringSliceFlag{Name: "flag", Value: []string{"default1", "default2"}, Sources: EnvVars("ssflag")}, toParse: []string{"--flag", "parsed"}, expect: `--flag string [ --flag string ] (default: "default1", "default2")` + withEnvHint([]string{"ssflag"}, ""), environ: map[string]string{ "ssflag": "some-other-env_value", }, }, { name: "float64Slice", flag: &FloatSliceFlag{Name: "flag", Value: []float64{1.1, 2.2}, Sources: EnvVars("fsflag")}, toParse: []string{"--flag", "13.3"}, expect: `--flag float [ --flag float ] (default: 1.1, 2.2)` + withEnvHint([]string{"fsflag"}, ""), environ: map[string]string{ "fsflag": "20304.222", }, }, { name: "intSlice", flag: &Int64SliceFlag{Name: "flag", Value: []int64{1, 2}, Sources: EnvVars("isflag")}, toParse: []string{"--flag", "13"}, expect: `--flag int [ --flag int ] (default: 1, 2)` + withEnvHint([]string{"isflag"}, ""), environ: map[string]string{ "isflag": "101", }, }, { name: "uintSlice", flag: &Uint64SliceFlag{Name: "flag", Value: []uint64{1, 2}, Sources: EnvVars("uisflag")}, toParse: []string{"--flag", "13"}, expect: `--flag uint [ --flag uint ] (default: 1, 2)` + withEnvHint([]string{"uisflag"}, ""), environ: map[string]string{ "uisflag": "3", }, }, { name: "string", flag: &StringFlag{Name: "flag", Value: "default", Sources: EnvVars("uflag")}, toParse: []string{"--flag", "parsed"}, expect: `--flag string (default: "default")` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "some-other-string", }, }, { name: "bool", flag: &BoolFlag{Name: "flag", Value: true, Sources: EnvVars("uflag")}, toParse: []string{"--flag=false"}, expect: `--flag ` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "false", }, }, { name: "bool", flag: &BoolWithInverseFlag{Name: "flag", Value: true, Sources: EnvVars("uflag")}, expect: `--[no-]flag (default: true)` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "false", }, }, { name: "uint64", flag: &Uint64Flag{Name: "flag", Value: 1, Sources: EnvVars("uflag")}, toParse: []string{"--flag", "13"}, expect: `--flag uint (default: 1)` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "10", }, }, { name: "uint", flag: &Uint64Flag{Name: "flag", Value: 1, Sources: EnvVars("uflag")}, toParse: []string{"--flag", "13"}, expect: `--flag uint (default: 1)` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "10", }, }, { name: "int64", flag: &Int64Flag{Name: "flag", Value: 1, Sources: EnvVars("uflag")}, toParse: []string{"--flag", "13"}, expect: `--flag int (default: 1)` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "10", }, }, { name: "int", flag: &Int64Flag{Name: "flag", Value: 1, Sources: EnvVars("uflag")}, toParse: []string{"--flag", "13"}, expect: `--flag int (default: 1)` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "10", }, }, { name: "duration", flag: &DurationFlag{Name: "flag", Value: time.Second, Sources: EnvVars("uflag")}, toParse: []string{"--flag", "2m"}, expect: `--flag duration (default: 1s)` + withEnvHint([]string{"uflag"}, ""), environ: map[string]string{ "uflag": "2h4m10s", }, }, { name: "timestamp", flag: &TimestampFlag{Name: "flag", Value: ts, Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Sources: EnvVars("tflag")}, toParse: []string{"--flag", "2006-11-02T15:04:05Z"}, expect: `--flag time (default: 2005-01-02 15:04:05 +0000 UTC)` + withEnvHint([]string{"tflag"}, ""), environ: map[string]string{ "tflag": "2010-01-02T15:04:05Z", }, }, { name: "stringMap", flag: &StringMapFlag{Name: "flag", Value: map[string]string{"default1": "default2"}, Sources: EnvVars("ssflag")}, toParse: []string{"--flag", "parsed="}, expect: `--flag string=string [ --flag string=string ] (default: default1="default2")` + withEnvHint([]string{"ssflag"}, ""), environ: map[string]string{ "ssflag": "some-other-env_value=", }, }, // TODO /*{ name: "generic", flag: &GenericFlag{Name: "flag", Value: &Parser{"11", "12"}, Sources: EnvVars("gflag")}, toParse: []string{"--flag", "15,16"}, expect: `--flag value (default: 11,12)` + withEnvHint([]string{"gflag"}, ""), environ: map[string]string{ "gflag": "13,14", }, },*/ } for _, v := range cases { t.Run(v.name, func(t *testing.T) { for key, val := range v.environ { t.Setenv(key, val) } cmd := &Command{ Flags: []Flag{ v.flag, }, } require.NoError(t, cmd.Run(buildTestContext(t), append([]string{""}, v.toParse...))) assert.Equal(t, v.expect, v.flag.String()) }) } } type flagValueTestCase struct { name string flag Flag toParse []string expect string } func TestFlagValue(t *testing.T) { t.SkipNow() cases := []*flagValueTestCase{ { name: "stringSlice", flag: &StringSliceFlag{Name: "flag", Value: []string{"default1", "default2"}}, toParse: []string{"--flag", "parsed,parsed2", "--flag", "parsed3,parsed4"}, expect: `[parsed parsed2 parsed3 parsed4]`, }, { name: "float64Slice", flag: &FloatSliceFlag{Name: "flag", Value: []float64{1.1, 2.2}}, toParse: []string{"--flag", "13.3,14.4", "--flag", "15.5,16.6"}, expect: `[]float64{13.3, 14.4, 15.5, 16.6}`, }, { name: "intSlice", flag: &Int64SliceFlag{Name: "flag", Value: []int64{1, 2}}, toParse: []string{"--flag", "13,14", "--flag", "15,16"}, expect: `[]int64{13, 14, 15, 16}`, }, { name: "uintSlice", flag: &Uint64SliceFlag{Name: "flag", Value: []uint64{1, 2}}, toParse: []string{"--flag", "13,14", "--flag", "15,16"}, expect: `[]uint64{13, 14, 15, 16}`, }, { name: "stringMap", flag: &StringMapFlag{Name: "flag", Value: map[string]string{"default1": "default2"}}, toParse: []string{"--flag", "parsed=parsed2", "--flag", "parsed3=parsed4"}, expect: `map[parsed:parsed2 parsed3:parsed4]`, }, { name: "int", flag: &IntFlag{Name: "flag", Value: 1}, toParse: []string{"--flag", "42"}, expect: `int(42)`, }, { name: "uint", flag: &UintFlag{Name: "flag", Value: 1}, toParse: []string{"--flag", "42"}, expect: `uint(42)`, }, } for _, v := range cases { t.Run(v.name, func(t *testing.T) { cmd := &Command{ Flags: []Flag{ v.flag, }, } assert.NoError(t, cmd.Run(buildTestContext(t), append([]string{""}, v.toParse...))) f := cmd.lookupFlag("flag") require.Equal(t, v.expect, f.String()) }) } } func TestTimestampFlagApply_WithDestination(t *testing.T) { var destination time.Time expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Destination: &destination} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--time", "2006-01-02T15:04:05Z"})) assert.Equal(t, expectedResult, destination) } // Test issue #1254 // StringSlice() with UseShortOptionHandling causes duplicated entries, depending on the ordering of the flags func TestSliceShortOptionHandle(t *testing.T) { wasCalled := false err := (&Command{ Name: "foobar", UseShortOptionHandling: true, Action: func(_ context.Context, cmd *Command) error { wasCalled = true if !cmd.Bool("i") { return fmt.Errorf("bool i not set") } if !cmd.Bool("t") { return fmt.Errorf("bool i not set") } ss := cmd.StringSlice("net") if !reflect.DeepEqual(ss, []string{"foo"}) { return fmt.Errorf("got different slice %q than expected", ss) } return nil }, Flags: []Flag{ &StringSliceFlag{Name: "net"}, &BoolFlag{Name: "i"}, &BoolFlag{Name: "t"}, }, }).Run(buildTestContext(t), []string{"foobar", "--net=foo", "-it"}) r := require.New(t) r.NoError(err) r.Truef(wasCalled, "action callback was never called") } // Test issue #1541 func TestCustomizedSliceFlagSeparator(t *testing.T) { opts := []string{"opt1", "opt2", "opt3,op", "opt4"} ret := flagSplitMultiValues(strings.Join(opts, ";"), ";", disableSliceFlagSeparator) require.Equal(t, 4, len(ret), "split slice flag failed") for idx, r := range ret { require.Equal(t, opts[idx], r, "get %dth failed", idx) } } func TestFlagSplitMultiValues_Disabled(t *testing.T) { opts := []string{"opt1", "opt2", "opt3,op", "opt4"} ret := flagSplitMultiValues(strings.Join(opts, defaultSliceFlagSeparator), defaultSliceFlagSeparator, true) require.Equal(t, 1, len(ret), "failed to disable split slice flag") require.Equal(t, strings.Join(opts, defaultSliceFlagSeparator), ret[0]) } var stringMapFlagTests = []struct { name string aliases []string value map[string]string expected string }{ {"foo", nil, nil, "--foo string=string [ --foo string=string ]\t"}, {"f", nil, nil, "-f string=string [ -f string=string ]\t"}, {"f", nil, map[string]string{"Lipstick": ""}, "-f string=string [ -f string=string ]\t(default: Lipstick=)"}, {"test", nil, map[string]string{"Something": ""}, "--test string=string [ --test string=string ]\t(default: Something=)"}, {"dee", []string{"d"}, map[string]string{"Inka": "Dinka", "dooo": ""}, "--dee string=string, -d string=string [ --dee string=string, -d string=string ]\t(default: Inka=\"Dinka\", dooo=)"}, } func TestStringMapFlagHelpOutput(t *testing.T) { for _, test := range stringMapFlagTests { f := &StringMapFlag{Name: test.name, Aliases: test.aliases, Value: test.value} assert.Equal(t, test.expected, f.String()) } } func TestStringMapFlagWithEnvVarHelpOutput(t *testing.T) { t.Setenv("APP_QWWX", "11,4") for _, test := range stringMapFlagTests { fl := &StringMapFlag{Name: test.name, Aliases: test.aliases, Value: test.value, Sources: EnvVars("APP_QWWX")} output := fl.String() expectedSuffix := withEnvHint([]string{"APP_QWWX"}, "") if !strings.HasSuffix(output, expectedSuffix) { t.Errorf("%q does not end with"+expectedSuffix, output) } } } func TestStringMapFlagApply_SetsAllNames(t *testing.T) { fl := StringMapFlag{Name: "goat", Aliases: []string{"G", "gooots"}} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--goat", "aaa=", "-G", "bbb=", "--gooots", "eeeee="})) } func TestStringMapFlagApply_UsesEnvValues_noDefault(t *testing.T) { t.Setenv("MY_GOAT", "vincent van goat=scape goat") var val map[string]string fl := StringMapFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Nil(t, val) assert.Equal(t, map[string]string{"vincent van goat": "scape goat"}, cmd.Value("goat")) } func TestStringMapFlagApply_UsesEnvValues_withDefault(t *testing.T) { t.Setenv("MY_GOAT", "vincent van goat=scape goat") val := map[string]string{`some default`: `values here`} fl := StringMapFlag{Name: "goat", Sources: EnvVars("MY_GOAT"), Value: val} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, map[string]string{`some default`: `values here`}, val) assert.Equal(t, map[string]string{"vincent van goat": "scape goat"}, cmd.Value("goat")) } func TestStringMapFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := map[string]string{"UA": "US"} fl := StringMapFlag{Name: "country", Value: defValue, Destination: &map[string]string{"CA": ""}} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) assert.Equal(t, defValue, *fl.Destination) } func TestStringMapFlagValueFromCommand(t *testing.T) { f := &StringMapFlag{Name: "myflag"} cmd := &Command{ Flags: []Flag{ f, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{""})) require.NoError(t, cmd.Set("myflag", "a=b")) require.NoError(t, cmd.Set("myflag", "c=")) require.Equal(t, map[string]string{"a": "b", "c": ""}, cmd.StringMap(f.Name)) } func TestStringMapFlagApply_Error(t *testing.T) { fl := StringMapFlag{Name: "goat"} cmd := &Command{ Flags: []Flag{ &fl, }, } assert.Error(t, cmd.Run(buildTestContext(t), []string{"", "--goat", "aaa", "bbb="})) } func TestZeroValueMutexFlag(t *testing.T) { var fl MutuallyExclusiveFlags assert.NoError(t, fl.check(&Command{})) } func TestMutexFlagCategory(t *testing.T) { cmd := buildMinimalTestCommand() cmd.Category = "TestCmd" cmd.MutuallyExclusiveFlags = []MutuallyExclusiveFlags{ { Flags: [][]Flag{ { &StringFlag{Name: "foo", Category: "Group1"}, &IntFlag{Name: "bar", Category: "Group1"}, }, { &StringFlag{Name: "baz", Category: "Group2"}, }, }, }, } assert.NoError(t, cmd.Run(buildTestContext(t), []string{"", "--foo", "value"})) } func TestExtFlag(t *testing.T) { var iv intValue[int64] var ipv int64 f := &flag.Flag{ Name: "bar", Usage: "bar usage", Value: iv.Create(11, &ipv, IntegerConfig{}), DefValue: "10", } extF := &extFlag{ f: f, } assert.Equal(t, []string{"bar"}, extF.Names()) assert.True(t, extF.IsVisible()) assert.False(t, extF.IsSet()) assert.False(t, extF.TakesValue()) assert.Equal(t, "bar usage", extF.GetUsage()) assert.Equal(t, "11", extF.GetValue()) assert.Equal(t, "10", extF.GetDefaultText()) assert.Nil(t, extF.GetEnvVars()) } func TestSliceValuesNil(t *testing.T) { assert.Equal(t, []float64(nil), NewFloatSlice().Value()) assert.Equal(t, []float32(nil), NewFloat32Slice().Value()) assert.Equal(t, []float64(nil), NewFloat64Slice().Value()) assert.Equal(t, []int64(nil), NewInt64Slice().Value()) assert.Equal(t, []uint64(nil), NewUint64Slice().Value()) assert.Equal(t, []string(nil), NewStringSlice().Value()) assert.Equal(t, []float64(nil), (&FloatSlice{}).Value()) assert.Equal(t, []float32(nil), (&Float32Slice{}).Value()) assert.Equal(t, []float64(nil), (&Float64Slice{}).Value()) assert.Equal(t, []int64(nil), (&Int64Slice{}).Value()) assert.Equal(t, []uint64(nil), (&Uint64Slice{}).Value()) assert.Equal(t, []string(nil), (&StringSlice{}).Value()) } func TestFileHint(t *testing.T) { assert.Equal(t, "", withFileHint("", "")) assert.Equal(t, " [/tmp/foo.txt]", withFileHint("/tmp/foo.txt", "")) assert.Equal(t, "foo", withFileHint("", "foo")) assert.Equal(t, "bar [/tmp/foo.txt]", withFileHint("/tmp/foo.txt", "bar")) } func TestHasFlags(t *testing.T) { flagToCheck := &StringFlag{Name: "foo"} flags := []Flag{ &StringFlag{Name: "bar"}, &Int64Flag{Name: "baz"}, flagToCheck, } assert.True(t, hasFlag(flags, flagToCheck)) } func TestFlagsByName(t *testing.T) { flags := []Flag{ &StringFlag{ Name: "b2", }, &Int64Flag{ Name: "a0", }, &FloatFlag{ Name: "b1", }, } flagsByName := FlagsByName(flags) sort.Sort(flagsByName) assert.Equal(t, len(flags), flagsByName.Len()) var prev Flag for _, f := range flags { if prev != nil { assert.LessOrEqual(t, prev.Names()[0], f.Names()[0]) } prev = f } } func TestNonStringMap(t *testing.T) { type ( floatMap = MapBase[float64, NoConfig, floatValue[float64]] ) p := map[string]float64{} var fv floatValue[float64] f := &floatMap{ value: &fv, } assert.Equal(t, map[string]float64{}, f.Value()) f.dict = &p assert.Equal(t, map[string]float64{}, f.Value()) assert.Equal(t, "map[string]float64{}", f.String()) assert.ErrorContains(t, f.Set("invalid=value"), "ParseFloat") } func TestUnquoteUsage(t *testing.T) { tests := []struct { str string expStr string expUsage string }{ {"foo", "", "foo"}, {"foo something", "", "foo something"}, {"foo `bar 11`", "bar 11", "foo bar 11"}, {"foo `bar 11` sobar", "bar 11", "foo bar 11 sobar"}, {"foo `bar 11", "", "foo `bar 11"}, } for i, test := range tests { t.Run(fmt.Sprintf("unquote %d", i), func(t *testing.T) { str, usage := unquoteUsage(test.str) assert.Equal(t, test.expStr, str) assert.Equal(t, test.expUsage, usage) }) } } func TestEnvHintWindows(t *testing.T) { if runtime.GOOS == "windows" && os.Getenv("PSHOME") == "" { assert.Equal(t, "something [%foo%, %bar%, %ss%]", withEnvHint([]string{"foo", "bar", "ss"}, "something")) } } func TestDocGetValue(t *testing.T) { assert.Equal(t, "true", (&BoolFlag{Name: "foo", Value: true}).GetValue()) assert.Equal(t, "false", (&BoolFlag{Name: "foo", Value: false}).GetValue()) assert.Equal(t, "\"bar\"", (&StringFlag{Name: "foo", Value: "bar"}).GetValue()) assert.Equal(t, "", (&BoolWithInverseFlag{Name: "foo", Value: false}).GetValue()) } func TestGenericFlag_SatisfiesFlagInterface(t *testing.T) { var f Flag = &GenericFlag{} _ = f.IsSet() _ = f.Names() } func TestGenericValue_SatisfiesBoolInterface(t *testing.T) { var f boolFlag = &genericValue{} var fpv float64 assert.False(t, f.IsBoolFlag()) fv := floatValue[float64]{val: &fpv} f = &genericValue{ val: &fv, } assert.False(t, f.IsBoolFlag()) f = &genericValue{ val: &boolValue{}, } assert.True(t, f.IsBoolFlag()) } func TestGenericFlag_SatisfiesFmtStringerInterface(t *testing.T) { var f fmt.Stringer = &GenericFlag{} _ = f.String() } func TestGenericFlag_SatisfiesRequiredFlagInterface(t *testing.T) { var f RequiredFlag = &GenericFlag{} _ = f.IsRequired() } func TestGenericFlag_SatisfiesVisibleFlagInterface(t *testing.T) { var f VisibleFlag = &GenericFlag{} _ = f.IsVisible() } func TestGenericFlag_SatisfiesDocFlagInterface(t *testing.T) { var f DocGenerationFlag = &GenericFlag{} _ = f.GetUsage() } func TestGenericValue(t *testing.T) { g := &genericValue{} assert.NoError(t, g.Set("something")) assert.Nil(t, g.Get()) assert.Empty(t, g.String()) } func TestEndValue(t *testing.T) { cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true cmd.Flags = []Flag{ &IntFlag{Name: "debug", Aliases: []string{"d"}}, &IntFlag{Name: "count", Aliases: []string{"c"}}, } assert.Error(t, cmd.Run(buildTestContext(t), []string{"foo", "-cd="})) assert.Error(t, cmd.Run(buildTestContext(t), []string{"foo", "-cd=s"})) } ================================================ FILE: flag_timestamp.go ================================================ package cli import ( "errors" "fmt" "time" ) type TimestampFlag = FlagBase[time.Time, TimestampConfig, timestampValue] // TimestampConfig defines the config for timestamp flags type TimestampConfig struct { Timezone *time.Location // Available layouts for flag value. // // Note that value for formats with missing year/date will be interpreted as current year/date respectively. // // Read more about time layouts: https://pkg.go.dev/time#pkg-constants Layouts []string } // timestampValue wrap to satisfy golang's flag interface. type timestampValue struct { timestamp *time.Time hasBeenSet bool layouts []string location *time.Location } var _ ValueCreator[time.Time, TimestampConfig] = timestampValue{} // Below functions are to satisfy the ValueCreator interface func (t timestampValue) Create(val time.Time, p *time.Time, c TimestampConfig) Value { *p = val return ×tampValue{ timestamp: p, layouts: c.Layouts, location: c.Timezone, } } func (t timestampValue) ToString(b time.Time) string { if b.IsZero() { return "" } t.timestamp = &b return t.String() } // Below functions are to satisfy the Value interface // Parses the string value to timestamp func (t *timestampValue) Set(value string) error { var timestamp time.Time var err error if t.location == nil { t.location = time.UTC } if len(t.layouts) == 0 { return errors.New("got nil/empty layouts slice") } for _, layout := range t.layouts { var locErr error timestamp, locErr = time.ParseInLocation(layout, value, t.location) if locErr != nil { if err == nil { err = locErr continue } err = newMultiError(err, locErr) continue } err = nil break } if err != nil { return err } defaultTS, _ := time.ParseInLocation(time.TimeOnly, time.TimeOnly, timestamp.Location()) n := time.Now().In(timestamp.Location()) // If format is missing date (or year only), set it explicitly to current if timestamp.Truncate(time.Hour*24).UnixNano() == defaultTS.Truncate(time.Hour*24).UnixNano() { timestamp = time.Date( n.Year(), n.Month(), n.Day(), timestamp.Hour(), timestamp.Minute(), timestamp.Second(), timestamp.Nanosecond(), timestamp.Location(), ) } else if timestamp.Year() == 0 { timestamp = time.Date( n.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), timestamp.Minute(), timestamp.Second(), timestamp.Nanosecond(), timestamp.Location(), ) } if t.timestamp != nil { *t.timestamp = timestamp } t.hasBeenSet = true return nil } // String returns a readable representation of this value (for usage defaults) func (t *timestampValue) String() string { return fmt.Sprintf("%v", t.timestamp) } // Get returns the flag structure func (t *timestampValue) Get() any { return *t.timestamp } // Timestamp gets the timestamp from a flag name func (cmd *Command) Timestamp(name string) time.Time { if v, ok := cmd.Value(name).(time.Time); ok { tracef("time.Time available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("time.Time NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return time.Time{} } ================================================ FILE: flag_uint.go ================================================ package cli import ( "strconv" "unsafe" ) type ( UintFlag = FlagBase[uint, IntegerConfig, uintValue[uint]] Uint8Flag = FlagBase[uint8, IntegerConfig, uintValue[uint8]] Uint16Flag = FlagBase[uint16, IntegerConfig, uintValue[uint16]] Uint32Flag = FlagBase[uint32, IntegerConfig, uintValue[uint32]] Uint64Flag = FlagBase[uint64, IntegerConfig, uintValue[uint64]] ) // -- uint Value type uintValue[T uint | uint8 | uint16 | uint32 | uint64] struct { val *T base int } // Below functions are to satisfy the ValueCreator interface func (i uintValue[T]) Create(val T, p *T, c IntegerConfig) Value { *p = val return &uintValue[T]{ val: p, base: c.Base, } } func (i uintValue[T]) ToString(b T) string { i.val = &b return i.String() } // Below functions are to satisfy the flag.Value interface func (i *uintValue[T]) Set(s string) error { v, err := strconv.ParseUint(s, i.base, int(unsafe.Sizeof(T(0))*8)) if err != nil { return err } *i.val = T(v) return err } func (i *uintValue[T]) Get() any { return *i.val } func (i *uintValue[T]) String() string { base := i.base if base == 0 { base = 10 } return strconv.FormatUint(uint64(*i.val), base) } // Uint looks up the value of a local Uint64Flag, returns // 0 if not found func (cmd *Command) Uint(name string) uint { return getUint[uint](cmd, name) } // Uint8 looks up the value of a local Uint8Flag, returns // 0 if not found func (cmd *Command) Uint8(name string) uint8 { return getUint[uint8](cmd, name) } // Uint16 looks up the value of a local Uint16Flag, returns // 0 if not found func (cmd *Command) Uint16(name string) uint16 { return getUint[uint16](cmd, name) } // Uint32 looks up the value of a local Uint32Flag, returns // 0 if not found func (cmd *Command) Uint32(name string) uint32 { return getUint[uint32](cmd, name) } // Uint64 looks up the value of a local Uint64Flag, returns // 0 if not found func (cmd *Command) Uint64(name string) uint64 { return getUint[uint64](cmd, name) } func getUint[T uint | uint8 | uint16 | uint32 | uint64](cmd *Command, name string) T { if v, ok := cmd.Value(name).(T); ok { tracef("uint available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("uint NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } ================================================ FILE: flag_uint_slice.go ================================================ package cli type ( UintSlice = SliceBase[uint, IntegerConfig, uintValue[uint]] Uint8Slice = SliceBase[uint8, IntegerConfig, uintValue[uint8]] Uint16Slice = SliceBase[uint16, IntegerConfig, uintValue[uint16]] Uint32Slice = SliceBase[uint32, IntegerConfig, uintValue[uint32]] Uint64Slice = SliceBase[uint64, IntegerConfig, uintValue[uint64]] UintSliceFlag = FlagBase[[]uint, IntegerConfig, UintSlice] Uint8SliceFlag = FlagBase[[]uint8, IntegerConfig, Uint8Slice] Uint16SliceFlag = FlagBase[[]uint16, IntegerConfig, Uint16Slice] Uint32SliceFlag = FlagBase[[]uint32, IntegerConfig, Uint32Slice] Uint64SliceFlag = FlagBase[[]uint64, IntegerConfig, Uint64Slice] ) var ( NewUintSlice = NewSliceBase[uint, IntegerConfig, uintValue[uint]] NewUint8Slice = NewSliceBase[uint8, IntegerConfig, uintValue[uint8]] NewUint16Slice = NewSliceBase[uint16, IntegerConfig, uintValue[uint16]] NewUint32Slice = NewSliceBase[uint32, IntegerConfig, uintValue[uint32]] NewUint64Slice = NewSliceBase[uint64, IntegerConfig, uintValue[uint64]] ) // UintSlice looks up the value of a local UintSliceFlag, returns // nil if not found func (cmd *Command) UintSlice(name string) []uint { return getUintSlice[uint](cmd, name) } // Uint8Slice looks up the value of a local Uint8SliceFlag, returns // nil if not found func (cmd *Command) Uint8Slice(name string) []uint8 { return getUintSlice[uint8](cmd, name) } // Uint16Slice looks up the value of a local Uint16SliceFlag, returns // nil if not found func (cmd *Command) Uint16Slice(name string) []uint16 { return getUintSlice[uint16](cmd, name) } // Uint32Slice looks up the value of a local Uint32SliceFlag, returns // nil if not found func (cmd *Command) Uint32Slice(name string) []uint32 { return getUintSlice[uint32](cmd, name) } // Uint64Slice looks up the value of a local Uint64SliceFlag, returns // nil if not found func (cmd *Command) Uint64Slice(name string) []uint64 { return getUintSlice[uint64](cmd, name) } func getUintSlice[T uint | uint8 | uint16 | uint32 | uint64](cmd *Command, name string) []T { if v, ok := cmd.Value(name).([]T); ok { tracef("uint slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } tracef("uint slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } ================================================ FILE: flag_uint_slice_test.go ================================================ package cli import ( "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCommand_UintSlice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []uint expectErr bool }{ { flag: &UintSliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []uint{1, 2, 3, 4}, }, { flag: &UintSliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []uint{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.UintSlice(name), "UintSlice(%v)", name) } }) } } func TestCommand_Uint8Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []uint8 expectErr bool }{ { flag: &Uint8SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []uint8{1, 2, 3, 4}, }, { flag: &Uint8SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []uint8{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Uint8Slice(name), "Uint8Slice(%v)", name) } }) } } func TestCommand_Uint16Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []uint16 expectErr bool }{ { flag: &Uint16SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []uint16{1, 2, 3, 4}, }, { flag: &Uint16SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []uint16{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Uint16Slice(name), "Uint16Slice(%v)", name) } }) } } func TestCommand_Uint32Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []uint32 expectErr bool }{ { flag: &Uint32SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []uint32{1, 2, 3, 4}, }, { flag: &Uint32SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []uint32{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Uint32Slice(name), "Uint32Slice(%v)", name) } }) } } func TestCommand_Uint64Slice(t *testing.T) { tests := []struct { name string flag Flag arguments []string expect []uint64 expectErr bool }{ { flag: &Uint64SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2,3,4"}, expect: []uint64{1, 2, 3, 4}, }, { flag: &Uint64SliceFlag{ Name: "numbers", }, arguments: []string{"--numbers", "1,2", "--numbers", "3,4"}, expect: []uint64{1, 2, 3, 4}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equalf(t, tt.expect, cmd.Uint64Slice(name), "Uint64Slice(%v)", name) } }) } } ================================================ FILE: flag_uint_test.go ================================================ package cli import ( "flag" "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUintFlag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue uint expectErr bool }{ { name: "valid", flag: &UintFlag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "234567"}, expectedValue: 234567, }, { name: "invalid", flag: &UintFlag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Uint(name)) } }) } } func TestUint8Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue uint8 expectErr bool }{ { name: "valid", flag: &Uint8Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "255"}, expectedValue: 255, }, { name: "invalid", flag: &Uint8Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Uint8(name)) } }) } } func TestUint16Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue uint16 expectErr bool }{ { name: "valid", flag: &Uint16Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "65535"}, expectedValue: 65535, }, { name: "invalid", flag: &Uint16Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, { name: "out of range", flag: &Uint16Flag{ Name: "number", }, arguments: []string{"--number", "65536"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Uint16(name)) } }) } } func TestUint32Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue uint32 expectErr bool }{ { name: "valid", flag: &Uint32Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "2147483648"}, expectedValue: 2147483648, }, { name: "invalid", flag: &Uint32Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, { name: "out of range", flag: &Uint32Flag{ Name: "number", }, arguments: []string{"--number", "4294967297"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Uint32(name)) } }) } } func TestUint64Flag(t *testing.T) { tests := []struct { name string flag Flag arguments []string expectedValue uint64 expectErr bool }{ { name: "valid", flag: &Uint64Flag{ Name: "number", Aliases: []string{"n"}, }, arguments: []string{"--number", "21474836480"}, expectedValue: 21474836480, }, { name: "invalid", flag: &Uint64Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &Command{ Name: "mock", Flags: []Flag{tt.flag}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) for _, name := range tt.flag.Names() { assert.Equal(t, tt.expectedValue, cmd.Uint64(name)) } }) } } func TestUintFlagExt(t *testing.T) { tests := []struct { name string flag *flag.Flag config IntegerConfig arguments []string expectedValue string expectErr bool }{ { name: "valid", flag: &flag.Flag{ Name: "number", }, config: IntegerConfig{}, arguments: []string{"--number", "234567"}, expectedValue: "234567", }, { name: "valid", flag: &flag.Flag{ Name: "number", }, config: IntegerConfig{Base: 10}, arguments: []string{"--number", "234567"}, expectedValue: "234567", }, { name: "valid hex", flag: &flag.Flag{ Name: "number", }, config: IntegerConfig{Base: 16}, arguments: []string{"--number", "39447"}, expectedValue: "39447", }, { name: "valid hex default", flag: &flag.Flag{ Name: "number", DefValue: "FFFF", }, config: IntegerConfig{Base: 16}, expectedValue: "ffff", }, { name: "invalid", flag: &flag.Flag{ Name: "number", }, arguments: []string{"--number", "gopher"}, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var uValue uintValue[uint] var u uint f := &extFlag{f: tt.flag} tt.flag.Value = uValue.Create(u, &u, tt.config) cmd := &Command{ Name: "mock", Flags: []Flag{f}, Writer: io.Discard, ErrWriter: io.Discard, } err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.arguments...)) if tt.expectErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tt.expectedValue, f.GetValue()) }) } } ================================================ FILE: flag_validation_test.go ================================================ package cli import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestFlagDefaultValidation(t *testing.T) { cmd := &Command{ Name: "foo", Flags: []Flag{ &Int64Flag{ Name: "if", Value: 2, // this value should fail validation Validator: func(i int64) error { if (i >= 3 && i <= 10) || (i >= 20 && i <= 24) { return nil } return fmt.Errorf("Value %d not in range [3,10] or [20,24]", i) }, ValidateDefaults: true, }, }, } r := require.New(t) // this is a simple call to test PreParse failure before // parsing has been done r.Error(cmd.Set("if", "11")) // Default value of flag is 2 which should fail validation err := cmd.Run(buildTestContext(t), []string{"foo", "--if", "5"}) r.Error(err) } func TestBoolInverseFlagDefaultValidation(t *testing.T) { cmd := &Command{ Name: "foo", Flags: []Flag{ &BoolWithInverseFlag{ Name: "bif", Value: true, // this value should fail validation Validator: func(i bool) error { if i { return fmt.Errorf("invalid value") } return nil }, ValidateDefaults: true, }, }, } r := require.New(t) // Default value of flag is 2 which should fail validation err := cmd.Run(buildTestContext(t), []string{"foo", "--bif"}) r.Error(err) } func TestFlagValidation(t *testing.T) { r := require.New(t) testCases := []struct { name string arg string errExpected bool }{ { name: "first range less than min", arg: "2", errExpected: true, }, { name: "first range min", arg: "3", }, { name: "first range mid", arg: "7", }, { name: "first range max", arg: "10", }, { name: "first range greater than max", arg: "15", errExpected: true, }, { name: "second range less than min", arg: "19", errExpected: true, }, { name: "second range min", arg: "20", }, { name: "second range mid", arg: "21", }, { name: "second range max", arg: "24", }, { name: "second range greater than max", arg: "27", errExpected: true, }, } for _, testCase := range testCases { cmd := &Command{ Name: "foo", Flags: []Flag{ &Int64Flag{ Name: "it", Value: 5, // note that this value should pass validation Validator: func(i int64) error { if (i >= 3 && i <= 10) || (i >= 20 && i <= 24) { return nil } return fmt.Errorf("Value %d not in range [3,10]U[20,24]", i) }, }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "--it", testCase.arg}) if !testCase.errExpected { r.NoError(err) } else { r.Error(err) } } } func TestBoolInverseFlagValidation(t *testing.T) { r := require.New(t) cmd := &Command{ Name: "foo", Flags: []Flag{ &BoolWithInverseFlag{ Name: "it", Validator: func(b bool) error { if b { return nil } return fmt.Errorf("not valid") }, }, }, } err := cmd.Run(buildTestContext(t), []string{"foo", "--it=false"}) r.Error(err) } ================================================ FILE: funcs.go ================================================ package cli import "context" // ShellCompleteFunc is an action to execute when the shell completion flag is set type ShellCompleteFunc func(context.Context, *Command) // BeforeFunc is an action that executes prior to any subcommands being run once // the context is ready. If a non-nil error is returned, no subcommands are // run. type BeforeFunc func(context.Context, *Command) (context.Context, error) // AfterFunc is an action that executes after any subcommands are run and have // finished. The AfterFunc is run even if Action() panics. type AfterFunc func(context.Context, *Command) error // ActionFunc is the action to execute when no subcommands are specified type ActionFunc func(context.Context, *Command) error // CommandNotFoundFunc is executed if the proper command cannot be found type CommandNotFoundFunc func(context.Context, *Command, string) // ConfigureShellCompletionCommand is a function to configure a shell completion command type ConfigureShellCompletionCommand func(*Command) // OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying // customized usage error messages. This function is able to replace the // original error messages. If this function is not set, the "Incorrect usage" // is displayed and the execution is interrupted. type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error // InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. type InvalidFlagAccessFunc func(context.Context, *Command, string) // ExitErrHandlerFunc is executed if provided in order to handle exitError values // returned by Actions and Before/After functions. type ExitErrHandlerFunc func(context.Context, *Command, error) // FlagStringFunc is used by the help generation to display a flag, which is // expected to be a single line. type FlagStringFunc func(Flag) string // FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix // text for a flag's full name. type FlagNamePrefixFunc func(fullName []string, placeholder string) string // FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help // with the environment variable details. type FlagEnvHintFunc func(envVars []string, str string) string // FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help // with the file path details. type FlagFileHintFunc func(filePath, str string) string ================================================ FILE: go.mod ================================================ module github.com/urfave/cli/v3 go 1.22 require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: godoc-current.txt ================================================ package cli // import "github.com/urfave/cli/v3" Package cli provides a minimal framework for creating and organizing command line Go applications. cli is designed to be easy to understand and write, the most simple cli application can be written as follows: func main() { (&cli.Command{}).Run(context.Background(), os.Args) } Of course this application does not do much, so let's make this an actual application: func main() { cmd := &cli.Command{ Name: "greet", Usage: "say a greeting", Action: func(c *cli.Context) error { fmt.Println("Greetings") return nil }, } cmd.Run(context.Background(), os.Args) } VARIABLES var ( NewFloatSlice = NewSliceBase[float64, NoConfig, floatValue[float64]] NewFloat32Slice = NewSliceBase[float32, NoConfig, floatValue[float32]] NewFloat64Slice = NewSliceBase[float64, NoConfig, floatValue[float64]] ) var ( NewIntSlice = NewSliceBase[int, IntegerConfig, intValue[int]] NewInt8Slice = NewSliceBase[int8, IntegerConfig, intValue[int8]] NewInt16Slice = NewSliceBase[int16, IntegerConfig, intValue[int16]] NewInt32Slice = NewSliceBase[int32, IntegerConfig, intValue[int32]] NewInt64Slice = NewSliceBase[int64, IntegerConfig, intValue[int64]] ) var ( NewUintSlice = NewSliceBase[uint, IntegerConfig, uintValue[uint]] NewUint8Slice = NewSliceBase[uint8, IntegerConfig, uintValue[uint8]] NewUint16Slice = NewSliceBase[uint16, IntegerConfig, uintValue[uint16]] NewUint32Slice = NewSliceBase[uint32, IntegerConfig, uintValue[uint32]] NewUint64Slice = NewSliceBase[uint64, IntegerConfig, uintValue[uint64]] ) var ( SuggestFlag SuggestFlagFunc = suggestFlag SuggestCommand SuggestCommandFunc = suggestCommand SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate ) var AnyArguments = []Argument{ &StringArgs{ Max: -1, }, } AnyArguments to differentiate between no arguments(nil) vs aleast one var ArgsUsageCommandHelp = "[command]" ArgsUsageCommandHelp is a short description of the arguments of the help command var CommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{template "usageTemplate" .}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}} GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}} ` CommandHelpTemplate is the text template for the command help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var DefaultAppComplete = DefaultRootCommandComplete DefaultAppComplete is a backward-compatible name for DefaultRootCommandComplete. var DefaultInverseBoolPrefix = "no-" var ErrWriter io.Writer = os.Stderr ErrWriter is used to write errors to the user. This can be anything implementing the io.Writer interface and defaults to os.Stderr. var FishCompletionTemplate = `# {{ .Command.Name }} fish shell completion function __fish_{{ .Command.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' for i in (commandline -opc) if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} return 1 end end return 0 end {{ range $v := .Completions }}{{ $v }} {{ end }}` var NewStringMap = NewMapBase[string, StringConfig, stringValue] var NewStringSlice = NewSliceBase[string, StringConfig, stringValue] var OsExiter = os.Exit OsExiter is the function used when the app exits. If not set defaults to os.Exit. var RootCommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}} {{if .VisibleFlags}}[global options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}} {{- if len .Authors}} AUTHOR{{template "authorsTemplate" .}}{{end}}{{if .VisibleCommands}} COMMANDS:{{template "visibleCommandCategoryTemplate" .}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .Copyright}} COPYRIGHT: {{template "copyrightTemplate" .}}{{end}} ` RootCommandHelpTemplate is the text template for the Default help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var ShowAppHelp = ShowRootCommandHelp ShowAppHelp is a backward-compatible name for ShowRootCommandHelp. var ShowAppHelpAndExit = ShowRootCommandHelpAndExit ShowAppHelpAndExit is a backward-compatible name for ShowRootCommandHelp. var ShowCommandHelp = DefaultShowCommandHelp ShowCommandHelp prints help for the given command var ShowRootCommandHelp = DefaultShowRootCommandHelp ShowRootCommandHelp is an action that displays help for the root command. var ShowSubcommandHelp = DefaultShowSubcommandHelp ShowSubcommandHelp prints help for the given subcommand var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}} COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} ` SubcommandHelpTemplate is the text template for the subcommand help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var UsageCommandHelp = "Shows a list of commands or help for one command" UsageCommandHelp is the text to override the USAGE section of the help command var VersionPrinter = DefaultPrintVersion VersionPrinter prints the version for the root Command. FUNCTIONS func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) func DefaultPrintHelp(out io.Writer, templ string, data any) DefaultPrintHelp is the default implementation of HelpPrinter. func DefaultPrintHelpCustom(out io.Writer, templ string, data any, customFuncs map[string]any) DefaultPrintHelpCustom is the default implementation of HelpPrinterCustom. The customFuncs map will be combined with a default template.FuncMap to allow using arbitrary functions in template rendering. func DefaultPrintVersion(cmd *Command) DefaultPrintVersion is the default implementation of VersionPrinter. func DefaultRootCommandComplete(ctx context.Context, cmd *Command) DefaultRootCommandComplete prints the list of subcommands as the default completion method. func DefaultShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error DefaultShowCommandHelp is the default implementation of ShowCommandHelp. func DefaultShowRootCommandHelp(cmd *Command) error DefaultShowRootCommandHelp is the default implementation of ShowRootCommandHelp. func DefaultShowSubcommandHelp(cmd *Command) error DefaultShowSubcommandHelp is the default implementation of ShowSubcommandHelp. func FlagNames(name string, aliases []string) []string func HandleExitCoder(err error) HandleExitCoder handles errors implementing ExitCoder by printing their message and calling OsExiter with the given exit code. If the given error instead implements MultiError, each error will be checked for the ExitCoder interface, and OsExiter will be called with the last exit code found, or exit code 1 if no ExitCoder is found. This function is the default error-handling behavior for a Command. func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) ShowCommandHelpAndExit exits with code after showing help via ShowCommandHelp. func ShowRootCommandHelpAndExit(cmd *Command, exitCode int) ShowRootCommandHelpAndExit prints the list of subcommands and exits with exit code. func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) ShowSubcommandHelpAndExit prints help for the given subcommand via ShowSubcommandHelp and exits with exit code. func ShowVersion(cmd *Command) ShowVersion prints the version number of the root Command. TYPES type ActionFunc func(context.Context, *Command) error ActionFunc is the action to execute when no subcommands are specified type ActionableFlag interface { RunAction(context.Context, *Command) error } ActionableFlag is an interface that wraps Flag interface and RunAction operation. type AfterFunc func(context.Context, *Command) error AfterFunc is an action that executes after any subcommands are run and have finished. The AfterFunc is run even if Action() panics. type Args interface { // Get returns the nth argument, or else a blank string Get(n int) string // First returns the first argument, or else a blank string First() string // Tail returns the rest of the arguments (not the first one) // or else an empty string slice Tail() []string // Len returns the length of the wrapped slice Len() int // Present checks if there are any arguments present Present() bool // Slice returns a copy of the internal slice Slice() []string } type Argument interface { // which this argument can be accessed using the given name HasName(string) bool // Parse the given args and return unparsed args and/or error Parse([]string) ([]string, error) // The usage template for this argument to use in help Usage() string // The Value of this Arg Get() any } Argument captures a positional argument that can be parsed type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // the name of this argument Value T `json:"value"` // the default value of this argument Destination *T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show Config C `json:"config"` // config for this argument similar to Flag Config // Has unexported fields. } func (a *ArgumentBase[T, C, VC]) Get() any func (a *ArgumentBase[T, C, VC]) HasName(s string) bool func (a *ArgumentBase[T, C, VC]) Parse(s []string) ([]string, error) func (a *ArgumentBase[T, C, VC]) Usage() string type ArgumentsBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // the name of this argument Value T `json:"value"` // the default value of this argument Destination *[]T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show Min int `json:"minTimes"` // the min num of occurrences of this argument Max int `json:"maxTimes"` // the max num of occurrences of this argument, set to -1 for unlimited Config C `json:"config"` // config for this argument similar to Flag Config // Has unexported fields. } ArgumentsBase is a base type for slice arguments func (a *ArgumentsBase[T, C, VC]) Get() any func (a *ArgumentsBase[T, C, VC]) HasName(s string) bool func (a *ArgumentsBase[T, C, VC]) Parse(s []string) ([]string, error) func (a *ArgumentsBase[T, C, VC]) Usage() string type BeforeFunc func(context.Context, *Command) (context.Context, error) BeforeFunc is an action that executes prior to any subcommands being run once the context is ready. If a non-nil error is returned, no subcommands are run. type BoolConfig struct { Count *int } BoolConfig defines the configuration for bool flags type BoolFlag = FlagBase[bool, BoolConfig, boolValue] type BoolWithInverseFlag struct { Name string `json:"name"` // name of the flag Category string `json:"category"` // category of the flag, if any DefaultText string `json:"defaultText"` // default text of the flag for usage purposes HideDefault bool `json:"hideDefault"` // whether to hide the default value in output Usage string `json:"usage"` // usage string for help output Sources ValueSourceChain `json:"-"` // sources to load flag value from Required bool `json:"required"` // whether the flag is required or not Hidden bool `json:"hidden"` // whether to hide the flag in help output Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well Value bool `json:"defaultValue"` // default value for this flag if not set by from any source Destination *bool `json:"-"` // destination pointer for value when set Aliases []string `json:"aliases"` // Aliases that are allowed for this flag TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes Action func(context.Context, *Command, bool) error `json:"-"` // Action callback to be called when flag is set OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line Validator func(bool) error `json:"-"` // custom function to validate this flag value ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not Config BoolConfig `json:"config"` // Additional/Custom configuration associated with this flag type InversePrefix string `json:"invPrefix"` // The prefix used to indicate a negative value. Default: `env` becomes `no-env` // Has unexported fields. } func (bif *BoolWithInverseFlag) Count() int Count returns the number of times this flag has been invoked func (bif *BoolWithInverseFlag) Get() any func (bif *BoolWithInverseFlag) GetCategory() string GetCategory returns the category of the flag func (bif *BoolWithInverseFlag) GetDefaultText() string GetDefaultText returns the default text for this flag func (bif *BoolWithInverseFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag func (bif *BoolWithInverseFlag) GetUsage() string GetUsage returns the usage string for the flag func (bif *BoolWithInverseFlag) GetValue() string GetValue returns the flags value as string representation and an empty string if the flag takes no value at all. func (bif *BoolWithInverseFlag) IsBoolFlag() bool IsBoolFlag returns whether the flag doesnt need to accept args func (bif *BoolWithInverseFlag) IsDefaultVisible() bool IsDefaultVisible returns true if the flag is not hidden, otherwise false func (bif *BoolWithInverseFlag) IsLocal() bool func (bif *BoolWithInverseFlag) IsRequired() bool func (bif *BoolWithInverseFlag) IsSet() bool func (bif *BoolWithInverseFlag) IsVisible() bool func (bif *BoolWithInverseFlag) Names() []string func (bif *BoolWithInverseFlag) PostParse() error func (bif *BoolWithInverseFlag) PreParse() error func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) func (bif *BoolWithInverseFlag) String() string String implements the standard Stringer interface. Example for BoolFlag{Name: "env"} --[no-]env (default: false) func (bif *BoolWithInverseFlag) TakesValue() bool func (bif *BoolWithInverseFlag) TypeName() string TypeName is used for stringify/docs. For bool its a no-op type CategorizableFlag interface { // Returns the category of the flag GetCategory() string // Sets the category of the flag SetCategory(string) } CategorizableFlag is an interface that allows us to potentially use a flag in a categorized representation. type Command struct { // The name of the command Name string `json:"name"` // A list of aliases for the command Aliases []string `json:"aliases"` // A short description of the usage of this command Usage string `json:"usage"` // Text to override the USAGE section of help UsageText string `json:"usageText"` // A short description of the arguments of this command ArgsUsage string `json:"argsUsage"` // Version of the command Version string `json:"version"` // Longer explanation of how the command works Description string `json:"description"` // DefaultCommand is the (optional) name of a command // to run if no command names are passed as CLI arguments. DefaultCommand string `json:"defaultCommand"` // The category the command is part of Category string `json:"category"` // List of child commands Commands []*Command `json:"commands"` // List of flags to parse Flags []Flag `json:"flags"` // Boolean to hide built-in help command and help flag HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands EnableShellCompletion bool `json:"-"` // Shell Completion generation command name ShellCompletionCommandName string `json:"-"` // The function to call when checking for shell command completions ShellComplete ShellCompleteFunc `json:"-"` // The function to configure a shell completion command ConfigureShellCompletionCommand ConfigureShellCompletionCommand `json:"-"` // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc `json:"-"` // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc `json:"-"` // The function to call when this command is invoked Action ActionFunc `json:"-"` // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc `json:"-"` // Execute this function if a usage error occurs. OnUsageError OnUsageErrorFunc `json:"-"` // Execute this function when an invalid flag is accessed from the context InvalidFlagAccessHandler InvalidFlagAccessFunc `json:"-"` // Boolean to hide this command from help or completion Hidden bool `json:"hidden"` // List of all authors who contributed (string or fmt.Stringer) // TODO: ~string | fmt.Stringer when interface unions are available Authors []any `json:"authors"` // Copyright of the binary if any Copyright string `json:"copyright"` // Reader reader to write input to (useful for tests) Reader io.Reader `json:"-"` // Writer writer to write output to Writer io.Writer `json:"-"` // ErrWriter writes error output ErrWriter io.Writer `json:"-"` // ExitErrHandler processes any error encountered while running a Command before it is // returned to the caller. If no function is provided, HandleExitCoder is used as the // default behavior. ExitErrHandler ExitErrHandlerFunc `json:"-"` // Other custom info Metadata map[string]interface{} `json:"metadata"` // Carries a function which returns app specific info. ExtraInfo func() map[string]string `json:"-"` // CustomRootCommandHelpTemplate the text template for app help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomRootCommandHelpTemplate string `json:"-"` // SliceFlagSeparator is used to customize the separator for SliceFlag, the default is "," SliceFlagSeparator string `json:"sliceFlagSeparator"` // DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"` // MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "=" MapFlagKeyValueSeparator string `json:"mapFlagKeyValueSeparator"` // Boolean to enable short-option handling so user can combine several // single-character bool arguments into one // i.e. foobar -o -v -> foobar -ov UseShortOptionHandling bool `json:"useShortOptionHandling"` // Enable suggestions for commands and flags Suggest bool `json:"suggest"` // Allows global flags set by libraries which use flag.XXXVar(...) directly // to be parsed through this library AllowExtFlags bool `json:"allowExtFlags"` // Treat all flags as normal arguments if true SkipFlagParsing bool `json:"skipFlagParsing"` // CustomHelpTemplate the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string `json:"-"` // Use longest prefix match for commands PrefixMatchCommands bool `json:"prefixMatchCommands"` // Custom suggest command for matching SuggestCommandFunc SuggestCommandFunc `json:"-"` // Flag exclusion group MutuallyExclusiveFlags []MutuallyExclusiveFlags `json:"mutuallyExclusiveFlags"` // Arguments to parse for this command Arguments []Argument `json:"arguments"` // Whether to read arguments from stdin // applicable to root command only ReadArgsFromStdin bool `json:"readArgsFromStdin"` // StopOnNthArg provides v2-like behavior for specific commands by stopping // flag parsing after N positional arguments are encountered. When set to N, // all remaining arguments after the Nth positional argument will be treated // as arguments, not flags. // // A value of 0 means all arguments are treated as positional (no flag parsing). // A nil value means normal v3 flag parsing behavior (flags can appear anywhere). StopOnNthArg *int `json:"stopOnNthArg"` // Has unexported fields. } Command contains everything needed to run an application that accepts a string slice of arguments such as os.Args. A given Command may contain Flags and sub-commands in Commands. func (cmd *Command) Args() Args Args returns the command line arguments associated with the command. func (cmd *Command) Bool(name string) bool func (cmd *Command) Command(name string) *Command func (cmd *Command) Count(name string) int Count returns the num of occurrences of this flag func (cmd *Command) Duration(name string) time.Duration func (cmd *Command) FlagNames() []string FlagNames returns a slice of flag names used by the this command and all of its parent commands. func (cmd *Command) Float(name string) float64 Float looks up the value of a local FloatFlag, returns 0 if not found func (cmd *Command) Float32(name string) float32 Float32 looks up the value of a local Float32Flag, returns 0 if not found func (c *Command) Float32Arg(name string) float32 func (c *Command) Float32Args(name string) []float32 func (cmd *Command) Float32Slice(name string) []float32 Float32Slice looks up the value of a local Float32Slice, returns nil if not found func (cmd *Command) Float64(name string) float64 Float64 looks up the value of a local Float64Flag, returns 0 if not found func (c *Command) Float64Arg(name string) float64 func (c *Command) Float64Args(name string) []float64 func (cmd *Command) Float64Slice(name string) []float64 Float64Slice looks up the value of a local Float64SliceFlag, returns nil if not found func (c *Command) FloatArg(name string) float64 func (c *Command) FloatArgs(name string) []float64 func (cmd *Command) FloatSlice(name string) []float64 FloatSlice looks up the value of a local FloatSliceFlag, returns nil if not found func (cmd *Command) FullName() string FullName returns the full name of the command. For commands with parents this ensures that the parent commands are part of the command path. func (cmd *Command) Generic(name string) Value Generic looks up the value of a local GenericFlag, returns nil if not found func (cmd *Command) HasName(name string) bool HasName returns true if Command.Name matches given name func (cmd *Command) Int(name string) int Int looks up the value of a local Int64Flag, returns 0 if not found func (cmd *Command) Int16(name string) int16 Int16 looks up the value of a local Int16Flag, returns 0 if not found func (c *Command) Int16Arg(name string) int16 func (c *Command) Int16Args(name string) []int16 func (cmd *Command) Int16Slice(name string) []int16 Int16Slice looks up the value of a local Int16SliceFlag, returns nil if not found func (cmd *Command) Int32(name string) int32 Int32 looks up the value of a local Int32Flag, returns 0 if not found func (c *Command) Int32Arg(name string) int32 func (c *Command) Int32Args(name string) []int32 func (cmd *Command) Int32Slice(name string) []int32 Int32Slice looks up the value of a local Int32SliceFlag, returns nil if not found func (cmd *Command) Int64(name string) int64 Int64 looks up the value of a local Int64Flag, returns 0 if not found func (c *Command) Int64Arg(name string) int64 func (c *Command) Int64Args(name string) []int64 func (cmd *Command) Int64Slice(name string) []int64 Int64Slice looks up the value of a local Int64SliceFlag, returns nil if not found func (cmd *Command) Int8(name string) int8 Int8 looks up the value of a local Int8Flag, returns 0 if not found func (c *Command) Int8Arg(name string) int8 func (c *Command) Int8Args(name string) []int8 func (cmd *Command) Int8Slice(name string) []int8 Int8Slice looks up the value of a local Int8SliceFlag, returns nil if not found func (c *Command) IntArg(name string) int func (c *Command) IntArgs(name string) []int func (cmd *Command) IntSlice(name string) []int IntSlice looks up the value of a local IntSliceFlag, returns nil if not found func (cmd *Command) IsSet(name string) bool IsSet determines if the flag was actually set func (cmd *Command) Lineage() []*Command Lineage returns *this* command and all of its ancestor commands in order from child to parent func (cmd *Command) LocalFlagNames() []string LocalFlagNames returns a slice of flag names used in this command. func (cmd *Command) NArg() int NArg returns the number of the command line arguments. func (cmd *Command) Names() []string Names returns the names including short names and aliases. func (cmd *Command) NumFlags() int NumFlags returns the number of flags set func (cmd *Command) Root() *Command Root returns the Command at the root of the graph func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) Run is the entry point to the command graph. The positional arguments are parsed according to the Flag and Command definitions and the matching Action functions are run. func (cmd *Command) Set(name, value string) error Set sets a context flag to a value. func (cmd *Command) String(name string) string func (c *Command) StringArg(name string) string func (c *Command) StringArgs(name string) []string func (cmd *Command) StringMap(name string) map[string]string StringMap looks up the value of a local StringMapFlag, returns nil if not found func (cmd *Command) StringSlice(name string) []string StringSlice looks up the value of a local StringSliceFlag, returns nil if not found func (cmd *Command) Timestamp(name string) time.Time Timestamp gets the timestamp from a flag name func (c *Command) TimestampArg(name string) time.Time func (c *Command) TimestampArgs(name string) []time.Time func (cmd *Command) ToFishCompletion() (string, error) ToFishCompletion creates a fish completion string for the `*Command` The function errors if either parsing or writing of the string fails. func (cmd *Command) Uint(name string) uint Uint looks up the value of a local Uint64Flag, returns 0 if not found func (cmd *Command) Uint16(name string) uint16 Uint16 looks up the value of a local Uint16Flag, returns 0 if not found func (c *Command) Uint16Arg(name string) uint16 func (c *Command) Uint16Args(name string) []uint16 func (cmd *Command) Uint16Slice(name string) []uint16 Uint16Slice looks up the value of a local Uint16SliceFlag, returns nil if not found func (cmd *Command) Uint32(name string) uint32 Uint32 looks up the value of a local Uint32Flag, returns 0 if not found func (c *Command) Uint32Arg(name string) uint32 func (c *Command) Uint32Args(name string) []uint32 func (cmd *Command) Uint32Slice(name string) []uint32 Uint32Slice looks up the value of a local Uint32SliceFlag, returns nil if not found func (cmd *Command) Uint64(name string) uint64 Uint64 looks up the value of a local Uint64Flag, returns 0 if not found func (c *Command) Uint64Arg(name string) uint64 func (c *Command) Uint64Args(name string) []uint64 func (cmd *Command) Uint64Slice(name string) []uint64 Uint64Slice looks up the value of a local Uint64SliceFlag, returns nil if not found func (cmd *Command) Uint8(name string) uint8 Uint8 looks up the value of a local Uint8Flag, returns 0 if not found func (c *Command) Uint8Arg(name string) uint8 func (c *Command) Uint8Args(name string) []uint8 func (cmd *Command) Uint8Slice(name string) []uint8 Uint8Slice looks up the value of a local Uint8SliceFlag, returns nil if not found func (c *Command) UintArg(name string) uint func (c *Command) UintArgs(name string) []uint func (cmd *Command) UintSlice(name string) []uint UintSlice looks up the value of a local UintSliceFlag, returns nil if not found func (cmd *Command) Value(name string) interface{} Value returns the value of the flag corresponding to `name` func (cmd *Command) VisibleCategories() []CommandCategory VisibleCategories returns a slice of categories and commands that are Hidden=false func (cmd *Command) VisibleCommands() []*Command VisibleCommands returns a slice of the Commands with Hidden=false func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain func (cmd *Command) VisibleFlags() []Flag VisibleFlags returns a slice of the Flags with Hidden=false func (cmd *Command) VisiblePersistentFlags() []Flag VisiblePersistentFlags returns a slice of LocalFlag with Persistent=true and Hidden=false. type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command) // Categories returns a slice of categories sorted by name Categories() []CommandCategory } CommandCategories interface allows for category manipulation type CommandCategory interface { // Name returns the category name string Name() string // VisibleCommands returns a slice of the Commands with Hidden=false VisibleCommands() []*Command } CommandCategory is a category containing commands. type CommandNotFoundFunc func(context.Context, *Command, string) CommandNotFoundFunc is executed if the proper command cannot be found type ConfigureShellCompletionCommand func(*Command) ConfigureShellCompletionCommand is a function to configure a shell completion command type Countable interface { Count() int } Countable is an interface to enable detection of flag values which support repetitive flags type DocGenerationFlag interface { // TakesValue returns true if the flag takes a value, otherwise false TakesValue() bool // GetUsage returns the usage string for the flag GetUsage() string // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. GetValue() string // GetDefaultText returns the default text for this flag GetDefaultText() string // GetEnvVars returns the env vars for this flag GetEnvVars() []string // IsDefaultVisible returns whether the default value should be shown in // help text IsDefaultVisible() bool // TypeName to detect if a flag is a string, bool, etc. TypeName() string } DocGenerationFlag is an interface that allows documentation generation for the flag type DocGenerationMultiValueFlag interface { DocGenerationFlag // IsMultiValueFlag returns true for flags that can be given multiple times. IsMultiValueFlag() bool } DocGenerationMultiValueFlag extends DocGenerationFlag for slice/map based flags. type DurationFlag = FlagBase[time.Duration, NoConfig, durationValue] type EnvValueSource interface { IsFromEnv() bool Key() string } EnvValueSource is to specifically detect env sources when printing help text type ErrorFormatter interface { Format(s fmt.State, verb rune) } ErrorFormatter is the interface that will suitably format the error output type ExitCoder interface { error ExitCode() int } ExitCoder is the interface checked by `Command` for a custom exit code. func Exit(message any, exitCode int) ExitCoder Exit wraps a message and exit code into an error, which by default is handled with a call to os.Exit during default error handling. This is the simplest way to trigger a non-zero exit code for a Command without having to call os.Exit manually. During testing, this behavior can be avoided by overriding the ExitErrHandler function on a Command or the package-global OsExiter function. type ExitErrHandlerFunc func(context.Context, *Command, error) ExitErrHandlerFunc is executed if provided in order to handle exitError values returned by Actions and Before/After functions. type Flag interface { fmt.Stringer // Retrieve the value of the Flag Get() any // Lifecycle methods. // flag callback prior to parsing PreParse() error // flag callback post parsing PostParse() error // Apply Flag settings to the given flag set Set(string, string) error // All possible names for this flag Names() []string // Whether the flag has been set or not IsSet() bool } Flag is a common interface related to parsing flags in cli. For more advanced flag parsing techniques, it is recommended that this interface be implemented. var GenerateShellCompletionFlag Flag = &BoolFlag{ Name: "generate-shell-completion", Hidden: true, } GenerateShellCompletionFlag enables shell completion var HelpFlag Flag = &BoolFlag{ Name: "help", Aliases: []string{"h"}, Usage: "show help", HideDefault: true, Local: true, } HelpFlag prints the help for all commands and subcommands. Set to nil to disable the flag. The subcommand will still be added unless HideHelp or HideHelpCommand is set to true. var VersionFlag Flag = &BoolFlag{ Name: "version", Aliases: []string{"v"}, Usage: "print the version", HideDefault: true, Local: true, } VersionFlag prints the version for the application type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // name of the flag Category string `json:"category"` // category of the flag, if any DefaultText string `json:"defaultText"` // default text of the flag for usage purposes HideDefault bool `json:"hideDefault"` // whether to hide the default value in output Usage string `json:"usage"` // usage string for help output Sources ValueSourceChain `json:"-"` // sources to load flag value from Required bool `json:"required"` // whether the flag is required or not Hidden bool `json:"hidden"` // whether to hide the flag in help output Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well Value T `json:"defaultValue"` // default value for this flag if not set by from any source Destination *T `json:"-"` // destination pointer for value when set Aliases []string `json:"aliases"` // Aliases that are allowed for this flag TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes Action func(context.Context, *Command, T) error `json:"-"` // Action callback to be called when flag is set Config C `json:"config"` // Additional/Custom configuration associated with this flag type OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line Validator func(T) error `json:"-"` // custom function to validate this flag value ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not // Has unexported fields. } FlagBase [T,C,VC] is a generic flag base which can be used as a boilerplate to implement the most common interfaces used by urfave/cli. T specifies the type C specifies the configuration required(if any for that flag type) VC specifies the value creator which creates the flag.Value emulation func (f *FlagBase[T, C, VC]) Count() int Count returns the number of times this flag has been invoked func (f *FlagBase[T, C, V]) Get() any func (f *FlagBase[T, C, V]) GetCategory() string GetCategory returns the category of the flag func (f *FlagBase[T, C, V]) GetDefaultText() string GetDefaultText returns the default text for this flag func (f *FlagBase[T, C, V]) GetEnvVars() []string GetEnvVars returns the env vars for this flag func (f *FlagBase[T, C, V]) GetUsage() string GetUsage returns the usage string for the flag func (f *FlagBase[T, C, V]) GetValue() string GetValue returns the flags value as string representation and an empty string if the flag takes no value at all. func (f *FlagBase[T, C, VC]) IsBoolFlag() bool IsBoolFlag returns whether the flag doesnt need to accept args func (f *FlagBase[T, C, V]) IsDefaultVisible() bool IsDefaultVisible returns true if the flag is not hidden, otherwise false func (f *FlagBase[T, C, VC]) IsLocal() bool IsLocal returns false if flag needs to be persistent across subcommands func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool IsMultiValueFlag returns true if the value type T can take multiple values from cmd line. This is true for slice and map type flags func (f *FlagBase[T, C, V]) IsRequired() bool IsRequired returns whether or not the flag is required func (f *FlagBase[T, C, V]) IsSet() bool IsSet returns whether or not the flag has been set through env or file func (f *FlagBase[T, C, V]) IsVisible() bool IsVisible returns true if the flag is not hidden, otherwise false func (f *FlagBase[T, C, V]) Names() []string Names returns the names of the flag func (f *FlagBase[T, C, V]) PostParse() error PostParse populates the flag given the flag set and environment func (f *FlagBase[T, C, V]) PreParse() error func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set func (f *FlagBase[T, C, V]) Set(_ string, val string) error Set applies given value from string func (f *FlagBase[T, C, V]) SetCategory(c string) func (f *FlagBase[T, C, V]) String() string String returns a readable representation of this value (for usage defaults) func (f *FlagBase[T, C, V]) TakesValue() bool TakesValue returns true if the flag takes a value, otherwise false func (f *FlagBase[T, C, V]) TypeName() string TypeName returns the type of the flag. type FlagCategories interface { // AddFlags adds a flag to a category, creating a new category if necessary. AddFlag(category string, fl Flag) // VisibleCategories returns a slice of visible flag categories sorted by name VisibleCategories() []VisibleFlagCategory } FlagCategories interface allows for category manipulation type FlagEnvHintFunc func(envVars []string, str string) string FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help with the environment variable details. var FlagEnvHinter FlagEnvHintFunc = withEnvHint FlagEnvHinter annotates flag help message with the environment variable details. This is used by the default FlagStringer. type FlagFileHintFunc func(filePath, str string) string FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help with the file path details. var FlagFileHinter FlagFileHintFunc = withFileHint FlagFileHinter annotates flag help message with the environment variable details. This is used by the default FlagStringer. type FlagNamePrefixFunc func(fullName []string, placeholder string) string FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix text for a flag's full name. var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames FlagNamePrefixer converts a full flag name and its placeholder into the help message flag prefix. This is used by the default FlagStringer. type FlagStringFunc func(Flag) string FlagStringFunc is used by the help generation to display a flag, which is expected to be a single line. var FlagStringer FlagStringFunc = stringifyFlag FlagStringer converts a flag definition to a string. This is used by help to display a flag. type FlagsByName []Flag FlagsByName is a slice of Flag. func (f FlagsByName) Len() int func (f FlagsByName) Less(i, j int) bool func (f FlagsByName) Swap(i, j int) type Float32Arg = ArgumentBase[float32, NoConfig, floatValue[float32]] type Float32Args = ArgumentsBase[float32, NoConfig, floatValue[float32]] type Float32Flag = FlagBase[float32, NoConfig, floatValue[float32]] type Float32Slice = SliceBase[float32, NoConfig, floatValue[float32]] type Float32SliceFlag = FlagBase[[]float32, NoConfig, Float32Slice] type Float64Arg = ArgumentBase[float64, NoConfig, floatValue[float64]] type Float64Args = ArgumentsBase[float64, NoConfig, floatValue[float64]] type Float64Flag = FlagBase[float64, NoConfig, floatValue[float64]] type Float64Slice = SliceBase[float64, NoConfig, floatValue[float64]] type Float64SliceFlag = FlagBase[[]float64, NoConfig, Float64Slice] type FloatArg = ArgumentBase[float64, NoConfig, floatValue[float64]] type FloatArgs = ArgumentsBase[float64, NoConfig, floatValue[float64]] type FloatFlag = FlagBase[float64, NoConfig, floatValue[float64]] type FloatSlice = SliceBase[float64, NoConfig, floatValue[float64]] type FloatSliceFlag = FlagBase[[]float64, NoConfig, FloatSlice] type GenericFlag = FlagBase[Value, NoConfig, genericValue] type HelpPrinterCustomFunc func(w io.Writer, templ string, data any, customFunc map[string]any) Prints help for the Command with custom template function. var HelpPrinterCustom HelpPrinterCustomFunc = DefaultPrintHelpCustom HelpPrinterCustom is a function that writes the help output. It is used as the default implementation of HelpPrinter, and may be called directly if the ExtraInfo field is set on a Command. In the default implementation, if the customFuncs argument contains a "wrapAt" key, which is a function which takes no arguments and returns an int, this int value will be used to produce a "wrap" function used by the default template to wrap long lines. type HelpPrinterFunc func(w io.Writer, templ string, data any) HelpPrinterFunc prints help for the Command. var HelpPrinter HelpPrinterFunc = DefaultPrintHelp HelpPrinter is a function that writes the help output. If not set explicitly, this calls HelpPrinterCustom using only the default template functions. If custom logic for printing help is required, this function can be overridden. If the ExtraInfo field is defined on a Command, this function should not be modified, as HelpPrinterCustom will be used directly in order to capture the extra information. type Int16Arg = ArgumentBase[int16, IntegerConfig, intValue[int16]] type Int16Args = ArgumentsBase[int16, IntegerConfig, intValue[int16]] type Int16Flag = FlagBase[int16, IntegerConfig, intValue[int16]] type Int16Slice = SliceBase[int16, IntegerConfig, intValue[int16]] type Int16SliceFlag = FlagBase[[]int16, IntegerConfig, Int16Slice] type Int32Arg = ArgumentBase[int32, IntegerConfig, intValue[int32]] type Int32Args = ArgumentsBase[int32, IntegerConfig, intValue[int32]] type Int32Flag = FlagBase[int32, IntegerConfig, intValue[int32]] type Int32Slice = SliceBase[int32, IntegerConfig, intValue[int32]] type Int32SliceFlag = FlagBase[[]int32, IntegerConfig, Int32Slice] type Int64Arg = ArgumentBase[int64, IntegerConfig, intValue[int64]] type Int64Args = ArgumentsBase[int64, IntegerConfig, intValue[int64]] type Int64Flag = FlagBase[int64, IntegerConfig, intValue[int64]] type Int64Slice = SliceBase[int64, IntegerConfig, intValue[int64]] type Int64SliceFlag = FlagBase[[]int64, IntegerConfig, Int64Slice] type Int8Arg = ArgumentBase[int8, IntegerConfig, intValue[int8]] type Int8Args = ArgumentsBase[int8, IntegerConfig, intValue[int8]] type Int8Flag = FlagBase[int8, IntegerConfig, intValue[int8]] type Int8Slice = SliceBase[int8, IntegerConfig, intValue[int8]] type Int8SliceFlag = FlagBase[[]int8, IntegerConfig, Int8Slice] type IntArg = ArgumentBase[int, IntegerConfig, intValue[int]] type IntArgs = ArgumentsBase[int, IntegerConfig, intValue[int]] type IntFlag = FlagBase[int, IntegerConfig, intValue[int]] type IntSlice = SliceBase[int, IntegerConfig, intValue[int]] type IntSliceFlag = FlagBase[[]int, IntegerConfig, IntSlice] type IntegerConfig struct { Base int } IntegerConfig is the configuration for all integer type flags type InvalidFlagAccessFunc func(context.Context, *Command, string) InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. type LocalFlag interface { IsLocal() bool } LocalFlag is an interface to enable detection of flags which are local to current command type MapBase[T any, C any, VC ValueCreator[T, C]] struct { // Has unexported fields. } MapBase wraps map[string]T to satisfy flag.Value func NewMapBase[T any, C any, VC ValueCreator[T, C]](defaults map[string]T) *MapBase[T, C, VC] NewMapBase makes a *MapBase with default values func (i MapBase[T, C, VC]) Create(val map[string]T, p *map[string]T, c C) Value func (i *MapBase[T, C, VC]) Get() interface{} Get returns the mapping of values set by this flag func (i *MapBase[T, C, VC]) Serialize() string Serialize allows MapBase to fulfill Serializer func (i *MapBase[T, C, VC]) Set(value string) error Set parses the value and appends it to the list of values func (i *MapBase[T, C, VC]) String() string String returns a readable representation of this value (for usage defaults) func (i MapBase[T, C, VC]) ToString(t map[string]T) string func (i *MapBase[T, C, VC]) Value() map[string]T Value returns the mapping of values set by this flag type MapSource interface { fmt.Stringer fmt.GoStringer // Lookup returns the value from the source based on key // and if it was found // or returns an empty string and false Lookup(string) (any, bool) } MapSource is a source which can be used to look up a value based on a key typically for use with a cli.Flag func NewMapSource(name string, m map[any]any) MapSource type MultiError interface { error Errors() []error } MultiError is an error that wraps multiple errors. type MutuallyExclusiveFlags struct { // Flag list Flags [][]Flag // whether this group is required Required bool // Category to apply to all flags within group Category string } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline So for example [ --foo | [ --bar something --darth somethingelse ] ] type NoConfig struct{} NoConfig is for flags which dont need a custom configuration type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying customized usage error messages. This function is able to replace the original error messages. If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. type RequiredFlag interface { // whether the flag is a required flag or not IsRequired() bool } RequiredFlag is an interface that allows us to mark flags as required it allows flags required flags to be backwards compatible with the Flag interface type Serializer interface { Serialize() string } Serializer is used to circumvent the limitations of flag.FlagSet.Set type ShellCompleteFunc func(context.Context, *Command) ShellCompleteFunc is an action to execute when the shell completion flag is set type SliceBase[T any, C any, VC ValueCreator[T, C]] struct { // Has unexported fields. } SliceBase wraps []T to satisfy flag.Value func NewSliceBase[T any, C any, VC ValueCreator[T, C]](defaults ...T) *SliceBase[T, C, VC] NewSliceBase makes a *SliceBase with default values func (i SliceBase[T, C, VC]) Create(val []T, p *[]T, c C) Value func (i *SliceBase[T, C, VC]) Get() interface{} Get returns the slice of values set by this flag func (i *SliceBase[T, C, VC]) Serialize() string Serialize allows SliceBase to fulfill Serializer func (i *SliceBase[T, C, VC]) Set(value string) error Set parses the value and appends it to the list of values func (i *SliceBase[T, C, VC]) String() string String returns a readable representation of this value (for usage defaults) func (i SliceBase[T, C, VC]) ToString(t []T) string func (i *SliceBase[T, C, VC]) Value() []T Value returns the slice of values set by this flag type StringArg = ArgumentBase[string, StringConfig, stringValue] type StringArgs = ArgumentsBase[string, StringConfig, stringValue] type StringConfig struct { // Whether to trim whitespace of parsed value TrimSpace bool } StringConfig defines the configuration for string flags type StringFlag = FlagBase[string, StringConfig, stringValue] type StringMap = MapBase[string, StringConfig, stringValue] type StringMapArgs = ArgumentBase[map[string]string, StringConfig, StringMap] type StringMapFlag = FlagBase[map[string]string, StringConfig, StringMap] type StringSlice = SliceBase[string, StringConfig, stringValue] type StringSliceFlag = FlagBase[[]string, StringConfig, StringSlice] type SuggestCommandFunc func(commands []*Command, provided string) string type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue] type TimestampArgs = ArgumentsBase[time.Time, TimestampConfig, timestampValue] type TimestampConfig struct { Timezone *time.Location // Available layouts for flag value. // // Note that value for formats with missing year/date will be interpreted as current year/date respectively. // // Read more about time layouts: https://pkg.go.dev/time#pkg-constants Layouts []string } TimestampConfig defines the config for timestamp flags type TimestampFlag = FlagBase[time.Time, TimestampConfig, timestampValue] type Uint16Arg = ArgumentBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16Args = ArgumentsBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16Flag = FlagBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16Slice = SliceBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16SliceFlag = FlagBase[[]uint16, IntegerConfig, Uint16Slice] type Uint32Arg = ArgumentBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32Args = ArgumentsBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32Flag = FlagBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32Slice = SliceBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32SliceFlag = FlagBase[[]uint32, IntegerConfig, Uint32Slice] type Uint64Arg = ArgumentBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64Args = ArgumentsBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64Flag = FlagBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64Slice = SliceBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64SliceFlag = FlagBase[[]uint64, IntegerConfig, Uint64Slice] type Uint8Arg = ArgumentBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8Args = ArgumentsBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8Flag = FlagBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8Slice = SliceBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8SliceFlag = FlagBase[[]uint8, IntegerConfig, Uint8Slice] type UintArg = ArgumentBase[uint, IntegerConfig, uintValue[uint]] type UintArgs = ArgumentsBase[uint, IntegerConfig, uintValue[uint]] type UintFlag = FlagBase[uint, IntegerConfig, uintValue[uint]] type UintSlice = SliceBase[uint, IntegerConfig, uintValue[uint]] type UintSliceFlag = FlagBase[[]uint, IntegerConfig, UintSlice] type Value interface { flag.Value flag.Getter } Value represents a value as used by cli. For now it implements the golang flag.Value interface type ValueCreator[T any, C any] interface { Create(T, *T, C) Value ToString(T) string } ValueCreator is responsible for creating a flag.Value emulation as well as custom formatting T specifies the type C specifies the config for the type type ValueSource interface { fmt.Stringer fmt.GoStringer // Lookup returns the value from the source and if it was found // or returns an empty string and false Lookup() (string, bool) } ValueSource is a source which can be used to look up a value, typically for use with a cli.Flag func EnvVar(key string) ValueSource func File(path string) ValueSource func NewMapValueSource(key string, ms MapSource) ValueSource type ValueSourceChain struct { Chain []ValueSource } ValueSourceChain contains an ordered series of ValueSource that allows for lookup where the first ValueSource to resolve is returned func EnvVars(keys ...string) ValueSourceChain EnvVars is a helper function to encapsulate a number of envVarValueSource together as a ValueSourceChain func Files(paths ...string) ValueSourceChain Files is a helper function to encapsulate a number of fileValueSource together as a ValueSourceChain func NewValueSourceChain(src ...ValueSource) ValueSourceChain func (vsc *ValueSourceChain) Append(other ValueSourceChain) func (vsc *ValueSourceChain) EnvKeys() []string func (vsc *ValueSourceChain) GoString() string func (vsc *ValueSourceChain) Lookup() (string, bool) func (vsc *ValueSourceChain) LookupWithSource() (string, ValueSource, bool) func (vsc *ValueSourceChain) String() string type VisibleFlag interface { // IsVisible returns true if the flag is not hidden, otherwise false IsVisible() bool } VisibleFlag is an interface that allows to check if a flag is visible type VisibleFlagCategory interface { // Name returns the category name string Name() string // Flags returns a slice of VisibleFlag sorted by name Flags() []Flag } VisibleFlagCategory is a category containing flags. ================================================ FILE: help.go ================================================ package cli import ( "context" "fmt" "io" "os" "strings" "text/tabwriter" "text/template" "unicode/utf8" ) const ( helpName = "help" helpAlias = "h" ) // HelpPrinterFunc prints help for the Command. type HelpPrinterFunc func(w io.Writer, templ string, data any) // Prints help for the Command with custom template function. type HelpPrinterCustomFunc func(w io.Writer, templ string, data any, customFunc map[string]any) // HelpPrinter is a function that writes the help output. If not set explicitly, // this calls HelpPrinterCustom using only the default template functions. // // If custom logic for printing help is required, this function can be // overridden. If the ExtraInfo field is defined on a Command, this function // should not be modified, as HelpPrinterCustom will be used directly in order // to capture the extra information. var HelpPrinter HelpPrinterFunc = DefaultPrintHelp // HelpPrinterCustom is a function that writes the help output. It is used as // the default implementation of HelpPrinter, and may be called directly if // the ExtraInfo field is set on a Command. // // In the default implementation, if the customFuncs argument contains a // "wrapAt" key, which is a function which takes no arguments and returns // an int, this int value will be used to produce a "wrap" function used // by the default template to wrap long lines. var HelpPrinterCustom HelpPrinterCustomFunc = DefaultPrintHelpCustom // VersionPrinter prints the version for the root Command. var VersionPrinter = DefaultPrintVersion // ShowRootCommandHelp is an action that displays help for the root command. var ShowRootCommandHelp = DefaultShowRootCommandHelp // ShowAppHelp is a backward-compatible name for ShowRootCommandHelp. var ShowAppHelp = ShowRootCommandHelp // ShowCommandHelp prints help for the given command var ShowCommandHelp = DefaultShowCommandHelp // ShowSubcommandHelp prints help for the given subcommand var ShowSubcommandHelp = DefaultShowSubcommandHelp // UsageCommandHelp is the text to override the USAGE section of the help command var UsageCommandHelp = "Shows a list of commands or help for one command" // ArgsUsageCommandHelp is a short description of the arguments of the help command var ArgsUsageCommandHelp = "[command]" func buildHelpCommand(withAction bool) *Command { cmd := &Command{ Name: helpName, Aliases: []string{helpAlias}, Usage: UsageCommandHelp, ArgsUsage: ArgsUsageCommandHelp, HideHelp: true, } if withAction { cmd.Action = helpCommandAction } return cmd } func helpCommandAction(ctx context.Context, cmd *Command) error { args := cmd.Args() firstArg := args.First() tracef("doing help for cmd %[1]q with args %[2]q", cmd, args) // This action can be triggered by a "default" action of a command // or via cmd.Run when cmd == helpCmd. So we have following possibilities // // 1 $ app // 2 $ app help // 3 $ app foo // 4 $ app help foo // 5 $ app foo help // Case 4. when executing a help command set the context to parent // to allow resolution of subsequent args. This will transform // $ app help foo // to // $ app foo // which will then be handled as case 3 if cmd.parent != nil && (cmd.HasName(helpName) || cmd.HasName(helpAlias)) { tracef("setting cmd to cmd.parent") cmd = cmd.parent } // Case 4. $ app help foo // foo is the command for which help needs to be shown if firstArg != "" { /* if firstArg == "--" { return nil }*/ tracef("returning ShowCommandHelp with %[1]q", firstArg) return ShowCommandHelp(ctx, cmd, firstArg) } // Case 1 & 2 // Special case when running help on main app itself as opposed to individual // commands/subcommands if cmd.parent == nil { tracef("returning ShowRootCommandHelp") _ = ShowRootCommandHelp(cmd) return nil } // Case 3, 5 if len(cmd.VisibleCommands()) == 0 { tmpl := cmd.CustomHelpTemplate if tmpl == "" { tmpl = CommandHelpTemplate } tracef("running HelpPrinter with command %[1]q", cmd.Name) HelpPrinter(cmd.Root().Writer, tmpl, cmd) return nil } tracef("running ShowSubcommandHelp") return ShowSubcommandHelp(cmd) } // ShowRootCommandHelpAndExit prints the list of subcommands and exits with exit code. func ShowRootCommandHelpAndExit(cmd *Command, exitCode int) { _ = ShowRootCommandHelp(cmd) OsExiter(exitCode) } // ShowAppHelpAndExit is a backward-compatible name for ShowRootCommandHelp. var ShowAppHelpAndExit = ShowRootCommandHelpAndExit // DefaultShowRootCommandHelp is the default implementation of ShowRootCommandHelp. func DefaultShowRootCommandHelp(cmd *Command) error { tmpl := cmd.CustomRootCommandHelpTemplate if tmpl == "" { tracef("using RootCommandHelpTemplate") tmpl = RootCommandHelpTemplate } if cmd.ExtraInfo == nil { HelpPrinter(cmd.Root().Writer, tmpl, cmd.Root()) return nil } tracef("setting ExtraInfo in customAppData") customAppData := func() map[string]any { return map[string]any{ "ExtraInfo": cmd.ExtraInfo, } } HelpPrinterCustom(cmd.Root().Writer, tmpl, cmd.Root(), customAppData()) return nil } // DefaultRootCommandComplete prints the list of subcommands as the default completion method. func DefaultRootCommandComplete(ctx context.Context, cmd *Command) { DefaultCompleteWithFlags(ctx, cmd) } // DefaultAppComplete is a backward-compatible name for DefaultRootCommandComplete. var DefaultAppComplete = DefaultRootCommandComplete func printCommandSuggestions(commands []*Command, writer io.Writer) { shell := os.Getenv("SHELL") for _, command := range commands { if command.Hidden { continue } if (strings.HasSuffix(shell, "zsh") || strings.HasSuffix(shell, "fish")) && len(command.Usage) > 0 { _, _ = fmt.Fprintf(writer, "%s:%s\n", command.Name, command.Usage) } else { _, _ = fmt.Fprintf(writer, "%s\n", command.Name) } } } func cliArgContains(flagName string, args []string) bool { for _, name := range strings.Split(flagName, ",") { name = strings.TrimSpace(name) count := utf8.RuneCountInString(name) if count > 2 { count = 2 } flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) for _, a := range args { if a == flag { return true } } } return false } func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { // Trim to handle both "-short" and "--long" flags. cur := strings.TrimLeft(lastArg, "-") for _, flag := range flags { if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { continue } usage := "" if docFlag, ok := flag.(DocGenerationFlag); ok { usage = docFlag.GetUsage() } name := strings.TrimSpace(flag.Names()[0]) // this will get total count utf8 letters in flag name count := utf8.RuneCountInString(name) if count > 2 { count = 2 // reuse this count to generate single - or -- in flag completion } // if flag name has more than one utf8 letter and last argument in cli has -- prefix then // skip flag completion for short flags example -v or -x if strings.HasPrefix(lastArg, "--") && count == 1 { continue } // match if last argument matches this flag and it is not repeated if strings.HasPrefix(name, cur) && cur != name /* && !cliArgContains(name, os.Args)*/ { flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) shell := os.Getenv("SHELL") if usage != "" && (strings.HasSuffix(shell, "zsh") || strings.HasSuffix(shell, "fish")) { flagCompletion = fmt.Sprintf("%s:%s", flagCompletion, usage) } fmt.Fprintln(writer, flagCompletion) } } } func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) { args := os.Args if cmd != nil && cmd.parent != nil { args = cmd.Args().Slice() tracef("running default complete with flags[%v] on command %[2]q", args, cmd.Name) } else { tracef("running default complete with os.Args flags[%v]", args) } argsLen := len(args) lastArg := "" // parent command will have --generate-shell-completion so we need // to account for that if argsLen > 1 { lastArg = args[argsLen-2] } else if argsLen > 0 { lastArg = args[argsLen-1] } if lastArg == "--" { tracef("No completions due to termination") return } if lastArg == completionFlag { lastArg = "" } if strings.HasPrefix(lastArg, "-") { tracef("printing flag suggestion for flag[%v] on command %[1]q", lastArg, cmd.Name) printFlagSuggestions(lastArg, cmd.Flags, cmd.Root().Writer) return } if cmd != nil { tracef("printing command suggestions on command %[1]q", cmd.Name) printCommandSuggestions(cmd.Commands, cmd.Root().Writer) return } } // ShowCommandHelpAndExit exits with code after showing help via ShowCommandHelp. func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) { _ = ShowCommandHelp(ctx, cmd, command) OsExiter(code) } // DefaultShowCommandHelp is the default implementation of ShowCommandHelp. func DefaultShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error { for _, subCmd := range cmd.Commands { if !subCmd.HasName(commandName) { continue } tmpl := subCmd.CustomHelpTemplate if tmpl == "" { if len(subCmd.Commands) == 0 { tracef("using CommandHelpTemplate") tmpl = CommandHelpTemplate } else { tracef("using SubcommandHelpTemplate") tmpl = SubcommandHelpTemplate } } tracef("running HelpPrinter") HelpPrinter(cmd.Root().Writer, tmpl, subCmd) tracef("returning nil after printing help") return nil } tracef("no matching command found") if cmd.CommandNotFound == nil { errMsg := fmt.Sprintf("No help topic for '%v'", commandName) if cmd.Suggest { if suggestion := SuggestCommand(cmd.Commands, commandName); suggestion != "" { errMsg += ". " + suggestion } } tracef("exiting 3 with errMsg %[1]q", errMsg) return Exit(errMsg, 3) } tracef("running CommandNotFound func for %[1]q", commandName) cmd.CommandNotFound(ctx, cmd, commandName) return nil } // ShowSubcommandHelpAndExit prints help for the given subcommand via ShowSubcommandHelp and exits with exit code. func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) { _ = ShowSubcommandHelp(cmd) OsExiter(exitCode) } // DefaultShowSubcommandHelp is the default implementation of ShowSubcommandHelp. func DefaultShowSubcommandHelp(cmd *Command) error { HelpPrinter(cmd.Root().Writer, SubcommandHelpTemplate, cmd) return nil } // ShowVersion prints the version number of the root Command. func ShowVersion(cmd *Command) { tracef("showing version via VersionPrinter (cmd=%[1]q)", cmd.Name) VersionPrinter(cmd) } // DefaultPrintVersion is the default implementation of VersionPrinter. func DefaultPrintVersion(cmd *Command) { _, _ = fmt.Fprintf(cmd.Root().Writer, "%v version %v\n", cmd.Name, cmd.Version) } func handleTemplateError(err error) { if err != nil { tracef("error encountered during template parse: %[1]v", err) // If the writer is closed, t.Execute will fail, and there's nothing // we can do to recover. if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { _, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) } return } } // DefaultPrintHelpCustom is the default implementation of HelpPrinterCustom. // // The customFuncs map will be combined with a default template.FuncMap to // allow using arbitrary functions in template rendering. func DefaultPrintHelpCustom(out io.Writer, templ string, data any, customFuncs map[string]any) { const maxLineLength = 10000 tracef("building default funcMap") funcMap := template.FuncMap{ "join": strings.Join, "subtract": subtract, "indent": indent, "nindent": nindent, "trim": strings.TrimSpace, "wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) }, "offset": offset, "offsetCommands": offsetCommands, } if wa, ok := customFuncs["wrapAt"]; ok { if wrapAtFunc, ok := wa.(func() int); ok { wrapAt := wrapAtFunc() customFuncs["wrap"] = func(input string, offset int) string { return wrap(input, offset, wrapAt) } } } for key, value := range customFuncs { funcMap[key] = value } w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) if _, err := t.New("helpNameTemplate").Parse(helpNameTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("argsTemplate").Parse(argsTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("usageTemplate").Parse(usageTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("descriptionTemplate").Parse(descriptionTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("visibleCommandTemplate").Parse(visibleCommandTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("copyrightTemplate").Parse(copyrightTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("versionTemplate").Parse(versionTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("visibleFlagCategoryTemplate").Parse(visibleFlagCategoryTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("visibleFlagTemplate").Parse(visibleFlagTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("visiblePersistentFlagTemplate").Parse(visiblePersistentFlagTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("visibleGlobalFlagCategoryTemplate").Parse(strings.ReplaceAll(visibleFlagCategoryTemplate, "OPTIONS", "GLOBAL OPTIONS")); err != nil { handleTemplateError(err) } if _, err := t.New("authorsTemplate").Parse(authorsTemplate); err != nil { handleTemplateError(err) } if _, err := t.New("visibleCommandCategoryTemplate").Parse(visibleCommandCategoryTemplate); err != nil { handleTemplateError(err) } tracef("executing template") handleTemplateError(t.Execute(w, data)) _ = w.Flush() } // DefaultPrintHelp is the default implementation of HelpPrinter. func DefaultPrintHelp(out io.Writer, templ string, data any) { HelpPrinterCustom(out, templ, data, nil) } func checkVersion(cmd *Command) bool { found := false for _, name := range VersionFlag.Names() { if cmd.Bool(name) { found = true } } return found } func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) { if (c.parent == nil && !c.EnableShellCompletion) || (c.parent != nil && !c.Root().shellCompletion) { return false, arguments } pos := len(arguments) - 1 lastArg := arguments[pos] if lastArg != completionFlag { return false, arguments } for _, arg := range arguments { // If arguments include "--", shell completion is disabled // because after "--" only positional arguments are accepted. // https://unix.stackexchange.com/a/11382 if arg == "--" { return false, arguments[:pos] } } return true, arguments[:pos] } func checkCompletions(ctx context.Context, cmd *Command) bool { tracef("checking completions on command %[1]q", cmd.Name) if !cmd.Root().shellCompletion { tracef("completion not enabled skipping %[1]q", cmd.Name) return false } if argsArguments := cmd.Args(); argsArguments.Present() { name := argsArguments.First() if cmd := cmd.Command(name); cmd != nil { // let the command handle the completion return false } } tracef("no subcommand found for completion %[1]q", cmd.Name) if cmd.ShellComplete != nil { tracef("running shell completion func for command %[1]q", cmd.Name) cmd.ShellComplete(ctx, cmd) } return true } func subtract(a, b int) int { return a - b } func indent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) return pad + strings.ReplaceAll(v, "\n", "\n"+pad) } func nindent(spaces int, v string) string { return "\n" + indent(spaces, v) } func wrap(input string, offset int, wrapAt int) string { var ss []string lines := strings.Split(input, "\n") padding := strings.Repeat(" ", offset) for i, line := range lines { if line == "" { ss = append(ss, line) } else { wrapped := wrapLine(line, offset, wrapAt, padding) if i == 0 { ss = append(ss, wrapped) } else { ss = append(ss, padding+wrapped) } } } return strings.Join(ss, "\n") } func wrapLine(input string, offset int, wrapAt int, padding string) string { if wrapAt <= offset || len(input) <= wrapAt-offset { return input } lineWidth := wrapAt - offset words := strings.Fields(input) if len(words) == 0 { return input } wrapped := words[0] spaceLeft := lineWidth - len(wrapped) for _, word := range words[1:] { if len(word)+1 > spaceLeft { wrapped += "\n" + padding + word spaceLeft = lineWidth - len(word) } else { wrapped += " " + word spaceLeft -= 1 + len(word) } } return wrapped } func offset(input string, fixed int) int { return len(input) + fixed } // this function tries to find the max width of the names column // so say we have the following rows for help // // foo1, foo2, foo3 some string here // bar1, b2 some other string here // // We want to offset the 2nd row usage by some amount so that everything // is aligned // // foo1, foo2, foo3 some string here // bar1, b2 some other string here // // to find that offset we find the length of all the rows and use the max // to calculate the offset func offsetCommands(cmds []*Command, fixed int) int { max := 0 for _, cmd := range cmds { s := strings.Join(cmd.Names(), ", ") if len(s) > max { max = len(s) } } return max + fixed } ================================================ FILE: help_test.go ================================================ package cli import ( "bytes" "context" "errors" "fmt" "io" "os" "runtime" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_ShowRootCommandHelp_NoAuthor(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{Writer: output} _ = ShowRootCommandHelp(cmd) if bytes.Contains(output.Bytes(), []byte("AUTHOR(S):")) { t.Errorf("expected\n%snot to include %s", output.String(), "AUTHOR(S):") } } func Test_ShowRootCommandHelp_NoVersion(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{Writer: output} cmd.Version = "" _ = ShowRootCommandHelp(cmd) if bytes.Contains(output.Bytes(), []byte("VERSION:")) { t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:") } } func Test_ShowRootCommandHelp_HideVersion(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{Writer: output} cmd.HideVersion = true _ = ShowRootCommandHelp(cmd) if bytes.Contains(output.Bytes(), []byte("VERSION:")) { t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:") } } func Test_ShowRootCommandHelp_MultiLineDescription(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{Writer: output} cmd.HideVersion = true cmd.Description = "multi\n line" _ = ShowRootCommandHelp(cmd) 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 TestShowCommandHelpAndExit(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{ Commands: []*Command{ { Name: "ok", Writer: output, }, }, Writer: output, } ShowCommandHelpAndExit(context.Background(), cmd, "ok", 42) require.Equal(t, 42, lastExitCode) } func TestShowRootCommandHelpAndExit(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{Writer: output} ShowRootCommandHelpAndExit(cmd, 42) require.Equal(t, 42, lastExitCode) } func TestShowSubcommandHelpAndExit(t *testing.T) { output := new(bytes.Buffer) ok := &Command{ Name: "ok", Writer: output, } cmd := &Command{ Commands: []*Command{ok}, Writer: output, } _ = cmd ShowSubcommandHelpAndExit(ok, 42) require.Equal(t, 42, lastExitCode) } func Test_HelpFlag_RequiredFlagsNoDefault(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{ Flags: []Flag{ &Int64Flag{Name: "foo", Aliases: []string{"f"}, Required: true}, }, Arguments: AnyArguments, Writer: output, } _ = cmd.Run(buildTestContext(t), []string{"test", "-h"}) expected := `NAME: test - A new cli application USAGE: test [global options] [arguments...] GLOBAL OPTIONS: --foo int, -f int --help, -h show help ` assert.Contains(t, output.String(), expected, "expected output to include usage text") } func Test_HelpCommand_RequiredFlagsNoDefault(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{ Flags: []Flag{ &Int64Flag{Name: "foo", Aliases: []string{"f"}, Required: true}, }, Arguments: AnyArguments, Writer: output, } _ = cmd.Run(buildTestContext(t), []string{"test", "help"}) expected := `NAME: test - A new cli application USAGE: test [global options] [arguments...] GLOBAL OPTIONS: --foo int, -f int --help, -h show help ` assert.Contains(t, output.String(), expected, "expected output to include usage text") } func Test_Help_Custom_Flags(t *testing.T) { oldFlag := HelpFlag defer func() { HelpFlag = oldFlag }() HelpFlag = &BoolFlag{ Name: "help", Aliases: []string{"x"}, Usage: "show help", } out := &bytes.Buffer{} cmd := &Command{ Flags: []Flag{ &BoolFlag{Name: "foo", Aliases: []string{"h"}}, }, Action: func(_ context.Context, cmd *Command) error { assert.True(t, cmd.Bool("h"), "custom help flag not set") return nil }, Writer: out, } _ = cmd.Run(buildTestContext(t), []string{"test", "-h"}) require.Len(t, out.String(), 0) } func Test_Help_Nil_Flags(t *testing.T) { oldFlag := HelpFlag defer func() { HelpFlag = oldFlag }() HelpFlag = nil cmd := &Command{ Action: func(_ context.Context, cmd *Command) error { return nil }, } out := new(bytes.Buffer) cmd.Writer = out _ = cmd.Run(buildTestContext(t), []string{"test"}) require.Len(t, out.String(), 0) } func Test_Version_Custom_Flags(t *testing.T) { oldFlag := VersionFlag defer func() { VersionFlag = oldFlag }() VersionFlag = &BoolFlag{ Name: "version", Aliases: []string{"V"}, Usage: "show version", } out := &bytes.Buffer{} cmd := &Command{ Flags: []Flag{ &BoolFlag{Name: "foo", Aliases: []string{"v"}}, }, Action: func(_ context.Context, cmd *Command) error { assert.True(t, cmd.Bool("v"), "custom version flag not set") return nil }, Writer: out, } _ = cmd.Run(buildTestContext(t), []string{"test", "-v"}) require.Len(t, out.String(), 0) } func Test_helpCommand_Action_ErrorIfNoTopic(t *testing.T) { cmd := &Command{} _ = cmd.Run(context.Background(), []string{"foo", "bar"}) err := helpCommandAction(context.Background(), cmd) require.Error(t, err, "expected error from helpCommandAction()") exitErr, ok := err.(*exitError) require.True(t, ok, "expected *exitError from helpCommandAction()") require.Contains(t, exitErr.Error(), "No help topic for", "expected an unknown help topic error") require.Equal(t, 3, exitErr.exitCode, "expected exit value = 3") } func Test_helpCommand_InHelpOutput(t *testing.T) { cmd := &Command{} output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"test", "--help"}) s := output.String() require.NotContains(t, s, "\nCOMMANDS:\nGLOBAL OPTIONS:\n", "empty COMMANDS section detected") require.Contains(t, s, "--help, -h", "missing \"--help, --h\"") } func TestHelpCommand_FullName(t *testing.T) { testCases := []struct { name string args []string contains string skip bool }{ { name: "app help's FullName", args: []string{"app", "help", "help"}, contains: "app help -", }, { name: "app help's FullName via flag", args: []string{"app", "-h", "help"}, contains: "app help -", }, { name: "cmd help's FullName", args: []string{"app", "cmd", "help", "help"}, contains: "app cmd help -", skip: true, // FIXME: App Command collapse }, { name: "cmd help's FullName via flag", args: []string{"app", "cmd", "-h", "help"}, contains: "app cmd help -", skip: true, // FIXME: App Command collapse }, { name: "subcmd help's FullName", args: []string{"app", "cmd", "subcmd", "help", "help"}, contains: "app cmd subcmd help -", }, { name: "subcmd help's FullName via flag", args: []string{"app", "cmd", "subcmd", "-h", "help"}, contains: "app cmd subcmd help -", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { out := &bytes.Buffer{} if tc.skip { t.SkipNow() } cmd := &Command{ Name: "app", Commands: []*Command{ { Name: "cmd", Commands: []*Command{ { Name: "subcmd", }, }, }, }, Writer: out, ErrWriter: out, } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), tc.args)) r.Contains(out.String(), tc.contains) }) } } func Test_helpCommand_HideHelpCommand(t *testing.T) { buf := &bytes.Buffer{} cmd := &Command{ Name: "app", Writer: buf, } err := cmd.Run(buildTestContext(t), []string{"app", "help", "help"}) assert.NoError(t, err) got := buf.String() notWant := "COMMANDS:" assert.NotContains(t, got, notWant) } func Test_helpCommand_HideHelpFlag(t *testing.T) { cmd := buildMinimalTestCommand() cmd.HideHelp = true assert.Error(t, cmd.Run(buildTestContext(t), []string{"app", "help", "-h"}), "Expected flag error - Got nil") } func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { cmd := &Command{} _ = cmd.Run(context.Background(), []string{"foo", "bar"}) err := helpCommandAction(context.Background(), cmd) require.Error(t, err, "expected error from helpCommandAction(), but got nil") exitErr, ok := err.(*exitError) require.True(t, ok, "expected *exitError from helpCommandAction(), but instead got: %v", err.Error()) require.Contains(t, exitErr.Error(), "No help topic for", "expected an unknown help topic error") require.Equal(t, 3, exitErr.exitCode, "unexpected exit value") } func TestShowRootCommandHelp_CommandAliases(t *testing.T) { out := &bytes.Buffer{} cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Aliases: []string{"fr", "frob"}, Action: func(context.Context, *Command) error { return nil }, }, }, Writer: out, } _ = cmd.Run(buildTestContext(t), []string{"foo", "--help"}) require.Contains(t, out.String(), "frobbly, fr, frob") } func TestShowCommandHelp_AppendHelp(t *testing.T) { testCases := []struct { name string hideHelp bool hideHelpCommand bool args []string verify func(*testing.T, string) }{ { name: "with HideHelp", hideHelp: true, args: []string{"app", "help"}, verify: func(t *testing.T, outString string) { r := require.New(t) r.NotContains(outString, "help, h Shows a list of commands or help for one command") r.NotContains(outString, "--help, -h show help") }, }, { name: "with HideHelpCommand", hideHelpCommand: true, args: []string{"app", "--help"}, verify: func(t *testing.T, outString string) { r := require.New(t) r.NotContains(outString, "help, h Shows a list of commands or help for one command") r.Contains(outString, "--help, -h show help") }, }, { name: "with Subcommand", args: []string{"app", "cmd", "help"}, verify: func(t *testing.T, outString string) { r := require.New(t) r.Contains(outString, "--help, -h show help") }, }, { name: "without Subcommand", args: []string{"app", "help"}, verify: func(t *testing.T, outString string) { r := require.New(t) r.Contains(outString, "help, h Shows a list of commands or help for one command") r.Contains(outString, "--help, -h show help") }, }, } for _, tc := range testCases { out := &bytes.Buffer{} t.Run(tc.name, func(t *testing.T) { cmd := &Command{ Name: "app", HideHelp: tc.hideHelp, HideHelpCommand: tc.hideHelpCommand, Commands: []*Command{ { Name: "cmd", HideHelp: tc.hideHelp, HideHelpCommand: tc.hideHelpCommand, Commands: []*Command{{Name: "subcmd"}}, }, }, Writer: out, ErrWriter: out, } _ = cmd.Run(buildTestContext(t), tc.args) tc.verify(t, out.String()) }) } } func TestShowCommandHelp_HelpPrinter(t *testing.T) { /*doublecho := func(text string) string { return text + " " + text }*/ tests := []struct { name string template string printer HelpPrinterFunc command string wantTemplate string wantOutput string }{ { name: "no-command", template: "", printer: func(w io.Writer, _ string, _ interface{}) { fmt.Fprint(w, "yo") }, command: "", wantTemplate: RootCommandHelpTemplate, wantOutput: "yo", }, /*{ name: "standard-command", template: "", printer: func(w io.Writer, templ string, data interface{}) { fmt.Fprint(w, "yo") }, command: "my-command", wantTemplate: CommandHelpTemplate, wantOutput: "yo", }, { name: "custom-template-command", template: "{{doublecho .Name}}", printer: func(w io.Writer, templ string, data interface{}) { // Pass a custom function to ensure it gets used fm := map[string]interface{}{"doublecho": doublecho} HelpPrinterCustom(w, templ, data, fm) }, command: "my-command", wantTemplate: "{{doublecho .Name}}", wantOutput: "my-command my-command", },*/ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) HelpPrinter = func(w io.Writer, templ string, data interface{}) { assert.Equal(t, tt.wantTemplate, templ, "template mismatch") tt.printer(w, templ, data) } var buf bytes.Buffer cmd := &Command{ Name: "my-app", Writer: &buf, Commands: []*Command{ { Name: "my-command", CustomHelpTemplate: tt.template, }, }, } err := cmd.Run(buildTestContext(t), []string{"my-app", "help", tt.command}) require.NoError(t, err) got := buf.String() assert.Equal(t, tt.wantOutput, got) }) } } func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { doublecho := func(text string) string { return text + " " + text } testCases := []struct { name string template string printer HelpPrinterCustomFunc arguments []string wantTemplate string wantOutput string }{ { name: "no command", printer: func(w io.Writer, _ string, _ any, _ map[string]any) { fmt.Fprint(w, "yo") }, arguments: []string{"my-app", "help"}, wantTemplate: RootCommandHelpTemplate, wantOutput: "yo", }, { name: "standard command", printer: func(w io.Writer, _ string, _ any, _ map[string]any) { fmt.Fprint(w, "yo") }, arguments: []string{"my-app", "help", "my-command"}, wantTemplate: SubcommandHelpTemplate, wantOutput: "yo", }, { name: "custom template command", template: "{{doublecho .Name}}", printer: func(w io.Writer, templ string, data any, _ map[string]any) { // Pass a custom function to ensure it gets used fm := map[string]any{"doublecho": doublecho} DefaultPrintHelpCustom(w, templ, data, fm) }, arguments: []string{"my-app", "help", "my-command"}, wantTemplate: "{{doublecho .Name}}", wantOutput: "my-command my-command", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r := require.New(t) defer func(old HelpPrinterCustomFunc) { HelpPrinterCustom = old }(HelpPrinterCustom) HelpPrinterCustom = func(w io.Writer, tmpl string, data any, fm map[string]any) { r.Nil(fm) r.Equal(tc.wantTemplate, tmpl) tc.printer(w, tmpl, data, fm) } out := &bytes.Buffer{} cmd := &Command{ Name: "my-app", Writer: out, Commands: []*Command{ { Name: "my-command", CustomHelpTemplate: tc.template, }, }, } t.Logf("cmd.Run(ctx, %+[1]v)", tc.arguments) r.NoError(cmd.Run(buildTestContext(t), tc.arguments)) r.Equal(tc.wantOutput, out.String()) }) } } func TestShowCommandHelp_CommandAliases(t *testing.T) { out := &bytes.Buffer{} cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Aliases: []string{"fr", "frob", "bork"}, Action: func(context.Context, *Command) error { return nil }, }, }, Writer: out, } _ = cmd.Run(buildTestContext(t), []string{"foo", "help", "fr"}) require.Contains(t, out.String(), "frobbly") } func TestShowSubcommandHelp_CommandAliases(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Aliases: []string{"fr", "frob", "bork"}, Action: func(context.Context, *Command) error { return nil }, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "help"}) assert.Contains(t, output.String(), "frobbly, fr, frob, bork", "expected output to include all command aliases") } func TestShowCommandHelp_Customtemplate(t *testing.T) { cmd := &Command{ Name: "foo", Commands: []*Command{ { Name: "frobbly", Action: func(context.Context, *Command) error { return nil }, CustomHelpTemplate: `NAME: {{.FullName}} - {{.Usage}} USAGE: {{.FullName}} [FLAGS] TARGET [TARGET ...] FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: 1. Frobbly runs with this param locally. $ {{.FullName}} wobbly `, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "help", "frobbly"}) assert.NotContains(t, output.String(), "2. Frobbly runs without this param locally.", "expected output to exclude \"2. Frobbly runs without this param locally.\";") assert.Contains(t, output.String(), "1. Frobbly runs with this param locally.", "expected output to include \"1. Frobbly runs with this param locally.\"") assert.Contains(t, output.String(), "$ foo frobbly wobbly", "expected output to include \"$ foo frobbly wobbly\"") } func TestShowSubcommandHelp_CommandUsageText(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", UsageText: "this is usage text", }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "frobbly", "--help"}) assert.Contains(t, output.String(), "this is usage text", "expected output to include usage text") } func TestShowSubcommandHelp_MultiLine_CommandUsageText(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", UsageText: `This is a multi line UsageText`, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "frobbly", "--help"}) expected := `USAGE: This is a multi line UsageText ` assert.Contains(t, output.String(), expected, "expected output to include usage text") } func TestShowSubcommandHelp_GlobalOptions(t *testing.T) { cmd := &Command{ Flags: []Flag{ &StringFlag{ Name: "foo", }, }, Commands: []*Command{ { Name: "frobbly", Flags: []Flag{ &StringFlag{ Name: "bar", Local: true, }, }, Action: func(context.Context, *Command) error { return nil }, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "frobbly", "--help"}) expected := `NAME: foo frobbly USAGE: foo frobbly [options] OPTIONS: --bar string --help, -h show help GLOBAL OPTIONS: --foo string ` assert.Contains(t, output.String(), expected, "expected output to include global options") } func TestShowSubcommandHelp_GlobalOptions_HideHelpCommand(t *testing.T) { cmd := &Command{ Flags: []Flag{ &StringFlag{ Name: "foo", }, }, Commands: []*Command{ { Name: "frobbly", HideHelpCommand: true, Flags: []Flag{ &StringFlag{ Name: "bar", Local: true, }, }, Action: func(context.Context, *Command) error { return nil }, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "frobbly", "--help"}) expected := `NAME: foo frobbly USAGE: foo frobbly [options] OPTIONS: --bar string --help, -h show help GLOBAL OPTIONS: --foo string ` assert.Contains(t, output.String(), expected, "expected output to include global options") } func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Commands: []*Command{ { Name: "bobbly", UsageText: "this is usage text", }, }, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "frobbly", "bobbly", "--help"}) assert.Contains(t, output.String(), "this is usage text", "expected output to include usage text") } func TestShowSubcommandHelp_MultiLine_SubcommandUsageText(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Commands: []*Command{ { Name: "bobbly", UsageText: `This is a multi line UsageText`, }, }, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo", "frobbly", "bobbly", "--help"}) expected := `USAGE: This is a multi line UsageText ` assert.Contains(t, output.String(), expected, "expected output to include usage text") } func TestShowRootCommandHelp_HiddenCommand(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Action: func(context.Context, *Command) error { return nil }, }, { Name: "secretfrob", Hidden: true, Action: func(context.Context, *Command) error { return nil }, }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"app", "--help"}) assert.NotContains(t, output.String(), "secretfrob", "expected output to exclude \"secretfrob\"") assert.Contains(t, output.String(), "frobbly", "expected output to include \"frobbly\"") } func TestShowRootCommandHelp_HelpPrinter(t *testing.T) { doublecho := func(text string) string { return text + " " + text } tests := []struct { name string template string printer HelpPrinterFunc wantTemplate string wantOutput string }{ { name: "standard-command", template: "", printer: func(w io.Writer, _ string, _ interface{}) { fmt.Fprint(w, "yo") }, wantTemplate: RootCommandHelpTemplate, wantOutput: "yo", }, { name: "custom-template-command", template: "{{doublecho .Name}}", printer: func(w io.Writer, templ string, data interface{}) { // Pass a custom function to ensure it gets used fm := map[string]any{"doublecho": doublecho} DefaultPrintHelpCustom(w, templ, data, fm) }, wantTemplate: "{{doublecho .Name}}", wantOutput: "my-app my-app", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) HelpPrinter = func(w io.Writer, templ string, data interface{}) { assert.Equal(t, tt.wantTemplate, templ, "unexpected template") tt.printer(w, templ, data) } var buf bytes.Buffer cmd := &Command{ Name: "my-app", Writer: &buf, CustomRootCommandHelpTemplate: tt.template, } err := cmd.Run(buildTestContext(t), []string{"my-app", "help"}) require.NoError(t, err) assert.Equal(t, tt.wantOutput, buf.String()) }) } } func TestShowRootCommandHelp_HelpPrinterCustom(t *testing.T) { doublecho := func(text string) string { return text + " " + text } tests := []struct { name string template string printer HelpPrinterCustomFunc wantTemplate string wantOutput string }{ { name: "standard-command", template: "", printer: func(w io.Writer, _ string, _ interface{}, _ map[string]interface{}) { fmt.Fprint(w, "yo") }, wantTemplate: RootCommandHelpTemplate, wantOutput: "yo", }, { name: "custom-template-command", template: "{{doublecho .Name}}", printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}) { // Pass a custom function to ensure it gets used fm := map[string]any{"doublecho": doublecho} DefaultPrintHelpCustom(w, templ, data, fm) }, wantTemplate: "{{doublecho .Name}}", wantOutput: "my-app my-app", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer func(old HelpPrinterCustomFunc) { HelpPrinterCustom = old }(HelpPrinterCustom) HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { assert.Nil(t, fm, "unexpected function map passed") assert.Equal(t, tt.wantTemplate, templ, "unexpected template") tt.printer(w, templ, data, fm) } var buf bytes.Buffer cmd := &Command{ Name: "my-app", Writer: &buf, CustomRootCommandHelpTemplate: tt.template, } err := cmd.Run(buildTestContext(t), []string{"my-app", "help"}) require.NoError(t, err) assert.Equal(t, tt.wantOutput, buf.String()) }) } } func TestShowRootCommandHelp_CustomAppTemplate(t *testing.T) { cmd := &Command{ Commands: []*Command{ { Name: "frobbly", Action: func(context.Context, *Command) error { return nil }, }, { Name: "secretfrob", Hidden: true, Action: func(context.Context, *Command) error { return nil }, }, }, ExtraInfo: func() map[string]string { platform := fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH) goruntime := fmt.Sprintf("Version: %s | CPUs: %d", runtime.Version(), runtime.NumCPU()) return map[string]string{ "PLATFORM": platform, "RUNTIME": goruntime, } }, CustomRootCommandHelpTemplate: `NAME: {{.Name}} - {{.Usage}} USAGE: {{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...] COMMANDS: {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} {{end}}{{if .VisibleFlags}} GLOBAL FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} VERSION: 2.0.0 {{"\n"}}{{range $key, $value := ExtraInfo}} {{$key}}: {{$value}} {{end}}`, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"app", "--help"}) assert.NotContains(t, output.String(), "secretfrob", "expected output to exclude \"secretfrob\"") assert.Contains(t, output.String(), "frobbly", "expected output to include \"frobbly\"") if !strings.Contains(output.String(), "PLATFORM:") || !strings.Contains(output.String(), "OS:") || !strings.Contains(output.String(), "Arch:") { t.Errorf("expected output to include \"PLATFORM:, OS: and Arch:\"; got: %q", output.String()) } if !strings.Contains(output.String(), "RUNTIME:") || !strings.Contains(output.String(), "Version:") || !strings.Contains(output.String(), "CPUs:") { t.Errorf("expected output to include \"RUNTIME:, Version: and CPUs:\"; got: %q", output.String()) } if !strings.Contains(output.String(), "VERSION:") || !strings.Contains(output.String(), "2.0.0") { t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String()) } } func TestShowRootCommandHelp_UsageText(t *testing.T) { cmd := &Command{ UsageText: "This is a single line of UsageText", Commands: []*Command{ { Name: "frobbly", }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo"}) assert.Contains(t, output.String(), "This is a single line of UsageText", "expected output to include usage text") } func TestShowRootCommandHelp_MultiLine_UsageText(t *testing.T) { cmd := &Command{ UsageText: `This is a multi line App UsageText`, Commands: []*Command{ { Name: "frobbly", }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo"}) expected := `USAGE: This is a multi line App UsageText ` assert.Contains(t, output.String(), expected, "expected output to include usage text") } func TestShowRootCommandHelp_CommandMultiLine_UsageText(t *testing.T) { cmd := &Command{ UsageText: `This is a multi line App UsageText`, Commands: []*Command{ { Name: "frobbly", Aliases: []string{"frb1", "frbb2", "frl2"}, Usage: "this is a long help output for the run command, long usage \noutput, long usage output, long usage output, long usage output\noutput, long usage output, long usage output", }, { Name: "grobbly", Aliases: []string{"grb1", "grbb2"}, Usage: "this is another long help output for the run command, long usage \noutput, long usage output", }, }, } output := &bytes.Buffer{} cmd.Writer = output _ = cmd.Run(buildTestContext(t), []string{"foo"}) expected := "COMMANDS:\n" + " frobbly, frb1, frbb2, frl2 this is a long help output for the run command, long usage \n" + " output, long usage output, long usage output, long usage output\n" + " output, long usage output, long usage output\n" + " grobbly, grb1, grbb2 this is another long help output for the run command, long usage \n" + " output, long usage output" assert.Contains(t, output.String(), expected, "expected output to include usage text") } func TestHideHelpCommand(t *testing.T) { cmd := &Command{ HideHelpCommand: true, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"foo", "help"}) require.ErrorContains(t, err, "No help topic for 'help'") err = cmd.Run(buildTestContext(t), []string{"foo", "--help"}) assert.NoError(t, err) } func TestHideHelpCommand_False(t *testing.T) { cmd := &Command{ HideHelpCommand: false, Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"foo", "help"}) assert.NoError(t, err) err = cmd.Run(buildTestContext(t), []string{"foo", "--help"}) assert.NoError(t, err) } func TestHideHelpCommand_WithHideHelp(t *testing.T) { cmd := &Command{ HideHelp: true, // effective (hides both command and flag) HideHelpCommand: true, // ignored Writer: io.Discard, } err := cmd.Run(buildTestContext(t), []string{"foo", "help"}) require.ErrorContains(t, err, "No help topic for 'help'") err = cmd.Run(buildTestContext(t), []string{"foo", "--help"}) require.ErrorContains(t, err, providedButNotDefinedErrMsg) } func TestHideHelpCommand_WithSubcommands(t *testing.T) { cmd := &Command{ HideHelpCommand: true, Commands: []*Command{ { Name: "nully", Commands: []*Command{ { Name: "nully2", }, }, }, }, } r := require.New(t) r.ErrorContains(cmd.Run(buildTestContext(t), []string{"cli.test", "help"}), "No help topic for 'help'") r.NoError(cmd.Run(buildTestContext(t), []string{"cli.test", "--help"})) } func TestDefaultCompleteWithFlags(t *testing.T) { origArgv := os.Args t.Cleanup(func() { os.Args = origArgv }) for _, tc := range []struct { name string cmd *Command argv []string env map[string]string expected string }{ { name: "empty", cmd: &Command{}, argv: []string{"prog", "cmd"}, env: map[string]string{"SHELL": "bash"}, expected: "", }, { name: "typical-flag-suggestion", cmd: &Command{ Flags: []Flag{ &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, Commands: []*Command{ {Name: "putz"}, }, }, }, argv: []string{"cmd", "--e", completionFlag}, env: map[string]string{"SHELL": "bash"}, expected: "--excitement\n", }, { name: "typical-flag-suggestion-hidden-bool", cmd: &Command{ Flags: []Flag{ &BoolFlag{Name: "excitement", Hidden: true}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, Commands: []*Command{ {Name: "putz"}, }, }, }, argv: []string{"cmd", "--e", completionFlag}, env: map[string]string{"SHELL": "bash"}, expected: "", }, { name: "flag-suggestion-end-args", cmd: &Command{ Flags: []Flag{ &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, Commands: []*Command{ {Name: "putz"}, }, }, }, argv: []string{"cmd", "--e", "--", completionFlag}, env: map[string]string{"SHELL": "bash"}, expected: "", }, { name: "typical-command-suggestion", cmd: &Command{ Name: "putz", Commands: []*Command{ {Name: "futz"}, }, Flags: []Flag{ &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, }, }, argv: []string{"cmd", completionFlag}, env: map[string]string{"SHELL": "bash"}, expected: "futz\n", }, { name: "autocomplete-with-spaces", cmd: &Command{ Name: "putz", Commands: []*Command{ {Name: "help"}, }, Flags: []Flag{ &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, }, }, argv: []string{"cmd", "--url", "http://localhost:8000", "h", completionFlag}, env: map[string]string{"SHELL": "bash"}, expected: "help\n", }, { name: "zsh-autocomplete-with-flag-descriptions", cmd: &Command{ Name: "putz", Flags: []Flag{ &BoolFlag{Name: "excitement", Usage: "an exciting flag"}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, }, }, argv: []string{"cmd", "putz", "-e", completionFlag}, env: map[string]string{"SHELL": "zsh"}, expected: "--excitement:an exciting flag\n", }, { name: "zsh-autocomplete-with-empty-flag-descriptions", cmd: &Command{ Name: "putz", Flags: []Flag{ &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, parent: &Command{ Name: "cmd", Flags: []Flag{ &BoolFlag{Name: "happiness"}, &Int64Flag{Name: "everybody-jump-on"}, }, }, }, argv: []string{"cmd", "putz", "-e", completionFlag}, env: map[string]string{"SHELL": "zsh"}, expected: "--excitement\n", }, } { t.Run(tc.name, func(ct *testing.T) { writer := &bytes.Buffer{} rootCmd := tc.cmd.Root() rootCmd.Writer = writer os.Args = tc.argv for k, v := range tc.env { ct.Setenv(k, v) } tc.cmd.parsedArgs = &stringSliceArgs{ tc.argv[1:], } f := DefaultCompleteWithFlags f(buildTestContext(ct), tc.cmd) written := writer.String() assert.Equal(ct, tc.expected, written, "written help does not match") }) } } func TestMutuallyExclusiveFlags(t *testing.T) { writer := &bytes.Buffer{} cmd := &Command{ Name: "cmd", Writer: writer, MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { Flags: [][]Flag{ { &StringFlag{ Name: "s1", }, }, }, }, }, } _ = ShowRootCommandHelp(cmd) assert.Contains(t, writer.String(), "--s1", "written help does not include mutex flag") } func TestWrap(t *testing.T) { emptywrap := wrap("", 4, 16) assert.Empty(t, emptywrap, "Wrapping empty line should return empty line") } func TestWrappedHelp(t *testing.T) { // Reset HelpPrinter after this test. defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) output := new(bytes.Buffer) cmd := &Command{ Writer: output, Flags: []Flag{ &BoolFlag{ Name: "foo", Aliases: []string{"h"}, Usage: "here's a really long help text line, let's see where it wraps. blah blah blah and so on.", }, }, Usage: "here's a sample App.Usage string long enough that it should be wrapped in this test", UsageText: "i'm not sure how App.UsageText differs from App.Usage, but this should also be wrapped in this test", // TODO: figure out how to make ArgsUsage appear in the help text, and test that Description: `here's a sample App.Description string long enough that it should be wrapped in this test with a newline and an indented line`, Copyright: `Here's a sample copyright text string long enough that it should be wrapped. Including newlines. And also indented lines. And then another long line. Blah blah blah does anybody ever read these things?`, } HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ "wrapAt": func() int { return 30 }, } HelpPrinterCustom(w, templ, data, funcMap) } _ = ShowRootCommandHelp(cmd) expected := `NAME: - here's a sample App.Usage string long enough that it should be wrapped in this test USAGE: i'm not sure how App.UsageText differs from App.Usage, but this should also be wrapped in this test DESCRIPTION: here's a sample App.Description string long enough that it should be wrapped in this test with a newline and an indented line GLOBAL OPTIONS: --foo, -h here's a really long help text line, let's see where it wraps. blah blah blah and so on. COPYRIGHT: Here's a sample copyright text string long enough that it should be wrapped. Including newlines. And also indented lines. And then another long line. Blah blah blah does anybody ever read these things? ` assert.Equal(t, expected, output.String(), "Unexpected wrapping") } func TestWrappedCommandHelp(t *testing.T) { // Reset HelpPrinter after this test. defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) output := &bytes.Buffer{} cmd := &Command{ Writer: output, ErrWriter: output, Commands: []*Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", UsageText: "this is an even longer way of describing adding a task to the list", Description: "and a description long enough to wrap in this test case", Action: func(context.Context, *Command) error { return nil }, }, }, } cmd.setupDefaults([]string{"cli.test"}) cmd.setupCommandGraph() HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ "wrapAt": func() int { return 30 }, } HelpPrinterCustom(w, templ, data, funcMap) } r := require.New(t) r.NoError(ShowCommandHelp(context.Background(), cmd, "add")) r.Equal(`NAME: cli.test add - add a task to the list USAGE: this is an even longer way of describing adding a task to the list DESCRIPTION: and a description long enough to wrap in this test case OPTIONS: --help, -h show help `, output.String(), ) } func TestWrappedSubcommandHelp(t *testing.T) { // Reset HelpPrinter after this test. defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) output := new(bytes.Buffer) cmd := &Command{ Name: "cli.test", Writer: output, Commands: []*Command{ { Name: "bar", Aliases: []string{"a"}, Usage: "add a task to the list", UsageText: "this is an even longer way of describing adding a task to the list", Description: "and a description long enough to wrap in this test case", Action: func(context.Context, *Command) error { return nil }, Commands: []*Command{ { Name: "grok", Usage: "remove an existing template", UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more", Action: func(context.Context, *Command) error { return nil }, }, }, }, }, } HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ "wrapAt": func() int { return 30 }, } HelpPrinterCustom(w, templ, data, funcMap) } _ = cmd.Run(buildTestContext(t), []string{"foo", "bar", "grok", "--help"}) expected := `NAME: cli.test bar grok - remove an existing template USAGE: longer usage text goes here, la la la, hopefully this is long enough to wrap even more OPTIONS: --help, -h show help ` assert.Equal(t, expected, output.String(), "Unexpected wrapping") } func TestWrappedHelpSubcommand(t *testing.T) { // Reset HelpPrinter after this test. defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) output := &bytes.Buffer{} cmd := &Command{ Name: "cli.test", Writer: output, ErrWriter: output, Commands: []*Command{ { Name: "bar", Aliases: []string{"a"}, Usage: "add a task to the list", UsageText: "this is an even longer way of describing adding a task to the list", Description: "and a description long enough to wrap in this test case", Action: func(context.Context, *Command) error { return nil }, Commands: []*Command{ { Name: "grok", Usage: "remove an existing template", UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more", Action: func(context.Context, *Command) error { return nil }, Flags: []Flag{ &StringFlag{ Name: "test-f", Usage: "my test usage", }, }, }, }, }, }, } HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ "wrapAt": func() int { return 30 }, } HelpPrinterCustom(w, templ, data, funcMap) } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"cli.test", "bar", "help", "grok"})) r.Equal(`NAME: cli.test bar grok - remove an existing template USAGE: longer usage text goes here, la la la, hopefully this is long enough to wrap even more OPTIONS: --test-f string my test usage --help, -h show help `, output.String(), ) } func TestCategorizedHelp(t *testing.T) { // Reset HelpPrinter after this test. defer func(old HelpPrinterFunc) { HelpPrinter = old }(HelpPrinter) output := new(bytes.Buffer) cmd := &Command{ Name: "cli.test", Writer: output, Action: func(context.Context, *Command) error { return nil }, Flags: []Flag{ &StringFlag{ Name: "strd", // no category set }, &Int64Flag{ Name: "intd", Aliases: []string{"altd1", "altd2"}, Category: "cat1", }, &BoolWithInverseFlag{ Name: "bf", // Category: "cat1", }, }, MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { Category: "cat1", Flags: [][]Flag{ { &StringFlag{ Name: "m1", Category: "overridden", }, }, }, }, { Flags: [][]Flag{ { &StringFlag{ Name: "m2", Category: "ignored", }, }, }, }, }, } HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ "wrapAt": func() int { return 30 }, } HelpPrinterCustom(w, templ, data, funcMap) } r := require.New(t) r.NoError(cmd.Run(buildTestContext(t), []string{"cli.test", "help"})) r.Equal(`NAME: cli.test - A new cli application USAGE: cli.test [global options] GLOBAL OPTIONS: --[no-]bf (default: false) --help, -h show help --m2 string --strd string cat1 --intd int, --altd1 int, --altd2 int (default: 0) --m1 string `, output.String()) } func Test_checkShellCompleteFlag(t *testing.T) { t.Parallel() tests := []struct { name string cmd *Command arguments []string wantShellCompletion bool wantArgs []string }{ { name: "disable-shell-completion", arguments: []string{completionFlag}, cmd: &Command{}, wantShellCompletion: false, wantArgs: []string{completionFlag}, }, { name: "child-disable-shell-completion", arguments: []string{completionFlag}, cmd: &Command{ parent: &Command{}, }, wantShellCompletion: false, wantArgs: []string{completionFlag}, }, { name: "last argument isn't --generate-shell-completion", arguments: []string{"foo"}, cmd: &Command{ EnableShellCompletion: true, }, wantShellCompletion: false, wantArgs: []string{"foo"}, }, { name: "arguments include double dash", arguments: []string{"--", "foo", completionFlag}, cmd: &Command{ EnableShellCompletion: true, }, wantShellCompletion: false, wantArgs: []string{"--", "foo"}, }, { name: "shell completion", arguments: []string{"foo", completionFlag}, cmd: &Command{ EnableShellCompletion: true, }, wantShellCompletion: true, wantArgs: []string{"foo"}, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() shellCompletion, args := checkShellCompleteFlag(tt.cmd, tt.arguments) assert.Equal(t, tt.wantShellCompletion, shellCompletion) assert.Equal(t, tt.wantArgs, args) }) } } func TestNIndent(t *testing.T) { t.Parallel() tests := []struct { numSpaces int str string expected string }{ { numSpaces: 0, str: "foo", expected: "\nfoo", }, { numSpaces: 0, str: "foo\n", expected: "\nfoo\n", }, { numSpaces: 2, str: "foo", expected: "\n foo", }, { numSpaces: 3, str: "foo\n", expected: "\n foo\n ", }, } for _, test := range tests { assert.Equal(t, test.expected, nindent(test.numSpaces, test.str)) } } func TestTemplateError(t *testing.T) { oldew := ErrWriter defer func() { ErrWriter = oldew }() var buf bytes.Buffer ErrWriter = &buf err := errors.New("some error") handleTemplateError(err) assert.Equal(t, []byte(nil), buf.Bytes()) t.Setenv("CLI_TEMPLATE_ERROR_DEBUG", "true") handleTemplateError(err) assert.Contains(t, buf.String(), "CLI TEMPLATE ERROR") assert.Contains(t, buf.String(), err.Error()) } func TestCliArgContainsFlag(t *testing.T) { tests := []struct { name string args []string contains bool }{ { name: "", args: []string{}, }, { name: "f", args: []string{}, }, { name: "f", args: []string{"g", "foo", "f"}, }, { name: "f", args: []string{"-f", "foo", "f"}, contains: true, }, { name: "f", args: []string{"g", "-f", "f"}, contains: true, }, { name: "fh", args: []string{"g", "f", "--fh"}, contains: true, }, { name: "fhg", args: []string{"-fhg", "f", "fh"}, }, { name: "fhg", args: []string{"--fhg", "f", "fh"}, contains: true, }, } for _, test := range tests { if test.contains { assert.True(t, cliArgContains(test.name, test.args)) } else { assert.False(t, cliArgContains(test.name, test.args)) } } } func TestCommandHelpSuggest(t *testing.T) { cmd := &Command{ Suggest: true, Commands: []*Command{ { Name: "putz", }, }, } cmd.setupDefaults([]string{"foo"}) err := ShowCommandHelp(context.Background(), cmd, "put") assert.ErrorContains(t, err, "No help topic for 'put'. putz") } func TestWrapLine(t *testing.T) { assert.Equal(t, " ", wrapLine(" ", 0, 3, " ")) } func TestPrintHelpCustomTemplateError(t *testing.T) { tmpls := []*string{ &helpNameTemplate, &argsTemplate, &usageTemplate, &descriptionTemplate, &visibleCommandTemplate, ©rightTemplate, &versionTemplate, &visibleFlagCategoryTemplate, &visibleFlagTemplate, &visiblePersistentFlagTemplate, &visibleFlagCategoryTemplate, &authorsTemplate, &visibleCommandCategoryTemplate, } oldErrWriter := ErrWriter defer func() { ErrWriter = oldErrWriter }() t.Setenv("CLI_TEMPLATE_ERROR_DEBUG", "true") for _, tmpl := range tmpls { oldtmpl := *tmpl // safety mechanism in case something fails defer func(stmpl *string) { *stmpl = oldtmpl }(tmpl) errBuf := &bytes.Buffer{} ErrWriter = errBuf buf := &bytes.Buffer{} *tmpl = "{{junk" DefaultPrintHelpCustom(buf, "", "", nil) assert.Contains(t, errBuf.String(), "CLI TEMPLATE ERROR") // reset template back. *tmpl = oldtmpl } } func TestCustomUsageCommandHelp(t *testing.T) { old := UsageCommandHelp defer func() { UsageCommandHelp = old }() UsageCommandHelp = "Custom usage help command" out := &bytes.Buffer{} cmd := &Command{ Name: "app", Commands: []*Command{ { Name: "cmd", Commands: []*Command{{Name: "subcmd"}}, }, }, Writer: out, ErrWriter: out, } _ = cmd.Run(buildTestContext(t), []string{"app", "help"}) assert.Contains(t, out.String(), UsageCommandHelp) } ================================================ FILE: helpers_test.go ================================================ package cli import ( "os" ) func init() { _ = os.Setenv("CLI_TEMPLATE_REPANIC", "1") } ================================================ FILE: mkdocs-requirements.txt ================================================ mkdocs-git-revision-date-localized-plugin==1.5.1 mkdocs-material==9.7.4 mkdocs==1.6.1 mkdocs-redirects==1.2.2 pygments==2.19.2 ================================================ FILE: mkdocs.yml ================================================ # NOTE: the mkdocs dependencies will need to be installed out of # band until this whole thing gets more automated: # # pip install -r mkdocs-requirements.txt # site_name: urfave/cli site_url: https://cli.urfave.org/ repo_url: https://github.com/urfave/cli edit_uri: edit/main/docs/ nav: - Home: - Welcome: index.md - Contributing: CONTRIBUTING.md - Code of Conduct: CODE_OF_CONDUCT.md - Releasing: RELEASING.md - Security: SECURITY.md - Migrate v2 to v3: migrate-v2-to-v3.md - Migrate v1 to v2: migrate-v1-to-v2.md - v3 Manual: - Getting Started: v3/getting-started.md - Migrating From Older Releases: v3/migrating-from-older-releases.md - Examples: - Greet: v3/examples/greet.md - Flags: - Basics: v3/examples/flags/basics.md - Value Sources: v3/examples/flags/value-sources.md - Short Options: v3/examples/flags/short-options.md - Advanced: v3/examples/flags/advanced.md - Arguments: - Basics: v3/examples/arguments/basics.md - Advanced: v3/examples/arguments/advanced.md - Subcommands: - Basics: v3/examples/subcommands/basics.md - Categories: v3/examples/subcommands/categories.md - Completions: - Shell Completions: v3/examples/completions/shell-completions.md - Customizations: v3/examples/completions/customizations.md - Help Text: - Generated Help Text: v3/examples/help/generated-help-text.md - Suggestions: v3/examples/help/suggestions.md - Error Handling: - Exit Codes: v3/examples/exit-codes.md - Full API Example: v3/examples/full-api-example.md - v2 Manual: - Getting Started: v2/getting-started.md - Migrating to v3: v2/migrating-to-v3.md - Migrating From Older Releases: v2/migrating-from-older-releases.md - Examples: - Greet: v2/examples/greet.md - Arguments: v2/examples/arguments.md - Flags: v2/examples/flags.md - Subcommands: v2/examples/subcommands.md - Subcommands Categories: v2/examples/subcommands-categories.md - Exit Codes: v2/examples/exit-codes.md - Combining Short Options: v2/examples/combining-short-options.md - Bash Completions: v2/examples/bash-completions.md - Generated Help Text: v2/examples/generated-help-text.md - Version Flag: v2/examples/version-flag.md - Timestamp Flag: v2/examples/timestamp-flag.md - Suggestions: v2/examples/suggestions.md - Full API Example: v2/examples/full-api-example.md - v1 Manual: - Getting Started: v1/getting-started.md - Migrating to v2: v1/migrating-to-v2.md - Examples: - Greet: v1/examples/greet.md - Arguments: v1/examples/arguments.md - Flags: v1/examples/flags.md - Subcommands: v1/examples/subcommands.md - Subcommands (Categories): v1/examples/subcommands-categories.md - Exit Codes: v1/examples/exit-codes.md - Combining Short Options: v1/examples/combining-short-options.md - Bash Completions: v1/examples/bash-completions.md - Generated Help Text: v1/examples/generated-help-text.md - Version Flag: v1/examples/version-flag.md theme: name: material palette: - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/brightness-4 name: dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/brightness-7 name: light mode features: - content.code.annotate - navigation.top - navigation.instant - navigation.expand - navigation.sections - navigation.tabs - navigation.tabs.sticky plugins: - git-revision-date-localized - search - redirects: redirect_maps: 'v3/examples/bash-completions.md': 'v3/examples/completions/shell-completions.md' - tags # NOTE: this is the recommended configuration from # https://squidfunk.github.io/mkdocs-material/setup/extensions/#recommended-configuration markdown_extensions: - abbr - admonition - attr_list - def_list - footnotes - meta - md_in_html - toc: permalink: true - pymdownx.arithmatex: generic: true - pymdownx.betterem: smart_enable: all - pymdownx.caret - pymdownx.details - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight - pymdownx.inlinehilite - pymdownx.keys - pymdownx.mark - pymdownx.smartsymbols - pymdownx.superfences - pymdownx.tabbed: alternate_style: true - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde ================================================ FILE: scripts/build.go ================================================ // local build script file, similar to a makefile or collection of bash scripts in other projects package main import ( "bufio" "bytes" "context" "errors" "fmt" "io" "log" "math" "net/http" "net/url" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "github.com/urfave/cli/v3" ) const ( badNewsEmoji = "🚨" goodNewsEmoji = "✨" checksPassedEmoji = "✅" gfmrunVersion = "v1.3.0" v3diffWarning = ` # The unified diff above indicates that the public API surface area # has changed. If you feel that the changes are acceptable for the # v3.x series, please run the following command to promote the # current go docs: # # make v3approve # ` ) func main() { topDir, err := func() (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() if v, err := sh(ctx, "git", "rev-parse", "--show-toplevel"); err == nil { return strings.TrimSpace(v), nil } return os.Getwd() }() if err != nil { log.Fatal(err) } app := &cli.Command{ Name: "builder", Usage: "Do a thing for urfave/cli! (maybe build?)", Commands: []*cli.Command{ { Name: "vet", Action: topRunAction("go", "vet", "./..."), }, { Name: "test", Action: TestActionFunc, }, { Name: "gfmrun", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "walk", Value: false, Usage: "Walk the specified directory and perform validation on all markdown files", }, }, Action: GfmrunActionFunc, }, { Name: "check-binary-size", Action: checkBinarySizeActionFunc, }, { Name: "generate", Action: GenerateActionFunc, }, { Name: "diffcheck", Action: DiffCheckActionFunc, }, { Name: "ensure-goimports", Action: EnsureGoimportsActionFunc, }, { Name: "ensure-gfmrun", Action: EnsureGfmrunActionFunc, }, { Name: "ensure-mkdocs", Action: EnsureMkdocsActionFunc, Flags: []cli.Flag{ &cli.BoolFlag{Name: "upgrade-pip"}, }, }, { Name: "set-mkdocs-remote", Action: SetMkdocsRemoteActionFunc, Flags: []cli.Flag{ &cli.StringFlag{ Name: "github-token", Sources: cli.EnvVars("MKDOCS_REMOTE_GITHUB_TOKEN"), Required: true, }, }, }, { Name: "deploy-mkdocs", Action: topRunAction("mkdocs", "gh-deploy", "--force"), }, { Name: "lint", Action: LintActionFunc, }, { Name: "v3diff", Flags: []cli.Flag{ &cli.BoolFlag{Name: "color", Value: false}, }, Action: V3Diff, }, { Name: "v3approve", Action: topRunAction( "cp", "-v", "godoc-current.txt", filepath.Join("testdata", "godoc-v3.x.txt"), ), }, }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "tags", Usage: "set build tags", }, &cli.StringFlag{ Name: "top-dir", Value: topDir, }, &cli.StringSliceFlag{ Name: "packages", Value: []string{"cli", "scripts"}, }, }, } if err := app.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } func sh(ctx context.Context, exe string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, exe, args...) cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd) outBytes, err := cmd.Output() return string(outBytes), err } func topRunAction(arg string, args ...string) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { if err := os.Chdir(cmd.String("top-dir")); err != nil { return err } return runCmd(ctx, arg, args...) } } func runCmd(ctx context.Context, arg string, args ...string) error { cmd := exec.CommandContext(ctx, arg, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr fmt.Fprintf(os.Stderr, "# ---> %s\n", cmd) return cmd.Run() } func downloadFile(src, dest string, dirPerm, perm os.FileMode) error { req, err := http.NewRequest(http.MethodGet, src, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 300 { return fmt.Errorf("download file from %[2]s into %[3]s: response %[1]v", resp.StatusCode, src, dest) } if err := os.MkdirAll(filepath.Dir(dest), dirPerm); err != nil { return err } out, err := os.Create(dest) if err != nil { return err } if _, err := io.Copy(out, resp.Body); err != nil { return err } if err := out.Close(); err != nil { return err } return os.Chmod(dest, perm) } func VetActionFunc(ctx context.Context, cmd *cli.Command) error { return runCmd(ctx, "go", "vet", cmd.String("top-dir")+"/...") } func TestActionFunc(ctx context.Context, cmd *cli.Command) error { tags := cmd.String("tags") for _, pkg := range cmd.StringSlice("packages") { packageName := "github.com/urfave/cli/v3" if pkg != "cli" { packageName = fmt.Sprintf("github.com/urfave/cli/v3/%s", pkg) } args := []string{"test"} if tags != "" { args = append(args, []string{"-tags", tags}...) } args = append(args, []string{ "-v", "-race", "--coverprofile", pkg + ".coverprofile", "--covermode", "atomic", "--cover", packageName, packageName, }...) if err := runCmd(ctx, "go", args...); err != nil { return err } } return testCleanup(cmd.StringSlice("packages")) } func testCleanup(packages []string) error { out := &bytes.Buffer{} fmt.Fprintf(out, "mode: count\n") for _, pkg := range packages { filename := pkg + ".coverprofile" lineBytes, err := os.ReadFile(filename) if err != nil { return err } lines := strings.Split(string(lineBytes), "\n") fmt.Fprint(out, strings.Join(lines[1:], "\n")) if err := os.Remove(filename); err != nil { return err } } return os.WriteFile("coverage.txt", out.Bytes(), 0o644) } func GfmrunActionFunc(ctx context.Context, cmd *cli.Command) error { docsDir := filepath.Join(cmd.String("top-dir"), "docs") bash, err := exec.LookPath("bash") if err != nil { return err } os.Setenv("SHELL", bash) tmpDir, err := os.MkdirTemp("", "urfave-cli*") if err != nil { return err } wd, err := os.Getwd() if err != nil { return err } if err := os.Chdir(tmpDir); err != nil { return err } fmt.Fprintf(cmd.ErrWriter, "# ---> workspace/TMPDIR is %q\n", tmpDir) if err := runCmd(ctx, "go", "work", "init", docsDir); err != nil { return err } os.Setenv("TMPDIR", tmpDir) if err := os.Chdir(wd); err != nil { return err } dirPath := cmd.Args().Get(0) if dirPath == "" { dirPath = "README.md" } walk := cmd.Bool("walk") sources := []string{} if walk { // Walk the directory and find all markdown files. err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if filepath.Ext(path) != ".md" { return nil } sources = append(sources, path) return nil }) if err != nil { return err } } else { sources = append(sources, dirPath) } var counter int for _, src := range sources { file, err := os.Open(src) if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { if strings.Contains(scanner.Text(), "package main") { counter++ } } err = file.Close() if err != nil { return err } err = scanner.Err() if err != nil { return err } } gfmArgs := []string{ "--count", fmt.Sprint(counter), } for _, src := range sources { gfmArgs = append(gfmArgs, "--sources", src) } if err := runCmd(ctx, "gfmrun", gfmArgs...); err != nil { return err } return os.RemoveAll(tmpDir) } // checkBinarySizeActionFunc checks the size of an example binary to ensure that we are keeping size down // this was originally inspired by https://github.com/urfave/cli/issues/1055, and followed up on as a part // of https://github.com/urfave/cli/issues/1057 func checkBinarySizeActionFunc(ctx context.Context, cmd *cli.Command) (err error) { const ( cliSourceFilePath = "./examples/example-cli/example-cli.go" cliBuiltFilePath = "./examples/example-cli/built-example" helloSourceFilePath = "./examples/example-hello-world/example-hello-world.go" helloBuiltFilePath = "./examples/example-hello-world/built-example" desiredMaxBinarySize = 2.2 desiredMinBinarySize = 1.49 mbStringFormatter = "%.1fMB" ) tags := cmd.String("tags") // get cli example size cliSize, err := getSize(ctx, cliSourceFilePath, cliBuiltFilePath, tags) if err != nil { return err } // get hello world size helloSize, err := getSize(ctx, helloSourceFilePath, helloBuiltFilePath, tags) if err != nil { return err } // The CLI size diff is the number we are interested in. // This tells us how much our CLI package contributes to the binary size. cliSizeDiff := cliSize - helloSize // get human readable size, in MB with one decimal place. // example output is: 35.2MB. (note: this simply an example) // that output is much easier to reason about than the `35223432` // that you would see output without the rounding fileSizeInMB := float64(cliSizeDiff) / float64(1000000) roundedFileSize := math.Round(fileSizeInMB*10) / 10 roundedFileSizeString := fmt.Sprintf(mbStringFormatter, roundedFileSize) // check against bounds isLessThanDesiredMin := roundedFileSize < desiredMinBinarySize isMoreThanDesiredMax := roundedFileSize > desiredMaxBinarySize desiredMinSizeString := fmt.Sprintf(mbStringFormatter, desiredMinBinarySize) desiredMaxSizeString := fmt.Sprintf(mbStringFormatter, desiredMaxBinarySize) // show guidance fmt.Printf("\n%s is the current binary size\n", roundedFileSizeString) // show guidance for min size if isLessThanDesiredMin { fmt.Printf(" %s %s is the target min size\n", goodNewsEmoji, desiredMinSizeString) fmt.Println("") // visual spacing fmt.Println(" The binary is smaller than the target min size, which is great news!") fmt.Println(" That means that your changes are shrinking the binary size.") fmt.Println(" You'll want to go into ./scripts/build.go and decrease") fmt.Println(" the desiredMinBinarySize, and also probably decrease the ") fmt.Println(" desiredMaxBinarySize by the same amount. That will ensure that") fmt.Println(" future PRs will enforce the newly shrunk binary sizes.") fmt.Println("") // visual spacing os.Exit(1) } else { fmt.Printf(" %s %s is the target min size\n", checksPassedEmoji, desiredMinSizeString) } // show guidance for max size if isMoreThanDesiredMax { fmt.Printf(" %s %s is the target max size\n", badNewsEmoji, desiredMaxSizeString) fmt.Println("") // visual spacing fmt.Println(" The binary is larger than the target max size.") fmt.Println(" That means that your changes are increasing the binary size.") fmt.Println(" The first thing you'll want to do is ask your yourself") fmt.Println(" Is this change worth increasing the binary size?") fmt.Println(" Larger binary sizes for this package can dissuade its use.") fmt.Println(" If this change is worth the increase, then we can up the") fmt.Println(" desired max binary size. To do that you'll want to go into") fmt.Println(" ./scripts/build.go and increase the desiredMaxBinarySize,") fmt.Println(" and increase the desiredMinBinarySize by the same amount.") fmt.Println("") // visual spacing os.Exit(1) } else { fmt.Printf(" %s %s is the target max size\n", checksPassedEmoji, desiredMaxSizeString) } return nil } func GenerateActionFunc(ctx context.Context, cmd *cli.Command) error { topDir := cmd.String("top-dir") cliDocs, err := sh(ctx, "go", "doc", "-all", topDir) if err != nil { return err } return os.WriteFile( filepath.Join(topDir, "godoc-current.txt"), []byte(cliDocs), 0o644, ) } func DiffCheckActionFunc(ctx context.Context, cmd *cli.Command) error { if err := os.Chdir(cmd.String("top-dir")); err != nil { return err } if err := runCmd(ctx, "git", "diff", "--exit-code"); err != nil { return err } return runCmd(ctx, "git", "diff", "--cached", "--exit-code") } func EnsureGoimportsActionFunc(ctx context.Context, cmd *cli.Command) error { topDir := cmd.String("top-dir") if err := os.Chdir(topDir); err != nil { return err } if err := runCmd( ctx, "goimports", "-d", filepath.Join(topDir, "scripts/build.go"), ); err == nil { return nil } os.Setenv("GOBIN", filepath.Join(topDir, ".local/bin")) return runCmd(ctx, "go", "install", "golang.org/x/tools/cmd/goimports@latest") } func EnsureGfmrunActionFunc(ctx context.Context, cmd *cli.Command) error { topDir := cmd.String("top-dir") gfmrunExe := filepath.Join(topDir, ".local/bin/gfmrun") if err := os.Chdir(topDir); err != nil { return err } if v, err := sh(ctx, gfmrunExe, "--version"); err == nil && strings.TrimSpace(v) == gfmrunVersion { return nil } gfmrunURL, err := url.Parse( fmt.Sprintf( "https://github.com/urfave/gfmrun/releases/download/%[1]s/gfmrun-%[2]s-%[3]s-%[1]s", gfmrunVersion, runtime.GOOS, runtime.GOARCH, ), ) if err != nil { return err } return downloadFile(gfmrunURL.String(), gfmrunExe, 0o755, 0o755) } func EnsureMkdocsActionFunc(ctx context.Context, cmd *cli.Command) error { if err := os.Chdir(cmd.String("top-dir")); err != nil { return err } if err := runCmd(ctx, "mkdocs", "--version"); err == nil { return nil } if cmd.Bool("upgrade-pip") { if err := runCmd(ctx, "pip", "install", "-U", "pip"); err != nil { return err } } return runCmd(ctx, "pip", "install", "-r", "mkdocs-requirements.txt") } func SetMkdocsRemoteActionFunc(ctx context.Context, cmd *cli.Command) error { ghToken := strings.TrimSpace(cmd.String("github-token")) if ghToken == "" { return errors.New("empty github token") } if err := os.Chdir(cmd.String("top-dir")); err != nil { return err } if err := runCmd(ctx, "git", "remote", "rm", "origin"); err != nil { return err } return runCmd( ctx, "git", "remote", "add", "origin", fmt.Sprintf("https://x-access-token:%[1]s@github.com/urfave/cli.git", ghToken), ) } func LintActionFunc(ctx context.Context, cmd *cli.Command) error { topDir := cmd.String("top-dir") if err := os.Chdir(topDir); err != nil { return err } out, err := sh(ctx, filepath.Join(topDir, ".local/bin/goimports"), "-l", ".") if err != nil { return err } if strings.TrimSpace(out) != "" { fmt.Fprintln(cmd.ErrWriter, "# ---> goimports -l is non-empty:") fmt.Fprintln(cmd.ErrWriter, out) return errors.New("goimports needed") } return nil } func V3Diff(ctx context.Context, cmd *cli.Command) error { if err := os.Chdir(cmd.String("top-dir")); err != nil { return err } err := runCmd( ctx, "diff", "--ignore-all-space", "--minimal", "--color="+func() string { if cmd.Bool("color") { return "always" } return "auto" }(), "--unified", "--label=a/godoc", filepath.Join("testdata", "godoc-v3.x.txt"), "--label=b/godoc", "godoc-current.txt", ) if err != nil { fmt.Printf("# %v ---> Hey! <---\n", badNewsEmoji) fmt.Println(strings.TrimSpace(v3diffWarning)) } return err } func getSize(ctx context.Context, sourcePath, builtPath, tags string) (int64, error) { args := []string{"build"} if tags != "" { args = append(args, []string{"-tags", tags}...) } args = append(args, []string{ "-o", builtPath, "-ldflags", "-s -w", sourcePath, }...) if err := runCmd(ctx, "go", args...); err != nil { fmt.Println("issue getting size for example binary") return 0, err } fileInfo, err := os.Stat(builtPath) if err != nil { fmt.Println("issue getting size for example binary") return 0, err } return fileInfo.Size(), nil } ================================================ FILE: sort.go ================================================ package cli import "unicode" // lexicographicLess compares strings alphabetically considering case. func lexicographicLess(i, j string) bool { iRunes := []rune(i) jRunes := []rune(j) lenShared := len(iRunes) if lenShared > len(jRunes) { lenShared = len(jRunes) } for index := 0; index < lenShared; index++ { ir := iRunes[index] jr := jRunes[index] if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { return lir < ljr } if ir != jr { return ir < jr } } return i < j } ================================================ FILE: sort_test.go ================================================ package cli import ( "testing" "github.com/stretchr/testify/assert" ) var lexicographicLessTests = []struct { i string j string expected bool }{ {"", "a", true}, {"a", "", false}, {"a", "a", false}, {"a", "A", false}, {"A", "a", true}, {"aa", "a", false}, {"a", "aa", true}, {"a", "b", true}, {"a", "B", true}, {"A", "b", true}, {"A", "B", true}, } func TestLexicographicLess(t *testing.T) { for _, test := range lexicographicLessTests { actual := lexicographicLess(test.i, test.j) assert.Equal(t, test.expected, actual) } } ================================================ FILE: staticcheck.conf ================================================ checks=["all"] ================================================ FILE: suggestions.go ================================================ package cli import ( "math" ) const suggestDidYouMeanTemplate = "Did you mean %q?" var ( SuggestFlag SuggestFlagFunc = suggestFlag SuggestCommand SuggestCommandFunc = suggestCommand SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate ) type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string type SuggestCommandFunc func(commands []*Command, provided string) string // jaroDistance is the measure of similarity between two strings. It returns a // value between 0 and 1, where 1 indicates identical strings and 0 indicates // completely different strings. // // Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro.go. func jaroDistance(a, b string) float64 { if len(a) == 0 && len(b) == 0 { return 1 } if len(a) == 0 || len(b) == 0 { return 0 } lenA := float64(len(a)) lenB := float64(len(b)) hashA := make([]bool, len(a)) hashB := make([]bool, len(b)) maxDistance := int(math.Max(0, math.Floor(math.Max(lenA, lenB)/2.0)-1)) var matches float64 for i := 0; i < len(a); i++ { start := int(math.Max(0, float64(i-maxDistance))) end := int(math.Min(lenB-1, float64(i+maxDistance))) for j := start; j <= end; j++ { if hashB[j] { continue } if a[i] == b[j] { hashA[i] = true hashB[j] = true matches++ break } } } if matches == 0 { return 0 } var transpositions float64 var j int for i := 0; i < len(a); i++ { if !hashA[i] { continue } for !hashB[j] { j++ } if a[i] != b[j] { transpositions++ } j++ } transpositions /= 2 return ((matches / lenA) + (matches / lenB) + ((matches - transpositions) / matches)) / 3.0 } // jaroWinkler is more accurate when strings have a common prefix up to a // defined maximum length. // // Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro-winkler.go. func jaroWinkler(a, b string) float64 { const ( boostThreshold = 0.7 prefixSize = 4 ) jaroDist := jaroDistance(a, b) if jaroDist <= boostThreshold { return jaroDist } prefix := int(math.Min(float64(len(a)), math.Min(float64(prefixSize), float64(len(b))))) var prefixMatch float64 for i := 0; i < prefix; i++ { if a[i] == b[i] { prefixMatch++ } else { break } } return jaroDist + 0.1*prefixMatch*(1.0-jaroDist) } func suggestFlag(flags []Flag, provided string, hideHelp bool) string { distance := 0.0 suggestion := "" for _, flag := range flags { flagNames := flag.Names() if !hideHelp && HelpFlag != nil { flagNames = append(flagNames, HelpFlag.Names()...) } for _, name := range flagNames { newDistance := jaroWinkler(name, provided) if newDistance > distance { distance = newDistance suggestion = name } } } if len(suggestion) == 1 { suggestion = "-" + suggestion } else if len(suggestion) > 1 { suggestion = "--" + suggestion } return suggestion } // suggestCommand takes a list of commands and a provided string to suggest a // command name func suggestCommand(commands []*Command, provided string) (suggestion string) { distance := 0.0 for _, command := range commands { for _, name := range append(command.Names(), helpName, helpAlias) { newDistance := jaroWinkler(name, provided) if newDistance > distance { distance = newDistance suggestion = name } } } return suggestion } ================================================ FILE: suggestions_test.go ================================================ package cli import ( "errors" "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestJaroWinkler(t *testing.T) { // Given for _, testCase := range []struct { a, b string expected float64 }{ {"", "", 1}, {"a", "", 0}, {"", "a", 0}, {"a", "a", 1}, {"a", "b", 0}, {"aa", "aa", 1}, {"aa", "bb", 0}, {"aaa", "aaa", 1}, {"aa", "ab", 0.6666666666666666}, {"aa", "ba", 0.6666666666666666}, {"ba", "aa", 0.6666666666666666}, {"ab", "aa", 0.6666666666666666}, } { // When res := jaroWinkler(testCase.a, testCase.b) // Then assert.Equal(t, testCase.expected, res) } } func TestSuggestFlag(t *testing.T) { // Given app := buildExtendedTestCommand() for _, testCase := range []struct { provided, expected string }{ {"", ""}, {"a", "--another-flag"}, {"hlp", "--help"}, {"k", ""}, {"s", "-s"}, } { // When res := suggestFlag(app.Flags, testCase.provided, false) // Then assert.Equal(t, testCase.expected, res) } } func TestSuggestFlagHideHelp(t *testing.T) { // Given app := buildExtendedTestCommand() // When res := suggestFlag(app.Flags, "hlp", true) // Then assert.Equal(t, "--fl", res) } func TestSuggestFlagFromError(t *testing.T) { // Given app := buildExtendedTestCommand() for _, testCase := range []struct { command, provided, expected string }{ {"", "hel", "--help"}, {"", "soccer", "--socket"}, {"config", "anot", "--another-flag"}, } { // When res, _ := app.suggestFlagFromError( errors.New(providedButNotDefinedErrMsg+testCase.provided), testCase.command, ) // Then assert.Equal(t, fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", testCase.expected), res) } } func TestSuggestFlagFromErrorWrongError(t *testing.T) { // Given app := buildExtendedTestCommand() // When _, err := app.suggestFlagFromError(errors.New("invalid"), "") // Then assert.Error(t, err) } func TestSuggestFlagFromErrorWrongCommand(t *testing.T) { // Given app := buildExtendedTestCommand() // When _, err := app.suggestFlagFromError( errors.New(providedButNotDefinedErrMsg+"flag"), "invalid", ) // Then assert.Error(t, err) } func TestSuggestFlagFromErrorNoSuggestion(t *testing.T) { // Given app := buildExtendedTestCommand() // When _, err := app.suggestFlagFromError( errors.New(providedButNotDefinedErrMsg+""), "", ) // Then assert.Error(t, err) } func TestSuggestCommand(t *testing.T) { // Given app := buildExtendedTestCommand() for _, testCase := range []struct { provided, expected string }{ {"", ""}, {"conf", "config"}, {"i", "i"}, {"information", "info"}, {"inf", "info"}, {"con", "config"}, {"not-existing", "info"}, } { // When res := suggestCommand(app.Commands, testCase.provided) // Then assert.Equal(t, testCase.expected, res) } } ================================================ FILE: template.go ================================================ package cli var ( helpNameTemplate = `{{$v := offset .FullName 6}}{{wrap .FullName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}` argsTemplate = `{{if .Arguments}}{{range .Arguments}}{{.Usage}} {{end}}{{end}}` usageTemplate = `{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleFlags}} [options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} {{template "argsTemplate" .}}{{end}}{{end}}{{end}}` descriptionTemplate = `{{wrap .Description 3}}` authorsTemplate = `{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} {{end}}{{$author}}{{end}}` ) var visibleCommandTemplate = `{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}` var visibleCommandCategoryTemplate = `{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{template "visibleCommandTemplate" .}}{{end}}{{end}}` var visibleFlagCategoryTemplate = `{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} {{end}}{{$flglen := len .Flags}}{{range $i, $e := .Flags}}{{if eq (subtract $flglen $i) 1}}{{$e}} {{else}}{{$e}} {{end}}{{end}}{{end}}` var visibleFlagTemplate = `{{range $i, $e := .VisibleFlags}} {{wrap $e.String 6}}{{end}}` var visiblePersistentFlagTemplate = `{{range $i, $e := .VisiblePersistentFlags}} {{wrap $e.String 6}}{{end}}` var versionTemplate = `{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}` var copyrightTemplate = `{{wrap .Copyright 3}}` // RootCommandHelpTemplate is the text template for the Default help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var RootCommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}} {{if .VisibleFlags}}[global options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}} {{- if len .Authors}} AUTHOR{{template "authorsTemplate" .}}{{end}}{{if .VisibleCommands}} COMMANDS:{{template "visibleCommandCategoryTemplate" .}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .Copyright}} COPYRIGHT: {{template "copyrightTemplate" .}}{{end}} ` // CommandHelpTemplate is the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var CommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{template "usageTemplate" .}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}} GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}} ` // SubcommandHelpTemplate is the text template for the subcommand help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}} COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} ` var FishCompletionTemplate = `# {{ .Command.Name }} fish shell completion function __fish_{{ .Command.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' for i in (commandline -opc) if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} return 1 end end return 0 end {{ range $v := .Completions }}{{ $v }} {{ end }}` ================================================ FILE: testdata/empty.yml ================================================ # empty file ================================================ FILE: testdata/expected-doc-full.man ================================================ .nh .TH greet 8 .SH NAME .PP greet - Some app .SH SYNOPSIS .PP greet .PP .RS .nf [--another-flag|-b] [--flag|--fl|-f]=[value] [--socket|-s]=[value] .fi .RE .SH DESCRIPTION .PP Description of the application. .PP \fBUsage\fP: .PP .RS .nf app [first_arg] [second_arg] .fi .RE .SH GLOBAL OPTIONS .PP \fB--another-flag, -b\fP: another usage text .PP \fB--flag, --fl, -f\fP="": .PP \fB--socket, -s\fP="": some 'usage' text (default: value) .SH COMMANDS .SH config, c .PP another usage test .PP \fB--another-flag, -b\fP: another usage text .PP \fB--flag, --fl, -f\fP="": .SS sub-config, s, ss .PP another usage test .PP \fB--sub-command-flag, -s\fP: some usage text .PP \fB--sub-flag, --sub-fl, -s\fP="": .SH info, i, in .PP retrieve generic information .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 ================================================ FILE: testdata/expected-doc-full.md ================================================ # NAME greet - Some app # SYNOPSIS greet ``` [--another-flag|-b] [--flag|--fl|-f]=[value] [--socket|-s]=[value] ``` # DESCRIPTION Description of the application. **Usage**: ``` app [first_arg] [second_arg] ``` # 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 ================================================ FILE: testdata/expected-doc-no-authors.md ================================================ # NAME greet - Some app # SYNOPSIS greet ``` [--another-flag|-b] [--flag|--fl|-f]=[value] [--socket|-s]=[value] ``` # DESCRIPTION Description of the application. **Usage**: ``` app [first_arg] [second_arg] ``` # 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 ================================================ FILE: testdata/expected-doc-no-commands.md ================================================ # NAME greet - Some app # SYNOPSIS greet ``` [--another-flag|-b] [--flag|--fl|-f]=[value] [--socket|-s]=[value] ``` # DESCRIPTION Description of the application. **Usage**: ``` app [first_arg] [second_arg] ``` # GLOBAL OPTIONS **--another-flag, -b**: another usage text **--flag, --fl, -f**="": **--socket, -s**="": some 'usage' text (default: value) ================================================ FILE: testdata/expected-doc-no-flags.md ================================================ # NAME greet - Some app # SYNOPSIS greet # DESCRIPTION Description of the application. **Usage**: ``` app [first_arg] [second_arg] ``` # 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 ================================================ FILE: testdata/expected-doc-no-usagetext.md ================================================ # 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 ================================================ FILE: testdata/expected-fish-full.fish ================================================ # greet fish shell completion function __fish_greet_no_subcommand --description 'Test if there has been any subcommand yet' for i in (commandline -opc) if contains -- $i config c info i in some-command hidden-command usage u return 1 end end return 0 end complete -c greet -n '__fish_greet_no_subcommand' -l socket -s s -r -d 'some \'usage\' text' complete -c greet -n '__fish_greet_no_subcommand' -f -l flag -s fl -s f -r complete -c greet -n '__fish_greet_no_subcommand' -f -l another-flag -s b -d 'another usage text' complete -c greet -n '__fish_greet_no_subcommand' -l logfile -r complete -c greet -n '__fish_greet_no_subcommand' -l foofile -r complete -x -c greet -n '__fish_greet_no_subcommand' -a 'config' -d 'another usage test' complete -c greet -n '__fish_seen_subcommand_from config c' -l flag -s fl -s f -r complete -c greet -n '__fish_seen_subcommand_from config c' -f -l another-flag -s b -d 'another usage text' complete -c greet -n '__fish_seen_subcommand_from config c' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss help h' -a 'sub-config' -d 'another usage test' complete -c greet -n '__fish_seen_subcommand_from config c; and __fish_seen_subcommand_from sub-config s ss' -f -l sub-flag -s sub-fl -s s -r complete -c greet -n '__fish_seen_subcommand_from config c; and __fish_seen_subcommand_from sub-config s ss' -f -l sub-command-flag -s s -d 'some usage text' complete -c greet -n '__fish_seen_subcommand_from config c; and __fish_seen_subcommand_from sub-config s ss' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from config c; and __fish_seen_subcommand_from sub-config s ss; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_seen_subcommand_from config c; and not __fish_seen_subcommand_from sub-config s ss help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'info' -d 'retrieve generic information' complete -c greet -n '__fish_seen_subcommand_from info i in' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from info i in; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'some-command' complete -c greet -n '__fish_seen_subcommand_from some-command' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from some-command; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -c greet -n '__fish_seen_subcommand_from hidden-command' -f -l completable complete -c greet -n '__fish_seen_subcommand_from hidden-command' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from hidden-command; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_greet_no_subcommand' -a 'usage' -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 usage u' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su help h' -a 'sub-usage' -d 'standard usage text' complete -c greet -n '__fish_seen_subcommand_from usage u; and __fish_seen_subcommand_from sub-usage su' -f -l sub-command-flag -s s -d 'some usage text' complete -c greet -n '__fish_seen_subcommand_from usage u; and __fish_seen_subcommand_from sub-usage su' -f -l help -s h -d 'show help' complete -x -c greet -n '__fish_seen_subcommand_from usage u; and __fish_seen_subcommand_from sub-usage su; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command' complete -x -c greet -n '__fish_seen_subcommand_from usage u; and not __fish_seen_subcommand_from sub-usage su help h' -a 'help' -d 'Shows a list of commands or help for one command' ================================================ FILE: testdata/expected-tabular-markdown-custom-app-path.md ================================================ ## CLI interface - greet Description of the application. Some app. > app [first_arg] [second_arg] Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...] ``` Global flags: | Name | Description | Default value | Environment variables | |-----------------------------|--------------------|:-------------:|:-----------------------:| | `--socket="…"` (`-s`) | some 'usage' text | `value` | *none* | | `--flag="…"` (`--fl`, `-f`) | | | *none* | | `--another-flag` (`-b`) | another usage text | `false` | `EXAMPLE_VARIABLE_NAME` | ### `config` command (aliases: `c`) another usage test. Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] config [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|--------------------|:-------------:|:---------------------:| | `--flag="…"` (`--fl`, `-f`) | | | *none* | | `--another-flag` (`-b`) | another usage text | `false` | *none* | ### `config sub-config` subcommand (aliases: `s`, `ss`) another usage test. Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] config sub-config [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-------------------------------------|-----------------|:-------------:|:---------------------:| | `--sub-flag="…"` (`--sub-fl`, `-s`) | | | *none* | | `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | ### `info` command (aliases: `i`, `in`) retrieve generic information. Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] info [ARGUMENTS...] ``` ### `some-command` command Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] some-command [ARGUMENTS...] ``` ### `usage` command (aliases: `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 Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] usage [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|--------------------|:-------------:|:---------------------:| | `--flag="…"` (`--fl`, `-f`) | | | *none* | | `--another-flag` (`-b`) | another usage text | `false` | *none* | ### `usage sub-usage` subcommand (aliases: `su`) standard usage text. > Single line of UsageText Usage: ```bash $ /usr/local/bin [GLOBAL FLAGS] usage sub-usage [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|-----------------|:-------------:|:---------------------:| | `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | ================================================ FILE: testdata/expected-tabular-markdown-full.md ================================================ ## CLI interface - greet Description of the application. Some app. > app [first_arg] [second_arg] Usage: ```bash $ app [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...] ``` Global flags: | Name | Description | Default value | Environment variables | |-----------------------------|--------------------|:-------------:|:-----------------------:| | `--socket="…"` (`-s`) | some 'usage' text | `value` | *none* | | `--flag="…"` (`--fl`, `-f`) | | | *none* | | `--another-flag` (`-b`) | another usage text | `false` | `EXAMPLE_VARIABLE_NAME` | ### `config` command (aliases: `c`) another usage test. Usage: ```bash $ app [GLOBAL FLAGS] config [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|--------------------|:-------------:|:---------------------:| | `--flag="…"` (`--fl`, `-f`) | | | *none* | | `--another-flag` (`-b`) | another usage text | `false` | *none* | ### `config sub-config` subcommand (aliases: `s`, `ss`) another usage test. Usage: ```bash $ app [GLOBAL FLAGS] config sub-config [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-------------------------------------|-----------------|:-------------:|:---------------------:| | `--sub-flag="…"` (`--sub-fl`, `-s`) | | | *none* | | `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | ### `info` command (aliases: `i`, `in`) retrieve generic information. Usage: ```bash $ app [GLOBAL FLAGS] info [ARGUMENTS...] ``` ### `some-command` command Usage: ```bash $ app [GLOBAL FLAGS] some-command [ARGUMENTS...] ``` ### `usage` command (aliases: `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 Usage: ```bash $ app [GLOBAL FLAGS] usage [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|--------------------|:-------------:|:---------------------:| | `--flag="…"` (`--fl`, `-f`) | | | *none* | | `--another-flag` (`-b`) | another usage text | `false` | *none* | ### `usage sub-usage` subcommand (aliases: `su`) standard usage text. > Single line of UsageText Usage: ```bash $ app [GLOBAL FLAGS] usage sub-usage [COMMAND FLAGS] [ARGUMENTS...] ``` The following flags are supported: | Name | Description | Default value | Environment variables | |-----------------------------|-----------------|:-------------:|:---------------------:| | `--sub-command-flag` (`-s`) | some usage text | `false` | *none* | ================================================ FILE: testdata/godoc-v3.x.txt ================================================ package cli // import "github.com/urfave/cli/v3" Package cli provides a minimal framework for creating and organizing command line Go applications. cli is designed to be easy to understand and write, the most simple cli application can be written as follows: func main() { (&cli.Command{}).Run(context.Background(), os.Args) } Of course this application does not do much, so let's make this an actual application: func main() { cmd := &cli.Command{ Name: "greet", Usage: "say a greeting", Action: func(c *cli.Context) error { fmt.Println("Greetings") return nil }, } cmd.Run(context.Background(), os.Args) } VARIABLES var ( NewFloatSlice = NewSliceBase[float64, NoConfig, floatValue[float64]] NewFloat32Slice = NewSliceBase[float32, NoConfig, floatValue[float32]] NewFloat64Slice = NewSliceBase[float64, NoConfig, floatValue[float64]] ) var ( NewIntSlice = NewSliceBase[int, IntegerConfig, intValue[int]] NewInt8Slice = NewSliceBase[int8, IntegerConfig, intValue[int8]] NewInt16Slice = NewSliceBase[int16, IntegerConfig, intValue[int16]] NewInt32Slice = NewSliceBase[int32, IntegerConfig, intValue[int32]] NewInt64Slice = NewSliceBase[int64, IntegerConfig, intValue[int64]] ) var ( NewUintSlice = NewSliceBase[uint, IntegerConfig, uintValue[uint]] NewUint8Slice = NewSliceBase[uint8, IntegerConfig, uintValue[uint8]] NewUint16Slice = NewSliceBase[uint16, IntegerConfig, uintValue[uint16]] NewUint32Slice = NewSliceBase[uint32, IntegerConfig, uintValue[uint32]] NewUint64Slice = NewSliceBase[uint64, IntegerConfig, uintValue[uint64]] ) var ( SuggestFlag SuggestFlagFunc = suggestFlag SuggestCommand SuggestCommandFunc = suggestCommand SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate ) var AnyArguments = []Argument{ &StringArgs{ Max: -1, }, } AnyArguments to differentiate between no arguments(nil) vs aleast one var ArgsUsageCommandHelp = "[command]" ArgsUsageCommandHelp is a short description of the arguments of the help command var CommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{template "usageTemplate" .}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}} GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}} ` CommandHelpTemplate is the text template for the command help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var DefaultAppComplete = DefaultRootCommandComplete DefaultAppComplete is a backward-compatible name for DefaultRootCommandComplete. var DefaultInverseBoolPrefix = "no-" var ErrWriter io.Writer = os.Stderr ErrWriter is used to write errors to the user. This can be anything implementing the io.Writer interface and defaults to os.Stderr. var FishCompletionTemplate = `# {{ .Command.Name }} fish shell completion function __fish_{{ .Command.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' for i in (commandline -opc) if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} return 1 end end return 0 end {{ range $v := .Completions }}{{ $v }} {{ end }}` var NewStringMap = NewMapBase[string, StringConfig, stringValue] var NewStringSlice = NewSliceBase[string, StringConfig, stringValue] var OsExiter = os.Exit OsExiter is the function used when the app exits. If not set defaults to os.Exit. var RootCommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}} {{if .VisibleFlags}}[global options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} VERSION: {{.Version}}{{end}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}} {{- if len .Authors}} AUTHOR{{template "authorsTemplate" .}}{{end}}{{if .VisibleCommands}} COMMANDS:{{template "visibleCommandCategoryTemplate" .}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .Copyright}} COPYRIGHT: {{template "copyrightTemplate" .}}{{end}} ` RootCommandHelpTemplate is the text template for the Default help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var ShowAppHelp = ShowRootCommandHelp ShowAppHelp is a backward-compatible name for ShowRootCommandHelp. var ShowAppHelpAndExit = ShowRootCommandHelpAndExit ShowAppHelpAndExit is a backward-compatible name for ShowRootCommandHelp. var ShowCommandHelp = DefaultShowCommandHelp ShowCommandHelp prints help for the given command var ShowRootCommandHelp = DefaultShowRootCommandHelp ShowRootCommandHelp is an action that displays help for the root command. var ShowSubcommandHelp = DefaultShowSubcommandHelp ShowSubcommandHelp prints help for the given subcommand var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}} COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategories}} OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} ` SubcommandHelpTemplate is the text template for the subcommand help topic. cli.go uses text/template to render templates. You can render custom help text by setting this variable. var UsageCommandHelp = "Shows a list of commands or help for one command" UsageCommandHelp is the text to override the USAGE section of the help command var VersionPrinter = DefaultPrintVersion VersionPrinter prints the version for the root Command. FUNCTIONS func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) func DefaultPrintHelp(out io.Writer, templ string, data any) DefaultPrintHelp is the default implementation of HelpPrinter. func DefaultPrintHelpCustom(out io.Writer, templ string, data any, customFuncs map[string]any) DefaultPrintHelpCustom is the default implementation of HelpPrinterCustom. The customFuncs map will be combined with a default template.FuncMap to allow using arbitrary functions in template rendering. func DefaultPrintVersion(cmd *Command) DefaultPrintVersion is the default implementation of VersionPrinter. func DefaultRootCommandComplete(ctx context.Context, cmd *Command) DefaultRootCommandComplete prints the list of subcommands as the default completion method. func DefaultShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error DefaultShowCommandHelp is the default implementation of ShowCommandHelp. func DefaultShowRootCommandHelp(cmd *Command) error DefaultShowRootCommandHelp is the default implementation of ShowRootCommandHelp. func DefaultShowSubcommandHelp(cmd *Command) error DefaultShowSubcommandHelp is the default implementation of ShowSubcommandHelp. func FlagNames(name string, aliases []string) []string func HandleExitCoder(err error) HandleExitCoder handles errors implementing ExitCoder by printing their message and calling OsExiter with the given exit code. If the given error instead implements MultiError, each error will be checked for the ExitCoder interface, and OsExiter will be called with the last exit code found, or exit code 1 if no ExitCoder is found. This function is the default error-handling behavior for a Command. func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) ShowCommandHelpAndExit exits with code after showing help via ShowCommandHelp. func ShowRootCommandHelpAndExit(cmd *Command, exitCode int) ShowRootCommandHelpAndExit prints the list of subcommands and exits with exit code. func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) ShowSubcommandHelpAndExit prints help for the given subcommand via ShowSubcommandHelp and exits with exit code. func ShowVersion(cmd *Command) ShowVersion prints the version number of the root Command. TYPES type ActionFunc func(context.Context, *Command) error ActionFunc is the action to execute when no subcommands are specified type ActionableFlag interface { RunAction(context.Context, *Command) error } ActionableFlag is an interface that wraps Flag interface and RunAction operation. type AfterFunc func(context.Context, *Command) error AfterFunc is an action that executes after any subcommands are run and have finished. The AfterFunc is run even if Action() panics. type Args interface { // Get returns the nth argument, or else a blank string Get(n int) string // First returns the first argument, or else a blank string First() string // Tail returns the rest of the arguments (not the first one) // or else an empty string slice Tail() []string // Len returns the length of the wrapped slice Len() int // Present checks if there are any arguments present Present() bool // Slice returns a copy of the internal slice Slice() []string } type Argument interface { // which this argument can be accessed using the given name HasName(string) bool // Parse the given args and return unparsed args and/or error Parse([]string) ([]string, error) // The usage template for this argument to use in help Usage() string // The Value of this Arg Get() any } Argument captures a positional argument that can be parsed type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // the name of this argument Value T `json:"value"` // the default value of this argument Destination *T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show Config C `json:"config"` // config for this argument similar to Flag Config // Has unexported fields. } func (a *ArgumentBase[T, C, VC]) Get() any func (a *ArgumentBase[T, C, VC]) HasName(s string) bool func (a *ArgumentBase[T, C, VC]) Parse(s []string) ([]string, error) func (a *ArgumentBase[T, C, VC]) Usage() string type ArgumentsBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // the name of this argument Value T `json:"value"` // the default value of this argument Destination *[]T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show Min int `json:"minTimes"` // the min num of occurrences of this argument Max int `json:"maxTimes"` // the max num of occurrences of this argument, set to -1 for unlimited Config C `json:"config"` // config for this argument similar to Flag Config // Has unexported fields. } ArgumentsBase is a base type for slice arguments func (a *ArgumentsBase[T, C, VC]) Get() any func (a *ArgumentsBase[T, C, VC]) HasName(s string) bool func (a *ArgumentsBase[T, C, VC]) Parse(s []string) ([]string, error) func (a *ArgumentsBase[T, C, VC]) Usage() string type BeforeFunc func(context.Context, *Command) (context.Context, error) BeforeFunc is an action that executes prior to any subcommands being run once the context is ready. If a non-nil error is returned, no subcommands are run. type BoolConfig struct { Count *int } BoolConfig defines the configuration for bool flags type BoolFlag = FlagBase[bool, BoolConfig, boolValue] type BoolWithInverseFlag struct { Name string `json:"name"` // name of the flag Category string `json:"category"` // category of the flag, if any DefaultText string `json:"defaultText"` // default text of the flag for usage purposes HideDefault bool `json:"hideDefault"` // whether to hide the default value in output Usage string `json:"usage"` // usage string for help output Sources ValueSourceChain `json:"-"` // sources to load flag value from Required bool `json:"required"` // whether the flag is required or not Hidden bool `json:"hidden"` // whether to hide the flag in help output Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well Value bool `json:"defaultValue"` // default value for this flag if not set by from any source Destination *bool `json:"-"` // destination pointer for value when set Aliases []string `json:"aliases"` // Aliases that are allowed for this flag TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes Action func(context.Context, *Command, bool) error `json:"-"` // Action callback to be called when flag is set OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line Validator func(bool) error `json:"-"` // custom function to validate this flag value ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not Config BoolConfig `json:"config"` // Additional/Custom configuration associated with this flag type InversePrefix string `json:"invPrefix"` // The prefix used to indicate a negative value. Default: `env` becomes `no-env` // Has unexported fields. } func (bif *BoolWithInverseFlag) Count() int Count returns the number of times this flag has been invoked func (bif *BoolWithInverseFlag) Get() any func (bif *BoolWithInverseFlag) GetCategory() string GetCategory returns the category of the flag func (bif *BoolWithInverseFlag) GetDefaultText() string GetDefaultText returns the default text for this flag func (bif *BoolWithInverseFlag) GetEnvVars() []string GetEnvVars returns the env vars for this flag func (bif *BoolWithInverseFlag) GetUsage() string GetUsage returns the usage string for the flag func (bif *BoolWithInverseFlag) GetValue() string GetValue returns the flags value as string representation and an empty string if the flag takes no value at all. func (bif *BoolWithInverseFlag) IsBoolFlag() bool IsBoolFlag returns whether the flag doesnt need to accept args func (bif *BoolWithInverseFlag) IsDefaultVisible() bool IsDefaultVisible returns true if the flag is not hidden, otherwise false func (bif *BoolWithInverseFlag) IsLocal() bool func (bif *BoolWithInverseFlag) IsRequired() bool func (bif *BoolWithInverseFlag) IsSet() bool func (bif *BoolWithInverseFlag) IsVisible() bool func (bif *BoolWithInverseFlag) Names() []string func (bif *BoolWithInverseFlag) PostParse() error func (bif *BoolWithInverseFlag) PreParse() error func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) func (bif *BoolWithInverseFlag) String() string String implements the standard Stringer interface. Example for BoolFlag{Name: "env"} --[no-]env (default: false) func (bif *BoolWithInverseFlag) TakesValue() bool func (bif *BoolWithInverseFlag) TypeName() string TypeName is used for stringify/docs. For bool its a no-op type CategorizableFlag interface { // Returns the category of the flag GetCategory() string // Sets the category of the flag SetCategory(string) } CategorizableFlag is an interface that allows us to potentially use a flag in a categorized representation. type Command struct { // The name of the command Name string `json:"name"` // A list of aliases for the command Aliases []string `json:"aliases"` // A short description of the usage of this command Usage string `json:"usage"` // Text to override the USAGE section of help UsageText string `json:"usageText"` // A short description of the arguments of this command ArgsUsage string `json:"argsUsage"` // Version of the command Version string `json:"version"` // Longer explanation of how the command works Description string `json:"description"` // DefaultCommand is the (optional) name of a command // to run if no command names are passed as CLI arguments. DefaultCommand string `json:"defaultCommand"` // The category the command is part of Category string `json:"category"` // List of child commands Commands []*Command `json:"commands"` // List of flags to parse Flags []Flag `json:"flags"` // Boolean to hide built-in help command and help flag HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands EnableShellCompletion bool `json:"-"` // Shell Completion generation command name ShellCompletionCommandName string `json:"-"` // The function to call when checking for shell command completions ShellComplete ShellCompleteFunc `json:"-"` // The function to configure a shell completion command ConfigureShellCompletionCommand ConfigureShellCompletionCommand `json:"-"` // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc `json:"-"` // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc `json:"-"` // The function to call when this command is invoked Action ActionFunc `json:"-"` // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc `json:"-"` // Execute this function if a usage error occurs. OnUsageError OnUsageErrorFunc `json:"-"` // Execute this function when an invalid flag is accessed from the context InvalidFlagAccessHandler InvalidFlagAccessFunc `json:"-"` // Boolean to hide this command from help or completion Hidden bool `json:"hidden"` // List of all authors who contributed (string or fmt.Stringer) // TODO: ~string | fmt.Stringer when interface unions are available Authors []any `json:"authors"` // Copyright of the binary if any Copyright string `json:"copyright"` // Reader reader to write input to (useful for tests) Reader io.Reader `json:"-"` // Writer writer to write output to Writer io.Writer `json:"-"` // ErrWriter writes error output ErrWriter io.Writer `json:"-"` // ExitErrHandler processes any error encountered while running a Command before it is // returned to the caller. If no function is provided, HandleExitCoder is used as the // default behavior. ExitErrHandler ExitErrHandlerFunc `json:"-"` // Other custom info Metadata map[string]interface{} `json:"metadata"` // Carries a function which returns app specific info. ExtraInfo func() map[string]string `json:"-"` // CustomRootCommandHelpTemplate the text template for app help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomRootCommandHelpTemplate string `json:"-"` // SliceFlagSeparator is used to customize the separator for SliceFlag, the default is "," SliceFlagSeparator string `json:"sliceFlagSeparator"` // DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"` // MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "=" MapFlagKeyValueSeparator string `json:"mapFlagKeyValueSeparator"` // Boolean to enable short-option handling so user can combine several // single-character bool arguments into one // i.e. foobar -o -v -> foobar -ov UseShortOptionHandling bool `json:"useShortOptionHandling"` // Enable suggestions for commands and flags Suggest bool `json:"suggest"` // Allows global flags set by libraries which use flag.XXXVar(...) directly // to be parsed through this library AllowExtFlags bool `json:"allowExtFlags"` // Treat all flags as normal arguments if true SkipFlagParsing bool `json:"skipFlagParsing"` // CustomHelpTemplate the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string `json:"-"` // Use longest prefix match for commands PrefixMatchCommands bool `json:"prefixMatchCommands"` // Custom suggest command for matching SuggestCommandFunc SuggestCommandFunc `json:"-"` // Flag exclusion group MutuallyExclusiveFlags []MutuallyExclusiveFlags `json:"mutuallyExclusiveFlags"` // Arguments to parse for this command Arguments []Argument `json:"arguments"` // Whether to read arguments from stdin // applicable to root command only ReadArgsFromStdin bool `json:"readArgsFromStdin"` // StopOnNthArg provides v2-like behavior for specific commands by stopping // flag parsing after N positional arguments are encountered. When set to N, // all remaining arguments after the Nth positional argument will be treated // as arguments, not flags. // // A value of 0 means all arguments are treated as positional (no flag parsing). // A nil value means normal v3 flag parsing behavior (flags can appear anywhere). StopOnNthArg *int `json:"stopOnNthArg"` // Has unexported fields. } Command contains everything needed to run an application that accepts a string slice of arguments such as os.Args. A given Command may contain Flags and sub-commands in Commands. func (cmd *Command) Args() Args Args returns the command line arguments associated with the command. func (cmd *Command) Bool(name string) bool func (cmd *Command) Command(name string) *Command func (cmd *Command) Count(name string) int Count returns the num of occurrences of this flag func (cmd *Command) Duration(name string) time.Duration func (cmd *Command) FlagNames() []string FlagNames returns a slice of flag names used by the this command and all of its parent commands. func (cmd *Command) Float(name string) float64 Float looks up the value of a local FloatFlag, returns 0 if not found func (cmd *Command) Float32(name string) float32 Float32 looks up the value of a local Float32Flag, returns 0 if not found func (c *Command) Float32Arg(name string) float32 func (c *Command) Float32Args(name string) []float32 func (cmd *Command) Float32Slice(name string) []float32 Float32Slice looks up the value of a local Float32Slice, returns nil if not found func (cmd *Command) Float64(name string) float64 Float64 looks up the value of a local Float64Flag, returns 0 if not found func (c *Command) Float64Arg(name string) float64 func (c *Command) Float64Args(name string) []float64 func (cmd *Command) Float64Slice(name string) []float64 Float64Slice looks up the value of a local Float64SliceFlag, returns nil if not found func (c *Command) FloatArg(name string) float64 func (c *Command) FloatArgs(name string) []float64 func (cmd *Command) FloatSlice(name string) []float64 FloatSlice looks up the value of a local FloatSliceFlag, returns nil if not found func (cmd *Command) FullName() string FullName returns the full name of the command. For commands with parents this ensures that the parent commands are part of the command path. func (cmd *Command) Generic(name string) Value Generic looks up the value of a local GenericFlag, returns nil if not found func (cmd *Command) HasName(name string) bool HasName returns true if Command.Name matches given name func (cmd *Command) Int(name string) int Int looks up the value of a local Int64Flag, returns 0 if not found func (cmd *Command) Int16(name string) int16 Int16 looks up the value of a local Int16Flag, returns 0 if not found func (c *Command) Int16Arg(name string) int16 func (c *Command) Int16Args(name string) []int16 func (cmd *Command) Int16Slice(name string) []int16 Int16Slice looks up the value of a local Int16SliceFlag, returns nil if not found func (cmd *Command) Int32(name string) int32 Int32 looks up the value of a local Int32Flag, returns 0 if not found func (c *Command) Int32Arg(name string) int32 func (c *Command) Int32Args(name string) []int32 func (cmd *Command) Int32Slice(name string) []int32 Int32Slice looks up the value of a local Int32SliceFlag, returns nil if not found func (cmd *Command) Int64(name string) int64 Int64 looks up the value of a local Int64Flag, returns 0 if not found func (c *Command) Int64Arg(name string) int64 func (c *Command) Int64Args(name string) []int64 func (cmd *Command) Int64Slice(name string) []int64 Int64Slice looks up the value of a local Int64SliceFlag, returns nil if not found func (cmd *Command) Int8(name string) int8 Int8 looks up the value of a local Int8Flag, returns 0 if not found func (c *Command) Int8Arg(name string) int8 func (c *Command) Int8Args(name string) []int8 func (cmd *Command) Int8Slice(name string) []int8 Int8Slice looks up the value of a local Int8SliceFlag, returns nil if not found func (c *Command) IntArg(name string) int func (c *Command) IntArgs(name string) []int func (cmd *Command) IntSlice(name string) []int IntSlice looks up the value of a local IntSliceFlag, returns nil if not found func (cmd *Command) IsSet(name string) bool IsSet determines if the flag was actually set func (cmd *Command) Lineage() []*Command Lineage returns *this* command and all of its ancestor commands in order from child to parent func (cmd *Command) LocalFlagNames() []string LocalFlagNames returns a slice of flag names used in this command. func (cmd *Command) NArg() int NArg returns the number of the command line arguments. func (cmd *Command) Names() []string Names returns the names including short names and aliases. func (cmd *Command) NumFlags() int NumFlags returns the number of flags set func (cmd *Command) Root() *Command Root returns the Command at the root of the graph func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) Run is the entry point to the command graph. The positional arguments are parsed according to the Flag and Command definitions and the matching Action functions are run. func (cmd *Command) Set(name, value string) error Set sets a context flag to a value. func (cmd *Command) String(name string) string func (c *Command) StringArg(name string) string func (c *Command) StringArgs(name string) []string func (cmd *Command) StringMap(name string) map[string]string StringMap looks up the value of a local StringMapFlag, returns nil if not found func (cmd *Command) StringSlice(name string) []string StringSlice looks up the value of a local StringSliceFlag, returns nil if not found func (cmd *Command) Timestamp(name string) time.Time Timestamp gets the timestamp from a flag name func (c *Command) TimestampArg(name string) time.Time func (c *Command) TimestampArgs(name string) []time.Time func (cmd *Command) ToFishCompletion() (string, error) ToFishCompletion creates a fish completion string for the `*Command` The function errors if either parsing or writing of the string fails. func (cmd *Command) Uint(name string) uint Uint looks up the value of a local Uint64Flag, returns 0 if not found func (cmd *Command) Uint16(name string) uint16 Uint16 looks up the value of a local Uint16Flag, returns 0 if not found func (c *Command) Uint16Arg(name string) uint16 func (c *Command) Uint16Args(name string) []uint16 func (cmd *Command) Uint16Slice(name string) []uint16 Uint16Slice looks up the value of a local Uint16SliceFlag, returns nil if not found func (cmd *Command) Uint32(name string) uint32 Uint32 looks up the value of a local Uint32Flag, returns 0 if not found func (c *Command) Uint32Arg(name string) uint32 func (c *Command) Uint32Args(name string) []uint32 func (cmd *Command) Uint32Slice(name string) []uint32 Uint32Slice looks up the value of a local Uint32SliceFlag, returns nil if not found func (cmd *Command) Uint64(name string) uint64 Uint64 looks up the value of a local Uint64Flag, returns 0 if not found func (c *Command) Uint64Arg(name string) uint64 func (c *Command) Uint64Args(name string) []uint64 func (cmd *Command) Uint64Slice(name string) []uint64 Uint64Slice looks up the value of a local Uint64SliceFlag, returns nil if not found func (cmd *Command) Uint8(name string) uint8 Uint8 looks up the value of a local Uint8Flag, returns 0 if not found func (c *Command) Uint8Arg(name string) uint8 func (c *Command) Uint8Args(name string) []uint8 func (cmd *Command) Uint8Slice(name string) []uint8 Uint8Slice looks up the value of a local Uint8SliceFlag, returns nil if not found func (c *Command) UintArg(name string) uint func (c *Command) UintArgs(name string) []uint func (cmd *Command) UintSlice(name string) []uint UintSlice looks up the value of a local UintSliceFlag, returns nil if not found func (cmd *Command) Value(name string) interface{} Value returns the value of the flag corresponding to `name` func (cmd *Command) VisibleCategories() []CommandCategory VisibleCategories returns a slice of categories and commands that are Hidden=false func (cmd *Command) VisibleCommands() []*Command VisibleCommands returns a slice of the Commands with Hidden=false func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain func (cmd *Command) VisibleFlags() []Flag VisibleFlags returns a slice of the Flags with Hidden=false func (cmd *Command) VisiblePersistentFlags() []Flag VisiblePersistentFlags returns a slice of LocalFlag with Persistent=true and Hidden=false. type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command) // Categories returns a slice of categories sorted by name Categories() []CommandCategory } CommandCategories interface allows for category manipulation type CommandCategory interface { // Name returns the category name string Name() string // VisibleCommands returns a slice of the Commands with Hidden=false VisibleCommands() []*Command } CommandCategory is a category containing commands. type CommandNotFoundFunc func(context.Context, *Command, string) CommandNotFoundFunc is executed if the proper command cannot be found type ConfigureShellCompletionCommand func(*Command) ConfigureShellCompletionCommand is a function to configure a shell completion command type Countable interface { Count() int } Countable is an interface to enable detection of flag values which support repetitive flags type DocGenerationFlag interface { // TakesValue returns true if the flag takes a value, otherwise false TakesValue() bool // GetUsage returns the usage string for the flag GetUsage() string // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. GetValue() string // GetDefaultText returns the default text for this flag GetDefaultText() string // GetEnvVars returns the env vars for this flag GetEnvVars() []string // IsDefaultVisible returns whether the default value should be shown in // help text IsDefaultVisible() bool // TypeName to detect if a flag is a string, bool, etc. TypeName() string } DocGenerationFlag is an interface that allows documentation generation for the flag type DocGenerationMultiValueFlag interface { DocGenerationFlag // IsMultiValueFlag returns true for flags that can be given multiple times. IsMultiValueFlag() bool } DocGenerationMultiValueFlag extends DocGenerationFlag for slice/map based flags. type DurationFlag = FlagBase[time.Duration, NoConfig, durationValue] type EnvValueSource interface { IsFromEnv() bool Key() string } EnvValueSource is to specifically detect env sources when printing help text type ErrorFormatter interface { Format(s fmt.State, verb rune) } ErrorFormatter is the interface that will suitably format the error output type ExitCoder interface { error ExitCode() int } ExitCoder is the interface checked by `Command` for a custom exit code. func Exit(message any, exitCode int) ExitCoder Exit wraps a message and exit code into an error, which by default is handled with a call to os.Exit during default error handling. This is the simplest way to trigger a non-zero exit code for a Command without having to call os.Exit manually. During testing, this behavior can be avoided by overriding the ExitErrHandler function on a Command or the package-global OsExiter function. type ExitErrHandlerFunc func(context.Context, *Command, error) ExitErrHandlerFunc is executed if provided in order to handle exitError values returned by Actions and Before/After functions. type Flag interface { fmt.Stringer // Retrieve the value of the Flag Get() any // Lifecycle methods. // flag callback prior to parsing PreParse() error // flag callback post parsing PostParse() error // Apply Flag settings to the given flag set Set(string, string) error // All possible names for this flag Names() []string // Whether the flag has been set or not IsSet() bool } Flag is a common interface related to parsing flags in cli. For more advanced flag parsing techniques, it is recommended that this interface be implemented. var GenerateShellCompletionFlag Flag = &BoolFlag{ Name: "generate-shell-completion", Hidden: true, } GenerateShellCompletionFlag enables shell completion var HelpFlag Flag = &BoolFlag{ Name: "help", Aliases: []string{"h"}, Usage: "show help", HideDefault: true, Local: true, } HelpFlag prints the help for all commands and subcommands. Set to nil to disable the flag. The subcommand will still be added unless HideHelp or HideHelpCommand is set to true. var VersionFlag Flag = &BoolFlag{ Name: "version", Aliases: []string{"v"}, Usage: "print the version", HideDefault: true, Local: true, } VersionFlag prints the version for the application type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { Name string `json:"name"` // name of the flag Category string `json:"category"` // category of the flag, if any DefaultText string `json:"defaultText"` // default text of the flag for usage purposes HideDefault bool `json:"hideDefault"` // whether to hide the default value in output Usage string `json:"usage"` // usage string for help output Sources ValueSourceChain `json:"-"` // sources to load flag value from Required bool `json:"required"` // whether the flag is required or not Hidden bool `json:"hidden"` // whether to hide the flag in help output Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well Value T `json:"defaultValue"` // default value for this flag if not set by from any source Destination *T `json:"-"` // destination pointer for value when set Aliases []string `json:"aliases"` // Aliases that are allowed for this flag TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes Action func(context.Context, *Command, T) error `json:"-"` // Action callback to be called when flag is set Config C `json:"config"` // Additional/Custom configuration associated with this flag type OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line Validator func(T) error `json:"-"` // custom function to validate this flag value ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not // Has unexported fields. } FlagBase [T,C,VC] is a generic flag base which can be used as a boilerplate to implement the most common interfaces used by urfave/cli. T specifies the type C specifies the configuration required(if any for that flag type) VC specifies the value creator which creates the flag.Value emulation func (f *FlagBase[T, C, VC]) Count() int Count returns the number of times this flag has been invoked func (f *FlagBase[T, C, V]) Get() any func (f *FlagBase[T, C, V]) GetCategory() string GetCategory returns the category of the flag func (f *FlagBase[T, C, V]) GetDefaultText() string GetDefaultText returns the default text for this flag func (f *FlagBase[T, C, V]) GetEnvVars() []string GetEnvVars returns the env vars for this flag func (f *FlagBase[T, C, V]) GetUsage() string GetUsage returns the usage string for the flag func (f *FlagBase[T, C, V]) GetValue() string GetValue returns the flags value as string representation and an empty string if the flag takes no value at all. func (f *FlagBase[T, C, VC]) IsBoolFlag() bool IsBoolFlag returns whether the flag doesnt need to accept args func (f *FlagBase[T, C, V]) IsDefaultVisible() bool IsDefaultVisible returns true if the flag is not hidden, otherwise false func (f *FlagBase[T, C, VC]) IsLocal() bool IsLocal returns false if flag needs to be persistent across subcommands func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool IsMultiValueFlag returns true if the value type T can take multiple values from cmd line. This is true for slice and map type flags func (f *FlagBase[T, C, V]) IsRequired() bool IsRequired returns whether or not the flag is required func (f *FlagBase[T, C, V]) IsSet() bool IsSet returns whether or not the flag has been set through env or file func (f *FlagBase[T, C, V]) IsVisible() bool IsVisible returns true if the flag is not hidden, otherwise false func (f *FlagBase[T, C, V]) Names() []string Names returns the names of the flag func (f *FlagBase[T, C, V]) PostParse() error PostParse populates the flag given the flag set and environment func (f *FlagBase[T, C, V]) PreParse() error func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set func (f *FlagBase[T, C, V]) Set(_ string, val string) error Set applies given value from string func (f *FlagBase[T, C, V]) SetCategory(c string) func (f *FlagBase[T, C, V]) String() string String returns a readable representation of this value (for usage defaults) func (f *FlagBase[T, C, V]) TakesValue() bool TakesValue returns true if the flag takes a value, otherwise false func (f *FlagBase[T, C, V]) TypeName() string TypeName returns the type of the flag. type FlagCategories interface { // AddFlags adds a flag to a category, creating a new category if necessary. AddFlag(category string, fl Flag) // VisibleCategories returns a slice of visible flag categories sorted by name VisibleCategories() []VisibleFlagCategory } FlagCategories interface allows for category manipulation type FlagEnvHintFunc func(envVars []string, str string) string FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help with the environment variable details. var FlagEnvHinter FlagEnvHintFunc = withEnvHint FlagEnvHinter annotates flag help message with the environment variable details. This is used by the default FlagStringer. type FlagFileHintFunc func(filePath, str string) string FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help with the file path details. var FlagFileHinter FlagFileHintFunc = withFileHint FlagFileHinter annotates flag help message with the environment variable details. This is used by the default FlagStringer. type FlagNamePrefixFunc func(fullName []string, placeholder string) string FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix text for a flag's full name. var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames FlagNamePrefixer converts a full flag name and its placeholder into the help message flag prefix. This is used by the default FlagStringer. type FlagStringFunc func(Flag) string FlagStringFunc is used by the help generation to display a flag, which is expected to be a single line. var FlagStringer FlagStringFunc = stringifyFlag FlagStringer converts a flag definition to a string. This is used by help to display a flag. type FlagsByName []Flag FlagsByName is a slice of Flag. func (f FlagsByName) Len() int func (f FlagsByName) Less(i, j int) bool func (f FlagsByName) Swap(i, j int) type Float32Arg = ArgumentBase[float32, NoConfig, floatValue[float32]] type Float32Args = ArgumentsBase[float32, NoConfig, floatValue[float32]] type Float32Flag = FlagBase[float32, NoConfig, floatValue[float32]] type Float32Slice = SliceBase[float32, NoConfig, floatValue[float32]] type Float32SliceFlag = FlagBase[[]float32, NoConfig, Float32Slice] type Float64Arg = ArgumentBase[float64, NoConfig, floatValue[float64]] type Float64Args = ArgumentsBase[float64, NoConfig, floatValue[float64]] type Float64Flag = FlagBase[float64, NoConfig, floatValue[float64]] type Float64Slice = SliceBase[float64, NoConfig, floatValue[float64]] type Float64SliceFlag = FlagBase[[]float64, NoConfig, Float64Slice] type FloatArg = ArgumentBase[float64, NoConfig, floatValue[float64]] type FloatArgs = ArgumentsBase[float64, NoConfig, floatValue[float64]] type FloatFlag = FlagBase[float64, NoConfig, floatValue[float64]] type FloatSlice = SliceBase[float64, NoConfig, floatValue[float64]] type FloatSliceFlag = FlagBase[[]float64, NoConfig, FloatSlice] type GenericFlag = FlagBase[Value, NoConfig, genericValue] type HelpPrinterCustomFunc func(w io.Writer, templ string, data any, customFunc map[string]any) Prints help for the Command with custom template function. var HelpPrinterCustom HelpPrinterCustomFunc = DefaultPrintHelpCustom HelpPrinterCustom is a function that writes the help output. It is used as the default implementation of HelpPrinter, and may be called directly if the ExtraInfo field is set on a Command. In the default implementation, if the customFuncs argument contains a "wrapAt" key, which is a function which takes no arguments and returns an int, this int value will be used to produce a "wrap" function used by the default template to wrap long lines. type HelpPrinterFunc func(w io.Writer, templ string, data any) HelpPrinterFunc prints help for the Command. var HelpPrinter HelpPrinterFunc = DefaultPrintHelp HelpPrinter is a function that writes the help output. If not set explicitly, this calls HelpPrinterCustom using only the default template functions. If custom logic for printing help is required, this function can be overridden. If the ExtraInfo field is defined on a Command, this function should not be modified, as HelpPrinterCustom will be used directly in order to capture the extra information. type Int16Arg = ArgumentBase[int16, IntegerConfig, intValue[int16]] type Int16Args = ArgumentsBase[int16, IntegerConfig, intValue[int16]] type Int16Flag = FlagBase[int16, IntegerConfig, intValue[int16]] type Int16Slice = SliceBase[int16, IntegerConfig, intValue[int16]] type Int16SliceFlag = FlagBase[[]int16, IntegerConfig, Int16Slice] type Int32Arg = ArgumentBase[int32, IntegerConfig, intValue[int32]] type Int32Args = ArgumentsBase[int32, IntegerConfig, intValue[int32]] type Int32Flag = FlagBase[int32, IntegerConfig, intValue[int32]] type Int32Slice = SliceBase[int32, IntegerConfig, intValue[int32]] type Int32SliceFlag = FlagBase[[]int32, IntegerConfig, Int32Slice] type Int64Arg = ArgumentBase[int64, IntegerConfig, intValue[int64]] type Int64Args = ArgumentsBase[int64, IntegerConfig, intValue[int64]] type Int64Flag = FlagBase[int64, IntegerConfig, intValue[int64]] type Int64Slice = SliceBase[int64, IntegerConfig, intValue[int64]] type Int64SliceFlag = FlagBase[[]int64, IntegerConfig, Int64Slice] type Int8Arg = ArgumentBase[int8, IntegerConfig, intValue[int8]] type Int8Args = ArgumentsBase[int8, IntegerConfig, intValue[int8]] type Int8Flag = FlagBase[int8, IntegerConfig, intValue[int8]] type Int8Slice = SliceBase[int8, IntegerConfig, intValue[int8]] type Int8SliceFlag = FlagBase[[]int8, IntegerConfig, Int8Slice] type IntArg = ArgumentBase[int, IntegerConfig, intValue[int]] type IntArgs = ArgumentsBase[int, IntegerConfig, intValue[int]] type IntFlag = FlagBase[int, IntegerConfig, intValue[int]] type IntSlice = SliceBase[int, IntegerConfig, intValue[int]] type IntSliceFlag = FlagBase[[]int, IntegerConfig, IntSlice] type IntegerConfig struct { Base int } IntegerConfig is the configuration for all integer type flags type InvalidFlagAccessFunc func(context.Context, *Command, string) InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. type LocalFlag interface { IsLocal() bool } LocalFlag is an interface to enable detection of flags which are local to current command type MapBase[T any, C any, VC ValueCreator[T, C]] struct { // Has unexported fields. } MapBase wraps map[string]T to satisfy flag.Value func NewMapBase[T any, C any, VC ValueCreator[T, C]](defaults map[string]T) *MapBase[T, C, VC] NewMapBase makes a *MapBase with default values func (i MapBase[T, C, VC]) Create(val map[string]T, p *map[string]T, c C) Value func (i *MapBase[T, C, VC]) Get() interface{} Get returns the mapping of values set by this flag func (i *MapBase[T, C, VC]) Serialize() string Serialize allows MapBase to fulfill Serializer func (i *MapBase[T, C, VC]) Set(value string) error Set parses the value and appends it to the list of values func (i *MapBase[T, C, VC]) String() string String returns a readable representation of this value (for usage defaults) func (i MapBase[T, C, VC]) ToString(t map[string]T) string func (i *MapBase[T, C, VC]) Value() map[string]T Value returns the mapping of values set by this flag type MapSource interface { fmt.Stringer fmt.GoStringer // Lookup returns the value from the source based on key // and if it was found // or returns an empty string and false Lookup(string) (any, bool) } MapSource is a source which can be used to look up a value based on a key typically for use with a cli.Flag func NewMapSource(name string, m map[any]any) MapSource type MultiError interface { error Errors() []error } MultiError is an error that wraps multiple errors. type MutuallyExclusiveFlags struct { // Flag list Flags [][]Flag // whether this group is required Required bool // Category to apply to all flags within group Category string } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline So for example [ --foo | [ --bar something --darth somethingelse ] ] type NoConfig struct{} NoConfig is for flags which dont need a custom configuration type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying customized usage error messages. This function is able to replace the original error messages. If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. type RequiredFlag interface { // whether the flag is a required flag or not IsRequired() bool } RequiredFlag is an interface that allows us to mark flags as required it allows flags required flags to be backwards compatible with the Flag interface type Serializer interface { Serialize() string } Serializer is used to circumvent the limitations of flag.FlagSet.Set type ShellCompleteFunc func(context.Context, *Command) ShellCompleteFunc is an action to execute when the shell completion flag is set type SliceBase[T any, C any, VC ValueCreator[T, C]] struct { // Has unexported fields. } SliceBase wraps []T to satisfy flag.Value func NewSliceBase[T any, C any, VC ValueCreator[T, C]](defaults ...T) *SliceBase[T, C, VC] NewSliceBase makes a *SliceBase with default values func (i SliceBase[T, C, VC]) Create(val []T, p *[]T, c C) Value func (i *SliceBase[T, C, VC]) Get() interface{} Get returns the slice of values set by this flag func (i *SliceBase[T, C, VC]) Serialize() string Serialize allows SliceBase to fulfill Serializer func (i *SliceBase[T, C, VC]) Set(value string) error Set parses the value and appends it to the list of values func (i *SliceBase[T, C, VC]) String() string String returns a readable representation of this value (for usage defaults) func (i SliceBase[T, C, VC]) ToString(t []T) string func (i *SliceBase[T, C, VC]) Value() []T Value returns the slice of values set by this flag type StringArg = ArgumentBase[string, StringConfig, stringValue] type StringArgs = ArgumentsBase[string, StringConfig, stringValue] type StringConfig struct { // Whether to trim whitespace of parsed value TrimSpace bool } StringConfig defines the configuration for string flags type StringFlag = FlagBase[string, StringConfig, stringValue] type StringMap = MapBase[string, StringConfig, stringValue] type StringMapArgs = ArgumentBase[map[string]string, StringConfig, StringMap] type StringMapFlag = FlagBase[map[string]string, StringConfig, StringMap] type StringSlice = SliceBase[string, StringConfig, stringValue] type StringSliceFlag = FlagBase[[]string, StringConfig, StringSlice] type SuggestCommandFunc func(commands []*Command, provided string) string type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue] type TimestampArgs = ArgumentsBase[time.Time, TimestampConfig, timestampValue] type TimestampConfig struct { Timezone *time.Location // Available layouts for flag value. // // Note that value for formats with missing year/date will be interpreted as current year/date respectively. // // Read more about time layouts: https://pkg.go.dev/time#pkg-constants Layouts []string } TimestampConfig defines the config for timestamp flags type TimestampFlag = FlagBase[time.Time, TimestampConfig, timestampValue] type Uint16Arg = ArgumentBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16Args = ArgumentsBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16Flag = FlagBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16Slice = SliceBase[uint16, IntegerConfig, uintValue[uint16]] type Uint16SliceFlag = FlagBase[[]uint16, IntegerConfig, Uint16Slice] type Uint32Arg = ArgumentBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32Args = ArgumentsBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32Flag = FlagBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32Slice = SliceBase[uint32, IntegerConfig, uintValue[uint32]] type Uint32SliceFlag = FlagBase[[]uint32, IntegerConfig, Uint32Slice] type Uint64Arg = ArgumentBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64Args = ArgumentsBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64Flag = FlagBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64Slice = SliceBase[uint64, IntegerConfig, uintValue[uint64]] type Uint64SliceFlag = FlagBase[[]uint64, IntegerConfig, Uint64Slice] type Uint8Arg = ArgumentBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8Args = ArgumentsBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8Flag = FlagBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8Slice = SliceBase[uint8, IntegerConfig, uintValue[uint8]] type Uint8SliceFlag = FlagBase[[]uint8, IntegerConfig, Uint8Slice] type UintArg = ArgumentBase[uint, IntegerConfig, uintValue[uint]] type UintArgs = ArgumentsBase[uint, IntegerConfig, uintValue[uint]] type UintFlag = FlagBase[uint, IntegerConfig, uintValue[uint]] type UintSlice = SliceBase[uint, IntegerConfig, uintValue[uint]] type UintSliceFlag = FlagBase[[]uint, IntegerConfig, UintSlice] type Value interface { flag.Value flag.Getter } Value represents a value as used by cli. For now it implements the golang flag.Value interface type ValueCreator[T any, C any] interface { Create(T, *T, C) Value ToString(T) string } ValueCreator is responsible for creating a flag.Value emulation as well as custom formatting T specifies the type C specifies the config for the type type ValueSource interface { fmt.Stringer fmt.GoStringer // Lookup returns the value from the source and if it was found // or returns an empty string and false Lookup() (string, bool) } ValueSource is a source which can be used to look up a value, typically for use with a cli.Flag func EnvVar(key string) ValueSource func File(path string) ValueSource func NewMapValueSource(key string, ms MapSource) ValueSource type ValueSourceChain struct { Chain []ValueSource } ValueSourceChain contains an ordered series of ValueSource that allows for lookup where the first ValueSource to resolve is returned func EnvVars(keys ...string) ValueSourceChain EnvVars is a helper function to encapsulate a number of envVarValueSource together as a ValueSourceChain func Files(paths ...string) ValueSourceChain Files is a helper function to encapsulate a number of fileValueSource together as a ValueSourceChain func NewValueSourceChain(src ...ValueSource) ValueSourceChain func (vsc *ValueSourceChain) Append(other ValueSourceChain) func (vsc *ValueSourceChain) EnvKeys() []string func (vsc *ValueSourceChain) GoString() string func (vsc *ValueSourceChain) Lookup() (string, bool) func (vsc *ValueSourceChain) LookupWithSource() (string, ValueSource, bool) func (vsc *ValueSourceChain) String() string type VisibleFlag interface { // IsVisible returns true if the flag is not hidden, otherwise false IsVisible() bool } VisibleFlag is an interface that allows to check if a flag is visible type VisibleFlagCategory interface { // Name returns the category name string Name() string // Flags returns a slice of VisibleFlag sorted by name Flags() []Flag } VisibleFlagCategory is a category containing flags. ================================================ FILE: value_source.go ================================================ package cli import ( "fmt" "os" "strings" ) // ValueSource is a source which can be used to look up a value, // typically for use with a cli.Flag type ValueSource interface { fmt.Stringer fmt.GoStringer // Lookup returns the value from the source and if it was found // or returns an empty string and false Lookup() (string, bool) } // EnvValueSource is to specifically detect env sources when // printing help text type EnvValueSource interface { IsFromEnv() bool Key() string } // MapSource is a source which can be used to look up a value // based on a key // typically for use with a cli.Flag type MapSource interface { fmt.Stringer fmt.GoStringer // Lookup returns the value from the source based on key // and if it was found // or returns an empty string and false Lookup(string) (any, bool) } // ValueSourceChain contains an ordered series of ValueSource that // allows for lookup where the first ValueSource to resolve is // returned type ValueSourceChain struct { Chain []ValueSource } func NewValueSourceChain(src ...ValueSource) ValueSourceChain { return ValueSourceChain{ Chain: src, } } func (vsc *ValueSourceChain) Append(other ValueSourceChain) { vsc.Chain = append(vsc.Chain, other.Chain...) } func (vsc *ValueSourceChain) EnvKeys() []string { vals := []string{} for _, src := range vsc.Chain { if v, ok := src.(EnvValueSource); ok && v.IsFromEnv() { vals = append(vals, v.Key()) } } return vals } func (vsc *ValueSourceChain) String() string { s := []string{} for _, vs := range vsc.Chain { s = append(s, vs.String()) } return strings.Join(s, ",") } func (vsc *ValueSourceChain) GoString() string { s := []string{} for _, vs := range vsc.Chain { s = append(s, vs.GoString()) } return fmt.Sprintf("&ValueSourceChain{Chain:{%[1]s}}", strings.Join(s, ",")) } func (vsc *ValueSourceChain) Lookup() (string, bool) { s, _, ok := vsc.LookupWithSource() return s, ok } func (vsc *ValueSourceChain) LookupWithSource() (string, ValueSource, bool) { for _, src := range vsc.Chain { if value, found := src.Lookup(); found { return value, src, true } } return "", nil, false } // envVarValueSource encapsulates a ValueSource from an environment variable type envVarValueSource struct { key string } func (e *envVarValueSource) Lookup() (string, bool) { return os.LookupEnv(strings.TrimSpace(string(e.key))) } func (e *envVarValueSource) IsFromEnv() bool { return true } func (e *envVarValueSource) Key() string { return e.key } func (e *envVarValueSource) String() string { return fmt.Sprintf("environment variable %[1]q", e.key) } func (e *envVarValueSource) GoString() string { return fmt.Sprintf("&envVarValueSource{Key:%[1]q}", e.key) } func EnvVar(key string) ValueSource { return &envVarValueSource{ key: key, } } // EnvVars is a helper function to encapsulate a number of // envVarValueSource together as a ValueSourceChain func EnvVars(keys ...string) ValueSourceChain { vsc := ValueSourceChain{Chain: []ValueSource{}} for _, key := range keys { vsc.Chain = append(vsc.Chain, EnvVar(key)) } return vsc } // fileValueSource encapsulates a ValueSource from a file type fileValueSource struct { Path string } func (f *fileValueSource) Lookup() (string, bool) { data, err := os.ReadFile(f.Path) return string(data), err == nil } func (f *fileValueSource) String() string { return fmt.Sprintf("file %[1]q", f.Path) } func (f *fileValueSource) GoString() string { return fmt.Sprintf("&fileValueSource{Path:%[1]q}", f.Path) } func File(path string) ValueSource { return &fileValueSource{Path: path} } // Files is a helper function to encapsulate a number of // fileValueSource together as a ValueSourceChain func Files(paths ...string) ValueSourceChain { vsc := ValueSourceChain{Chain: []ValueSource{}} for _, path := range paths { vsc.Chain = append(vsc.Chain, File(path)) } return vsc } type mapSource struct { name string m map[any]any } func NewMapSource(name string, m map[any]any) MapSource { return &mapSource{ name: name, m: m, } } func (ms *mapSource) String() string { return fmt.Sprintf("map source %[1]q", ms.name) } func (ms *mapSource) GoString() string { return fmt.Sprintf("&mapSource{name:%[1]q}", ms.name) } // Lookup returns a value from the map source. The lookup name may be a dot-separated path into the map. // If that is the case, it will recursively traverse the map based on the '.' delimited sections to find // a nested value for the key. func (ms *mapSource) Lookup(name string) (any, bool) { sections := strings.Split(name, ".") if name == "" || len(sections) == 0 { return nil, false } node := ms.m // traverse into the map based on the dot-separated sections if len(sections) >= 2 { // the last section is the value we want, we will return it directly at the end for _, section := range sections[:len(sections)-1] { child, ok := node[section] if !ok { return nil, false } switch child := child.(type) { case map[string]any: node = make(map[any]any, len(child)) for k, v := range child { node[k] = v } case map[any]any: node = child default: return nil, false } } } if val, ok := node[sections[len(sections)-1]]; ok { return val, true } return nil, false } type mapValueSource struct { key string ms MapSource } func NewMapValueSource(key string, ms MapSource) ValueSource { return &mapValueSource{ key: key, ms: ms, } } func (mvs *mapValueSource) String() string { return fmt.Sprintf("key %[1]q from %[2]s", mvs.key, mvs.ms.String()) } func (mvs *mapValueSource) GoString() string { return fmt.Sprintf("&mapValueSource{key:%[1]q, src:%[2]s}", mvs.key, mvs.ms.GoString()) } func (mvs *mapValueSource) Lookup() (string, bool) { if v, ok := mvs.ms.Lookup(mvs.key); !ok { return "", false } else { return fmt.Sprintf("%+v", v), true } } ================================================ FILE: value_source_test.go ================================================ package cli import ( "fmt" "math/rand" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestZeroValueSourceChain(t *testing.T) { var vc ValueSourceChain assert.Empty(t, vc.EnvKeys()) assert.NotEmpty(t, vc.GoString()) assert.Empty(t, vc.Chain) assert.Empty(t, vc.String()) } func TestEnvVarValueSource(t *testing.T) { t.Run("implements ValueSource", func(t *testing.T) { src := EnvVar("foo") require.Implements(t, (*ValueSource)(nil), src) t.Run("not found", func(t *testing.T) { t.Setenv("foo", "bar") src := EnvVar("foo_1") _, ok := src.Lookup() require.False(t, ok) }) t.Run("found", func(t *testing.T) { t.Setenv("foo", "bar") r := require.New(t) src := EnvVar("foo") str, ok := src.Lookup() r.True(ok) r.Equal(str, "bar") }) }) t.Run("implements fmt.Stringer", func(t *testing.T) { src := EnvVar("foo") r := require.New(t) r.Implements((*fmt.Stringer)(nil), src) r.Equal("environment variable \"foo\"", src.String()) }) t.Run("implements fmt.GoStringer", func(t *testing.T) { src := EnvVar("foo") r := require.New(t) r.Implements((*fmt.GoStringer)(nil), src) r.Equal("&envVarValueSource{Key:\"foo\"}", src.GoString()) }) } func TestEnvVars(t *testing.T) { t.Setenv("myfoo", "mybar") source := EnvVars("foo1", "myfoo") str, src, ok := source.LookupWithSource() r := require.New(t) r.True(ok) r.Equal(str, "mybar") r.Contains(src.String(), "\"myfoo\"") } func TestFileValueSource(t *testing.T) { t.Run("implements ValueSource", func(t *testing.T) { r := require.New(t) r.Implements((*ValueSource)(nil), &fileValueSource{}) t.Run("not found", func(t *testing.T) { src := File(fmt.Sprintf("junk_file_name-%[1]v", rand.Int())) _, ok := src.Lookup() r.False(ok) }) fileName := filepath.Join(os.TempDir(), fmt.Sprintf("urfave-cli-testing-existing_file-%[1]v", rand.Int())) t.Cleanup(func() { _ = os.Remove(fileName) }) r.Nil(os.WriteFile(fileName, []byte("pita"), 0o644)) t.Run("found", func(t *testing.T) { src := File(fileName) str, ok := src.Lookup() r.True(ok) r.Equal("pita", str) }) }) t.Run("implements fmt.Stringer", func(t *testing.T) { src := File("/dev/null") r := require.New(t) r.Implements((*ValueSource)(nil), src) r.Equal("file \"/dev/null\"", src.String()) }) t.Run("implements fmt.GoStringer", func(t *testing.T) { src := File("/dev/null") r := require.New(t) r.Implements((*ValueSource)(nil), src) r.Equal("&fileValueSource{Path:\"/dev/null\"}", src.GoString()) }) } func TestFilePaths(t *testing.T) { r := require.New(t) fileName := filepath.Join(os.TempDir(), fmt.Sprintf("urfave-cli-tests-some_file_name_%[1]v", rand.Int())) t.Cleanup(func() { _ = os.Remove(fileName) }) r.Nil(os.WriteFile(fileName, []byte("Hello"), 0o644)) sources := Files("junk_file_name", fileName) str, src, ok := sources.LookupWithSource() r.True(ok) r.Equal(str, "Hello") r.Contains(src.String(), fmt.Sprintf("%[1]q", fileName)) } func TestValueSourceChainEnvKeys(t *testing.T) { chain := NewValueSourceChain( &staticValueSource{"hello"}, ) chain.Append(EnvVars("foo", "bar")) r := require.New(t) r.Equal([]string{"foo", "bar"}, chain.EnvKeys()) } func TestValueSourceChain(t *testing.T) { t.Run("implements ValueSource", func(t *testing.T) { vsc := &ValueSourceChain{} r := require.New(t) r.Implements((*ValueSource)(nil), vsc) _, ok := vsc.Lookup() r.False(ok) }) t.Run("implements fmt.GoStringer", func(t *testing.T) { vsc := &ValueSourceChain{} r := require.New(t) r.Implements((*fmt.GoStringer)(nil), vsc) r.Equal("&ValueSourceChain{Chain:{}}", vsc.GoString()) vsc1 := NewValueSourceChain(&staticValueSource{v: "yahtzee"}, &staticValueSource{v: "matzoh"}, ) r.Equal("&ValueSourceChain{Chain:{&staticValueSource{v:\"yahtzee\"},&staticValueSource{v:\"matzoh\"}}}", vsc1.GoString()) }) t.Run("implements fmt.Stringer", func(t *testing.T) { vsc := &ValueSourceChain{} r := require.New(t) r.Implements((*fmt.Stringer)(nil), vsc) r.Equal("", vsc.String()) vsc1 := NewValueSourceChain( &staticValueSource{v: "soup"}, &staticValueSource{v: "salad"}, &staticValueSource{v: "pumpkins"}, ) r.Equal("soup,salad,pumpkins", vsc1.String()) }) } type staticValueSource struct { v string } func (svs *staticValueSource) GoString() string { return fmt.Sprintf("&staticValueSource{v:%[1]q}", svs.v) } func (svs *staticValueSource) String() string { return svs.v } func (svs *staticValueSource) Lookup() (string, bool) { return svs.v, true } func TestMapValueSource(t *testing.T) { tests := []struct { name string m map[any]any key string val string found bool }{ { name: "No map no key", }, { name: "No map with key", key: "foo", }, { name: "Empty map no key", m: map[any]any{}, }, { name: "Empty map with key", key: "foo", m: map[any]any{}, }, { name: "Level 1 no key", key: ".foob", m: map[any]any{ "foo": 10, }, }, { name: "Level 1", key: "foobar", m: map[any]any{ "foobar": 10, }, val: "10", found: true, }, { name: "Level 2", key: "foo.bar", m: map[any]any{ "foo": map[any]any{ "bar": 10, }, }, val: "10", found: true, }, { name: "Level 2 invalid key", key: "foo.bar1", m: map[any]any{ "foo": map[any]any{ "bar": "10", }, }, }, { name: "Level 2 string map type", key: "foo.bar1", m: map[any]any{ "foo": map[string]any{ "bar": "10", }, }, }, { name: "Level 3 no entry", key: "foo.bar.t", m: map[any]any{ "foo": map[any]any{ "bar": "sss", }, }, }, { name: "Level 3", key: "foo.bar.t", m: map[any]any{ "foo": map[any]any{ "bar": map[any]any{ "t": "sss", }, }, }, val: "sss", found: true, }, { name: "Level 3 invalid key", key: "foo.bar.t", m: map[any]any{ "foo": map[any]any{ "bar": map[any]any{ "t1": 10, }, }, }, }, { name: "Level 4 no entry", key: "foo.bar.t.gh", m: map[any]any{ "foo": map[any]any{ "bar": map[any]any{ "t1": 10, }, }, }, }, { name: "Level 4 slice entry", key: "foo.bar.t.gh", m: map[any]any{ "foo": map[any]any{ "bar": map[string]any{ "t": map[any]any{ "gh": []int{10}, }, }, }, }, val: "[10]", found: true, }, } for _, test := range tests { t.Run(test.key, func(t *testing.T) { ms := NewMapSource("test", test.m) m := NewMapValueSource(test.key, ms) val, b := m.Lookup() if !test.found { assert.False(t, b) } else { assert.True(t, b) assert.Equal(t, val, test.val) } }) } } func TestMapValueSourceStringer(t *testing.T) { m := map[any]any{ "foo": map[any]any{ "bar": 10, }, } mvs := NewMapValueSource("bar", NewMapSource("test", m)) assert.Equal(t, `&mapValueSource{key:"bar", src:&mapSource{name:"test"}}`, mvs.GoString()) assert.Equal(t, `key "bar" from map source "test"`, mvs.String()) }