diff --git a/command.go b/command.go index 4cd712b..1960294 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 0000000..23bc0fe --- /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 0000000..8b69f15 --- /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 cd44992..837b6b3 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 8fccdaf..0862d3f 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 df153fc..a8f378e 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 dfa5e16..ae6c8e5 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 8c80da0..3959690 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 ab40b43..1be8028 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 a830b7b..746dcb9 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 1b02c70..ae6d723 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: