Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(bash): Add a bunch of tests for bash special characters. #25

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion testprog/testprog.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ import (
)

var (
completions = []string{"bear\tan animal", "bearpaw\ta dessert", "dog", "unicorn\tmythical"}
completions = []string{"bear\tan animal", "bearpaw\ta dessert", "dog", "unicorn\tmythical"}
completionsWithSpecialCharacters = []string{
"bash1 space\twith space",
`bash2\escape` + "\twith escape",
`bash3\ escaped\ space` + "\twith escape and space",
"bash4>redirect\twith redirect",
"bash5#comment\twith comment",
"bash6$var\twith var",
"bash7|pipe\twith pipe",
"bash8;semicolon\twith semicolon",
"bash9=equals\twith equal",
"bashA:colon\twith colon",
}
specialCharComps = []string{"at@", "equal=", "slash/", "colon:", "period.", "comma,", "letter"}
specialFlag string
)

func getCompsFilteredByPrefix(prefix string) []string {
Expand Down Expand Up @@ -47,6 +60,34 @@ var defaultCmdPrefix = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {},
}

var specialCharsCmdPrefix = &cobra.Command{
Use: "special-chars",
Short: "Directive: special chars",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

var finalComps []string
for _, comp := range completionsWithSpecialCharacters {
if strings.HasPrefix(comp, toComplete) {
finalComps = append(finalComps, comp)
}
}
return finalComps, cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
if specialFlag != "" {
fmt.Println("special flag:", specialFlag)
}

fmt.Println("args:")
for _, arg := range args {
fmt.Println(arg)
}
},
}

var noSpaceCmdPrefix = &cobra.Command{
Use: "nospace",
Short: "Directive: no space",
Expand Down Expand Up @@ -221,6 +262,12 @@ func setFlags() {
rootCmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"dir"})

dashArgCmd.Flags().Bool("flag", false, "a flag")

specialCharsCmdPrefix.Flags().StringVar(&specialFlag, "special", "", "special char")
specialCharsCmdPrefix.RegisterFlagCompletionFunc("special", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completionsWithSpecialCharacters, cobra.ShellCompDirectiveNoFileComp
})

}

