Merge branch 'spf13:main' into dm/local-flags-doc

This commit is contained in:
Dmytro Milinevskyi 2024-01-06 18:14:14 +01:00 committed by GitHub
commit d35fd5fad3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 566 additions and 61 deletions

19
.github/labeler.yml vendored
View file

@ -1,17 +1,24 @@
# changes to documentation generation
"area/docs-generation": doc/**/*
"area/docs-generation":
- changed-files:
- any-glob-to-any-file: 'doc/**'
# changes to the core cobra command
"area/cobra-command":
- any: ['./cobra.go', './cobra_test.go', './*command*.go']
- changed-files:
- any-glob-to-any-file: ['./cobra.go', './cobra_test.go', './*command*.go']
# changes made to command flags/args
"area/flags": ./args*.go
"area/flags":
- changed-files:
- any-glob-to-any-file: './args*.go'
# changes to Github workflows
"area/github": .github/**/*
"area/github":
- changed-files:
- any-glob-to-any-file: '.github/**'
# changes to shell completions
"area/shell-completion":
- ./*completions*
- changed-files:
- any-glob-to-any-file: './*completions*'

View file

@ -12,7 +12,7 @@ jobs:
pull-requests: write # for actions/labeler to add labels to PRs
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
with:
repo-token: "${{ github.token }}"

View file

@ -41,14 +41,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: '^1.21'
check-latest: true
cache: true
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v3.7.0
with:
version: latest
@ -74,7 +72,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 1.${{ matrix.go }}.x
cache: true

View file

@ -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 <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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)
}

View file

@ -52,9 +52,9 @@ func OnlyValidArgs(cmd *Command, args []string) error {
if len(cmd.ValidArgs) > 0 {
// Remove any description that may be included in ValidArgs.
// A description is following a tab character.
var validArgs []string
validArgs := make([]string, 0, len(cmd.ValidArgs))
for _, v := range cmd.ValidArgs {
validArgs = append(validArgs, strings.Split(v, "\t")[0])
validArgs = append(validArgs, strings.SplitN(v, "\t", 2)[0])
}
for _, v := range args {
if !stringInSlice(v, validArgs) {

View file

@ -621,7 +621,7 @@ func writeRequiredNouns(buf io.StringWriter, cmd *Command) {
for _, value := range cmd.ValidArgs {
// Remove any description that may be included following a tab character.
// Descriptions are not supported by bash completion.
value = strings.Split(value, "\t")[0]
value = strings.SplitN(value, "\t", 2)[0]
WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_noun+=(%q)\n", value))
}
if cmd.ValidArgsFunction != nil {

View file

@ -193,8 +193,6 @@ func ld(s, t string, ignoreCase bool) int {
d := make([][]int, len(s)+1)
for i := range d {
d[i] = make([]int, len(t)+1)
}
for i := range d {
d[i][0] = i
}
for j := range d[0] {

View file

@ -40,3 +40,185 @@ func TestAddTemplateFunctions(t *testing.T) {
t.Errorf("Expected UsageString: %v\nGot: %v", expected, got)
}
}
func TestLevenshteinDistance(t *testing.T) {
tests := []struct {
name string
s string
t string
ignoreCase bool
expected int
}{
{
name: "Equal strings (case-sensitive)",
s: "hello",
t: "hello",
ignoreCase: false,
expected: 0,
},
{
name: "Equal strings (case-insensitive)",
s: "Hello",
t: "hello",
ignoreCase: true,
expected: 0,
},
{
name: "Different strings (case-sensitive)",
s: "kitten",
t: "sitting",
ignoreCase: false,
expected: 3,
},
{
name: "Different strings (case-insensitive)",
s: "Kitten",
t: "Sitting",
ignoreCase: true,
expected: 3,
},
{
name: "Empty strings",
s: "",
t: "",
ignoreCase: false,
expected: 0,
},
{
name: "One empty string",
s: "abc",
t: "",
ignoreCase: false,
expected: 3,
},
{
name: "Both empty strings",
s: "",
t: "",
ignoreCase: true,
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Act
got := ld(tt.s, tt.t, tt.ignoreCase)
// Assert
if got != tt.expected {
t.Errorf("Expected ld: %v\nGot: %v", tt.expected, got)
}
})
}
}
func TestStringInSlice(t *testing.T) {
tests := []struct {
name string
a string
list []string
expected bool
}{
{
name: "String in slice (case-sensitive)",
a: "apple",
list: []string{"orange", "banana", "apple", "grape"},
expected: true,
},
{
name: "String not in slice (case-sensitive)",
a: "pear",
list: []string{"orange", "banana", "apple", "grape"},
expected: false,
},
{
name: "String in slice (case-insensitive)",
a: "APPLE",
list: []string{"orange", "banana", "apple", "grape"},
expected: false,
},
{
name: "Empty slice",
a: "apple",
list: []string{},
expected: false,
},
{
name: "Empty string",
a: "",
list: []string{"orange", "banana", "apple", "grape"},
expected: false,
},
{
name: "Empty strings match",
a: "",
list: []string{"orange", ""},
expected: true,
},
{
name: "Empty string in empty slice",
a: "",
list: []string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Act
got := stringInSlice(tt.a, tt.list)
// Assert
if got != tt.expected {
t.Errorf("Expected stringInSlice: %v\nGot: %v", tt.expected, got)
}
})
}
}
func TestRpad(t *testing.T) {
tests := []struct {
name string
inputString string
padding int
expected string
}{
{
name: "Padding required",
inputString: "Hello",
padding: 10,
expected: "Hello ",
},
{
name: "No padding required",
inputString: "World",
padding: 5,
expected: "World",
},
{
name: "Empty string",
inputString: "",
padding: 8,
expected: " ",
},
{
name: "Zero padding",
inputString: "cobra",
padding: 0,
expected: "cobra",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Act
got := rpad(tt.inputString, tt.padding)
// Assert
if got != tt.expected {
t.Errorf("Expected rpad: %v\nGot: %v", tt.expected, got)
}
})
}
}

View file

@ -708,7 +708,7 @@ Loop:
// This is not a flag or a flag value. Check to see if it matches what we're looking for, and if so,
// return the args, excluding the one at this position.
if s == x {
ret := []string{}
ret := make([]string, 0, len(args)-1)
ret = append(ret, args[:pos]...)
ret = append(ret, args[pos+1:]...)
return ret
@ -756,14 +756,14 @@ func (c *Command) findSuggestions(arg string) string {
if c.SuggestionsMinimumDistance <= 0 {
c.SuggestionsMinimumDistance = 2
}
suggestionsString := ""
var sb strings.Builder
if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 {
suggestionsString += "\n\nDid you mean this?\n"
sb.WriteString("\n\nDid you mean this?\n")
for _, s := range suggestions {
suggestionsString += fmt.Sprintf("\t%v\n", s)
_, _ = fmt.Fprintf(&sb, "\t%v\n", s)
}
}
return suggestionsString
return sb.String()
}
func (c *Command) findNext(next string) *Command {
@ -1189,10 +1189,11 @@ func (c *Command) InitDefaultHelpFlag() {
c.mergePersistentFlags()
if c.Flags().Lookup("help") == nil {
usage := "help for "
if c.Name() == "" {
name := c.displayName()
if name == "" {
usage += "this command"
} else {
usage += c.Name()
usage += name
}
c.Flags().BoolP("help", "h", false, usage)
_ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"})
@ -1238,7 +1239,7 @@ func (c *Command) InitDefaultHelpCmd() {
Use: "help [command]",
Short: "Help about any command",
Long: `Help provides help for any command in the application.
Simply type ` + c.Name() + ` help [path to command] for full details.`,
Simply type ` + c.displayName() + ` help [path to command] for full details.`,
ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
var completions []string
cmd, _, e := c.Root().Find(args)
@ -1429,6 +1430,10 @@ func (c *Command) CommandPath() string {
if c.HasParent() {
return c.Parent().CommandPath() + " " + c.Name()
}
return c.displayName()
}
func (c *Command) displayName() string {
if displayName, ok := c.Annotations[CommandDisplayNameAnnotation]; ok {
return displayName
}
@ -1438,10 +1443,11 @@ func (c *Command) CommandPath() string {
// UseLine puts out the full usage for a given command (including parents).
func (c *Command) UseLine() string {
var useline string
use := strings.Replace(c.Use, c.Name(), c.displayName(), 1)
if c.HasParent() {
useline = c.parent.CommandPath() + " " + c.Use
useline = c.parent.CommandPath() + " " + use
} else {
useline = c.Use
useline = use
}
if c.DisableFlagsInUseLine {
return useline
@ -1644,7 +1650,7 @@ func (c *Command) GlobalNormalizationFunc() func(f *flag.FlagSet, name string) f
// to this command (local and persistent declared here and by all parents).
func (c *Command) Flags() *flag.FlagSet {
if c.flags == nil {
c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
if c.flagErrorBuf == nil {
c.flagErrorBuf = new(bytes.Buffer)
}
@ -1659,7 +1665,7 @@ func (c *Command) Flags() *flag.FlagSet {
func (c *Command) LocalNonPersistentFlags() *flag.FlagSet {
persistentFlags := c.PersistentFlags()
out := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
out := flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
c.LocalFlags().VisitAll(func(f *flag.Flag) {
if persistentFlags.Lookup(f.Name) == nil {
out.AddFlag(f)
@ -1674,7 +1680,7 @@ func (c *Command) LocalFlags() *flag.FlagSet {
c.mergePersistentFlags()
if c.lflags == nil {
c.lflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.lflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
if c.flagErrorBuf == nil {
c.flagErrorBuf = new(bytes.Buffer)
}
@ -1702,7 +1708,7 @@ func (c *Command) InheritedFlags() *flag.FlagSet {
c.mergePersistentFlags()
if c.iflags == nil {
c.iflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.iflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
if c.flagErrorBuf == nil {
c.flagErrorBuf = new(bytes.Buffer)
}
@ -1731,7 +1737,7 @@ func (c *Command) NonInheritedFlags() *flag.FlagSet {
// PersistentFlags returns the persistent FlagSet specifically set in the current command.
func (c *Command) PersistentFlags() *flag.FlagSet {
if c.pflags == nil {
c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
if c.flagErrorBuf == nil {
c.flagErrorBuf = new(bytes.Buffer)
}
@ -1744,9 +1750,9 @@ func (c *Command) PersistentFlags() *flag.FlagSet {
func (c *Command) ResetFlags() {
c.flagErrorBuf = new(bytes.Buffer)
c.flagErrorBuf.Reset()
c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
c.flags.SetOutput(c.flagErrorBuf)
c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
c.pflags.SetOutput(c.flagErrorBuf)
c.lflags = nil
@ -1863,7 +1869,7 @@ func (c *Command) mergePersistentFlags() {
// If c.parentsPflags == nil, it makes new.
func (c *Command) updateParentsPflags() {
if c.parentsPflags == nil {
c.parentsPflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
c.parentsPflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError)
c.parentsPflags.SetOutput(c.flagErrorBuf)
c.parentsPflags.SortFlags = false
}

View file

@ -370,8 +370,28 @@ func TestAliasPrefixMatching(t *testing.T) {
// executable is `kubectl-plugin`, but we run it as `kubectl plugin`. The help
// text should reflect the way we run the command.
func TestPlugin(t *testing.T) {
cmd := &Command{
Use: "kubectl-plugin",
Args: NoArgs,
Annotations: map[string]string{
CommandDisplayNameAnnotation: "kubectl plugin",
},
Run: emptyRun,
}
cmdHelp, err := executeCommand(cmd, "-h")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
checkStringContains(t, cmdHelp, "kubectl plugin [flags]")
checkStringContains(t, cmdHelp, "help for kubectl plugin")
}
// TestPlugin checks usage as plugin with sub commands.
func TestPluginWithSubCommands(t *testing.T) {
rootCmd := &Command{
Use: "plugin",
Use: "kubectl-plugin",
Args: NoArgs,
Annotations: map[string]string{
CommandDisplayNameAnnotation: "kubectl plugin",
@ -387,6 +407,8 @@ func TestPlugin(t *testing.T) {
}
checkStringContains(t, rootHelp, "kubectl plugin [command]")
checkStringContains(t, rootHelp, "help for kubectl plugin")
checkStringContains(t, rootHelp, "kubectl plugin [command] --help")
childHelp, err := executeCommand(rootCmd, "sub", "-h")
if err != nil {
@ -394,6 +416,15 @@ func TestPlugin(t *testing.T) {
}
checkStringContains(t, childHelp, "kubectl plugin sub [flags]")
checkStringContains(t, childHelp, "help for sub")
helpHelp, err := executeCommand(rootCmd, "help", "-h")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
checkStringContains(t, helpHelp, "kubectl plugin help [path to command]")
checkStringContains(t, helpHelp, "kubectl plugin help [command]")
}
// TestChildSameName checks the correct behaviour of cobra in cases,

View file

@ -17,6 +17,8 @@ package cobra
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"sync"
@ -211,24 +213,29 @@ 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 {
if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable {
// Remove all activeHelp entries in this case
if strings.HasPrefix(comp, activeHelpMarker) {
continue
}
if noActiveHelp && strings.HasPrefix(comp, activeHelpMarker) {
// Remove all activeHelp entries if it's disabled.
continue
}
if noDescriptions {
// Remove any description that may be included following a tab character.
comp = strings.Split(comp, "\t")[0]
comp = strings.SplitN(comp, "\t", 2)[0]
}
// Make sure we only write the first line to the output.
// This is needed if a description contains a linebreak.
// Otherwise the shell scripts will interpret the other lines as new flags
// and could therefore provide a wrong completion.
comp = strings.Split(comp, "\n")[0]
comp = strings.SplitN(comp, "\n", 2)[0]
// Finally trim the completion. This is especially important to get rid
// of a trailing tab when there are no description following it.
@ -237,14 +244,14 @@ func (c *Command) initCompleteCmd(args []string) {
// although there is no description).
comp = strings.TrimSpace(comp)
// Print each possible completion to stdout for the completion script to consume.
fmt.Fprintln(finalCmd.OutOrStdout(), comp)
// Print each possible completion to the output for the completion script to consume.
fmt.Fprintln(out, comp)
}
// As the last printout, print the completion directive for the completion script to parse.
// The directive integer must be that last character following a single colon (:).
// The completion script expects :<directive>
fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive)
fmt.Fprintf(out, ":%d\n", directive)
// Print some helpful info to stderr for the user to understand.
// Output from stderr must be ignored by the completion script.
@ -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 <PROGRAM>_<SUFFIX> where <PROGRAM> 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
// <PROGRAM>_<SUFFIX> where <PROGRAM> 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_<SUFFIX> 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
}

View file

@ -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()
}
})
}
}

View file

@ -130,7 +130,7 @@ func processFlagForGroupAnnotation(flags *flag.FlagSet, pflag *flag.Flag, annota
continue
}
groupStatus[group] = map[string]bool{}
groupStatus[group] = make(map[string]bool, len(flagnames))
for _, name := range flagnames {
groupStatus[group][name] = false
}
@ -253,17 +253,17 @@ func (c *Command) enforceFlagGroupsForCompletion() {
// If none of the flags of a one-required group are present, we make all the flags
// of that group required so that the shell completion suggests them automatically
for flagList, flagnameAndStatus := range oneRequiredGroupStatus {
set := 0
isSet := false
for _, isSet := range flagnameAndStatus {
for _, isSet = range flagnameAndStatus {
if isSet {
set++
break
}
}
// None of the flags of the group are set, mark all flags in the group
// as required
if set == 0 {
if !isSet {
for _, fName := range strings.Split(flagList, " ") {
_ = c.MarkFlagRequired(fName)
}

View file

@ -393,6 +393,9 @@ $ source <(helm completion bash --no-descriptions)
$ helm completion [tab][tab]
bash fish powershell zsh
```
Setting the `<PROGRAM>_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. `<PROGRAM>` is the name of your program with all non-ASCII-alphanumeric characters replaced by `_`.
## Bash completions
### Dependencies

View file

@ -12,6 +12,7 @@
- [Datree](https://github.com/datreeio/datree)
- [Delve](https://github.com/derekparker/delve)
- [Docker (distribution)](https://github.com/docker/distribution)
- [Encore](https://encore.dev)
- [Etcd](https://etcd.io/)
- [Gardener](https://github.com/gardener/gardenctl)
- [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl)
@ -30,6 +31,7 @@
- [Kubescape](https://github.com/kubescape/kubescape)
- [KubeVirt](https://github.com/kubevirt/kubevirt)
- [Linkerd](https://linkerd.io/)
- [LXC](https://github.com/canonical/lxd)
- [Mattermost-server](https://github.com/mattermost/mattermost-server)
- [Mercure](https://mercure.rocks/)
- [Meroxa CLI](https://github.com/meroxa/cli)
@ -61,4 +63,5 @@
- [Vitess](https://vitess.io)
- 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/)
- [Zarf](https://github.com/defenseunicorns/zarf)
- [ZITADEL](https://github.com/zitadel/zitadel)

View file

@ -748,3 +748,57 @@ Read more about it in [Shell Completions](completions/_index.md).
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).
## Creating a plugin
When creating a plugin for tools like *kubectl*, the executable is named
`kubectl-myplugin`, but it is used as `kubectl myplugin`. To fix help
messages and completions, annotate the root command with the
`cobra.CommandDisplayNameAnnotation` annotation.
### Example kubectl plugin
```go
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func main() {
rootCmd := &cobra.Command{
Use: "kubectl-myplugin",
Annotations: map[string]string{
cobra.CommandDisplayNameAnnotation: "kubectl myplugin",
},
}
subCmd := &cobra.Command{
Use: "subcmd",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("kubectl myplugin subcmd")
},
}
rootCmd.AddCommand(subCmd)
rootCmd.Execute()
}
```
Example run as a kubectl plugin:
```
$ kubectl myplugin
Usage:
kubectl myplugin [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
subcmd
Flags:
-h, --help help for kubectl myplugin
Use "kubectl myplugin [command] --help" for more information about a command.
```