zsh_completions.go (11020B)
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 ) 23 24 // GenZshCompletionFile generates zsh completion file including descriptions. 25 func (c *Command) GenZshCompletionFile(filename string) error { 26 return c.genZshCompletionFile(filename, true) 27 } 28 29 // GenZshCompletion generates zsh completion file including descriptions 30 // and writes it to the passed writer. 31 func (c *Command) GenZshCompletion(w io.Writer) error { 32 return c.genZshCompletion(w, true) 33 } 34 35 // GenZshCompletionFileNoDesc generates zsh completion file without descriptions. 36 func (c *Command) GenZshCompletionFileNoDesc(filename string) error { 37 return c.genZshCompletionFile(filename, false) 38 } 39 40 // GenZshCompletionNoDesc generates zsh completion file without descriptions 41 // and writes it to the passed writer. 42 func (c *Command) GenZshCompletionNoDesc(w io.Writer) error { 43 return c.genZshCompletion(w, false) 44 } 45 46 // MarkZshCompPositionalArgumentFile only worked for zsh and its behavior was 47 // not consistent with Bash completion. It has therefore been disabled. 48 // Instead, when no other completion is specified, file completion is done by 49 // default for every argument. One can disable file completion on a per-argument 50 // basis by using ValidArgsFunction and ShellCompDirectiveNoFileComp. 51 // To achieve file extension filtering, one can use ValidArgsFunction and 52 // ShellCompDirectiveFilterFileExt. 53 // 54 // Deprecated 55 func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { 56 return nil 57 } 58 59 // MarkZshCompPositionalArgumentWords only worked for zsh. It has therefore 60 // been disabled. 61 // To achieve the same behavior across all shells, one can use 62 // ValidArgs (for the first argument only) or ValidArgsFunction for 63 // any argument (can include the first one also). 64 // 65 // Deprecated 66 func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { 67 return nil 68 } 69 70 func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error { 71 outFile, err := os.Create(filename) 72 if err != nil { 73 return err 74 } 75 defer outFile.Close() 76 77 return c.genZshCompletion(outFile, includeDesc) 78 } 79 80 func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { 81 buf := new(bytes.Buffer) 82 genZshComp(buf, c.Name(), includeDesc) 83 _, err := buf.WriteTo(w) 84 return err 85 } 86 87 func genZshComp(buf io.StringWriter, name string, includeDesc bool) { 88 compCmd := ShellCompRequestCmd 89 if !includeDesc { 90 compCmd = ShellCompNoDescRequestCmd 91 } 92 WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[1]s 93 compdef _%[1]s %[1]s 94 95 # zsh completion for %-36[1]s -*- shell-script -*- 96 97 __%[1]s_debug() 98 { 99 local file="$BASH_COMP_DEBUG_FILE" 100 if [[ -n ${file} ]]; then 101 echo "$*" >> "${file}" 102 fi 103 } 104 105 _%[1]s() 106 { 107 local shellCompDirectiveError=%[3]d 108 local shellCompDirectiveNoSpace=%[4]d 109 local shellCompDirectiveNoFileComp=%[5]d 110 local shellCompDirectiveFilterFileExt=%[6]d 111 local shellCompDirectiveFilterDirs=%[7]d 112 local shellCompDirectiveKeepOrder=%[8]d 113 114 local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder 115 local -a completions 116 117 __%[1]s_debug "\n========= starting completion logic ==========" 118 __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" 119 120 # The user could have moved the cursor backwards on the command-line. 121 # We need to trigger completion from the $CURRENT location, so we need 122 # to truncate the command-line ($words) up to the $CURRENT location. 123 # (We cannot use $CURSOR as its value does not work when a command is an alias.) 124 words=("${=words[1,CURRENT]}") 125 __%[1]s_debug "Truncated words[*]: ${words[*]}," 126 127 lastParam=${words[-1]} 128 lastChar=${lastParam[-1]} 129 __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" 130 131 # For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>) 132 # completions must be prefixed with the flag 133 setopt local_options BASH_REMATCH 134 if [[ "${lastParam}" =~ '-.*=' ]]; then 135 # We are dealing with a flag with an = 136 flagPrefix="-P ${BASH_REMATCH}" 137 fi 138 139 # Prepare the command to obtain completions 140 requestComp="${words[1]} %[2]s ${words[2,-1]}" 141 if [ "${lastChar}" = "" ]; then 142 # If the last parameter is complete (there is a space following it) 143 # We add an extra empty parameter so we can indicate this to the go completion code. 144 __%[1]s_debug "Adding extra empty parameter" 145 requestComp="${requestComp} \"\"" 146 fi 147 148 __%[1]s_debug "About to call: eval ${requestComp}" 149 150 # Use eval to handle any environment variables and such 151 out=$(eval ${requestComp} 2>/dev/null) 152 __%[1]s_debug "completion output: ${out}" 153 154 # Extract the directive integer following a : from the last line 155 local lastLine 156 while IFS='\n' read -r line; do 157 lastLine=${line} 158 done < <(printf "%%s\n" "${out[@]}") 159 __%[1]s_debug "last line: ${lastLine}" 160 161 if [ "${lastLine[1]}" = : ]; then 162 directive=${lastLine[2,-1]} 163 # Remove the directive including the : and the newline 164 local suffix 165 (( suffix=${#lastLine}+2)) 166 out=${out[1,-$suffix]} 167 else 168 # There is no directive specified. Leave $out as is. 169 __%[1]s_debug "No directive found. Setting do default" 170 directive=0 171 fi 172 173 __%[1]s_debug "directive: ${directive}" 174 __%[1]s_debug "completions: ${out}" 175 __%[1]s_debug "flagPrefix: ${flagPrefix}" 176 177 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 178 __%[1]s_debug "Completion received error. Ignoring completions." 179 return 180 fi 181 182 local activeHelpMarker="%[9]s" 183 local endIndex=${#activeHelpMarker} 184 local startIndex=$((${#activeHelpMarker}+1)) 185 local hasActiveHelp=0 186 while IFS='\n' read -r comp; do 187 # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) 188 if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then 189 __%[1]s_debug "ActiveHelp found: $comp" 190 comp="${comp[$startIndex,-1]}" 191 if [ -n "$comp" ]; then 192 compadd -x "${comp}" 193 __%[1]s_debug "ActiveHelp will need delimiter" 194 hasActiveHelp=1 195 fi 196 197 continue 198 fi 199 200 if [ -n "$comp" ]; then 201 # If requested, completions are returned with a description. 202 # The description is preceded by a TAB character. 203 # For zsh's _describe, we need to use a : instead of a TAB. 204 # We first need to escape any : as part of the completion itself. 205 comp=${comp//:/\\:} 206 207 local tab="$(printf '\t')" 208 comp=${comp//$tab/:} 209 210 __%[1]s_debug "Adding completion: ${comp}" 211 completions+=${comp} 212 lastComp=$comp 213 fi 214 done < <(printf "%%s\n" "${out[@]}") 215 216 # Add a delimiter after the activeHelp statements, but only if: 217 # - there are completions following the activeHelp statements, or 218 # - file completion will be performed (so there will be choices after the activeHelp) 219 if [ $hasActiveHelp -eq 1 ]; then 220 if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 221 __%[1]s_debug "Adding activeHelp delimiter" 222 compadd -x "--" 223 hasActiveHelp=0 224 fi 225 fi 226 227 if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 228 __%[1]s_debug "Activating nospace." 229 noSpace="-S ''" 230 fi 231 232 if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then 233 __%[1]s_debug "Activating keep order." 234 keepOrder="-V" 235 fi 236 237 if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 238 # File extension filtering 239 local filteringCmd 240 filteringCmd='_files' 241 for filter in ${completions[@]}; do 242 if [ ${filter[1]} != '*' ]; then 243 # zsh requires a glob pattern to do file filtering 244 filter="\*.$filter" 245 fi 246 filteringCmd+=" -g $filter" 247 done 248 filteringCmd+=" ${flagPrefix}" 249 250 __%[1]s_debug "File filtering command: $filteringCmd" 251 _arguments '*:filename:'"$filteringCmd" 252 elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 253 # File completion for directories only 254 local subdir 255 subdir="${completions[1]}" 256 if [ -n "$subdir" ]; then 257 __%[1]s_debug "Listing directories in $subdir" 258 pushd "${subdir}" >/dev/null 2>&1 259 else 260 __%[1]s_debug "Listing directories in ." 261 fi 262 263 local result 264 _arguments '*:dirname:_files -/'" ${flagPrefix}" 265 result=$? 266 if [ -n "$subdir" ]; then 267 popd >/dev/null 2>&1 268 fi 269 return $result 270 else 271 __%[1]s_debug "Calling _describe" 272 if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then 273 __%[1]s_debug "_describe found some completions" 274 275 # Return the success of having called _describe 276 return 0 277 else 278 __%[1]s_debug "_describe did not find completions." 279 __%[1]s_debug "Checking if we should do file completion." 280 if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 281 __%[1]s_debug "deactivating file completion" 282 283 # We must return an error code here to let zsh know that there were no 284 # completions found by _describe; this is what will trigger other 285 # matching algorithms to attempt to find completions. 286 # For example zsh can match letters in the middle of words. 287 return 1 288 else 289 # Perform file completion 290 __%[1]s_debug "Activating file completion" 291 292 # We must return the result of this command, so it must be the 293 # last command, or else we must store its result to return it. 294 _arguments '*:filename:_files'" ${flagPrefix}" 295 fi 296 fi 297 fi 298 } 299 300 # don't run the completion function when being source-ed or eval-ed 301 if [ "$funcstack[1]" = "_%[1]s" ]; then 302 _%[1]s 303 fi 304 `, name, compCmd, 305 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 306 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, 307 activeHelpMarker)) 308 }