func main() {
Expand All @@ -244,6 +291,7 @@ func main() {
noFileCmdPrefix,
noFileNoSpaceCmdPrefix,
defaultCmdPrefix,
specialCharsCmdPrefix,
)

noPrefixCmd.AddCommand(
Expand Down
24 changes: 12 additions & 12 deletions tests/bash/comp-test-lib.bash
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!bash

# COLOR codes
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
RED="$(echo -e '\033[0;31m')"
GREEN="$(echo -e '\033[0;32m')"
NC="$(echo -e '\033[0m')"
_compTests_nofile=/tmp/comptests.bash.nofile
_compTests_nospace=/tmp/comptests.bash.nospace
# Global variable to keep track of if a test has failed.
Expand Down Expand Up @@ -55,8 +55,8 @@ shopt -s expand_aliases
_completionTests_verifyCompletion() {
_completionTests_reset

local cmdLine=$1
local expected=$2
local cmdLine="$1"
local expected="$2"
local currentFailure=0

local nofile=0
Expand Down Expand Up @@ -114,12 +114,12 @@ _completionTests_verifyCompletion() {
if [ "${#result}" -gt 50 ]; then
resultOut="${result:0:50} <truncated>"
fi
echo -e "${GREEN}SUCCESS: \"$cmdLine\" completes to \"$resultOut\"$NC"
echo "${GREEN}SUCCESS: \"$cmdLine\" completes to \"$resultOut\"$NC"
return 0
fi

_completionTests_TEST_FAILED=1
echo -e "${RED}ERROR: \"$cmdLine\" should complete to \"$expected\" but we got \"$result\"$NC"
echo "${RED}ERROR: \"$cmdLine\" should complete to \"$expected\" but we got \"$result\"$NC"
return 1
}

Expand All @@ -143,10 +143,10 @@ _completionTests_timing() {
timing=$({ time { _completionTests_complete "$1" > /dev/null; } } 2>&1)
if (( $(echo "$timing > ${2}" | bc -l) )); then
_completionTests_TEST_FAILED=1
echo -e "${RED}<= TIMING => ${3}: 1000 completions took ${timing} seconds > ${2-0.1} seconds limit$NC"
echo "${RED}<= TIMING => ${3}: 1000 completions took ${timing} seconds > ${2-0.1} seconds limit$NC"
return 1
else
echo -e "${GREEN}<= TIMING => ${3}: 1000 completions took ${timing} seconds < ${2-0.1} seconds limit$NC"
echo "${GREEN}<= TIMING => ${3}: 1000 completions took ${timing} seconds < ${2-0.1} seconds limit$NC"
return 0
fi
}
Expand Down Expand Up @@ -180,7 +180,7 @@ _completionTests_complete() {
COMP_CWORD=$((${#COMP_WORDS[@]}-1))
# We must check for a space as the last character which will tell us
# that the previous word is complete and the cursor is on the next word.
[ "${cmdLine: -1}" = " " ] && COMP_CWORD=${#COMP_WORDS[@]}
[ "${cmdLine: -1}" = " " ] && COMP_CWORD=${#COMP_WORDS[@]} && COMP_WORDS[COMP_CWORD]=''
JeffFaer marked this conversation as resolved.
Show resolved Hide resolved

# Call the completion function associated with the binary being called.
# Also redirect stderr to stdout so that the tests fail if anything is printed
Expand Down Expand Up @@ -222,11 +222,11 @@ _completionTests_checkDirective() {
[ -f $_compTests_nospace ] && realnospace=1

if [ $requestnofile -ne $realnofile ]; then
echo -e "${RED}ERROR: \"$cmdLine\" expected nofile=$requestnofile but got nofile=$realnofile$NC"
echo "${RED}ERROR: \"$cmdLine\" expected nofile=$requestnofile but got nofile=$realnofile$NC"
return 1
fi
if [ $requestnospace -ne $realnospace ]; then
echo -e "${RED}ERROR: \"$cmdLine\" expected nospace=$requestnospace but got nospace=$realnospace$NC"
echo "${RED}ERROR: \"$cmdLine\" expected nospace=$requestnospace but got nospace=$realnospace$NC"
return 1
fi

Expand Down
190 changes: 152 additions & 38 deletions tests/bash/comp-tests.bash
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ verifyDebug() {
_completionTests_verifyCompletion "testprog help comp" "completion" nofile
if ! test -s $debugfile; then
# File should not be empty
echo -e "${RED}ERROR: No debug logs were printed to ${debugfile}${NC}"
echo "${RED}ERROR: No debug logs were printed to ${debugfile}${NC}"
_completionTests_TEST_FAILED=1
else
echo -e "${GREEN}SUCCESS: Debug logs were printed to ${debugfile}${NC}"
echo "${GREEN}SUCCESS: Debug logs were printed to ${debugfile}${NC}"
fi
unset BASH_COMP_DEBUG_FILE
}
Expand All @@ -27,11 +27,11 @@ verifyRedirect() {
_completionTests_verifyCompletion "testprog completion bash > notexist" ""
if test -f notexist; then
# File should not exist
echo -e "${RED}ERROR: completion mistakenly created the file 'notexist'${NC}"
echo "${RED}ERROR: completion mistakenly created the file 'notexist'${NC}"
_completionTests_TEST_FAILED=1
rm -f notexist
else
echo -e "${GREEN}SUCCESS: No extra file created, as expected${NC}"
echo "${GREEN}SUCCESS: No extra file created, as expected${NC}"
fi
}

Expand Down Expand Up @@ -154,6 +154,55 @@ _completionTests_verifyCompletion "testprog --customComp f" "firstComp forthComp
_completionTests_verifyCompletion "testprog --customComp=" "firstComp secondComp forthComp" nofile
_completionTests_verifyCompletion "testprog --customComp=f" "firstComp forthComp" nofile

#################################################
# Special characters
#################################################
if [ "$BASHCOMP_VERSION" = bash2 ]; then
# When there are may completions that match, these completions will be shown in a list
# and we do not escape them. We only escape them when there is a single completion as it
# will be inserted directly into the command line.
_completionTests_verifyCompletion 'testprog prefix special-chars bash' 'bash1 space bash2\escape bash3\ escaped\ space bash4>redirect bash5#comment bash6$var bash7|pipe bash8;semicolon bash9=equals bashA:colon' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash1' 'bash1\ space' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash1 ' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash2' 'bash2\\escape' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash2\\e' 'bash2\\escape' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash2e' '' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash2\e' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash3' 'bash3\\\ escaped\\\ space' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash3\\' 'bash3\\\ escaped\\\ space' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash3\ ' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash4' 'bash4\>redirect' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash4\>' 'bash4\>redirect' nofile
# Surprisingly, bash still calls the completion function with an unescaped redirect, but it does not
# pass the directive appropriately. This looks like a bug in bash. Either way, we want our
# script to return no completion so as to let bash do file completion.
_completionTests_verifyCompletion 'testprog prefix special-chars bash4>' ''

_completionTests_verifyCompletion 'testprog prefix special-chars bash5#c' 'bash5#comment' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash6\$v' 'bash6\$var' nofile
# Bash still calls the completion function with an unescaped variable
# Furthermore, compgen ignores escape characters when matchging, so bash6\$var matches bash6$v
_completionTests_verifyCompletion 'testprog prefix special-chars bash6$v' 'bash6\$var' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash7\|p' 'bash7\|pipe' nofile
# In practice, bash justifiably does not call our completion script in the below case
# because after the pipe (|), it expects another command. So, we don't need to test this.
# _completionTests_verifyCompletion 'testprog prefix special-chars bash7|p' ''

_completionTests_verifyCompletion 'testprog prefix special-chars bash8\;s' 'bash8\;semicolon' nofile
# In practice, bash justifiably does not call our completion script in the below case
# because after the semicolon (;), it expects another command. So, we don't need to test this.
# _completionTests_verifyCompletion 'testprog prefix special-chars bash8;s' '''

_completionTests_verifyCompletion 'testprog prefix special-chars bash9=e' 'equals' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bashA:c' 'colon' nofile
fi

#################################################
# Special cases
#################################################
Expand Down Expand Up @@ -192,25 +241,28 @@ fi
# Measure speed of execution without descriptions (for both v1 and v2)
_completionTests_timing "testprog manycomps " 0.2 "no descriptions"

# Test other bash completion types with descriptions disabled.
# There should be no change in behaviour when there are no descriptions.
# The types are: menu-complete/menu-complete-backward (COMP_TYPE == 37)
# and insert-completions (COMP_TYPE == 42)
COMP_TYPE=37
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile
# COMP_TYPE does not get set by bash 3
if [ $BASH_VERSINFO != 3 ]; then
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that there is no point in running the tests that set COMP_TYPE for bash 3 because COMP_TYPE only gets set by bash 4. So the tests were passing because we were setting COMP_TYPE ourselves, but it is not representative of what bash 3 does. So I have disable then for bash 3.

# Test other bash completion types with descriptions disabled.
# There should be no change in behaviour when there are no descriptions.
# The types are: menu-complete/menu-complete-backward (COMP_TYPE == 37)
# and insert-completions (COMP_TYPE == 42)
COMP_TYPE=37
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile

# Measure speed of execution with menu-complete without descriptions (for both v1 and v2)
_completionTests_timing "testprog manycomps " 0.2 "menu-complete no descs"
# Measure speed of execution with menu-complete without descriptions (for both v1 and v2)
_completionTests_timing "testprog manycomps " 0.2 "menu-complete no descs"

COMP_TYPE=42
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile
COMP_TYPE=42
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile

# Measure speed of execution with insert-completions without descriptions (for both v1 and v2)
_completionTests_timing "testprog manycomps " 0.2 "insert-completions no descs"
# Measure speed of execution with insert-completions without descriptions (for both v1 and v2)
_completionTests_timing "testprog manycomps " 0.2 "insert-completions no descs"

unset COMP_TYPE
unset COMP_TYPE
fi

# Test descriptions of bash v2
if [ "$BASHCOMP_VERSION" = bash2 ]; then
Expand Down Expand Up @@ -256,26 +308,88 @@ EOF
# Measure speed of execution with descriptions
_completionTests_timing "testprog manycomps " 0.5 "with descriptions"

# Test descriptions are properly removed when using other bash completion types
# The types are: menu-complete/menu-complete-backward (COMP_TYPE == 37)
# and insert-completions (COMP_TYPE == 42)
COMP_TYPE=37
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile

# Measure speed of execution with menu-complete with descriptions
_completionTests_timing "testprog manycomps " 0.2 "menu-complete with descs"

COMP_TYPE=42
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile

# Measure speed of execution with insert-completions with descriptions
_completionTests_timing "testprog manycomps " 0.2 "insert-completions no descs"

unset COMP_TYPE
############################
# Special character handling
############################
# When there are may completions that match, these completions will be shown in a list
# and we do not escape them. We only escape them when there is a single completion as it
# will be inserted directly into the command line.
_completionTests_verifyCompletion 'testprog prefix special-chars bash' "\
bash1 space (with space) \
bash2\escape (with escape) \
bash3\ escaped\ space (with escape and space) \
bash4>redirect (with redirect) \
bash5#comment (with comment) \
bash6\$var (with var) \
bash7|pipe (with pipe) \
bash8;semicolon (with semicolon) \
bash9=equals (with equal) \
bashA:colon (with colon)" nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash1' 'bash1\ space' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash1 ' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash2' 'bash2\\escape' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash2\\e' 'bash2\\escape' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash2e' '' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash2\e' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash3' 'bash3\\\ escaped\\\ space' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash3\\' 'bash3\\\ escaped\\\ space' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash3\ ' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash4' 'bash4\>redirect' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bash4\>' 'bash4\>redirect' nofile
# Surprisingly, bash still calls the completion function with an unescaped redirect, but it does not
# pass the directive appropriately. This looks like a bug in bash. Either way, we want our
# script to return no completion so as to let bash do file completion.
_completionTests_verifyCompletion 'testprog prefix special-chars bash4>' ''

_completionTests_verifyCompletion 'testprog prefix special-chars bash5#c' 'bash5#comment' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash6\$v' 'bash6\$var' nofile
# Bash still calls the completion function with an unescaped variable
_completionTests_verifyCompletion 'testprog prefix special-chars bash6$v' '' nofile

_completionTests_verifyCompletion 'testprog prefix special-chars bash7\|p' 'bash7\|pipe' nofile
# In practice, bash justifiably does not call our completion script in the below case
# because after the pipe (|), it expects another command. So, we don't need to test this.
# _completionTests_verifyCompletion 'testprog prefix special-chars bash7|p' ''

_completionTests_verifyCompletion 'testprog prefix special-chars bash8\;s' 'bash8\;semicolon' nofile
# In practice, bash justifiably does not call our completion script in the below case
# because after the semicolon (;), it expects another command. So, we don't need to test this.
# _completionTests_verifyCompletion 'testprog prefix special-chars bash8;s' '''

_completionTests_verifyCompletion 'testprog prefix special-chars bash9=e' 'equals' nofile
_completionTests_verifyCompletion 'testprog prefix special-chars bashA:c' 'colon' nofile
##################################
# end of pecial character handling
##################################

# COMP_TYPE does not get set by bash 3
if [ $BASH_VERSINFO != 3 ]; then
# Test descriptions are properly removed when using other bash completion types
# The types are: menu-complete/menu-complete-backward (COMP_TYPE == 37)
# and insert-completions (COMP_TYPE == 42)
COMP_TYPE=37
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile

# Measure speed of execution with menu-complete with descriptions
_completionTests_timing "testprog manycomps " 0.2 "menu-complete with descs"

COMP_TYPE=42
_completionTests_verifyCompletion "testprog prefix nospace b" "bear bearpaw" nospace
_completionTests_verifyCompletion "testprog prefix nofile b" "bear bearpaw" nofile

# Measure speed of execution with insert-completions with descriptions
_completionTests_timing "testprog manycomps " 0.2 "insert-completions no descs"

unset COMP_TYPE
fi
fi

# This must be the last call. It allows to exit with an exit code
# that reflects the final status of all the tests.
_completionTests_exit
_completionTests_exit