[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

fix: update fish script based on cobra (#100)

* fix: update fish script based on cobra

* fix: lint

* chore: add changeset

authored by

AmirHossein Sakhravi and committed by
GitHub
(Jan 24, 2026, 1:22 PM +0330) 23a6ef9e efaa0603

+710 -321
+5
.changeset/witty-shoes-brake.md
··· 1 + --- 2 + '@bomb.sh/tab': patch 3 + --- 4 + 5 + Update fish shell completion script to match latest Cobra output. (#99)
+192 -107
src/fish.ts
··· 19 19 20 20 return `# fish completion for ${name} -*- shell-script -*- 21 21 22 - # Define shell completion directives 23 - set -l ShellCompDirectiveError ${ShellCompDirectiveError} 24 - set -l ShellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace} 25 - set -l ShellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp} 26 - set -l ShellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt} 27 - set -l ShellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs} 28 - set -l ShellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder} 29 - 30 22 function __${nameForVar}_debug 31 23 set -l file "$BASH_COMP_DEBUG_FILE" 32 24 if test -n "$file" ··· 37 29 function __${nameForVar}_perform_completion 38 30 __${nameForVar}_debug "Starting __${nameForVar}_perform_completion" 39 31 40 - # Extract all args except the completion flag 41 - set -l args (string match -v -- "--completion=" (commandline -opc)) 42 - 43 - # Extract the current token being completed 44 - set -l current_token (commandline -ct) 45 - 46 - # Check if current token starts with a dash 47 - set -l flag_prefix "" 48 - if string match -q -- "-*" $current_token 49 - set flag_prefix "--flag=" 50 - end 51 - 52 - __${nameForVar}_debug "Current token: $current_token" 53 - __${nameForVar}_debug "All args: $args" 32 + # Extract all args except the last one 33 + set -l args (commandline -opc) 34 + # Extract the last arg and escape it in case it is a space or wildcard 35 + set -l lastArg (string escape -- (commandline -ct)) 36 + 37 + __${nameForVar}_debug "args: $args" 38 + __${nameForVar}_debug "last arg: $lastArg" 39 + 40 + # Build the completion request command 41 + set -l requestComp "${exec} complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg" 54 42 55 - # Call the completion program and get the results 56 - set -l requestComp "${exec} complete -- $args" 57 43 __${nameForVar}_debug "Calling $requestComp" 58 44 set -l results (eval $requestComp 2> /dev/null) 59 - 45 + 60 46 # Some programs may output extra empty lines after the directive. 61 47 # Let's ignore them or else it will break completion. 62 48 # Ref: https://github.com/spf13/cobra/issues/1279 63 49 for line in $results[-1..1] 64 - if test (string sub -s 1 -l 1 -- $line) = ":" 65 - # The directive 66 - set -l directive (string sub -s 2 -- $line) 67 - set -l directive_num (math $directive) 50 + if test (string trim -- $line) = "" 51 + # Found an empty line, remove it 52 + set results $results[1..-2] 53 + else 54 + # Found non-empty line, we have our proper output 68 55 break 69 56 end 70 57 end 71 - 72 - # No directive specified, use default 73 - if not set -q directive_num 74 - set directive_num 0 58 + 59 + set -l comps $results[1..-2] 60 + set -l directiveLine $results[-1] 61 + 62 + # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>) 63 + # completions must be prefixed with the flag 64 + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 65 + 66 + __${nameForVar}_debug "Comps: $comps" 67 + __${nameForVar}_debug "DirectiveLine: $directiveLine" 68 + __${nameForVar}_debug "flagPrefix: $flagPrefix" 69 + 70 + for comp in $comps 71 + printf "%s%s\\n" "$flagPrefix" "$comp" 75 72 end 76 - 77 - __${nameForVar}_debug "Directive: $directive_num" 78 73 79 - # Process completions based on directive 80 - if test $directive_num -eq $ShellCompDirectiveError 81 - # Error code. No completion. 82 - __${nameForVar}_debug "Received error directive: aborting." 74 + printf "%s\\n" "$directiveLine" 75 + end 76 + 77 + # This function limits calls to __${nameForVar}_perform_completion, by caching the result 78 + function __${nameForVar}_perform_completion_once 79 + __${nameForVar}_debug "Starting __${nameForVar}_perform_completion_once" 80 + 81 + if test -n "$__${nameForVar}_perform_completion_once_result" 82 + __${nameForVar}_debug "Seems like a valid result already exists, skipping __${nameForVar}_perform_completion" 83 + return 0 84 + end 85 + 86 + set --global __${nameForVar}_perform_completion_once_result (__${nameForVar}_perform_completion) 87 + if test -z "$__${nameForVar}_perform_completion_once_result" 88 + __${nameForVar}_debug "No completions, probably due to a failure" 83 89 return 1 84 90 end 85 91 86 - # Filter out the directive (last line) 87 - if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":" 88 - set results $results[1..-2] 92 + __${nameForVar}_debug "Performed completions and set __${nameForVar}_perform_completion_once_result" 93 + return 0 94 + end 95 + 96 + # This function is used to clear the cached result after completions are run 97 + function __${nameForVar}_clear_perform_completion_once_result 98 + __${nameForVar}_debug "" 99 + __${nameForVar}_debug "========= clearing previously set __${nameForVar}_perform_completion_once_result variable ==========" 100 + set --erase __${nameForVar}_perform_completion_once_result 101 + __${nameForVar}_debug "Successfully erased the variable __${nameForVar}_perform_completion_once_result" 102 + end 103 + 104 + function __${nameForVar}_requires_order_preservation 105 + __${nameForVar}_debug "" 106 + __${nameForVar}_debug "========= checking if order preservation is required ==========" 107 + 108 + __${nameForVar}_perform_completion_once 109 + if test -z "$__${nameForVar}_perform_completion_once_result" 110 + __${nameForVar}_debug "Error determining if order preservation is required" 111 + return 1 89 112 end 90 113 91 - # No completions, let fish handle file completions unless forbidden 92 - if test (count $results) -eq 0 93 - if test $directive_num -ne $ShellCompDirectiveNoFileComp 94 - __${nameForVar}_debug "No completions, performing file completion" 95 - return 1 96 - end 97 - __${nameForVar}_debug "No completions, but file completion forbidden" 114 + set -l directive (string sub --start 2 $__${nameForVar}_perform_completion_once_result[-1]) 115 + __${nameForVar}_debug "Directive is: $directive" 116 + 117 + set -l shellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder} 118 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) 119 + __${nameForVar}_debug "Keeporder is: $keeporder" 120 + 121 + if test $keeporder -ne 0 122 + __${nameForVar}_debug "This does require order preservation" 98 123 return 0 99 124 end 100 125 101 - # Filter file extensions 102 - if test $directive_num -eq $ShellCompDirectiveFilterFileExt 103 - __${nameForVar}_debug "File extension filtering" 104 - set -l file_extensions 105 - for item in $results 106 - if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-" 107 - set -a file_extensions "*$item" 108 - end 109 - end 110 - __${nameForVar}_debug "File extensions: $file_extensions" 111 - 112 - # Use the file extensions as completions 113 - set -l completions 114 - for ext in $file_extensions 115 - # Get all files matching the extension 116 - set -a completions (string replace -r '^.*/' '' -- $ext) 117 - end 118 - 119 - for item in $completions 120 - echo -e "$item\t" 121 - end 122 - return 0 126 + __${nameForVar}_debug "This doesn't require order preservation" 127 + return 1 128 + end 129 + 130 + # This function does two things: 131 + # - Obtain the completions and store them in the global __${nameForVar}_comp_results 132 + # - Return false if file completion should be performed 133 + function __${nameForVar}_prepare_completions 134 + __${nameForVar}_debug "" 135 + __${nameForVar}_debug "========= starting completion logic ==========" 136 + 137 + # Start fresh 138 + set --erase __${nameForVar}_comp_results 139 + 140 + __${nameForVar}_perform_completion_once 141 + __${nameForVar}_debug "Completion results: $__${nameForVar}_perform_completion_once_result" 142 + 143 + if test -z "$__${nameForVar}_perform_completion_once_result" 144 + __${nameForVar}_debug "No completion, probably due to a failure" 145 + # Might as well do file completion, in case it helps 146 + return 1 147 + end 148 + 149 + set -l directive (string sub --start 2 $__${nameForVar}_perform_completion_once_result[-1]) 150 + set --global __${nameForVar}_comp_results $__${nameForVar}_perform_completion_once_result[1..-2] 151 + 152 + __${nameForVar}_debug "Completions are: $__${nameForVar}_comp_results" 153 + __${nameForVar}_debug "Directive is: $directive" 154 + 155 + set -l shellCompDirectiveError ${ShellCompDirectiveError} 156 + set -l shellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace} 157 + set -l shellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp} 158 + set -l shellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt} 159 + set -l shellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs} 160 + 161 + if test -z "$directive" 162 + set directive 0 163 + end 164 + 165 + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) 166 + if test $compErr -eq 1 167 + __${nameForVar}_debug "Received error directive: aborting." 168 + # Might as well do file completion, in case it helps 169 + return 1 170 + end 171 + 172 + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) 173 + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) 174 + if test $filefilter -eq 1; or test $dirfilter -eq 1 175 + __${nameForVar}_debug "File extension filtering or directory filtering not supported" 176 + # Do full file completion instead 177 + return 1 123 178 end 124 179 125 - # Filter directories 126 - if test $directive_num -eq $ShellCompDirectiveFilterDirs 127 - __${nameForVar}_debug "Directory filtering" 128 - set -l dirs 129 - for item in $results 130 - if test -d "$item" 131 - set -a dirs "$item/" 180 + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) 181 + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) 182 + 183 + __${nameForVar}_debug "nospace: $nospace, nofiles: $nofiles" 184 + 185 + # If we want to prevent a space, or if file completion is NOT disabled, 186 + # we need to count the number of valid completions. 187 + # To do so, we will filter on prefix as the completions we have received 188 + # may not already be filtered so as to allow fish to match on different 189 + # criteria than the prefix. 190 + if test $nospace -ne 0; or test $nofiles -eq 0 191 + set -l prefix (commandline -t | string escape --style=regex) 192 + __${nameForVar}_debug "prefix: $prefix" 193 + 194 + set -l completions (string match -r -- "^$prefix.*" $__${nameForVar}_comp_results) 195 + set --global __${nameForVar}_comp_results $completions 196 + __${nameForVar}_debug "Filtered completions are: $__${nameForVar}_comp_results" 197 + 198 + # Important not to quote the variable for count to work 199 + set -l numComps (count $__${nameForVar}_comp_results) 200 + __${nameForVar}_debug "numComps: $numComps" 201 + 202 + if test $numComps -eq 1; and test $nospace -ne 0 203 + # We must first split on \\t to get rid of the descriptions to be 204 + # able to check what the actual completion will be. 205 + # We don't need descriptions anyway since there is only a single 206 + # real completion which the shell will expand immediately. 207 + set -l split (string split --max 1 "\\t" $__${nameForVar}_comp_results[1]) 208 + 209 + # Fish won't add a space if the completion ends with any 210 + # of the following characters: @=/:., 211 + set -l lastChar (string sub -s -1 -- $split) 212 + if not string match -r -q "[@=/:.,]" -- "$lastChar" 213 + # In other cases, to support the "nospace" directive we trick the shell 214 + # by outputting an extra, longer completion. 215 + __${nameForVar}_debug "Adding second completion to perform nospace directive" 216 + set --global __${nameForVar}_comp_results $split[1] $split[1]. 217 + __${nameForVar}_debug "Completions are now: $__${nameForVar}_comp_results" 132 218 end 133 219 end 134 - 135 - for item in $dirs 136 - echo -e "$item\t" 137 - end 138 - return 0 139 - end 140 220 141 - # Process remaining completions 142 - for item in $results 143 - if test -n "$item" 144 - # Check if the item has a description 145 - if string match -q "*\t*" -- "$item" 146 - set -l completion_parts (string split \t -- "$item") 147 - set -l comp $completion_parts[1] 148 - set -l desc $completion_parts[2] 149 - 150 - # Add the completion and description 151 - echo -e "$comp\t$desc" 152 - else 153 - # Add just the completion 154 - echo -e "$item\t" 155 - end 221 + if test $numComps -eq 0; and test $nofiles -eq 0 222 + # To be consistent with bash and zsh, we only trigger file 223 + # completion when there are no other completions 224 + __${nameForVar}_debug "Requesting file completion" 225 + return 1 156 226 end 157 227 end 158 - 159 - # If directive contains NoSpace, tell fish not to add a space after completion 160 - if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0 161 - return 2 162 - end 163 - 228 + 164 229 return 0 165 230 end 166 231 167 - # Set up the completion for the ${name} command 168 - complete -c ${name} -f -a "(eval __${nameForVar}_perform_completion)" 232 + # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 233 + # so we can properly delete any completions provided by another script. 234 + # Only do this if the program can be found, or else fish may print some errors; besides, 235 + # the existing completions will only be loaded if the program can be found. 236 + if type -q "${name}" 237 + # The space after the program name is essential to trigger completion for the program 238 + # and not completion of the program name itself. 239 + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 240 + complete --do-complete "${name} " > /dev/null 2>&1 241 + end 242 + 243 + # Remove any pre-existing completions for the program since we will be handling all of them. 244 + complete -c ${name} -e 245 + 246 + # This will get called after the two calls below and clear the cached result 247 + complete -c ${name} -n '__${nameForVar}_clear_perform_completion_once_result' 248 + # The call to __${nameForVar}_prepare_completions will setup __${nameForVar}_comp_results 249 + # which provides the program's completion choices. 250 + # If this doesn't require order preservation, we don't use the -k flag 251 + complete -c ${name} -n 'not __${nameForVar}_requires_order_preservation && __${nameForVar}_prepare_completions' -f -a '$__${nameForVar}_comp_results' 252 + # Otherwise we use the -k flag 253 + complete -k -c ${name} -n '__${nameForVar}_requires_order_preservation && __${nameForVar}_prepare_completions' -f -a '$__${nameForVar}_comp_results' 169 254 `; 170 255 }
+384 -214
tests/__snapshots__/shell.test.ts.snap
··· 3 3 exports[`shell completion generators > fish shell completion > should generate a valid fish completion script 1`] = ` 4 4 "# fish completion for testcli -*- shell-script -*- 5 5 6 - # Define shell completion directives 7 - set -l ShellCompDirectiveError 1 8 - set -l ShellCompDirectiveNoSpace 2 9 - set -l ShellCompDirectiveNoFileComp 4 10 - set -l ShellCompDirectiveFilterFileExt 8 11 - set -l ShellCompDirectiveFilterDirs 16 12 - set -l ShellCompDirectiveKeepOrder 32 13 - 14 6 function __testcli_debug 15 7 set -l file "$BASH_COMP_DEBUG_FILE" 16 8 if test -n "$file" ··· 21 13 function __testcli_perform_completion 22 14 __testcli_debug "Starting __testcli_perform_completion" 23 15 24 - # Extract all args except the completion flag 25 - set -l args (string match -v -- "--completion=" (commandline -opc)) 26 - 27 - # Extract the current token being completed 28 - set -l current_token (commandline -ct) 29 - 30 - # Check if current token starts with a dash 31 - set -l flag_prefix "" 32 - if string match -q -- "-*" $current_token 33 - set flag_prefix "--flag=" 34 - end 35 - 36 - __testcli_debug "Current token: $current_token" 37 - __testcli_debug "All args: $args" 16 + # Extract all args except the last one 17 + set -l args (commandline -opc) 18 + # Extract the last arg and escape it in case it is a space or wildcard 19 + set -l lastArg (string escape -- (commandline -ct)) 20 + 21 + __testcli_debug "args: $args" 22 + __testcli_debug "last arg: $lastArg" 23 + 24 + # Build the completion request command 25 + set -l requestComp "/usr/bin/node /path/to/testcli complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg" 38 26 39 - # Call the completion program and get the results 40 - set -l requestComp "/usr/bin/node /path/to/testcli complete -- $args" 41 27 __testcli_debug "Calling $requestComp" 42 28 set -l results (eval $requestComp 2> /dev/null) 43 - 29 + 44 30 # Some programs may output extra empty lines after the directive. 45 31 # Let's ignore them or else it will break completion. 46 32 # Ref: https://github.com/spf13/cobra/issues/1279 47 33 for line in $results[-1..1] 48 - if test (string sub -s 1 -l 1 -- $line) = ":" 49 - # The directive 50 - set -l directive (string sub -s 2 -- $line) 51 - set -l directive_num (math $directive) 34 + if test (string trim -- $line) = "" 35 + # Found an empty line, remove it 36 + set results $results[1..-2] 37 + else 38 + # Found non-empty line, we have our proper output 52 39 break 53 40 end 54 41 end 55 - 56 - # No directive specified, use default 57 - if not set -q directive_num 58 - set directive_num 0 42 + 43 + set -l comps $results[1..-2] 44 + set -l directiveLine $results[-1] 45 + 46 + # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>) 47 + # completions must be prefixed with the flag 48 + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 49 + 50 + __testcli_debug "Comps: $comps" 51 + __testcli_debug "DirectiveLine: $directiveLine" 52 + __testcli_debug "flagPrefix: $flagPrefix" 53 + 54 + for comp in $comps 55 + printf "%s%s\\n" "$flagPrefix" "$comp" 59 56 end 60 - 61 - __testcli_debug "Directive: $directive_num" 62 57 63 - # Process completions based on directive 64 - if test $directive_num -eq $ShellCompDirectiveError 65 - # Error code. No completion. 66 - __testcli_debug "Received error directive: aborting." 58 + printf "%s\\n" "$directiveLine" 59 + end 60 + 61 + # This function limits calls to __testcli_perform_completion, by caching the result 62 + function __testcli_perform_completion_once 63 + __testcli_debug "Starting __testcli_perform_completion_once" 64 + 65 + if test -n "$__testcli_perform_completion_once_result" 66 + __testcli_debug "Seems like a valid result already exists, skipping __testcli_perform_completion" 67 + return 0 68 + end 69 + 70 + set --global __testcli_perform_completion_once_result (__testcli_perform_completion) 71 + if test -z "$__testcli_perform_completion_once_result" 72 + __testcli_debug "No completions, probably due to a failure" 67 73 return 1 68 74 end 69 75 70 - # Filter out the directive (last line) 71 - if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":" 72 - set results $results[1..-2] 76 + __testcli_debug "Performed completions and set __testcli_perform_completion_once_result" 77 + return 0 78 + end 79 + 80 + # This function is used to clear the cached result after completions are run 81 + function __testcli_clear_perform_completion_once_result 82 + __testcli_debug "" 83 + __testcli_debug "========= clearing previously set __testcli_perform_completion_once_result variable ==========" 84 + set --erase __testcli_perform_completion_once_result 85 + __testcli_debug "Successfully erased the variable __testcli_perform_completion_once_result" 86 + end 87 + 88 + function __testcli_requires_order_preservation 89 + __testcli_debug "" 90 + __testcli_debug "========= checking if order preservation is required ==========" 91 + 92 + __testcli_perform_completion_once 93 + if test -z "$__testcli_perform_completion_once_result" 94 + __testcli_debug "Error determining if order preservation is required" 95 + return 1 73 96 end 74 97 75 - # No completions, let fish handle file completions unless forbidden 76 - if test (count $results) -eq 0 77 - if test $directive_num -ne $ShellCompDirectiveNoFileComp 78 - __testcli_debug "No completions, performing file completion" 79 - return 1 80 - end 81 - __testcli_debug "No completions, but file completion forbidden" 98 + set -l directive (string sub --start 2 $__testcli_perform_completion_once_result[-1]) 99 + __testcli_debug "Directive is: $directive" 100 + 101 + set -l shellCompDirectiveKeepOrder 32 102 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) 103 + __testcli_debug "Keeporder is: $keeporder" 104 + 105 + if test $keeporder -ne 0 106 + __testcli_debug "This does require order preservation" 82 107 return 0 83 108 end 84 109 85 - # Filter file extensions 86 - if test $directive_num -eq $ShellCompDirectiveFilterFileExt 87 - __testcli_debug "File extension filtering" 88 - set -l file_extensions 89 - for item in $results 90 - if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-" 91 - set -a file_extensions "*$item" 92 - end 93 - end 94 - __testcli_debug "File extensions: $file_extensions" 95 - 96 - # Use the file extensions as completions 97 - set -l completions 98 - for ext in $file_extensions 99 - # Get all files matching the extension 100 - set -a completions (string replace -r '^.*/' '' -- $ext) 101 - end 102 - 103 - for item in $completions 104 - echo -e "$item " 105 - end 106 - return 0 110 + __testcli_debug "This doesn't require order preservation" 111 + return 1 112 + end 113 + 114 + # This function does two things: 115 + # - Obtain the completions and store them in the global __testcli_comp_results 116 + # - Return false if file completion should be performed 117 + function __testcli_prepare_completions 118 + __testcli_debug "" 119 + __testcli_debug "========= starting completion logic ==========" 120 + 121 + # Start fresh 122 + set --erase __testcli_comp_results 123 + 124 + __testcli_perform_completion_once 125 + __testcli_debug "Completion results: $__testcli_perform_completion_once_result" 126 + 127 + if test -z "$__testcli_perform_completion_once_result" 128 + __testcli_debug "No completion, probably due to a failure" 129 + # Might as well do file completion, in case it helps 130 + return 1 131 + end 132 + 133 + set -l directive (string sub --start 2 $__testcli_perform_completion_once_result[-1]) 134 + set --global __testcli_comp_results $__testcli_perform_completion_once_result[1..-2] 135 + 136 + __testcli_debug "Completions are: $__testcli_comp_results" 137 + __testcli_debug "Directive is: $directive" 138 + 139 + set -l shellCompDirectiveError 1 140 + set -l shellCompDirectiveNoSpace 2 141 + set -l shellCompDirectiveNoFileComp 4 142 + set -l shellCompDirectiveFilterFileExt 8 143 + set -l shellCompDirectiveFilterDirs 16 144 + 145 + if test -z "$directive" 146 + set directive 0 147 + end 148 + 149 + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) 150 + if test $compErr -eq 1 151 + __testcli_debug "Received error directive: aborting." 152 + # Might as well do file completion, in case it helps 153 + return 1 154 + end 155 + 156 + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) 157 + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) 158 + if test $filefilter -eq 1; or test $dirfilter -eq 1 159 + __testcli_debug "File extension filtering or directory filtering not supported" 160 + # Do full file completion instead 161 + return 1 107 162 end 108 163 109 - # Filter directories 110 - if test $directive_num -eq $ShellCompDirectiveFilterDirs 111 - __testcli_debug "Directory filtering" 112 - set -l dirs 113 - for item in $results 114 - if test -d "$item" 115 - set -a dirs "$item/" 164 + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) 165 + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) 166 + 167 + __testcli_debug "nospace: $nospace, nofiles: $nofiles" 168 + 169 + # If we want to prevent a space, or if file completion is NOT disabled, 170 + # we need to count the number of valid completions. 171 + # To do so, we will filter on prefix as the completions we have received 172 + # may not already be filtered so as to allow fish to match on different 173 + # criteria than the prefix. 174 + if test $nospace -ne 0; or test $nofiles -eq 0 175 + set -l prefix (commandline -t | string escape --style=regex) 176 + __testcli_debug "prefix: $prefix" 177 + 178 + set -l completions (string match -r -- "^$prefix.*" $__testcli_comp_results) 179 + set --global __testcli_comp_results $completions 180 + __testcli_debug "Filtered completions are: $__testcli_comp_results" 181 + 182 + # Important not to quote the variable for count to work 183 + set -l numComps (count $__testcli_comp_results) 184 + __testcli_debug "numComps: $numComps" 185 + 186 + if test $numComps -eq 1; and test $nospace -ne 0 187 + # We must first split on \\t to get rid of the descriptions to be 188 + # able to check what the actual completion will be. 189 + # We don't need descriptions anyway since there is only a single 190 + # real completion which the shell will expand immediately. 191 + set -l split (string split --max 1 "\\t" $__testcli_comp_results[1]) 192 + 193 + # Fish won't add a space if the completion ends with any 194 + # of the following characters: @=/:., 195 + set -l lastChar (string sub -s -1 -- $split) 196 + if not string match -r -q "[@=/:.,]" -- "$lastChar" 197 + # In other cases, to support the "nospace" directive we trick the shell 198 + # by outputting an extra, longer completion. 199 + __testcli_debug "Adding second completion to perform nospace directive" 200 + set --global __testcli_comp_results $split[1] $split[1]. 201 + __testcli_debug "Completions are now: $__testcli_comp_results" 116 202 end 117 203 end 118 - 119 - for item in $dirs 120 - echo -e "$item " 121 - end 122 - return 0 123 - end 124 204 125 - # Process remaining completions 126 - for item in $results 127 - if test -n "$item" 128 - # Check if the item has a description 129 - if string match -q "* *" -- "$item" 130 - set -l completion_parts (string split -- "$item") 131 - set -l comp $completion_parts[1] 132 - set -l desc $completion_parts[2] 133 - 134 - # Add the completion and description 135 - echo -e "$comp $desc" 136 - else 137 - # Add just the completion 138 - echo -e "$item " 139 - end 205 + if test $numComps -eq 0; and test $nofiles -eq 0 206 + # To be consistent with bash and zsh, we only trigger file 207 + # completion when there are no other completions 208 + __testcli_debug "Requesting file completion" 209 + return 1 140 210 end 141 211 end 142 - 143 - # If directive contains NoSpace, tell fish not to add a space after completion 144 - if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0 145 - return 2 146 - end 147 - 212 + 148 213 return 0 149 214 end 150 215 151 - # Set up the completion for the testcli command 152 - complete -c testcli -f -a "(eval __testcli_perform_completion)" 216 + # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 217 + # so we can properly delete any completions provided by another script. 218 + # Only do this if the program can be found, or else fish may print some errors; besides, 219 + # the existing completions will only be loaded if the program can be found. 220 + if type -q "testcli" 221 + # The space after the program name is essential to trigger completion for the program 222 + # and not completion of the program name itself. 223 + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 224 + complete --do-complete "testcli " > /dev/null 2>&1 225 + end 226 + 227 + # Remove any pre-existing completions for the program since we will be handling all of them. 228 + complete -c testcli -e 229 + 230 + # This will get called after the two calls below and clear the cached result 231 + complete -c testcli -n '__testcli_clear_perform_completion_once_result' 232 + # The call to __testcli_prepare_completions will setup __testcli_comp_results 233 + # which provides the program's completion choices. 234 + # If this doesn't require order preservation, we don't use the -k flag 235 + complete -c testcli -n 'not __testcli_requires_order_preservation && __testcli_prepare_completions' -f -a '$__testcli_comp_results' 236 + # Otherwise we use the -k flag 237 + complete -k -c testcli -n '__testcli_requires_order_preservation && __testcli_prepare_completions' -f -a '$__testcli_comp_results' 153 238 " 154 239 `; 155 240 156 241 exports[`shell completion generators > fish shell completion > should handle special characters in the name 1`] = ` 157 242 "# fish completion for test-cli:app -*- shell-script -*- 158 243 159 - # Define shell completion directives 160 - set -l ShellCompDirectiveError 1 161 - set -l ShellCompDirectiveNoSpace 2 162 - set -l ShellCompDirectiveNoFileComp 4 163 - set -l ShellCompDirectiveFilterFileExt 8 164 - set -l ShellCompDirectiveFilterDirs 16 165 - set -l ShellCompDirectiveKeepOrder 32 166 - 167 244 function __test_cli_app_debug 168 245 set -l file "$BASH_COMP_DEBUG_FILE" 169 246 if test -n "$file" ··· 174 251 function __test_cli_app_perform_completion 175 252 __test_cli_app_debug "Starting __test_cli_app_perform_completion" 176 253 177 - # Extract all args except the completion flag 178 - set -l args (string match -v -- "--completion=" (commandline -opc)) 179 - 180 - # Extract the current token being completed 181 - set -l current_token (commandline -ct) 182 - 183 - # Check if current token starts with a dash 184 - set -l flag_prefix "" 185 - if string match -q -- "-*" $current_token 186 - set flag_prefix "--flag=" 187 - end 188 - 189 - __test_cli_app_debug "Current token: $current_token" 190 - __test_cli_app_debug "All args: $args" 254 + # Extract all args except the last one 255 + set -l args (commandline -opc) 256 + # Extract the last arg and escape it in case it is a space or wildcard 257 + set -l lastArg (string escape -- (commandline -ct)) 258 + 259 + __test_cli_app_debug "args: $args" 260 + __test_cli_app_debug "last arg: $lastArg" 261 + 262 + # Build the completion request command 263 + set -l requestComp "/usr/bin/node /path/to/testcli complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg" 191 264 192 - # Call the completion program and get the results 193 - set -l requestComp "/usr/bin/node /path/to/testcli complete -- $args" 194 265 __test_cli_app_debug "Calling $requestComp" 195 266 set -l results (eval $requestComp 2> /dev/null) 196 - 267 + 197 268 # Some programs may output extra empty lines after the directive. 198 269 # Let's ignore them or else it will break completion. 199 270 # Ref: https://github.com/spf13/cobra/issues/1279 200 271 for line in $results[-1..1] 201 - if test (string sub -s 1 -l 1 -- $line) = ":" 202 - # The directive 203 - set -l directive (string sub -s 2 -- $line) 204 - set -l directive_num (math $directive) 272 + if test (string trim -- $line) = "" 273 + # Found an empty line, remove it 274 + set results $results[1..-2] 275 + else 276 + # Found non-empty line, we have our proper output 205 277 break 206 278 end 207 279 end 208 - 209 - # No directive specified, use default 210 - if not set -q directive_num 211 - set directive_num 0 280 + 281 + set -l comps $results[1..-2] 282 + set -l directiveLine $results[-1] 283 + 284 + # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>) 285 + # completions must be prefixed with the flag 286 + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 287 + 288 + __test_cli_app_debug "Comps: $comps" 289 + __test_cli_app_debug "DirectiveLine: $directiveLine" 290 + __test_cli_app_debug "flagPrefix: $flagPrefix" 291 + 292 + for comp in $comps 293 + printf "%s%s\\n" "$flagPrefix" "$comp" 212 294 end 213 - 214 - __test_cli_app_debug "Directive: $directive_num" 215 295 216 - # Process completions based on directive 217 - if test $directive_num -eq $ShellCompDirectiveError 218 - # Error code. No completion. 219 - __test_cli_app_debug "Received error directive: aborting." 296 + printf "%s\\n" "$directiveLine" 297 + end 298 + 299 + # This function limits calls to __test_cli_app_perform_completion, by caching the result 300 + function __test_cli_app_perform_completion_once 301 + __test_cli_app_debug "Starting __test_cli_app_perform_completion_once" 302 + 303 + if test -n "$__test_cli_app_perform_completion_once_result" 304 + __test_cli_app_debug "Seems like a valid result already exists, skipping __test_cli_app_perform_completion" 305 + return 0 306 + end 307 + 308 + set --global __test_cli_app_perform_completion_once_result (__test_cli_app_perform_completion) 309 + if test -z "$__test_cli_app_perform_completion_once_result" 310 + __test_cli_app_debug "No completions, probably due to a failure" 220 311 return 1 221 312 end 222 313 223 - # Filter out the directive (last line) 224 - if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":" 225 - set results $results[1..-2] 314 + __test_cli_app_debug "Performed completions and set __test_cli_app_perform_completion_once_result" 315 + return 0 316 + end 317 + 318 + # This function is used to clear the cached result after completions are run 319 + function __test_cli_app_clear_perform_completion_once_result 320 + __test_cli_app_debug "" 321 + __test_cli_app_debug "========= clearing previously set __test_cli_app_perform_completion_once_result variable ==========" 322 + set --erase __test_cli_app_perform_completion_once_result 323 + __test_cli_app_debug "Successfully erased the variable __test_cli_app_perform_completion_once_result" 324 + end 325 + 326 + function __test_cli_app_requires_order_preservation 327 + __test_cli_app_debug "" 328 + __test_cli_app_debug "========= checking if order preservation is required ==========" 329 + 330 + __test_cli_app_perform_completion_once 331 + if test -z "$__test_cli_app_perform_completion_once_result" 332 + __test_cli_app_debug "Error determining if order preservation is required" 333 + return 1 226 334 end 227 335 228 - # No completions, let fish handle file completions unless forbidden 229 - if test (count $results) -eq 0 230 - if test $directive_num -ne $ShellCompDirectiveNoFileComp 231 - __test_cli_app_debug "No completions, performing file completion" 232 - return 1 233 - end 234 - __test_cli_app_debug "No completions, but file completion forbidden" 336 + set -l directive (string sub --start 2 $__test_cli_app_perform_completion_once_result[-1]) 337 + __test_cli_app_debug "Directive is: $directive" 338 + 339 + set -l shellCompDirectiveKeepOrder 32 340 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) 341 + __test_cli_app_debug "Keeporder is: $keeporder" 342 + 343 + if test $keeporder -ne 0 344 + __test_cli_app_debug "This does require order preservation" 235 345 return 0 236 346 end 237 347 238 - # Filter file extensions 239 - if test $directive_num -eq $ShellCompDirectiveFilterFileExt 240 - __test_cli_app_debug "File extension filtering" 241 - set -l file_extensions 242 - for item in $results 243 - if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-" 244 - set -a file_extensions "*$item" 245 - end 246 - end 247 - __test_cli_app_debug "File extensions: $file_extensions" 248 - 249 - # Use the file extensions as completions 250 - set -l completions 251 - for ext in $file_extensions 252 - # Get all files matching the extension 253 - set -a completions (string replace -r '^.*/' '' -- $ext) 254 - end 255 - 256 - for item in $completions 257 - echo -e "$item " 258 - end 259 - return 0 348 + __test_cli_app_debug "This doesn't require order preservation" 349 + return 1 350 + end 351 + 352 + # This function does two things: 353 + # - Obtain the completions and store them in the global __test_cli_app_comp_results 354 + # - Return false if file completion should be performed 355 + function __test_cli_app_prepare_completions 356 + __test_cli_app_debug "" 357 + __test_cli_app_debug "========= starting completion logic ==========" 358 + 359 + # Start fresh 360 + set --erase __test_cli_app_comp_results 361 + 362 + __test_cli_app_perform_completion_once 363 + __test_cli_app_debug "Completion results: $__test_cli_app_perform_completion_once_result" 364 + 365 + if test -z "$__test_cli_app_perform_completion_once_result" 366 + __test_cli_app_debug "No completion, probably due to a failure" 367 + # Might as well do file completion, in case it helps 368 + return 1 369 + end 370 + 371 + set -l directive (string sub --start 2 $__test_cli_app_perform_completion_once_result[-1]) 372 + set --global __test_cli_app_comp_results $__test_cli_app_perform_completion_once_result[1..-2] 373 + 374 + __test_cli_app_debug "Completions are: $__test_cli_app_comp_results" 375 + __test_cli_app_debug "Directive is: $directive" 376 + 377 + set -l shellCompDirectiveError 1 378 + set -l shellCompDirectiveNoSpace 2 379 + set -l shellCompDirectiveNoFileComp 4 380 + set -l shellCompDirectiveFilterFileExt 8 381 + set -l shellCompDirectiveFilterDirs 16 382 + 383 + if test -z "$directive" 384 + set directive 0 385 + end 386 + 387 + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) 388 + if test $compErr -eq 1 389 + __test_cli_app_debug "Received error directive: aborting." 390 + # Might as well do file completion, in case it helps 391 + return 1 392 + end 393 + 394 + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) 395 + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) 396 + if test $filefilter -eq 1; or test $dirfilter -eq 1 397 + __test_cli_app_debug "File extension filtering or directory filtering not supported" 398 + # Do full file completion instead 399 + return 1 260 400 end 261 401 262 - # Filter directories 263 - if test $directive_num -eq $ShellCompDirectiveFilterDirs 264 - __test_cli_app_debug "Directory filtering" 265 - set -l dirs 266 - for item in $results 267 - if test -d "$item" 268 - set -a dirs "$item/" 402 + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) 403 + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) 404 + 405 + __test_cli_app_debug "nospace: $nospace, nofiles: $nofiles" 406 + 407 + # If we want to prevent a space, or if file completion is NOT disabled, 408 + # we need to count the number of valid completions. 409 + # To do so, we will filter on prefix as the completions we have received 410 + # may not already be filtered so as to allow fish to match on different 411 + # criteria than the prefix. 412 + if test $nospace -ne 0; or test $nofiles -eq 0 413 + set -l prefix (commandline -t | string escape --style=regex) 414 + __test_cli_app_debug "prefix: $prefix" 415 + 416 + set -l completions (string match -r -- "^$prefix.*" $__test_cli_app_comp_results) 417 + set --global __test_cli_app_comp_results $completions 418 + __test_cli_app_debug "Filtered completions are: $__test_cli_app_comp_results" 419 + 420 + # Important not to quote the variable for count to work 421 + set -l numComps (count $__test_cli_app_comp_results) 422 + __test_cli_app_debug "numComps: $numComps" 423 + 424 + if test $numComps -eq 1; and test $nospace -ne 0 425 + # We must first split on \\t to get rid of the descriptions to be 426 + # able to check what the actual completion will be. 427 + # We don't need descriptions anyway since there is only a single 428 + # real completion which the shell will expand immediately. 429 + set -l split (string split --max 1 "\\t" $__test_cli_app_comp_results[1]) 430 + 431 + # Fish won't add a space if the completion ends with any 432 + # of the following characters: @=/:., 433 + set -l lastChar (string sub -s -1 -- $split) 434 + if not string match -r -q "[@=/:.,]" -- "$lastChar" 435 + # In other cases, to support the "nospace" directive we trick the shell 436 + # by outputting an extra, longer completion. 437 + __test_cli_app_debug "Adding second completion to perform nospace directive" 438 + set --global __test_cli_app_comp_results $split[1] $split[1]. 439 + __test_cli_app_debug "Completions are now: $__test_cli_app_comp_results" 269 440 end 270 441 end 271 - 272 - for item in $dirs 273 - echo -e "$item " 274 - end 275 - return 0 276 - end 277 442 278 - # Process remaining completions 279 - for item in $results 280 - if test -n "$item" 281 - # Check if the item has a description 282 - if string match -q "* *" -- "$item" 283 - set -l completion_parts (string split -- "$item") 284 - set -l comp $completion_parts[1] 285 - set -l desc $completion_parts[2] 286 - 287 - # Add the completion and description 288 - echo -e "$comp $desc" 289 - else 290 - # Add just the completion 291 - echo -e "$item " 292 - end 443 + if test $numComps -eq 0; and test $nofiles -eq 0 444 + # To be consistent with bash and zsh, we only trigger file 445 + # completion when there are no other completions 446 + __test_cli_app_debug "Requesting file completion" 447 + return 1 293 448 end 294 449 end 295 - 296 - # If directive contains NoSpace, tell fish not to add a space after completion 297 - if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0 298 - return 2 299 - end 300 - 450 + 301 451 return 0 302 452 end 303 453 304 - # Set up the completion for the test-cli:app command 305 - complete -c test-cli:app -f -a "(eval __test_cli_app_perform_completion)" 454 + # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 455 + # so we can properly delete any completions provided by another script. 456 + # Only do this if the program can be found, or else fish may print some errors; besides, 457 + # the existing completions will only be loaded if the program can be found. 458 + if type -q "test-cli:app" 459 + # The space after the program name is essential to trigger completion for the program 460 + # and not completion of the program name itself. 461 + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 462 + complete --do-complete "test-cli:app " > /dev/null 2>&1 463 + end 464 + 465 + # Remove any pre-existing completions for the program since we will be handling all of them. 466 + complete -c test-cli:app -e 467 + 468 + # This will get called after the two calls below and clear the cached result 469 + complete -c test-cli:app -n '__test_cli_app_clear_perform_completion_once_result' 470 + # The call to __test_cli_app_prepare_completions will setup __test_cli_app_comp_results 471 + # which provides the program's completion choices. 472 + # If this doesn't require order preservation, we don't use the -k flag 473 + complete -c test-cli:app -n 'not __test_cli_app_requires_order_preservation && __test_cli_app_prepare_completions' -f -a '$__test_cli_app_comp_results' 474 + # Otherwise we use the -k flag 475 + complete -k -c test-cli:app -n '__test_cli_app_requires_order_preservation && __test_cli_app_prepare_completions' -f -a '$__test_cli_app_comp_results' 306 476 " 307 477 `;
+129
tests/fish-integration.test.ts
··· 1 + import { describe, it, expect, beforeAll } from 'vitest'; 2 + import { execSync, spawnSync } from 'child_process'; 3 + import * as path from 'path'; 4 + 5 + // Check if fish is available 6 + function isFishAvailable(): boolean { 7 + try { 8 + execSync('fish --version', { stdio: 'pipe' }); 9 + return true; 10 + } catch { 11 + return false; 12 + } 13 + } 14 + 15 + // Run fish command and return output 16 + function runFish(script: string): string { 17 + const result = spawnSync('fish', ['-c', script], { 18 + encoding: 'utf-8', 19 + stdio: ['pipe', 'pipe', 'pipe'], 20 + }); 21 + if (result.error) { 22 + throw result.error; 23 + } 24 + return result.stdout + result.stderr; 25 + } 26 + 27 + // Simulate TAB completion in fish 28 + function simulateTab(completionScript: string, commandLine: string): string[] { 29 + const script = ` 30 + source (echo '${completionScript.replace(/'/g, "\\'")}' | psub) 31 + complete --do-complete "${commandLine}" 32 + `; 33 + const output = runFish(script); 34 + return output 35 + .split('\n') 36 + .filter((line) => line.trim() !== '') 37 + .map((line) => line.trim()); 38 + } 39 + 40 + describe.skipIf(!isFishAvailable())( 41 + 'fish shell completion integration tests', 42 + () => { 43 + const demoCliPath = path.join( 44 + __dirname, 45 + '../examples/demo-cli-cac/demo-cli-cac.js' 46 + ); 47 + let completionScript: string; 48 + 49 + beforeAll(() => { 50 + // Generate the completion script from demo-cli-cac 51 + const result = spawnSync('node', [demoCliPath, 'complete', 'fish'], { 52 + encoding: 'utf-8', 53 + cwd: path.dirname(demoCliPath), 54 + }); 55 + completionScript = result.stdout; 56 + }); 57 + 58 + it('should complete subcommands when pressing TAB after command', () => { 59 + const completions = simulateTab(completionScript, 'demo-cli-cac '); 60 + 61 + // Should contain subcommands 62 + expect(completions.some((c) => c.startsWith('start'))).toBe(true); 63 + expect(completions.some((c) => c.startsWith('build'))).toBe(true); 64 + }); 65 + 66 + it('should complete flags when pressing TAB after --', () => { 67 + const completions = simulateTab(completionScript, 'demo-cli-cac --'); 68 + 69 + // Should contain global flags 70 + expect(completions.some((c) => c.includes('--config'))).toBe(true); 71 + expect(completions.some((c) => c.includes('--debug'))).toBe(true); 72 + expect(completions.some((c) => c.includes('--help'))).toBe(true); 73 + expect(completions.some((c) => c.includes('--version'))).toBe(true); 74 + }); 75 + 76 + it('should complete subcommand-specific flags', () => { 77 + const completions = simulateTab( 78 + completionScript, 79 + 'demo-cli-cac start --' 80 + ); 81 + 82 + // Should contain start-specific flag 83 + expect(completions.some((c) => c.includes('--port'))).toBe(true); 84 + // Should also contain global flags 85 + expect(completions.some((c) => c.includes('--config'))).toBe(true); 86 + }); 87 + 88 + it('should complete build command flags', () => { 89 + const completions = simulateTab( 90 + completionScript, 91 + 'demo-cli-cac build --' 92 + ); 93 + 94 + // Should contain build-specific flag 95 + expect(completions.some((c) => c.includes('--mode'))).toBe(true); 96 + // Should also contain global flags 97 + expect(completions.some((c) => c.includes('--config'))).toBe(true); 98 + }); 99 + 100 + it('should show descriptions with completions', () => { 101 + const completions = simulateTab(completionScript, 'demo-cli-cac '); 102 + 103 + // Check that descriptions are included (tab-separated) 104 + const startCompletion = completions.find((c) => c.startsWith('start')); 105 + expect(startCompletion).toContain('Start the application'); 106 + 107 + const buildCompletion = completions.find((c) => c.startsWith('build')); 108 + expect(buildCompletion).toContain('Build the application'); 109 + }); 110 + 111 + it('should filter completions based on partial input', () => { 112 + const completions = simulateTab(completionScript, 'demo-cli-cac st'); 113 + 114 + // Should only show completions starting with 'st' 115 + expect(completions.some((c) => c.startsWith('start'))).toBe(true); 116 + // 'build' should not appear 117 + expect(completions.some((c) => c.startsWith('build'))).toBe(false); 118 + }); 119 + 120 + it('should filter flag completions based on partial input', () => { 121 + const completions = simulateTab(completionScript, 'demo-cli-cac --c'); 122 + 123 + // Should show --config 124 + expect(completions.some((c) => c.includes('--config'))).toBe(true); 125 + // Should not show --debug (doesn't start with --c) 126 + expect(completions.some((c) => c.includes('--debug'))).toBe(false); 127 + }); 128 + } 129 + );