diff --git a/command.go b/command.go index 4cd712bcb..19602946e 100644 --- a/command.go +++ b/command.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "os" - "path/filepath" "sort" "strings" @@ -1078,8 +1077,11 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { args := c.args - // Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155 - if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" { + // If running unit tests, we don't want to take the os.Args, see #155 and #2173. + // For example, the following would fail: + // go test -c -o foo.test + // ./foo.test -test.run TestNoArgs + if c.args == nil && !isTesting() { args = os.Args[1:] } diff --git a/command_go120.go b/command_go120.go new file mode 100644 index 000000000..23bc0feb0 --- /dev/null +++ b/command_go120.go @@ -0,0 +1,33 @@ +// Copyright 2013-2024 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 !go1.21 +// +build !go1.21 + +package cobra + +import ( + "os" + "strings" +) + +// based on golang.org/x/mod/internal/lazyregexp: https://cs.opensource.google/go/x/mod/+/refs/tags/v0.19.0:internal/lazyregexp/lazyre.go;l=66 +// For a non-go-test program which still has a name ending with ".test[.exe]", it will need to either: +// 1- Use go >= 1.21, or +// 2- call "rootCmd.SetArgs(os.Args[1:])" before calling "rootCmd.Execute()" +var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test") + +func isTesting() bool { + return inTest +} diff --git a/command_go121.go b/command_go121.go new file mode 100644 index 000000000..8b69f1524 --- /dev/null +++ b/command_go121.go @@ -0,0 +1,25 @@ +// Copyright 2013-2024 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 go1.21 +// +build go1.21 + +package cobra + +import "testing" + +func isTesting() bool { + // Only available starting with go 1.21 + return testing.Testing() +} diff --git a/command_test.go b/command_test.go index cd44992bd..837b6b300 100644 --- a/command_test.go +++ b/command_test.go @@ -390,7 +390,7 @@ func TestPlugin(t *testing.T) { checkStringContains(t, cmdHelp, "version for kubectl plugin") } -// TestPlugin checks usage as plugin with sub commands. +// TestPluginWithSubCommands checks usage as plugin with sub commands. func TestPluginWithSubCommands(t *testing.T) { rootCmd := &Command{ Use: "kubectl-plugin", @@ -2839,3 +2839,16 @@ func TestUnknownFlagShouldReturnSameErrorRegardlessOfArgPosition(t *testing.T) { }) } } + +// This tests verifies that when running unit tests, os.Args are not used. +// This is because we don't want to process any arguments that are provided +// by "go test"; instead, unit tests must set the arguments they need using +// rootCmd.SetArgs(). +func TestNoOSArgsWhenTesting(t *testing.T) { + root := &Command{Use: "root", Run: emptyRun} + os.Args = append(os.Args, "--unknown") + + if _, err := root.ExecuteC(); err != nil { + t.Errorf("error: %v", err) + } +} diff --git a/completions.go b/completions.go index 8fccdaf2c..0862d3f6c 100644 --- a/completions.go +++ b/completions.go @@ -270,6 +270,14 @@ func (c *Command) initCompleteCmd(args []string) { } } +// SliceValue is a reduced version of [pflag.SliceValue]. It is used to detect +// flags that accept multiple values and therefore can provide completion +// multiple times. +type SliceValue interface { + // GetSlice returns the flag value list as an array of strings. + GetSlice() []string +} + func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) { // The last argument, which is not completely typed by the user, // should not be part of the list of arguments @@ -399,10 +407,13 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi // If we have not found any required flags, only then can we show regular flags if len(completions) == 0 { doCompleteFlags := func(flag *pflag.Flag) { - if !flag.Changed || + _, acceptsMultiple := flag.Value.(SliceValue) + acceptsMultiple = acceptsMultiple || strings.Contains(flag.Value.Type(), "Slice") || strings.Contains(flag.Value.Type(), "Array") || - strings.HasPrefix(flag.Value.Type(), "stringTo") { + strings.HasPrefix(flag.Value.Type(), "stringTo") + + if !flag.Changed || acceptsMultiple { // If the flag is not already present, or if it can be specified multiple times (Array, Slice, or stringTo) // we suggest it as a completion completions = append(completions, getFlagNameCompletions(flag, toComplete)...) diff --git a/completions_test.go b/completions_test.go index df153fcf2..a8f378eb0 100644 --- a/completions_test.go +++ b/completions_test.go @@ -671,6 +671,29 @@ func TestFlagNameCompletionInGoWithDesc(t *testing.T) { } } +// customMultiString is a custom Value type that accepts multiple values, +// but does not include "Slice" or "Array" in its "Type" string. +type customMultiString []string + +var _ SliceValue = (*customMultiString)(nil) + +func (s *customMultiString) String() string { + return fmt.Sprintf("%v", *s) +} + +func (s *customMultiString) Set(v string) error { + *s = append(*s, v) + return nil +} + +func (s *customMultiString) Type() string { + return "multi string" +} + +func (s *customMultiString) GetSlice() []string { + return *s +} + func TestFlagNameCompletionRepeat(t *testing.T) { rootCmd := &Command{ Use: "root", @@ -693,6 +716,8 @@ func TestFlagNameCompletionRepeat(t *testing.T) { sliceFlag := rootCmd.Flags().Lookup("slice") rootCmd.Flags().BoolSliceP("bslice", "b", nil, "bool slice flag") bsliceFlag := rootCmd.Flags().Lookup("bslice") + rootCmd.Flags().VarP(&customMultiString{}, "multi", "m", "multi string flag") + multiFlag := rootCmd.Flags().Lookup("multi") // Test that flag names are not repeated unless they are an array or slice output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--first", "1", "--") @@ -706,6 +731,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) { "--array", "--bslice", "--help", + "--multi", "--second", "--slice", ":4", @@ -728,6 +754,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) { "--array", "--bslice", "--help", + "--multi", "--slice", ":4", "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") @@ -737,7 +764,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) { } // Test that flag names are not repeated unless they are an array or slice - output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--slice", "1", "--slice=2", "--array", "val", "--bslice", "true", "--") + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--slice", "1", "--slice=2", "--array", "val", "--bslice", "true", "--multi", "val", "--") if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -745,12 +772,14 @@ func TestFlagNameCompletionRepeat(t *testing.T) { sliceFlag.Changed = false arrayFlag.Changed = false bsliceFlag.Changed = false + multiFlag.Changed = false expected = strings.Join([]string{ "--array", "--bslice", "--first", "--help", + "--multi", "--second", "--slice", ":4", @@ -768,6 +797,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) { // Reset the flag for the next command sliceFlag.Changed = false arrayFlag.Changed = false + multiFlag.Changed = false expected = strings.Join([]string{ "--array", @@ -778,6 +808,8 @@ func TestFlagNameCompletionRepeat(t *testing.T) { "-f", "--help", "-h", + "--multi", + "-m", "--second", "-s", "--slice", @@ -797,6 +829,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) { // Reset the flag for the next command sliceFlag.Changed = false arrayFlag.Changed = false + multiFlag.Changed = false expected = strings.Join([]string{ "-a", diff --git a/doc/man_docs_test.go b/doc/man_docs_test.go index dfa5e16f8..ae6c8e533 100644 --- a/doc/man_docs_test.go +++ b/doc/man_docs_test.go @@ -141,9 +141,6 @@ func TestGenManSeeAlso(t *testing.T) { if err := assertLineFound(scanner, ".SH SEE ALSO"); err != nil { t.Fatalf("Couldn't find SEE ALSO section header: %v", err) } - if err := assertNextLineEquals(scanner, ".PP"); err != nil { - t.Fatalf("First line after SEE ALSO wasn't break-indent: %v", err) - } if err := assertNextLineEquals(scanner, `\fBroot-bbb(1)\fP, \fBroot-ccc(1)\fP`); err != nil { t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err) } diff --git a/go.mod b/go.mod index 8c80da018..3959690c0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/spf13/cobra go 1.15 require ( - github.com/cpuguy83/go-md2man/v2 v2.0.4 + github.com/cpuguy83/go-md2man/v2 v2.0.6 github.com/inconshreveable/mousetrap v1.1.0 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index ab40b433b..1be802826 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= diff --git a/powershell_completions.go b/powershell_completions.go index a830b7bca..746dcb92e 100644 --- a/powershell_completions.go +++ b/powershell_completions.go @@ -162,7 +162,10 @@ filter __%[1]s_escapeStringWithSpecialChars { if (-Not $Description) { $Description = " " } - @{Name="$Name";Description="$Description"} + New-Object -TypeName PSCustomObject -Property @{ + Name = "$Name" + Description = "$Description" + } } @@ -240,7 +243,12 @@ filter __%[1]s_escapeStringWithSpecialChars { __%[1]s_debug "Only one completion left" # insert space after value - [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space + if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ + [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } else { + $CompletionText + } } else { # Add the proper number of spaces to align the descriptions @@ -255,7 +263,12 @@ filter __%[1]s_escapeStringWithSpecialChars { $Description = " ($($comp.Description))" } - [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") + $CompletionText = "$($comp.Name)$Description" + if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ + [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") + } else { + $CompletionText + } } } @@ -264,7 +277,13 @@ filter __%[1]s_escapeStringWithSpecialChars { # insert space after value # MenuComplete will automatically show the ToolTip of # the highlighted value at the bottom of the suggestions. - [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + + $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space + if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ + [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } else { + $CompletionText + } } # TabCompleteNext and in case we get something unknown @@ -272,7 +291,13 @@ filter __%[1]s_escapeStringWithSpecialChars { # Like MenuComplete but we don't want to add a space here because # the user need to press space anyway to get the completion. # Description will not be shown because that's not possible with TabCompleteNext - [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + + $CompletionText = $($comp.Name | __%[1]s_escapeStringWithSpecialChars) + if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage"){ + [System.Management.Automation.CompletionResult]::new($CompletionText, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } else { + $CompletionText + } } } diff --git a/site/content/active_help.md b/site/content/active_help.md index 1b02c7014..ae6d723e3 100644 --- a/site/content/active_help.md +++ b/site/content/active_help.md @@ -20,12 +20,12 @@ bin/ internal/ scripts/ pkg/ testdata/ ## 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. +- Bash (using [bash completion V2](completions/_index.md#bash-completion-v2) only). Note that bash 4.4 or higher is required for the prompt to appear when an Active Help message is printed. - 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). +As Active Help uses the shell completion system, the implementation of Active Help messages is done by enhancing custom dynamic completions. If you are not familiar with dynamic completions, please refer to [Shell Completions](completions/_index.md). 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. @@ -148,7 +148,7 @@ 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. +Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](completions/_index.md#debugging) for details. When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `_ACTIVE_HELP` where any non-ASCII-alphanumeric characters are replaced by an `_`. For example, we can test deactivating some Active Help as shown below: