git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cobra/zsh_completions.go (about) 1 // Copyright 2013-2022 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 94 # zsh completion for %-36[1]s -*- shell-script -*- 95 96 __%[1]s_debug() 97 { 98 local file="$BASH_COMP_DEBUG_FILE" 99 if [[ -n ${file} ]]; then 100 echo "$*" >> "${file}" 101 fi 102 } 103 104 _%[1]s() 105 { 106 local shellCompDirectiveError=%[3]d 107 local shellCompDirectiveNoSpace=%[4]d 108 local shellCompDirectiveNoFileComp=%[5]d 109 local shellCompDirectiveFilterFileExt=%[6]d 110 local shellCompDirectiveFilterDirs=%[7]d 111 112 local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace 113 local -a completions 114 115 __%[1]s_debug "\n========= starting completion logic ==========" 116 __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" 117 118 # The user could have moved the cursor backwards on the command-line. 119 # We need to trigger completion from the $CURRENT location, so we need 120 # to truncate the command-line ($words) up to the $CURRENT location. 121 # (We cannot use $CURSOR as its value does not work when a command is an alias.) 122 words=("${=words[1,CURRENT]}") 123 __%[1]s_debug "Truncated words[*]: ${words[*]}," 124 125 lastParam=${words[-1]} 126 lastChar=${lastParam[-1]} 127 __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" 128 129 # For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>) 130 # completions must be prefixed with the flag 131 setopt local_options BASH_REMATCH 132 if [[ "${lastParam}" =~ '-.*=' ]]; then 133 # We are dealing with a flag with an = 134 flagPrefix="-P ${BASH_REMATCH}" 135 fi 136 137 # Prepare the command to obtain completions 138 requestComp="${words[1]} %[2]s ${words[2,-1]}" 139 if [ "${lastChar}" = "" ]; then 140 # If the last parameter is complete (there is a space following it) 141 # We add an extra empty parameter so we can indicate this to the go completion code. 142 __%[1]s_debug "Adding extra empty parameter" 143 requestComp="${requestComp} \"\"" 144 fi 145 146 __%[1]s_debug "About to call: eval ${requestComp}" 147 148 # Use eval to handle any environment variables and such 149 out=$(eval ${requestComp} 2>/dev/null) 150 __%[1]s_debug "completion output: ${out}" 151 152 # Extract the directive integer following a : from the last line 153 local lastLine 154 while IFS='\n' read -r line; do 155 lastLine=${line} 156 done < <(printf "%%s\n" "${out[@]}") 157 __%[1]s_debug "last line: ${lastLine}" 158 159 if [ "${lastLine[1]}" = : ]; then 160 directive=${lastLine[2,-1]} 161 # Remove the directive including the : and the newline 162 local suffix 163 (( suffix=${#lastLine}+2)) 164 out=${out[1,-$suffix]} 165 else 166 # There is no directive specified. Leave $out as is. 167 __%[1]s_debug "No directive found. Setting do default" 168 directive=0 169 fi 170 171 __%[1]s_debug "directive: ${directive}" 172 __%[1]s_debug "completions: ${out}" 173 __%[1]s_debug "flagPrefix: ${flagPrefix}" 174 175 if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 176 __%[1]s_debug "Completion received error. Ignoring completions." 177 return 178 fi 179 180 local activeHelpMarker="%[8]s" 181 local endIndex=${#activeHelpMarker} 182 local startIndex=$((${#activeHelpMarker}+1)) 183 local hasActiveHelp=0 184 while IFS='\n' read -r comp; do 185 # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) 186 if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then 187 __%[1]s_debug "ActiveHelp found: $comp" 188 comp="${comp[$startIndex,-1]}" 189 if [ -n "$comp" ]; then 190 compadd -x "${comp}" 191 __%[1]s_debug "ActiveHelp will need delimiter" 192 hasActiveHelp=1 193 fi 194 195 continue 196 fi 197 198 if [ -n "$comp" ]; then 199 # If requested, completions are returned with a description. 200 # The description is preceded by a TAB character. 201 # For zsh's _describe, we need to use a : instead of a TAB. 202 # We first need to escape any : as part of the completion itself. 203 comp=${comp//:/\\:} 204 205 local tab="$(printf '\t')" 206 comp=${comp//$tab/:} 207 208 __%[1]s_debug "Adding completion: ${comp}" 209 completions+=${comp} 210 lastComp=$comp 211 fi 212 done < <(printf "%%s\n" "${out[@]}") 213 214 # Add a delimiter after the activeHelp statements, but only if: 215 # - there are completions following the activeHelp statements, or 216 # - file completion will be performed (so there will be choices after the activeHelp) 217 if [ $hasActiveHelp -eq 1 ]; then 218 if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 219 __%[1]s_debug "Adding activeHelp delimiter" 220 compadd -x "--" 221 hasActiveHelp=0 222 fi 223 fi 224 225 if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 226 __%[1]s_debug "Activating nospace." 227 noSpace="-S ''" 228 fi 229 230 if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 231 # File extension filtering 232 local filteringCmd 233 filteringCmd='_files' 234 for filter in ${completions[@]}; do 235 if [ ${filter[1]} != '*' ]; then 236 # zsh requires a glob pattern to do file filtering 237 filter="\*.$filter" 238 fi 239 filteringCmd+=" -g $filter" 240 done 241 filteringCmd+=" ${flagPrefix}" 242 243 __%[1]s_debug "File filtering command: $filteringCmd" 244 _arguments '*:filename:'"$filteringCmd" 245 elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 246 # File completion for directories only 247 local subdir 248 subdir="${completions[1]}" 249 if [ -n "$subdir" ]; then 250 __%[1]s_debug "Listing directories in $subdir" 251 pushd "${subdir}" >/dev/null 2>&1 252 else 253 __%[1]s_debug "Listing directories in ." 254 fi 255 256 local result 257 _arguments '*:dirname:_files -/'" ${flagPrefix}" 258 result=$? 259 if [ -n "$subdir" ]; then 260 popd >/dev/null 2>&1 261 fi 262 return $result 263 else 264 __%[1]s_debug "Calling _describe" 265 if eval _describe "completions" completions $flagPrefix $noSpace; then 266 __%[1]s_debug "_describe found some completions" 267 268 # Return the success of having called _describe 269 return 0 270 else 271 __%[1]s_debug "_describe did not find completions." 272 __%[1]s_debug "Checking if we should do file completion." 273 if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 274 __%[1]s_debug "deactivating file completion" 275 276 # We must return an error code here to let zsh know that there were no 277 # completions found by _describe; this is what will trigger other 278 # matching algorithms to attempt to find completions. 279 # For example zsh can match letters in the middle of words. 280 return 1 281 else 282 # Perform file completion 283 __%[1]s_debug "Activating file completion" 284 285 # We must return the result of this command, so it must be the 286 # last command, or else we must store its result to return it. 287 _arguments '*:filename:_files'" ${flagPrefix}" 288 fi 289 fi 290 fi 291 } 292 293 # don't run the completion function when being source-ed or eval-ed 294 if [ "$funcstack[1]" = "_%[1]s" ]; then 295 _%[1]s 296 fi 297 `, name, compCmd, 298 ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, 299 ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, 300 activeHelpMarker)) 301 }