From e63925d3216142ba2c8d7641d729c6c15f4433e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 17 Dec 2023 20:50:59 +0200 Subject: [PATCH] Add env variable to suppress completion descriptions on create (#1938) COBRA_COMPLETION_DESCRIPTIONS=0 or _COMPLETION_DESCRIPTIONS=0 can now be used to disable shell completion descriptions. --- active_help.go | 13 +- completions.go | 40 +++++- completions_test.go | 192 +++++++++++++++++++++++++++++ site/content/completions/_index.md | 3 + 4 files changed, 237 insertions(+), 11 deletions(-) diff --git a/active_help.go b/active_help.go index 5f965e0..25c30e3 100644 --- a/active_help.go +++ b/active_help.go @@ -17,21 +17,17 @@ package cobra import ( "fmt" "os" - "regexp" - "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" + activeHelpEnvVarSuffix = "ACTIVE_HELP" + activeHelpGlobalEnvVar = configEnvVarGlobalPrefix + "_" + activeHelpEnvVarSuffix activeHelpGlobalDisable = "0" ) -var activeHelpEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`) - // 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. @@ -60,8 +56,5 @@ func GetActiveHelpConfig(cmd *Command) string { // variable. It has the format _ACTIVE_HELP where is the name of the // root command in upper case, with all non-ASCII-alphanumeric characters 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)) - activeHelpEnvVar = activeHelpEnvVarPrefixSubstRegexp.ReplaceAllString(activeHelpEnvVar, "_") - return activeHelpEnvVar + return configEnvVar(name, activeHelpEnvVarSuffix) } diff --git a/completions.go b/completions.go index b0e41df..ad7b6d0 100644 --- a/completions.go +++ b/completions.go @@ -17,6 +17,8 @@ package cobra import ( "fmt" "os" + "regexp" + "strconv" "strings" "sync" @@ -211,7 +213,12 @@ func (c *Command) initCompleteCmd(args []string) { // 2- Even without completions, we need to print the directive } - noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) + noDescriptions := cmd.CalledAs() == ShellCompNoDescRequestCmd + if !noDescriptions { + if doDescriptions, err := strconv.ParseBool(getEnvConfig(cmd, configEnvVarSuffixDescriptions)); err == nil { + noDescriptions = !doDescriptions + } + } noActiveHelp := GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable out := finalCmd.OutOrStdout() for _, comp := range completions { @@ -899,3 +906,34 @@ func CompError(msg string) { func CompErrorln(msg string) { CompError(fmt.Sprintf("%s\n", msg)) } + +// These values should not be changed: users will be using them explicitly. +const ( + configEnvVarGlobalPrefix = "COBRA" + configEnvVarSuffixDescriptions = "COMPLETION_DESCRIPTIONS" +) + +var configEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`) + +// configEnvVar returns the name of the program-specific configuration environment +// variable. It has the format _ where is the name of the +// root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`. +func configEnvVar(name, suffix string) string { + // This format should not be changed: users will be using it explicitly. + v := strings.ToUpper(fmt.Sprintf("%s_%s", name, suffix)) + v = configEnvVarPrefixSubstRegexp.ReplaceAllString(v, "_") + return v +} + +// getEnvConfig returns the value of the configuration environment variable +// _ where is the name of the root command in upper +// case, with all non-ASCII-alphanumeric characters replaced by `_`. +// If the value is empty or not set, the value of the environment variable +// COBRA_ is returned instead. +func getEnvConfig(cmd *Command, suffix string) string { + v := os.Getenv(configEnvVar(cmd.Root().Name(), suffix)) + if v == "" { + v = os.Getenv(configEnvVar(configEnvVarGlobalPrefix, suffix)) + } + return v +} diff --git a/completions_test.go b/completions_test.go index d5aee25..df153fc 100644 --- a/completions_test.go +++ b/completions_test.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + "os" "strings" "sync" "testing" @@ -3517,3 +3518,194 @@ func TestGetFlagCompletion(t *testing.T) { }) } } + +func TestGetEnvConfig(t *testing.T) { + testCases := []struct { + desc string + use string + suffix string + cmdVar string + globalVar string + cmdVal string + globalVal string + expected string + }{ + { + desc: "Command envvar overrides global", + use: "root", + suffix: "test", + cmdVar: "ROOT_TEST", + globalVar: "COBRA_TEST", + cmdVal: "cmd", + globalVal: "global", + expected: "cmd", + }, + { + desc: "Missing/empty command envvar falls back to global", + use: "root", + suffix: "test", + cmdVar: "ROOT_TEST", + globalVar: "COBRA_TEST", + cmdVal: "", + globalVal: "global", + expected: "global", + }, + { + desc: "Missing/empty command and global envvars fall back to empty", + use: "root", + suffix: "test", + cmdVar: "ROOT_TEST", + globalVar: "COBRA_TEST", + cmdVal: "", + globalVal: "", + expected: "", + }, + { + desc: "Periods in command use transform to underscores in env var name", + use: "foo.bar", + suffix: "test", + cmdVar: "FOO_BAR_TEST", + globalVar: "COBRA_TEST", + cmdVal: "cmd", + globalVal: "global", + expected: "cmd", + }, + { + desc: "Dashes in command use transform to underscores in env var name", + use: "quux-BAZ", + suffix: "test", + cmdVar: "QUUX_BAZ_TEST", + globalVar: "COBRA_TEST", + cmdVal: "cmd", + globalVal: "global", + expected: "cmd", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + // Could make env handling cleaner with t.Setenv with Go >= 1.17 + err := os.Setenv(tc.cmdVar, tc.cmdVal) + defer func() { + assertNoErr(t, os.Unsetenv(tc.cmdVar)) + }() + assertNoErr(t, err) + err = os.Setenv(tc.globalVar, tc.globalVal) + defer func() { + assertNoErr(t, os.Unsetenv(tc.globalVar)) + }() + assertNoErr(t, err) + cmd := &Command{Use: tc.use} + got := getEnvConfig(cmd, tc.suffix) + if got != tc.expected { + t.Errorf("expected: %q, got: %q", tc.expected, got) + } + }) + } +} + +func TestDisableDescriptions(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + specificDescriptionsEnvVar := configEnvVar(rootCmd.Name(), configEnvVarSuffixDescriptions) + globalDescriptionsEnvVar := configEnvVar(configEnvVarGlobalPrefix, configEnvVarSuffixDescriptions) + + const ( + descLineWithDescription = "first\tdescription" + descLineWithoutDescription = "first" + ) + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{descLineWithDescription} + return comps, ShellCompDirectiveDefault + } + + testCases := []struct { + desc string + globalEnvValue string + specificEnvValue string + expectedLine string + }{ + { + "No env variables set", + "", + "", + descLineWithDescription, + }, + { + "Global value false", + "false", + "", + descLineWithoutDescription, + }, + { + "Specific value false", + "", + "false", + descLineWithoutDescription, + }, + { + "Both values false", + "false", + "false", + descLineWithoutDescription, + }, + { + "Both values true", + "true", + "true", + descLineWithDescription, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + if err := os.Setenv(specificDescriptionsEnvVar, tc.specificEnvValue); err != nil { + t.Errorf("Unexpected error setting %s: %v", specificDescriptionsEnvVar, err) + } + if err := os.Setenv(globalDescriptionsEnvVar, tc.globalEnvValue); err != nil { + t.Errorf("Unexpected error setting %s: %v", globalDescriptionsEnvVar, err) + } + + var run = func() { + output, err := executeCommand(rootCmd, ShellCompRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + tc.expectedLine, + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + } + + run() + + // For empty cases, test also unset state + if tc.specificEnvValue == "" { + if err := os.Unsetenv(specificDescriptionsEnvVar); err != nil { + t.Errorf("Unexpected error unsetting %s: %v", specificDescriptionsEnvVar, err) + } + run() + } + if tc.globalEnvValue == "" { + if err := os.Unsetenv(globalDescriptionsEnvVar); err != nil { + t.Errorf("Unexpected error unsetting %s: %v", globalDescriptionsEnvVar, err) + } + run() + } + }) + } +} diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 4efad29..02257ad 100644 --- a/site/content/completions/_index.md +++ b/site/content/completions/_index.md @@ -393,6 +393,9 @@ $ source <(helm completion bash --no-descriptions) $ helm completion [tab][tab] bash fish powershell zsh ``` + +Setting the `_COMPLETION_DESCRIPTIONS` environment variable (falling back to `COBRA_COMPLETION_DESCRIPTIONS` if empty or not set) to a [falsey value](https://pkg.go.dev/strconv#ParseBool) achieves the same. `` is the name of your program with all non-ASCII-alphanumeric characters replaced by `_`. + ## Bash completions ### Dependencies