···11+---
22+'@bomb.sh/tab': patch
33+---
44+55+Update fish shell completion script to match latest Cobra output. (#99)
+192-107
src/fish.ts
···19192020 return `# fish completion for ${name} -*- shell-script -*-
21212222-# Define shell completion directives
2323-set -l ShellCompDirectiveError ${ShellCompDirectiveError}
2424-set -l ShellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace}
2525-set -l ShellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp}
2626-set -l ShellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt}
2727-set -l ShellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs}
2828-set -l ShellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder}
2929-3022function __${nameForVar}_debug
3123 set -l file "$BASH_COMP_DEBUG_FILE"
3224 if test -n "$file"
···3729function __${nameForVar}_perform_completion
3830 __${nameForVar}_debug "Starting __${nameForVar}_perform_completion"
39314040- # Extract all args except the completion flag
4141- set -l args (string match -v -- "--completion=" (commandline -opc))
4242-4343- # Extract the current token being completed
4444- set -l current_token (commandline -ct)
4545-4646- # Check if current token starts with a dash
4747- set -l flag_prefix ""
4848- if string match -q -- "-*" $current_token
4949- set flag_prefix "--flag="
5050- end
5151-5252- __${nameForVar}_debug "Current token: $current_token"
5353- __${nameForVar}_debug "All args: $args"
3232+ # Extract all args except the last one
3333+ set -l args (commandline -opc)
3434+ # Extract the last arg and escape it in case it is a space or wildcard
3535+ set -l lastArg (string escape -- (commandline -ct))
3636+3737+ __${nameForVar}_debug "args: $args"
3838+ __${nameForVar}_debug "last arg: $lastArg"
3939+4040+ # Build the completion request command
4141+ set -l requestComp "${exec} complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg"
54425555- # Call the completion program and get the results
5656- set -l requestComp "${exec} complete -- $args"
5743 __${nameForVar}_debug "Calling $requestComp"
5844 set -l results (eval $requestComp 2> /dev/null)
5959-4545+6046 # Some programs may output extra empty lines after the directive.
6147 # Let's ignore them or else it will break completion.
6248 # Ref: https://github.com/spf13/cobra/issues/1279
6349 for line in $results[-1..1]
6464- if test (string sub -s 1 -l 1 -- $line) = ":"
6565- # The directive
6666- set -l directive (string sub -s 2 -- $line)
6767- set -l directive_num (math $directive)
5050+ if test (string trim -- $line) = ""
5151+ # Found an empty line, remove it
5252+ set results $results[1..-2]
5353+ else
5454+ # Found non-empty line, we have our proper output
6855 break
6956 end
7057 end
7171-7272- # No directive specified, use default
7373- if not set -q directive_num
7474- set directive_num 0
5858+5959+ set -l comps $results[1..-2]
6060+ set -l directiveLine $results[-1]
6161+6262+ # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
6363+ # completions must be prefixed with the flag
6464+ set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
6565+6666+ __${nameForVar}_debug "Comps: $comps"
6767+ __${nameForVar}_debug "DirectiveLine: $directiveLine"
6868+ __${nameForVar}_debug "flagPrefix: $flagPrefix"
6969+7070+ for comp in $comps
7171+ printf "%s%s\\n" "$flagPrefix" "$comp"
7572 end
7676-7777- __${nameForVar}_debug "Directive: $directive_num"
78737979- # Process completions based on directive
8080- if test $directive_num -eq $ShellCompDirectiveError
8181- # Error code. No completion.
8282- __${nameForVar}_debug "Received error directive: aborting."
7474+ printf "%s\\n" "$directiveLine"
7575+end
7676+7777+# This function limits calls to __${nameForVar}_perform_completion, by caching the result
7878+function __${nameForVar}_perform_completion_once
7979+ __${nameForVar}_debug "Starting __${nameForVar}_perform_completion_once"
8080+8181+ if test -n "$__${nameForVar}_perform_completion_once_result"
8282+ __${nameForVar}_debug "Seems like a valid result already exists, skipping __${nameForVar}_perform_completion"
8383+ return 0
8484+ end
8585+8686+ set --global __${nameForVar}_perform_completion_once_result (__${nameForVar}_perform_completion)
8787+ if test -z "$__${nameForVar}_perform_completion_once_result"
8888+ __${nameForVar}_debug "No completions, probably due to a failure"
8389 return 1
8490 end
85918686- # Filter out the directive (last line)
8787- if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":"
8888- set results $results[1..-2]
9292+ __${nameForVar}_debug "Performed completions and set __${nameForVar}_perform_completion_once_result"
9393+ return 0
9494+end
9595+9696+# This function is used to clear the cached result after completions are run
9797+function __${nameForVar}_clear_perform_completion_once_result
9898+ __${nameForVar}_debug ""
9999+ __${nameForVar}_debug "========= clearing previously set __${nameForVar}_perform_completion_once_result variable =========="
100100+ set --erase __${nameForVar}_perform_completion_once_result
101101+ __${nameForVar}_debug "Successfully erased the variable __${nameForVar}_perform_completion_once_result"
102102+end
103103+104104+function __${nameForVar}_requires_order_preservation
105105+ __${nameForVar}_debug ""
106106+ __${nameForVar}_debug "========= checking if order preservation is required =========="
107107+108108+ __${nameForVar}_perform_completion_once
109109+ if test -z "$__${nameForVar}_perform_completion_once_result"
110110+ __${nameForVar}_debug "Error determining if order preservation is required"
111111+ return 1
89112 end
901139191- # No completions, let fish handle file completions unless forbidden
9292- if test (count $results) -eq 0
9393- if test $directive_num -ne $ShellCompDirectiveNoFileComp
9494- __${nameForVar}_debug "No completions, performing file completion"
9595- return 1
9696- end
9797- __${nameForVar}_debug "No completions, but file completion forbidden"
114114+ set -l directive (string sub --start 2 $__${nameForVar}_perform_completion_once_result[-1])
115115+ __${nameForVar}_debug "Directive is: $directive"
116116+117117+ set -l shellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder}
118118+ set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2)
119119+ __${nameForVar}_debug "Keeporder is: $keeporder"
120120+121121+ if test $keeporder -ne 0
122122+ __${nameForVar}_debug "This does require order preservation"
98123 return 0
99124 end
100125101101- # Filter file extensions
102102- if test $directive_num -eq $ShellCompDirectiveFilterFileExt
103103- __${nameForVar}_debug "File extension filtering"
104104- set -l file_extensions
105105- for item in $results
106106- if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-"
107107- set -a file_extensions "*$item"
108108- end
109109- end
110110- __${nameForVar}_debug "File extensions: $file_extensions"
111111-112112- # Use the file extensions as completions
113113- set -l completions
114114- for ext in $file_extensions
115115- # Get all files matching the extension
116116- set -a completions (string replace -r '^.*/' '' -- $ext)
117117- end
118118-119119- for item in $completions
120120- echo -e "$item\t"
121121- end
122122- return 0
126126+ __${nameForVar}_debug "This doesn't require order preservation"
127127+ return 1
128128+end
129129+130130+# This function does two things:
131131+# - Obtain the completions and store them in the global __${nameForVar}_comp_results
132132+# - Return false if file completion should be performed
133133+function __${nameForVar}_prepare_completions
134134+ __${nameForVar}_debug ""
135135+ __${nameForVar}_debug "========= starting completion logic =========="
136136+137137+ # Start fresh
138138+ set --erase __${nameForVar}_comp_results
139139+140140+ __${nameForVar}_perform_completion_once
141141+ __${nameForVar}_debug "Completion results: $__${nameForVar}_perform_completion_once_result"
142142+143143+ if test -z "$__${nameForVar}_perform_completion_once_result"
144144+ __${nameForVar}_debug "No completion, probably due to a failure"
145145+ # Might as well do file completion, in case it helps
146146+ return 1
147147+ end
148148+149149+ set -l directive (string sub --start 2 $__${nameForVar}_perform_completion_once_result[-1])
150150+ set --global __${nameForVar}_comp_results $__${nameForVar}_perform_completion_once_result[1..-2]
151151+152152+ __${nameForVar}_debug "Completions are: $__${nameForVar}_comp_results"
153153+ __${nameForVar}_debug "Directive is: $directive"
154154+155155+ set -l shellCompDirectiveError ${ShellCompDirectiveError}
156156+ set -l shellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace}
157157+ set -l shellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp}
158158+ set -l shellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt}
159159+ set -l shellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs}
160160+161161+ if test -z "$directive"
162162+ set directive 0
163163+ end
164164+165165+ set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2)
166166+ if test $compErr -eq 1
167167+ __${nameForVar}_debug "Received error directive: aborting."
168168+ # Might as well do file completion, in case it helps
169169+ return 1
170170+ end
171171+172172+ set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2)
173173+ set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)
174174+ if test $filefilter -eq 1; or test $dirfilter -eq 1
175175+ __${nameForVar}_debug "File extension filtering or directory filtering not supported"
176176+ # Do full file completion instead
177177+ return 1
123178 end
124179125125- # Filter directories
126126- if test $directive_num -eq $ShellCompDirectiveFilterDirs
127127- __${nameForVar}_debug "Directory filtering"
128128- set -l dirs
129129- for item in $results
130130- if test -d "$item"
131131- set -a dirs "$item/"
180180+ set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2)
181181+ set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2)
182182+183183+ __${nameForVar}_debug "nospace: $nospace, nofiles: $nofiles"
184184+185185+ # If we want to prevent a space, or if file completion is NOT disabled,
186186+ # we need to count the number of valid completions.
187187+ # To do so, we will filter on prefix as the completions we have received
188188+ # may not already be filtered so as to allow fish to match on different
189189+ # criteria than the prefix.
190190+ if test $nospace -ne 0; or test $nofiles -eq 0
191191+ set -l prefix (commandline -t | string escape --style=regex)
192192+ __${nameForVar}_debug "prefix: $prefix"
193193+194194+ set -l completions (string match -r -- "^$prefix.*" $__${nameForVar}_comp_results)
195195+ set --global __${nameForVar}_comp_results $completions
196196+ __${nameForVar}_debug "Filtered completions are: $__${nameForVar}_comp_results"
197197+198198+ # Important not to quote the variable for count to work
199199+ set -l numComps (count $__${nameForVar}_comp_results)
200200+ __${nameForVar}_debug "numComps: $numComps"
201201+202202+ if test $numComps -eq 1; and test $nospace -ne 0
203203+ # We must first split on \\t to get rid of the descriptions to be
204204+ # able to check what the actual completion will be.
205205+ # We don't need descriptions anyway since there is only a single
206206+ # real completion which the shell will expand immediately.
207207+ set -l split (string split --max 1 "\\t" $__${nameForVar}_comp_results[1])
208208+209209+ # Fish won't add a space if the completion ends with any
210210+ # of the following characters: @=/:.,
211211+ set -l lastChar (string sub -s -1 -- $split)
212212+ if not string match -r -q "[@=/:.,]" -- "$lastChar"
213213+ # In other cases, to support the "nospace" directive we trick the shell
214214+ # by outputting an extra, longer completion.
215215+ __${nameForVar}_debug "Adding second completion to perform nospace directive"
216216+ set --global __${nameForVar}_comp_results $split[1] $split[1].
217217+ __${nameForVar}_debug "Completions are now: $__${nameForVar}_comp_results"
132218 end
133219 end
134134-135135- for item in $dirs
136136- echo -e "$item\t"
137137- end
138138- return 0
139139- end
140220141141- # Process remaining completions
142142- for item in $results
143143- if test -n "$item"
144144- # Check if the item has a description
145145- if string match -q "*\t*" -- "$item"
146146- set -l completion_parts (string split \t -- "$item")
147147- set -l comp $completion_parts[1]
148148- set -l desc $completion_parts[2]
149149-150150- # Add the completion and description
151151- echo -e "$comp\t$desc"
152152- else
153153- # Add just the completion
154154- echo -e "$item\t"
155155- end
221221+ if test $numComps -eq 0; and test $nofiles -eq 0
222222+ # To be consistent with bash and zsh, we only trigger file
223223+ # completion when there are no other completions
224224+ __${nameForVar}_debug "Requesting file completion"
225225+ return 1
156226 end
157227 end
158158-159159- # If directive contains NoSpace, tell fish not to add a space after completion
160160- if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0
161161- return 2
162162- end
163163-228228+164229 return 0
165230end
166231167167-# Set up the completion for the ${name} command
168168-complete -c ${name} -f -a "(eval __${nameForVar}_perform_completion)"
232232+# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
233233+# so we can properly delete any completions provided by another script.
234234+# Only do this if the program can be found, or else fish may print some errors; besides,
235235+# the existing completions will only be loaded if the program can be found.
236236+if type -q "${name}"
237237+ # The space after the program name is essential to trigger completion for the program
238238+ # and not completion of the program name itself.
239239+ # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
240240+ complete --do-complete "${name} " > /dev/null 2>&1
241241+end
242242+243243+# Remove any pre-existing completions for the program since we will be handling all of them.
244244+complete -c ${name} -e
245245+246246+# This will get called after the two calls below and clear the cached result
247247+complete -c ${name} -n '__${nameForVar}_clear_perform_completion_once_result'
248248+# The call to __${nameForVar}_prepare_completions will setup __${nameForVar}_comp_results
249249+# which provides the program's completion choices.
250250+# If this doesn't require order preservation, we don't use the -k flag
251251+complete -c ${name} -n 'not __${nameForVar}_requires_order_preservation && __${nameForVar}_prepare_completions' -f -a '$__${nameForVar}_comp_results'
252252+# Otherwise we use the -k flag
253253+complete -k -c ${name} -n '__${nameForVar}_requires_order_preservation && __${nameForVar}_prepare_completions' -f -a '$__${nameForVar}_comp_results'
169254`;
170255}
+384-214
tests/__snapshots__/shell.test.ts.snap
···33exports[`shell completion generators > fish shell completion > should generate a valid fish completion script 1`] = `
44"# fish completion for testcli -*- shell-script -*-
5566-# Define shell completion directives
77-set -l ShellCompDirectiveError 1
88-set -l ShellCompDirectiveNoSpace 2
99-set -l ShellCompDirectiveNoFileComp 4
1010-set -l ShellCompDirectiveFilterFileExt 8
1111-set -l ShellCompDirectiveFilterDirs 16
1212-set -l ShellCompDirectiveKeepOrder 32
1313-146function __testcli_debug
157 set -l file "$BASH_COMP_DEBUG_FILE"
168 if test -n "$file"
···2113function __testcli_perform_completion
2214 __testcli_debug "Starting __testcli_perform_completion"
23152424- # Extract all args except the completion flag
2525- set -l args (string match -v -- "--completion=" (commandline -opc))
2626-2727- # Extract the current token being completed
2828- set -l current_token (commandline -ct)
2929-3030- # Check if current token starts with a dash
3131- set -l flag_prefix ""
3232- if string match -q -- "-*" $current_token
3333- set flag_prefix "--flag="
3434- end
3535-3636- __testcli_debug "Current token: $current_token"
3737- __testcli_debug "All args: $args"
1616+ # Extract all args except the last one
1717+ set -l args (commandline -opc)
1818+ # Extract the last arg and escape it in case it is a space or wildcard
1919+ set -l lastArg (string escape -- (commandline -ct))
2020+2121+ __testcli_debug "args: $args"
2222+ __testcli_debug "last arg: $lastArg"
2323+2424+ # Build the completion request command
2525+ set -l requestComp "/usr/bin/node /path/to/testcli complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg"
38263939- # Call the completion program and get the results
4040- set -l requestComp "/usr/bin/node /path/to/testcli complete -- $args"
4127 __testcli_debug "Calling $requestComp"
4228 set -l results (eval $requestComp 2> /dev/null)
4343-2929+4430 # Some programs may output extra empty lines after the directive.
4531 # Let's ignore them or else it will break completion.
4632 # Ref: https://github.com/spf13/cobra/issues/1279
4733 for line in $results[-1..1]
4848- if test (string sub -s 1 -l 1 -- $line) = ":"
4949- # The directive
5050- set -l directive (string sub -s 2 -- $line)
5151- set -l directive_num (math $directive)
3434+ if test (string trim -- $line) = ""
3535+ # Found an empty line, remove it
3636+ set results $results[1..-2]
3737+ else
3838+ # Found non-empty line, we have our proper output
5239 break
5340 end
5441 end
5555-5656- # No directive specified, use default
5757- if not set -q directive_num
5858- set directive_num 0
4242+4343+ set -l comps $results[1..-2]
4444+ set -l directiveLine $results[-1]
4545+4646+ # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
4747+ # completions must be prefixed with the flag
4848+ set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
4949+5050+ __testcli_debug "Comps: $comps"
5151+ __testcli_debug "DirectiveLine: $directiveLine"
5252+ __testcli_debug "flagPrefix: $flagPrefix"
5353+5454+ for comp in $comps
5555+ printf "%s%s\\n" "$flagPrefix" "$comp"
5956 end
6060-6161- __testcli_debug "Directive: $directive_num"
62576363- # Process completions based on directive
6464- if test $directive_num -eq $ShellCompDirectiveError
6565- # Error code. No completion.
6666- __testcli_debug "Received error directive: aborting."
5858+ printf "%s\\n" "$directiveLine"
5959+end
6060+6161+# This function limits calls to __testcli_perform_completion, by caching the result
6262+function __testcli_perform_completion_once
6363+ __testcli_debug "Starting __testcli_perform_completion_once"
6464+6565+ if test -n "$__testcli_perform_completion_once_result"
6666+ __testcli_debug "Seems like a valid result already exists, skipping __testcli_perform_completion"
6767+ return 0
6868+ end
6969+7070+ set --global __testcli_perform_completion_once_result (__testcli_perform_completion)
7171+ if test -z "$__testcli_perform_completion_once_result"
7272+ __testcli_debug "No completions, probably due to a failure"
6773 return 1
6874 end
69757070- # Filter out the directive (last line)
7171- if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":"
7272- set results $results[1..-2]
7676+ __testcli_debug "Performed completions and set __testcli_perform_completion_once_result"
7777+ return 0
7878+end
7979+8080+# This function is used to clear the cached result after completions are run
8181+function __testcli_clear_perform_completion_once_result
8282+ __testcli_debug ""
8383+ __testcli_debug "========= clearing previously set __testcli_perform_completion_once_result variable =========="
8484+ set --erase __testcli_perform_completion_once_result
8585+ __testcli_debug "Successfully erased the variable __testcli_perform_completion_once_result"
8686+end
8787+8888+function __testcli_requires_order_preservation
8989+ __testcli_debug ""
9090+ __testcli_debug "========= checking if order preservation is required =========="
9191+9292+ __testcli_perform_completion_once
9393+ if test -z "$__testcli_perform_completion_once_result"
9494+ __testcli_debug "Error determining if order preservation is required"
9595+ return 1
7396 end
74977575- # No completions, let fish handle file completions unless forbidden
7676- if test (count $results) -eq 0
7777- if test $directive_num -ne $ShellCompDirectiveNoFileComp
7878- __testcli_debug "No completions, performing file completion"
7979- return 1
8080- end
8181- __testcli_debug "No completions, but file completion forbidden"
9898+ set -l directive (string sub --start 2 $__testcli_perform_completion_once_result[-1])
9999+ __testcli_debug "Directive is: $directive"
100100+101101+ set -l shellCompDirectiveKeepOrder 32
102102+ set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2)
103103+ __testcli_debug "Keeporder is: $keeporder"
104104+105105+ if test $keeporder -ne 0
106106+ __testcli_debug "This does require order preservation"
82107 return 0
83108 end
841098585- # Filter file extensions
8686- if test $directive_num -eq $ShellCompDirectiveFilterFileExt
8787- __testcli_debug "File extension filtering"
8888- set -l file_extensions
8989- for item in $results
9090- if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-"
9191- set -a file_extensions "*$item"
9292- end
9393- end
9494- __testcli_debug "File extensions: $file_extensions"
9595-9696- # Use the file extensions as completions
9797- set -l completions
9898- for ext in $file_extensions
9999- # Get all files matching the extension
100100- set -a completions (string replace -r '^.*/' '' -- $ext)
101101- end
102102-103103- for item in $completions
104104- echo -e "$item "
105105- end
106106- return 0
110110+ __testcli_debug "This doesn't require order preservation"
111111+ return 1
112112+end
113113+114114+# This function does two things:
115115+# - Obtain the completions and store them in the global __testcli_comp_results
116116+# - Return false if file completion should be performed
117117+function __testcli_prepare_completions
118118+ __testcli_debug ""
119119+ __testcli_debug "========= starting completion logic =========="
120120+121121+ # Start fresh
122122+ set --erase __testcli_comp_results
123123+124124+ __testcli_perform_completion_once
125125+ __testcli_debug "Completion results: $__testcli_perform_completion_once_result"
126126+127127+ if test -z "$__testcli_perform_completion_once_result"
128128+ __testcli_debug "No completion, probably due to a failure"
129129+ # Might as well do file completion, in case it helps
130130+ return 1
131131+ end
132132+133133+ set -l directive (string sub --start 2 $__testcli_perform_completion_once_result[-1])
134134+ set --global __testcli_comp_results $__testcli_perform_completion_once_result[1..-2]
135135+136136+ __testcli_debug "Completions are: $__testcli_comp_results"
137137+ __testcli_debug "Directive is: $directive"
138138+139139+ set -l shellCompDirectiveError 1
140140+ set -l shellCompDirectiveNoSpace 2
141141+ set -l shellCompDirectiveNoFileComp 4
142142+ set -l shellCompDirectiveFilterFileExt 8
143143+ set -l shellCompDirectiveFilterDirs 16
144144+145145+ if test -z "$directive"
146146+ set directive 0
147147+ end
148148+149149+ set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2)
150150+ if test $compErr -eq 1
151151+ __testcli_debug "Received error directive: aborting."
152152+ # Might as well do file completion, in case it helps
153153+ return 1
154154+ end
155155+156156+ set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2)
157157+ set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)
158158+ if test $filefilter -eq 1; or test $dirfilter -eq 1
159159+ __testcli_debug "File extension filtering or directory filtering not supported"
160160+ # Do full file completion instead
161161+ return 1
107162 end
108163109109- # Filter directories
110110- if test $directive_num -eq $ShellCompDirectiveFilterDirs
111111- __testcli_debug "Directory filtering"
112112- set -l dirs
113113- for item in $results
114114- if test -d "$item"
115115- set -a dirs "$item/"
164164+ set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2)
165165+ set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2)
166166+167167+ __testcli_debug "nospace: $nospace, nofiles: $nofiles"
168168+169169+ # If we want to prevent a space, or if file completion is NOT disabled,
170170+ # we need to count the number of valid completions.
171171+ # To do so, we will filter on prefix as the completions we have received
172172+ # may not already be filtered so as to allow fish to match on different
173173+ # criteria than the prefix.
174174+ if test $nospace -ne 0; or test $nofiles -eq 0
175175+ set -l prefix (commandline -t | string escape --style=regex)
176176+ __testcli_debug "prefix: $prefix"
177177+178178+ set -l completions (string match -r -- "^$prefix.*" $__testcli_comp_results)
179179+ set --global __testcli_comp_results $completions
180180+ __testcli_debug "Filtered completions are: $__testcli_comp_results"
181181+182182+ # Important not to quote the variable for count to work
183183+ set -l numComps (count $__testcli_comp_results)
184184+ __testcli_debug "numComps: $numComps"
185185+186186+ if test $numComps -eq 1; and test $nospace -ne 0
187187+ # We must first split on \\t to get rid of the descriptions to be
188188+ # able to check what the actual completion will be.
189189+ # We don't need descriptions anyway since there is only a single
190190+ # real completion which the shell will expand immediately.
191191+ set -l split (string split --max 1 "\\t" $__testcli_comp_results[1])
192192+193193+ # Fish won't add a space if the completion ends with any
194194+ # of the following characters: @=/:.,
195195+ set -l lastChar (string sub -s -1 -- $split)
196196+ if not string match -r -q "[@=/:.,]" -- "$lastChar"
197197+ # In other cases, to support the "nospace" directive we trick the shell
198198+ # by outputting an extra, longer completion.
199199+ __testcli_debug "Adding second completion to perform nospace directive"
200200+ set --global __testcli_comp_results $split[1] $split[1].
201201+ __testcli_debug "Completions are now: $__testcli_comp_results"
116202 end
117203 end
118118-119119- for item in $dirs
120120- echo -e "$item "
121121- end
122122- return 0
123123- end
124204125125- # Process remaining completions
126126- for item in $results
127127- if test -n "$item"
128128- # Check if the item has a description
129129- if string match -q "* *" -- "$item"
130130- set -l completion_parts (string split -- "$item")
131131- set -l comp $completion_parts[1]
132132- set -l desc $completion_parts[2]
133133-134134- # Add the completion and description
135135- echo -e "$comp $desc"
136136- else
137137- # Add just the completion
138138- echo -e "$item "
139139- end
205205+ if test $numComps -eq 0; and test $nofiles -eq 0
206206+ # To be consistent with bash and zsh, we only trigger file
207207+ # completion when there are no other completions
208208+ __testcli_debug "Requesting file completion"
209209+ return 1
140210 end
141211 end
142142-143143- # If directive contains NoSpace, tell fish not to add a space after completion
144144- if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0
145145- return 2
146146- end
147147-212212+148213 return 0
149214end
150215151151-# Set up the completion for the testcli command
152152-complete -c testcli -f -a "(eval __testcli_perform_completion)"
216216+# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
217217+# so we can properly delete any completions provided by another script.
218218+# Only do this if the program can be found, or else fish may print some errors; besides,
219219+# the existing completions will only be loaded if the program can be found.
220220+if type -q "testcli"
221221+ # The space after the program name is essential to trigger completion for the program
222222+ # and not completion of the program name itself.
223223+ # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
224224+ complete --do-complete "testcli " > /dev/null 2>&1
225225+end
226226+227227+# Remove any pre-existing completions for the program since we will be handling all of them.
228228+complete -c testcli -e
229229+230230+# This will get called after the two calls below and clear the cached result
231231+complete -c testcli -n '__testcli_clear_perform_completion_once_result'
232232+# The call to __testcli_prepare_completions will setup __testcli_comp_results
233233+# which provides the program's completion choices.
234234+# If this doesn't require order preservation, we don't use the -k flag
235235+complete -c testcli -n 'not __testcli_requires_order_preservation && __testcli_prepare_completions' -f -a '$__testcli_comp_results'
236236+# Otherwise we use the -k flag
237237+complete -k -c testcli -n '__testcli_requires_order_preservation && __testcli_prepare_completions' -f -a '$__testcli_comp_results'
153238"
154239`;
155240156241exports[`shell completion generators > fish shell completion > should handle special characters in the name 1`] = `
157242"# fish completion for test-cli:app -*- shell-script -*-
158243159159-# Define shell completion directives
160160-set -l ShellCompDirectiveError 1
161161-set -l ShellCompDirectiveNoSpace 2
162162-set -l ShellCompDirectiveNoFileComp 4
163163-set -l ShellCompDirectiveFilterFileExt 8
164164-set -l ShellCompDirectiveFilterDirs 16
165165-set -l ShellCompDirectiveKeepOrder 32
166166-167244function __test_cli_app_debug
168245 set -l file "$BASH_COMP_DEBUG_FILE"
169246 if test -n "$file"
···174251function __test_cli_app_perform_completion
175252 __test_cli_app_debug "Starting __test_cli_app_perform_completion"
176253177177- # Extract all args except the completion flag
178178- set -l args (string match -v -- "--completion=" (commandline -opc))
179179-180180- # Extract the current token being completed
181181- set -l current_token (commandline -ct)
182182-183183- # Check if current token starts with a dash
184184- set -l flag_prefix ""
185185- if string match -q -- "-*" $current_token
186186- set flag_prefix "--flag="
187187- end
188188-189189- __test_cli_app_debug "Current token: $current_token"
190190- __test_cli_app_debug "All args: $args"
254254+ # Extract all args except the last one
255255+ set -l args (commandline -opc)
256256+ # Extract the last arg and escape it in case it is a space or wildcard
257257+ set -l lastArg (string escape -- (commandline -ct))
258258+259259+ __test_cli_app_debug "args: $args"
260260+ __test_cli_app_debug "last arg: $lastArg"
261261+262262+ # Build the completion request command
263263+ set -l requestComp "/usr/bin/node /path/to/testcli complete -- (string join ' ' -- (string escape -- $args[2..-1])) $lastArg"
191264192192- # Call the completion program and get the results
193193- set -l requestComp "/usr/bin/node /path/to/testcli complete -- $args"
194265 __test_cli_app_debug "Calling $requestComp"
195266 set -l results (eval $requestComp 2> /dev/null)
196196-267267+197268 # Some programs may output extra empty lines after the directive.
198269 # Let's ignore them or else it will break completion.
199270 # Ref: https://github.com/spf13/cobra/issues/1279
200271 for line in $results[-1..1]
201201- if test (string sub -s 1 -l 1 -- $line) = ":"
202202- # The directive
203203- set -l directive (string sub -s 2 -- $line)
204204- set -l directive_num (math $directive)
272272+ if test (string trim -- $line) = ""
273273+ # Found an empty line, remove it
274274+ set results $results[1..-2]
275275+ else
276276+ # Found non-empty line, we have our proper output
205277 break
206278 end
207279 end
208208-209209- # No directive specified, use default
210210- if not set -q directive_num
211211- set directive_num 0
280280+281281+ set -l comps $results[1..-2]
282282+ set -l directiveLine $results[-1]
283283+284284+ # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
285285+ # completions must be prefixed with the flag
286286+ set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
287287+288288+ __test_cli_app_debug "Comps: $comps"
289289+ __test_cli_app_debug "DirectiveLine: $directiveLine"
290290+ __test_cli_app_debug "flagPrefix: $flagPrefix"
291291+292292+ for comp in $comps
293293+ printf "%s%s\\n" "$flagPrefix" "$comp"
212294 end
213213-214214- __test_cli_app_debug "Directive: $directive_num"
215295216216- # Process completions based on directive
217217- if test $directive_num -eq $ShellCompDirectiveError
218218- # Error code. No completion.
219219- __test_cli_app_debug "Received error directive: aborting."
296296+ printf "%s\\n" "$directiveLine"
297297+end
298298+299299+# This function limits calls to __test_cli_app_perform_completion, by caching the result
300300+function __test_cli_app_perform_completion_once
301301+ __test_cli_app_debug "Starting __test_cli_app_perform_completion_once"
302302+303303+ if test -n "$__test_cli_app_perform_completion_once_result"
304304+ __test_cli_app_debug "Seems like a valid result already exists, skipping __test_cli_app_perform_completion"
305305+ return 0
306306+ end
307307+308308+ set --global __test_cli_app_perform_completion_once_result (__test_cli_app_perform_completion)
309309+ if test -z "$__test_cli_app_perform_completion_once_result"
310310+ __test_cli_app_debug "No completions, probably due to a failure"
220311 return 1
221312 end
222313223223- # Filter out the directive (last line)
224224- if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":"
225225- set results $results[1..-2]
314314+ __test_cli_app_debug "Performed completions and set __test_cli_app_perform_completion_once_result"
315315+ return 0
316316+end
317317+318318+# This function is used to clear the cached result after completions are run
319319+function __test_cli_app_clear_perform_completion_once_result
320320+ __test_cli_app_debug ""
321321+ __test_cli_app_debug "========= clearing previously set __test_cli_app_perform_completion_once_result variable =========="
322322+ set --erase __test_cli_app_perform_completion_once_result
323323+ __test_cli_app_debug "Successfully erased the variable __test_cli_app_perform_completion_once_result"
324324+end
325325+326326+function __test_cli_app_requires_order_preservation
327327+ __test_cli_app_debug ""
328328+ __test_cli_app_debug "========= checking if order preservation is required =========="
329329+330330+ __test_cli_app_perform_completion_once
331331+ if test -z "$__test_cli_app_perform_completion_once_result"
332332+ __test_cli_app_debug "Error determining if order preservation is required"
333333+ return 1
226334 end
227335228228- # No completions, let fish handle file completions unless forbidden
229229- if test (count $results) -eq 0
230230- if test $directive_num -ne $ShellCompDirectiveNoFileComp
231231- __test_cli_app_debug "No completions, performing file completion"
232232- return 1
233233- end
234234- __test_cli_app_debug "No completions, but file completion forbidden"
336336+ set -l directive (string sub --start 2 $__test_cli_app_perform_completion_once_result[-1])
337337+ __test_cli_app_debug "Directive is: $directive"
338338+339339+ set -l shellCompDirectiveKeepOrder 32
340340+ set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2)
341341+ __test_cli_app_debug "Keeporder is: $keeporder"
342342+343343+ if test $keeporder -ne 0
344344+ __test_cli_app_debug "This does require order preservation"
235345 return 0
236346 end
237347238238- # Filter file extensions
239239- if test $directive_num -eq $ShellCompDirectiveFilterFileExt
240240- __test_cli_app_debug "File extension filtering"
241241- set -l file_extensions
242242- for item in $results
243243- if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-"
244244- set -a file_extensions "*$item"
245245- end
246246- end
247247- __test_cli_app_debug "File extensions: $file_extensions"
248248-249249- # Use the file extensions as completions
250250- set -l completions
251251- for ext in $file_extensions
252252- # Get all files matching the extension
253253- set -a completions (string replace -r '^.*/' '' -- $ext)
254254- end
255255-256256- for item in $completions
257257- echo -e "$item "
258258- end
259259- return 0
348348+ __test_cli_app_debug "This doesn't require order preservation"
349349+ return 1
350350+end
351351+352352+# This function does two things:
353353+# - Obtain the completions and store them in the global __test_cli_app_comp_results
354354+# - Return false if file completion should be performed
355355+function __test_cli_app_prepare_completions
356356+ __test_cli_app_debug ""
357357+ __test_cli_app_debug "========= starting completion logic =========="
358358+359359+ # Start fresh
360360+ set --erase __test_cli_app_comp_results
361361+362362+ __test_cli_app_perform_completion_once
363363+ __test_cli_app_debug "Completion results: $__test_cli_app_perform_completion_once_result"
364364+365365+ if test -z "$__test_cli_app_perform_completion_once_result"
366366+ __test_cli_app_debug "No completion, probably due to a failure"
367367+ # Might as well do file completion, in case it helps
368368+ return 1
369369+ end
370370+371371+ set -l directive (string sub --start 2 $__test_cli_app_perform_completion_once_result[-1])
372372+ set --global __test_cli_app_comp_results $__test_cli_app_perform_completion_once_result[1..-2]
373373+374374+ __test_cli_app_debug "Completions are: $__test_cli_app_comp_results"
375375+ __test_cli_app_debug "Directive is: $directive"
376376+377377+ set -l shellCompDirectiveError 1
378378+ set -l shellCompDirectiveNoSpace 2
379379+ set -l shellCompDirectiveNoFileComp 4
380380+ set -l shellCompDirectiveFilterFileExt 8
381381+ set -l shellCompDirectiveFilterDirs 16
382382+383383+ if test -z "$directive"
384384+ set directive 0
385385+ end
386386+387387+ set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2)
388388+ if test $compErr -eq 1
389389+ __test_cli_app_debug "Received error directive: aborting."
390390+ # Might as well do file completion, in case it helps
391391+ return 1
392392+ end
393393+394394+ set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2)
395395+ set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)
396396+ if test $filefilter -eq 1; or test $dirfilter -eq 1
397397+ __test_cli_app_debug "File extension filtering or directory filtering not supported"
398398+ # Do full file completion instead
399399+ return 1
260400 end
261401262262- # Filter directories
263263- if test $directive_num -eq $ShellCompDirectiveFilterDirs
264264- __test_cli_app_debug "Directory filtering"
265265- set -l dirs
266266- for item in $results
267267- if test -d "$item"
268268- set -a dirs "$item/"
402402+ set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2)
403403+ set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2)
404404+405405+ __test_cli_app_debug "nospace: $nospace, nofiles: $nofiles"
406406+407407+ # If we want to prevent a space, or if file completion is NOT disabled,
408408+ # we need to count the number of valid completions.
409409+ # To do so, we will filter on prefix as the completions we have received
410410+ # may not already be filtered so as to allow fish to match on different
411411+ # criteria than the prefix.
412412+ if test $nospace -ne 0; or test $nofiles -eq 0
413413+ set -l prefix (commandline -t | string escape --style=regex)
414414+ __test_cli_app_debug "prefix: $prefix"
415415+416416+ set -l completions (string match -r -- "^$prefix.*" $__test_cli_app_comp_results)
417417+ set --global __test_cli_app_comp_results $completions
418418+ __test_cli_app_debug "Filtered completions are: $__test_cli_app_comp_results"
419419+420420+ # Important not to quote the variable for count to work
421421+ set -l numComps (count $__test_cli_app_comp_results)
422422+ __test_cli_app_debug "numComps: $numComps"
423423+424424+ if test $numComps -eq 1; and test $nospace -ne 0
425425+ # We must first split on \\t to get rid of the descriptions to be
426426+ # able to check what the actual completion will be.
427427+ # We don't need descriptions anyway since there is only a single
428428+ # real completion which the shell will expand immediately.
429429+ set -l split (string split --max 1 "\\t" $__test_cli_app_comp_results[1])
430430+431431+ # Fish won't add a space if the completion ends with any
432432+ # of the following characters: @=/:.,
433433+ set -l lastChar (string sub -s -1 -- $split)
434434+ if not string match -r -q "[@=/:.,]" -- "$lastChar"
435435+ # In other cases, to support the "nospace" directive we trick the shell
436436+ # by outputting an extra, longer completion.
437437+ __test_cli_app_debug "Adding second completion to perform nospace directive"
438438+ set --global __test_cli_app_comp_results $split[1] $split[1].
439439+ __test_cli_app_debug "Completions are now: $__test_cli_app_comp_results"
269440 end
270441 end
271271-272272- for item in $dirs
273273- echo -e "$item "
274274- end
275275- return 0
276276- end
277442278278- # Process remaining completions
279279- for item in $results
280280- if test -n "$item"
281281- # Check if the item has a description
282282- if string match -q "* *" -- "$item"
283283- set -l completion_parts (string split -- "$item")
284284- set -l comp $completion_parts[1]
285285- set -l desc $completion_parts[2]
286286-287287- # Add the completion and description
288288- echo -e "$comp $desc"
289289- else
290290- # Add just the completion
291291- echo -e "$item "
292292- end
443443+ if test $numComps -eq 0; and test $nofiles -eq 0
444444+ # To be consistent with bash and zsh, we only trigger file
445445+ # completion when there are no other completions
446446+ __test_cli_app_debug "Requesting file completion"
447447+ return 1
293448 end
294449 end
295295-296296- # If directive contains NoSpace, tell fish not to add a space after completion
297297- if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0
298298- return 2
299299- end
300300-450450+301451 return 0
302452end
303453304304-# Set up the completion for the test-cli:app command
305305-complete -c test-cli:app -f -a "(eval __test_cli_app_perform_completion)"
454454+# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
455455+# so we can properly delete any completions provided by another script.
456456+# Only do this if the program can be found, or else fish may print some errors; besides,
457457+# the existing completions will only be loaded if the program can be found.
458458+if type -q "test-cli:app"
459459+ # The space after the program name is essential to trigger completion for the program
460460+ # and not completion of the program name itself.
461461+ # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
462462+ complete --do-complete "test-cli:app " > /dev/null 2>&1
463463+end
464464+465465+# Remove any pre-existing completions for the program since we will be handling all of them.
466466+complete -c test-cli:app -e
467467+468468+# This will get called after the two calls below and clear the cached result
469469+complete -c test-cli:app -n '__test_cli_app_clear_perform_completion_once_result'
470470+# The call to __test_cli_app_prepare_completions will setup __test_cli_app_comp_results
471471+# which provides the program's completion choices.
472472+# If this doesn't require order preservation, we don't use the -k flag
473473+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'
474474+# Otherwise we use the -k flag
475475+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'
306476"
307477`;
+129
tests/fish-integration.test.ts
···11+import { describe, it, expect, beforeAll } from 'vitest';
22+import { execSync, spawnSync } from 'child_process';
33+import * as path from 'path';
44+55+// Check if fish is available
66+function isFishAvailable(): boolean {
77+ try {
88+ execSync('fish --version', { stdio: 'pipe' });
99+ return true;
1010+ } catch {
1111+ return false;
1212+ }
1313+}
1414+1515+// Run fish command and return output
1616+function runFish(script: string): string {
1717+ const result = spawnSync('fish', ['-c', script], {
1818+ encoding: 'utf-8',
1919+ stdio: ['pipe', 'pipe', 'pipe'],
2020+ });
2121+ if (result.error) {
2222+ throw result.error;
2323+ }
2424+ return result.stdout + result.stderr;
2525+}
2626+2727+// Simulate TAB completion in fish
2828+function simulateTab(completionScript: string, commandLine: string): string[] {
2929+ const script = `
3030+ source (echo '${completionScript.replace(/'/g, "\\'")}' | psub)
3131+ complete --do-complete "${commandLine}"
3232+ `;
3333+ const output = runFish(script);
3434+ return output
3535+ .split('\n')
3636+ .filter((line) => line.trim() !== '')
3737+ .map((line) => line.trim());
3838+}
3939+4040+describe.skipIf(!isFishAvailable())(
4141+ 'fish shell completion integration tests',
4242+ () => {
4343+ const demoCliPath = path.join(
4444+ __dirname,
4545+ '../examples/demo-cli-cac/demo-cli-cac.js'
4646+ );
4747+ let completionScript: string;
4848+4949+ beforeAll(() => {
5050+ // Generate the completion script from demo-cli-cac
5151+ const result = spawnSync('node', [demoCliPath, 'complete', 'fish'], {
5252+ encoding: 'utf-8',
5353+ cwd: path.dirname(demoCliPath),
5454+ });
5555+ completionScript = result.stdout;
5656+ });
5757+5858+ it('should complete subcommands when pressing TAB after command', () => {
5959+ const completions = simulateTab(completionScript, 'demo-cli-cac ');
6060+6161+ // Should contain subcommands
6262+ expect(completions.some((c) => c.startsWith('start'))).toBe(true);
6363+ expect(completions.some((c) => c.startsWith('build'))).toBe(true);
6464+ });
6565+6666+ it('should complete flags when pressing TAB after --', () => {
6767+ const completions = simulateTab(completionScript, 'demo-cli-cac --');
6868+6969+ // Should contain global flags
7070+ expect(completions.some((c) => c.includes('--config'))).toBe(true);
7171+ expect(completions.some((c) => c.includes('--debug'))).toBe(true);
7272+ expect(completions.some((c) => c.includes('--help'))).toBe(true);
7373+ expect(completions.some((c) => c.includes('--version'))).toBe(true);
7474+ });
7575+7676+ it('should complete subcommand-specific flags', () => {
7777+ const completions = simulateTab(
7878+ completionScript,
7979+ 'demo-cli-cac start --'
8080+ );
8181+8282+ // Should contain start-specific flag
8383+ expect(completions.some((c) => c.includes('--port'))).toBe(true);
8484+ // Should also contain global flags
8585+ expect(completions.some((c) => c.includes('--config'))).toBe(true);
8686+ });
8787+8888+ it('should complete build command flags', () => {
8989+ const completions = simulateTab(
9090+ completionScript,
9191+ 'demo-cli-cac build --'
9292+ );
9393+9494+ // Should contain build-specific flag
9595+ expect(completions.some((c) => c.includes('--mode'))).toBe(true);
9696+ // Should also contain global flags
9797+ expect(completions.some((c) => c.includes('--config'))).toBe(true);
9898+ });
9999+100100+ it('should show descriptions with completions', () => {
101101+ const completions = simulateTab(completionScript, 'demo-cli-cac ');
102102+103103+ // Check that descriptions are included (tab-separated)
104104+ const startCompletion = completions.find((c) => c.startsWith('start'));
105105+ expect(startCompletion).toContain('Start the application');
106106+107107+ const buildCompletion = completions.find((c) => c.startsWith('build'));
108108+ expect(buildCompletion).toContain('Build the application');
109109+ });
110110+111111+ it('should filter completions based on partial input', () => {
112112+ const completions = simulateTab(completionScript, 'demo-cli-cac st');
113113+114114+ // Should only show completions starting with 'st'
115115+ expect(completions.some((c) => c.startsWith('start'))).toBe(true);
116116+ // 'build' should not appear
117117+ expect(completions.some((c) => c.startsWith('build'))).toBe(false);
118118+ });
119119+120120+ it('should filter flag completions based on partial input', () => {
121121+ const completions = simulateTab(completionScript, 'demo-cli-cac --c');
122122+123123+ // Should show --config
124124+ expect(completions.some((c) => c.includes('--config'))).toBe(true);
125125+ // Should not show --debug (doesn't start with --c)
126126+ expect(completions.some((c) => c.includes('--debug'))).toBe(false);
127127+ });
128128+ }
129129+);