mirror of
https://github.com/spf13/cobra
synced 2025-04-27 17:17:20 +00:00
Merge branch 'spf13:main' into dm/local-flags-doc
This commit is contained in:
commit
d35fd5fad3
16 changed files with 566 additions and 61 deletions
19
.github/labeler.yml
vendored
19
.github/labeler.yml
vendored
|
@ -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*'
|
||||
|
|
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
|
@ -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 }}"
|
||||
|
||||
|
|
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
4
args.go
4
args.go
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
cobra.go
2
cobra.go
|
@ -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] {
|
||||
|
|
182
cobra_test.go
182
cobra_test.go
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
42
command.go
42
command.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
```
|
||||
|
|
Loading…
Add table
Reference in a new issue