zsh_completions.go

  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
 15package cobra
 16
 17import (
 18	"bytes"
 19	"fmt"
 20	"io"
 21	"os"
 22)
 23
 24// GenZshCompletionFile generates zsh completion file including descriptions.
 25func (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.
 31func (c *Command) GenZshCompletion(w io.Writer) error {
 32	return c.genZshCompletion(w, true)
 33}
 34
 35// GenZshCompletionFileNoDesc generates zsh completion file without descriptions.
 36func (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.
 42func (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
 55func (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
 66func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error {
 67	return nil
 68}
 69
 70func (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
 80func (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
 87func 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
 93compdef _%[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
301if [ "$funcstack[1]" = "_%[1]s" ]; then
302    _%[1]s
303fi
304`, name, compCmd,
305		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
306		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder,
307		activeHelpMarker))
308}