├── .github ├── dependabot.yml ├── labeler.yml └── workflows │ ├── labeler.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .mailmap ├── CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MAINTAINERS ├── Makefile ├── README.md ├── SECURITY.md ├── active_help.go ├── active_help_test.go ├── args.go ├── args_test.go ├── assets └── CobraMain.png ├── bash_completions.go ├── bash_completionsV2.go ├── bash_completionsV2_test.go ├── bash_completions_test.go ├── cobra.go ├── cobra_test.go ├── command.go ├── command_notwin.go ├── command_test.go ├── command_win.go ├── completions.go ├── completions_test.go ├── doc ├── cmd_test.go ├── man_docs.go ├── man_docs_test.go ├── man_examples_test.go ├── md_docs.go ├── md_docs_test.go ├── rest_docs.go ├── rest_docs_test.go ├── util.go ├── yaml_docs.go └── yaml_docs_test.go ├── fish_completions.go ├── fish_completions_test.go ├── flag_groups.go ├── flag_groups_test.go ├── go.mod ├── go.sum ├── powershell_completions.go ├── powershell_completions_test.go ├── shell_completions.go ├── site └── content │ ├── active_help.md │ ├── completions │ ├── _index.md │ ├── bash.md │ ├── fish.md │ ├── powershell.md │ └── zsh.md │ ├── docgen │ ├── _index.md │ ├── man.md │ ├── md.md │ ├── rest.md │ └── yaml.md │ ├── projects_using_cobra.md │ └── user_guide.md ├── zsh_completions.go └── zsh_completions_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 99 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | open-pull-requests-limit: 99 13 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # changes to documentation generation 2 | "area/docs-generation": 3 | - changed-files: 4 | - any-glob-to-any-file: 'doc/**' 5 | 6 | # changes to the core cobra command 7 | "area/cobra-command": 8 | - changed-files: 9 | - any-glob-to-any-file: ['./cobra.go', './cobra_test.go', './*command*.go'] 10 | 11 | # changes made to command flags/args 12 | "area/flags": 13 | - changed-files: 14 | - any-glob-to-any-file: './args*.go' 15 | 16 | # changes to Github workflows 17 | "area/github": 18 | - changed-files: 19 | - any-glob-to-any-file: '.github/**' 20 | 21 | # changes to shell completions 22 | "area/shell-completion": 23 | - changed-files: 24 | - any-glob-to-any-file: './*completions*' 25 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | triage: 10 | permissions: 11 | contents: read # for actions/labeler to determine modified files 12 | pull-requests: write # for actions/labeler to add labels to PRs 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/labeler@v5 16 | with: 17 | repo-token: "${{ github.token }}" 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | GO111MODULE: on 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | 16 | 17 | lic-headers: 18 | runs-on: ubuntu-latest 19 | steps: 20 | 21 | - uses: actions/checkout@v4 22 | 23 | - run: >- 24 | docker run 25 | -v $(pwd):/wrk -w /wrk 26 | ghcr.io/google/addlicense 27 | -c 'The Cobra Authors' 28 | -y '2013-2023' 29 | -l apache 30 | -ignore '.github/**' 31 | -check 32 | . 33 | 34 | 35 | golangci-lint: 36 | permissions: 37 | contents: read # for actions/checkout to fetch code 38 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 39 | runs-on: ubuntu-latest 40 | steps: 41 | 42 | - uses: actions/checkout@v4 43 | 44 | - uses: actions/setup-go@v5 45 | with: 46 | go-version: '^1.22' 47 | check-latest: true 48 | cache: true 49 | 50 | - uses: golangci/golangci-lint-action@v8.0.0 51 | with: 52 | version: latest 53 | args: --verbose 54 | 55 | 56 | test-unix: 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | platform: 61 | - ubuntu 62 | - macOS 63 | go: 64 | - 17 65 | - 18 66 | - 19 67 | - 20 68 | - 21 69 | - 22 70 | - 23 71 | - 24 72 | name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' 73 | runs-on: ${{ matrix.platform }}-latest 74 | steps: 75 | 76 | - uses: actions/checkout@v4 77 | 78 | - uses: actions/setup-go@v5 79 | with: 80 | go-version: 1.${{ matrix.go }}.x 81 | cache: true 82 | 83 | - run: | 84 | export GOBIN=$HOME/go/bin 85 | go install github.com/kyoh86/richgo@latest 86 | go install github.com/mitchellh/gox@latest 87 | 88 | - run: RICHGO_FORCE_COLOR=1 PATH=$HOME/go/bin/:$PATH make richtest 89 | 90 | 91 | test-win: 92 | name: MINGW64 93 | defaults: 94 | run: 95 | shell: msys2 {0} 96 | runs-on: windows-latest 97 | steps: 98 | 99 | - shell: bash 100 | run: git config --global core.autocrlf input 101 | 102 | - uses: msys2/setup-msys2@v2 103 | with: 104 | msystem: MINGW64 105 | update: true 106 | install: > 107 | git 108 | make 109 | unzip 110 | mingw-w64-x86_64-go 111 | 112 | - uses: actions/checkout@v4 113 | 114 | - uses: actions/cache@v4 115 | with: 116 | path: ~/go/pkg/mod 117 | key: ${{ runner.os }}-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} 118 | restore-keys: ${{ runner.os }}-${{ matrix.go }}- 119 | 120 | - run: | 121 | export GOBIN=$HOME/go/bin 122 | go install github.com/kyoh86/richgo@latest 123 | go install github.com/mitchellh/gox@latest 124 | 125 | - run: RICHGO_FORCE_COLOR=1 PATH=$HOME/go/bin:$PATH make richtest 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | # Vim files https://github.com/github/gitignore/blob/master/Global/Vim.gitignore 23 | # swap 24 | [._]*.s[a-w][a-z] 25 | [._]s[a-w][a-z] 26 | # session 27 | Session.vim 28 | # temporary 29 | .netrwhist 30 | *~ 31 | # auto-generated tag files 32 | tags 33 | 34 | *.exe 35 | cobra.test 36 | bin 37 | 38 | .idea/ 39 | *.iml 40 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2023 The Cobra Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | version: "2" 16 | 17 | run: 18 | timeout: 5m 19 | 20 | formatters: 21 | enable: 22 | - gofmt 23 | - goimports 24 | 25 | linters: 26 | default: none 27 | enable: 28 | #- bodyclose 29 | #- depguard 30 | #- dogsled 31 | #- dupl 32 | - errcheck 33 | #- exhaustive 34 | #- funlen 35 | #- gochecknoinits 36 | - goconst 37 | - gocritic 38 | #- gocyclo 39 | #- goprintffuncname 40 | - gosec 41 | - govet 42 | - ineffassign 43 | #- lll 44 | - misspell 45 | #- mnd 46 | #- nakedret 47 | #- noctx 48 | - nolintlint 49 | #- rowserrcheck 50 | - staticcheck 51 | - unconvert 52 | #- unparam 53 | - unused 54 | #- whitespace 55 | exclusions: 56 | presets: 57 | - common-false-positives 58 | - legacy 59 | - std-error-handling 60 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Steve Francia 2 | Bjørn Erik Pedersen 3 | Fabiano Franz 4 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Cobra User Contract 2 | 3 | ### Versioning 4 | Cobra will follow a steady release cadence. Non breaking changes will be released as minor versions quarterly. Patch bug releases are at the discretion of the maintainers. Users can expect security patch fixes to be released within relatively short order of a CVE becoming known. For more information on security patch fixes see the CVE section below. Releases will follow [Semantic Versioning](https://semver.org/). Users tracking the Master branch should expect unpredictable breaking changes as the project continues to move forward. For stability, it is highly recommended to use a release. 5 | 6 | ### Backward Compatibility 7 | We will maintain two major releases in a moving window. The N-1 release will only receive bug fixes and security updates and will be dropped once N+1 is released. 8 | 9 | ### Deprecation 10 | Deprecation of Go versions or dependent packages will only occur in major releases. To reduce the change of this taking users by surprise, any large deprecation will be preceded by an announcement in the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) and an Issue on Github. 11 | 12 | ### CVE 13 | Maintainers will make every effort to release security patches in the case of a medium to high severity CVE directly impacting the library. The speed in which these patches reach a release is up to the discretion of the maintainers. A low severity CVE may be a lower priority than a high severity one. 14 | 15 | ### Communication 16 | Cobra maintainers will use GitHub issues and the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) as the primary means of communication with the community. This is to foster open communication with all users and contributors. 17 | 18 | ### Breaking Changes 19 | Breaking changes are generally allowed in the master branch, as this is the branch used to develop the next release of Cobra. 20 | 21 | There may be times, however, when master is closed for breaking changes. This is likely to happen as we near the release of a new version. 22 | 23 | Breaking changes are not allowed in release branches, as these represent minor versions that have already been released. These version have consumers who expect the APIs, behaviors, etc, to remain stable during the lifetime of the patch stream for the minor release. 24 | 25 | Examples of breaking changes include: 26 | - Removing or renaming exported constant, variable, type, or function. 27 | - Updating the version of critical libraries such as `spf13/pflag`, `spf13/viper` etc... 28 | - Some version updates may be acceptable for picking up bug fixes, but maintainers must exercise caution when reviewing. 29 | 30 | There may, at times, need to be exceptions where breaking changes are allowed in release branches. These are at the discretion of the project's maintainers, and must be carefully considered before merging. 31 | 32 | ### CI Testing 33 | Maintainers will ensure the Cobra test suite utilizes the current supported versions of Golang. 34 | 35 | ### Disclaimer 36 | Changes to this document and the contents therein are at the discretion of the maintainers. 37 | None of the contents of this document are legally binding in any way to the maintainers or the users. 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cobra 2 | 3 | Thank you so much for contributing to Cobra. We appreciate your time and help. 4 | Here are some guidelines to help you get started. 5 | 6 | ## Code of Conduct 7 | 8 | Be kind and respectful to the members of the community. Take time to educate 9 | others who are seeking help. Harassment of any kind will not be tolerated. 10 | 11 | ## Questions 12 | 13 | If you have questions regarding Cobra, feel free to ask it in the community 14 | [#cobra Slack channel][cobra-slack] 15 | 16 | ## Filing a bug or feature 17 | 18 | 1. Before filing an issue, please check the existing issues to see if a 19 | similar one was already opened. If there is one already opened, feel free 20 | to comment on it. 21 | 1. If you believe you've found a bug, please provide detailed steps of 22 | reproduction, the version of Cobra and anything else you believe will be 23 | useful to help troubleshoot it (e.g. OS environment, environment variables, 24 | etc...). Also state the current behavior vs. the expected behavior. 25 | 1. If you'd like to see a feature or an enhancement please open an issue with 26 | a clear title and description of what the feature is and why it would be 27 | beneficial to the project and its users. 28 | 29 | ## Submitting changes 30 | 31 | 1. CLA: Upon submitting a Pull Request (PR), contributors will be prompted to 32 | sign a CLA. Please sign the CLA :slightly_smiling_face: 33 | 1. Tests: If you are submitting code, please ensure you have adequate tests 34 | for the feature. Tests can be run via `go test ./...` or `make test`. 35 | 1. Since this is golang project, ensure the new code is properly formatted to 36 | ensure code consistency. Run `make all`. 37 | 38 | ### Quick steps to contribute 39 | 40 | 1. Fork the project. 41 | 1. Download your fork to your PC (`git clone https://github.com/your_username/cobra && cd cobra`) 42 | 1. Create your feature branch (`git checkout -b my-new-feature`) 43 | 1. Make changes and run tests (`make test`) 44 | 1. Add them to staging (`git add .`) 45 | 1. Commit your changes (`git commit -m 'Add some feature'`) 46 | 1. Push to the branch (`git push origin my-new-feature`) 47 | 1. Create new pull request 48 | 49 | 50 | [cobra-slack]: https://gophers.slack.com/archives/CD3LP1199 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | maintainers: 2 | - spf13 3 | - johnSchnake 4 | - jpmcb 5 | - marckhouzam 6 | inactive: 7 | - anthonyfok 8 | - bep 9 | - bogem 10 | - broady 11 | - eparis 12 | - jharshman 13 | - wfernandes 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN="./bin" 2 | SRC=$(shell find . -name "*.go") 3 | 4 | ifeq (, $(shell which golangci-lint)) 5 | $(warning "could not find golangci-lint in $(PATH), run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh") 6 | endif 7 | 8 | .PHONY: fmt lint test install_deps clean 9 | 10 | default: all 11 | 12 | all: fmt test 13 | 14 | fmt: 15 | $(info ******************** checking formatting ********************) 16 | @test -z $(shell gofmt -l $(SRC)) || (gofmt -d $(SRC); exit 1) 17 | 18 | lint: 19 | $(info ******************** running lint tools ********************) 20 | golangci-lint run -v 21 | 22 | test: install_deps 23 | $(info ******************** running tests ********************) 24 | go test -v ./... 25 | 26 | richtest: install_deps 27 | $(info ******************** running tests with kyoh86/richgo ********************) 28 | richgo test -v ./... 29 | 30 | install_deps: 31 | $(info ******************** downloading dependencies ********************) 32 | go get -v ./... 33 | 34 | clean: 35 | rm -rf $(BIN) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![cobra logo](https://github.com/user-attachments/assets/cbc3adf8-0dff-46e9-a88d-5e2d971c169e) 3 | 4 | Cobra is a library for creating powerful modern CLI applications. 5 | 6 | Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/), 7 | [Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to 8 | name a few. [This list](site/content/projects_using_cobra.md) contains a more extensive list of projects using Cobra. 9 | 10 | [![](https://img.shields.io/github/actions/workflow/status/spf13/cobra/test.yml?branch=main&longCache=true&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest) 11 | [![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra) 12 | [![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra) 13 | [![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199) 14 |
15 |
16 | Supported by: 17 |
18 |
19 | 20 | Warp sponsorship 21 | 22 | 23 | ### [Warp, the AI terminal for devs](https://www.warp.dev/cobra) 24 | [Try Cobra in Warp today](https://www.warp.dev/cobra)
25 | 26 |
27 |
28 | 29 | # Overview 30 | 31 | Cobra is a library providing a simple interface to create powerful modern CLI 32 | interfaces similar to git & go tools. 33 | 34 | Cobra provides: 35 | * Easy subcommand-based CLIs: `app server`, `app fetch`, etc. 36 | * Fully POSIX-compliant flags (including short & long versions) 37 | * Nested subcommands 38 | * Global, local and cascading flags 39 | * Intelligent suggestions (`app srver`... did you mean `app server`?) 40 | * Automatic help generation for commands and flags 41 | * Grouping help for subcommands 42 | * Automatic help flag recognition of `-h`, `--help`, etc. 43 | * Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell) 44 | * Automatically generated man pages for your application 45 | * Command aliases so you can change things without breaking them 46 | * The flexibility to define your own help, usage, etc. 47 | * Optional seamless integration with [viper](https://github.com/spf13/viper) for 12-factor apps 48 | 49 | # Concepts 50 | 51 | Cobra is built on a structure of commands, arguments & flags. 52 | 53 | **Commands** represent actions, **Args** are things and **Flags** are modifiers for those actions. 54 | 55 | The best applications read like sentences when used, and as a result, users 56 | intuitively know how to interact with them. 57 | 58 | The pattern to follow is 59 | `APPNAME VERB NOUN --ADJECTIVE` 60 | or 61 | `APPNAME COMMAND ARG --FLAG`. 62 | 63 | A few good real world examples may better illustrate this point. 64 | 65 | In the following example, 'server' is a command, and 'port' is a flag: 66 | 67 | hugo server --port=1313 68 | 69 | In this command we are telling Git to clone the url bare. 70 | 71 | git clone URL --bare 72 | 73 | ## Commands 74 | 75 | Command is the central point of the application. Each interaction that 76 | the application supports will be contained in a Command. A command can 77 | have children commands and optionally run an action. 78 | 79 | In the example above, 'server' is the command. 80 | 81 | [More about cobra.Command](https://pkg.go.dev/github.com/spf13/cobra#Command) 82 | 83 | ## Flags 84 | 85 | A flag is a way to modify the behavior of a command. Cobra supports 86 | fully POSIX-compliant flags as well as the Go [flag package](https://golang.org/pkg/flag/). 87 | A Cobra command can define flags that persist through to children commands 88 | and flags that are only available to that command. 89 | 90 | In the example above, 'port' is the flag. 91 | 92 | Flag functionality is provided by the [pflag 93 | library](https://github.com/spf13/pflag), a fork of the flag standard library 94 | which maintains the same interface while adding POSIX compliance. 95 | 96 | # Installing 97 | Using Cobra is easy. First, use `go get` to install the latest version 98 | of the library. 99 | 100 | ``` 101 | go get -u github.com/spf13/cobra@latest 102 | ``` 103 | 104 | Next, include Cobra in your application: 105 | 106 | ```go 107 | import "github.com/spf13/cobra" 108 | ``` 109 | 110 | # Usage 111 | `cobra-cli` is a command line program to generate cobra applications and command files. 112 | It will bootstrap your application scaffolding to rapidly 113 | develop a Cobra-based application. It is the easiest way to incorporate Cobra into your application. 114 | 115 | It can be installed by running: 116 | 117 | ``` 118 | go install github.com/spf13/cobra-cli@latest 119 | ``` 120 | 121 | For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md) 122 | 123 | For complete details on using the Cobra library, please read [The Cobra User Guide](site/content/user_guide.md). 124 | 125 | # License 126 | 127 | Cobra is released under the Apache 2.0 license. See [LICENSE.txt](LICENSE.txt) 128 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | The `cobra` maintainers take security issues seriously and 6 | we appreciate your efforts to _**responsibly**_ disclose your findings. 7 | We will make every effort to swiftly respond and address concerns. 8 | 9 | To report a security vulnerability: 10 | 11 | 1. **DO NOT** create a public GitHub issue for the vulnerability! 12 | 2. **DO NOT** create a public GitHub Pull Request with a fix for the vulnerability! 13 | 3. Send an email to `cobra-security@googlegroups.com`. 14 | 4. Include the following details in your report: 15 | - Description of the vulnerability 16 | - Steps to reproduce 17 | - Potential impact of the vulnerability (to your downstream project, to the Go ecosystem, etc.) 18 | - Any potential mitigations you've already identified 19 | 5. Allow up to 7 days for an initial response. 20 | You should receive an acknowledgment of your report and an estimated timeline for a fix. 21 | 6. (Optional) If you have a fix and would like to contribute your patch, please work 22 | directly with the maintainers via `cobra-security@googlegroups.com` to 23 | coordinate pushing the patch to GitHub, cutting a new release, and disclosing the change. 24 | 25 | ## Response Process 26 | 27 | When a security vulnerability report is received, the `cobra` maintainers will: 28 | 29 | 1. Confirm receipt of the vulnerability report within 7 days. 30 | 2. Assess the report to determine if it constitutes a security vulnerability. 31 | 3. If confirmed, assign the vulnerability a severity level and create a timeline for addressing it. 32 | 4. Develop and test a fix. 33 | 5. Patch the vulnerability and make a new GitHub release: the maintainers will coordinate disclosure with the reporter. 34 | 6. Create a new GitHub Security Advisory to inform the broader Go ecosystem 35 | 36 | ## Disclosure Policy 37 | 38 | The `cobra` maintainers follow a coordinated disclosure process: 39 | 40 | 1. Security vulnerabilities will be addressed as quickly as possible. 41 | 2. A CVE (Common Vulnerabilities and Exposures) identifier will be requested for significant vulnerabilities 42 | that are within `cobra` itself. 43 | 3. Once a fix is ready, the maintainers will: 44 | - Release a new version containing the fix. 45 | - Update the security advisory with details about the vulnerability. 46 | - Credit the reporter (unless they wish to remain anonymous). 47 | - Credit the fixer (unless they wish to remain anonymous, this may be the same as the reporter). 48 | - Announce the vulnerability through appropriate channels 49 | (GitHub Security Advisory, mailing lists, GitHub Releases, etc.) 50 | 51 | ## Supported Versions 52 | 53 | Security fixes will typically only be released for the most recent major release. 54 | 55 | ## Upstream Security Issues 56 | 57 | `cobra` generally will not accept vulnerability reports that originate in upstream 58 | dependencies. I.e., if there is a problem in Go code that `cobra` depends on, 59 | it is best to engage that project's maintainers and owners. 60 | 61 | This security policy primarily pertains only to `cobra` itself but if you believe you've 62 | identified a problem that originates in an upstream dependency and is being widely 63 | distributed by `cobra`, please follow the disclosure procedure above: the `cobra` 64 | maintainers will work with you to determine the severity and ecosystem impact. 65 | 66 | ## Security Updates and CVEs 67 | 68 | Information about known security vulnerabilities and CVEs affecting `cobra` will 69 | be published as GitHub Security Advisories at 70 | https://github.com/spf13/cobra/security/advisories. 71 | 72 | All users are encouraged to watch the repository and upgrade promptly when 73 | security releases are published. 74 | 75 | ## `cobra` Security Best Practices for Users 76 | 77 | When using `cobra` in your CLIs, the `cobra` maintainers recommend the following: 78 | 79 | 1. Always use the latest version of `cobra`. 80 | 2. [Use Go modules](https://go.dev/blog/using-go-modules) for dependency management. 81 | 3. Always use the latest possible version of Go. 82 | 83 | ## Security Best Practices for Contributors 84 | 85 | When contributing to `cobra`: 86 | 87 | 1. Be mindful of security implications when adding new features or modifying existing ones. 88 | 2. Be aware of `cobra`'s extremely large reach: it is used in nearly every Go CLI 89 | (like Kubernetes, Docker, Prometheus, etc. etc.) 90 | 3. Write tests that explicitly cover edge cases and potential issues. 91 | 4. If you discover a security issue while working on `cobra`, please report it 92 | following the process above rather than opening a public pull request or issue that 93 | addresses the vulnerability. 94 | 5. Take personal sec-ops seriously and secure your GitHub account: use [two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa), 95 | [sign your commits with a GPG or SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification), 96 | etc. 97 | 98 | ## Acknowledgments 99 | 100 | The `cobra` maintainers would like to thank all security researchers and 101 | community members who help keep cobra, its users, and the entire Go ecosystem secure through responsible disclosures!! 102 | 103 | --- 104 | 105 | *This security policy is inspired by the [Open Web Application Security Project (OWASP)](https://owasp.org/) guidelines and security best practices.* 106 | -------------------------------------------------------------------------------- /active_help.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | ) 21 | 22 | const ( 23 | activeHelpMarker = "_activeHelp_ " 24 | // The below values should not be changed: programs will be using them explicitly 25 | // in their user documentation, and users will be using them explicitly. 26 | activeHelpEnvVarSuffix = "ACTIVE_HELP" 27 | activeHelpGlobalEnvVar = configEnvVarGlobalPrefix + "_" + activeHelpEnvVarSuffix 28 | activeHelpGlobalDisable = "0" 29 | ) 30 | 31 | // AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp. 32 | // Such strings will be processed by the completion script and will be shown as ActiveHelp 33 | // to the user. 34 | // The array parameter should be the array that will contain the completions. 35 | // This function can be called multiple times before and/or after completions are added to 36 | // the array. Each time this function is called with the same array, the new 37 | // ActiveHelp line will be shown below the previous ones when completion is triggered. 38 | func AppendActiveHelp(compArray []Completion, activeHelpStr string) []Completion { 39 | return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr)) 40 | } 41 | 42 | // GetActiveHelpConfig returns the value of the ActiveHelp environment variable 43 | // _ACTIVE_HELP where is the name of the root command in upper 44 | // case, with all non-ASCII-alphanumeric characters replaced by `_`. 45 | // It will always return "0" if the global environment variable COBRA_ACTIVE_HELP 46 | // is set to "0". 47 | func GetActiveHelpConfig(cmd *Command) string { 48 | activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar) 49 | if activeHelpCfg != activeHelpGlobalDisable { 50 | activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name())) 51 | } 52 | return activeHelpCfg 53 | } 54 | 55 | // activeHelpEnvVar returns the name of the program-specific ActiveHelp environment 56 | // variable. It has the format _ACTIVE_HELP where is the name of the 57 | // root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`. 58 | func activeHelpEnvVar(name string) string { 59 | return configEnvVar(name, activeHelpEnvVarSuffix) 60 | } 61 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | type PositionalArgs func(cmd *Command, args []string) error 23 | 24 | // legacyArgs validation has the following behaviour: 25 | // - root commands with no subcommands can take arbitrary arguments 26 | // - root commands with subcommands will do subcommand validity checking 27 | // - subcommands will always accept arbitrary arguments 28 | func legacyArgs(cmd *Command, args []string) error { 29 | // no subcommand, always take args 30 | if !cmd.HasSubCommands() { 31 | return nil 32 | } 33 | 34 | // root command with subcommands, do subcommand checking. 35 | if !cmd.HasParent() && len(args) > 0 { 36 | return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) 37 | } 38 | return nil 39 | } 40 | 41 | // NoArgs returns an error if any args are included. 42 | func NoArgs(cmd *Command, args []string) error { 43 | if len(args) > 0 { 44 | return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) 45 | } 46 | return nil 47 | } 48 | 49 | // OnlyValidArgs returns an error if there are any positional args that are not in 50 | // the `ValidArgs` field of `Command` 51 | func OnlyValidArgs(cmd *Command, args []string) error { 52 | if len(cmd.ValidArgs) > 0 { 53 | // Remove any description that may be included in ValidArgs. 54 | // A description is following a tab character. 55 | validArgs := make([]string, 0, len(cmd.ValidArgs)) 56 | for _, v := range cmd.ValidArgs { 57 | validArgs = append(validArgs, strings.SplitN(v, "\t", 2)[0]) 58 | } 59 | for _, v := range args { 60 | if !stringInSlice(v, validArgs) { 61 | return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0])) 62 | } 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | // ArbitraryArgs never returns an error. 69 | func ArbitraryArgs(cmd *Command, args []string) error { 70 | return nil 71 | } 72 | 73 | // MinimumNArgs returns an error if there is not at least N args. 74 | func MinimumNArgs(n int) PositionalArgs { 75 | return func(cmd *Command, args []string) error { 76 | if len(args) < n { 77 | return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args)) 78 | } 79 | return nil 80 | } 81 | } 82 | 83 | // MaximumNArgs returns an error if there are more than N args. 84 | func MaximumNArgs(n int) PositionalArgs { 85 | return func(cmd *Command, args []string) error { 86 | if len(args) > n { 87 | return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args)) 88 | } 89 | return nil 90 | } 91 | } 92 | 93 | // ExactArgs returns an error if there are not exactly n args. 94 | func ExactArgs(n int) PositionalArgs { 95 | return func(cmd *Command, args []string) error { 96 | if len(args) != n { 97 | return fmt.Errorf("accepts %d arg(s), received %d", n, len(args)) 98 | } 99 | return nil 100 | } 101 | } 102 | 103 | // RangeArgs returns an error if the number of args is not within the expected range. 104 | func RangeArgs(min int, max int) PositionalArgs { 105 | return func(cmd *Command, args []string) error { 106 | if len(args) < min || len(args) > max { 107 | return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args)) 108 | } 109 | return nil 110 | } 111 | } 112 | 113 | // MatchAll allows combining several PositionalArgs to work in concert. 114 | func MatchAll(pargs ...PositionalArgs) PositionalArgs { 115 | return func(cmd *Command, args []string) error { 116 | for _, parg := range pargs { 117 | if err := parg(cmd, args); err != nil { 118 | return err 119 | } 120 | } 121 | return nil 122 | } 123 | } 124 | 125 | // ExactValidArgs returns an error if there are not exactly N positional args OR 126 | // there are any positional args that are not in the `ValidArgs` field of `Command` 127 | // 128 | // Deprecated: use MatchAll(ExactArgs(n), OnlyValidArgs) instead 129 | func ExactValidArgs(n int) PositionalArgs { 130 | return MatchAll(ExactArgs(n), OnlyValidArgs) 131 | } 132 | -------------------------------------------------------------------------------- /assets/CobraMain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spf13/cobra/6dec1ae26659a130bdb4c985768d1853b0e1bc06/assets/CobraMain.png -------------------------------------------------------------------------------- /bash_completionsV2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "testing" 21 | ) 22 | 23 | func TestBashCompletionV2WithActiveHelp(t *testing.T) { 24 | c := &Command{Use: "c", Run: emptyRun} 25 | 26 | buf := new(bytes.Buffer) 27 | assertNoErr(t, c.GenBashCompletionV2(buf, true)) 28 | output := buf.String() 29 | 30 | // check that active help is not being disabled 31 | activeHelpVar := activeHelpEnvVar(c.Name()) 32 | checkOmit(t, output, fmt.Sprintf("%s=0", activeHelpVar)) 33 | } 34 | -------------------------------------------------------------------------------- /bash_completions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "regexp" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | func checkOmit(t *testing.T, found, unexpected string) { 28 | if strings.Contains(found, unexpected) { 29 | t.Errorf("Got: %q\nBut should not have!\n", unexpected) 30 | } 31 | } 32 | 33 | func check(t *testing.T, found, expected string) { 34 | if !strings.Contains(found, expected) { 35 | t.Errorf("Expecting to contain: \n %q\nGot:\n %q\n", expected, found) 36 | } 37 | } 38 | 39 | func checkNumOccurrences(t *testing.T, found, expected string, expectedOccurrences int) { 40 | numOccurrences := strings.Count(found, expected) 41 | if numOccurrences != expectedOccurrences { 42 | t.Errorf("Expecting to contain %d occurrences of: \n %q\nGot %d:\n %q\n", expectedOccurrences, expected, numOccurrences, found) 43 | } 44 | } 45 | 46 | func checkRegex(t *testing.T, found, pattern string) { 47 | matched, err := regexp.MatchString(pattern, found) 48 | if err != nil { 49 | t.Errorf("Error thrown performing MatchString: \n %s\n", err) 50 | } 51 | if !matched { 52 | t.Errorf("Expecting to match: \n %q\nGot:\n %q\n", pattern, found) 53 | } 54 | } 55 | 56 | func runShellCheck(s string) error { 57 | cmd := exec.Command("shellcheck", "-s", "bash", "-", "-e", 58 | "SC2034", // PREFIX appears unused. Verify it or export it. 59 | ) 60 | cmd.Stderr = os.Stderr 61 | cmd.Stdout = os.Stdout 62 | 63 | stdin, err := cmd.StdinPipe() 64 | if err != nil { 65 | return err 66 | } 67 | go func() { 68 | _, err := stdin.Write([]byte(s)) 69 | CheckErr(err) 70 | 71 | stdin.Close() 72 | }() 73 | 74 | return cmd.Run() 75 | } 76 | 77 | // World worst custom function, just keep telling you to enter hello! 78 | const bashCompletionFunc = `__root_custom_func() { 79 | COMPREPLY=( "hello" ) 80 | } 81 | ` 82 | 83 | func TestBashCompletions(t *testing.T) { 84 | rootCmd := &Command{ 85 | Use: "root", 86 | ArgAliases: []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"}, 87 | ValidArgs: []string{"pod", "node", "service", "replicationcontroller"}, 88 | BashCompletionFunction: bashCompletionFunc, 89 | Run: emptyRun, 90 | } 91 | rootCmd.Flags().IntP("introot", "i", -1, "help message for flag introot") 92 | assertNoErr(t, rootCmd.MarkFlagRequired("introot")) 93 | 94 | // Filename. 95 | rootCmd.Flags().String("filename", "", "Enter a filename") 96 | assertNoErr(t, rootCmd.MarkFlagFilename("filename", "json", "yaml", "yml")) 97 | 98 | // Persistent filename. 99 | rootCmd.PersistentFlags().String("persistent-filename", "", "Enter a filename") 100 | assertNoErr(t, rootCmd.MarkPersistentFlagFilename("persistent-filename")) 101 | assertNoErr(t, rootCmd.MarkPersistentFlagRequired("persistent-filename")) 102 | 103 | // Filename extensions. 104 | rootCmd.Flags().String("filename-ext", "", "Enter a filename (extension limited)") 105 | assertNoErr(t, rootCmd.MarkFlagFilename("filename-ext")) 106 | rootCmd.Flags().String("custom", "", "Enter a filename (extension limited)") 107 | assertNoErr(t, rootCmd.MarkFlagCustom("custom", "__complete_custom")) 108 | 109 | // Subdirectories in a given directory. 110 | rootCmd.Flags().String("theme", "", "theme to use (located in /themes/THEMENAME/)") 111 | assertNoErr(t, rootCmd.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"})) 112 | 113 | // For two word flags check 114 | rootCmd.Flags().StringP("two", "t", "", "this is two word flags") 115 | rootCmd.Flags().BoolP("two-w-default", "T", false, "this is not two word flags") 116 | 117 | echoCmd := &Command{ 118 | Use: "echo [string to echo]", 119 | Aliases: []string{"say"}, 120 | Short: "Echo anything to the screen", 121 | Long: "an utterly useless command for testing.", 122 | Example: "Just run cobra-test echo", 123 | Run: emptyRun, 124 | } 125 | 126 | echoCmd.Flags().String("filename", "", "Enter a filename") 127 | assertNoErr(t, echoCmd.MarkFlagFilename("filename", "json", "yaml", "yml")) 128 | echoCmd.Flags().String("config", "", "config to use (located in /config/PROFILE/)") 129 | assertNoErr(t, echoCmd.Flags().SetAnnotation("config", BashCompSubdirsInDir, []string{"config"})) 130 | 131 | printCmd := &Command{ 132 | Use: "print [string to print]", 133 | Args: MinimumNArgs(1), 134 | Short: "Print anything to the screen", 135 | Long: "an absolutely utterly useless command for testing.", 136 | Run: emptyRun, 137 | } 138 | 139 | deprecatedCmd := &Command{ 140 | Use: "deprecated [can't do anything here]", 141 | Args: NoArgs, 142 | Short: "A command which is deprecated", 143 | Long: "an absolutely utterly useless command for testing deprecation!.", 144 | Deprecated: "Please use echo instead", 145 | Run: emptyRun, 146 | } 147 | 148 | colonCmd := &Command{ 149 | Use: "cmd:colon", 150 | Run: emptyRun, 151 | } 152 | 153 | timesCmd := &Command{ 154 | Use: "times [# times] [string to echo]", 155 | SuggestFor: []string{"counts"}, 156 | Args: OnlyValidArgs, 157 | ValidArgs: []string{"one", "two", "three", "four"}, 158 | Short: "Echo anything to the screen more times", 159 | Long: "a slightly useless command for testing.", 160 | Run: emptyRun, 161 | } 162 | 163 | echoCmd.AddCommand(timesCmd) 164 | rootCmd.AddCommand(echoCmd, printCmd, deprecatedCmd, colonCmd) 165 | 166 | buf := new(bytes.Buffer) 167 | assertNoErr(t, rootCmd.GenBashCompletion(buf)) 168 | output := buf.String() 169 | 170 | check(t, output, "_root") 171 | check(t, output, "_root_echo") 172 | check(t, output, "_root_echo_times") 173 | check(t, output, "_root_print") 174 | check(t, output, "_root_cmd__colon") 175 | 176 | // check for required flags 177 | check(t, output, `must_have_one_flag+=("--introot=")`) 178 | check(t, output, `must_have_one_flag+=("--persistent-filename=")`) 179 | // check for custom completion function with both qualified and unqualified name 180 | checkNumOccurrences(t, output, `__custom_func`, 2) // 1. check existence, 2. invoke 181 | checkNumOccurrences(t, output, `__root_custom_func`, 3) // 1. check existence, 2. invoke, 3. actual definition 182 | // check for custom completion function body 183 | check(t, output, `COMPREPLY=( "hello" )`) 184 | // check for required nouns 185 | check(t, output, `must_have_one_noun+=("pod")`) 186 | // check for noun aliases 187 | check(t, output, `noun_aliases+=("pods")`) 188 | check(t, output, `noun_aliases+=("rc")`) 189 | checkOmit(t, output, `must_have_one_noun+=("pods")`) 190 | // check for filename extension flags 191 | check(t, output, `flags_completion+=("_filedir")`) 192 | // check for filename extension flags 193 | check(t, output, `must_have_one_noun+=("three")`) 194 | // check for filename extension flags 195 | check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_filename_extension_flag json|yaml|yml")`, rootCmd.Name())) 196 | // check for filename extension flags in a subcommand 197 | checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_filename_extension_flag json\|yaml\|yml"\)`, rootCmd.Name())) 198 | // check for custom flags 199 | check(t, output, `flags_completion+=("__complete_custom")`) 200 | // check for subdirs_in_dir flags 201 | check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_subdirs_in_dir_flag themes")`, rootCmd.Name())) 202 | // check for subdirs_in_dir flags in a subcommand 203 | checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_subdirs_in_dir_flag config"\)`, rootCmd.Name())) 204 | 205 | // check two word flags 206 | check(t, output, `two_word_flags+=("--two")`) 207 | check(t, output, `two_word_flags+=("-t")`) 208 | checkOmit(t, output, `two_word_flags+=("--two-w-default")`) 209 | checkOmit(t, output, `two_word_flags+=("-T")`) 210 | 211 | // check local nonpersistent flag 212 | check(t, output, `local_nonpersistent_flags+=("--two")`) 213 | check(t, output, `local_nonpersistent_flags+=("--two=")`) 214 | check(t, output, `local_nonpersistent_flags+=("-t")`) 215 | check(t, output, `local_nonpersistent_flags+=("--two-w-default")`) 216 | check(t, output, `local_nonpersistent_flags+=("-T")`) 217 | 218 | checkOmit(t, output, deprecatedCmd.Name()) 219 | 220 | // If available, run shellcheck against the script. 221 | if err := exec.Command("which", "shellcheck").Run(); err != nil { 222 | return 223 | } 224 | if err := runShellCheck(output); err != nil { 225 | t.Fatalf("shellcheck failed: %v", err) 226 | } 227 | } 228 | 229 | func TestBashCompletionHiddenFlag(t *testing.T) { 230 | c := &Command{Use: "c", Run: emptyRun} 231 | 232 | const flagName = "hiddenFlag" 233 | c.Flags().Bool(flagName, false, "") 234 | assertNoErr(t, c.Flags().MarkHidden(flagName)) 235 | 236 | buf := new(bytes.Buffer) 237 | assertNoErr(t, c.GenBashCompletion(buf)) 238 | output := buf.String() 239 | 240 | if strings.Contains(output, flagName) { 241 | t.Errorf("Expected completion to not include %q flag: Got %v", flagName, output) 242 | } 243 | } 244 | 245 | func TestBashCompletionDeprecatedFlag(t *testing.T) { 246 | c := &Command{Use: "c", Run: emptyRun} 247 | 248 | const flagName = "deprecated-flag" 249 | c.Flags().Bool(flagName, false, "") 250 | assertNoErr(t, c.Flags().MarkDeprecated(flagName, "use --not-deprecated instead")) 251 | 252 | buf := new(bytes.Buffer) 253 | assertNoErr(t, c.GenBashCompletion(buf)) 254 | output := buf.String() 255 | 256 | if strings.Contains(output, flagName) { 257 | t.Errorf("expected completion to not include %q flag: Got %v", flagName, output) 258 | } 259 | } 260 | 261 | func TestBashCompletionTraverseChildren(t *testing.T) { 262 | c := &Command{Use: "c", Run: emptyRun, TraverseChildren: true} 263 | 264 | c.Flags().StringP("string-flag", "s", "", "string flag") 265 | c.Flags().BoolP("bool-flag", "b", false, "bool flag") 266 | 267 | buf := new(bytes.Buffer) 268 | assertNoErr(t, c.GenBashCompletion(buf)) 269 | output := buf.String() 270 | 271 | // check that local nonpersistent flag are not set since we have TraverseChildren set to true 272 | checkOmit(t, output, `local_nonpersistent_flags+=("--string-flag")`) 273 | checkOmit(t, output, `local_nonpersistent_flags+=("--string-flag=")`) 274 | checkOmit(t, output, `local_nonpersistent_flags+=("-s")`) 275 | checkOmit(t, output, `local_nonpersistent_flags+=("--bool-flag")`) 276 | checkOmit(t, output, `local_nonpersistent_flags+=("-b")`) 277 | } 278 | 279 | func TestBashCompletionNoActiveHelp(t *testing.T) { 280 | c := &Command{Use: "c", Run: emptyRun} 281 | 282 | buf := new(bytes.Buffer) 283 | assertNoErr(t, c.GenBashCompletion(buf)) 284 | output := buf.String() 285 | 286 | // check that active help is being disabled 287 | activeHelpVar := activeHelpEnvVar(c.Name()) 288 | check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) 289 | } 290 | -------------------------------------------------------------------------------- /cobra.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Commands similar to git, go tools and other modern CLI tools 16 | // inspired by go, go-Commander, gh and subcommand 17 | 18 | package cobra 19 | 20 | import ( 21 | "fmt" 22 | "io" 23 | "os" 24 | "reflect" 25 | "strconv" 26 | "strings" 27 | "text/template" 28 | "time" 29 | "unicode" 30 | ) 31 | 32 | var templateFuncs = template.FuncMap{ 33 | "trim": strings.TrimSpace, 34 | "trimRightSpace": trimRightSpace, 35 | "trimTrailingWhitespaces": trimRightSpace, 36 | "appendIfNotPresent": appendIfNotPresent, 37 | "rpad": rpad, 38 | "gt": Gt, 39 | "eq": Eq, 40 | } 41 | 42 | var initializers []func() 43 | var finalizers []func() 44 | 45 | const ( 46 | defaultPrefixMatching = false 47 | defaultCommandSorting = true 48 | defaultCaseInsensitive = false 49 | defaultTraverseRunHooks = false 50 | ) 51 | 52 | // EnablePrefixMatching allows setting automatic prefix matching. Automatic prefix matching can be a dangerous thing 53 | // to automatically enable in CLI tools. 54 | // Set this to true to enable it. 55 | var EnablePrefixMatching = defaultPrefixMatching 56 | 57 | // EnableCommandSorting controls sorting of the slice of commands, which is turned on by default. 58 | // To disable sorting, set it to false. 59 | var EnableCommandSorting = defaultCommandSorting 60 | 61 | // EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default) 62 | var EnableCaseInsensitive = defaultCaseInsensitive 63 | 64 | // EnableTraverseRunHooks executes persistent pre-run and post-run hooks from all parents. 65 | // By default this is disabled, which means only the first run hook to be found is executed. 66 | var EnableTraverseRunHooks = defaultTraverseRunHooks 67 | 68 | // MousetrapHelpText enables an information splash screen on Windows 69 | // if the CLI is started from explorer.exe. 70 | // To disable the mousetrap, just set this variable to blank string (""). 71 | // Works only on Microsoft Windows. 72 | var MousetrapHelpText = `This is a command line tool. 73 | 74 | You need to open cmd.exe and run it from there. 75 | ` 76 | 77 | // MousetrapDisplayDuration controls how long the MousetrapHelpText message is displayed on Windows 78 | // if the CLI is started from explorer.exe. Set to 0 to wait for the return key to be pressed. 79 | // To disable the mousetrap, just set MousetrapHelpText to blank string (""). 80 | // Works only on Microsoft Windows. 81 | var MousetrapDisplayDuration = 5 * time.Second 82 | 83 | // AddTemplateFunc adds a template function that's available to Usage and Help 84 | // template generation. 85 | func AddTemplateFunc(name string, tmplFunc interface{}) { 86 | templateFuncs[name] = tmplFunc 87 | } 88 | 89 | // AddTemplateFuncs adds multiple template functions that are available to Usage and 90 | // Help template generation. 91 | func AddTemplateFuncs(tmplFuncs template.FuncMap) { 92 | for k, v := range tmplFuncs { 93 | templateFuncs[k] = v 94 | } 95 | } 96 | 97 | // OnInitialize sets the passed functions to be run when each command's 98 | // Execute method is called. 99 | func OnInitialize(y ...func()) { 100 | initializers = append(initializers, y...) 101 | } 102 | 103 | // OnFinalize sets the passed functions to be run when each command's 104 | // Execute method is terminated. 105 | func OnFinalize(y ...func()) { 106 | finalizers = append(finalizers, y...) 107 | } 108 | 109 | // FIXME Gt is unused by cobra and should be removed in a version 2. It exists only for compatibility with users of cobra. 110 | 111 | // Gt takes two types and checks whether the first type is greater than the second. In case of types Arrays, Chans, 112 | // Maps and Slices, Gt will compare their lengths. Ints are compared directly while strings are first parsed as 113 | // ints and then compared. 114 | func Gt(a interface{}, b interface{}) bool { 115 | var left, right int64 116 | av := reflect.ValueOf(a) 117 | 118 | switch av.Kind() { 119 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: 120 | left = int64(av.Len()) 121 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 122 | left = av.Int() 123 | case reflect.String: 124 | left, _ = strconv.ParseInt(av.String(), 10, 64) 125 | } 126 | 127 | bv := reflect.ValueOf(b) 128 | 129 | switch bv.Kind() { 130 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: 131 | right = int64(bv.Len()) 132 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 133 | right = bv.Int() 134 | case reflect.String: 135 | right, _ = strconv.ParseInt(bv.String(), 10, 64) 136 | } 137 | 138 | return left > right 139 | } 140 | 141 | // FIXME Eq is unused by cobra and should be removed in a version 2. It exists only for compatibility with users of cobra. 142 | 143 | // Eq takes two types and checks whether they are equal. Supported types are int and string. Unsupported types will panic. 144 | func Eq(a interface{}, b interface{}) bool { 145 | av := reflect.ValueOf(a) 146 | bv := reflect.ValueOf(b) 147 | 148 | switch av.Kind() { 149 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: 150 | panic("Eq called on unsupported type") 151 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 152 | return av.Int() == bv.Int() 153 | case reflect.String: 154 | return av.String() == bv.String() 155 | } 156 | return false 157 | } 158 | 159 | func trimRightSpace(s string) string { 160 | return strings.TrimRightFunc(s, unicode.IsSpace) 161 | } 162 | 163 | // FIXME appendIfNotPresent is unused by cobra and should be removed in a version 2. It exists only for compatibility with users of cobra. 164 | 165 | // appendIfNotPresent will append stringToAppend to the end of s, but only if it's not yet present in s. 166 | func appendIfNotPresent(s, stringToAppend string) string { 167 | if strings.Contains(s, stringToAppend) { 168 | return s 169 | } 170 | return s + " " + stringToAppend 171 | } 172 | 173 | // rpad adds padding to the right of a string. 174 | func rpad(s string, padding int) string { 175 | formattedString := fmt.Sprintf("%%-%ds", padding) 176 | return fmt.Sprintf(formattedString, s) 177 | } 178 | 179 | func tmpl(text string) *tmplFunc { 180 | return &tmplFunc{ 181 | tmpl: text, 182 | fn: func(w io.Writer, data interface{}) error { 183 | t := template.New("top") 184 | t.Funcs(templateFuncs) 185 | template.Must(t.Parse(text)) 186 | return t.Execute(w, data) 187 | }, 188 | } 189 | } 190 | 191 | // ld compares two strings and returns the levenshtein distance between them. 192 | func ld(s, t string, ignoreCase bool) int { 193 | if ignoreCase { 194 | s = strings.ToLower(s) 195 | t = strings.ToLower(t) 196 | } 197 | d := make([][]int, len(s)+1) 198 | for i := range d { 199 | d[i] = make([]int, len(t)+1) 200 | d[i][0] = i 201 | } 202 | for j := range d[0] { 203 | d[0][j] = j 204 | } 205 | for j := 1; j <= len(t); j++ { 206 | for i := 1; i <= len(s); i++ { 207 | if s[i-1] == t[j-1] { 208 | d[i][j] = d[i-1][j-1] 209 | } else { 210 | min := d[i-1][j] 211 | if d[i][j-1] < min { 212 | min = d[i][j-1] 213 | } 214 | if d[i-1][j-1] < min { 215 | min = d[i-1][j-1] 216 | } 217 | d[i][j] = min + 1 218 | } 219 | } 220 | 221 | } 222 | return d[len(s)][len(t)] 223 | } 224 | 225 | func stringInSlice(a string, list []string) bool { 226 | for _, b := range list { 227 | if b == a { 228 | return true 229 | } 230 | } 231 | return false 232 | } 233 | 234 | // CheckErr prints the msg with the prefix 'Error:' and exits with error code 1. If the msg is nil, it does nothing. 235 | func CheckErr(msg interface{}) { 236 | if msg != nil { 237 | fmt.Fprintln(os.Stderr, "Error:", msg) 238 | os.Exit(1) 239 | } 240 | } 241 | 242 | // WriteStringAndCheck writes a string into a buffer, and checks if the error is not nil. 243 | func WriteStringAndCheck(b io.StringWriter, s string) { 244 | _, err := b.WriteString(s) 245 | CheckErr(err) 246 | } 247 | -------------------------------------------------------------------------------- /cobra_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "os" 19 | "os/exec" 20 | "path/filepath" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | "text/template" 25 | ) 26 | 27 | func assertNoErr(t *testing.T, e error) { 28 | if e != nil { 29 | t.Error(e) 30 | } 31 | } 32 | 33 | func TestAddTemplateFunctions(t *testing.T) { 34 | AddTemplateFunc("t", func() bool { return true }) 35 | AddTemplateFuncs(template.FuncMap{ 36 | "f": func() bool { return false }, 37 | "h": func() string { return "Hello," }, 38 | "w": func() string { return "world." }}) 39 | 40 | c := &Command{} 41 | c.SetUsageTemplate(`{{if t}}{{h}}{{end}}{{if f}}{{h}}{{end}} {{w}}`) 42 | 43 | const expected = "Hello, world." 44 | if got := c.UsageString(); got != expected { 45 | t.Errorf("Expected UsageString: %v\nGot: %v", expected, got) 46 | } 47 | } 48 | 49 | func TestLevenshteinDistance(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | s string 53 | t string 54 | ignoreCase bool 55 | expected int 56 | }{ 57 | { 58 | name: "Equal strings (case-sensitive)", 59 | s: "hello", 60 | t: "hello", 61 | ignoreCase: false, 62 | expected: 0, 63 | }, 64 | { 65 | name: "Equal strings (case-insensitive)", 66 | s: "Hello", 67 | t: "hello", 68 | ignoreCase: true, 69 | expected: 0, 70 | }, 71 | { 72 | name: "Different strings (case-sensitive)", 73 | s: "kitten", 74 | t: "sitting", 75 | ignoreCase: false, 76 | expected: 3, 77 | }, 78 | { 79 | name: "Different strings (case-insensitive)", 80 | s: "Kitten", 81 | t: "Sitting", 82 | ignoreCase: true, 83 | expected: 3, 84 | }, 85 | { 86 | name: "Empty strings", 87 | s: "", 88 | t: "", 89 | ignoreCase: false, 90 | expected: 0, 91 | }, 92 | { 93 | name: "One empty string", 94 | s: "abc", 95 | t: "", 96 | ignoreCase: false, 97 | expected: 3, 98 | }, 99 | { 100 | name: "Both empty strings", 101 | s: "", 102 | t: "", 103 | ignoreCase: true, 104 | expected: 0, 105 | }, 106 | } 107 | 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | // Act 111 | got := ld(tt.s, tt.t, tt.ignoreCase) 112 | 113 | // Assert 114 | if got != tt.expected { 115 | t.Errorf("Expected ld: %v\nGot: %v", tt.expected, got) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func TestStringInSlice(t *testing.T) { 122 | tests := []struct { 123 | name string 124 | a string 125 | list []string 126 | expected bool 127 | }{ 128 | { 129 | name: "String in slice (case-sensitive)", 130 | a: "apple", 131 | list: []string{"orange", "banana", "apple", "grape"}, 132 | expected: true, 133 | }, 134 | { 135 | name: "String not in slice (case-sensitive)", 136 | a: "pear", 137 | list: []string{"orange", "banana", "apple", "grape"}, 138 | expected: false, 139 | }, 140 | { 141 | name: "String in slice (case-insensitive)", 142 | a: "APPLE", 143 | list: []string{"orange", "banana", "apple", "grape"}, 144 | expected: false, 145 | }, 146 | { 147 | name: "Empty slice", 148 | a: "apple", 149 | list: []string{}, 150 | expected: false, 151 | }, 152 | { 153 | name: "Empty string", 154 | a: "", 155 | list: []string{"orange", "banana", "apple", "grape"}, 156 | expected: false, 157 | }, 158 | { 159 | name: "Empty strings match", 160 | a: "", 161 | list: []string{"orange", ""}, 162 | expected: true, 163 | }, 164 | { 165 | name: "Empty string in empty slice", 166 | a: "", 167 | list: []string{}, 168 | expected: false, 169 | }, 170 | } 171 | 172 | for _, tt := range tests { 173 | t.Run(tt.name, func(t *testing.T) { 174 | // Act 175 | got := stringInSlice(tt.a, tt.list) 176 | 177 | // Assert 178 | if got != tt.expected { 179 | t.Errorf("Expected stringInSlice: %v\nGot: %v", tt.expected, got) 180 | } 181 | }) 182 | } 183 | } 184 | 185 | func TestRpad(t *testing.T) { 186 | tests := []struct { 187 | name string 188 | inputString string 189 | padding int 190 | expected string 191 | }{ 192 | { 193 | name: "Padding required", 194 | inputString: "Hello", 195 | padding: 10, 196 | expected: "Hello ", 197 | }, 198 | { 199 | name: "No padding required", 200 | inputString: "World", 201 | padding: 5, 202 | expected: "World", 203 | }, 204 | { 205 | name: "Empty string", 206 | inputString: "", 207 | padding: 8, 208 | expected: " ", 209 | }, 210 | { 211 | name: "Zero padding", 212 | inputString: "cobra", 213 | padding: 0, 214 | expected: "cobra", 215 | }, 216 | } 217 | 218 | for _, tt := range tests { 219 | t.Run(tt.name, func(t *testing.T) { 220 | // Act 221 | got := rpad(tt.inputString, tt.padding) 222 | 223 | // Assert 224 | if got != tt.expected { 225 | t.Errorf("Expected rpad: %v\nGot: %v", tt.expected, got) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | // TestDeadcodeElimination checks that a simple program using cobra in its 232 | // default configuration is linked taking full advantage of the linker's 233 | // deadcode elimination step. 234 | // 235 | // If reflect.Value.MethodByName/reflect.Value.Method are reachable the 236 | // linker will not always be able to prove that exported methods are 237 | // unreachable, making deadcode elimination less effective. Using 238 | // text/template and html/template makes reflect.Value.MethodByName 239 | // reachable. 240 | // Since cobra can use text/template templates this test checks that in its 241 | // default configuration that code path can be proven to be unreachable by 242 | // the linker. 243 | // 244 | // See also: https://github.com/spf13/cobra/pull/1956 245 | func TestDeadcodeElimination(t *testing.T) { 246 | if runtime.GOOS == "windows" { 247 | t.Skip("go tool nm fails on windows") 248 | } 249 | 250 | // check that a simple program using cobra in its default configuration is 251 | // linked with deadcode elimination enabled. 252 | const ( 253 | dirname = "test_deadcode" 254 | progname = "test_deadcode_elimination" 255 | ) 256 | _ = os.Mkdir(dirname, 0770) 257 | defer os.RemoveAll(dirname) 258 | filename := filepath.Join(dirname, progname+".go") 259 | err := os.WriteFile(filename, []byte(`package main 260 | 261 | import ( 262 | "fmt" 263 | "os" 264 | 265 | "github.com/spf13/cobra" 266 | ) 267 | 268 | var rootCmd = &cobra.Command{ 269 | Version: "1.0", 270 | Use: "example_program", 271 | Short: "example_program - test fixture to check that deadcode elimination is allowed", 272 | Run: func(cmd *cobra.Command, args []string) { 273 | fmt.Println("hello world") 274 | }, 275 | Aliases: []string{"alias1", "alias2"}, 276 | Example: "stringer --help", 277 | } 278 | 279 | func main() { 280 | if err := rootCmd.Execute(); err != nil { 281 | fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err) 282 | os.Exit(1) 283 | } 284 | } 285 | `), 0600) 286 | if err != nil { 287 | t.Fatalf("could not write test program: %v", err) 288 | } 289 | buf, err := exec.Command("go", "build", filename).CombinedOutput() 290 | if err != nil { 291 | t.Fatalf("could not compile test program: %s", string(buf)) 292 | } 293 | defer os.Remove(progname) 294 | buf, err = exec.Command("go", "tool", "nm", progname).CombinedOutput() 295 | if err != nil { 296 | t.Fatalf("could not run go tool nm: %v", err) 297 | } 298 | if strings.Contains(string(buf), "MethodByName") { 299 | t.Error("compiled programs contains MethodByName symbol") 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /command_notwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !windows 16 | // +build !windows 17 | 18 | package cobra 19 | 20 | var preExecHookFn func(*Command) 21 | -------------------------------------------------------------------------------- /command_win.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build windows 16 | // +build windows 17 | 18 | package cobra 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "time" 24 | 25 | "github.com/inconshreveable/mousetrap" 26 | ) 27 | 28 | var preExecHookFn = preExecHook 29 | 30 | func preExecHook(c *Command) { 31 | if MousetrapHelpText != "" && mousetrap.StartedByExplorer() { 32 | c.Print(MousetrapHelpText) 33 | if MousetrapDisplayDuration > 0 { 34 | time.Sleep(MousetrapDisplayDuration) 35 | } else { 36 | c.Println("Press return to continue...") 37 | fmt.Scanln() 38 | } 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /doc/cmd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | func emptyRun(*cobra.Command, []string) {} 25 | 26 | func init() { 27 | rootCmd.PersistentFlags().StringP("rootflag", "r", "two", "") 28 | rootCmd.PersistentFlags().StringP("strtwo", "t", "two", "help message for parent flag strtwo") 29 | 30 | echoCmd.PersistentFlags().StringP("strone", "s", "one", "help message for flag strone") 31 | echoCmd.PersistentFlags().BoolP("persistentbool", "p", false, "help message for flag persistentbool") 32 | echoCmd.Flags().IntP("intone", "i", 123, "help message for flag intone") 33 | echoCmd.Flags().BoolP("boolone", "b", true, "help message for flag boolone") 34 | 35 | timesCmd.PersistentFlags().StringP("strtwo", "t", "2", "help message for child flag strtwo") 36 | timesCmd.Flags().IntP("inttwo", "j", 234, "help message for flag inttwo") 37 | timesCmd.Flags().BoolP("booltwo", "c", false, "help message for flag booltwo") 38 | 39 | printCmd.PersistentFlags().StringP("strthree", "s", "three", "help message for flag strthree") 40 | printCmd.Flags().IntP("intthree", "i", 345, "help message for flag intthree") 41 | printCmd.Flags().BoolP("boolthree", "b", true, "help message for flag boolthree") 42 | 43 | echoCmd.AddCommand(timesCmd, echoSubCmd, deprecatedCmd) 44 | rootCmd.AddCommand(printCmd, echoCmd, dummyCmd) 45 | } 46 | 47 | var rootCmd = &cobra.Command{ 48 | Use: "root", 49 | Short: "Root short description", 50 | Long: "Root long description", 51 | Run: emptyRun, 52 | } 53 | 54 | var echoCmd = &cobra.Command{ 55 | Use: "echo [string to echo]", 56 | Aliases: []string{"say"}, 57 | Short: "Echo anything to the screen", 58 | Long: "an utterly useless command for testing", 59 | Example: "Just run cobra-test echo", 60 | } 61 | 62 | var echoSubCmd = &cobra.Command{ 63 | Use: "echosub [string to print]", 64 | Short: "second sub command for echo", 65 | Long: "an absolutely utterly useless command for testing gendocs!.", 66 | Run: emptyRun, 67 | } 68 | 69 | var timesCmd = &cobra.Command{ 70 | Use: "times [# times] [string to echo]", 71 | SuggestFor: []string{"counts"}, 72 | Short: "Echo anything to the screen more times", 73 | Long: `a slightly useless command for testing.`, 74 | Run: emptyRun, 75 | } 76 | 77 | var deprecatedCmd = &cobra.Command{ 78 | Use: "deprecated [can't do anything here]", 79 | Short: "A command which is deprecated", 80 | Long: `an absolutely utterly useless command for testing deprecation!.`, 81 | Deprecated: "Please use echo instead", 82 | } 83 | 84 | var printCmd = &cobra.Command{ 85 | Use: "print [string to print]", 86 | Short: "Print anything to the screen", 87 | Long: `an absolutely utterly useless command for testing.`, 88 | } 89 | 90 | var dummyCmd = &cobra.Command{ 91 | Use: "dummy [action]", 92 | Short: "Performs a dummy action", 93 | } 94 | 95 | func checkStringContains(t *testing.T, got, expected string) { 96 | if !strings.Contains(got, expected) { 97 | t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got) 98 | } 99 | } 100 | 101 | func checkStringOmits(t *testing.T, got, expected string) { 102 | if strings.Contains(got, expected) { 103 | t.Errorf("Expected to not contain: \n %v\nGot: %v", expected, got) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /doc/man_docs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "sort" 24 | "strconv" 25 | "strings" 26 | "time" 27 | 28 | "github.com/cpuguy83/go-md2man/v2/md2man" 29 | "github.com/spf13/cobra" 30 | "github.com/spf13/pflag" 31 | ) 32 | 33 | // GenManTree will generate a man page for this command and all descendants 34 | // in the directory given. The header may be nil. This function may not work 35 | // correctly if your command names have `-` in them. If you have `cmd` with two 36 | // subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` 37 | // it is undefined which help output will be in the file `cmd-sub-third.1`. 38 | func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error { 39 | return GenManTreeFromOpts(cmd, GenManTreeOptions{ 40 | Header: header, 41 | Path: dir, 42 | CommandSeparator: "-", 43 | }) 44 | } 45 | 46 | // GenManTreeFromOpts generates a man page for the command and all descendants. 47 | // The pages are written to the opts.Path directory. 48 | func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error { 49 | header := opts.Header 50 | if header == nil { 51 | header = &GenManHeader{} 52 | } 53 | for _, c := range cmd.Commands() { 54 | if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 55 | continue 56 | } 57 | if err := GenManTreeFromOpts(c, opts); err != nil { 58 | return err 59 | } 60 | } 61 | section := "1" 62 | if header.Section != "" { 63 | section = header.Section 64 | } 65 | 66 | separator := "_" 67 | if opts.CommandSeparator != "" { 68 | separator = opts.CommandSeparator 69 | } 70 | basename := strings.ReplaceAll(cmd.CommandPath(), " ", separator) 71 | filename := filepath.Join(opts.Path, basename+"."+section) 72 | f, err := os.Create(filename) 73 | if err != nil { 74 | return err 75 | } 76 | defer f.Close() 77 | 78 | headerCopy := *header 79 | return GenMan(cmd, &headerCopy, f) 80 | } 81 | 82 | // GenManTreeOptions is the options for generating the man pages. 83 | // Used only in GenManTreeFromOpts. 84 | type GenManTreeOptions struct { 85 | Header *GenManHeader 86 | Path string 87 | CommandSeparator string 88 | } 89 | 90 | // GenManHeader is a lot like the .TH header at the start of man pages. These 91 | // include the title, section, date, source, and manual. We will use the 92 | // current time if Date is unset and will use "Auto generated by spf13/cobra" 93 | // if the Source is unset. 94 | type GenManHeader struct { 95 | Title string 96 | Section string 97 | Date *time.Time 98 | date string 99 | Source string 100 | Manual string 101 | } 102 | 103 | // GenMan will generate a man page for the given command and write it to 104 | // w. The header argument may be nil, however obviously w may not. 105 | func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error { 106 | if header == nil { 107 | header = &GenManHeader{} 108 | } 109 | if err := fillHeader(header, cmd.CommandPath(), cmd.DisableAutoGenTag); err != nil { 110 | return err 111 | } 112 | 113 | b := genMan(cmd, header) 114 | _, err := w.Write(md2man.Render(b)) 115 | return err 116 | } 117 | 118 | func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error { 119 | if header.Title == "" { 120 | header.Title = strings.ToUpper(strings.ReplaceAll(name, " ", "\\-")) 121 | } 122 | if header.Section == "" { 123 | header.Section = "1" 124 | } 125 | if header.Date == nil { 126 | now := time.Now() 127 | if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { 128 | unixEpoch, err := strconv.ParseInt(epoch, 10, 64) 129 | if err != nil { 130 | return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err) 131 | } 132 | now = time.Unix(unixEpoch, 0) 133 | } 134 | header.Date = &now 135 | } 136 | header.date = header.Date.Format("Jan 2006") 137 | if header.Source == "" && !disableAutoGen { 138 | header.Source = "Auto generated by spf13/cobra" 139 | } 140 | return nil 141 | } 142 | 143 | func manPreamble(buf io.StringWriter, header *GenManHeader, cmd *cobra.Command, dashedName string) { 144 | description := cmd.Long 145 | if len(description) == 0 { 146 | description = cmd.Short 147 | } 148 | 149 | cobra.WriteStringAndCheck(buf, fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s" 150 | # NAME 151 | `, header.Title, header.Section, header.date, header.Source, header.Manual)) 152 | cobra.WriteStringAndCheck(buf, fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short)) 153 | cobra.WriteStringAndCheck(buf, "# SYNOPSIS\n") 154 | cobra.WriteStringAndCheck(buf, fmt.Sprintf("**%s**\n\n", cmd.UseLine())) 155 | cobra.WriteStringAndCheck(buf, "# DESCRIPTION\n") 156 | cobra.WriteStringAndCheck(buf, description+"\n\n") 157 | } 158 | 159 | func manPrintFlags(buf io.StringWriter, flags *pflag.FlagSet) { 160 | flags.VisitAll(func(flag *pflag.Flag) { 161 | if len(flag.Deprecated) > 0 || flag.Hidden { 162 | return 163 | } 164 | format := "" 165 | if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 { 166 | format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name) 167 | } else { 168 | format = fmt.Sprintf("**--%s**", flag.Name) 169 | } 170 | if len(flag.NoOptDefVal) > 0 { 171 | format += "[" 172 | } 173 | if flag.Value.Type() == "string" { 174 | // put quotes on the value 175 | format += "=%q" 176 | } else { 177 | format += "=%s" 178 | } 179 | if len(flag.NoOptDefVal) > 0 { 180 | format += "]" 181 | } 182 | format += "\n\t%s\n\n" 183 | cobra.WriteStringAndCheck(buf, fmt.Sprintf(format, flag.DefValue, flag.Usage)) 184 | }) 185 | } 186 | 187 | func manPrintOptions(buf io.StringWriter, command *cobra.Command) { 188 | flags := command.NonInheritedFlags() 189 | if flags.HasAvailableFlags() { 190 | cobra.WriteStringAndCheck(buf, "# OPTIONS\n") 191 | manPrintFlags(buf, flags) 192 | cobra.WriteStringAndCheck(buf, "\n") 193 | } 194 | flags = command.InheritedFlags() 195 | if flags.HasAvailableFlags() { 196 | cobra.WriteStringAndCheck(buf, "# OPTIONS INHERITED FROM PARENT COMMANDS\n") 197 | manPrintFlags(buf, flags) 198 | cobra.WriteStringAndCheck(buf, "\n") 199 | } 200 | } 201 | 202 | func genMan(cmd *cobra.Command, header *GenManHeader) []byte { 203 | cmd.InitDefaultHelpCmd() 204 | cmd.InitDefaultHelpFlag() 205 | 206 | // something like `rootcmd-subcmd1-subcmd2` 207 | dashCommandName := strings.ReplaceAll(cmd.CommandPath(), " ", "-") 208 | 209 | buf := new(bytes.Buffer) 210 | 211 | manPreamble(buf, header, cmd, dashCommandName) 212 | manPrintOptions(buf, cmd) 213 | if len(cmd.Example) > 0 { 214 | buf.WriteString("# EXAMPLE\n") 215 | fmt.Fprintf(buf, "```\n%s\n```\n", cmd.Example) 216 | } 217 | if hasSeeAlso(cmd) { 218 | buf.WriteString("# SEE ALSO\n") 219 | seealsos := make([]string, 0) 220 | if cmd.HasParent() { 221 | parentPath := cmd.Parent().CommandPath() 222 | dashParentPath := strings.ReplaceAll(parentPath, " ", "-") 223 | seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section) 224 | seealsos = append(seealsos, seealso) 225 | cmd.VisitParents(func(c *cobra.Command) { 226 | if c.DisableAutoGenTag { 227 | cmd.DisableAutoGenTag = c.DisableAutoGenTag 228 | } 229 | }) 230 | } 231 | children := cmd.Commands() 232 | sort.Sort(byName(children)) 233 | for _, c := range children { 234 | if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 235 | continue 236 | } 237 | seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section) 238 | seealsos = append(seealsos, seealso) 239 | } 240 | buf.WriteString(strings.Join(seealsos, ", ") + "\n") 241 | } 242 | if !cmd.DisableAutoGenTag { 243 | fmt.Fprintf(buf, "# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006")) 244 | } 245 | return buf.Bytes() 246 | } 247 | -------------------------------------------------------------------------------- /doc/man_docs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | func assertNoErr(t *testing.T, e error) { 30 | if e != nil { 31 | t.Error(e) 32 | } 33 | } 34 | 35 | func translate(in string) string { 36 | return strings.ReplaceAll(in, "-", "\\-") 37 | } 38 | 39 | func TestGenManDoc(t *testing.T) { 40 | header := &GenManHeader{ 41 | Title: "Project", 42 | Section: "2", 43 | } 44 | 45 | // We generate on a subcommand so we have both subcommands and parents 46 | buf := new(bytes.Buffer) 47 | if err := GenMan(echoCmd, header, buf); err != nil { 48 | t.Fatal(err) 49 | } 50 | output := buf.String() 51 | 52 | // Make sure parent has - in CommandPath() in SEE ALSO: 53 | parentPath := echoCmd.Parent().CommandPath() 54 | dashParentPath := strings.ReplaceAll(parentPath, " ", "-") 55 | expected := translate(dashParentPath) 56 | expected = expected + "(" + header.Section + ")" 57 | checkStringContains(t, output, expected) 58 | 59 | checkStringContains(t, output, translate(echoCmd.Name())) 60 | checkStringContains(t, output, translate(echoCmd.Name())) 61 | checkStringContains(t, output, "boolone") 62 | checkStringContains(t, output, "rootflag") 63 | checkStringContains(t, output, translate(rootCmd.Name())) 64 | checkStringContains(t, output, translate(echoSubCmd.Name())) 65 | checkStringOmits(t, output, translate(deprecatedCmd.Name())) 66 | checkStringContains(t, output, translate("Auto generated")) 67 | } 68 | 69 | func TestGenManNoHiddenParents(t *testing.T) { 70 | header := &GenManHeader{ 71 | Title: "Project", 72 | Section: "2", 73 | } 74 | 75 | // We generate on a subcommand so we have both subcommands and parents 76 | for _, name := range []string{"rootflag", "strtwo"} { 77 | f := rootCmd.PersistentFlags().Lookup(name) 78 | f.Hidden = true 79 | defer func() { f.Hidden = false }() 80 | } 81 | buf := new(bytes.Buffer) 82 | if err := GenMan(echoCmd, header, buf); err != nil { 83 | t.Fatal(err) 84 | } 85 | output := buf.String() 86 | 87 | // Make sure parent has - in CommandPath() in SEE ALSO: 88 | parentPath := echoCmd.Parent().CommandPath() 89 | dashParentPath := strings.ReplaceAll(parentPath, " ", "-") 90 | expected := translate(dashParentPath) 91 | expected = expected + "(" + header.Section + ")" 92 | checkStringContains(t, output, expected) 93 | 94 | checkStringContains(t, output, translate(echoCmd.Name())) 95 | checkStringContains(t, output, translate(echoCmd.Name())) 96 | checkStringContains(t, output, "boolone") 97 | checkStringOmits(t, output, "rootflag") 98 | checkStringContains(t, output, translate(rootCmd.Name())) 99 | checkStringContains(t, output, translate(echoSubCmd.Name())) 100 | checkStringOmits(t, output, translate(deprecatedCmd.Name())) 101 | checkStringContains(t, output, translate("Auto generated")) 102 | checkStringOmits(t, output, "OPTIONS INHERITED FROM PARENT COMMANDS") 103 | } 104 | 105 | func TestGenManNoGenTag(t *testing.T) { 106 | echoCmd.DisableAutoGenTag = true 107 | defer func() { echoCmd.DisableAutoGenTag = false }() 108 | 109 | header := &GenManHeader{ 110 | Title: "Project", 111 | Section: "2", 112 | } 113 | 114 | // We generate on a subcommand so we have both subcommands and parents 115 | buf := new(bytes.Buffer) 116 | if err := GenMan(echoCmd, header, buf); err != nil { 117 | t.Fatal(err) 118 | } 119 | output := buf.String() 120 | 121 | unexpected := translate("#HISTORY") 122 | checkStringOmits(t, output, unexpected) 123 | unexpected = translate("Auto generated by spf13/cobra") 124 | checkStringOmits(t, output, unexpected) 125 | } 126 | 127 | func TestGenManSeeAlso(t *testing.T) { 128 | rootCmd := &cobra.Command{Use: "root", Run: emptyRun} 129 | aCmd := &cobra.Command{Use: "aaa", Run: emptyRun, Hidden: true} // #229 130 | bCmd := &cobra.Command{Use: "bbb", Run: emptyRun} 131 | cCmd := &cobra.Command{Use: "ccc", Run: emptyRun} 132 | rootCmd.AddCommand(aCmd, bCmd, cCmd) 133 | 134 | buf := new(bytes.Buffer) 135 | header := &GenManHeader{} 136 | if err := GenMan(rootCmd, header, buf); err != nil { 137 | t.Fatal(err) 138 | } 139 | scanner := bufio.NewScanner(buf) 140 | 141 | if err := assertLineFound(scanner, ".SH SEE ALSO"); err != nil { 142 | t.Fatalf("Couldn't find SEE ALSO section header: %v", err) 143 | } 144 | if err := assertNextLineEquals(scanner, `\fBroot-bbb(1)\fP, \fBroot-ccc(1)\fP`); err != nil { 145 | t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err) 146 | } 147 | } 148 | 149 | func TestManPrintFlagsHidesShortDeprecated(t *testing.T) { 150 | c := &cobra.Command{} 151 | c.Flags().StringP("foo", "f", "default", "Foo flag") 152 | assertNoErr(t, c.Flags().MarkShorthandDeprecated("foo", "don't use it no more")) 153 | 154 | buf := new(bytes.Buffer) 155 | manPrintFlags(buf, c.Flags()) 156 | 157 | got := buf.String() 158 | expected := "**--foo**=\"default\"\n\tFoo flag\n\n" 159 | if got != expected { 160 | t.Errorf("Expected %v, got %v", expected, got) 161 | } 162 | } 163 | 164 | func TestGenManTree(t *testing.T) { 165 | c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} 166 | header := &GenManHeader{Section: "2"} 167 | tmpdir, err := os.MkdirTemp("", "test-gen-man-tree") 168 | if err != nil { 169 | t.Fatalf("Failed to create tmpdir: %s", err.Error()) 170 | } 171 | defer os.RemoveAll(tmpdir) 172 | 173 | if err := GenManTree(c, header, tmpdir); err != nil { 174 | t.Fatalf("GenManTree failed: %s", err.Error()) 175 | } 176 | 177 | if _, err := os.Stat(filepath.Join(tmpdir, "do.2")); err != nil { 178 | t.Fatalf("Expected file 'do.2' to exist") 179 | } 180 | 181 | if header.Title != "" { 182 | t.Fatalf("Expected header.Title to be unmodified") 183 | } 184 | } 185 | 186 | func assertLineFound(scanner *bufio.Scanner, expectedLine string) error { 187 | for scanner.Scan() { 188 | line := scanner.Text() 189 | if line == expectedLine { 190 | return nil 191 | } 192 | } 193 | 194 | if err := scanner.Err(); err != nil { 195 | return fmt.Errorf("scan failed: %s", err) 196 | } 197 | 198 | return fmt.Errorf("hit EOF before finding %v", expectedLine) 199 | } 200 | 201 | func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error { 202 | if scanner.Scan() { 203 | line := scanner.Text() 204 | if line == expectedLine { 205 | return nil 206 | } 207 | return fmt.Errorf("got %v, not %v", line, expectedLine) 208 | } 209 | 210 | if err := scanner.Err(); err != nil { 211 | return fmt.Errorf("scan failed: %v", err) 212 | } 213 | 214 | return fmt.Errorf("hit EOF before finding %v", expectedLine) 215 | } 216 | 217 | func BenchmarkGenManToFile(b *testing.B) { 218 | file, err := os.CreateTemp("", "") 219 | if err != nil { 220 | b.Fatal(err) 221 | } 222 | defer os.Remove(file.Name()) 223 | defer file.Close() 224 | 225 | b.ResetTimer() 226 | for i := 0; i < b.N; i++ { 227 | if err := GenMan(rootCmd, nil, file); err != nil { 228 | b.Fatal(err) 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /doc/man_examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc_test 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | "github.com/spf13/cobra/doc" 23 | ) 24 | 25 | func ExampleGenManTree() { 26 | cmd := &cobra.Command{ 27 | Use: "test", 28 | Short: "my test program", 29 | } 30 | header := &doc.GenManHeader{ 31 | Title: "MINE", 32 | Section: "3", 33 | } 34 | cobra.CheckErr(doc.GenManTree(cmd, header, "/tmp")) 35 | } 36 | 37 | func ExampleGenMan() { 38 | cmd := &cobra.Command{ 39 | Use: "test", 40 | Short: "my test program", 41 | } 42 | header := &doc.GenManHeader{ 43 | Title: "MINE", 44 | Section: "3", 45 | } 46 | out := new(bytes.Buffer) 47 | cobra.CheckErr(doc.GenMan(cmd, header, out)) 48 | fmt.Print(out.String()) 49 | } 50 | -------------------------------------------------------------------------------- /doc/md_docs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "sort" 24 | "strings" 25 | "time" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | const markdownExtension = ".md" 31 | 32 | func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { 33 | flags := cmd.NonInheritedFlags() 34 | flags.SetOutput(buf) 35 | if flags.HasAvailableFlags() { 36 | buf.WriteString("### Options\n\n```\n") 37 | flags.PrintDefaults() 38 | buf.WriteString("```\n\n") 39 | } 40 | 41 | parentFlags := cmd.InheritedFlags() 42 | parentFlags.SetOutput(buf) 43 | if parentFlags.HasAvailableFlags() { 44 | buf.WriteString("### Options inherited from parent commands\n\n```\n") 45 | parentFlags.PrintDefaults() 46 | buf.WriteString("```\n\n") 47 | } 48 | return nil 49 | } 50 | 51 | // GenMarkdown creates markdown output. 52 | func GenMarkdown(cmd *cobra.Command, w io.Writer) error { 53 | return GenMarkdownCustom(cmd, w, func(s string) string { return s }) 54 | } 55 | 56 | // GenMarkdownCustom creates custom markdown output. 57 | func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { 58 | cmd.InitDefaultHelpCmd() 59 | cmd.InitDefaultHelpFlag() 60 | 61 | buf := new(bytes.Buffer) 62 | name := cmd.CommandPath() 63 | 64 | buf.WriteString("## " + name + "\n\n") 65 | buf.WriteString(cmd.Short + "\n\n") 66 | if len(cmd.Long) > 0 { 67 | buf.WriteString("### Synopsis\n\n") 68 | buf.WriteString(cmd.Long + "\n\n") 69 | } 70 | 71 | if cmd.Runnable() { 72 | fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine()) 73 | } 74 | 75 | if len(cmd.Example) > 0 { 76 | buf.WriteString("### Examples\n\n") 77 | fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example) 78 | } 79 | 80 | if err := printOptions(buf, cmd, name); err != nil { 81 | return err 82 | } 83 | if hasSeeAlso(cmd) { 84 | buf.WriteString("### SEE ALSO\n\n") 85 | if cmd.HasParent() { 86 | parent := cmd.Parent() 87 | pname := parent.CommandPath() 88 | link := pname + markdownExtension 89 | link = strings.ReplaceAll(link, " ", "_") 90 | fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short) 91 | cmd.VisitParents(func(c *cobra.Command) { 92 | if c.DisableAutoGenTag { 93 | cmd.DisableAutoGenTag = c.DisableAutoGenTag 94 | } 95 | }) 96 | } 97 | 98 | children := cmd.Commands() 99 | sort.Sort(byName(children)) 100 | 101 | for _, child := range children { 102 | if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { 103 | continue 104 | } 105 | cname := name + " " + child.Name() 106 | link := cname + markdownExtension 107 | link = strings.ReplaceAll(link, " ", "_") 108 | fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short) 109 | } 110 | buf.WriteString("\n") 111 | } 112 | if !cmd.DisableAutoGenTag { 113 | buf.WriteString("###### Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "\n") 114 | } 115 | _, err := buf.WriteTo(w) 116 | return err 117 | } 118 | 119 | // GenMarkdownTree will generate a markdown page for this command and all 120 | // descendants in the directory given. The header may be nil. 121 | // This function may not work correctly if your command names have `-` in them. 122 | // If you have `cmd` with two subcmds, `sub` and `sub-third`, 123 | // and `sub` has a subcommand called `third`, it is undefined which 124 | // help output will be in the file `cmd-sub-third.1`. 125 | func GenMarkdownTree(cmd *cobra.Command, dir string) error { 126 | identity := func(s string) string { return s } 127 | emptyStr := func(s string) string { return "" } 128 | return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) 129 | } 130 | 131 | // GenMarkdownTreeCustom is the same as GenMarkdownTree, but 132 | // with custom filePrepender and linkHandler. 133 | func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { 134 | for _, c := range cmd.Commands() { 135 | if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 136 | continue 137 | } 138 | if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil { 139 | return err 140 | } 141 | } 142 | 143 | basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + markdownExtension 144 | filename := filepath.Join(dir, basename) 145 | f, err := os.Create(filename) 146 | if err != nil { 147 | return err 148 | } 149 | defer f.Close() 150 | 151 | if _, err := io.WriteString(f, filePrepender(filename)); err != nil { 152 | return err 153 | } 154 | if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil { 155 | return err 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /doc/md_docs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bytes" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | func TestGenMdDoc(t *testing.T) { 27 | // We generate on subcommand so we have both subcommands and parents. 28 | buf := new(bytes.Buffer) 29 | if err := GenMarkdown(echoCmd, buf); err != nil { 30 | t.Fatal(err) 31 | } 32 | output := buf.String() 33 | 34 | checkStringContains(t, output, echoCmd.Long) 35 | checkStringContains(t, output, echoCmd.Example) 36 | checkStringContains(t, output, "boolone") 37 | checkStringContains(t, output, "rootflag") 38 | checkStringContains(t, output, rootCmd.Short) 39 | checkStringContains(t, output, echoSubCmd.Short) 40 | checkStringOmits(t, output, deprecatedCmd.Short) 41 | checkStringContains(t, output, "Options inherited from parent commands") 42 | } 43 | 44 | func TestGenMdDocWithNoLongOrSynopsis(t *testing.T) { 45 | // We generate on subcommand so we have both subcommands and parents. 46 | buf := new(bytes.Buffer) 47 | if err := GenMarkdown(dummyCmd, buf); err != nil { 48 | t.Fatal(err) 49 | } 50 | output := buf.String() 51 | 52 | checkStringContains(t, output, dummyCmd.Example) 53 | checkStringContains(t, output, dummyCmd.Short) 54 | checkStringContains(t, output, "Options inherited from parent commands") 55 | checkStringOmits(t, output, "### Synopsis") 56 | } 57 | 58 | func TestGenMdNoHiddenParents(t *testing.T) { 59 | // We generate on subcommand so we have both subcommands and parents. 60 | for _, name := range []string{"rootflag", "strtwo"} { 61 | f := rootCmd.PersistentFlags().Lookup(name) 62 | f.Hidden = true 63 | defer func() { f.Hidden = false }() 64 | } 65 | buf := new(bytes.Buffer) 66 | if err := GenMarkdown(echoCmd, buf); err != nil { 67 | t.Fatal(err) 68 | } 69 | output := buf.String() 70 | 71 | checkStringContains(t, output, echoCmd.Long) 72 | checkStringContains(t, output, echoCmd.Example) 73 | checkStringContains(t, output, "boolone") 74 | checkStringOmits(t, output, "rootflag") 75 | checkStringContains(t, output, rootCmd.Short) 76 | checkStringContains(t, output, echoSubCmd.Short) 77 | checkStringOmits(t, output, deprecatedCmd.Short) 78 | checkStringOmits(t, output, "Options inherited from parent commands") 79 | } 80 | 81 | func TestGenMdNoTag(t *testing.T) { 82 | rootCmd.DisableAutoGenTag = true 83 | defer func() { rootCmd.DisableAutoGenTag = false }() 84 | 85 | buf := new(bytes.Buffer) 86 | if err := GenMarkdown(rootCmd, buf); err != nil { 87 | t.Fatal(err) 88 | } 89 | output := buf.String() 90 | 91 | checkStringOmits(t, output, "Auto generated") 92 | } 93 | 94 | func TestGenMdTree(t *testing.T) { 95 | c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} 96 | tmpdir, err := os.MkdirTemp("", "test-gen-md-tree") 97 | if err != nil { 98 | t.Fatalf("Failed to create tmpdir: %v", err) 99 | } 100 | defer os.RemoveAll(tmpdir) 101 | 102 | if err := GenMarkdownTree(c, tmpdir); err != nil { 103 | t.Fatalf("GenMarkdownTree failed: %v", err) 104 | } 105 | 106 | if _, err := os.Stat(filepath.Join(tmpdir, "do.md")); err != nil { 107 | t.Fatalf("Expected file 'do.md' to exist") 108 | } 109 | } 110 | 111 | func BenchmarkGenMarkdownToFile(b *testing.B) { 112 | file, err := os.CreateTemp("", "") 113 | if err != nil { 114 | b.Fatal(err) 115 | } 116 | defer os.Remove(file.Name()) 117 | defer file.Close() 118 | 119 | b.ResetTimer() 120 | for i := 0; i < b.N; i++ { 121 | if err := GenMarkdown(rootCmd, file); err != nil { 122 | b.Fatal(err) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /doc/rest_docs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "path/filepath" 23 | "sort" 24 | "strings" 25 | "time" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | func printOptionsReST(buf *bytes.Buffer, cmd *cobra.Command, name string) error { 31 | flags := cmd.NonInheritedFlags() 32 | flags.SetOutput(buf) 33 | if flags.HasAvailableFlags() { 34 | buf.WriteString("Options\n") 35 | buf.WriteString("~~~~~~~\n\n::\n\n") 36 | flags.PrintDefaults() 37 | buf.WriteString("\n") 38 | } 39 | 40 | parentFlags := cmd.InheritedFlags() 41 | parentFlags.SetOutput(buf) 42 | if parentFlags.HasAvailableFlags() { 43 | buf.WriteString("Options inherited from parent commands\n") 44 | buf.WriteString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n::\n\n") 45 | parentFlags.PrintDefaults() 46 | buf.WriteString("\n") 47 | } 48 | return nil 49 | } 50 | 51 | // defaultLinkHandler for default ReST hyperlink markup 52 | func defaultLinkHandler(name, ref string) string { 53 | return fmt.Sprintf("`%s <%s.rst>`_", name, ref) 54 | } 55 | 56 | // GenReST creates reStructured Text output. 57 | func GenReST(cmd *cobra.Command, w io.Writer) error { 58 | return GenReSTCustom(cmd, w, defaultLinkHandler) 59 | } 60 | 61 | // GenReSTCustom creates custom reStructured Text output. 62 | func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, string) string) error { 63 | cmd.InitDefaultHelpCmd() 64 | cmd.InitDefaultHelpFlag() 65 | 66 | buf := new(bytes.Buffer) 67 | name := cmd.CommandPath() 68 | 69 | short := cmd.Short 70 | long := cmd.Long 71 | if len(long) == 0 { 72 | long = short 73 | } 74 | ref := strings.ReplaceAll(name, " ", "_") 75 | 76 | buf.WriteString(".. _" + ref + ":\n\n") 77 | buf.WriteString(name + "\n") 78 | buf.WriteString(strings.Repeat("-", len(name)) + "\n\n") 79 | buf.WriteString(short + "\n\n") 80 | buf.WriteString("Synopsis\n") 81 | buf.WriteString("~~~~~~~~\n\n") 82 | buf.WriteString("\n" + long + "\n\n") 83 | 84 | if cmd.Runnable() { 85 | fmt.Fprintf(buf, "::\n\n %s\n\n", cmd.UseLine()) 86 | } 87 | 88 | if len(cmd.Example) > 0 { 89 | buf.WriteString("Examples\n") 90 | buf.WriteString("~~~~~~~~\n\n") 91 | fmt.Fprintf(buf, "::\n\n%s\n\n", indentString(cmd.Example, " ")) 92 | } 93 | 94 | if err := printOptionsReST(buf, cmd, name); err != nil { 95 | return err 96 | } 97 | if hasSeeAlso(cmd) { 98 | buf.WriteString("SEE ALSO\n") 99 | buf.WriteString("~~~~~~~~\n\n") 100 | if cmd.HasParent() { 101 | parent := cmd.Parent() 102 | pname := parent.CommandPath() 103 | ref = strings.ReplaceAll(pname, " ", "_") 104 | fmt.Fprintf(buf, "* %s \t - %s\n", linkHandler(pname, ref), parent.Short) 105 | cmd.VisitParents(func(c *cobra.Command) { 106 | if c.DisableAutoGenTag { 107 | cmd.DisableAutoGenTag = c.DisableAutoGenTag 108 | } 109 | }) 110 | } 111 | 112 | children := cmd.Commands() 113 | sort.Sort(byName(children)) 114 | 115 | for _, child := range children { 116 | if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { 117 | continue 118 | } 119 | cname := name + " " + child.Name() 120 | ref = strings.ReplaceAll(cname, " ", "_") 121 | fmt.Fprintf(buf, "* %s \t - %s\n", linkHandler(cname, ref), child.Short) 122 | } 123 | buf.WriteString("\n") 124 | } 125 | if !cmd.DisableAutoGenTag { 126 | buf.WriteString("*Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "*\n") 127 | } 128 | _, err := buf.WriteTo(w) 129 | return err 130 | } 131 | 132 | // GenReSTTree will generate a ReST page for this command and all 133 | // descendants in the directory given. 134 | // This function may not work correctly if your command names have `-` in them. 135 | // If you have `cmd` with two subcmds, `sub` and `sub-third`, 136 | // and `sub` has a subcommand called `third`, it is undefined which 137 | // help output will be in the file `cmd-sub-third.1`. 138 | func GenReSTTree(cmd *cobra.Command, dir string) error { 139 | emptyStr := func(s string) string { return "" } 140 | return GenReSTTreeCustom(cmd, dir, emptyStr, defaultLinkHandler) 141 | } 142 | 143 | // GenReSTTreeCustom is the same as GenReSTTree, but 144 | // with custom filePrepender and linkHandler. 145 | func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error { 146 | for _, c := range cmd.Commands() { 147 | if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 148 | continue 149 | } 150 | if err := GenReSTTreeCustom(c, dir, filePrepender, linkHandler); err != nil { 151 | return err 152 | } 153 | } 154 | 155 | basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".rst" 156 | filename := filepath.Join(dir, basename) 157 | f, err := os.Create(filename) 158 | if err != nil { 159 | return err 160 | } 161 | defer f.Close() 162 | 163 | if _, err := io.WriteString(f, filePrepender(filename)); err != nil { 164 | return err 165 | } 166 | if err := GenReSTCustom(cmd, f, linkHandler); err != nil { 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | // indentString adapted from: https://github.com/kr/text/blob/main/indent.go 173 | func indentString(s, p string) string { 174 | var res []byte 175 | b := []byte(s) 176 | prefix := []byte(p) 177 | bol := true 178 | for _, c := range b { 179 | if bol && c != '\n' { 180 | res = append(res, prefix...) 181 | } 182 | res = append(res, c) 183 | bol = c == '\n' 184 | } 185 | return string(res) 186 | } 187 | -------------------------------------------------------------------------------- /doc/rest_docs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bytes" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | func TestGenRSTDoc(t *testing.T) { 27 | // We generate on a subcommand so we have both subcommands and parents 28 | buf := new(bytes.Buffer) 29 | if err := GenReST(echoCmd, buf); err != nil { 30 | t.Fatal(err) 31 | } 32 | output := buf.String() 33 | 34 | checkStringContains(t, output, echoCmd.Long) 35 | checkStringContains(t, output, echoCmd.Example) 36 | checkStringContains(t, output, "boolone") 37 | checkStringContains(t, output, "rootflag") 38 | checkStringContains(t, output, rootCmd.Short) 39 | checkStringContains(t, output, echoSubCmd.Short) 40 | checkStringOmits(t, output, deprecatedCmd.Short) 41 | } 42 | 43 | func TestGenRSTNoHiddenParents(t *testing.T) { 44 | // We generate on a subcommand so we have both subcommands and parents 45 | for _, name := range []string{"rootflag", "strtwo"} { 46 | f := rootCmd.PersistentFlags().Lookup(name) 47 | f.Hidden = true 48 | defer func() { f.Hidden = false }() 49 | } 50 | buf := new(bytes.Buffer) 51 | if err := GenReST(echoCmd, buf); err != nil { 52 | t.Fatal(err) 53 | } 54 | output := buf.String() 55 | 56 | checkStringContains(t, output, echoCmd.Long) 57 | checkStringContains(t, output, echoCmd.Example) 58 | checkStringContains(t, output, "boolone") 59 | checkStringOmits(t, output, "rootflag") 60 | checkStringContains(t, output, rootCmd.Short) 61 | checkStringContains(t, output, echoSubCmd.Short) 62 | checkStringOmits(t, output, deprecatedCmd.Short) 63 | checkStringOmits(t, output, "Options inherited from parent commands") 64 | } 65 | 66 | func TestGenRSTNoTag(t *testing.T) { 67 | rootCmd.DisableAutoGenTag = true 68 | defer func() { rootCmd.DisableAutoGenTag = false }() 69 | 70 | buf := new(bytes.Buffer) 71 | if err := GenReST(rootCmd, buf); err != nil { 72 | t.Fatal(err) 73 | } 74 | output := buf.String() 75 | 76 | unexpected := "Auto generated" 77 | checkStringOmits(t, output, unexpected) 78 | } 79 | 80 | func TestGenRSTTree(t *testing.T) { 81 | c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} 82 | 83 | tmpdir, err := os.MkdirTemp("", "test-gen-rst-tree") 84 | if err != nil { 85 | t.Fatalf("Failed to create tmpdir: %s", err.Error()) 86 | } 87 | defer os.RemoveAll(tmpdir) 88 | 89 | if err := GenReSTTree(c, tmpdir); err != nil { 90 | t.Fatalf("GenReSTTree failed: %s", err.Error()) 91 | } 92 | 93 | if _, err := os.Stat(filepath.Join(tmpdir, "do.rst")); err != nil { 94 | t.Fatalf("Expected file 'do.rst' to exist") 95 | } 96 | } 97 | 98 | func BenchmarkGenReSTToFile(b *testing.B) { 99 | file, err := os.CreateTemp("", "") 100 | if err != nil { 101 | b.Fatal(err) 102 | } 103 | defer os.Remove(file.Name()) 104 | defer file.Close() 105 | 106 | b.ResetTimer() 107 | for i := 0; i < b.N; i++ { 108 | if err := GenReST(rootCmd, file); err != nil { 109 | b.Fatal(err) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /doc/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | // Test to see if we have a reason to print See Also information in docs 24 | // Basically this is a test for a parent command or a subcommand which is 25 | // both not deprecated and not the autogenerated help command. 26 | func hasSeeAlso(cmd *cobra.Command) bool { 27 | if cmd.HasParent() { 28 | return true 29 | } 30 | for _, c := range cmd.Commands() { 31 | if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 32 | continue 33 | } 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | // Temporary workaround for yaml lib generating incorrect yaml with long strings 40 | // that do not contain \n. 41 | func forceMultiLine(s string) string { 42 | if len(s) > 60 && !strings.Contains(s, "\n") { 43 | s += "\n" 44 | } 45 | return s 46 | } 47 | 48 | type byName []*cobra.Command 49 | 50 | func (s byName) Len() int { return len(s) } 51 | func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 52 | func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } 53 | -------------------------------------------------------------------------------- /doc/yaml_docs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os" 21 | "path/filepath" 22 | "sort" 23 | "strings" 24 | 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/pflag" 27 | "gopkg.in/yaml.v3" 28 | ) 29 | 30 | type cmdOption struct { 31 | Name string 32 | Shorthand string `yaml:",omitempty"` 33 | DefaultValue string `yaml:"default_value,omitempty"` 34 | Usage string `yaml:",omitempty"` 35 | } 36 | 37 | type cmdDoc struct { 38 | Name string 39 | Synopsis string `yaml:",omitempty"` 40 | Description string `yaml:",omitempty"` 41 | Usage string `yaml:",omitempty"` 42 | Options []cmdOption `yaml:",omitempty"` 43 | InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"` 44 | Example string `yaml:",omitempty"` 45 | SeeAlso []string `yaml:"see_also,omitempty"` 46 | } 47 | 48 | // GenYamlTree creates yaml structured ref files for this command and all descendants 49 | // in the directory given. This function may not work 50 | // correctly if your command names have `-` in them. If you have `cmd` with two 51 | // subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` 52 | // it is undefined which help output will be in the file `cmd-sub-third.1`. 53 | func GenYamlTree(cmd *cobra.Command, dir string) error { 54 | identity := func(s string) string { return s } 55 | emptyStr := func(s string) string { return "" } 56 | return GenYamlTreeCustom(cmd, dir, emptyStr, identity) 57 | } 58 | 59 | // GenYamlTreeCustom creates yaml structured ref files. 60 | func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { 61 | for _, c := range cmd.Commands() { 62 | if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 63 | continue 64 | } 65 | if err := GenYamlTreeCustom(c, dir, filePrepender, linkHandler); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".yaml" 71 | filename := filepath.Join(dir, basename) 72 | f, err := os.Create(filename) 73 | if err != nil { 74 | return err 75 | } 76 | defer f.Close() 77 | 78 | if _, err := io.WriteString(f, filePrepender(filename)); err != nil { 79 | return err 80 | } 81 | if err := GenYamlCustom(cmd, f, linkHandler); err != nil { 82 | return err 83 | } 84 | return nil 85 | } 86 | 87 | // GenYaml creates yaml output. 88 | func GenYaml(cmd *cobra.Command, w io.Writer) error { 89 | return GenYamlCustom(cmd, w, func(s string) string { return s }) 90 | } 91 | 92 | // GenYamlCustom creates custom yaml output. 93 | func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { 94 | cmd.InitDefaultHelpCmd() 95 | cmd.InitDefaultHelpFlag() 96 | 97 | yamlDoc := cmdDoc{} 98 | yamlDoc.Name = cmd.CommandPath() 99 | 100 | yamlDoc.Synopsis = forceMultiLine(cmd.Short) 101 | yamlDoc.Description = forceMultiLine(cmd.Long) 102 | 103 | if cmd.Runnable() { 104 | yamlDoc.Usage = cmd.UseLine() 105 | } 106 | 107 | if len(cmd.Example) > 0 { 108 | yamlDoc.Example = cmd.Example 109 | } 110 | 111 | flags := cmd.NonInheritedFlags() 112 | if flags.HasFlags() { 113 | yamlDoc.Options = genFlagResult(flags) 114 | } 115 | flags = cmd.InheritedFlags() 116 | if flags.HasFlags() { 117 | yamlDoc.InheritedOptions = genFlagResult(flags) 118 | } 119 | 120 | if hasSeeAlso(cmd) { 121 | result := []string{} 122 | if cmd.HasParent() { 123 | parent := cmd.Parent() 124 | result = append(result, parent.CommandPath()+" - "+parent.Short) 125 | } 126 | children := cmd.Commands() 127 | sort.Sort(byName(children)) 128 | for _, child := range children { 129 | if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { 130 | continue 131 | } 132 | result = append(result, child.CommandPath()+" - "+child.Short) 133 | } 134 | yamlDoc.SeeAlso = result 135 | } 136 | 137 | final, err := yaml.Marshal(&yamlDoc) 138 | if err != nil { 139 | fmt.Println(err) 140 | os.Exit(1) 141 | } 142 | 143 | if _, err := w.Write(final); err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | func genFlagResult(flags *pflag.FlagSet) []cmdOption { 150 | var result []cmdOption 151 | 152 | flags.VisitAll(func(flag *pflag.Flag) { 153 | // Todo, when we mark a shorthand is deprecated, but specify an empty message. 154 | // The flag.ShorthandDeprecated is empty as the shorthand is deprecated. 155 | // Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok. 156 | if len(flag.ShorthandDeprecated) == 0 && len(flag.Shorthand) > 0 { 157 | opt := cmdOption{ 158 | flag.Name, 159 | flag.Shorthand, 160 | flag.DefValue, 161 | forceMultiLine(flag.Usage), 162 | } 163 | result = append(result, opt) 164 | } else { 165 | opt := cmdOption{ 166 | Name: flag.Name, 167 | DefaultValue: forceMultiLine(flag.DefValue), 168 | Usage: forceMultiLine(flag.Usage), 169 | } 170 | result = append(result, opt) 171 | } 172 | }) 173 | 174 | return result 175 | } 176 | -------------------------------------------------------------------------------- /doc/yaml_docs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package doc 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func TestGenYamlDoc(t *testing.T) { 28 | // We generate on s subcommand so we have both subcommands and parents 29 | buf := new(bytes.Buffer) 30 | if err := GenYaml(echoCmd, buf); err != nil { 31 | t.Fatal(err) 32 | } 33 | output := buf.String() 34 | 35 | checkStringContains(t, output, echoCmd.Long) 36 | checkStringContains(t, output, echoCmd.Example) 37 | checkStringContains(t, output, "boolone") 38 | checkStringContains(t, output, "rootflag") 39 | checkStringContains(t, output, rootCmd.Short) 40 | checkStringContains(t, output, echoSubCmd.Short) 41 | checkStringContains(t, output, fmt.Sprintf("- %s - %s", echoSubCmd.CommandPath(), echoSubCmd.Short)) 42 | } 43 | 44 | func TestGenYamlNoTag(t *testing.T) { 45 | rootCmd.DisableAutoGenTag = true 46 | defer func() { rootCmd.DisableAutoGenTag = false }() 47 | 48 | buf := new(bytes.Buffer) 49 | if err := GenYaml(rootCmd, buf); err != nil { 50 | t.Fatal(err) 51 | } 52 | output := buf.String() 53 | 54 | checkStringOmits(t, output, "Auto generated") 55 | } 56 | 57 | func TestGenYamlTree(t *testing.T) { 58 | c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} 59 | 60 | tmpdir, err := os.MkdirTemp("", "test-gen-yaml-tree") 61 | if err != nil { 62 | t.Fatalf("Failed to create tmpdir: %s", err.Error()) 63 | } 64 | defer os.RemoveAll(tmpdir) 65 | 66 | if err := GenYamlTree(c, tmpdir); err != nil { 67 | t.Fatalf("GenYamlTree failed: %s", err.Error()) 68 | } 69 | 70 | if _, err := os.Stat(filepath.Join(tmpdir, "do.yaml")); err != nil { 71 | t.Fatalf("Expected file 'do.yaml' to exist") 72 | } 73 | } 74 | 75 | func TestGenYamlDocRunnable(t *testing.T) { 76 | // Testing a runnable command: should contain the "usage" field 77 | buf := new(bytes.Buffer) 78 | if err := GenYaml(rootCmd, buf); err != nil { 79 | t.Fatal(err) 80 | } 81 | output := buf.String() 82 | 83 | checkStringContains(t, output, "usage: "+rootCmd.Use) 84 | } 85 | 86 | func BenchmarkGenYamlToFile(b *testing.B) { 87 | file, err := os.CreateTemp("", "") 88 | if err != nil { 89 | b.Fatal(err) 90 | } 91 | defer os.Remove(file.Name()) 92 | defer file.Close() 93 | 94 | b.ResetTimer() 95 | for i := 0; i < b.N; i++ { 96 | if err := GenYaml(rootCmd, file); err != nil { 97 | b.Fatal(err) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /fish_completions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | "strings" 23 | ) 24 | 25 | func genFishComp(buf io.StringWriter, name string, includeDesc bool) { 26 | // Variables should not contain a '-' or ':' character 27 | nameForVar := name 28 | nameForVar = strings.ReplaceAll(nameForVar, "-", "_") 29 | nameForVar = strings.ReplaceAll(nameForVar, ":", "_") 30 | 31 | compCmd := ShellCompRequestCmd 32 | if !includeDesc { 33 | compCmd = ShellCompNoDescRequestCmd 34 | } 35 | WriteStringAndCheck(buf, fmt.Sprintf("# fish completion for %-36s -*- shell-script -*-\n", name)) 36 | WriteStringAndCheck(buf, fmt.Sprintf(` 37 | function __%[1]s_debug 38 | set -l file "$BASH_COMP_DEBUG_FILE" 39 | if test -n "$file" 40 | echo "$argv" >> $file 41 | end 42 | end 43 | 44 | function __%[1]s_perform_completion 45 | __%[1]s_debug "Starting __%[1]s_perform_completion" 46 | 47 | # Extract all args except the last one 48 | set -l args (commandline -opc) 49 | # Extract the last arg and escape it in case it is a space 50 | set -l lastArg (string escape -- (commandline -ct)) 51 | 52 | __%[1]s_debug "args: $args" 53 | __%[1]s_debug "last arg: $lastArg" 54 | 55 | # Disable ActiveHelp which is not supported for fish shell 56 | set -l requestComp "%[10]s=0 $args[1] %[3]s $args[2..-1] $lastArg" 57 | 58 | __%[1]s_debug "Calling $requestComp" 59 | set -l results (eval $requestComp 2> /dev/null) 60 | 61 | # Some programs may output extra empty lines after the directive. 62 | # Let's ignore them or else it will break completion. 63 | # Ref: https://github.com/spf13/cobra/issues/1279 64 | for line in $results[-1..1] 65 | if test (string trim -- $line) = "" 66 | # Found an empty line, remove it 67 | set results $results[1..-2] 68 | else 69 | # Found non-empty line, we have our proper output 70 | break 71 | end 72 | end 73 | 74 | set -l comps $results[1..-2] 75 | set -l directiveLine $results[-1] 76 | 77 | # For Fish, when completing a flag with an = (e.g., -n=) 78 | # completions must be prefixed with the flag 79 | set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 80 | 81 | __%[1]s_debug "Comps: $comps" 82 | __%[1]s_debug "DirectiveLine: $directiveLine" 83 | __%[1]s_debug "flagPrefix: $flagPrefix" 84 | 85 | for comp in $comps 86 | printf "%%s%%s\n" "$flagPrefix" "$comp" 87 | end 88 | 89 | printf "%%s\n" "$directiveLine" 90 | end 91 | 92 | # this function limits calls to __%[1]s_perform_completion, by caching the result behind $__%[1]s_perform_completion_once_result 93 | function __%[1]s_perform_completion_once 94 | __%[1]s_debug "Starting __%[1]s_perform_completion_once" 95 | 96 | if test -n "$__%[1]s_perform_completion_once_result" 97 | __%[1]s_debug "Seems like a valid result already exists, skipping __%[1]s_perform_completion" 98 | return 0 99 | end 100 | 101 | set --global __%[1]s_perform_completion_once_result (__%[1]s_perform_completion) 102 | if test -z "$__%[1]s_perform_completion_once_result" 103 | __%[1]s_debug "No completions, probably due to a failure" 104 | return 1 105 | end 106 | 107 | __%[1]s_debug "Performed completions and set __%[1]s_perform_completion_once_result" 108 | return 0 109 | end 110 | 111 | # this function is used to clear the $__%[1]s_perform_completion_once_result variable after completions are run 112 | function __%[1]s_clear_perform_completion_once_result 113 | __%[1]s_debug "" 114 | __%[1]s_debug "========= clearing previously set __%[1]s_perform_completion_once_result variable ==========" 115 | set --erase __%[1]s_perform_completion_once_result 116 | __%[1]s_debug "Successfully erased the variable __%[1]s_perform_completion_once_result" 117 | end 118 | 119 | function __%[1]s_requires_order_preservation 120 | __%[1]s_debug "" 121 | __%[1]s_debug "========= checking if order preservation is required ==========" 122 | 123 | __%[1]s_perform_completion_once 124 | if test -z "$__%[1]s_perform_completion_once_result" 125 | __%[1]s_debug "Error determining if order preservation is required" 126 | return 1 127 | end 128 | 129 | set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) 130 | __%[1]s_debug "Directive is: $directive" 131 | 132 | set -l shellCompDirectiveKeepOrder %[9]d 133 | set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) %% 2) 134 | __%[1]s_debug "Keeporder is: $keeporder" 135 | 136 | if test $keeporder -ne 0 137 | __%[1]s_debug "This does require order preservation" 138 | return 0 139 | end 140 | 141 | __%[1]s_debug "This doesn't require order preservation" 142 | return 1 143 | end 144 | 145 | 146 | # This function does two things: 147 | # - Obtain the completions and store them in the global __%[1]s_comp_results 148 | # - Return false if file completion should be performed 149 | function __%[1]s_prepare_completions 150 | __%[1]s_debug "" 151 | __%[1]s_debug "========= starting completion logic ==========" 152 | 153 | # Start fresh 154 | set --erase __%[1]s_comp_results 155 | 156 | __%[1]s_perform_completion_once 157 | __%[1]s_debug "Completion results: $__%[1]s_perform_completion_once_result" 158 | 159 | if test -z "$__%[1]s_perform_completion_once_result" 160 | __%[1]s_debug "No completion, probably due to a failure" 161 | # Might as well do file completion, in case it helps 162 | return 1 163 | end 164 | 165 | set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) 166 | set --global __%[1]s_comp_results $__%[1]s_perform_completion_once_result[1..-2] 167 | 168 | __%[1]s_debug "Completions are: $__%[1]s_comp_results" 169 | __%[1]s_debug "Directive is: $directive" 170 | 171 | set -l shellCompDirectiveError %[4]d 172 | set -l shellCompDirectiveNoSpace %[5]d 173 | set -l shellCompDirectiveNoFileComp %[6]d 174 | set -l shellCompDirectiveFilterFileExt %[7]d 175 | set -l shellCompDirectiveFilterDirs %[8]d 176 | 177 | if test -z "$directive" 178 | set directive 0 179 | end 180 | 181 | set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) 182 | if test $compErr -eq 1 183 | __%[1]s_debug "Received error directive: aborting." 184 | # Might as well do file completion, in case it helps 185 | return 1 186 | end 187 | 188 | set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) 189 | set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) 190 | if test $filefilter -eq 1; or test $dirfilter -eq 1 191 | __%[1]s_debug "File extension filtering or directory filtering not supported" 192 | # Do full file completion instead 193 | return 1 194 | end 195 | 196 | set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) 197 | set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) 198 | 199 | __%[1]s_debug "nospace: $nospace, nofiles: $nofiles" 200 | 201 | # If we want to prevent a space, or if file completion is NOT disabled, 202 | # we need to count the number of valid completions. 203 | # To do so, we will filter on prefix as the completions we have received 204 | # may not already be filtered so as to allow fish to match on different 205 | # criteria than the prefix. 206 | if test $nospace -ne 0; or test $nofiles -eq 0 207 | set -l prefix (commandline -t | string escape --style=regex) 208 | __%[1]s_debug "prefix: $prefix" 209 | 210 | set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results) 211 | set --global __%[1]s_comp_results $completions 212 | __%[1]s_debug "Filtered completions are: $__%[1]s_comp_results" 213 | 214 | # Important not to quote the variable for count to work 215 | set -l numComps (count $__%[1]s_comp_results) 216 | __%[1]s_debug "numComps: $numComps" 217 | 218 | if test $numComps -eq 1; and test $nospace -ne 0 219 | # We must first split on \t to get rid of the descriptions to be 220 | # able to check what the actual completion will be. 221 | # We don't need descriptions anyway since there is only a single 222 | # real completion which the shell will expand immediately. 223 | set -l split (string split --max 1 \t $__%[1]s_comp_results[1]) 224 | 225 | # Fish won't add a space if the completion ends with any 226 | # of the following characters: @=/:., 227 | set -l lastChar (string sub -s -1 -- $split) 228 | if not string match -r -q "[@=/:.,]" -- "$lastChar" 229 | # In other cases, to support the "nospace" directive we trick the shell 230 | # by outputting an extra, longer completion. 231 | __%[1]s_debug "Adding second completion to perform nospace directive" 232 | set --global __%[1]s_comp_results $split[1] $split[1]. 233 | __%[1]s_debug "Completions are now: $__%[1]s_comp_results" 234 | end 235 | end 236 | 237 | if test $numComps -eq 0; and test $nofiles -eq 0 238 | # To be consistent with bash and zsh, we only trigger file 239 | # completion when there are no other completions 240 | __%[1]s_debug "Requesting file completion" 241 | return 1 242 | end 243 | end 244 | 245 | return 0 246 | end 247 | 248 | # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 249 | # so we can properly delete any completions provided by another script. 250 | # Only do this if the program can be found, or else fish may print some errors; besides, 251 | # the existing completions will only be loaded if the program can be found. 252 | if type -q "%[2]s" 253 | # The space after the program name is essential to trigger completion for the program 254 | # and not completion of the program name itself. 255 | # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 256 | complete --do-complete "%[2]s " > /dev/null 2>&1 257 | end 258 | 259 | # Remove any pre-existing completions for the program since we will be handling all of them. 260 | complete -c %[2]s -e 261 | 262 | # this will get called after the two calls below and clear the $__%[1]s_perform_completion_once_result global 263 | complete -c %[2]s -n '__%[1]s_clear_perform_completion_once_result' 264 | # The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results 265 | # which provides the program's completion choices. 266 | # If this doesn't require order preservation, we don't use the -k flag 267 | complete -c %[2]s -n 'not __%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' 268 | # otherwise we use the -k flag 269 | complete -k -c %[2]s -n '__%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' 270 | `, nameForVar, name, compCmd, 271 | ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 272 | ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name))) 273 | } 274 | 275 | // GenFishCompletion generates fish completion file and writes to the passed writer. 276 | func (c *Command) GenFishCompletion(w io.Writer, includeDesc bool) error { 277 | buf := new(bytes.Buffer) 278 | genFishComp(buf, c.Name(), includeDesc) 279 | _, err := buf.WriteTo(w) 280 | return err 281 | } 282 | 283 | // GenFishCompletionFile generates fish completion file. 284 | func (c *Command) GenFishCompletionFile(filename string, includeDesc bool) error { 285 | outFile, err := os.Create(filename) 286 | if err != nil { 287 | return err 288 | } 289 | defer outFile.Close() 290 | 291 | return c.GenFishCompletion(outFile, includeDesc) 292 | } 293 | -------------------------------------------------------------------------------- /fish_completions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | ) 25 | 26 | func TestCompleteNoDesCmdInFishScript(t *testing.T) { 27 | rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} 28 | child := &Command{ 29 | Use: "child", 30 | ValidArgsFunction: validArgsFunc, 31 | Run: emptyRun, 32 | } 33 | rootCmd.AddCommand(child) 34 | 35 | buf := new(bytes.Buffer) 36 | assertNoErr(t, rootCmd.GenFishCompletion(buf, false)) 37 | output := buf.String() 38 | 39 | check(t, output, ShellCompNoDescRequestCmd) 40 | } 41 | 42 | func TestCompleteCmdInFishScript(t *testing.T) { 43 | rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} 44 | child := &Command{ 45 | Use: "child", 46 | ValidArgsFunction: validArgsFunc, 47 | Run: emptyRun, 48 | } 49 | rootCmd.AddCommand(child) 50 | 51 | buf := new(bytes.Buffer) 52 | assertNoErr(t, rootCmd.GenFishCompletion(buf, true)) 53 | output := buf.String() 54 | 55 | check(t, output, ShellCompRequestCmd) 56 | checkOmit(t, output, ShellCompNoDescRequestCmd) 57 | } 58 | 59 | func TestProgWithDash(t *testing.T) { 60 | rootCmd := &Command{Use: "root-dash", Args: NoArgs, Run: emptyRun} 61 | buf := new(bytes.Buffer) 62 | assertNoErr(t, rootCmd.GenFishCompletion(buf, false)) 63 | output := buf.String() 64 | 65 | // Functions name should have replace the '-' 66 | check(t, output, "__root_dash_perform_completion") 67 | checkOmit(t, output, "__root-dash_perform_completion") 68 | 69 | // The command name should not have replaced the '-' 70 | check(t, output, "-c root-dash") 71 | checkOmit(t, output, "-c root_dash") 72 | } 73 | 74 | func TestProgWithColon(t *testing.T) { 75 | rootCmd := &Command{Use: "root:colon", Args: NoArgs, Run: emptyRun} 76 | buf := new(bytes.Buffer) 77 | assertNoErr(t, rootCmd.GenFishCompletion(buf, false)) 78 | output := buf.String() 79 | 80 | // Functions name should have replace the ':' 81 | check(t, output, "__root_colon_perform_completion") 82 | checkOmit(t, output, "__root:colon_perform_completion") 83 | 84 | // The command name should not have replaced the ':' 85 | check(t, output, "-c root:colon") 86 | checkOmit(t, output, "-c root_colon") 87 | } 88 | 89 | func TestFishCompletionNoActiveHelp(t *testing.T) { 90 | c := &Command{Use: "c", Run: emptyRun} 91 | 92 | buf := new(bytes.Buffer) 93 | assertNoErr(t, c.GenFishCompletion(buf, true)) 94 | output := buf.String() 95 | 96 | // check that active help is being disabled 97 | activeHelpVar := activeHelpEnvVar(c.Name()) 98 | check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) 99 | } 100 | 101 | func TestGenFishCompletionFile(t *testing.T) { 102 | tmpFile, err := os.CreateTemp("", "cobra-test") 103 | if err != nil { 104 | t.Fatal(err.Error()) 105 | } 106 | 107 | defer os.Remove(tmpFile.Name()) 108 | 109 | rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} 110 | child := &Command{ 111 | Use: "child", 112 | ValidArgsFunction: validArgsFunc, 113 | Run: emptyRun, 114 | } 115 | rootCmd.AddCommand(child) 116 | 117 | assertNoErr(t, rootCmd.GenFishCompletionFile(tmpFile.Name(), false)) 118 | } 119 | 120 | func TestFailGenFishCompletionFile(t *testing.T) { 121 | tmpDir, err := os.MkdirTemp("", "cobra-test") 122 | if err != nil { 123 | t.Fatal(err.Error()) 124 | } 125 | 126 | defer os.RemoveAll(tmpDir) 127 | 128 | f, _ := os.OpenFile(filepath.Join(tmpDir, "test"), os.O_CREATE, 0400) 129 | defer f.Close() 130 | 131 | rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} 132 | child := &Command{ 133 | Use: "child", 134 | ValidArgsFunction: validArgsFunc, 135 | Run: emptyRun, 136 | } 137 | rootCmd.AddCommand(child) 138 | 139 | got := rootCmd.GenFishCompletionFile(f.Name(), false) 140 | if !errors.Is(got, os.ErrPermission) { 141 | t.Errorf("got: %s, want: %s", got.Error(), os.ErrPermission.Error()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /flag_groups.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "fmt" 19 | "sort" 20 | "strings" 21 | 22 | flag "github.com/spf13/pflag" 23 | ) 24 | 25 | const ( 26 | requiredAsGroupAnnotation = "cobra_annotation_required_if_others_set" 27 | oneRequiredAnnotation = "cobra_annotation_one_required" 28 | mutuallyExclusiveAnnotation = "cobra_annotation_mutually_exclusive" 29 | ) 30 | 31 | // MarkFlagsRequiredTogether marks the given flags with annotations so that Cobra errors 32 | // if the command is invoked with a subset (but not all) of the given flags. 33 | func (c *Command) MarkFlagsRequiredTogether(flagNames ...string) { 34 | c.mergePersistentFlags() 35 | for _, v := range flagNames { 36 | f := c.Flags().Lookup(v) 37 | if f == nil { 38 | panic(fmt.Sprintf("Failed to find flag %q and mark it as being required in a flag group", v)) 39 | } 40 | if err := c.Flags().SetAnnotation(v, requiredAsGroupAnnotation, append(f.Annotations[requiredAsGroupAnnotation], strings.Join(flagNames, " "))); err != nil { 41 | // Only errs if the flag isn't found. 42 | panic(err) 43 | } 44 | } 45 | } 46 | 47 | // MarkFlagsOneRequired marks the given flags with annotations so that Cobra errors 48 | // if the command is invoked without at least one flag from the given set of flags. 49 | func (c *Command) MarkFlagsOneRequired(flagNames ...string) { 50 | c.mergePersistentFlags() 51 | for _, v := range flagNames { 52 | f := c.Flags().Lookup(v) 53 | if f == nil { 54 | panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a one-required flag group", v)) 55 | } 56 | if err := c.Flags().SetAnnotation(v, oneRequiredAnnotation, append(f.Annotations[oneRequiredAnnotation], strings.Join(flagNames, " "))); err != nil { 57 | // Only errs if the flag isn't found. 58 | panic(err) 59 | } 60 | } 61 | } 62 | 63 | // MarkFlagsMutuallyExclusive marks the given flags with annotations so that Cobra errors 64 | // if the command is invoked with more than one flag from the given set of flags. 65 | func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { 66 | c.mergePersistentFlags() 67 | for _, v := range flagNames { 68 | f := c.Flags().Lookup(v) 69 | if f == nil { 70 | panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a mutually exclusive flag group", v)) 71 | } 72 | // Each time this is called is a single new entry; this allows it to be a member of multiple groups if needed. 73 | if err := c.Flags().SetAnnotation(v, mutuallyExclusiveAnnotation, append(f.Annotations[mutuallyExclusiveAnnotation], strings.Join(flagNames, " "))); err != nil { 74 | panic(err) 75 | } 76 | } 77 | } 78 | 79 | // ValidateFlagGroups validates the mutuallyExclusive/oneRequired/requiredAsGroup logic and returns the 80 | // first error encountered. 81 | func (c *Command) ValidateFlagGroups() error { 82 | if c.DisableFlagParsing { 83 | return nil 84 | } 85 | 86 | flags := c.Flags() 87 | 88 | // groupStatus format is the list of flags as a unique ID, 89 | // then a map of each flag name and whether it is set or not. 90 | groupStatus := map[string]map[string]bool{} 91 | oneRequiredGroupStatus := map[string]map[string]bool{} 92 | mutuallyExclusiveGroupStatus := map[string]map[string]bool{} 93 | flags.VisitAll(func(pflag *flag.Flag) { 94 | processFlagForGroupAnnotation(flags, pflag, requiredAsGroupAnnotation, groupStatus) 95 | processFlagForGroupAnnotation(flags, pflag, oneRequiredAnnotation, oneRequiredGroupStatus) 96 | processFlagForGroupAnnotation(flags, pflag, mutuallyExclusiveAnnotation, mutuallyExclusiveGroupStatus) 97 | }) 98 | 99 | if err := validateRequiredFlagGroups(groupStatus); err != nil { 100 | return err 101 | } 102 | if err := validateOneRequiredFlagGroups(oneRequiredGroupStatus); err != nil { 103 | return err 104 | } 105 | if err := validateExclusiveFlagGroups(mutuallyExclusiveGroupStatus); err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | func hasAllFlags(fs *flag.FlagSet, flagnames ...string) bool { 112 | for _, fname := range flagnames { 113 | f := fs.Lookup(fname) 114 | if f == nil { 115 | return false 116 | } 117 | } 118 | return true 119 | } 120 | 121 | func processFlagForGroupAnnotation(flags *flag.FlagSet, pflag *flag.Flag, annotation string, groupStatus map[string]map[string]bool) { 122 | groupInfo, found := pflag.Annotations[annotation] 123 | if found { 124 | for _, group := range groupInfo { 125 | if groupStatus[group] == nil { 126 | flagnames := strings.Split(group, " ") 127 | 128 | // Only consider this flag group at all if all the flags are defined. 129 | if !hasAllFlags(flags, flagnames...) { 130 | continue 131 | } 132 | 133 | groupStatus[group] = make(map[string]bool, len(flagnames)) 134 | for _, name := range flagnames { 135 | groupStatus[group][name] = false 136 | } 137 | } 138 | 139 | groupStatus[group][pflag.Name] = pflag.Changed 140 | } 141 | } 142 | } 143 | 144 | func validateRequiredFlagGroups(data map[string]map[string]bool) error { 145 | keys := sortedKeys(data) 146 | for _, flagList := range keys { 147 | flagnameAndStatus := data[flagList] 148 | 149 | unset := []string{} 150 | for flagname, isSet := range flagnameAndStatus { 151 | if !isSet { 152 | unset = append(unset, flagname) 153 | } 154 | } 155 | if len(unset) == len(flagnameAndStatus) || len(unset) == 0 { 156 | continue 157 | } 158 | 159 | // Sort values, so they can be tested/scripted against consistently. 160 | sort.Strings(unset) 161 | return fmt.Errorf("if any flags in the group [%v] are set they must all be set; missing %v", flagList, unset) 162 | } 163 | 164 | return nil 165 | } 166 | 167 | func validateOneRequiredFlagGroups(data map[string]map[string]bool) error { 168 | keys := sortedKeys(data) 169 | for _, flagList := range keys { 170 | flagnameAndStatus := data[flagList] 171 | var set []string 172 | for flagname, isSet := range flagnameAndStatus { 173 | if isSet { 174 | set = append(set, flagname) 175 | } 176 | } 177 | if len(set) >= 1 { 178 | continue 179 | } 180 | 181 | // Sort values, so they can be tested/scripted against consistently. 182 | sort.Strings(set) 183 | return fmt.Errorf("at least one of the flags in the group [%v] is required", flagList) 184 | } 185 | return nil 186 | } 187 | 188 | func validateExclusiveFlagGroups(data map[string]map[string]bool) error { 189 | keys := sortedKeys(data) 190 | for _, flagList := range keys { 191 | flagnameAndStatus := data[flagList] 192 | var set []string 193 | for flagname, isSet := range flagnameAndStatus { 194 | if isSet { 195 | set = append(set, flagname) 196 | } 197 | } 198 | if len(set) == 0 || len(set) == 1 { 199 | continue 200 | } 201 | 202 | // Sort values, so they can be tested/scripted against consistently. 203 | sort.Strings(set) 204 | return fmt.Errorf("if any flags in the group [%v] are set none of the others can be; %v were all set", flagList, set) 205 | } 206 | return nil 207 | } 208 | 209 | func sortedKeys(m map[string]map[string]bool) []string { 210 | keys := make([]string, len(m)) 211 | i := 0 212 | for k := range m { 213 | keys[i] = k 214 | i++ 215 | } 216 | sort.Strings(keys) 217 | return keys 218 | } 219 | 220 | // enforceFlagGroupsForCompletion will do the following: 221 | // - when a flag in a group is present, other flags in the group will be marked required 222 | // - when none of the flags in a one-required group are present, all flags in the group will be marked required 223 | // - when a flag in a mutually exclusive group is present, other flags in the group will be marked as hidden 224 | // This allows the standard completion logic to behave appropriately for flag groups 225 | func (c *Command) enforceFlagGroupsForCompletion() { 226 | if c.DisableFlagParsing { 227 | return 228 | } 229 | 230 | flags := c.Flags() 231 | groupStatus := map[string]map[string]bool{} 232 | oneRequiredGroupStatus := map[string]map[string]bool{} 233 | mutuallyExclusiveGroupStatus := map[string]map[string]bool{} 234 | c.Flags().VisitAll(func(pflag *flag.Flag) { 235 | processFlagForGroupAnnotation(flags, pflag, requiredAsGroupAnnotation, groupStatus) 236 | processFlagForGroupAnnotation(flags, pflag, oneRequiredAnnotation, oneRequiredGroupStatus) 237 | processFlagForGroupAnnotation(flags, pflag, mutuallyExclusiveAnnotation, mutuallyExclusiveGroupStatus) 238 | }) 239 | 240 | // If a flag that is part of a group is present, we make all the other flags 241 | // of that group required so that the shell completion suggests them automatically 242 | for flagList, flagnameAndStatus := range groupStatus { 243 | for _, isSet := range flagnameAndStatus { 244 | if isSet { 245 | // One of the flags of the group is set, mark the other ones as required 246 | for _, fName := range strings.Split(flagList, " ") { 247 | _ = c.MarkFlagRequired(fName) 248 | } 249 | } 250 | } 251 | } 252 | 253 | // If none of the flags of a one-required group are present, we make all the flags 254 | // of that group required so that the shell completion suggests them automatically 255 | for flagList, flagnameAndStatus := range oneRequiredGroupStatus { 256 | isSet := false 257 | 258 | for _, isSet = range flagnameAndStatus { 259 | if isSet { 260 | break 261 | } 262 | } 263 | 264 | // None of the flags of the group are set, mark all flags in the group 265 | // as required 266 | if !isSet { 267 | for _, fName := range strings.Split(flagList, " ") { 268 | _ = c.MarkFlagRequired(fName) 269 | } 270 | } 271 | } 272 | 273 | // If a flag that is mutually exclusive to others is present, we hide the other 274 | // flags of that group so the shell completion does not suggest them 275 | for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus { 276 | for flagName, isSet := range flagnameAndStatus { 277 | if isSet { 278 | // One of the flags of the mutually exclusive group is set, mark the other ones as hidden 279 | // Don't mark the flag that is already set as hidden because it may be an 280 | // array or slice flag and therefore must continue being suggested 281 | for _, fName := range strings.Split(flagList, " ") { 282 | if fName != flagName { 283 | flag := c.Flags().Lookup(fName) 284 | flag.Hidden = true 285 | } 286 | } 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /flag_groups_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | ) 21 | 22 | func TestValidateFlagGroups(t *testing.T) { 23 | getCmd := func() *Command { 24 | c := &Command{ 25 | Use: "testcmd", 26 | Run: func(cmd *Command, args []string) { 27 | }} 28 | // Define lots of flags to utilize for testing. 29 | for _, v := range []string{"a", "b", "c", "d"} { 30 | c.Flags().String(v, "", "") 31 | } 32 | for _, v := range []string{"e", "f", "g"} { 33 | c.PersistentFlags().String(v, "", "") 34 | } 35 | subC := &Command{ 36 | Use: "subcmd", 37 | Run: func(cmd *Command, args []string) { 38 | }} 39 | subC.Flags().String("subonly", "", "") 40 | c.AddCommand(subC) 41 | return c 42 | } 43 | 44 | // Each test case uses a unique command from the function above. 45 | testcases := []struct { 46 | desc string 47 | flagGroupsRequired []string 48 | flagGroupsOneRequired []string 49 | flagGroupsExclusive []string 50 | subCmdFlagGroupsRequired []string 51 | subCmdFlagGroupsOneRequired []string 52 | subCmdFlagGroupsExclusive []string 53 | args []string 54 | expectErr string 55 | }{ 56 | { 57 | desc: "No flags no problem", 58 | }, { 59 | desc: "No flags no problem even with conflicting groups", 60 | flagGroupsRequired: []string{"a b"}, 61 | flagGroupsExclusive: []string{"a b"}, 62 | }, { 63 | desc: "Required flag group not satisfied", 64 | flagGroupsRequired: []string{"a b c"}, 65 | args: []string{"--a=foo"}, 66 | expectErr: "if any flags in the group [a b c] are set they must all be set; missing [b c]", 67 | }, { 68 | desc: "One-required flag group not satisfied", 69 | flagGroupsOneRequired: []string{"a b"}, 70 | args: []string{"--c=foo"}, 71 | expectErr: "at least one of the flags in the group [a b] is required", 72 | }, { 73 | desc: "Exclusive flag group not satisfied", 74 | flagGroupsExclusive: []string{"a b c"}, 75 | args: []string{"--a=foo", "--b=foo"}, 76 | expectErr: "if any flags in the group [a b c] are set none of the others can be; [a b] were all set", 77 | }, { 78 | desc: "Multiple required flag group not satisfied returns first error", 79 | flagGroupsRequired: []string{"a b c", "a d"}, 80 | args: []string{"--c=foo", "--d=foo"}, 81 | expectErr: `if any flags in the group [a b c] are set they must all be set; missing [a b]`, 82 | }, { 83 | desc: "Multiple one-required flag group not satisfied returns first error", 84 | flagGroupsOneRequired: []string{"a b", "d e"}, 85 | args: []string{"--c=foo", "--f=foo"}, 86 | expectErr: `at least one of the flags in the group [a b] is required`, 87 | }, { 88 | desc: "Multiple exclusive flag group not satisfied returns first error", 89 | flagGroupsExclusive: []string{"a b c", "a d"}, 90 | args: []string{"--a=foo", "--c=foo", "--d=foo"}, 91 | expectErr: `if any flags in the group [a b c] are set none of the others can be; [a c] were all set`, 92 | }, { 93 | desc: "Validation of required groups occurs on groups in sorted order", 94 | flagGroupsRequired: []string{"a d", "a b", "a c"}, 95 | args: []string{"--a=foo"}, 96 | expectErr: `if any flags in the group [a b] are set they must all be set; missing [b]`, 97 | }, { 98 | desc: "Validation of one-required groups occurs on groups in sorted order", 99 | flagGroupsOneRequired: []string{"d e", "a b", "f g"}, 100 | args: []string{"--c=foo"}, 101 | expectErr: `at least one of the flags in the group [a b] is required`, 102 | }, { 103 | desc: "Validation of exclusive groups occurs on groups in sorted order", 104 | flagGroupsExclusive: []string{"a d", "a b", "a c"}, 105 | args: []string{"--a=foo", "--b=foo", "--c=foo"}, 106 | expectErr: `if any flags in the group [a b] are set none of the others can be; [a b] were all set`, 107 | }, { 108 | desc: "Persistent flags utilize required and exclusive groups and can fail required groups", 109 | flagGroupsRequired: []string{"a e", "e f"}, 110 | flagGroupsExclusive: []string{"f g"}, 111 | args: []string{"--a=foo", "--f=foo", "--g=foo"}, 112 | expectErr: `if any flags in the group [a e] are set they must all be set; missing [e]`, 113 | }, { 114 | desc: "Persistent flags utilize one-required and exclusive groups and can fail one-required groups", 115 | flagGroupsOneRequired: []string{"a b", "e f"}, 116 | flagGroupsExclusive: []string{"e f"}, 117 | args: []string{"--e=foo"}, 118 | expectErr: `at least one of the flags in the group [a b] is required`, 119 | }, { 120 | desc: "Persistent flags utilize required and exclusive groups and can fail mutually exclusive groups", 121 | flagGroupsRequired: []string{"a e", "e f"}, 122 | flagGroupsExclusive: []string{"f g"}, 123 | args: []string{"--a=foo", "--e=foo", "--f=foo", "--g=foo"}, 124 | expectErr: `if any flags in the group [f g] are set none of the others can be; [f g] were all set`, 125 | }, { 126 | desc: "Persistent flags utilize required and exclusive groups and can pass", 127 | flagGroupsRequired: []string{"a e", "e f"}, 128 | flagGroupsExclusive: []string{"f g"}, 129 | args: []string{"--a=foo", "--e=foo", "--f=foo"}, 130 | }, { 131 | desc: "Persistent flags utilize one-required and exclusive groups and can pass", 132 | flagGroupsOneRequired: []string{"a e", "e f"}, 133 | flagGroupsExclusive: []string{"f g"}, 134 | args: []string{"--a=foo", "--e=foo", "--f=foo"}, 135 | }, { 136 | desc: "Subcmds can use required groups using inherited flags", 137 | subCmdFlagGroupsRequired: []string{"e subonly"}, 138 | args: []string{"subcmd", "--e=foo", "--subonly=foo"}, 139 | }, { 140 | desc: "Subcmds can use one-required groups using inherited flags", 141 | subCmdFlagGroupsOneRequired: []string{"e subonly"}, 142 | args: []string{"subcmd", "--e=foo", "--subonly=foo"}, 143 | }, { 144 | desc: "Subcmds can use one-required groups using inherited flags and fail one-required groups", 145 | subCmdFlagGroupsOneRequired: []string{"e subonly"}, 146 | args: []string{"subcmd"}, 147 | expectErr: "at least one of the flags in the group [e subonly] is required", 148 | }, { 149 | desc: "Subcmds can use exclusive groups using inherited flags", 150 | subCmdFlagGroupsExclusive: []string{"e subonly"}, 151 | args: []string{"subcmd", "--e=foo", "--subonly=foo"}, 152 | expectErr: "if any flags in the group [e subonly] are set none of the others can be; [e subonly] were all set", 153 | }, { 154 | desc: "Subcmds can use exclusive groups using inherited flags and pass", 155 | subCmdFlagGroupsExclusive: []string{"e subonly"}, 156 | args: []string{"subcmd", "--e=foo"}, 157 | }, { 158 | desc: "Flag groups not applied if not found on invoked command", 159 | subCmdFlagGroupsRequired: []string{"e subonly"}, 160 | args: []string{"--e=foo"}, 161 | }, 162 | } 163 | for _, tc := range testcases { 164 | t.Run(tc.desc, func(t *testing.T) { 165 | c := getCmd() 166 | sub := c.Commands()[0] 167 | for _, flagGroup := range tc.flagGroupsRequired { 168 | c.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...) 169 | } 170 | for _, flagGroup := range tc.flagGroupsOneRequired { 171 | c.MarkFlagsOneRequired(strings.Split(flagGroup, " ")...) 172 | } 173 | for _, flagGroup := range tc.flagGroupsExclusive { 174 | c.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...) 175 | } 176 | for _, flagGroup := range tc.subCmdFlagGroupsRequired { 177 | sub.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...) 178 | } 179 | for _, flagGroup := range tc.subCmdFlagGroupsOneRequired { 180 | sub.MarkFlagsOneRequired(strings.Split(flagGroup, " ")...) 181 | } 182 | for _, flagGroup := range tc.subCmdFlagGroupsExclusive { 183 | sub.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...) 184 | } 185 | c.SetArgs(tc.args) 186 | err := c.Execute() 187 | switch { 188 | case err == nil && len(tc.expectErr) > 0: 189 | t.Errorf("Expected error %q but got nil", tc.expectErr) 190 | case err != nil && err.Error() != tc.expectErr: 191 | t.Errorf("Expected error %q but got %q", tc.expectErr, err) 192 | } 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spf13/cobra 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.6 7 | github.com/inconshreveable/mousetrap v1.1.0 8 | github.com/spf13/pflag v1.0.6 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 3 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 4 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 5 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 6 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 7 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 8 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /powershell_completions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "testing" 21 | ) 22 | 23 | func TestPwshCompletionNoActiveHelp(t *testing.T) { 24 | c := &Command{Use: "c", Run: emptyRun} 25 | 26 | buf := new(bytes.Buffer) 27 | assertNoErr(t, c.GenPowerShellCompletion(buf)) 28 | output := buf.String() 29 | 30 | // check that active help is being disabled 31 | activeHelpVar := activeHelpEnvVar(c.Name()) 32 | check(t, output, fmt.Sprintf("${env:%s}=0", activeHelpVar)) 33 | } 34 | -------------------------------------------------------------------------------- /shell_completions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "github.com/spf13/pflag" 19 | ) 20 | 21 | // MarkFlagRequired instructs the various shell completion implementations to 22 | // prioritize the named flag when performing completion, 23 | // and causes your command to report an error if invoked without the flag. 24 | func (c *Command) MarkFlagRequired(name string) error { 25 | return MarkFlagRequired(c.Flags(), name) 26 | } 27 | 28 | // MarkPersistentFlagRequired instructs the various shell completion implementations to 29 | // prioritize the named persistent flag when performing completion, 30 | // and causes your command to report an error if invoked without the flag. 31 | func (c *Command) MarkPersistentFlagRequired(name string) error { 32 | return MarkFlagRequired(c.PersistentFlags(), name) 33 | } 34 | 35 | // MarkFlagRequired instructs the various shell completion implementations to 36 | // prioritize the named flag when performing completion, 37 | // and causes your command to report an error if invoked without the flag. 38 | func MarkFlagRequired(flags *pflag.FlagSet, name string) error { 39 | return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) 40 | } 41 | 42 | // MarkFlagFilename instructs the various shell completion implementations to 43 | // limit completions for the named flag to the specified file extensions. 44 | func (c *Command) MarkFlagFilename(name string, extensions ...string) error { 45 | return MarkFlagFilename(c.Flags(), name, extensions...) 46 | } 47 | 48 | // MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. 49 | // The bash completion script will call the bash function f for the flag. 50 | // 51 | // This will only work for bash completion. 52 | // It is recommended to instead use c.RegisterFlagCompletionFunc(...) which allows 53 | // to register a Go function which will work across all shells. 54 | func (c *Command) MarkFlagCustom(name string, f string) error { 55 | return MarkFlagCustom(c.Flags(), name, f) 56 | } 57 | 58 | // MarkPersistentFlagFilename instructs the various shell completion 59 | // implementations to limit completions for the named persistent flag to the 60 | // specified file extensions. 61 | func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error { 62 | return MarkFlagFilename(c.PersistentFlags(), name, extensions...) 63 | } 64 | 65 | // MarkFlagFilename instructs the various shell completion implementations to 66 | // limit completions for the named flag to the specified file extensions. 67 | func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error { 68 | return flags.SetAnnotation(name, BashCompFilenameExt, extensions) 69 | } 70 | 71 | // MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. 72 | // The bash completion script will call the bash function f for the flag. 73 | // 74 | // This will only work for bash completion. 75 | // It is recommended to instead use c.RegisterFlagCompletionFunc(...) which allows 76 | // to register a Go function which will work across all shells. 77 | func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error { 78 | return flags.SetAnnotation(name, BashCompCustom, []string{f}) 79 | } 80 | 81 | // MarkFlagDirname instructs the various shell completion implementations to 82 | // limit completions for the named flag to directory names. 83 | func (c *Command) MarkFlagDirname(name string) error { 84 | return MarkFlagDirname(c.Flags(), name) 85 | } 86 | 87 | // MarkPersistentFlagDirname instructs the various shell completion 88 | // implementations to limit completions for the named persistent flag to 89 | // directory names. 90 | func (c *Command) MarkPersistentFlagDirname(name string) error { 91 | return MarkFlagDirname(c.PersistentFlags(), name) 92 | } 93 | 94 | // MarkFlagDirname instructs the various shell completion implementations to 95 | // limit completions for the named flag to directory names. 96 | func MarkFlagDirname(flags *pflag.FlagSet, name string) error { 97 | return flags.SetAnnotation(name, BashCompSubdirsInDir, []string{}) 98 | } 99 | -------------------------------------------------------------------------------- /site/content/active_help.md: -------------------------------------------------------------------------------- 1 | # Active Help 2 | 3 | Active Help is a framework provided by Cobra which allows a program to define messages (hints, warnings, etc) that will be printed during program usage. It aims to make it easier for your users to learn how to use your program. If configured by the program, Active Help is printed when the user triggers shell completion. 4 | 5 | For example, 6 | 7 | ```console 8 | $ helm repo add [tab] 9 | You must choose a name for the repo you are adding. 10 | 11 | $ bin/helm package [tab] 12 | Please specify the path to the chart to package 13 | 14 | $ bin/helm package [tab][tab] 15 | bin/ internal/ scripts/ pkg/ testdata/ 16 | ``` 17 | 18 | **Hint**: A good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions to guide the user in knowing what is expected by the program. 19 | 20 | ## Supported shells 21 | 22 | Active Help is currently only supported for the following shells: 23 | - Bash (using [bash completion V2](completions/_index.md#bash-completion-v2) only). Note that bash 4.4 or higher is required for the prompt to appear when an Active Help message is printed. 24 | - Zsh 25 | 26 | ## Adding Active Help messages 27 | 28 | As Active Help uses the shell completion system, the implementation of Active Help messages is done by enhancing custom dynamic completions. If you are not familiar with dynamic completions, please refer to [Shell Completions](completions/_index.md). 29 | 30 | Adding Active Help is done through the use of the `cobra.AppendActiveHelp(...)` function, where the program repeatedly adds Active Help messages to the list of completions. Keep reading for details. 31 | 32 | ### Active Help for nouns 33 | 34 | Adding Active Help when completing a noun is done within the `ValidArgsFunction(...)` of a command. Please notice the use of `cobra.AppendActiveHelp(...)` in the following example: 35 | 36 | ```go 37 | cmd := &cobra.Command{ 38 | Use: "add [NAME] [URL]", 39 | Short: "add a chart repository", 40 | Args: require.ExactArgs(2), 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | return addRepo(args) 43 | }, 44 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { 45 | var comps []cobra.Completion 46 | if len(args) == 0 { 47 | comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding") 48 | } else if len(args) == 1 { 49 | comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding") 50 | } else { 51 | comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments") 52 | } 53 | return comps, cobra.ShellCompDirectiveNoFileComp 54 | }, 55 | } 56 | ``` 57 | 58 | The example above defines the completions (none, in this specific example) as well as the Active Help messages for the `helm repo add` command. It yields the following behavior: 59 | 60 | ```console 61 | $ helm repo add [tab] 62 | You must choose a name for the repo you are adding 63 | 64 | $ helm repo add grafana [tab] 65 | You must specify the URL for the repo you are adding 66 | 67 | $ helm repo add grafana https://grafana.github.io/helm-charts [tab] 68 | This command does not take any more arguments 69 | ``` 70 | 71 | **Hint**: As can be seen in the above example, a good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions. 72 | 73 | ### Active Help for flags 74 | 75 | Providing Active Help for flags is done in the same fashion as for nouns, but using the completion function registered for the flag. For example: 76 | 77 | ```go 78 | _ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { 79 | if len(args) != 2 { 80 | return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp 81 | } 82 | return compVersionFlag(args[1], toComplete) 83 | }) 84 | ``` 85 | The example above prints an Active Help message when not enough information was given by the user to complete the `--version` flag. 86 | 87 | ```console 88 | $ bin/helm install myrelease --version 2.0.[tab] 89 | You must first specify the chart to install before the --version flag can be completed 90 | 91 | $ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab] 92 | 2.0.1 2.0.2 2.0.3 93 | ``` 94 | 95 | ## User control of Active Help 96 | 97 | You may want to allow your users to disable Active Help or choose between different levels of Active Help. It is entirely up to the program to define the type of configurability of Active Help that it wants to offer, if any. 98 | Allowing to configure Active Help is entirely optional; you can use Active Help in your program without doing anything about Active Help configuration. 99 | 100 | The way to configure Active Help is to use the program's Active Help environment 101 | variable. That variable is named `_ACTIVE_HELP` where `` is the name of your 102 | program in uppercase with any non-ASCII-alphanumeric characters replaced by an `_`. The variable should be set by the user to whatever 103 | Active Help configuration values are supported by the program. 104 | 105 | For example, say `helm` has chosen to support three levels for Active Help: `on`, `off`, `local`. Then a user 106 | would set the desired behavior to `local` by doing `export HELM_ACTIVE_HELP=local` in their shell. 107 | 108 | For simplicity, when in `cmd.ValidArgsFunction(...)` or a flag's completion function, the program should read the 109 | Active Help configuration using the `cobra.GetActiveHelpConfig(cmd)` function and select what Active Help messages 110 | should or should not be added (instead of reading the environment variable directly). 111 | 112 | For example: 113 | 114 | ```go 115 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { 116 | activeHelpLevel := cobra.GetActiveHelpConfig(cmd) 117 | 118 | var comps []cobra.Completion 119 | if len(args) == 0 { 120 | if activeHelpLevel != "off" { 121 | comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding") 122 | } 123 | } else if len(args) == 1 { 124 | if activeHelpLevel != "off" { 125 | comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding") 126 | } 127 | } else { 128 | if activeHelpLevel == "local" { 129 | comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments") 130 | } 131 | } 132 | return comps, cobra.ShellCompDirectiveNoFileComp 133 | }, 134 | ``` 135 | 136 | **Note 1**: If the `_ACTIVE_HELP` environment variable is set to the string "0", Cobra will automatically disable all Active Help output (even if some output was specified by the program using the `cobra.AppendActiveHelp(...)` function). Using "0" can simplify your code in situations where you want to blindly disable Active Help without having to call `cobra.GetActiveHelpConfig(cmd)` explicitly. 137 | 138 | **Note 2**: If a user wants to disable Active Help for every single program based on Cobra, she can set the environment variable `COBRA_ACTIVE_HELP` to "0". In this case `cobra.GetActiveHelpConfig(cmd)` will return "0" no matter what the variable `_ACTIVE_HELP` is set to. 139 | 140 | **Note 3**: If the user does not set `_ACTIVE_HELP` or `COBRA_ACTIVE_HELP` (which will be a common case), the default value for the Active Help configuration returned by `cobra.GetActiveHelpConfig(cmd)` will be the empty string. 141 | 142 | ## Active Help with Cobra's default completion command 143 | 144 | Cobra provides a default `completion` command for programs that wish to use it. 145 | When using the default `completion` command, Active Help is configurable in the same 146 | fashion as described above using environment variables. You may wish to document this in more 147 | details for your users. 148 | 149 | ## Debugging Active Help 150 | 151 | Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](completions/_index.md#debugging) for details. 152 | 153 | When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `_ACTIVE_HELP` where any non-ASCII-alphanumeric characters are replaced by an `_`. For example, we can test deactivating some Active Help as shown below: 154 | 155 | ```console 156 | $ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h 157 | bitnami/haproxy 158 | bitnami/harbor 159 | _activeHelp_ WARNING: cannot re-use a name that is still in use 160 | :0 161 | Completion ended with directive: ShellCompDirectiveDefault 162 | 163 | $ HELM_ACTIVE_HELP=0 bin/helm __complete install wordpress bitnami/h 164 | bitnami/haproxy 165 | bitnami/harbor 166 | :0 167 | Completion ended with directive: ShellCompDirectiveDefault 168 | ``` 169 | -------------------------------------------------------------------------------- /site/content/completions/bash.md: -------------------------------------------------------------------------------- 1 | # Generating Bash Completions For Your cobra.Command 2 | 3 | Please refer to [Shell Completions](_index.md) for details. 4 | 5 | ## Bash legacy dynamic completions 6 | 7 | For backward compatibility, Cobra still supports its legacy dynamic completion solution (described below). Unlike the `ValidArgsFunction` solution, the legacy solution will only work for Bash shell-completion and not for other shells. This legacy solution can be used along-side `ValidArgsFunction` and `RegisterFlagCompletionFunc()`, as long as both solutions are not used for the same command. This provides a path to gradually migrate from the legacy solution to the new solution. 8 | 9 | **Note**: Cobra's default `completion` command uses bash completion V2. If you are currently using Cobra's legacy dynamic completion solution, you should not use the default `completion` command but continue using your own. 10 | 11 | The legacy solution allows you to inject bash functions into the bash completion script. Those bash functions are responsible for providing the completion choices for your own completions. 12 | 13 | Some code that works in kubernetes: 14 | 15 | ```bash 16 | const ( 17 | bash_completion_func = `__kubectl_parse_get() 18 | { 19 | local kubectl_output out 20 | if kubectl_output=$(kubectl get --no-headers "$1" 2>/dev/null); then 21 | out=($(echo "${kubectl_output}" | awk '{print $1}')) 22 | COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) ) 23 | fi 24 | } 25 | 26 | __kubectl_get_resource() 27 | { 28 | if [[ ${#nouns[@]} -eq 0 ]]; then 29 | return 1 30 | fi 31 | __kubectl_parse_get ${nouns[${#nouns[@]} -1]} 32 | if [[ $? -eq 0 ]]; then 33 | return 0 34 | fi 35 | } 36 | 37 | __kubectl_custom_func() { 38 | case ${last_command} in 39 | kubectl_get | kubectl_describe | kubectl_delete | kubectl_stop) 40 | __kubectl_get_resource 41 | return 42 | ;; 43 | *) 44 | ;; 45 | esac 46 | } 47 | `) 48 | ``` 49 | 50 | And then I set that in my command definition: 51 | 52 | ```go 53 | cmds := &cobra.Command{ 54 | Use: "kubectl", 55 | Short: "kubectl controls the Kubernetes cluster manager", 56 | Long: `kubectl controls the Kubernetes cluster manager. 57 | 58 | Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, 59 | Run: runHelp, 60 | BashCompletionFunction: bash_completion_func, 61 | } 62 | ``` 63 | 64 | The `BashCompletionFunction` option is really only valid/useful on the root command. Doing the above will cause `__kubectl_custom_func()` (`___custom_func()`) to be called when the built in processor was unable to find a solution. In the case of kubernetes a valid command might look something like `kubectl get pod [mypod]`. If you type `kubectl get pod [tab][tab]` the `__kubectl_customc_func()` will run because the cobra.Command only understood "kubectl" and "get." `__kubectl_custom_func()` will see that the cobra.Command is "kubectl_get" and will thus call another helper `__kubectl_get_resource()`. `__kubectl_get_resource` will look at the 'nouns' collected. In our example the only noun will be `pod`. So it will call `__kubectl_parse_get pod`. `__kubectl_parse_get` will actually call out to kubernetes and get any pods. It will then set `COMPREPLY` to valid pods! 65 | 66 | Similarly, for flags: 67 | 68 | ```go 69 | annotation := make(map[string][]string) 70 | annotation[cobra.BashCompCustom] = []string{"__kubectl_get_namespaces"} 71 | 72 | flag := &pflag.Flag{ 73 | Name: "namespace", 74 | Usage: usage, 75 | Annotations: annotation, 76 | } 77 | cmd.Flags().AddFlag(flag) 78 | ``` 79 | 80 | In addition add the `__kubectl_get_namespaces` implementation in the `BashCompletionFunction` 81 | value, e.g.: 82 | 83 | ```bash 84 | __kubectl_get_namespaces() 85 | { 86 | local template 87 | template="{{ range .items }}{{ .metadata.name }} {{ end }}" 88 | local kubectl_out 89 | if kubectl_out=$(kubectl get -o template --template="${template}" namespace 2>/dev/null); then 90 | COMPREPLY=( $( compgen -W "${kubectl_out}[*]" -- "$cur" ) ) 91 | fi 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /site/content/completions/fish.md: -------------------------------------------------------------------------------- 1 | ## Generating Fish Completions For Your cobra.Command 2 | 3 | Please refer to [Shell Completions](_index.md) for details. 4 | 5 | -------------------------------------------------------------------------------- /site/content/completions/powershell.md: -------------------------------------------------------------------------------- 1 | # Generating PowerShell Completions For Your Own cobra.Command 2 | 3 | Please refer to [Shell Completions](_index.md#powershell-completions) for details. 4 | -------------------------------------------------------------------------------- /site/content/completions/zsh.md: -------------------------------------------------------------------------------- 1 | ## Generating Zsh Completion For Your cobra.Command 2 | 3 | Please refer to [Shell Completions](_index.md) for details. 4 | 5 | ## Zsh completions standardization 6 | 7 | Cobra 1.1 standardized its zsh completion support to align it with its other shell completions. Although the API was kept backwards-compatible, some small changes in behavior were introduced. 8 | 9 | ### Deprecation summary 10 | 11 | See further below for more details on these deprecations. 12 | 13 | * `cmd.MarkZshCompPositionalArgumentFile(pos, []string{})` is no longer needed. It is therefore **deprecated** and silently ignored. 14 | * `cmd.MarkZshCompPositionalArgumentFile(pos, glob[])` is **deprecated** and silently ignored. 15 | * Instead use `ValidArgsFunction` with `ShellCompDirectiveFilterFileExt`. 16 | * `cmd.MarkZshCompPositionalArgumentWords()` is **deprecated** and silently ignored. 17 | * Instead use `ValidArgsFunction`. 18 | 19 | ### Behavioral changes 20 | 21 | **Noun completion** 22 | |Old behavior|New behavior| 23 | |---|---| 24 | |No file completion by default (opposite of bash)|File completion by default; use `ValidArgsFunction` with `ShellCompDirectiveNoFileComp` to turn off file completion on a per-argument basis| 25 | |Completion of flag names without the `-` prefix having been typed|Flag names are only completed if the user has typed the first `-`| 26 | `cmd.MarkZshCompPositionalArgumentFile(pos, []string{})` used to turn on file completion on a per-argument position basis|File completion for all arguments by default; `cmd.MarkZshCompPositionalArgumentFile()` is **deprecated** and silently ignored| 27 | |`cmd.MarkZshCompPositionalArgumentFile(pos, glob[])` used to turn on file completion **with glob filtering** on a per-argument position basis (zsh-specific)|`cmd.MarkZshCompPositionalArgumentFile()` is **deprecated** and silently ignored; use `ValidArgsFunction` with `ShellCompDirectiveFilterFileExt` for file **extension** filtering (not full glob filtering)| 28 | |`cmd.MarkZshCompPositionalArgumentWords(pos, words[])` used to provide completion choices on a per-argument position basis (zsh-specific)|`cmd.MarkZshCompPositionalArgumentWords()` is **deprecated** and silently ignored; use `ValidArgsFunction` to achieve the same behavior| 29 | 30 | **Flag-value completion** 31 | 32 | |Old behavior|New behavior| 33 | |---|---| 34 | |No file completion by default (opposite of bash)|File completion by default; use `RegisterFlagCompletionFunc()` with `ShellCompDirectiveNoFileComp` to turn off file completion| 35 | |`cmd.MarkFlagFilename(flag, []string{})` and similar used to turn on file completion|File completion by default; `cmd.MarkFlagFilename(flag, []string{})` no longer needed in this context and silently ignored| 36 | |`cmd.MarkFlagFilename(flag, glob[])` used to turn on file completion **with glob filtering** (syntax of `[]string{"*.yaml", "*.yml"}` incompatible with bash)|Will continue to work, however, support for bash syntax is added and should be used instead so as to work for all shells (`[]string{"yaml", "yml"}`)| 37 | |`cmd.MarkFlagDirname(flag)` only completes directories (zsh-specific)|Has been added for all shells| 38 | |Completion of a flag name does not repeat, unless flag is of type `*Array` or `*Slice` (not supported by bash)|Retained for `zsh` and added to `fish`| 39 | |Completion of a flag name does not provide the `=` form (unlike bash)|Retained for `zsh` and added to `fish`| 40 | 41 | **Improvements** 42 | 43 | * Custom completion support (`ValidArgsFunction` and `RegisterFlagCompletionFunc()`) 44 | * File completion by default if no other completions found 45 | * Handling of required flags 46 | * File extension filtering no longer mutually exclusive with bash usage 47 | * Completion of directory names *within* another directory 48 | * Support for `=` form of flags 49 | -------------------------------------------------------------------------------- /site/content/docgen/_index.md: -------------------------------------------------------------------------------- 1 | # Documentation generation 2 | 3 | - [Man page docs](man.md) 4 | - [Markdown docs](md.md) 5 | - [Rest docs](rest.md) 6 | - [Yaml docs](yaml.md) 7 | 8 | ## Options 9 | ### `DisableAutoGenTag` 10 | 11 | You may set `cmd.DisableAutoGenTag = true` 12 | to _entirely_ remove the auto generated string "Auto generated by spf13/cobra..." 13 | from any documentation source. 14 | 15 | ### `InitDefaultCompletionCmd` 16 | 17 | You may call `cmd.InitDefaultCompletionCmd()` to document the default autocompletion command. 18 | -------------------------------------------------------------------------------- /site/content/docgen/man.md: -------------------------------------------------------------------------------- 1 | # Generating Man Pages For Your Own cobra.Command 2 | 3 | Generating man pages from a cobra command is incredibly easy. An example is as follows: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "log" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/cobra/doc" 13 | ) 14 | 15 | func main() { 16 | cmd := &cobra.Command{ 17 | Use: "test", 18 | Short: "my test program", 19 | } 20 | header := &doc.GenManHeader{ 21 | Title: "MINE", 22 | Section: "3", 23 | } 24 | err := doc.GenManTree(cmd, header, "/tmp") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | ``` 30 | 31 | That will get you a man page `/tmp/test.3` 32 | -------------------------------------------------------------------------------- /site/content/docgen/md.md: -------------------------------------------------------------------------------- 1 | # Generating Markdown Docs For Your Own cobra.Command 2 | 3 | Generating Markdown pages from a cobra command is incredibly easy. An example is as follows: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "log" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/cobra/doc" 13 | ) 14 | 15 | func main() { 16 | cmd := &cobra.Command{ 17 | Use: "test", 18 | Short: "my test program", 19 | } 20 | err := doc.GenMarkdownTree(cmd, "/tmp") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | ``` 26 | 27 | That will get you a Markdown document `/tmp/test.md` 28 | 29 | ## Generate markdown docs for the entire command tree 30 | 31 | This program can actually generate docs for the kubectl command in the kubernetes project 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "log" 38 | "io" 39 | "os" 40 | 41 | "k8s.io/kubernetes/pkg/kubectl/cmd" 42 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 43 | 44 | "github.com/spf13/cobra/doc" 45 | ) 46 | 47 | func main() { 48 | kubectl := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, io.Discard, io.Discard) 49 | err := doc.GenMarkdownTree(kubectl, "./") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | ``` 55 | 56 | This will generate a whole series of files, one for each command in the tree, in the directory specified (in this case "./") 57 | 58 | ## Generate markdown docs for a single command 59 | 60 | You may wish to have more control over the output, or only generate for a single command, instead of the entire command tree. If this is the case you may prefer to `GenMarkdown` instead of `GenMarkdownTree` 61 | 62 | ```go 63 | out := new(bytes.Buffer) 64 | err := doc.GenMarkdown(cmd, out) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | ``` 69 | 70 | This will write the markdown doc for ONLY "cmd" into the out, buffer. 71 | 72 | ## Customize the output 73 | 74 | Both `GenMarkdown` and `GenMarkdownTree` have alternate versions with callbacks to get some control of the output: 75 | 76 | ```go 77 | func GenMarkdownTreeCustom(cmd *Command, dir string, filePrepender, linkHandler func(string) string) error { 78 | //... 79 | } 80 | ``` 81 | 82 | ```go 83 | func GenMarkdownCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) string) error { 84 | //... 85 | } 86 | ``` 87 | 88 | The `filePrepender` will prepend the return value given the full filepath to the rendered Markdown file. A common use case is to add front matter to use the generated documentation with [Hugo](https://gohugo.io/): 89 | 90 | ```go 91 | const fmTemplate = `--- 92 | date: %s 93 | title: "%s" 94 | slug: %s 95 | url: %s 96 | --- 97 | ` 98 | 99 | filePrepender := func(filename string) string { 100 | now := time.Now().Format(time.RFC3339) 101 | name := filepath.Base(filename) 102 | base := strings.TrimSuffix(name, path.Ext(name)) 103 | url := "/commands/" + strings.ToLower(base) + "/" 104 | return fmt.Sprintf(fmTemplate, now, strings.Replace(base, "_", " ", -1), base, url) 105 | } 106 | ``` 107 | 108 | The `linkHandler` can be used to customize the rendered internal links to the commands, given a filename: 109 | 110 | ```go 111 | linkHandler := func(name string) string { 112 | base := strings.TrimSuffix(name, path.Ext(name)) 113 | return "/commands/" + strings.ToLower(base) + "/" 114 | } 115 | ``` 116 | -------------------------------------------------------------------------------- /site/content/docgen/rest.md: -------------------------------------------------------------------------------- 1 | # Generating ReStructured Text Docs For Your Own cobra.Command 2 | 3 | Generating ReST pages from a cobra command is incredibly easy. An example is as follows: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "log" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/cobra/doc" 13 | ) 14 | 15 | func main() { 16 | cmd := &cobra.Command{ 17 | Use: "test", 18 | Short: "my test program", 19 | } 20 | err := doc.GenReSTTree(cmd, "/tmp") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | ``` 26 | 27 | That will get you a ReST document `/tmp/test.rst` 28 | 29 | ## Generate ReST docs for the entire command tree 30 | 31 | This program can actually generate docs for the kubectl command in the kubernetes project 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "log" 38 | "io" 39 | "os" 40 | 41 | "k8s.io/kubernetes/pkg/kubectl/cmd" 42 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 43 | 44 | "github.com/spf13/cobra/doc" 45 | ) 46 | 47 | func main() { 48 | kubectl := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, io.Discard, io.Discard) 49 | err := doc.GenReSTTree(kubectl, "./") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | ``` 55 | 56 | This will generate a whole series of files, one for each command in the tree, in the directory specified (in this case "./") 57 | 58 | ## Generate ReST docs for a single command 59 | 60 | You may wish to have more control over the output, or only generate for a single command, instead of the entire command tree. If this is the case you may prefer to `GenReST` instead of `GenReSTTree` 61 | 62 | ```go 63 | out := new(bytes.Buffer) 64 | err := doc.GenReST(cmd, out) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | ``` 69 | 70 | This will write the ReST doc for ONLY "cmd" into the out, buffer. 71 | 72 | ## Customize the output 73 | 74 | Both `GenReST` and `GenReSTTree` have alternate versions with callbacks to get some control of the output: 75 | 76 | ```go 77 | func GenReSTTreeCustom(cmd *Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error { 78 | //... 79 | } 80 | ``` 81 | 82 | ```go 83 | func GenReSTCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string, string) string) error { 84 | //... 85 | } 86 | ``` 87 | 88 | The `filePrepender` will prepend the return value given the full filepath to the rendered ReST file. A common use case is to add front matter to use the generated documentation with [Hugo](https://gohugo.io/): 89 | 90 | ```go 91 | const fmTemplate = `--- 92 | date: %s 93 | title: "%s" 94 | slug: %s 95 | url: %s 96 | --- 97 | ` 98 | filePrepender := func(filename string) string { 99 | now := time.Now().Format(time.RFC3339) 100 | name := filepath.Base(filename) 101 | base := strings.TrimSuffix(name, path.Ext(name)) 102 | url := "/commands/" + strings.ToLower(base) + "/" 103 | return fmt.Sprintf(fmTemplate, now, strings.Replace(base, "_", " ", -1), base, url) 104 | } 105 | ``` 106 | 107 | The `linkHandler` can be used to customize the rendered links to the commands, given a command name and reference. This is useful while converting rst to html or while generating documentation with tools like Sphinx where `:ref:` is used: 108 | 109 | ```go 110 | // Sphinx cross-referencing format 111 | linkHandler := func(name, ref string) string { 112 | return fmt.Sprintf(":ref:`%s <%s>`", name, ref) 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /site/content/docgen/yaml.md: -------------------------------------------------------------------------------- 1 | # Generating Yaml Docs For Your Own cobra.Command 2 | 3 | Generating yaml files from a cobra command is incredibly easy. An example is as follows: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "log" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/cobra/doc" 13 | ) 14 | 15 | func main() { 16 | cmd := &cobra.Command{ 17 | Use: "test", 18 | Short: "my test program", 19 | } 20 | err := doc.GenYamlTree(cmd, "/tmp") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | ``` 26 | 27 | That will get you a Yaml document `/tmp/test.yaml` 28 | 29 | ## Generate yaml docs for the entire command tree 30 | 31 | This program can actually generate docs for the kubectl command in the kubernetes project 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "io" 38 | "log" 39 | "os" 40 | 41 | "k8s.io/kubernetes/pkg/kubectl/cmd" 42 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 43 | 44 | "github.com/spf13/cobra/doc" 45 | ) 46 | 47 | func main() { 48 | kubectl := cmd.NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, io.Discard, io.Discard) 49 | err := doc.GenYamlTree(kubectl, "./") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | ``` 55 | 56 | This will generate a whole series of files, one for each command in the tree, in the directory specified (in this case "./") 57 | 58 | ## Generate yaml docs for a single command 59 | 60 | You may wish to have more control over the output, or only generate for a single command, instead of the entire command tree. If this is the case you may prefer to `GenYaml` instead of `GenYamlTree` 61 | 62 | ```go 63 | out := new(bytes.Buffer) 64 | doc.GenYaml(cmd, out) 65 | ``` 66 | 67 | This will write the yaml doc for ONLY "cmd" into the out, buffer. 68 | 69 | ## Customize the output 70 | 71 | Both `GenYaml` and `GenYamlTree` have alternate versions with callbacks to get some control of the output: 72 | 73 | ```go 74 | func GenYamlTreeCustom(cmd *Command, dir string, filePrepender, linkHandler func(string) string) error { 75 | //... 76 | } 77 | ``` 78 | 79 | ```go 80 | func GenYamlCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) string) error { 81 | //... 82 | } 83 | ``` 84 | 85 | The `filePrepender` will prepend the return value given the full filepath to the rendered Yaml file. A common use case is to add front matter to use the generated documentation with [Hugo](https://gohugo.io/): 86 | 87 | ```go 88 | const fmTemplate = `--- 89 | date: %s 90 | title: "%s" 91 | slug: %s 92 | url: %s 93 | --- 94 | ` 95 | 96 | filePrepender := func(filename string) string { 97 | now := time.Now().Format(time.RFC3339) 98 | name := filepath.Base(filename) 99 | base := strings.TrimSuffix(name, path.Ext(name)) 100 | url := "/commands/" + strings.ToLower(base) + "/" 101 | return fmt.Sprintf(fmTemplate, now, strings.Replace(base, "_", " ", -1), base, url) 102 | } 103 | ``` 104 | 105 | The `linkHandler` can be used to customize the rendered internal links to the commands, given a filename: 106 | 107 | ```go 108 | linkHandler := func(name string) string { 109 | base := strings.TrimSuffix(name, path.Ext(name)) 110 | return "/commands/" + strings.ToLower(base) + "/" 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /site/content/projects_using_cobra.md: -------------------------------------------------------------------------------- 1 | ## Projects using Cobra 2 | 3 | - [Allero](https://github.com/allero-io/allero) 4 | - [Arewefastyet](https://benchmark.vitess.io) 5 | - [Arduino CLI](https://github.com/arduino/arduino-cli) 6 | - [Azion](https://github.com/aziontech/azion) 7 | - [Bleve](https://blevesearch.com/) 8 | - [Cilium](https://cilium.io/) 9 | - [CloudQuery](https://github.com/cloudquery/cloudquery) 10 | - [CockroachDB](https://www.cockroachlabs.com/) 11 | - [Conduit](https://github.com/conduitio/conduit) 12 | - [Constellation](https://github.com/edgelesssys/constellation) 13 | - [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) 14 | - [Datree](https://github.com/datreeio/datree) 15 | - [Delve](https://github.com/derekparker/delve) 16 | - [Docker (distribution)](https://github.com/docker/distribution) 17 | - [Encore](https://encore.dev) 18 | - [Etcd](https://etcd.io/) 19 | - [Gardener](https://github.com/gardener/gardenctl) 20 | - [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl) 21 | - [Git Bump](https://github.com/erdaltsksn/git-bump) 22 | - [GitHub CLI](https://github.com/cli/cli) 23 | - [GitHub Labeler](https://github.com/erdaltsksn/gh-label) 24 | - [Golangci-lint](https://golangci-lint.run) 25 | - [GopherJS](https://github.com/gopherjs/gopherjs) 26 | - [GoReleaser](https://goreleaser.com) 27 | - [Helm](https://helm.sh) 28 | - [Hugo](https://gohugo.io) 29 | - [Incus](https://linuxcontainers.org/incus/) 30 | - [Infracost](https://github.com/infracost/infracost) 31 | - [Istio](https://istio.io) 32 | - [Kool](https://github.com/kool-dev/kool) 33 | - [Kubernetes](https://kubernetes.io/) 34 | - [Kubescape](https://github.com/kubescape/kubescape) 35 | - [KubeVirt](https://github.com/kubevirt/kubevirt) 36 | - [Linkerd](https://linkerd.io/) 37 | - [LXC](https://github.com/canonical/lxd) 38 | - [Mattermost-server](https://github.com/mattermost/mattermost-server) 39 | - [Mercure](https://mercure.rocks/) 40 | - [Meroxa CLI](https://github.com/meroxa/cli) 41 | - [Metal Stack CLI](https://github.com/metal-stack/metalctl) 42 | - [Moby (former Docker)](https://github.com/moby/moby) 43 | - [Moldy](https://github.com/Moldy-Community/moldy) 44 | - [Multi-gitter](https://github.com/lindell/multi-gitter) 45 | - [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack) 46 | - [nFPM](https://nfpm.goreleaser.com) 47 | - [Okteto](https://github.com/okteto/okteto) 48 | - [OpenShift](https://www.openshift.com/) 49 | - [Ory Hydra](https://github.com/ory/hydra) 50 | - [Ory Kratos](https://github.com/ory/kratos) 51 | - [Pixie](https://github.com/pixie-io/pixie) 52 | - [Polygon Edge](https://github.com/0xPolygon/polygon-edge) 53 | - [Pouch](https://github.com/alibaba/pouch) 54 | - [ProjectAtomic (enterprise)](https://www.projectatomic.io/) 55 | - [Prototool](https://github.com/uber/prototool) 56 | - [Pulumi](https://www.pulumi.com) 57 | - [QRcp](https://github.com/claudiodangelis/qrcp) 58 | - [Random](https://github.com/erdaltsksn/random) 59 | - [Rclone](https://rclone.org/) 60 | - [Scaleway CLI](https://github.com/scaleway/scaleway-cli) 61 | - [Sia](https://github.com/SiaFoundation/siad) 62 | - [Skaffold](https://skaffold.dev/) 63 | - [Taikun](https://taikun.cloud/) 64 | - [Tendermint](https://github.com/tendermint/tendermint) 65 | - [Twitch CLI](https://github.com/twitchdev/twitch-cli) 66 | - [UpCloud CLI (`upctl`)](https://github.com/UpCloudLtd/upcloud-cli) 67 | - [Vitess](https://vitess.io) 68 | - VMware's [Tanzu Community Edition](https://github.com/vmware-tanzu/community-edition) & [Tanzu Framework](https://github.com/vmware-tanzu/tanzu-framework) 69 | - [Werf](https://werf.io/) 70 | - [Zarf](https://github.com/defenseunicorns/zarf) 71 | - [ZITADEL](https://github.com/zitadel/zitadel) 72 | -------------------------------------------------------------------------------- /zsh_completions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "os" 22 | ) 23 | 24 | // GenZshCompletionFile generates zsh completion file including descriptions. 25 | func (c *Command) GenZshCompletionFile(filename string) error { 26 | return c.genZshCompletionFile(filename, true) 27 | } 28 | 29 | // GenZshCompletion generates zsh completion file including descriptions 30 | // and writes it to the passed writer. 31 | func (c *Command) GenZshCompletion(w io.Writer) error { 32 | return c.genZshCompletion(w, true) 33 | } 34 | 35 | // GenZshCompletionFileNoDesc generates zsh completion file without descriptions. 36 | func (c *Command) GenZshCompletionFileNoDesc(filename string) error { 37 | return c.genZshCompletionFile(filename, false) 38 | } 39 | 40 | // GenZshCompletionNoDesc generates zsh completion file without descriptions 41 | // and writes it to the passed writer. 42 | func (c *Command) GenZshCompletionNoDesc(w io.Writer) error { 43 | return c.genZshCompletion(w, false) 44 | } 45 | 46 | // MarkZshCompPositionalArgumentFile only worked for zsh and its behavior was 47 | // not consistent with Bash completion. It has therefore been disabled. 48 | // Instead, when no other completion is specified, file completion is done by 49 | // default for every argument. One can disable file completion on a per-argument 50 | // basis by using ValidArgsFunction and ShellCompDirectiveNoFileComp. 51 | // To achieve file extension filtering, one can use ValidArgsFunction and 52 | // ShellCompDirectiveFilterFileExt. 53 | // 54 | // Deprecated 55 | func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { 56 | return nil 57 | } 58 | 59 | // MarkZshCompPositionalArgumentWords only worked for zsh. It has therefore 60 | // been disabled. 61 | // To achieve the same behavior across all shells, one can use 62 | // ValidArgs (for the first argument only) or ValidArgsFunction for 63 | // any argument (can include the first one also). 64 | // 65 | // Deprecated 66 | func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { 67 | return nil 68 | } 69 | 70 | func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error { 71 | outFile, err := os.Create(filename) 72 | if err != nil { 73 | return err 74 | } 75 | defer outFile.Close() 76 | 77 | return c.genZshCompletion(outFile, includeDesc) 78 | } 79 | 80 | func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { 81 | buf := new(bytes.Buffer) 82 | genZshComp(buf, c.Name(), includeDesc) 83 | _, err := buf.WriteTo(w) 84 | return err 85 | } 86 | 87 | func genZshComp(buf io.StringWriter, name string, includeDesc bool) { 88 | compCmd := ShellCompRequestCmd 89 | if !includeDesc { 90 | compCmd = ShellCompNoDescRequestCmd 91 | } 92 | WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[1]s 93 | compdef _%[1]s %[1]s 94 | 95 | # zsh completion for %-36[1]s -*- shell-script -*- 96 | 97 | __%[1]s_debug() 98 | { 99 | local file="$BASH_COMP_DEBUG_FILE" 100 | if [[ -n ${file} ]]; then 101 | echo "$*" >> "${file}" 102 | fi 103 | } 104 | 105 | _%[1]s() 106 | { 107 | local shellCompDirectiveError=%[3]d 108 | local shellCompDirectiveNoSpace=%[4]d 109 | local shellCompDirectiveNoFileComp=%[5]d 110 | local shellCompDirectiveFilterFileExt=%[6]d 111 | local shellCompDirectiveFilterDirs=%[7]d 112 | local shellCompDirectiveKeepOrder=%[8]d 113 | 114 | local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder 115 | local -a completions 116 | 117 | __%[1]s_debug "\n========= starting completion logic ==========" 118 | __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" 119 | 120 | # The user could have moved the cursor backwards on the command-line. 121 | # We need to trigger completion from the $CURRENT location, so we need 122 | # to truncate the command-line ($words) up to the $CURRENT location. 123 | # (We cannot use $CURSOR as its value does not work when a command is an alias.) 124 | words=("${=words[1,CURRENT]}") 125 | __%[1]s_debug "Truncated words[*]: ${words[*]}," 126 | 127 | lastParam=${words[-1]} 128 | lastChar=${lastParam[-1]} 129 | __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" 130 | 131 | # For zsh, when completing a flag with an = (e.g., %[1]s -n=) 132 | # completions must be prefixed with the flag 133 | setopt local_options BASH_REMATCH 134 | if [[ "${lastParam}" =~ '-.*=' ]]; then 135 | # We are dealing with a flag with an = 136 | flagPrefix="-P ${BASH_REMATCH}" 137 | fi 138 | 139 | # Prepare the command to obtain completions 140 | requestComp="${words[1]} %[2]s ${words[2,-1]}" 141 | if [ "${lastChar}" = "" ]; then 142 | # If the last parameter is complete (there is a space following it) 143 | # We add an extra empty parameter so we can indicate this to the go completion code. 144 | __%[1]s_debug "Adding extra empty parameter" 145 | requestComp="${requestComp} \"\"" 146 | fi 147 | 148 | __%[1]s_debug "About to call: eval ${requestComp}" 149 | 150 | # Use eval to handle any environment variables and such 151 | out=$(eval ${requestComp} 2>/dev/null) 152 | __%[1]s_debug "completion output: ${out}" 153 | 154 | # Extract the directive integer following a : from the last line 155 | local lastLine 156 | while IFS='\n' read -r line; do 157 | lastLine=${line} 158 | done < <(printf "%%s\n" "${out[@]}") 159 | __%[1]s_debug "last line: ${lastLine}" 160 | 161 | if [ "${lastLine[1]}" = : ]; then 162 | directive=${lastLine[2,-1]} 163 | # Remove the directive including the : and the newline 164 | local suffix 165 | (( suffix=${#lastLine}+2)) 166 | out=${out[1,-$suffix]} 167 | else 168 | # There is no directive specified. Leave $out as is. 169 | __%[1]s_debug "No directive found. Setting do default" 170 | directive=0 171 | fi 172 | 173 | __%[1]s_debug "directive: ${directive}" 174 | __%[1]s_debug "completions: ${out}" 175 | __%[1]s_debug "flagPrefix: ${flagPrefix}" 176 | 177 | if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 178 | __%[1]s_debug "Completion received error. Ignoring completions." 179 | return 180 | fi 181 | 182 | local activeHelpMarker="%[9]s" 183 | local endIndex=${#activeHelpMarker} 184 | local startIndex=$((${#activeHelpMarker}+1)) 185 | local hasActiveHelp=0 186 | while IFS='\n' read -r comp; do 187 | # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) 188 | if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then 189 | __%[1]s_debug "ActiveHelp found: $comp" 190 | comp="${comp[$startIndex,-1]}" 191 | if [ -n "$comp" ]; then 192 | compadd -x "${comp}" 193 | __%[1]s_debug "ActiveHelp will need delimiter" 194 | hasActiveHelp=1 195 | fi 196 | 197 | continue 198 | fi 199 | 200 | if [ -n "$comp" ]; then 201 | # If requested, completions are returned with a description. 202 | # The description is preceded by a TAB character. 203 | # For zsh's _describe, we need to use a : instead of a TAB. 204 | # We first need to escape any : as part of the completion itself. 205 | comp=${comp//:/\\:} 206 | 207 | local tab="$(printf '\t')" 208 | comp=${comp//$tab/:} 209 | 210 | __%[1]s_debug "Adding completion: ${comp}" 211 | completions+=${comp} 212 | lastComp=$comp 213 | fi 214 | done < <(printf "%%s\n" "${out[@]}") 215 | 216 | # Add a delimiter after the activeHelp statements, but only if: 217 | # - there are completions following the activeHelp statements, or 218 | # - file completion will be performed (so there will be choices after the activeHelp) 219 | if [ $hasActiveHelp -eq 1 ]; then 220 | if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 221 | __%[1]s_debug "Adding activeHelp delimiter" 222 | compadd -x "--" 223 | hasActiveHelp=0 224 | fi 225 | fi 226 | 227 | if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 228 | __%[1]s_debug "Activating nospace." 229 | noSpace="-S ''" 230 | fi 231 | 232 | if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then 233 | __%[1]s_debug "Activating keep order." 234 | keepOrder="-V" 235 | fi 236 | 237 | if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 238 | # File extension filtering 239 | local filteringCmd 240 | filteringCmd='_files' 241 | for filter in ${completions[@]}; do 242 | if [ ${filter[1]} != '*' ]; then 243 | # zsh requires a glob pattern to do file filtering 244 | filter="\*.$filter" 245 | fi 246 | filteringCmd+=" -g $filter" 247 | done 248 | filteringCmd+=" ${flagPrefix}" 249 | 250 | __%[1]s_debug "File filtering command: $filteringCmd" 251 | _arguments '*:filename:'"$filteringCmd" 252 | elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 253 | # File completion for directories only 254 | local subdir 255 | subdir="${completions[1]}" 256 | if [ -n "$subdir" ]; then 257 | __%[1]s_debug "Listing directories in $subdir" 258 | pushd "${subdir}" >/dev/null 2>&1 259 | else 260 | __%[1]s_debug "Listing directories in ." 261 | fi 262 | 263 | local result 264 | _arguments '*:dirname:_files -/'" ${flagPrefix}" 265 | result=$? 266 | if [ -n "$subdir" ]; then 267 | popd >/dev/null 2>&1 268 | fi 269 | return $result 270 | else 271 | __%[1]s_debug "Calling _describe" 272 | if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then 273 | __%[1]s_debug "_describe found some completions" 274 | 275 | # Return the success of having called _describe 276 | return 0 277 | else 278 | __%[1]s_debug "_describe did not find completions." 279 | __%[1]s_debug "Checking if we should do file completion." 280 | if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 281 | __%[1]s_debug "deactivating file completion" 282 | 283 | # We must return an error code here to let zsh know that there were no 284 | # completions found by _describe; this is what will trigger other 285 | # matching algorithms to attempt to find completions. 286 | # For example zsh can match letters in the middle of words. 287 | return 1 288 | else 289 | # Perform file completion 290 | __%[1]s_debug "Activating file completion" 291 | 292 | # We must return the result of this command, so it must be the 293 | # last command, or else we must store its result to return it. 294 | _arguments '*:filename:_files'" ${flagPrefix}" 295 | fi 296 | fi 297 | fi 298 | } 299 | 300 | # don't run the completion function when being source-ed or eval-ed 301 | if [ "$funcstack[1]" = "_%[1]s" ]; then 302 | _%[1]s 303 | fi 304 | `, name, compCmd, 305 | ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 306 | ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, 307 | activeHelpMarker)) 308 | } 309 | -------------------------------------------------------------------------------- /zsh_completions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2023 The Cobra Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cobra 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "testing" 21 | ) 22 | 23 | func TestZshCompletionWithActiveHelp(t *testing.T) { 24 | c := &Command{Use: "c", Run: emptyRun} 25 | 26 | buf := new(bytes.Buffer) 27 | assertNoErr(t, c.GenZshCompletion(buf)) 28 | output := buf.String() 29 | 30 | // check that active help is not being disabled 31 | activeHelpVar := activeHelpEnvVar(c.Name()) 32 | checkOmit(t, output, fmt.Sprintf("%s=0", activeHelpVar)) 33 | } 34 | --------------------------------------------------------------------------------