2023-03-06 02:28:31 +00:00
// Copyright 2013-2023 The Cobra Authors
2022-09-16 13:55:56 +02:00
//
// 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.
2015-03-16 15:31:03 -04:00
package cobra
import (
"bytes"
2018-02-08 21:34:46 +00:00
"fmt"
2015-03-16 15:31:03 -04:00
"os"
2016-04-02 22:45:01 +02:00
"os/exec"
2018-02-28 05:38:38 +00:00
"regexp"
2015-03-16 15:31:03 -04:00
"strings"
"testing"
)
2025-04-26 20:14:27 +00:00
// checkOmit checks if the 'unexpected' substring is present in 'found' .
// If it is, it logs an error using t.Errorf indicating that the 'unexpected'
// value was found. This function is typically used in tests to ensure that
// certain substrings are omitted from a string being tested.
//
// Parameters:
// - t: A testing.T instance for reporting errors.
// - found: The string to search within.
// - unexpected: The substring that should not be present in 'found' .
2015-04-07 17:38:22 -04:00
func checkOmit( t *testing.T, found, unexpected string) {
if strings.Contains( found, unexpected) {
2017-10-31 19:58:37 +01:00
t.Errorf( "Got: %q\nBut should not have!\n" , unexpected)
2015-04-07 17:38:22 -04:00
}
}
2025-04-26 20:14:27 +00:00
// check asserts that the ` found` string contains the ` expected` substring.
// If not, it logs an error with the expected and actual values.
// This function is typically used in testing to validate substrings within larger strings.
2015-03-16 15:31:03 -04:00
func check( t *testing.T, found, expected string) {
if !strings.Contains( found, expected) {
2017-10-31 19:58:37 +01:00
t.Errorf( "Expecting to contain: \n %q\nGot:\n %q\n" , expected, found)
2015-03-16 15:31:03 -04:00
}
}
2025-04-26 20:14:27 +00:00
// checkNumOccurrences checks if the string ` found` contains the substring ` expected` exactly ` expectedOccurrences` times. If not, it fails the test with an error message indicating the expected and actual occurrences.
2018-08-21 11:12:02 -05:00
func checkNumOccurrences( t *testing.T, found, expected string, expectedOccurrences int) {
numOccurrences := strings.Count( found, expected)
if numOccurrences != expectedOccurrences {
t.Errorf( "Expecting to contain %d occurrences of: \n %q\nGot %d:\n %q\n" , expectedOccurrences, expected, numOccurrences, found)
}
}
2025-04-26 20:14:27 +00:00
// checkRegex checks if the ` found` string matches the ` pattern` using regular expressions.
// If an error occurs during the matching process, it logs the error and fails the test.
// It asserts that the ` found` string should match the ` pattern` , otherwise failing the test with a detailed message.
2018-02-28 05:38:38 +00:00
func checkRegex( t *testing.T, found, pattern string) {
matched, err := regexp.MatchString( pattern, found)
if err != nil {
t.Errorf( "Error thrown performing MatchString: \n %s\n" , err)
}
if !matched {
t.Errorf( "Expecting to match: \n %q\nGot:\n %q\n" , pattern, found)
}
}
2025-04-26 20:14:27 +00:00
// runShellCheck runs shellcheck on the provided string with specific options and error codes.
// It executes shellcheck with the bash syntax, sending the input from the string through stdin.
// The function redirects the standard output and error of the command to os.Stdout and os.Stderr respectively.
// Returns an error if the execution of shellcheck fails or if there' s an issue with setting up the command input.
2016-04-02 22:45:01 +02:00
func runShellCheck( s string) error {
2021-02-08 00:08:50 +00:00
cmd := exec.Command( "shellcheck" , "-s" , "bash" , "-" , "-e" ,
2016-04-02 22:45:01 +02:00
"SC2034" , // PREFIX appears unused. Verify it or export it.
2021-02-08 00:08:50 +00:00
)
2016-04-02 22:45:01 +02:00
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
stdin, err := cmd.StdinPipe( )
if err != nil {
return err
}
go func( ) {
2021-02-08 00:08:50 +00:00
_, err := stdin.Write( [ ] byte( s) )
CheckErr( err)
2017-10-31 19:58:37 +01:00
stdin.Close( )
2016-04-02 22:45:01 +02:00
} ( )
return cmd.Run( )
}
2015-03-16 15:31:03 -04:00
// World worst custom function , just keep telling you to enter hello!
2018-08-21 11:12:02 -05:00
const bashCompletionFunc = ` __root_custom_func( ) {
2017-10-31 19:58:37 +01:00
COMPREPLY = ( "hello" )
2015-03-16 15:31:03 -04:00
}
`
2025-04-26 20:14:27 +00:00
// TestBashCompletions tests the generation of bash completion functions for a root command.
2015-03-16 15:31:03 -04:00
func TestBashCompletions( t *testing.T) {
2017-10-31 19:58:37 +01:00
rootCmd := & Command{
Use: "root" ,
ArgAliases: [ ] string{ "pods" , "nodes" , "services" , "replicationcontrollers" , "po" , "no" , "svc" , "rc" } ,
ValidArgs: [ ] string{ "pod" , "node" , "service" , "replicationcontroller" } ,
BashCompletionFunction: bashCompletionFunc,
2019-01-09 00:34:09 +00:00
Run: emptyRun,
2017-10-31 19:58:37 +01:00
}
rootCmd.Flags( ) .IntP( "introot" , "i" , -1, "help message for flag introot" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.MarkFlagRequired( "introot" ) )
2017-10-31 19:58:37 +01:00
// Filename.
rootCmd.Flags( ) .String( "filename" , "" , "Enter a filename" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.MarkFlagFilename( "filename" , "json" , "yaml" , "yml" ) )
2017-10-31 19:58:37 +01:00
// Persistent filename.
rootCmd.PersistentFlags( ) .String( "persistent-filename" , "" , "Enter a filename" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.MarkPersistentFlagFilename( "persistent-filename" ) )
assertNoErr( t, rootCmd.MarkPersistentFlagRequired( "persistent-filename" ) )
2017-10-31 19:58:37 +01:00
// Filename extensions.
rootCmd.Flags( ) .String( "filename-ext" , "" , "Enter a filename (extension limited)" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.MarkFlagFilename( "filename-ext" ) )
2017-10-31 19:58:37 +01:00
rootCmd.Flags( ) .String( "custom" , "" , "Enter a filename (extension limited)" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.MarkFlagCustom( "custom" , "__complete_custom" ) )
2017-10-31 19:58:37 +01:00
// Subdirectories in a given directory.
rootCmd.Flags( ) .String( "theme" , "" , "theme to use (located in /themes/THEMENAME/)" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.Flags( ) .SetAnnotation( "theme" , BashCompSubdirsInDir, [ ] string{ "themes" } ) )
2017-10-31 19:58:37 +01:00
2019-03-11 21:55:09 +09:00
// For two word flags check
rootCmd.Flags( ) .StringP( "two" , "t" , "" , "this is two word flags" )
rootCmd.Flags( ) .BoolP( "two-w-default" , "T" , false, "this is not two word flags" )
2017-10-31 19:58:37 +01:00
echoCmd := & Command{
Use: "echo [string to echo]" ,
Aliases: [ ] string{ "say" } ,
Short: "Echo anything to the screen" ,
Long: "an utterly useless command for testing." ,
Example: "Just run cobra-test echo" ,
Run: emptyRun,
}
2018-02-28 05:38:38 +00:00
echoCmd.Flags( ) .String( "filename" , "" , "Enter a filename" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, echoCmd.MarkFlagFilename( "filename" , "json" , "yaml" , "yml" ) )
2018-02-28 05:38:38 +00:00
echoCmd.Flags( ) .String( "config" , "" , "config to use (located in /config/PROFILE/)" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, echoCmd.Flags( ) .SetAnnotation( "config" , BashCompSubdirsInDir, [ ] string{ "config" } ) )
2018-02-28 05:38:38 +00:00
2017-10-31 19:58:37 +01:00
printCmd := & Command{
Use: "print [string to print]" ,
Args: MinimumNArgs( 1) ,
Short: "Print anything to the screen" ,
Long: "an absolutely utterly useless command for testing." ,
Run: emptyRun,
}
deprecatedCmd := & Command{
Use: "deprecated [can't do anything here]" ,
Args: NoArgs,
Short: "A command which is deprecated" ,
Long: "an absolutely utterly useless command for testing deprecation!." ,
Deprecated: "Please use echo instead" ,
Run: emptyRun,
}
colonCmd := & Command{
Use: "cmd:colon" ,
Run: emptyRun,
}
timesCmd := & Command{
Use: "times [# times] [string to echo]" ,
SuggestFor: [ ] string{ "counts" } ,
Args: OnlyValidArgs,
ValidArgs: [ ] string{ "one" , "two" , "three" , "four" } ,
Short: "Echo anything to the screen more times" ,
Long: "a slightly useless command for testing." ,
Run: emptyRun,
}
echoCmd.AddCommand( timesCmd)
rootCmd.AddCommand( echoCmd, printCmd, deprecatedCmd, colonCmd)
buf := new( bytes.Buffer)
2021-02-08 00:08:50 +00:00
assertNoErr( t, rootCmd.GenBashCompletion( buf) )
2017-10-31 19:58:37 +01:00
output := buf.String( )
check( t, output, "_root" )
check( t, output, "_root_echo" )
check( t, output, "_root_echo_times" )
check( t, output, "_root_print" )
check( t, output, "_root_cmd__colon" )
2015-03-16 15:31:03 -04:00
// check for required flags
2017-10-31 19:58:37 +01:00
check( t, output, ` must_have_one_flag += ( "--introot=" ) ` )
check( t, output, ` must_have_one_flag += ( "--persistent-filename=" ) ` )
2018-08-21 11:12:02 -05:00
// check for custom completion function with both qualified and unqualified name
checkNumOccurrences( t, output, ` __custom_func` , 2) // 1. check existence, 2. invoke
checkNumOccurrences( t, output, ` __root_custom_func` , 3) // 1. check existence, 2. invoke, 3. actual definition
// check for custom completion function body
2017-10-31 19:58:37 +01:00
check( t, output, ` COMPREPLY = ( "hello" ) ` )
2015-03-16 15:31:03 -04:00
// check for required nouns
2017-10-31 19:58:37 +01:00
check( t, output, ` must_have_one_noun += ( "pod" ) ` )
2016-03-25 16:05:56 +01:00
// check for noun aliases
2017-10-31 19:58:37 +01:00
check( t, output, ` noun_aliases += ( "pods" ) ` )
check( t, output, ` noun_aliases += ( "rc" ) ` )
checkOmit( t, output, ` must_have_one_noun += ( "pods" ) ` )
2015-06-22 14:28:16 -04:00
// check for filename extension flags
2017-10-31 19:58:37 +01:00
check( t, output, ` flags_completion += ( "_filedir" ) ` )
2015-06-22 14:28:16 -04:00
// check for filename extension flags
2017-10-31 19:58:37 +01:00
check( t, output, ` must_have_one_noun += ( "three" ) ` )
2017-09-01 16:16:37 +01:00
// check for filename extension flags
2018-02-08 21:34:46 +00:00
check( t, output, fmt.Sprintf( ` flags_completion += ( "__%s_handle_filename_extension_flag json|yaml|yml" ) ` , rootCmd.Name( ) ) )
2018-02-28 05:38:38 +00:00
// check for filename extension flags in a subcommand
checkRegex( t, output, fmt.Sprintf( ` _root_echo\( \) \n { [ ^} ] *flags_completion\+ = \( "__%s_handle_filename_extension_flag json\|yaml\|yml" \) ` , rootCmd.Name( ) ) )
2016-03-20 21:05:18 +01:00
// check for custom flags
2017-10-31 19:58:37 +01:00
check( t, output, ` flags_completion += ( "__complete_custom" ) ` )
2015-08-09 13:30:58 -06:00
// check for subdirs_in_dir flags
2018-02-08 21:34:46 +00:00
check( t, output, fmt.Sprintf( ` flags_completion += ( "__%s_handle_subdirs_in_dir_flag themes" ) ` , rootCmd.Name( ) ) )
2018-02-28 05:38:38 +00:00
// check for subdirs_in_dir flags in a subcommand
checkRegex( t, output, fmt.Sprintf( ` _root_echo\( \) \n { [ ^} ] *flags_completion\+ = \( "__%s_handle_subdirs_in_dir_flag config" \) ` , rootCmd.Name( ) ) )
2015-04-07 17:38:22 -04:00
2019-03-11 21:55:09 +09:00
// check two word flags
check( t, output, ` two_word_flags += ( "--two" ) ` )
check( t, output, ` two_word_flags += ( "-t" ) ` )
checkOmit( t, output, ` two_word_flags += ( "--two-w-default" ) ` )
checkOmit( t, output, ` two_word_flags += ( "-T" ) ` )
2020-09-09 17:34:51 +02:00
// check local nonpersistent flag
check( t, output, ` local_nonpersistent_flags += ( "--two" ) ` )
check( t, output, ` local_nonpersistent_flags += ( "--two=" ) ` )
check( t, output, ` local_nonpersistent_flags += ( "-t" ) ` )
check( t, output, ` local_nonpersistent_flags += ( "--two-w-default" ) ` )
check( t, output, ` local_nonpersistent_flags += ( "-T" ) ` )
2017-10-31 19:58:37 +01:00
checkOmit( t, output, deprecatedCmd.Name( ) )
2016-04-02 22:45:01 +02:00
2017-10-31 19:58:37 +01:00
// If available, run shellcheck against the script.
2016-04-02 22:45:01 +02:00
if err := exec.Command( "which" , "shellcheck" ) .Run( ) ; err != nil {
return
}
2017-10-31 19:58:37 +01:00
if err := runShellCheck( output) ; err != nil {
2016-04-02 22:45:01 +02:00
t.Fatalf( "shellcheck failed: %v" , err)
}
2015-03-16 15:31:03 -04:00
}
2016-08-02 14:49:33 -07:00
2025-04-26 20:14:27 +00:00
// TestBashCompletionHiddenFlag tests that a hidden flag is not included in the Bash completion output.
// It creates a command with a hidden flag and asserts that after generating the Bash completion script,
// the hidden flag is not present in the output.
2016-08-02 14:49:33 -07:00
func TestBashCompletionHiddenFlag( t *testing.T) {
2017-10-31 19:58:37 +01:00
c := & Command{ Use: "c" , Run: emptyRun}
2016-08-02 14:49:33 -07:00
2017-10-31 19:58:37 +01:00
const flagName = "hiddenFlag"
c.Flags( ) .Bool( flagName, false, "" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, c.Flags( ) .MarkHidden( flagName) )
2016-08-02 14:49:33 -07:00
2017-10-31 19:58:37 +01:00
buf := new( bytes.Buffer)
2021-02-08 00:08:50 +00:00
assertNoErr( t, c.GenBashCompletion( buf) )
2017-10-31 19:58:37 +01:00
output := buf.String( )
2016-08-02 14:49:33 -07:00
2017-10-31 19:58:37 +01:00
if strings.Contains( output, flagName) {
t.Errorf( "Expected completion to not include %q flag: Got %v" , flagName, output)
2016-08-02 14:49:33 -07:00
}
}
2016-08-02 15:01:33 -07:00
2025-04-26 20:14:27 +00:00
// TestBashCompletionDeprecatedFlag tests the generation of bash completion script for a command with a deprecated flag.
// It ensures that the deprecated flag is not included in the generated completion script.
// The function takes a testing.T pointer as an argument to perform assertions and checks.
2016-08-02 15:01:33 -07:00
func TestBashCompletionDeprecatedFlag( t *testing.T) {
2017-10-31 19:58:37 +01:00
c := & Command{ Use: "c" , Run: emptyRun}
2017-05-14 14:57:11 +02:00
2017-10-31 19:58:37 +01:00
const flagName = "deprecated-flag"
c.Flags( ) .Bool( flagName, false, "" )
2021-02-08 00:08:50 +00:00
assertNoErr( t, c.Flags( ) .MarkDeprecated( flagName, "use --not-deprecated instead" ) )
2017-05-14 14:57:11 +02:00
2017-06-05 19:32:33 +02:00
buf := new( bytes.Buffer)
2021-02-08 00:08:50 +00:00
assertNoErr( t, c.GenBashCompletion( buf) )
2017-10-31 19:58:37 +01:00
output := buf.String( )
2017-05-14 14:57:11 +02:00
2017-10-31 19:58:37 +01:00
if strings.Contains( output, flagName) {
t.Errorf( "expected completion to not include %q flag: Got %v" , flagName, output)
2017-05-14 14:57:11 +02:00
}
}
2020-09-09 17:34:51 +02:00
2025-04-26 20:14:27 +00:00
// TestBashCompletionTraverseChildren tests the bash completion generation for commands with TraverseChildren set to true.
// It checks that local non-persistent flags are not included in the generated completion script.
2020-09-09 17:34:51 +02:00
func TestBashCompletionTraverseChildren( t *testing.T) {
c := & Command{ Use: "c" , Run: emptyRun, TraverseChildren: true}
c.Flags( ) .StringP( "string-flag" , "s" , "" , "string flag" )
c.Flags( ) .BoolP( "bool-flag" , "b" , false, "bool flag" )
buf := new( bytes.Buffer)
2021-02-08 00:08:50 +00:00
assertNoErr( t, c.GenBashCompletion( buf) )
2020-09-09 17:34:51 +02:00
output := buf.String( )
// check that local nonpersistent flag are not set since we have TraverseChildren set to true
checkOmit( t, output, ` local_nonpersistent_flags += ( "--string-flag" ) ` )
checkOmit( t, output, ` local_nonpersistent_flags += ( "--string-flag=" ) ` )
checkOmit( t, output, ` local_nonpersistent_flags += ( "-s" ) ` )
checkOmit( t, output, ` local_nonpersistent_flags += ( "--bool-flag" ) ` )
checkOmit( t, output, ` local_nonpersistent_flags += ( "-b" ) ` )
}
2022-06-15 20:08:16 -04:00
2025-04-26 20:14:27 +00:00
// TestBashCompletionNoActiveHelp tests the generation of bash completion without active help.
//
// Parameters:
// - t: A testing.T instance for assertions and logging test failures.
//
// This function creates a Command instance, generates bash completion with disabled active help,
// and checks if the output contains the correct environment variable setting to disable active help.
2022-06-15 20:08:16 -04:00
func TestBashCompletionNoActiveHelp( t *testing.T) {
c := & Command{ Use: "c" , Run: emptyRun}
buf := new( bytes.Buffer)
assertNoErr( t, c.GenBashCompletion( buf) )
output := buf.String( )
// check that active help is being disabled
activeHelpVar := activeHelpEnvVar( c.Name( ) )
check( t, output, fmt.Sprintf( "%s=0" , activeHelpVar) )
}