| Abhay Kumar | a61c522 | 2025-11-10 07:32:50 +0000 | [diff] [blame^] | 1 | // Copyright 2013-2023 The Cobra Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package cobra |
| 16 | |
| 17 | import ( |
| 18 | "bytes" |
| 19 | "fmt" |
| 20 | "io" |
| 21 | "os" |
| 22 | "strings" |
| 23 | ) |
| 24 | |
| 25 | func genFishComp(buf io.StringWriter, name string, includeDesc bool) { |
| 26 | // Variables should not contain a '-' or ':' character |
| 27 | nameForVar := name |
| 28 | nameForVar = strings.ReplaceAll(nameForVar, "-", "_") |
| 29 | nameForVar = strings.ReplaceAll(nameForVar, ":", "_") |
| 30 | |
| 31 | compCmd := ShellCompRequestCmd |
| 32 | if !includeDesc { |
| 33 | compCmd = ShellCompNoDescRequestCmd |
| 34 | } |
| 35 | WriteStringAndCheck(buf, fmt.Sprintf("# fish completion for %-36s -*- shell-script -*-\n", name)) |
| 36 | WriteStringAndCheck(buf, fmt.Sprintf(` |
| 37 | function __%[1]s_debug |
| 38 | set -l file "$BASH_COMP_DEBUG_FILE" |
| 39 | if test -n "$file" |
| 40 | echo "$argv" >> $file |
| 41 | end |
| 42 | end |
| 43 | |
| 44 | function __%[1]s_perform_completion |
| 45 | __%[1]s_debug "Starting __%[1]s_perform_completion" |
| 46 | |
| 47 | # Extract all args except the last one |
| 48 | set -l args (commandline -opc) |
| 49 | # Extract the last arg and escape it in case it is a space |
| 50 | set -l lastArg (string escape -- (commandline -ct)) |
| 51 | |
| 52 | __%[1]s_debug "args: $args" |
| 53 | __%[1]s_debug "last arg: $lastArg" |
| 54 | |
| 55 | # Disable ActiveHelp which is not supported for fish shell |
| 56 | set -l requestComp "%[10]s=0 $args[1] %[3]s $args[2..-1] $lastArg" |
| 57 | |
| 58 | __%[1]s_debug "Calling $requestComp" |
| 59 | set -l results (eval $requestComp 2> /dev/null) |
| 60 | |
| 61 | # Some programs may output extra empty lines after the directive. |
| 62 | # Let's ignore them or else it will break completion. |
| 63 | # Ref: https://github.com/spf13/cobra/issues/1279 |
| 64 | for line in $results[-1..1] |
| 65 | if test (string trim -- $line) = "" |
| 66 | # Found an empty line, remove it |
| 67 | set results $results[1..-2] |
| 68 | else |
| 69 | # Found non-empty line, we have our proper output |
| 70 | break |
| 71 | end |
| 72 | end |
| 73 | |
| 74 | set -l comps $results[1..-2] |
| 75 | set -l directiveLine $results[-1] |
| 76 | |
| 77 | # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>) |
| 78 | # completions must be prefixed with the flag |
| 79 | set -l flagPrefix (string match -r -- '-.*=' "$lastArg") |
| 80 | |
| 81 | __%[1]s_debug "Comps: $comps" |
| 82 | __%[1]s_debug "DirectiveLine: $directiveLine" |
| 83 | __%[1]s_debug "flagPrefix: $flagPrefix" |
| 84 | |
| 85 | for comp in $comps |
| 86 | printf "%%s%%s\n" "$flagPrefix" "$comp" |
| 87 | end |
| 88 | |
| 89 | printf "%%s\n" "$directiveLine" |
| 90 | end |
| 91 | |
| 92 | # this function limits calls to __%[1]s_perform_completion, by caching the result behind $__%[1]s_perform_completion_once_result |
| 93 | function __%[1]s_perform_completion_once |
| 94 | __%[1]s_debug "Starting __%[1]s_perform_completion_once" |
| 95 | |
| 96 | if test -n "$__%[1]s_perform_completion_once_result" |
| 97 | __%[1]s_debug "Seems like a valid result already exists, skipping __%[1]s_perform_completion" |
| 98 | return 0 |
| 99 | end |
| 100 | |
| 101 | set --global __%[1]s_perform_completion_once_result (__%[1]s_perform_completion) |
| 102 | if test -z "$__%[1]s_perform_completion_once_result" |
| 103 | __%[1]s_debug "No completions, probably due to a failure" |
| 104 | return 1 |
| 105 | end |
| 106 | |
| 107 | __%[1]s_debug "Performed completions and set __%[1]s_perform_completion_once_result" |
| 108 | return 0 |
| 109 | end |
| 110 | |
| 111 | # this function is used to clear the $__%[1]s_perform_completion_once_result variable after completions are run |
| 112 | function __%[1]s_clear_perform_completion_once_result |
| 113 | __%[1]s_debug "" |
| 114 | __%[1]s_debug "========= clearing previously set __%[1]s_perform_completion_once_result variable ==========" |
| 115 | set --erase __%[1]s_perform_completion_once_result |
| 116 | __%[1]s_debug "Successfully erased the variable __%[1]s_perform_completion_once_result" |
| 117 | end |
| 118 | |
| 119 | function __%[1]s_requires_order_preservation |
| 120 | __%[1]s_debug "" |
| 121 | __%[1]s_debug "========= checking if order preservation is required ==========" |
| 122 | |
| 123 | __%[1]s_perform_completion_once |
| 124 | if test -z "$__%[1]s_perform_completion_once_result" |
| 125 | __%[1]s_debug "Error determining if order preservation is required" |
| 126 | return 1 |
| 127 | end |
| 128 | |
| 129 | set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) |
| 130 | __%[1]s_debug "Directive is: $directive" |
| 131 | |
| 132 | set -l shellCompDirectiveKeepOrder %[9]d |
| 133 | set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) %% 2) |
| 134 | __%[1]s_debug "Keeporder is: $keeporder" |
| 135 | |
| 136 | if test $keeporder -ne 0 |
| 137 | __%[1]s_debug "This does require order preservation" |
| 138 | return 0 |
| 139 | end |
| 140 | |
| 141 | __%[1]s_debug "This doesn't require order preservation" |
| 142 | return 1 |
| 143 | end |
| 144 | |
| 145 | |
| 146 | # This function does two things: |
| 147 | # - Obtain the completions and store them in the global __%[1]s_comp_results |
| 148 | # - Return false if file completion should be performed |
| 149 | function __%[1]s_prepare_completions |
| 150 | __%[1]s_debug "" |
| 151 | __%[1]s_debug "========= starting completion logic ==========" |
| 152 | |
| 153 | # Start fresh |
| 154 | set --erase __%[1]s_comp_results |
| 155 | |
| 156 | __%[1]s_perform_completion_once |
| 157 | __%[1]s_debug "Completion results: $__%[1]s_perform_completion_once_result" |
| 158 | |
| 159 | if test -z "$__%[1]s_perform_completion_once_result" |
| 160 | __%[1]s_debug "No completion, probably due to a failure" |
| 161 | # Might as well do file completion, in case it helps |
| 162 | return 1 |
| 163 | end |
| 164 | |
| 165 | set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) |
| 166 | set --global __%[1]s_comp_results $__%[1]s_perform_completion_once_result[1..-2] |
| 167 | |
| 168 | __%[1]s_debug "Completions are: $__%[1]s_comp_results" |
| 169 | __%[1]s_debug "Directive is: $directive" |
| 170 | |
| 171 | set -l shellCompDirectiveError %[4]d |
| 172 | set -l shellCompDirectiveNoSpace %[5]d |
| 173 | set -l shellCompDirectiveNoFileComp %[6]d |
| 174 | set -l shellCompDirectiveFilterFileExt %[7]d |
| 175 | set -l shellCompDirectiveFilterDirs %[8]d |
| 176 | |
| 177 | if test -z "$directive" |
| 178 | set directive 0 |
| 179 | end |
| 180 | |
| 181 | set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) |
| 182 | if test $compErr -eq 1 |
| 183 | __%[1]s_debug "Received error directive: aborting." |
| 184 | # Might as well do file completion, in case it helps |
| 185 | return 1 |
| 186 | end |
| 187 | |
| 188 | set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) |
| 189 | set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) |
| 190 | if test $filefilter -eq 1; or test $dirfilter -eq 1 |
| 191 | __%[1]s_debug "File extension filtering or directory filtering not supported" |
| 192 | # Do full file completion instead |
| 193 | return 1 |
| 194 | end |
| 195 | |
| 196 | set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) |
| 197 | set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) |
| 198 | |
| 199 | __%[1]s_debug "nospace: $nospace, nofiles: $nofiles" |
| 200 | |
| 201 | # If we want to prevent a space, or if file completion is NOT disabled, |
| 202 | # we need to count the number of valid completions. |
| 203 | # To do so, we will filter on prefix as the completions we have received |
| 204 | # may not already be filtered so as to allow fish to match on different |
| 205 | # criteria than the prefix. |
| 206 | if test $nospace -ne 0; or test $nofiles -eq 0 |
| 207 | set -l prefix (commandline -t | string escape --style=regex) |
| 208 | __%[1]s_debug "prefix: $prefix" |
| 209 | |
| 210 | set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results) |
| 211 | set --global __%[1]s_comp_results $completions |
| 212 | __%[1]s_debug "Filtered completions are: $__%[1]s_comp_results" |
| 213 | |
| 214 | # Important not to quote the variable for count to work |
| 215 | set -l numComps (count $__%[1]s_comp_results) |
| 216 | __%[1]s_debug "numComps: $numComps" |
| 217 | |
| 218 | if test $numComps -eq 1; and test $nospace -ne 0 |
| 219 | # We must first split on \t to get rid of the descriptions to be |
| 220 | # able to check what the actual completion will be. |
| 221 | # We don't need descriptions anyway since there is only a single |
| 222 | # real completion which the shell will expand immediately. |
| 223 | set -l split (string split --max 1 \t $__%[1]s_comp_results[1]) |
| 224 | |
| 225 | # Fish won't add a space if the completion ends with any |
| 226 | # of the following characters: @=/:., |
| 227 | set -l lastChar (string sub -s -1 -- $split) |
| 228 | if not string match -r -q "[@=/:.,]" -- "$lastChar" |
| 229 | # In other cases, to support the "nospace" directive we trick the shell |
| 230 | # by outputting an extra, longer completion. |
| 231 | __%[1]s_debug "Adding second completion to perform nospace directive" |
| 232 | set --global __%[1]s_comp_results $split[1] $split[1]. |
| 233 | __%[1]s_debug "Completions are now: $__%[1]s_comp_results" |
| 234 | end |
| 235 | end |
| 236 | |
| 237 | if test $numComps -eq 0; and test $nofiles -eq 0 |
| 238 | # To be consistent with bash and zsh, we only trigger file |
| 239 | # completion when there are no other completions |
| 240 | __%[1]s_debug "Requesting file completion" |
| 241 | return 1 |
| 242 | end |
| 243 | end |
| 244 | |
| 245 | return 0 |
| 246 | end |
| 247 | |
| 248 | # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves |
| 249 | # so we can properly delete any completions provided by another script. |
| 250 | # Only do this if the program can be found, or else fish may print some errors; besides, |
| 251 | # the existing completions will only be loaded if the program can be found. |
| 252 | if type -q "%[2]s" |
| 253 | # The space after the program name is essential to trigger completion for the program |
| 254 | # and not completion of the program name itself. |
| 255 | # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. |
| 256 | complete --do-complete "%[2]s " > /dev/null 2>&1 |
| 257 | end |
| 258 | |
| 259 | # Remove any pre-existing completions for the program since we will be handling all of them. |
| 260 | complete -c %[2]s -e |
| 261 | |
| 262 | # this will get called after the two calls below and clear the $__%[1]s_perform_completion_once_result global |
| 263 | complete -c %[2]s -n '__%[1]s_clear_perform_completion_once_result' |
| 264 | # The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results |
| 265 | # which provides the program's completion choices. |
| 266 | # If this doesn't require order preservation, we don't use the -k flag |
| 267 | complete -c %[2]s -n 'not __%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' |
| 268 | # otherwise we use the -k flag |
| 269 | complete -k -c %[2]s -n '__%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' |
| 270 | `, nameForVar, name, compCmd, |
| 271 | ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
| 272 | ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name))) |
| 273 | } |
| 274 | |
| 275 | // GenFishCompletion generates fish completion file and writes to the passed writer. |
| 276 | func (c *Command) GenFishCompletion(w io.Writer, includeDesc bool) error { |
| 277 | buf := new(bytes.Buffer) |
| 278 | genFishComp(buf, c.Name(), includeDesc) |
| 279 | _, err := buf.WriteTo(w) |
| 280 | return err |
| 281 | } |
| 282 | |
| 283 | // GenFishCompletionFile generates fish completion file. |
| 284 | func (c *Command) GenFishCompletionFile(filename string, includeDesc bool) error { |
| 285 | outFile, err := os.Create(filename) |
| 286 | if err != nil { |
| 287 | return err |
| 288 | } |
| 289 | defer outFile.Close() |
| 290 | |
| 291 | return c.GenFishCompletion(outFile, includeDesc) |
| 292 | } |