1package cobra
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"os"
8	"strings"
9)
10
11func genFishComp(buf io.StringWriter, name string, includeDesc bool) {
12	// Variables should not contain a '-' or ':' character
13	nameForVar := name
14	nameForVar = strings.Replace(nameForVar, "-", "_", -1)
15	nameForVar = strings.Replace(nameForVar, ":", "_", -1)
16
17	compCmd := ShellCompRequestCmd
18	if !includeDesc {
19		compCmd = ShellCompNoDescRequestCmd
20	}
21	WriteStringAndCheck(buf, fmt.Sprintf("# fish completion for %-36s -*- shell-script -*-\n", name))
22	WriteStringAndCheck(buf, fmt.Sprintf(`
23function __%[1]s_debug
24    set -l file "$BASH_COMP_DEBUG_FILE"
25    if test -n "$file"
26        echo "$argv" >> $file
27    end
28end
29
30function __%[1]s_perform_completion
31    __%[1]s_debug "Starting __%[1]s_perform_completion"
32
33    # Extract all args except the last one
34    set -l args (commandline -opc)
35    # Extract the last arg and escape it in case it is a space
36    set -l lastArg (string escape -- (commandline -ct))
37
38    __%[1]s_debug "args: $args"
39    __%[1]s_debug "last arg: $lastArg"
40
41    set -l requestComp "$args[1] %[3]s $args[2..-1] $lastArg"
42
43    __%[1]s_debug "Calling $requestComp"
44    set -l results (eval $requestComp 2> /dev/null)
45
46    # Some programs may output extra empty lines after the directive.
47    # Let's ignore them or else it will break completion.
48    # Ref: https://github.com/spf13/cobra/issues/1279
49    for line in $results[-1..1]
50        if test (string trim -- $line) = ""
51            # Found an empty line, remove it
52            set results $results[1..-2]
53        else
54            # Found non-empty line, we have our proper output
55            break
56        end
57    end
58
59    set -l comps $results[1..-2]
60    set -l directiveLine $results[-1]
61
62    # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
63    # completions must be prefixed with the flag
64    set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
65
66    __%[1]s_debug "Comps: $comps"
67    __%[1]s_debug "DirectiveLine: $directiveLine"
68    __%[1]s_debug "flagPrefix: $flagPrefix"
69
70    for comp in $comps
71        printf "%%s%%s\n" "$flagPrefix" "$comp"
72    end
73
74    printf "%%s\n" "$directiveLine"
75end
76
77# This function does two things:
78# - Obtain the completions and store them in the global __%[1]s_comp_results
79# - Return false if file completion should be performed
80function __%[1]s_prepare_completions
81    __%[1]s_debug ""
82    __%[1]s_debug "========= starting completion logic =========="
83
84    # Start fresh
85    set --erase __%[1]s_comp_results
86
87    set -l results (__%[1]s_perform_completion)
88    __%[1]s_debug "Completion results: $results"
89
90    if test -z "$results"
91        __%[1]s_debug "No completion, probably due to a failure"
92        # Might as well do file completion, in case it helps
93        return 1
94    end
95
96    set -l directive (string sub --start 2 $results[-1])
97    set --global __%[1]s_comp_results $results[1..-2]
98
99    __%[1]s_debug "Completions are: $__%[1]s_comp_results"
100    __%[1]s_debug "Directive is: $directive"
101
102    set -l shellCompDirectiveError %[4]d
103    set -l shellCompDirectiveNoSpace %[5]d
104    set -l shellCompDirectiveNoFileComp %[6]d
105    set -l shellCompDirectiveFilterFileExt %[7]d
106    set -l shellCompDirectiveFilterDirs %[8]d
107
108    if test -z "$directive"
109        set directive 0
110    end
111
112    set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2)
113    if test $compErr -eq 1
114        __%[1]s_debug "Received error directive: aborting."
115        # Might as well do file completion, in case it helps
116        return 1
117    end
118
119    set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2)
120    set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2)
121    if test $filefilter -eq 1; or test $dirfilter -eq 1
122        __%[1]s_debug "File extension filtering or directory filtering not supported"
123        # Do full file completion instead
124        return 1
125    end
126
127    set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2)
128    set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2)
129
130    __%[1]s_debug "nospace: $nospace, nofiles: $nofiles"
131
132    # If we want to prevent a space, or if file completion is NOT disabled,
133    # we need to count the number of valid completions.
134    # To do so, we will filter on prefix as the completions we have received
135    # may not already be filtered so as to allow fish to match on different
136    # criteria than the prefix.
137    if test $nospace -ne 0; or test $nofiles -eq 0
138        set -l prefix (commandline -t | string escape --style=regex)
139        __%[1]s_debug "prefix: $prefix"
140
141        set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results)
142        set --global __%[1]s_comp_results $completions
143        __%[1]s_debug "Filtered completions are: $__%[1]s_comp_results"
144
145        # Important not to quote the variable for count to work
146        set -l numComps (count $__%[1]s_comp_results)
147        __%[1]s_debug "numComps: $numComps"
148
149        if test $numComps -eq 1; and test $nospace -ne 0
150            # We must first split on \t to get rid of the descriptions to be
151            # able to check what the actual completion will be.
152            # We don't need descriptions anyway since there is only a single
153            # real completion which the shell will expand immediately.
154            set -l split (string split --max 1 \t $__%[1]s_comp_results[1])
155
156            # Fish won't add a space if the completion ends with any
157            # of the following characters: @=/:.,
158            set -l lastChar (string sub -s -1 -- $split)
159            if not string match -r -q "[@=/:.,]" -- "$lastChar"
160                # In other cases, to support the "nospace" directive we trick the shell
161                # by outputting an extra, longer completion.
162                __%[1]s_debug "Adding second completion to perform nospace directive"
163                set --global __%[1]s_comp_results $split[1] $split[1].
164                __%[1]s_debug "Completions are now: $__%[1]s_comp_results"
165            end
166        end
167
168        if test $numComps -eq 0; and test $nofiles -eq 0
169            # To be consistent with bash and zsh, we only trigger file
170            # completion when there are no other completions
171            __%[1]s_debug "Requesting file completion"
172            return 1
173        end
174    end
175
176    return 0
177end
178
179# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
180# so we can properly delete any completions provided by another script.
181# Only do this if the program can be found, or else fish may print some errors; besides,
182# the existing completions will only be loaded if the program can be found.
183if type -q "%[2]s"
184    # The space after the program name is essential to trigger completion for the program
185    # and not completion of the program name itself.
186    # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
187    complete --do-complete "%[2]s " > /dev/null 2>&1
188end
189
190# Remove any pre-existing completions for the program since we will be handling all of them.
191complete -c %[2]s -e
192
193# The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results
194# which provides the program's completion choices.
195complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results'
196
197`, nameForVar, name, compCmd,
198		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
199		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs))
200}
201
202// GenFishCompletion generates fish completion file and writes to the passed writer.
203func (c *Command) GenFishCompletion(w io.Writer, includeDesc bool) error {
204	buf := new(bytes.Buffer)
205	genFishComp(buf, c.Name(), includeDesc)
206	_, err := buf.WriteTo(w)
207	return err
208}
209
210// GenFishCompletionFile generates fish completion file.
211func (c *Command) GenFishCompletionFile(filename string, includeDesc bool) error {
212	outFile, err := os.Create(filename)
213	if err != nil {
214		return err
215	}
216	defer outFile.Close()
217
218	return c.GenFishCompletion(outFile, includeDesc)
219}
220