From e1ded5cd9480aa9921a8cb0f646b6b569ac1b9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 17 May 2022 20:47:47 +0300 Subject: [PATCH 01/39] fix(bash-v2): skip empty completions when filtering descriptions (#1691) `read` gives a last null value following a trailing newline. Regression from fb8031162c2ffab270774f13c6904bb04cbba5a7. --- bash_completionsV2.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bash_completionsV2.go b/bash_completionsV2.go index 5e558a511..97202f1b5 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -188,6 +188,7 @@ __%[1]s_handle_standard_completion_case() { local compline # Look for the longest completion so that we can format things nicely while IFS='' read -r compline; do + [[ -z $compline ]] && continue # Strip any description before checking the length comp=${compline%%%%$tab*} # Only consider the completions that match From ffa8860dbe017851bc4e8ff4fb2b6249cfc1e4d8 Mon Sep 17 00:00:00 2001 From: Andreas Deininger Date: Tue, 17 May 2022 20:28:13 +0200 Subject: [PATCH 02/39] Change links from http:// to https:// (#1695) --- README.md | 4 ++-- doc/md_docs.md | 2 +- doc/rest_docs.md | 2 +- doc/yaml_docs.md | 2 +- projects_using_cobra.md | 10 +++++----- user_guide.md | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 55dc39d0f..2bf152082 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Cobra is a library for creating powerful modern CLI applications. -Cobra is used in many Go projects such as [Kubernetes](http://kubernetes.io/), +Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/), [Hugo](https://gohugo.io), and [Github CLI](https://github.com/cli/cli) to name a few. [This list](./projects_using_cobra.md) contains a more extensive list of projects using Cobra. @@ -28,7 +28,7 @@ Cobra provides: * Automatically generated man pages for your application * Command aliases so you can change things without breaking them * The flexibility to define your own help, usage, etc. -* Optional seamless integration with [viper](http://github.com/spf13/viper) for 12-factor apps +* Optional seamless integration with [viper](https://github.com/spf13/viper) for 12-factor apps # Concepts diff --git a/doc/md_docs.md b/doc/md_docs.md index 5c870625f..1659175cf 100644 --- a/doc/md_docs.md +++ b/doc/md_docs.md @@ -85,7 +85,7 @@ func GenMarkdownCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) } ``` -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](http://gohugo.io/): +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/): ```go const fmTemplate = `--- diff --git a/doc/rest_docs.md b/doc/rest_docs.md index 6098430ef..3041c573a 100644 --- a/doc/rest_docs.md +++ b/doc/rest_docs.md @@ -85,7 +85,7 @@ func GenReSTCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string, str } ``` -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](http://gohugo.io/): +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/): ```go const fmTemplate = `--- diff --git a/doc/yaml_docs.md b/doc/yaml_docs.md index 1a9b7c6a3..172e61d12 100644 --- a/doc/yaml_docs.md +++ b/doc/yaml_docs.md @@ -82,7 +82,7 @@ func GenYamlCustom(cmd *Command, out *bytes.Buffer, linkHandler func(string) str } ``` -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](http://gohugo.io/): +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/): ```go const fmTemplate = `--- diff --git a/projects_using_cobra.md b/projects_using_cobra.md index 1d2266dc3..4ac87d3cc 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -1,8 +1,8 @@ ## Projects using Cobra - [Arduino CLI](https://github.com/arduino/arduino-cli) -- [Bleve](http://www.blevesearch.com/) -- [CockroachDB](http://www.cockroachlabs.com/) +- [Bleve](https://blevesearch.com/) +- [CockroachDB](https://www.cockroachlabs.com/) - [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) - [Datree](https://github.com/datreeio/datree) - [Delve](https://github.com/derekparker/delve) @@ -14,14 +14,14 @@ - [Github CLI](https://github.com/cli/cli) - [GitHub Labeler](https://github.com/erdaltsksn/gh-label) - [Golangci-lint](https://golangci-lint.run) -- [GopherJS](http://www.gopherjs.org/) +- [GopherJS](https://github.com/gopherjs/gopherjs) - [GoReleaser](https://goreleaser.com) - [Helm](https://helm.sh) - [Hugo](https://gohugo.io) - [Infracost](https://github.com/infracost/infracost) - [Istio](https://istio.io) - [Kool](https://github.com/kool-dev/kool) -- [Kubernetes](http://kubernetes.io/) +- [Kubernetes](https://kubernetes.io/) - [Kubescape](https://github.com/armosec/kubescape) - [Linkerd](https://linkerd.io/) - [Mattermost-server](https://github.com/mattermost/mattermost-server) @@ -39,7 +39,7 @@ - [Pixie](https://github.com/pixie-io/pixie) - [Polygon Edge](https://github.com/0xPolygon/polygon-edge) - [Pouch](https://github.com/alibaba/pouch) -- [ProjectAtomic (enterprise)](http://www.projectatomic.io/) +- [ProjectAtomic (enterprise)](https://www.projectatomic.io/) - [Prototool](https://github.com/uber/prototool) - [QRcp](https://github.com/claudiodangelis/qrcp) - [Random](https://github.com/erdaltsksn/random) diff --git a/user_guide.md b/user_guide.md index 56a1e9c60..cde4032bf 100644 --- a/user_guide.md +++ b/user_guide.md @@ -51,7 +51,7 @@ var rootCmd = &cobra.Command{ Short: "Hugo is a very fast static site generator", Long: `A Fast and Flexible Static Site Generator built with love by spf13 and friends in Go. - Complete documentation is available at http://hugo.spf13.com`, + Complete documentation is available at https://gohugo.io/documentation/`, Run: func(cmd *cobra.Command, args []string) { // Do Stuff Here }, @@ -429,7 +429,7 @@ a count and a string.`, } ``` -For a more complete example of a larger application, please checkout [Hugo](http://gohugo.io/). +For a more complete example of a larger application, please checkout [Hugo](https://gohugo.io/). ## Help Command @@ -627,7 +627,7 @@ Did you mean this? Run 'hugo --help' for usage. ``` -Suggestions are automatic based on every subcommand registered and use an implementation of [Levenshtein distance](http://en.wikipedia.org/wiki/Levenshtein_distance). Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion. +Suggestions are automatic based on every subcommand registered and use an implementation of [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion. If you need to disable suggestions or tweak the string distance in your command, use: From 5b11656e45a6a6579298a3b28c71f456ff196ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 18 May 2022 03:57:08 +0300 Subject: [PATCH 03/39] perf(bash-v2): read directly to COMPREPLY on descriptionless short circuit (#1700) Not that it'd really matter that much performancewise given the level we are at for this case, but this change makes the short circuit roughly twice as fast on my box as it was for the 1000 rounds done in marckhouzam/cobra-completion-testing. Perhaps more importantly, this makes the code arguably slightly cleaner. --- bash_completionsV2.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bash_completionsV2.go b/bash_completionsV2.go index 97202f1b5..e61e27c90 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -178,9 +178,7 @@ __%[1]s_handle_standard_completion_case() { # Short circuit to optimize if we don't have descriptions if [[ $out != *$tab* ]]; then - while IFS='' read -r comp; do - COMPREPLY+=("$comp") - done < <(IFS=$'\n' compgen -W "$out" -- "$cur") + IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "$out" -- "$cur") return 0 fi From 37d481d4d40a07b1a8f2f6c345124540e870c85c Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Sun, 5 Jun 2022 21:45:59 +0200 Subject: [PATCH 04/39] fix: Don't complete _command on zsh (#1690) Previously the generated zsh completion script started with the line #compdef _ where is the command that the zsh completion script is generated for. This enabled completions for both and _, but _ is the completion function itself and should not be completed. Furthermore, attempting to autocomplete _ (e.g. typing "_" in a zsh shell) causes zsh to hang. This commit fixes the #compdef line to only complete , not _. Co-authored-by: Arvid Norlander --- zsh_completions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zsh_completions.go b/zsh_completions.go index c6048a9e5..072921f91 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -75,7 +75,7 @@ func genZshComp(buf io.StringWriter, name string, includeDesc bool) { if !includeDesc { compCmd = ShellCompNoDescRequestCmd } - WriteStringAndCheck(buf, fmt.Sprintf(`#compdef _%[1]s %[1]s + WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[1]s # zsh completion for %-36[1]s -*- shell-script -*- From ca8e3c2779b8cc6168ab6ffdf9c1f2ac65652404 Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Wed, 8 Jun 2022 14:12:54 -0700 Subject: [PATCH 05/39] Add Pulumi as a project using cobra (#1720) * Add Pulumi as a project using cobra --- projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index 4ac87d3cc..ac680118e 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -41,6 +41,7 @@ - [Pouch](https://github.com/alibaba/pouch) - [ProjectAtomic (enterprise)](https://www.projectatomic.io/) - [Prototool](https://github.com/uber/prototool) +- [Pulumi](https://www.pulumi.com) - [QRcp](https://github.com/claudiodangelis/qrcp) - [Random](https://github.com/erdaltsksn/random) - [Rclone](https://rclone.org/) From 87ea1807f71dfa4d0bd7058c099b3978b128aea6 Mon Sep 17 00:00:00 2001 From: xuwu Date: Mon, 13 Jun 2022 10:22:49 +0800 Subject: [PATCH 06/39] Modify brew prefix path in macOS system (#1719) --- completions.go | 4 ++-- shell_completions.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/completions.go b/completions.go index ce1caab82..3de4e87a1 100644 --- a/completions.go +++ b/completions.go @@ -652,7 +652,7 @@ To load completions for every new session, execute once: #### macOS: - %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s + %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s You will need to start a new shell for this setup to take effect. `, c.Root().Name()), @@ -689,7 +689,7 @@ To load completions for every new session, execute once: #### macOS: - %[1]s completion zsh > /usr/local/share/zsh/site-functions/_%[1]s + %[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s You will need to start a new shell for this setup to take effect. `, c.Root().Name()), diff --git a/shell_completions.md b/shell_completions.md index be17bf8c3..1e2058ed6 100644 --- a/shell_completions.md +++ b/shell_completions.md @@ -40,7 +40,7 @@ Bash: # Linux: $ %[1]s completion bash > /etc/bash_completion.d/%[1]s # macOS: - $ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s + $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s Zsh: From 7dc8b004e653f2f69511378d52c2989c8f64d42b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:08:55 -0600 Subject: [PATCH 07/39] Bump actions/setup-go from 2 to 3 (#1660) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b3fb3690..0c83f303d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 with: go-version: '1.17' @@ -41,7 +41,7 @@ jobs: runs-on: ${{ matrix.platform }}-latest steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 with: go-version: 1.${{ matrix.go }}.x From f464d6c82e9af74b7a46301a775163984af32cd1 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Wed, 15 Jun 2022 20:08:16 -0400 Subject: [PATCH 08/39] Add Active Help support (#1482) --- active_help.go | 49 +++++ active_help.md | 157 +++++++++++++++ active_help_test.go | 386 +++++++++++++++++++++++++++++++++++++ bash_completions.go | 5 +- bash_completionsV2.go | 59 +++++- bash_completionsV2_test.go | 19 ++ bash_completions_test.go | 12 ++ completions.go | 6 + fish_completions.go | 5 +- fish_completions_test.go | 13 ++ power_completions_test.go | 19 ++ powershell_completions.go | 7 +- user_guide.md | 4 + zsh_completions.go | 31 ++- zsh_completions_test.go | 19 ++ 15 files changed, 776 insertions(+), 15 deletions(-) create mode 100644 active_help.go create mode 100644 active_help.md create mode 100644 active_help_test.go create mode 100644 bash_completionsV2_test.go create mode 100644 power_completions_test.go create mode 100644 zsh_completions_test.go diff --git a/active_help.go b/active_help.go new file mode 100644 index 000000000..0c631913d --- /dev/null +++ b/active_help.go @@ -0,0 +1,49 @@ +package cobra + +import ( + "fmt" + "os" + "strings" +) + +const ( + activeHelpMarker = "_activeHelp_ " + // The below values should not be changed: programs will be using them explicitly + // in their user documentation, and users will be using them explicitly. + activeHelpEnvVarSuffix = "_ACTIVE_HELP" + activeHelpGlobalEnvVar = "COBRA_ACTIVE_HELP" + activeHelpGlobalDisable = "0" +) + +// AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp. +// Such strings will be processed by the completion script and will be shown as ActiveHelp +// to the user. +// The array parameter should be the array that will contain the completions. +// This function can be called multiple times before and/or after completions are added to +// the array. Each time this function is called with the same array, the new +// ActiveHelp line will be shown below the previous ones when completion is triggered. +func AppendActiveHelp(compArray []string, activeHelpStr string) []string { + return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr)) +} + +// GetActiveHelpConfig returns the value of the ActiveHelp environment variable +// _ACTIVE_HELP where is the name of the root command in upper +// case, with all - replaced by _. +// It will always return "0" if the global environment variable COBRA_ACTIVE_HELP +// is set to "0". +func GetActiveHelpConfig(cmd *Command) string { + activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar) + if activeHelpCfg != activeHelpGlobalDisable { + activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name())) + } + return activeHelpCfg +} + +// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment +// variable. It has the format _ACTIVE_HELP where is the name of the +// root command in upper case, with all - replaced by _. +func activeHelpEnvVar(name string) string { + // This format should not be changed: users will be using it explicitly. + activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) + return strings.ReplaceAll(activeHelpEnvVar, "-", "_") +} diff --git a/active_help.md b/active_help.md new file mode 100644 index 000000000..5e7f59af3 --- /dev/null +++ b/active_help.md @@ -0,0 +1,157 @@ +# Active Help + +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. + +For example, +``` +bash-5.1$ helm repo add [tab] +You must choose a name for the repo you are adding. + +bash-5.1$ bin/helm package [tab] +Please specify the path to the chart to package + +bash-5.1$ bin/helm package [tab][tab] +bin/ internal/ scripts/ pkg/ testdata/ +``` + +**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. +## Supported shells + +Active Help is currently only supported for the following shells: +- Bash (using [bash completion V2](shell_completions.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. +- Zsh + +## Adding Active Help messages + +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](shell_completions.md). + +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. + +### Active Help for nouns + +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: + +```go +cmd := &cobra.Command{ + Use: "add [NAME] [URL]", + Short: "add a chart repository", + Args: require.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return addRepo(args) + }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + if len(args) == 0 { + comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding") + } else if len(args) == 1 { + comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding") + } else { + comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments") + } + return comps, cobra.ShellCompDirectiveNoFileComp + }, +} +``` +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: +``` +bash-5.1$ helm repo add [tab] +You must choose a name for the repo you are adding + +bash-5.1$ helm repo add grafana [tab] +You must specify the URL for the repo you are adding + +bash-5.1$ helm repo add grafana https://grafana.github.io/helm-charts [tab] +This command does not take any more arguments +``` +**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. + +### Active Help for flags + +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: +```go +_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 2 { + return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp + } + return compVersionFlag(args[1], toComplete) + }) +``` +The example above prints an Active Help message when not enough information was given by the user to complete the `--version` flag. +``` +bash-5.1$ bin/helm install myrelease --version 2.0.[tab] +You must first specify the chart to install before the --version flag can be completed + +bash-5.1$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab] +2.0.1 2.0.2 2.0.3 +``` + +## User control of Active Help + +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. +Allowing to configure Active Help is entirely optional; you can use Active Help in your program without doing anything about Active Help configuration. + +The way to configure Active Help is to use the program's Active Help environment +variable. That variable is named `_ACTIVE_HELP` where `` is the name of your +program in uppercase with any `-` replaced by an `_`. The variable should be set by the user to whatever +Active Help configuration values are supported by the program. + +For example, say `helm` has chosen to support three levels for Active Help: `on`, `off`, `local`. Then a user +would set the desired behavior to `local` by doing `export HELM_ACTIVE_HELP=local` in their shell. + +For simplicity, when in `cmd.ValidArgsFunction(...)` or a flag's completion function, the program should read the +Active Help configuration using the `cobra.GetActiveHelpConfig(cmd)` function and select what Active Help messages +should or should not be added (instead of reading the environment variable directly). + +For example: +```go +ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + activeHelpLevel := cobra.GetActiveHelpConfig(cmd) + + var comps []string + if len(args) == 0 { + if activeHelpLevel != "off" { + comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding") + } + } else if len(args) == 1 { + if activeHelpLevel != "off" { + comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding") + } + } else { + if activeHelpLevel == "local" { + comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments") + } + } + return comps, cobra.ShellCompDirectiveNoFileComp +}, +``` +**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. + +**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. + +**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. +## Active Help with Cobra's default completion command + +Cobra provides a default `completion` command for programs that wish to use it. +When using the default `completion` command, Active Help is configurable in the same +fashion as described above using environment variables. You may wish to document this in more +details for your users. + +## Debugging Active Help + +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](shell_completions.md#debugging) for details. + +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 `-` is replaced by an `_`. For example, we can test deactivating some Active Help as shown below: +``` +$ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h +bitnami/haproxy +bitnami/harbor +_activeHelp_ WARNING: cannot re-use a name that is still in use +:0 +Completion ended with directive: ShellCompDirectiveDefault + +$ HELM_ACTIVE_HELP=0 bin/helm __complete install wordpress bitnami/h +bitnami/haproxy +bitnami/harbor +:0 +Completion ended with directive: ShellCompDirectiveDefault +``` diff --git a/active_help_test.go b/active_help_test.go new file mode 100644 index 000000000..524be884b --- /dev/null +++ b/active_help_test.go @@ -0,0 +1,386 @@ +package cobra + +import ( + "fmt" + "os" + "strings" + "testing" +) + +const ( + activeHelpMessage = "This is an activeHelp message" + activeHelpMessage2 = "This is the rest of the activeHelp message" +) + +func TestActiveHelpAlone(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + activeHelpFunc := func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := AppendActiveHelp(nil, activeHelpMessage) + return comps, ShellCompDirectiveDefault + } + + // Test that activeHelp can be added to a root command + rootCmd.ValidArgsFunction = activeHelpFunc + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + rootCmd.ValidArgsFunction = nil + + // Test that activeHelp can be added to a child command + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + childCmd.ValidArgsFunction = activeHelpFunc + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestActiveHelpWithComps(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + // Test that activeHelp can be added following other completions + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{"first", "second"} + comps = AppendActiveHelp(comps, activeHelpMessage) + return comps, ShellCompDirectiveDefault + } + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + "first", + "second", + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that activeHelp can be added preceding other completions + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + var comps []string + comps = AppendActiveHelp(comps, activeHelpMessage) + comps = append(comps, []string{"first", "second"}...) + return comps, ShellCompDirectiveDefault + } + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + "first", + "second", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that activeHelp can be added interleaved with other completions + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{"first"} + comps = AppendActiveHelp(comps, activeHelpMessage) + comps = append(comps, "second") + return comps, ShellCompDirectiveDefault + } + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "first", + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + "second", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestMultiActiveHelp(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + // Test that multiple activeHelp message can be added + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := AppendActiveHelp(nil, activeHelpMessage) + comps = AppendActiveHelp(comps, activeHelpMessage2) + return comps, ShellCompDirectiveNoFileComp + } + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage2), + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that multiple activeHelp messages can be used along with completions + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{"first"} + comps = AppendActiveHelp(comps, activeHelpMessage) + comps = append(comps, "second") + comps = AppendActiveHelp(comps, activeHelpMessage2) + return comps, ShellCompDirectiveNoFileComp + } + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "first", + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + "second", + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage2), + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestActiveHelpForFlag(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + flagname := "flag" + rootCmd.Flags().String(flagname, "", "A flag") + + // Test that multiple activeHelp message can be added + _ = rootCmd.RegisterFlagCompletionFunc(flagname, func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{"first"} + comps = AppendActiveHelp(comps, activeHelpMessage) + comps = append(comps, "second") + comps = AppendActiveHelp(comps, activeHelpMessage2) + return comps, ShellCompDirectiveNoFileComp + }) + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--flag", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + "first", + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage), + "second", + fmt.Sprintf("%s%s", activeHelpMarker, activeHelpMessage2), + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestConfigActiveHelp(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + activeHelpCfg := "someconfig,anotherconfig" + // Set the variable that the user would be setting + os.Setenv(activeHelpEnvVar(rootCmd.Name()), activeHelpCfg) + + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + receivedActiveHelpCfg := GetActiveHelpConfig(cmd) + if receivedActiveHelpCfg != activeHelpCfg { + t.Errorf("expected activeHelpConfig: %q, but got: %q", activeHelpCfg, receivedActiveHelpCfg) + } + return nil, ShellCompDirectiveDefault + } + + _, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Test active help config for a flag + activeHelpCfg = "a config for a flag" + // Set the variable that the completions scripts will be setting + os.Setenv(activeHelpEnvVar(rootCmd.Name()), activeHelpCfg) + + flagname := "flag" + childCmd.Flags().String(flagname, "", "A flag") + + // Test that multiple activeHelp message can be added + _ = childCmd.RegisterFlagCompletionFunc(flagname, func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + receivedActiveHelpCfg := GetActiveHelpConfig(cmd) + if receivedActiveHelpCfg != activeHelpCfg { + t.Errorf("expected activeHelpConfig: %q, but got: %q", activeHelpCfg, receivedActiveHelpCfg) + } + return nil, ShellCompDirectiveDefault + }) + + _, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "--flag", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestDisableActiveHelp(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + // Test the disabling of activeHelp using the specific program + // environment variable that the completions scripts will be setting. + // Make sure the disabling value is "0" by hard-coding it in the tests; + // this is for backwards-compatibility as programs will be using this value. + os.Setenv(activeHelpEnvVar(rootCmd.Name()), "0") + + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{"first"} + comps = AppendActiveHelp(comps, activeHelpMessage) + return comps, ShellCompDirectiveDefault + } + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + os.Unsetenv(activeHelpEnvVar(rootCmd.Name())) + + // Make sure there is no ActiveHelp in the output + expected := strings.Join([]string{ + "first", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Now test the global disabling of ActiveHelp + os.Setenv(activeHelpGlobalEnvVar, "0") + // Set the specific variable, to make sure it is ignored when the global env + // var is set properly + os.Setenv(activeHelpEnvVar(rootCmd.Name()), "1") + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Make sure there is no ActiveHelp in the output + expected = strings.Join([]string{ + "first", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Make sure that if the global env variable is set to anything else than + // the disable value it is ignored + os.Setenv(activeHelpGlobalEnvVar, "on") + // Set the specific variable, to make sure it is used (while ignoring the global env var) + activeHelpCfg := "1" + os.Setenv(activeHelpEnvVar(rootCmd.Name()), activeHelpCfg) + + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + receivedActiveHelpCfg := GetActiveHelpConfig(cmd) + if receivedActiveHelpCfg != activeHelpCfg { + t.Errorf("expected activeHelpConfig: %q, but got: %q", activeHelpCfg, receivedActiveHelpCfg) + } + return nil, ShellCompDirectiveDefault + } + + _, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} diff --git a/bash_completions.go b/bash_completions.go index 3e1e52846..cb7e19537 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -73,7 +73,8 @@ __%[1]s_handle_go_custom_completion() # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly %[1]s allows to handle aliases args=("${words[@]:1}") - requestComp="${words[0]} %[2]s ${args[*]}" + # Disable ActiveHelp which is not supported for bash completion v1 + requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}" lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} @@ -383,7 +384,7 @@ __%[1]s_handle_word() `, name, ShellCompNoDescRequestCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name))) } func writePostscript(buf io.StringWriter, name string) { diff --git a/bash_completionsV2.go b/bash_completionsV2.go index e61e27c90..d295791e6 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -111,13 +111,18 @@ __%[1]s_process_completion_results() { fi fi + # Separate activeHelp from normal completions + local completions=() + local activeHelp=() + __%[1]s_extract_activeHelp + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then # File extension filtering local fullFilter filter filteringCmd - # Do not use quotes around the $out variable or else newline + # Do not use quotes around the $completions variable or else newline # characters will be kept. - for filter in ${out}; do + for filter in ${completions[*]}; do fullFilter+="$filter|" done @@ -129,7 +134,7 @@ __%[1]s_process_completion_results() { # Use printf to strip any trailing newline local subdir - subdir=$(printf "%%s" "${out}") + subdir=$(printf "%%s" "${completions[0]}") if [ -n "$subdir" ]; then __%[1]s_debug "Listing directories in $subdir" pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return @@ -143,6 +148,43 @@ __%[1]s_process_completion_results() { __%[1]s_handle_special_char "$cur" : __%[1]s_handle_special_char "$cur" = + + # Print the activeHelp statements before we finish + if [ ${#activeHelp} -ne 0 ]; then + printf "\n"; + printf "%%s\n" "${activeHelp[@]}" + printf "\n" + + # The prompt format is only available from bash 4.4. + # We test if it is available before using it. + if (x=${PS1@P}) 2> /dev/null; then + printf "%%s" "${PS1@P}${COMP_LINE[@]}" + else + # Can't print the prompt. Just print the + # text the user had typed, it is workable enough. + printf "%%s" "${COMP_LINE[@]}" + fi + fi +} + +# Separate activeHelp lines from real completions. +# Fills the $activeHelp and $completions arrays. +__%[1]s_extract_activeHelp() { + local activeHelpMarker="%[8]s" + local endIndex=${#activeHelpMarker} + + while IFS='' read -r comp; do + if [ "${comp:0:endIndex}" = "$activeHelpMarker" ]; then + comp=${comp:endIndex} + __%[1]s_debug "ActiveHelp found: $comp" + if [ -n "$comp" ]; then + activeHelp+=("$comp") + fi + else + # Not an activeHelp line but a normal completion + completions+=("$comp") + fi + done < <(printf "%%s\n" "${out}") } __%[1]s_handle_completion_types() { @@ -163,7 +205,7 @@ __%[1]s_handle_completion_types() { if [[ $comp == "$cur"* ]]; then COMPREPLY+=("$comp") fi - done < <(printf "%%s\n" "${out}") + done < <(printf "%%s\n" "${completions[@]}") ;; *) @@ -177,8 +219,8 @@ __%[1]s_handle_standard_completion_case() { local tab=$'\t' comp # Short circuit to optimize if we don't have descriptions - if [[ $out != *$tab* ]]; then - IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n' compgen -W "$out" -- "$cur") + if [[ ${completions[*]} != *$tab* ]]; then + IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") return 0 fi @@ -195,7 +237,7 @@ __%[1]s_handle_standard_completion_case() { if ((${#comp}>longest)); then longest=${#comp} fi - done < <(printf "%%s\n" "${out}") + done < <(printf "%%s\n" "${completions[@]}") # If there is a single completion left, remove the description text if [ ${#COMPREPLY[*]} -eq 1 ]; then @@ -305,7 +347,8 @@ fi # ex: ts=4 sw=4 et filetype=sh `, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, + activeHelpMarker)) } // GenBashCompletionFileV2 generates Bash completion version 2. diff --git a/bash_completionsV2_test.go b/bash_completionsV2_test.go new file mode 100644 index 000000000..a9a277ed2 --- /dev/null +++ b/bash_completionsV2_test.go @@ -0,0 +1,19 @@ +package cobra + +import ( + "bytes" + "fmt" + "testing" +) + +func TestBashCompletionV2WithActiveHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + + buf := new(bytes.Buffer) + assertNoErr(t, c.GenBashCompletionV2(buf, true)) + output := buf.String() + + // check that active help is not being disabled + activeHelpVar := activeHelpEnvVar(c.Name()) + checkOmit(t, output, fmt.Sprintf("%s=0", activeHelpVar)) +} diff --git a/bash_completions_test.go b/bash_completions_test.go index 21d1fe0eb..6896e91c7 100644 --- a/bash_completions_test.go +++ b/bash_completions_test.go @@ -261,3 +261,15 @@ func TestBashCompletionTraverseChildren(t *testing.T) { checkOmit(t, output, `local_nonpersistent_flags+=("--bool-flag")`) checkOmit(t, output, `local_nonpersistent_flags+=("-b")`) } + +func TestBashCompletionNoActiveHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + + buf := new(bytes.Buffer) + assertNoErr(t, c.GenBashCompletion(buf)) + output := buf.String() + + // check that active help is being disabled + activeHelpVar := activeHelpEnvVar(c.Name()) + check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) +} diff --git a/completions.go b/completions.go index 3de4e87a1..7a9a7fade 100644 --- a/completions.go +++ b/completions.go @@ -178,6 +178,12 @@ func (c *Command) initCompleteCmd(args []string) { noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) for _, comp := range completions { + if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable { + // Remove all activeHelp entries in this case + if strings.HasPrefix(comp, activeHelpMarker) { + continue + } + } if noDescriptions { // Remove any description that may be included following a tab character. comp = strings.Split(comp, "\t")[0] diff --git a/fish_completions.go b/fish_completions.go index e14a06ea2..005ee6be7 100644 --- a/fish_completions.go +++ b/fish_completions.go @@ -38,7 +38,8 @@ function __%[1]s_perform_completion __%[1]s_debug "args: $args" __%[1]s_debug "last arg: $lastArg" - set -l requestComp "$args[1] %[3]s $args[2..-1] $lastArg" + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "%[9]s=0 $args[1] %[3]s $args[2..-1] $lastArg" __%[1]s_debug "Calling $requestComp" set -l results (eval $requestComp 2> /dev/null) @@ -196,7 +197,7 @@ complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' `, nameForVar, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name))) } // GenFishCompletion generates fish completion file and writes to the passed writer. diff --git a/fish_completions_test.go b/fish_completions_test.go index a3171e481..37091dcba 100644 --- a/fish_completions_test.go +++ b/fish_completions_test.go @@ -2,6 +2,7 @@ package cobra import ( "bytes" + "fmt" "testing" ) @@ -67,3 +68,15 @@ func TestProgWithColon(t *testing.T) { check(t, output, "-c root:colon") checkOmit(t, output, "-c root_colon") } + +func TestFishCompletionNoActiveHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + + buf := new(bytes.Buffer) + assertNoErr(t, c.GenFishCompletion(buf, true)) + output := buf.String() + + // check that active help is being disabled + activeHelpVar := activeHelpEnvVar(c.Name()) + check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) +} diff --git a/power_completions_test.go b/power_completions_test.go new file mode 100644 index 000000000..771383597 --- /dev/null +++ b/power_completions_test.go @@ -0,0 +1,19 @@ +package cobra + +import ( + "bytes" + "fmt" + "testing" +) + +func TestPwshCompletionNoActiveHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + + buf := new(bytes.Buffer) + assertNoErr(t, c.GenPowerShellCompletion(buf)) + output := buf.String() + + // check that active help is being disabled + activeHelpVar := activeHelpEnvVar(c.Name()) + check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) +} diff --git a/powershell_completions.go b/powershell_completions.go index 62d719f0b..379e7c088 100644 --- a/powershell_completions.go +++ b/powershell_completions.go @@ -61,6 +61,7 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock { # Prepare the command to request completions for the program. # Split the command at the first space to separate the program and arguments. $Program,$Arguments = $Command.Split(" ",2) + $RequestComp="$Program %[2]s $Arguments" __%[1]s_debug "RequestComp: $RequestComp" @@ -90,11 +91,13 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock { } __%[1]s_debug "Calling $RequestComp" + # First disable ActiveHelp which is not supported for Powershell + $env:%[8]s=0 + #call the command store the output in $out and redirect stderr and stdout to null # $Out is an array contains each line per element Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null - # get directive from last line [int]$Directive = $Out[-1].TrimStart(':') if ($Directive -eq "") { @@ -242,7 +245,7 @@ Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock { } `, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name))) } func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { diff --git a/user_guide.md b/user_guide.md index cde4032bf..3a253de99 100644 --- a/user_guide.md +++ b/user_guide.md @@ -660,3 +660,7 @@ Cobra can generate documentation based on subcommands, flags, etc. Read more abo ## Generating shell completions Cobra can generate a shell-completion file for the following shells: bash, zsh, fish, PowerShell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md). + +## Providing Active Help + +Cobra makes use of the shell-completion system to define a framework allowing you to provide Active Help to your users. Active Help are messages (hints, warnings, etc) printed as the program is being used. Read more about it in [Active Help](active_help.md). diff --git a/zsh_completions.go b/zsh_completions.go index 072921f91..65cd94c60 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -163,7 +163,24 @@ _%[1]s() return fi + local activeHelpMarker="%[8]s" + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __%[1]s_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __%[1]s_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + if [ -n "$comp" ]; then # If requested, completions are returned with a description. # The description is preceded by a TAB character. @@ -180,6 +197,17 @@ _%[1]s() fi done < <(printf "%%s\n" "${out[@]}") + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __%[1]s_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then __%[1]s_debug "Activating nospace." noSpace="-S ''" @@ -254,5 +282,6 @@ if [ "$funcstack[1]" = "_%[1]s" ]; then fi `, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, + activeHelpMarker)) } diff --git a/zsh_completions_test.go b/zsh_completions_test.go new file mode 100644 index 000000000..b7addb4ca --- /dev/null +++ b/zsh_completions_test.go @@ -0,0 +1,19 @@ +package cobra + +import ( + "bytes" + "fmt" + "testing" +) + +func TestZshCompletionWithActiveHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + + buf := new(bytes.Buffer) + assertNoErr(t, c.GenZshCompletion(buf)) + output := buf.String() + + // check that active help is not being disabled + activeHelpVar := activeHelpEnvVar(c.Name()) + checkOmit(t, output, fmt.Sprintf("%s=0", activeHelpVar)) +} From ed7bb9dda481082672ed9ea60c29b3b70ae0fc04 Mon Sep 17 00:00:00 2001 From: kida <42762517+t29kida@users.noreply.github.com> Date: Mon, 20 Jun 2022 11:38:49 +0900 Subject: [PATCH 09/39] Add unit test for fish completion (#1515) --- fish_completions_test.go | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/fish_completions_test.go b/fish_completions_test.go index 37091dcba..381c66770 100644 --- a/fish_completions_test.go +++ b/fish_completions_test.go @@ -3,6 +3,8 @@ package cobra import ( "bytes" "fmt" + "log" + "os" "testing" ) @@ -80,3 +82,57 @@ func TestFishCompletionNoActiveHelp(t *testing.T) { activeHelpVar := activeHelpEnvVar(c.Name()) check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) } + +func TestGenFishCompletionFile(t *testing.T) { + err := os.Mkdir("./tmp", 0755) + if err != nil { + log.Fatal(err.Error()) + } + + defer os.RemoveAll("./tmp") + + rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} + child := &Command{ + Use: "child", + ValidArgsFunction: validArgsFunc, + Run: emptyRun, + } + rootCmd.AddCommand(child) + + assertNoErr(t, rootCmd.GenFishCompletionFile("./tmp/test", false)) +} + +func TestFailGenFishCompletionFile(t *testing.T) { + err := os.Mkdir("./tmp", 0755) + if err != nil { + log.Fatal(err.Error()) + } + + defer os.RemoveAll("./tmp") + + f, _ := os.OpenFile("./tmp/test", os.O_CREATE, 0400) + defer f.Close() + + rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} + child := &Command{ + Use: "child", + ValidArgsFunction: validArgsFunc, + Run: emptyRun, + } + rootCmd.AddCommand(child) + + got := rootCmd.GenFishCompletionFile("./tmp/test", false) + if got == nil { + t.Error("should raise permission denied error") + } + + if os.Getenv("MSYSTEM") == "MINGW64" { + if got.Error() != "open ./tmp/test: Access is denied." { + t.Errorf("got: %s, want: %s", got.Error(), "open ./tmp/test: Access is denied.") + } + } else { + if got.Error() != "open ./tmp/test: permission denied" { + t.Errorf("got: %s, want: %s", got.Error(), "open ./tmp/test: permission denied") + } + } +} From 7c9831d376a6c048ae9f95b435cdde9dc14358a0 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Mon, 20 Jun 2022 00:11:21 -0400 Subject: [PATCH 10/39] Fix handling of descriptions for bash v3 (#1735) Fixes #1734 Tab characters that introduce completion descriptions weren't properly being handled with bash v3. This change fixes that. Signed-off-by: Marc Khouzam --- bash_completionsV2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bash_completionsV2.go b/bash_completionsV2.go index d295791e6..767bf0312 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -219,7 +219,7 @@ __%[1]s_handle_standard_completion_case() { local tab=$'\t' comp # Short circuit to optimize if we don't have descriptions - if [[ ${completions[*]} != *$tab* ]]; then + if [[ "${completions[*]}" != *$tab* ]]; then IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") return 0 fi From ea94a3db55f84f026891709d82ebf25e17f89e0d Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Tue, 21 Jun 2022 03:00:50 +0100 Subject: [PATCH 11/39] undefined or nil Args default to ArbitraryArgs (#1612) --- command.go | 2 +- user_guide.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 0f4511f38..3349be636 100644 --- a/command.go +++ b/command.go @@ -1012,7 +1012,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { func (c *Command) ValidateArgs(args []string) error { if c.Args == nil { - return nil + return ArbitraryArgs(c, args) } return c.Args(c, args) } diff --git a/user_guide.md b/user_guide.md index 3a253de99..5a7acf88e 100644 --- a/user_guide.md +++ b/user_guide.md @@ -326,8 +326,8 @@ In both of these cases: ## Positional and Custom Arguments -Validation of positional arguments can be specified using the `Args` field -of `Command`. +Validation of positional arguments can be specified using the `Args` field of `Command`. +If `Args` is undefined or `nil`, it defaults to `ArbitraryArgs`. The following validators are built in: From b9ca5949e2f58373e8e4c3823c213401f7d9d0e3 Mon Sep 17 00:00:00 2001 From: Paul Holzinger <45212748+Luap99@users.noreply.github.com> Date: Tue, 21 Jun 2022 04:02:33 +0200 Subject: [PATCH 12/39] use errors.Is() to check for errors (#1730) Since go 1.13 you can wrap errors. This make it no longer possible to compare with `==`, instead you have to compare with `errors.Is()`. I noticed this problem because -h was no longer working after I stared wrapping the errors in my custom FlagErrorFunc function. Note that this is only a problem when a custom help flag is defined. Signed-off-by: Paul Holzinger --- command.go | 3 ++- command_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 3349be636..675bb1340 100644 --- a/command.go +++ b/command.go @@ -18,6 +18,7 @@ package cobra import ( "bytes" "context" + "errors" "fmt" "io" "os" @@ -990,7 +991,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { if err != nil { // Always show help if requested, even if SilenceErrors is in // effect - if err == flag.ErrHelp { + if errors.Is(err, flag.ErrHelp) { cmd.HelpFunc()(cmd, args) return cmd, nil } diff --git a/command_test.go b/command_test.go index d48fef1a0..0446e3c1d 100644 --- a/command_test.go +++ b/command_test.go @@ -1723,6 +1723,38 @@ func TestFlagErrorFunc(t *testing.T) { } } +func TestFlagErrorFuncHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + c.PersistentFlags().Bool("help", false, "help for c") + c.SetFlagErrorFunc(func(_ *Command, err error) error { + return fmt.Errorf("wrap error: %w", err) + }) + + out, err := executeCommand(c, "--help") + if err != nil { + t.Errorf("--help should not fail: %v", err) + } + + expected := `Usage: + c [flags] + +Flags: + --help help for c +` + if out != expected { + t.Errorf("Expected: %v, got: %v", expected, out) + } + + out, err = executeCommand(c, "-h") + if err != nil { + t.Errorf("-h should not fail: %v", err) + } + + if out != expected { + t.Errorf("Expected: %v, got: %v", expected, out) + } +} + // TestSortedFlags checks, // if cmd.LocalFlags() is unsorted when cmd.Flags().SortFlags set to false. // Related to https://github.com/spf13/cobra/issues/404. From 5f2ec3c897155c3346e77430932e3966e5cfa648 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Mon, 20 Jun 2022 22:04:28 -0400 Subject: [PATCH 13/39] Update shell completion to respect flag groups (#1659) Signed-off-by: Marc Khouzam Co-authored-by: Marc Khouzam --- completions.go | 3 + completions_test.go | 186 ++++++++++++++++++++++++++++++++++++++++++++ flag_groups.go | 49 ++++++++++++ 3 files changed, 238 insertions(+) diff --git a/completions.go b/completions.go index 7a9a7fade..2c2483998 100644 --- a/completions.go +++ b/completions.go @@ -325,6 +325,9 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi var completions []string var directive ShellCompDirective + // Enforce flag groups before doing flag completions + finalCmd.enforceFlagGroupsForCompletion() + // Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true; // doing this allows for completion of persistent flag names even for commands that disable flag parsing. // diff --git a/completions_test.go b/completions_test.go index 28a3228e3..fa087fd9e 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2691,3 +2691,189 @@ func TestFixedCompletions(t *testing.T) { t.Errorf("expected: %q, got: %q", expected, output) } } + +func TestCompletionForGroupedFlags(t *testing.T) { + getCmd := func() *Command { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + childCmd := &Command{ + Use: "child", + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"subArg"}, ShellCompDirectiveNoFileComp + }, + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + rootCmd.PersistentFlags().Int("ingroup1", -1, "ingroup1") + rootCmd.PersistentFlags().String("ingroup2", "", "ingroup2") + + childCmd.Flags().Bool("ingroup3", false, "ingroup3") + childCmd.Flags().Bool("nogroup", false, "nogroup") + + // Add flags to a group + childCmd.MarkFlagsRequiredTogether("ingroup1", "ingroup2", "ingroup3") + + return rootCmd + } + + // Each test case uses a unique command from the function above. + testcases := []struct { + desc string + args []string + expectedOutput string + }{ + { + desc: "flags in group not suggested without - prefix", + args: []string{"child", ""}, + expectedOutput: strings.Join([]string{ + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "flags in group suggested with - prefix", + args: []string{"child", "-"}, + expectedOutput: strings.Join([]string{ + "--ingroup1", + "--ingroup2", + "--ingroup3", + "--nogroup", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "when flag in group present, other flags in group suggested even without - prefix", + args: []string{"child", "--ingroup2", "value", ""}, + expectedOutput: strings.Join([]string{ + "--ingroup1", + "--ingroup3", + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "when all flags in group present, flags not suggested without - prefix", + args: []string{"child", "--ingroup1", "8", "--ingroup2", "value2", "--ingroup3", ""}, + expectedOutput: strings.Join([]string{ + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "group ignored if some flags not applicable", + args: []string{"--ingroup2", "value", ""}, + expectedOutput: strings.Join([]string{ + "child", + "completion", + "help", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + } + + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + c := getCmd() + args := []string{ShellCompNoDescRequestCmd} + args = append(args, tc.args...) + output, err := executeCommand(c, args...) + switch { + case err == nil && output != tc.expectedOutput: + t.Errorf("expected: %q, got: %q", tc.expectedOutput, output) + case err != nil: + t.Errorf("Unexpected error %q", err) + } + }) + } +} + +func TestCompletionForMutuallyExclusiveFlags(t *testing.T) { + getCmd := func() *Command { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + childCmd := &Command{ + Use: "child", + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"subArg"}, ShellCompDirectiveNoFileComp + }, + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + rootCmd.PersistentFlags().IntSlice("ingroup1", []int{1}, "ingroup1") + rootCmd.PersistentFlags().String("ingroup2", "", "ingroup2") + + childCmd.Flags().Bool("ingroup3", false, "ingroup3") + childCmd.Flags().Bool("nogroup", false, "nogroup") + + // Add flags to a group + childCmd.MarkFlagsMutuallyExclusive("ingroup1", "ingroup2", "ingroup3") + + return rootCmd + } + + // Each test case uses a unique command from the function above. + testcases := []struct { + desc string + args []string + expectedOutput string + }{ + { + desc: "flags in mutually exclusive group not suggested without the - prefix", + args: []string{"child", ""}, + expectedOutput: strings.Join([]string{ + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "flags in mutually exclusive group suggested with the - prefix", + args: []string{"child", "-"}, + expectedOutput: strings.Join([]string{ + "--ingroup1", + "--ingroup2", + "--ingroup3", + "--nogroup", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "when flag in mutually exclusive group present, other flags in group not suggested even with the - prefix", + args: []string{"child", "--ingroup1", "8", "-"}, + expectedOutput: strings.Join([]string{ + "--ingroup1", // Should be suggested again since it is a slice + "--nogroup", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "group ignored if some flags not applicable", + args: []string{"--ingroup1", "8", "-"}, + expectedOutput: strings.Join([]string{ + "--ingroup1", + "--ingroup2", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + } + + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + c := getCmd() + args := []string{ShellCompNoDescRequestCmd} + args = append(args, tc.args...) + output, err := executeCommand(c, args...) + switch { + case err == nil && output != tc.expectedOutput: + t.Errorf("expected: %q, got: %q", tc.expectedOutput, output) + case err != nil: + t.Errorf("Unexpected error %q", err) + } + }) + } +} diff --git a/flag_groups.go b/flag_groups.go index 1dba424ad..dc7843119 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -172,3 +172,52 @@ func sortedKeys(m map[string]map[string]bool) []string { sort.Strings(keys) return keys } + +// enforceFlagGroupsForCompletion will do the following: +// - when a flag in a group is present, other flags in the group will be marked required +// - when a flag in a mutually exclusive group is present, other flags in the group will be marked as hidden +// This allows the standard completion logic to behave appropriately for flag groups +func (c *Command) enforceFlagGroupsForCompletion() { + if c.DisableFlagParsing { + return + } + + flags := c.Flags() + groupStatus := map[string]map[string]bool{} + mutuallyExclusiveGroupStatus := map[string]map[string]bool{} + c.Flags().VisitAll(func(pflag *flag.Flag) { + processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) + processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) + }) + + // If a flag that is part of a group is present, we make all the other flags + // of that group required so that the shell completion suggests them automatically + for flagList, flagnameAndStatus := range groupStatus { + for _, isSet := range flagnameAndStatus { + if isSet { + // One of the flags of the group is set, mark the other ones as required + for _, fName := range strings.Split(flagList, " ") { + _ = c.MarkFlagRequired(fName) + } + } + } + } + + // If a flag that is mutually exclusive to others is present, we hide the other + // flags of that group so the shell completion does not suggest them + for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus { + for flagName, isSet := range flagnameAndStatus { + if isSet { + // One of the flags of the mutually exclusive group is set, mark the other ones as hidden + // Don't mark the flag that is already set as hidden because it may be an + // array or slice flag and therefore must continue being suggested + for _, fName := range strings.Split(flagList, " ") { + if fName != flagName { + flag := c.Flags().Lookup(fName) + flag.Hidden = true + } + } + } + } + } +} From 06b06a9dc9f9f5eba93c552b2532a3da64ef9877 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jun 2022 20:06:17 -0600 Subject: [PATCH 14/39] Bump golangci/golangci-lint-action from 3.1.0 to 3.2.0 (#1697) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.1.0...v3.2.0) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c83f303d..0ca8367a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v3 - - uses: golangci/golangci-lint-action@v3.1.0 + - uses: golangci/golangci-lint-action@v3.2.0 with: version: latest args: --verbose From 3d38d18a23b094d1fd08ea0c7b257704990ddecf Mon Sep 17 00:00:00 2001 From: Miguel Duarte Barroso Date: Thu, 23 Jun 2022 17:36:12 +0200 Subject: [PATCH 15/39] Add KubeVirt as a project using cobra (#1741) Reaching out on behalf of KubeVirt, an add-on for Kubernetes, enabling users to run Virtual Machines on Kubernetes pods. Signed-off-by: Miguel Duarte Barroso --- projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index ac680118e..39603321e 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -23,6 +23,7 @@ - [Kool](https://github.com/kool-dev/kool) - [Kubernetes](https://kubernetes.io/) - [Kubescape](https://github.com/armosec/kubescape) +- [KubeVirt](https://github.com/kubevirt/kubevirt) - [Linkerd](https://linkerd.io/) - [Mattermost-server](https://github.com/mattermost/mattermost-server) - [Mercure](https://mercure.rocks/) From bba9331d4e5e5ebe2d0fdf0d9432b4b1e67d5061 Mon Sep 17 00:00:00 2001 From: Yevgeny Pats Date: Sat, 25 Jun 2022 19:17:06 +0300 Subject: [PATCH 16/39] doc: Add CloudQuery for projects using Cobra (#1742) --- projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index 39603321e..ab25112bb 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -2,6 +2,7 @@ - [Arduino CLI](https://github.com/arduino/arduino-cli) - [Bleve](https://blevesearch.com/) +- [CloudQuery](https://github.com/cloudquery/cloudquery) - [CockroachDB](https://www.cockroachlabs.com/) - [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) - [Datree](https://github.com/datreeio/datree) From 69083f81b225303d63c23e14f77e4f361c4cc792 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 28 Jun 2022 23:19:33 +0200 Subject: [PATCH 17/39] Add Cilium to projects using Cobra (#1745) --- projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index ab25112bb..91896620d 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -2,6 +2,7 @@ - [Arduino CLI](https://github.com/arduino/arduino-cli) - [Bleve](https://blevesearch.com/) +- [Cilium](https://cilium.io/) - [CloudQuery](https://github.com/cloudquery/cloudquery) - [CockroachDB](https://www.cockroachlabs.com/) - [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) From a0aadc68eb88af3acad9be0ec18c8b91438fd984 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Thu, 7 Jul 2022 13:54:19 +0900 Subject: [PATCH 18/39] Spelling for GitHub CLI (#1744) --- README.md | 2 +- projects_using_cobra.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2bf152082..a672c2302 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Cobra is a library for creating powerful modern CLI applications. Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/), -[Hugo](https://gohugo.io), and [Github CLI](https://github.com/cli/cli) to +[Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to name a few. [This list](./projects_using_cobra.md) contains a more extensive list of projects using Cobra. [![](https://img.shields.io/github/workflow/status/spf13/cobra/Test?longCache=tru&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index 91896620d..dadfd24a8 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -13,7 +13,7 @@ - [Gardener](https://github.com/gardener/gardenctl) - [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl) - [Git Bump](https://github.com/erdaltsksn/git-bump) -- [Github CLI](https://github.com/cli/cli) +- [GitHub CLI](https://github.com/cli/cli) - [GitHub Labeler](https://github.com/erdaltsksn/gh-label) - [Golangci-lint](https://golangci-lint.run) - [GopherJS](https://github.com/gopherjs/gopherjs) From 162534f92f478e4af12e5d688615bb2c61fdb831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20L=C3=B3pez=20Barba?= Date: Wed, 20 Jul 2022 15:45:07 +0200 Subject: [PATCH 19/39] doc: Add Okteto for projects using Cobra (#1759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Javier López Barba --- projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index dadfd24a8..90296fe07 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -36,6 +36,7 @@ - [Multi-gitter](https://github.com/lindell/multi-gitter) - [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack) - [nFPM](https://nfpm.goreleaser.com) +- [Okteto](https://github.com/okteto/okteto) - [OpenShift](https://www.openshift.com/) - [Ory Hydra](https://github.com/ory/hydra) - [Ory Kratos](https://github.com/ory/kratos) From 965924ac89db4eb3781bfe4b376474e60c788c20 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Thu, 4 Aug 2022 17:18:09 +0200 Subject: [PATCH 20/39] docs: add zitadel to the list (#1772) --- projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/projects_using_cobra.md b/projects_using_cobra.md index 90296fe07..b5b9dbf3a 100644 --- a/projects_using_cobra.md +++ b/projects_using_cobra.md @@ -56,3 +56,4 @@ - [UpCloud CLI (`upctl`)](https://github.com/UpCloudLtd/upcloud-cli) - VMware's [Tanzu Community Edition](https://github.com/vmware-tanzu/community-edition) & [Tanzu Framework](https://github.com/vmware-tanzu/tanzu-framework) - [Werf](https://werf.io/) +- [ZITADEL](https://github.com/zitadel/zitadel) From 6bf8cd85825800c5cbc63009b1fb2c54185f05ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Aug 2022 21:02:01 +0000 Subject: [PATCH 21/39] build(deps): bump github.com/inconshreveable/mousetrap (#1774) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d45d9f9a..a78cc5dbc 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 - github.com/inconshreveable/mousetrap v1.0.0 + github.com/inconshreveable/mousetrap v1.0.1 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 8ed228800..2f5eab40b 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= From 7790bf97fd40e285913066cf65387ad281cd6c2e Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Sat, 13 Aug 2022 23:31:45 +0200 Subject: [PATCH 22/39] fix: correct command path in see_also for YAML doc (#1771) The `see_also` section for child commands would include only the name of the commands. This adds the whole path, similar to how it's done for the other documentation formats. --- doc/yaml_docs.go | 2 +- doc/yaml_docs_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/yaml_docs.go b/doc/yaml_docs.go index a79fa40e3..3069b4ee4 100644 --- a/doc/yaml_docs.go +++ b/doc/yaml_docs.go @@ -128,7 +128,7 @@ func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) str if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { continue } - result = append(result, child.Name()+" - "+child.Short) + result = append(result, child.CommandPath()+" - "+child.Short) } yamlDoc.SeeAlso = result } diff --git a/doc/yaml_docs_test.go b/doc/yaml_docs_test.go index d08fa4f82..9bf5aa6c7 100644 --- a/doc/yaml_docs_test.go +++ b/doc/yaml_docs_test.go @@ -2,6 +2,7 @@ package doc import ( "bytes" + "fmt" "io/ioutil" "os" "path/filepath" @@ -24,6 +25,7 @@ func TestGenYamlDoc(t *testing.T) { checkStringContains(t, output, "rootflag") checkStringContains(t, output, rootCmd.Short) checkStringContains(t, output, echoSubCmd.Short) + checkStringContains(t, output, fmt.Sprintf("- %s - %s", echoSubCmd.CommandPath(), echoSubCmd.Short)) } func TestGenYamlNoTag(t *testing.T) { From f911c0b89c6e0c41b6ad218c9235d8706c36a1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 14 Aug 2022 00:44:47 +0300 Subject: [PATCH 23/39] fix(bash-v2): activeHelp length check syntax (#1762) ```shell $ set -u $ foo=() $ echo ${#foo} bash: foo: unbound variable echo ${#foo[*]} 0 ``` The above shows that an empty array needs the suffix `[*]` when checking its length, or else it is considered unbound. --- bash_completionsV2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bash_completionsV2.go b/bash_completionsV2.go index 767bf0312..351391af3 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -150,7 +150,7 @@ __%[1]s_process_completion_results() { __%[1]s_handle_special_char "$cur" = # Print the activeHelp statements before we finish - if [ ${#activeHelp} -ne 0 ]; then + if [ ${#activeHelp[*]} -ne 0 ]; then printf "\n"; printf "%%s\n" "${activeHelp[@]}" printf "\n" From dbf85f6104904d539cabceebec234e817fa0df0c Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Sun, 14 Aug 2022 00:18:46 +0200 Subject: [PATCH 24/39] Update gopkg.in/yaml.v2 to gopkg.in/yaml.v3 (#1766) API v3 was released in 2019 [1]. Update from the v2 API, no changes to the packages's use needed and no breaking changes expected. [1] https://ubuntu.com/blog/api-v3-of-the-yaml-package-for-go-is-available --- doc/yaml_docs.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/yaml_docs.go b/doc/yaml_docs.go index 3069b4ee4..784c38788 100644 --- a/doc/yaml_docs.go +++ b/doc/yaml_docs.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type cmdOption struct { diff --git a/go.mod b/go.mod index a78cc5dbc..a1309e536 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,5 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 github.com/inconshreveable/mousetrap v1.0.1 github.com/spf13/pflag v1.0.5 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 2f5eab40b..a8c544e25 100644 --- a/go.sum +++ b/go.sum @@ -8,5 +8,5 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 22b617914c8890ba20db7ceafcdc2ef4ca4817d3 Mon Sep 17 00:00:00 2001 From: Brian Pursley Date: Sun, 28 Aug 2022 12:46:39 -0400 Subject: [PATCH 25/39] fix: show flags that shadow parent persistent flag in child help (#1776) This fixes a bug where a child flag that shadows (has the same name as) a parent persistent flag would not be shown in the child command's help output and the parent flag would be shown instead under the global flags section. This change makes the help output consistent with the observed behavior during execution, where the child flag is the one that is actually used. --- command.go | 3 ++- command_test.go | 39 ++++++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/command.go b/command.go index 675bb1340..5fca7dc63 100644 --- a/command.go +++ b/command.go @@ -1505,7 +1505,8 @@ func (c *Command) LocalFlags() *flag.FlagSet { } addToLocal := func(f *flag.Flag) { - if c.lflags.Lookup(f.Name) == nil && c.parentsPflags.Lookup(f.Name) == nil { + // Add the flag if it is not a parent PFlag, or it shadows a parent PFlag + if c.lflags.Lookup(f.Name) == nil && f != c.parentsPflags.Lookup(f.Name) { c.lflags.AddFlag(f) } } diff --git a/command_test.go b/command_test.go index 0446e3c1d..0b0b82132 100644 --- a/command_test.go +++ b/command_test.go @@ -707,10 +707,7 @@ func TestEmptyInputs(t *testing.T) { } } -func TestOverwrittenFlag(t *testing.T) { - // TODO: This test fails, but should work. - t.Skip() - +func TestChildFlagShadowsParentPersistentFlag(t *testing.T) { parent := &Command{Use: "parent", Run: emptyRun} child := &Command{Use: "child", Run: emptyRun} @@ -732,7 +729,7 @@ func TestOverwrittenFlag(t *testing.T) { } if childInherited.Lookup("intf") != nil { - t.Errorf(`InheritedFlags should not contain overwritten flag "intf"`) + t.Errorf(`InheritedFlags should not contain shadowed flag "intf"`) } if childLocal.Lookup("intf") == nil { t.Error(`LocalFlags expected to contain "intf", got "nil"`) @@ -887,6 +884,38 @@ func TestHelpCommandExecutedOnChild(t *testing.T) { checkStringContains(t, output, childCmd.Long) } +func TestHelpCommandExecutedOnChildWithFlagThatShadowsParentFlag(t *testing.T) { + parent := &Command{Use: "parent", Run: emptyRun} + child := &Command{Use: "child", Run: emptyRun} + parent.AddCommand(child) + + parent.PersistentFlags().Bool("foo", false, "parent foo usage") + parent.PersistentFlags().Bool("bar", false, "parent bar usage") + child.Flags().Bool("foo", false, "child foo usage") // This shadows parent's foo flag + child.Flags().Bool("baz", false, "child baz usage") + + got, err := executeCommand(parent, "help", "child") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := `Usage: + parent child [flags] + +Flags: + --baz child baz usage + --foo child foo usage + -h, --help help for child + +Global Flags: + --bar parent bar usage +` + + if got != expected { + t.Errorf("Help text mismatch.\nExpected:\n%s\n\nGot:\n%s\n", expected, got) + } +} + func TestSetHelpCommand(t *testing.T) { c := &Command{Use: "c", Run: emptyRun} c.AddCommand(&Command{Use: "empty", Run: emptyRun}) From 704540525032e759304ffdbdacb2050dbdf4b2c5 Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Tue, 30 Aug 2022 02:35:16 +0100 Subject: [PATCH 26/39] ci: test on Golang 1.19 (#1782) * ci: test on Golang 1.19 * ci: run golangci-lint on golang 1.19 * Adds `check-latest` for setup-go action v3 * ci/golangci-lint: use latest version available in setup-go's manifest Signed-off-by: John McBride Signed-off-by: umarcor Co-authored-by: John McBride --- .github/workflows/test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ca8367a4..e0c2f15c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,8 @@ jobs: - uses: actions/setup-go@v3 with: - go-version: '1.17' + go-version: '^1.19' + check-latest: true - uses: actions/checkout@v3 @@ -37,6 +38,7 @@ jobs: - 16 - 17 - 18 + - 19 name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' runs-on: ${{ matrix.platform }}-latest steps: @@ -56,8 +58,8 @@ jobs: - run: | export GOBIN=$HOME/go/bin case "${{ matrix.go }}" in - 16|17|18) _version='@latest';; - *) _version='';; + 14|15) _version='';; + *) _version='@latest';; esac go install github.com/kyoh86/richgo"${_version}" go install github.com/mitchellh/gox"${_version}" From 2a7647ff4661fd5bc54bdc022d349d7efe29674f Mon Sep 17 00:00:00 2001 From: Paul Meyer <49727155+katexochen@users.noreply.github.com> Date: Tue, 30 Aug 2022 04:12:56 +0200 Subject: [PATCH 27/39] Clarify SetContext documentation (#1748) --- command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index 5fca7dc63..6e611337e 100644 --- a/command.go +++ b/command.go @@ -236,8 +236,8 @@ func (c *Command) Context() context.Context { return c.ctx } -// SetContext sets context for the command. It is set to context.Background by default and will be overwritten by -// Command.ExecuteContext or Command.ExecuteContextC +// SetContext sets context for the command. This context will be overwritten by +// Command.ExecuteContext or Command.ExecuteContextC. func (c *Command) SetContext(ctx context.Context) { c.ctx = ctx } From 0580724b5fe33fbbff193befb787b1a1a0fe36a0 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Tue, 30 Aug 2022 10:34:58 -0400 Subject: [PATCH 28/39] With go 1.18, we must use go install for a binary (#1726) The format "go install github.com/kyoh86/richgo@latest" will work with go 1.16 and higher. It will not work with go 1.15. However, since installing "richgo" is only required for people who want to run the go tests for the Cobra project itself, I feel it is ok to require go 1.16 or higher in this case. Signed-off-by: Marc Khouzam Signed-off-by: Marc Khouzam --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 443ef1a98..c433a01bc 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ $(warning "could not find golangci-lint in $(PATH), run: curl -sfL https://insta endif ifeq (, $(shell which richgo)) -$(warning "could not find richgo in $(PATH), run: go get github.com/kyoh86/richgo") +$(warning "could not find richgo in $(PATH), run: go install github.com/kyoh86/richgo@latest") endif .PHONY: fmt lint test install_deps clean From 2e8ba6f3087bda425da42af31f7373cd2c2c08c5 Mon Sep 17 00:00:00 2001 From: John McBride Date: Wed, 7 Sep 2022 18:25:01 -0600 Subject: [PATCH 29/39] Use correct stale action `exempt-` yaml keys (#1800) Signed-off-by: John McBride --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a63518f69..8f8c73949 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -39,8 +39,8 @@ jobs: days-before-close: 30 stale-issue-label: 'lifecycle/stale' stale-pr-label: 'lifecycle/stale' - exempt-issue-label: 'lifecycle/frozen' - exempt-pr-label: 'lifecycle/frozen' + exempt-issue-labels: 'lifecycle/frozen' + exempt-pr-labels: 'lifecycle/frozen' close-issue-label: 'lifecycle/rotten' close-pr-label: 'lifecycle/rotten' From 70e53f62be77e03f376be845efacbd99c00024ca Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Sat, 10 Sep 2022 15:33:34 +0200 Subject: [PATCH 30/39] Deprecate ExactValidArgs() and test combinations of args validators (#1643) * deprecate ExactValidArgs in favour of MatchAll(OnlyValidArgs, ...) * test combinations of args validators * adjust docs --- args.go | 24 ++--- args_test.go | 248 +++++++++++++++++++++++++++++++++++++++++++++----- user_guide.md | 50 ++++++---- 3 files changed, 270 insertions(+), 52 deletions(-) diff --git a/args.go b/args.go index 20a022b30..e2f40d515 100644 --- a/args.go +++ b/args.go @@ -32,7 +32,8 @@ func NoArgs(cmd *Command, args []string) error { return nil } -// OnlyValidArgs returns an error if any args are not in the list of ValidArgs. +// OnlyValidArgs returns an error if there are any positional args that are not in +// the `ValidArgs` field of `Command` func OnlyValidArgs(cmd *Command, args []string) error { if len(cmd.ValidArgs) > 0 { // Remove any description that may be included in ValidArgs. @@ -41,7 +42,6 @@ func OnlyValidArgs(cmd *Command, args []string) error { for _, v := range cmd.ValidArgs { validArgs = append(validArgs, strings.Split(v, "\t")[0]) } - for _, v := range args { if !stringInSlice(v, validArgs) { return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0])) @@ -86,18 +86,6 @@ func ExactArgs(n int) PositionalArgs { } } -// ExactValidArgs returns an error if -// there are not exactly N positional args OR -// there are any positional args that are not in the `ValidArgs` field of `Command` -func ExactValidArgs(n int) PositionalArgs { - return func(cmd *Command, args []string) error { - if err := ExactArgs(n)(cmd, args); err != nil { - return err - } - return OnlyValidArgs(cmd, args) - } -} - // RangeArgs returns an error if the number of args is not within the expected range. func RangeArgs(min int, max int) PositionalArgs { return func(cmd *Command, args []string) error { @@ -119,3 +107,11 @@ func MatchAll(pargs ...PositionalArgs) PositionalArgs { return nil } } + +// ExactValidArgs returns an error if there are not exactly N positional args OR +// there are any positional args that are not in the `ValidArgs` field of `Command` +// +// Deprecated: use MatchAll(ExactArgs(n), OnlyValidArgs) instead +func ExactValidArgs(n int) PositionalArgs { + return MatchAll(ExactArgs(n), OnlyValidArgs) +} diff --git a/args_test.go b/args_test.go index 6bd41c8fd..ae4df2e24 100644 --- a/args_test.go +++ b/args_test.go @@ -27,7 +27,7 @@ func expectSuccess(output string, err error, t *testing.T) { } } -func validWithInvalidArgs(err error, t *testing.T) { +func validOnlyWithInvalidArgs(err error, t *testing.T) { if err == nil { t.Fatal("Expected an error") } @@ -38,12 +38,12 @@ func validWithInvalidArgs(err error, t *testing.T) { } } -func noArgsWithArgs(err error, t *testing.T) { +func noArgsWithArgs(err error, t *testing.T, arg string) { if err == nil { t.Fatal("Expected an error") } got := err.Error() - expected := `unknown command "illegal" for "c"` + expected := `unknown command "` + arg + `" for "c"` if got != expected { t.Errorf("Expected: %q, got: %q", expected, got) } @@ -93,102 +93,280 @@ func rangeArgsWithInvalidCount(err error, t *testing.T) { } } +// NoArgs + func TestNoArgs(t *testing.T) { c := getCommand(NoArgs, false) output, err := executeCommand(c) expectSuccess(output, err, t) } -func TestNoArgsWithArgs(t *testing.T) { +func TestNoArgs_WithArgs(t *testing.T) { c := getCommand(NoArgs, false) - _, err := executeCommand(c, "illegal") - noArgsWithArgs(err, t) + _, err := executeCommand(c, "one") + noArgsWithArgs(err, t, "one") +} + +func TestNoArgs_WithValid_WithArgs(t *testing.T) { + c := getCommand(NoArgs, true) + _, err := executeCommand(c, "one") + noArgsWithArgs(err, t, "one") +} + +func TestNoArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(NoArgs, true) + _, err := executeCommand(c, "a") + noArgsWithArgs(err, t, "a") +} + +func TestNoArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, NoArgs), true) + _, err := executeCommand(c, "a") + validOnlyWithInvalidArgs(err, t) } +// OnlyValidArgs + func TestOnlyValidArgs(t *testing.T) { c := getCommand(OnlyValidArgs, true) output, err := executeCommand(c, "one", "two") expectSuccess(output, err, t) } -func TestOnlyValidArgsWithInvalidArgs(t *testing.T) { +func TestOnlyValidArgs_WithInvalidArgs(t *testing.T) { c := getCommand(OnlyValidArgs, true) _, err := executeCommand(c, "a") - validWithInvalidArgs(err, t) + validOnlyWithInvalidArgs(err, t) } +// ArbitraryArgs + func TestArbitraryArgs(t *testing.T) { c := getCommand(ArbitraryArgs, false) output, err := executeCommand(c, "a", "b") expectSuccess(output, err, t) } +func TestArbitraryArgs_WithValid(t *testing.T) { + c := getCommand(ArbitraryArgs, true) + output, err := executeCommand(c, "one", "two") + expectSuccess(output, err, t) +} + +func TestArbitraryArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(ArbitraryArgs, true) + output, err := executeCommand(c, "a") + expectSuccess(output, err, t) +} + +func TestArbitraryArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, ArbitraryArgs), true) + _, err := executeCommand(c, "a") + validOnlyWithInvalidArgs(err, t) +} + +// MinimumNArgs + func TestMinimumNArgs(t *testing.T) { c := getCommand(MinimumNArgs(2), false) output, err := executeCommand(c, "a", "b", "c") expectSuccess(output, err, t) } -func TestMinimumNArgsWithLessArgs(t *testing.T) { +func TestMinimumNArgs_WithValid(t *testing.T) { + c := getCommand(MinimumNArgs(2), true) + output, err := executeCommand(c, "one", "three") + expectSuccess(output, err, t) +} + +func TestMinimumNArgs_WithValid__WithInvalidArgs(t *testing.T) { + c := getCommand(MinimumNArgs(2), true) + output, err := executeCommand(c, "a", "b") + expectSuccess(output, err, t) +} + +func TestMinimumNArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, MinimumNArgs(2)), true) + _, err := executeCommand(c, "a", "b") + validOnlyWithInvalidArgs(err, t) +} + +func TestMinimumNArgs_WithLessArgs(t *testing.T) { c := getCommand(MinimumNArgs(2), false) _, err := executeCommand(c, "a") minimumNArgsWithLessArgs(err, t) } +func TestMinimumNArgs_WithLessArgs_WithValid(t *testing.T) { + c := getCommand(MinimumNArgs(2), true) + _, err := executeCommand(c, "one") + minimumNArgsWithLessArgs(err, t) +} + +func TestMinimumNArgs_WithLessArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(MinimumNArgs(2), true) + _, err := executeCommand(c, "a") + minimumNArgsWithLessArgs(err, t) +} + +func TestMinimumNArgs_WithLessArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, MinimumNArgs(2)), true) + _, err := executeCommand(c, "a") + validOnlyWithInvalidArgs(err, t) +} + +// MaximumNArgs + func TestMaximumNArgs(t *testing.T) { c := getCommand(MaximumNArgs(3), false) output, err := executeCommand(c, "a", "b") expectSuccess(output, err, t) } -func TestMaximumNArgsWithMoreArgs(t *testing.T) { +func TestMaximumNArgs_WithValid(t *testing.T) { + c := getCommand(MaximumNArgs(2), true) + output, err := executeCommand(c, "one", "three") + expectSuccess(output, err, t) +} + +func TestMaximumNArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(MaximumNArgs(2), true) + output, err := executeCommand(c, "a", "b") + expectSuccess(output, err, t) +} + +func TestMaximumNArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, MaximumNArgs(2)), true) + _, err := executeCommand(c, "a", "b") + validOnlyWithInvalidArgs(err, t) +} + +func TestMaximumNArgs_WithMoreArgs(t *testing.T) { c := getCommand(MaximumNArgs(2), false) _, err := executeCommand(c, "a", "b", "c") maximumNArgsWithMoreArgs(err, t) } +func TestMaximumNArgs_WithMoreArgs_WithValid(t *testing.T) { + c := getCommand(MaximumNArgs(2), true) + _, err := executeCommand(c, "one", "three", "two") + maximumNArgsWithMoreArgs(err, t) +} + +func TestMaximumNArgs_WithMoreArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(MaximumNArgs(2), true) + _, err := executeCommand(c, "a", "b", "c") + maximumNArgsWithMoreArgs(err, t) +} + +func TestMaximumNArgs_WithMoreArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, MaximumNArgs(2)), true) + _, err := executeCommand(c, "a", "b", "c") + validOnlyWithInvalidArgs(err, t) +} + +// ExactArgs + func TestExactArgs(t *testing.T) { c := getCommand(ExactArgs(3), false) output, err := executeCommand(c, "a", "b", "c") expectSuccess(output, err, t) } -func TestExactArgsWithInvalidCount(t *testing.T) { +func TestExactArgs_WithValid(t *testing.T) { + c := getCommand(ExactArgs(3), true) + output, err := executeCommand(c, "three", "one", "two") + expectSuccess(output, err, t) +} + +func TestExactArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(ExactArgs(3), true) + output, err := executeCommand(c, "three", "a", "two") + expectSuccess(output, err, t) +} + +func TestExactArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, ExactArgs(3)), true) + _, err := executeCommand(c, "three", "a", "two") + validOnlyWithInvalidArgs(err, t) +} + +func TestExactArgs_WithInvalidCount(t *testing.T) { c := getCommand(ExactArgs(2), false) _, err := executeCommand(c, "a", "b", "c") exactArgsWithInvalidCount(err, t) } -func TestExactValidArgs(t *testing.T) { - c := getCommand(ExactValidArgs(3), true) - output, err := executeCommand(c, "three", "one", "two") - expectSuccess(output, err, t) +func TestExactArgs_WithInvalidCount_WithValid(t *testing.T) { + c := getCommand(ExactArgs(2), true) + _, err := executeCommand(c, "three", "one", "two") + exactArgsWithInvalidCount(err, t) } -func TestExactValidArgsWithInvalidCount(t *testing.T) { - c := getCommand(ExactValidArgs(2), false) - _, err := executeCommand(c, "three", "one", "two") +func TestExactArgs_WithInvalidCount_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(ExactArgs(2), true) + _, err := executeCommand(c, "three", "a", "two") exactArgsWithInvalidCount(err, t) } -func TestExactValidArgsWithInvalidArgs(t *testing.T) { - c := getCommand(ExactValidArgs(3), true) +func TestExactArgs_WithInvalidCount_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, ExactArgs(2)), true) _, err := executeCommand(c, "three", "a", "two") - validWithInvalidArgs(err, t) + validOnlyWithInvalidArgs(err, t) } +// RangeArgs + func TestRangeArgs(t *testing.T) { c := getCommand(RangeArgs(2, 4), false) output, err := executeCommand(c, "a", "b", "c") expectSuccess(output, err, t) } -func TestRangeArgsWithInvalidCount(t *testing.T) { +func TestRangeArgs_WithValid(t *testing.T) { + c := getCommand(RangeArgs(2, 4), true) + output, err := executeCommand(c, "three", "one", "two") + expectSuccess(output, err, t) +} + +func TestRangeArgs_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(RangeArgs(2, 4), true) + output, err := executeCommand(c, "three", "a", "two") + expectSuccess(output, err, t) +} + +func TestRangeArgs_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, RangeArgs(2, 4)), true) + _, err := executeCommand(c, "three", "a", "two") + validOnlyWithInvalidArgs(err, t) +} + +func TestRangeArgs_WithInvalidCount(t *testing.T) { c := getCommand(RangeArgs(2, 4), false) _, err := executeCommand(c, "a") rangeArgsWithInvalidCount(err, t) } +func TestRangeArgs_WithInvalidCount_WithValid(t *testing.T) { + c := getCommand(RangeArgs(2, 4), true) + _, err := executeCommand(c, "two") + rangeArgsWithInvalidCount(err, t) +} + +func TestRangeArgs_WithInvalidCount_WithValid_WithInvalidArgs(t *testing.T) { + c := getCommand(RangeArgs(2, 4), true) + _, err := executeCommand(c, "a") + rangeArgsWithInvalidCount(err, t) +} + +func TestRangeArgs_WithInvalidCount_WithValidOnly_WithInvalidArgs(t *testing.T) { + c := getCommand(MatchAll(OnlyValidArgs, RangeArgs(2, 4)), true) + _, err := executeCommand(c, "a") + validOnlyWithInvalidArgs(err, t) +} + +// Takes(No)Args + func TestRootTakesNoArgs(t *testing.T) { rootCmd := &Command{Use: "root", Run: emptyRun} childCmd := &Command{Use: "child", Run: emptyRun} @@ -293,6 +471,32 @@ func TestMatchAll(t *testing.T) { } } +// DEPRECATED + +func TestExactValidArgs(t *testing.T) { + c := getCommand(ExactValidArgs(3), true) + output, err := executeCommand(c, "three", "one", "two") + expectSuccess(output, err, t) +} + +func TestExactValidArgs_WithInvalidCount(t *testing.T) { + c := getCommand(ExactValidArgs(2), false) + _, err := executeCommand(c, "three", "one", "two") + exactArgsWithInvalidCount(err, t) +} + +func TestExactValidArgs_WithInvalidCount_WithInvalidArgs(t *testing.T) { + c := getCommand(ExactValidArgs(2), true) + _, err := executeCommand(c, "three", "a", "two") + exactArgsWithInvalidCount(err, t) +} + +func TestExactValidArgs_WithInvalidArgs(t *testing.T) { + c := getCommand(ExactValidArgs(2), true) + _, err := executeCommand(c, "three", "a") + validOnlyWithInvalidArgs(err, t) +} + // This test make sure we keep backwards-compatibility with respect // to the legacyArgs() function. // It makes sure the root command accepts arguments if it does not have diff --git a/user_guide.md b/user_guide.md index 5a7acf88e..6128ae46f 100644 --- a/user_guide.md +++ b/user_guide.md @@ -302,15 +302,15 @@ rootCmd.MarkPersistentFlagRequired("region") ### Flag Groups -If you have different flags that must be provided together (e.g. if they provide the `--username` flag they MUST provide the `--password` flag as well) then +If you have different flags that must be provided together (e.g. if they provide the `--username` flag they MUST provide the `--password` flag as well) then Cobra can enforce that requirement: ```go rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)") rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)") rootCmd.MarkFlagsRequiredTogether("username", "password") -``` +``` -You can also prevent different flags from being provided together if they represent mutually +You can also prevent different flags from being provided together if they represent mutually exclusive options such as specifying an output format as either `--json` or `--yaml` but never both: ```go rootCmd.Flags().BoolVar(&u, "json", false, "Output in JSON") @@ -327,29 +327,47 @@ In both of these cases: ## Positional and Custom Arguments Validation of positional arguments can be specified using the `Args` field of `Command`. +The following validators are built in: + +- Number of arguments: + - `NoArgs` - report an error if there are any positional args. + - `ArbitraryArgs` - accept any number of args. + - `MinimumNArgs(int)` - report an error if less than N positional args are provided. + - `MaximumNArgs(int)` - report an error if more than N positional args are provided. + - `ExactArgs(int)` - report an error if there are not exactly N positional args. + - `RangeArgs(min, max)` - report an error if the number of args is not between `min` and `max`. +- Content of the arguments: + - `OnlyValidArgs` - report an error if there are any positional args not specified in the `ValidArgs` field of `Command`, which can optionally be set to a list of valid values for positional args. + If `Args` is undefined or `nil`, it defaults to `ArbitraryArgs`. -The following validators are built in: +Moreover, `MatchAll(pargs ...PositionalArgs)` enables combining existing checks with arbitrary other checks. +For instance, if you want to report an error if there are not exactly N positional args OR if there are any positional +args that are not in the `ValidArgs` field of `Command`, you can call `MatchAll` on `ExactArgs` and `OnlyValidArgs`, as +shown below: -- `NoArgs` - the command will report an error if there are any positional args. -- `ArbitraryArgs` - the command will accept any args. -- `OnlyValidArgs` - the command will report an error if there are any positional args that are not in the `ValidArgs` field of `Command`. -- `MinimumNArgs(int)` - the command will report an error if there are not at least N positional args. -- `MaximumNArgs(int)` - the command will report an error if there are more than N positional args. -- `ExactArgs(int)` - the command will report an error if there are not exactly N positional args. -- `ExactValidArgs(int)` - the command will report an error if there are not exactly N positional args OR if there are any positional args that are not in the `ValidArgs` field of `Command` -- `RangeArgs(min, max)` - the command will report an error if the number of args is not between the minimum and maximum number of expected args. -- `MatchAll(pargs ...PositionalArgs)` - enables combining existing checks with arbitrary other checks (e.g. you want to check the ExactArgs length along with other qualities). +```go +var cmd = &cobra.Command{ + Short: "hello", + Args: MatchAll(ExactArgs(2), OnlyValidArgs), + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Hello, World!") + }, +} +``` -An example of setting the custom validator: +It is possible to set any custom validator that satisfies `func(cmd *cobra.Command, args []string) error`. +For example: ```go var cmd = &cobra.Command{ Short: "hello", Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return errors.New("requires a color argument") + // Optionally run one of the validators provided by cobra + if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + return err } + // Run the custom validation logic if myapp.IsValidColor(args[0]) { return nil } From d689184a421607457a18131e0a2b602fec22e3b4 Mon Sep 17 00:00:00 2001 From: Yuval Goldberg Date: Sun, 11 Sep 2022 15:25:22 +0300 Subject: [PATCH 31/39] Support for case-insensitive command names (#1802) Add a global `EnableCaseInsensitive` variable to allow case-insensitive command names. The variable supports commands names and aliases globally. Resolves #1382 --- cobra.go | 13 +++++- command.go | 15 ++++++- command_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/cobra.go b/cobra.go index d6cbfd719..e330f16b1 100644 --- a/cobra.go +++ b/cobra.go @@ -40,14 +40,23 @@ var templateFuncs = template.FuncMap{ var initializers []func() +const ( + defaultPrefixMatching = false + defaultCommandSorting = true + defaultCaseInsensitive = false +) + // EnablePrefixMatching allows to set automatic prefix matching. Automatic prefix matching can be a dangerous thing // to automatically enable in CLI tools. // Set this to true to enable it. -var EnablePrefixMatching = false +var EnablePrefixMatching = defaultPrefixMatching // EnableCommandSorting controls sorting of the slice of commands, which is turned on by default. // To disable sorting, set it to false. -var EnableCommandSorting = true +var EnableCommandSorting = defaultCommandSorting + +// EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default) +var EnableCaseInsensitive = defaultCaseInsensitive // MousetrapHelpText enables an information splash screen on Windows // if the CLI is started from explorer.exe. diff --git a/command.go b/command.go index 6e611337e..cc6f181d4 100644 --- a/command.go +++ b/command.go @@ -676,7 +676,7 @@ func (c *Command) findSuggestions(arg string) string { func (c *Command) findNext(next string) *Command { matches := make([]*Command, 0) for _, cmd := range c.commands { - if cmd.Name() == next || cmd.HasAlias(next) { + if commandNameMatches(cmd.Name(), next) || cmd.HasAlias(next) { cmd.commandCalledAs.name = next return cmd } @@ -1328,7 +1328,7 @@ func (c *Command) Name() string { // HasAlias determines if a given string is an alias of the command. func (c *Command) HasAlias(s string) bool { for _, a := range c.Aliases { - if a == s { + if commandNameMatches(a, s) { return true } } @@ -1695,3 +1695,14 @@ func (c *Command) updateParentsPflags() { c.parentsPflags.AddFlagSet(parent.PersistentFlags()) }) } + +// commandNameMatches checks if two command names are equal +// taking into account case sensitivity according to +// EnableCaseInsensitive global configuration. +func commandNameMatches(s string, t string) bool { + if EnableCaseInsensitive { + return strings.EqualFold(s, t) + } + + return s == t +} diff --git a/command_test.go b/command_test.go index 0b0b82132..3f77d586d 100644 --- a/command_test.go +++ b/command_test.go @@ -314,7 +314,7 @@ func TestEnablePrefixMatching(t *testing.T) { t.Errorf("aCmdArgs expected: %q, got: %q", onetwo, got) } - EnablePrefixMatching = false + EnablePrefixMatching = defaultPrefixMatching } func TestAliasPrefixMatching(t *testing.T) { @@ -349,7 +349,7 @@ func TestAliasPrefixMatching(t *testing.T) { t.Errorf("timesCmdArgs expected: %v, got: %v", onetwo, got) } - EnablePrefixMatching = false + EnablePrefixMatching = defaultPrefixMatching } // TestChildSameName checks the correct behaviour of cobra in cases, @@ -1263,6 +1263,113 @@ func TestSuggestions(t *testing.T) { } } +func TestCaseInsensitive(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + childCmd := &Command{Use: "child", Run: emptyRun, Aliases: []string{"alternative"}} + granchildCmd := &Command{Use: "GRANDCHILD", Run: emptyRun, Aliases: []string{"ALIAS"}} + + childCmd.AddCommand(granchildCmd) + rootCmd.AddCommand(childCmd) + + tests := []struct { + args []string + failWithoutEnabling bool + }{ + { + args: []string{"child"}, + failWithoutEnabling: false, + }, + { + args: []string{"CHILD"}, + failWithoutEnabling: true, + }, + { + args: []string{"chILD"}, + failWithoutEnabling: true, + }, + { + args: []string{"CHIld"}, + failWithoutEnabling: true, + }, + { + args: []string{"alternative"}, + failWithoutEnabling: false, + }, + { + args: []string{"ALTERNATIVE"}, + failWithoutEnabling: true, + }, + { + args: []string{"ALTernatIVE"}, + failWithoutEnabling: true, + }, + { + args: []string{"alternatiVE"}, + failWithoutEnabling: true, + }, + { + args: []string{"child", "GRANDCHILD"}, + failWithoutEnabling: false, + }, + { + args: []string{"child", "grandchild"}, + failWithoutEnabling: true, + }, + { + args: []string{"CHIld", "GRANdchild"}, + failWithoutEnabling: true, + }, + { + args: []string{"alternative", "ALIAS"}, + failWithoutEnabling: false, + }, + { + args: []string{"alternative", "alias"}, + failWithoutEnabling: true, + }, + { + args: []string{"CHILD", "alias"}, + failWithoutEnabling: true, + }, + { + args: []string{"CHIld", "aliAS"}, + failWithoutEnabling: true, + }, + } + + for _, test := range tests { + for _, enableCaseInsensitivity := range []bool{true, false} { + EnableCaseInsensitive = enableCaseInsensitivity + + output, err := executeCommand(rootCmd, test.args...) + expectedFailure := test.failWithoutEnabling && !enableCaseInsensitivity + + if !expectedFailure && output != "" { + t.Errorf("Unexpected output: %v", output) + } + if !expectedFailure && err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + } + + EnableCaseInsensitive = defaultCaseInsensitive +} + +// This test make sure we keep backwards-compatibility with respect +// to command names case sensitivity behavior. +func TestCaseSensitivityBackwardCompatibility(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + childCmd := &Command{Use: "child", Run: emptyRun} + + rootCmd.AddCommand(childCmd) + _, err := executeCommand(rootCmd, strings.ToUpper(childCmd.Use)) + if err == nil { + t.Error("Expected error on calling a command in upper case while command names are case sensitive. Got nil.") + } + +} + func TestRemoveCommand(t *testing.T) { rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} childCmd := &Command{Use: "child", Run: emptyRun} @@ -1622,7 +1729,7 @@ func TestCommandsAreSorted(t *testing.T) { } } - EnableCommandSorting = true + EnableCommandSorting = defaultCommandSorting } func TestEnableCommandSortingIsDisabled(t *testing.T) { @@ -1643,7 +1750,7 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) { } } - EnableCommandSorting = true + EnableCommandSorting = defaultCommandSorting } func TestSetOutput(t *testing.T) { From fe08012eddbdf42edd466b8defeee33ec0ec4446 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sun, 11 Sep 2022 17:04:53 -0400 Subject: [PATCH 32/39] Rename Powershell completion tests (#1803) The name of the powershell completion test file did not match the actual powershell file. Signed-off-by: Marc Khouzam --- power_completions_test.go => powershell_completions_test.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename power_completions_test.go => powershell_completions_test.go (100%) diff --git a/power_completions_test.go b/powershell_completions_test.go similarity index 100% rename from power_completions_test.go rename to powershell_completions_test.go From 7e289f46f1d1b2567b123cc26db0e7cd76644c32 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sun, 11 Sep 2022 17:48:42 -0400 Subject: [PATCH 33/39] Adjustments to documentation (#1656) Signed-off-by: Marc Khouzam --- README.md | 4 ++-- user_guide.md | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a672c2302..08e4fa528 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ The best applications read like sentences when used, and as a result, users intuitively know how to interact with them. The pattern to follow is -`APPNAME VERB NOUN --ADJECTIVE.` +`APPNAME VERB NOUN --ADJECTIVE` or -`APPNAME COMMAND ARG --FLAG` +`APPNAME COMMAND ARG --FLAG`. A few good real world examples may better illustrate this point. diff --git a/user_guide.md b/user_guide.md index 6128ae46f..c694d8719 100644 --- a/user_guide.md +++ b/user_guide.md @@ -462,28 +462,29 @@ create' is called. Every command will automatically have the '--help' flag adde The following output is automatically generated by Cobra. Nothing beyond the command and flag definitions are needed. - $ cobra help + $ cobra-cli help Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application. Usage: - cobra [command] + cobra-cli [command] Available Commands: add Add a command to a Cobra Application + completion Generate the autocompletion script for the specified shell help Help about any command init Initialize a Cobra Application Flags: -a, --author string author name for copyright attribution (default "YOUR NAME") --config string config file (default is $HOME/.cobra.yaml) - -h, --help help for cobra + -h, --help help for cobra-cli -l, --license string name of license for the project - --viper use Viper for configuration (default true) + --viper use Viper for configuration - Use "cobra [command] --help" for more information about a command. + Use "cobra-cli [command] --help" for more information about a command. Help is just a command like any other. There is no special logic or behavior @@ -492,7 +493,7 @@ around it. In fact, you can provide your own if you want. ### Defining your own help You can provide your own Help command or your own template for the default command to use -with following functions: +with the following functions: ```go cmd.SetHelpCommand(cmd *Command) @@ -511,22 +512,23 @@ showing the user the 'usage'. You may recognize this from the help above. That's because the default help embeds the usage as part of its output. - $ cobra --invalid + $ cobra-cli --invalid Error: unknown flag: --invalid Usage: - cobra [command] + cobra-cli [command] Available Commands: add Add a command to a Cobra Application + completion Generate the autocompletion script for the specified shell help Help about any command init Initialize a Cobra Application Flags: -a, --author string author name for copyright attribution (default "YOUR NAME") --config string config file (default is $HOME/.cobra.yaml) - -h, --help help for cobra + -h, --help help for cobra-cli -l, --license string name of license for the project - --viper use Viper for configuration (default true) + --viper use Viper for configuration Use "cobra [command] --help" for more information about a command. @@ -645,7 +647,7 @@ Did you mean this? Run 'hugo --help' for usage. ``` -Suggestions are automatic based on every subcommand registered and use an implementation of [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion. +Suggestions are automatically generated based on existing subcommands and use an implementation of [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion. If you need to disable suggestions or tweak the string distance in your command, use: @@ -659,7 +661,8 @@ or command.SuggestionsMinimumDistance = 1 ``` -You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but makes sense in your set of commands and for some which you don't want aliases. Example: +You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but make sense in your set of commands but for which +you don't want aliases. Example: ``` $ kubectl remove From 4065a33bfdf697b6ce748a521686aee7edc748eb Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Wed, 14 Sep 2022 05:31:59 +0200 Subject: [PATCH 34/39] ci: use action/setup-go's cache (#1783) * ci: use action/setup-go's cache * ci: deprecate Golang 1.14 --- .github/workflows/test.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0c2f15c7..4e041c7fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,13 @@ jobs: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: go-version: '^1.19' check-latest: true + cache: true - uses: actions/checkout@v3 @@ -33,7 +36,6 @@ jobs: - ubuntu - macOS go: - - 14 - 15 - 16 - 17 @@ -43,17 +45,12 @@ jobs: runs-on: ${{ matrix.platform }}-latest steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.${{ matrix.go }}.x - - uses: actions/checkout@v3 - - uses: actions/cache@v3 + - uses: actions/setup-go@v3 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-1.${{ matrix.go }}.x-${{ hashFiles('**/go.sum') }} - restore-keys: ${{ runner.os }}-1.${{ matrix.go }}.x- + go-version: 1.${{ matrix.go }}.x + cache: true - run: | export GOBIN=$HOME/go/bin From 6d978a911e7fff69b1ca2a873dd91d78ebca44cf Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Fri, 16 Sep 2022 13:55:56 +0200 Subject: [PATCH 35/39] add missing license headers (#1809) --- .github/workflows/test.yml | 21 +++++++++++++++++++++ .golangci.yml | 14 ++++++++++++++ active_help.go | 14 ++++++++++++++ active_help_test.go | 14 ++++++++++++++ args.go | 14 ++++++++++++++ args_test.go | 14 ++++++++++++++ bash_completions.go | 14 ++++++++++++++ bash_completionsV2.go | 14 ++++++++++++++ bash_completionsV2_test.go | 14 ++++++++++++++ bash_completions_test.go | 14 ++++++++++++++ cobra.go | 5 +++-- cobra_test.go | 14 ++++++++++++++ command.go | 5 +++-- command_notwin.go | 14 ++++++++++++++ command_test.go | 14 ++++++++++++++ command_win.go | 14 ++++++++++++++ completions.go | 14 ++++++++++++++ completions_test.go | 14 ++++++++++++++ doc/cmd_test.go | 14 ++++++++++++++ doc/man_docs.go | 5 +++-- doc/man_docs_test.go | 14 ++++++++++++++ doc/man_examples_test.go | 14 ++++++++++++++ doc/md_docs.go | 5 +++-- doc/md_docs_test.go | 14 ++++++++++++++ doc/rest_docs.go | 5 +++-- doc/rest_docs_test.go | 14 ++++++++++++++ doc/util.go | 5 +++-- doc/yaml_docs.go | 5 +++-- doc/yaml_docs_test.go | 14 ++++++++++++++ fish_completions.go | 14 ++++++++++++++ fish_completions_test.go | 14 ++++++++++++++ flag_groups.go | 5 +++-- flag_groups_test.go | 5 +++-- powershell_completions.go | 14 ++++++++++++++ powershell_completions_test.go | 14 ++++++++++++++ shell_completions.go | 14 ++++++++++++++ zsh_completions.go | 14 ++++++++++++++ zsh_completions_test.go | 14 ++++++++++++++ 38 files changed, 440 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e041c7fb..797ae60e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,25 @@ env: jobs: + + lic-headers: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v3 + + - run: >- + docker run + -v $(pwd):/wrk -w /wrk + ghcr.io/google/addlicense + -c 'The Cobra Authors' + -y '2013-2022' + -l apache + -ignore '.github/**' + -check + . + + golangci-lint: runs-on: ubuntu-latest steps: @@ -28,6 +47,7 @@ jobs: version: latest args: --verbose + test-unix: strategy: fail-fast: false @@ -63,6 +83,7 @@ jobs: - run: RICHGO_FORCE_COLOR=1 PATH=$HOME/go/bin/:$PATH make test + test-win: name: MINGW64 defaults: diff --git a/.golangci.yml b/.golangci.yml index 0d6e61793..439d3e1de 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,17 @@ +# Copyright 2013-2022 The Cobra Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + run: deadline: 5m diff --git a/active_help.go b/active_help.go index 0c631913d..95e03aecb 100644 --- a/active_help.go +++ b/active_help.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/active_help_test.go b/active_help_test.go index 524be884b..3653dd87d 100644 --- a/active_help_test.go +++ b/active_help_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/args.go b/args.go index e2f40d515..2c1f99e78 100644 --- a/args.go +++ b/args.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/args_test.go b/args_test.go index ae4df2e24..fd2c3b415 100644 --- a/args_test.go +++ b/args_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/bash_completions.go b/bash_completions.go index cb7e19537..3acdb2797 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/bash_completionsV2.go b/bash_completionsV2.go index 351391af3..bb4b71892 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/bash_completionsV2_test.go b/bash_completionsV2_test.go index a9a277ed2..9302baf04 100644 --- a/bash_completionsV2_test.go +++ b/bash_completionsV2_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/bash_completions_test.go b/bash_completions_test.go index 6896e91c7..5c32306d3 100644 --- a/bash_completions_test.go +++ b/bash_completions_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/cobra.go b/cobra.go index e330f16b1..1a95c3c1b 100644 --- a/cobra.go +++ b/cobra.go @@ -1,9 +1,10 @@ -// Copyright © 2013 Steve Francia . +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/cobra_test.go b/cobra_test.go index 1219cc079..71353c9dd 100644 --- a/cobra_test.go +++ b/cobra_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/command.go b/command.go index cc6f181d4..cdfd360f8 100644 --- a/command.go +++ b/command.go @@ -1,9 +1,10 @@ -// Copyright © 2013 Steve Francia . +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/command_notwin.go b/command_notwin.go index bb5dad90b..2b77f8f01 100644 --- a/command_notwin.go +++ b/command_notwin.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //go:build !windows // +build !windows diff --git a/command_test.go b/command_test.go index 3f77d586d..a4b7366ed 100644 --- a/command_test.go +++ b/command_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/command_win.go b/command_win.go index a84f5a82a..520f23abf 100644 --- a/command_win.go +++ b/command_win.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //go:build windows // +build windows diff --git a/completions.go b/completions.go index 2c2483998..f8bf4f69d 100644 --- a/completions.go +++ b/completions.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/completions_test.go b/completions_test.go index fa087fd9e..c5f11fa52 100644 --- a/completions_test.go +++ b/completions_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/doc/cmd_test.go b/doc/cmd_test.go index 0917d602a..4f2a0f581 100644 --- a/doc/cmd_test.go +++ b/doc/cmd_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package doc import ( diff --git a/doc/man_docs.go b/doc/man_docs.go index 8dbce35e9..b5a2c596e 100644 --- a/doc/man_docs.go +++ b/doc/man_docs.go @@ -1,9 +1,10 @@ -// Copyright 2015 Red Hat Inc. All rights reserved. +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/doc/man_docs_test.go b/doc/man_docs_test.go index f7994f899..d296eebea 100644 --- a/doc/man_docs_test.go +++ b/doc/man_docs_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package doc import ( diff --git a/doc/man_examples_test.go b/doc/man_examples_test.go index e20a34c38..b995bf6db 100644 --- a/doc/man_examples_test.go +++ b/doc/man_examples_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package doc_test import ( diff --git a/doc/md_docs.go b/doc/md_docs.go index 19d7e931f..bab4b4963 100644 --- a/doc/md_docs.go +++ b/doc/md_docs.go @@ -1,9 +1,10 @@ -//Copyright 2015 Red Hat Inc. All rights reserved. +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/doc/md_docs_test.go b/doc/md_docs_test.go index f32516795..b1632e9c5 100644 --- a/doc/md_docs_test.go +++ b/doc/md_docs_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package doc import ( diff --git a/doc/rest_docs.go b/doc/rest_docs.go index 23dca16a3..d65890652 100644 --- a/doc/rest_docs.go +++ b/doc/rest_docs.go @@ -1,9 +1,10 @@ -//Copyright 2015 Red Hat Inc. All rights reserved. +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/doc/rest_docs_test.go b/doc/rest_docs_test.go index 330a2e5e7..0c197f8dd 100644 --- a/doc/rest_docs_test.go +++ b/doc/rest_docs_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package doc import ( diff --git a/doc/util.go b/doc/util.go index bffde94d5..499ecdd91 100644 --- a/doc/util.go +++ b/doc/util.go @@ -1,9 +1,10 @@ -// Copyright 2015 Red Hat Inc. All rights reserved. +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/doc/yaml_docs.go b/doc/yaml_docs.go index 784c38788..12f8cf33b 100644 --- a/doc/yaml_docs.go +++ b/doc/yaml_docs.go @@ -1,9 +1,10 @@ -// Copyright 2016 French Ben. All rights reserved. +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/doc/yaml_docs_test.go b/doc/yaml_docs_test.go index 9bf5aa6c7..c552077d8 100644 --- a/doc/yaml_docs_test.go +++ b/doc/yaml_docs_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package doc import ( diff --git a/fish_completions.go b/fish_completions.go index 005ee6be7..97112a17b 100644 --- a/fish_completions.go +++ b/fish_completions.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/fish_completions_test.go b/fish_completions_test.go index 381c66770..935f5540c 100644 --- a/fish_completions_test.go +++ b/fish_completions_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/flag_groups.go b/flag_groups.go index dc7843119..b09236faa 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -1,9 +1,10 @@ -// Copyright © 2022 Steve Francia . +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/flag_groups_test.go b/flag_groups_test.go index 404ede562..b4b65ac05 100644 --- a/flag_groups_test.go +++ b/flag_groups_test.go @@ -1,9 +1,10 @@ -// Copyright © 2022 Steve Francia . +// Copyright 2013-2022 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/powershell_completions.go b/powershell_completions.go index 379e7c088..351ded657 100644 --- a/powershell_completions.go +++ b/powershell_completions.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + // The generated scripts require PowerShell v5.0+ (which comes Windows 10, but // can be downloaded separately for windows 7 or 8.1). diff --git a/powershell_completions_test.go b/powershell_completions_test.go index 771383597..3d3d10333 100644 --- a/powershell_completions_test.go +++ b/powershell_completions_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/shell_completions.go b/shell_completions.go index d99bf91e5..126e83c30 100644 --- a/shell_completions.go +++ b/shell_completions.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/zsh_completions.go b/zsh_completions.go index 65cd94c60..84cec76fd 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( diff --git a/zsh_completions_test.go b/zsh_completions_test.go index b7addb4ca..258b1f71d 100644 --- a/zsh_completions_test.go +++ b/zsh_completions_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( From 459fc5fce316465d5abc60faae27aa99687d62dc Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Fri, 16 Sep 2022 22:23:19 +0200 Subject: [PATCH 36/39] ci: add workflow_dispatch (#1387) --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 797ae60e8..7865add6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ name: Test on: push: pull_request: + workflow_dispatch: env: GO111MODULE: on From a281c8b47b315fe428cf31ad5c38b526dc136f55 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Fri, 16 Sep 2022 16:44:28 -0400 Subject: [PATCH 37/39] Document option to hide the default completion cmd (#1779) Signed-off-by: Marc Khouzam Signed-off-by: Marc Khouzam --- shell_completions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shell_completions.md b/shell_completions.md index 1e2058ed6..a0957b4d5 100644 --- a/shell_completions.md +++ b/shell_completions.md @@ -99,6 +99,11 @@ To tell Cobra *not* to provide the default `completion` command: rootCmd.CompletionOptions.DisableDefaultCmd = true ``` +To tell Cobra to mark the default `completion` command as *hidden*: +``` +rootCmd.CompletionOptions.HiddenDefaultCmd = true +``` + To tell Cobra *not* to provide the user with the `--no-descriptions` flag to the completion sub-commands: ``` rootCmd.CompletionOptions.DisableNoDescFlag = true From fce8d8aeb08dc6afe413cc0af67a7fbb3cffec4c Mon Sep 17 00:00:00 2001 From: Skeet WU Date: Tue, 27 Sep 2022 18:27:48 +0800 Subject: [PATCH 38/39] Expose ValidateRequiredFlags and ValidateFlagGroups (#1760) --- command.go | 7 ++++--- flag_groups.go | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/command.go b/command.go index cdfd360f8..1867d22c9 100644 --- a/command.go +++ b/command.go @@ -862,10 +862,10 @@ func (c *Command) execute(a []string) (err error) { c.PreRun(c, argWoFlags) } - if err := c.validateRequiredFlags(); err != nil { + if err := c.ValidateRequiredFlags(); err != nil { return err } - if err := c.validateFlagGroups(); err != nil { + if err := c.ValidateFlagGroups(); err != nil { return err } @@ -1019,7 +1019,8 @@ func (c *Command) ValidateArgs(args []string) error { return c.Args(c, args) } -func (c *Command) validateRequiredFlags() error { +// ValidateRequiredFlags validates all required flags are present and returns an error otherwise +func (c *Command) ValidateRequiredFlags() error { if c.DisableFlagParsing { return nil } diff --git a/flag_groups.go b/flag_groups.go index b09236faa..9c377aaf9 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -59,9 +59,9 @@ func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { } } -// validateFlagGroups validates the mutuallyExclusive/requiredAsGroup logic and returns the +// ValidateFlagGroups validates the mutuallyExclusive/requiredAsGroup logic and returns the // first error encountered. -func (c *Command) validateFlagGroups() error { +func (c *Command) ValidateFlagGroups() error { if c.DisableFlagParsing { return nil } From 7039e1fa214cfc1de404ed6540158c8fda64a758 Mon Sep 17 00:00:00 2001 From: Francis Nickels III Date: Fri, 30 Sep 2022 11:26:05 -0700 Subject: [PATCH 39/39] Add '--version' flag to Help output (#1707) --- command.go | 3 +- command_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 1867d22c9..5ed3df8de 100644 --- a/command.go +++ b/command.go @@ -1123,7 +1123,8 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`, c.Printf("Unknown help topic %#q\n", args) CheckErr(c.Root().Usage()) } else { - cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown + cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown + cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown CheckErr(cmd.Help()) } }, diff --git a/command_test.go b/command_test.go index a4b7366ed..b3dd03040 100644 --- a/command_test.go +++ b/command_test.go @@ -2343,3 +2343,90 @@ func TestSetContextPersistentPreRun(t *testing.T) { t.Error(err) } } + +const VersionFlag = "--version" +const HelpFlag = "--help" + +func TestNoRootRunCommandExecutedWithVersionSet(t *testing.T) { + rootCmd := &Command{Use: "root", Version: "1.0.0", Long: "Long description"} + rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) + + output, err := executeCommand(rootCmd) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, rootCmd.Long) + checkStringContains(t, output, HelpFlag) + checkStringContains(t, output, VersionFlag) +} + +func TestNoRootRunCommandExecutedWithoutVersionSet(t *testing.T) { + rootCmd := &Command{Use: "root", Long: "Long description"} + rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) + + output, err := executeCommand(rootCmd) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, rootCmd.Long) + checkStringContains(t, output, HelpFlag) + checkStringOmits(t, output, VersionFlag) +} + +func TestHelpCommandExecutedWithVersionSet(t *testing.T) { + rootCmd := &Command{Use: "root", Version: "1.0.0", Long: "Long description", Run: emptyRun} + rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) + + output, err := executeCommand(rootCmd, "help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, rootCmd.Long) + checkStringContains(t, output, HelpFlag) + checkStringContains(t, output, VersionFlag) +} + +func TestHelpCommandExecutedWithoutVersionSet(t *testing.T) { + rootCmd := &Command{Use: "root", Long: "Long description", Run: emptyRun} + rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) + + output, err := executeCommand(rootCmd, "help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, rootCmd.Long) + checkStringContains(t, output, HelpFlag) + checkStringOmits(t, output, VersionFlag) +} + +func TestHelpflagCommandExecutedWithVersionSet(t *testing.T) { + rootCmd := &Command{Use: "root", Version: "1.0.0", Long: "Long description", Run: emptyRun} + rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) + + output, err := executeCommand(rootCmd, HelpFlag) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, rootCmd.Long) + checkStringContains(t, output, HelpFlag) + checkStringContains(t, output, VersionFlag) +} + +func TestHelpflagCommandExecutedWithoutVersionSet(t *testing.T) { + rootCmd := &Command{Use: "root", Long: "Long description", Run: emptyRun} + rootCmd.AddCommand(&Command{Use: "child", Run: emptyRun}) + + output, err := executeCommand(rootCmd, HelpFlag) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, rootCmd.Long) + checkStringContains(t, output, HelpFlag) + checkStringOmits(t, output, VersionFlag) +}