bash_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	"sort"
 23	"strings"
 24
 25	"github.com/spf13/pflag"
 26)
 27
 28// Annotations for Bash completion.
 29const (
 30	BashCompFilenameExt     = "cobra_annotation_bash_completion_filename_extensions"
 31	BashCompCustom          = "cobra_annotation_bash_completion_custom"
 32	BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag"
 33	BashCompSubdirsInDir    = "cobra_annotation_bash_completion_subdirs_in_dir"
 34)
 35
 36func writePreamble(buf io.StringWriter, name string) {
 37	WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
 38	WriteStringAndCheck(buf, fmt.Sprintf(`
 39__%[1]s_debug()
 40{
 41    if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
 42        echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
 43    fi
 44}
 45
 46# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
 47# _init_completion. This is a very minimal version of that function.
 48__%[1]s_init_completion()
 49{
 50    COMPREPLY=()
 51    _get_comp_words_by_ref "$@" cur prev words cword
 52}
 53
 54__%[1]s_index_of_word()
 55{
 56    local w word=$1
 57    shift
 58    index=0
 59    for w in "$@"; do
 60        [[ $w = "$word" ]] && return
 61        index=$((index+1))
 62    done
 63    index=-1
 64}
 65
 66__%[1]s_contains_word()
 67{
 68    local w word=$1; shift
 69    for w in "$@"; do
 70        [[ $w = "$word" ]] && return
 71    done
 72    return 1
 73}
 74
 75__%[1]s_handle_go_custom_completion()
 76{
 77    __%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
 78
 79    local shellCompDirectiveError=%[3]d
 80    local shellCompDirectiveNoSpace=%[4]d
 81    local shellCompDirectiveNoFileComp=%[5]d
 82    local shellCompDirectiveFilterFileExt=%[6]d
 83    local shellCompDirectiveFilterDirs=%[7]d
 84
 85    local out requestComp lastParam lastChar comp directive args
 86
 87    # Prepare the command to request completions for the program.
 88    # Calling ${words[0]} instead of directly %[1]s allows handling aliases
 89    args=("${words[@]:1}")
 90    # Disable ActiveHelp which is not supported for bash completion v1
 91    requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}"
 92
 93    lastParam=${words[$((${#words[@]}-1))]}
 94    lastChar=${lastParam:$((${#lastParam}-1)):1}
 95    __%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
 96
 97    if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
 98        # If the last parameter is complete (there is a space following it)
 99        # We add an extra empty parameter so we can indicate this to the go method.
100        __%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter"
101        requestComp="${requestComp} \"\""
102    fi
103
104    __%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}"
105    # Use eval to handle any environment variables and such
106    out=$(eval "${requestComp}" 2>/dev/null)
107
108    # Extract the directive integer at the very end of the output following a colon (:)
109    directive=${out##*:}
110    # Remove the directive
111    out=${out%%:*}
112    if [ "${directive}" = "${out}" ]; then
113        # There is not directive specified
114        directive=0
115    fi
116    __%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
117    __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out}"
118
119    if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
120        # Error code.  No completion.
121        __%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code"
122        return
123    else
124        if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
125            if [[ $(type -t compopt) = "builtin" ]]; then
126                __%[1]s_debug "${FUNCNAME[0]}: activating no space"
127                compopt -o nospace
128            fi
129        fi
130        if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
131            if [[ $(type -t compopt) = "builtin" ]]; then
132                __%[1]s_debug "${FUNCNAME[0]}: activating no file completion"
133                compopt +o default
134            fi
135        fi
136    fi
137
138    if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
139        # File extension filtering
140        local fullFilter filter filteringCmd
141        # Do not use quotes around the $out variable or else newline
142        # characters will be kept.
143        for filter in ${out}; do
144            fullFilter+="$filter|"
145        done
146
147        filteringCmd="_filedir $fullFilter"
148        __%[1]s_debug "File filtering command: $filteringCmd"
149        $filteringCmd
150    elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
151        # File completion for directories only
152        local subdir
153        # Use printf to strip any trailing newline
154        subdir=$(printf "%%s" "${out}")
155        if [ -n "$subdir" ]; then
156            __%[1]s_debug "Listing directories in $subdir"
157            __%[1]s_handle_subdirs_in_dir_flag "$subdir"
158        else
159            __%[1]s_debug "Listing directories in ."
160            _filedir -d
161        fi
162    else
163        while IFS='' read -r comp; do
164            COMPREPLY+=("$comp")
165        done < <(compgen -W "${out}" -- "$cur")
166    fi
167}
168
169__%[1]s_handle_reply()
170{
171    __%[1]s_debug "${FUNCNAME[0]}"
172    local comp
173    case $cur in
174        -*)
175            if [[ $(type -t compopt) = "builtin" ]]; then
176                compopt -o nospace
177            fi
178            local allflags
179            if [ ${#must_have_one_flag[@]} -ne 0 ]; then
180                allflags=("${must_have_one_flag[@]}")
181            else
182                allflags=("${flags[*]} ${two_word_flags[*]}")
183            fi
184            while IFS='' read -r comp; do
185                COMPREPLY+=("$comp")
186            done < <(compgen -W "${allflags[*]}" -- "$cur")
187            if [[ $(type -t compopt) = "builtin" ]]; then
188                [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
189            fi
190
191            # complete after --flag=abc
192            if [[ $cur == *=* ]]; then
193                if [[ $(type -t compopt) = "builtin" ]]; then
194                    compopt +o nospace
195                fi
196
197                local index flag
198                flag="${cur%%=*}"
199                __%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}"
200                COMPREPLY=()
201                if [[ ${index} -ge 0 ]]; then
202                    PREFIX=""
203                    cur="${cur#*=}"
204                    ${flags_completion[${index}]}
205                    if [ -n "${ZSH_VERSION:-}" ]; then
206                        # zsh completion needs --flag= prefix
207                        eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
208                    fi
209                fi
210            fi
211
212            if [[ -z "${flag_parsing_disabled}" ]]; then
213                # If flag parsing is enabled, we have completed the flags and can return.
214                # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough
215                # to possibly call handle_go_custom_completion.
216                return 0;
217            fi
218            ;;
219    esac
220
221    # check if we are handling a flag with special work handling
222    local index
223    __%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}"
224    if [[ ${index} -ge 0 ]]; then
225        ${flags_completion[${index}]}
226        return
227    fi
228
229    # we are parsing a flag and don't have a special handler, no completion
230    if [[ ${cur} != "${words[cword]}" ]]; then
231        return
232    fi
233
234    local completions
235    completions=("${commands[@]}")
236    if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
237        completions+=("${must_have_one_noun[@]}")
238    elif [[ -n "${has_completion_function}" ]]; then
239        # if a go completion function is provided, defer to that function
240        __%[1]s_handle_go_custom_completion
241    fi
242    if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
243        completions+=("${must_have_one_flag[@]}")
244    fi
245    while IFS='' read -r comp; do
246        COMPREPLY+=("$comp")
247    done < <(compgen -W "${completions[*]}" -- "$cur")
248
249    if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
250        while IFS='' read -r comp; do
251            COMPREPLY+=("$comp")
252        done < <(compgen -W "${noun_aliases[*]}" -- "$cur")
253    fi
254
255    if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
256        if declare -F __%[1]s_custom_func >/dev/null; then
257            # try command name qualified custom func
258            __%[1]s_custom_func
259        else
260            # otherwise fall back to unqualified for compatibility
261            declare -F __custom_func >/dev/null && __custom_func
262        fi
263    fi
264
265    # available in bash-completion >= 2, not always present on macOS
266    if declare -F __ltrim_colon_completions >/dev/null; then
267        __ltrim_colon_completions "$cur"
268    fi
269
270    # If there is only 1 completion and it is a flag with an = it will be completed
271    # but we don't want a space after the =
272    if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
273       compopt -o nospace
274    fi
275}
276
277# The arguments should be in the form "ext1|ext2|extn"
278__%[1]s_handle_filename_extension_flag()
279{
280    local ext="$1"
281    _filedir "@(${ext})"
282}
283
284__%[1]s_handle_subdirs_in_dir_flag()
285{
286    local dir="$1"
287    pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
288}
289
290__%[1]s_handle_flag()
291{
292    __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
293
294    # if a command required a flag, and we found it, unset must_have_one_flag()
295    local flagname=${words[c]}
296    local flagvalue=""
297    # if the word contained an =
298    if [[ ${words[c]} == *"="* ]]; then
299        flagvalue=${flagname#*=} # take in as flagvalue after the =
300        flagname=${flagname%%=*} # strip everything after the =
301        flagname="${flagname}=" # but put the = back
302    fi
303    __%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}"
304    if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
305        must_have_one_flag=()
306    fi
307
308    # if you set a flag which only applies to this command, don't show subcommands
309    if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
310      commands=()
311    fi
312
313    # keep flag value with flagname as flaghash
314    # flaghash variable is an associative array which is only supported in bash > 3.
315    if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
316        if [ -n "${flagvalue}" ] ; then
317            flaghash[${flagname}]=${flagvalue}
318        elif [ -n "${words[ $((c+1)) ]}" ] ; then
319            flaghash[${flagname}]=${words[ $((c+1)) ]}
320        else
321            flaghash[${flagname}]="true" # pad "true" for bool flag
322        fi
323    fi
324
325    # skip the argument to a two word flag
326    if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then
327        __%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
328        c=$((c+1))
329        # if we are looking for a flags value, don't show commands
330        if [[ $c -eq $cword ]]; then
331            commands=()
332        fi
333    fi
334
335    c=$((c+1))
336
337}
338
339__%[1]s_handle_noun()
340{
341    __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
342
343    if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
344        must_have_one_noun=()
345    elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then
346        must_have_one_noun=()
347    fi
348
349    nouns+=("${words[c]}")
350    c=$((c+1))
351}
352
353__%[1]s_handle_command()
354{
355    __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
356
357    local next_command
358    if [[ -n ${last_command} ]]; then
359        next_command="_${last_command}_${words[c]//:/__}"
360    else
361        if [[ $c -eq 0 ]]; then
362            next_command="_%[1]s_root_command"
363        else
364            next_command="_${words[c]//:/__}"
365        fi
366    fi
367    c=$((c+1))
368    __%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}"
369    declare -F "$next_command" >/dev/null && $next_command
370}
371
372__%[1]s_handle_word()
373{
374    if [[ $c -ge $cword ]]; then
375        __%[1]s_handle_reply
376        return
377    fi
378    __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
379    if [[ "${words[c]}" == -* ]]; then
380        __%[1]s_handle_flag
381    elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then
382        __%[1]s_handle_command
383    elif [[ $c -eq 0 ]]; then
384        __%[1]s_handle_command
385    elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then
386        # aliashash variable is an associative array which is only supported in bash > 3.
387        if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
388            words[c]=${aliashash[${words[c]}]}
389            __%[1]s_handle_command
390        else
391            __%[1]s_handle_noun
392        fi
393    else
394        __%[1]s_handle_noun
395    fi
396    __%[1]s_handle_word
397}
398
399`, name, ShellCompNoDescRequestCmd,
400		ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
401		ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
402}
403
404func writePostscript(buf io.StringWriter, name string) {
405	name = strings.ReplaceAll(name, ":", "__")
406	WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name))
407	WriteStringAndCheck(buf, fmt.Sprintf(`{
408    local cur prev words cword split
409    declare -A flaghash 2>/dev/null || :
410    declare -A aliashash 2>/dev/null || :
411    if declare -F _init_completion >/dev/null 2>&1; then
412        _init_completion -s || return
413    else
414        __%[1]s_init_completion -n "=" || return
415    fi
416
417    local c=0
418    local flag_parsing_disabled=
419    local flags=()
420    local two_word_flags=()
421    local local_nonpersistent_flags=()
422    local flags_with_completion=()
423    local flags_completion=()
424    local commands=("%[1]s")
425    local command_aliases=()
426    local must_have_one_flag=()
427    local must_have_one_noun=()
428    local has_completion_function=""
429    local last_command=""
430    local nouns=()
431    local noun_aliases=()
432
433    __%[1]s_handle_word
434}
435
436`, name))
437	WriteStringAndCheck(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then
438    complete -o default -F __start_%s %s
439else
440    complete -o default -o nospace -F __start_%s %s
441fi
442
443`, name, name, name, name))
444	WriteStringAndCheck(buf, "# ex: ts=4 sw=4 et filetype=sh\n")
445}
446
447func writeCommands(buf io.StringWriter, cmd *Command) {
448	WriteStringAndCheck(buf, "    commands=()\n")
449	for _, c := range cmd.Commands() {
450		if !c.IsAvailableCommand() && c != cmd.helpCommand {
451			continue
452		}
453		WriteStringAndCheck(buf, fmt.Sprintf("    commands+=(%q)\n", c.Name()))
454		writeCmdAliases(buf, c)
455	}
456	WriteStringAndCheck(buf, "\n")
457}
458
459func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *Command) {
460	for key, value := range annotations {
461		switch key {
462		case BashCompFilenameExt:
463			WriteStringAndCheck(buf, fmt.Sprintf("    flags_with_completion+=(%q)\n", name))
464
465			var ext string
466			if len(value) > 0 {
467				ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Root().Name()) + strings.Join(value, "|")
468			} else {
469				ext = "_filedir"
470			}
471			WriteStringAndCheck(buf, fmt.Sprintf("    flags_completion+=(%q)\n", ext))
472		case BashCompCustom:
473			WriteStringAndCheck(buf, fmt.Sprintf("    flags_with_completion+=(%q)\n", name))
474
475			if len(value) > 0 {
476				handlers := strings.Join(value, "; ")
477				WriteStringAndCheck(buf, fmt.Sprintf("    flags_completion+=(%q)\n", handlers))
478			} else {
479				WriteStringAndCheck(buf, "    flags_completion+=(:)\n")
480			}
481		case BashCompSubdirsInDir:
482			WriteStringAndCheck(buf, fmt.Sprintf("    flags_with_completion+=(%q)\n", name))
483
484			var ext string
485			if len(value) == 1 {
486				ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Root().Name()) + value[0]
487			} else {
488				ext = "_filedir -d"
489			}
490			WriteStringAndCheck(buf, fmt.Sprintf("    flags_completion+=(%q)\n", ext))
491		}
492	}
493}
494
495const cbn = "\")\n"
496
497func writeShortFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) {
498	name := flag.Shorthand
499	format := "    "
500	if len(flag.NoOptDefVal) == 0 {
501		format += "two_word_"
502	}
503	format += "flags+=(\"-%s" + cbn
504	WriteStringAndCheck(buf, fmt.Sprintf(format, name))
505	writeFlagHandler(buf, "-"+name, flag.Annotations, cmd)
506}
507
508func writeFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) {
509	name := flag.Name
510	format := "    flags+=(\"--%s"
511	if len(flag.NoOptDefVal) == 0 {
512		format += "="
513	}
514	format += cbn
515	WriteStringAndCheck(buf, fmt.Sprintf(format, name))
516	if len(flag.NoOptDefVal) == 0 {
517		format = "    two_word_flags+=(\"--%s" + cbn
518		WriteStringAndCheck(buf, fmt.Sprintf(format, name))
519	}
520	writeFlagHandler(buf, "--"+name, flag.Annotations, cmd)
521}
522
523func writeLocalNonPersistentFlag(buf io.StringWriter, flag *pflag.Flag) {
524	name := flag.Name
525	format := "    local_nonpersistent_flags+=(\"--%[1]s" + cbn
526	if len(flag.NoOptDefVal) == 0 {
527		format += "    local_nonpersistent_flags+=(\"--%[1]s=" + cbn
528	}
529	WriteStringAndCheck(buf, fmt.Sprintf(format, name))
530	if len(flag.Shorthand) > 0 {
531		WriteStringAndCheck(buf, fmt.Sprintf("    local_nonpersistent_flags+=(\"-%s\")\n", flag.Shorthand))
532	}
533}
534
535// prepareCustomAnnotationsForFlags setup annotations for go completions for registered flags
536func prepareCustomAnnotationsForFlags(cmd *Command) {
537	flagCompletionMutex.RLock()
538	defer flagCompletionMutex.RUnlock()
539	for flag := range flagCompletionFunctions {
540		// Make sure the completion script calls the __*_go_custom_completion function for
541		// every registered flag.  We need to do this here (and not when the flag was registered
542		// for completion) so that we can know the root command name for the prefix
543		// of __<prefix>_go_custom_completion
544		if flag.Annotations == nil {
545			flag.Annotations = map[string][]string{}
546		}
547		flag.Annotations[BashCompCustom] = []string{fmt.Sprintf("__%[1]s_handle_go_custom_completion", cmd.Root().Name())}
548	}
549}
550
551func writeFlags(buf io.StringWriter, cmd *Command) {
552	prepareCustomAnnotationsForFlags(cmd)
553	WriteStringAndCheck(buf, `    flags=()
554    two_word_flags=()
555    local_nonpersistent_flags=()
556    flags_with_completion=()
557    flags_completion=()
558
559`)
560
561	if cmd.DisableFlagParsing {
562		WriteStringAndCheck(buf, "    flag_parsing_disabled=1\n")
563	}
564
565	localNonPersistentFlags := cmd.LocalNonPersistentFlags()
566	cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
567		if nonCompletableFlag(flag) {
568			return
569		}
570		writeFlag(buf, flag, cmd)
571		if len(flag.Shorthand) > 0 {
572			writeShortFlag(buf, flag, cmd)
573		}
574		// localNonPersistentFlags are used to stop the completion of subcommands when one is set
575		// if TraverseChildren is true we should allow to complete subcommands
576		if localNonPersistentFlags.Lookup(flag.Name) != nil && !cmd.Root().TraverseChildren {
577			writeLocalNonPersistentFlag(buf, flag)
578		}
579	})
580	cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
581		if nonCompletableFlag(flag) {
582			return
583		}
584		writeFlag(buf, flag, cmd)
585		if len(flag.Shorthand) > 0 {
586			writeShortFlag(buf, flag, cmd)
587		}
588	})
589
590	WriteStringAndCheck(buf, "\n")
591}
592
593func writeRequiredFlag(buf io.StringWriter, cmd *Command) {
594	WriteStringAndCheck(buf, "    must_have_one_flag=()\n")
595	flags := cmd.NonInheritedFlags()
596	flags.VisitAll(func(flag *pflag.Flag) {
597		if nonCompletableFlag(flag) {
598			return
599		}
600		if _, ok := flag.Annotations[BashCompOneRequiredFlag]; ok {
601			format := "    must_have_one_flag+=(\"--%s"
602			if flag.Value.Type() != "bool" {
603				format += "="
604			}
605			format += cbn
606			WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name))
607
608			if len(flag.Shorthand) > 0 {
609				WriteStringAndCheck(buf, fmt.Sprintf("    must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand))
610			}
611		}
612	})
613}
614
615func writeRequiredNouns(buf io.StringWriter, cmd *Command) {
616	WriteStringAndCheck(buf, "    must_have_one_noun=()\n")
617	sort.Strings(cmd.ValidArgs)
618	for _, value := range cmd.ValidArgs {
619		// Remove any description that may be included following a tab character.
620		// Descriptions are not supported by bash completion.
621		value = strings.SplitN(value, "\t", 2)[0]
622		WriteStringAndCheck(buf, fmt.Sprintf("    must_have_one_noun+=(%q)\n", value))
623	}
624	if cmd.ValidArgsFunction != nil {
625		WriteStringAndCheck(buf, "    has_completion_function=1\n")
626	}
627}
628
629func writeCmdAliases(buf io.StringWriter, cmd *Command) {
630	if len(cmd.Aliases) == 0 {
631		return
632	}
633
634	sort.Strings(cmd.Aliases)
635
636	WriteStringAndCheck(buf, fmt.Sprint(`    if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n"))
637	for _, value := range cmd.Aliases {
638		WriteStringAndCheck(buf, fmt.Sprintf("        command_aliases+=(%q)\n", value))
639		WriteStringAndCheck(buf, fmt.Sprintf("        aliashash[%q]=%q\n", value, cmd.Name()))
640	}
641	WriteStringAndCheck(buf, `    fi`)
642	WriteStringAndCheck(buf, "\n")
643}
644func writeArgAliases(buf io.StringWriter, cmd *Command) {
645	WriteStringAndCheck(buf, "    noun_aliases=()\n")
646	sort.Strings(cmd.ArgAliases)
647	for _, value := range cmd.ArgAliases {
648		WriteStringAndCheck(buf, fmt.Sprintf("    noun_aliases+=(%q)\n", value))
649	}
650}
651
652func gen(buf io.StringWriter, cmd *Command) {
653	for _, c := range cmd.Commands() {
654		if !c.IsAvailableCommand() && c != cmd.helpCommand {
655			continue
656		}
657		gen(buf, c)
658	}
659	commandName := cmd.CommandPath()
660	commandName = strings.ReplaceAll(commandName, " ", "_")
661	commandName = strings.ReplaceAll(commandName, ":", "__")
662
663	if cmd.Root() == cmd {
664		WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName))
665	} else {
666		WriteStringAndCheck(buf, fmt.Sprintf("_%s()\n{\n", commandName))
667	}
668
669	WriteStringAndCheck(buf, fmt.Sprintf("    last_command=%q\n", commandName))
670	WriteStringAndCheck(buf, "\n")
671	WriteStringAndCheck(buf, "    command_aliases=()\n")
672	WriteStringAndCheck(buf, "\n")
673
674	writeCommands(buf, cmd)
675	writeFlags(buf, cmd)
676	writeRequiredFlag(buf, cmd)
677	writeRequiredNouns(buf, cmd)
678	writeArgAliases(buf, cmd)
679	WriteStringAndCheck(buf, "}\n\n")
680}
681
682// GenBashCompletion generates bash completion file and writes to the passed writer.
683func (c *Command) GenBashCompletion(w io.Writer) error {
684	buf := new(bytes.Buffer)
685	writePreamble(buf, c.Name())
686	if len(c.BashCompletionFunction) > 0 {
687		buf.WriteString(c.BashCompletionFunction + "\n")
688	}
689	gen(buf, c)
690	writePostscript(buf, c.Name())
691
692	_, err := buf.WriteTo(w)
693	return err
694}
695
696func nonCompletableFlag(flag *pflag.Flag) bool {
697	return flag.Hidden || len(flag.Deprecated) > 0
698}
699
700// GenBashCompletionFile generates bash completion file.
701func (c *Command) GenBashCompletionFile(filename string) error {
702	outFile, err := os.Create(filename)
703	if err != nil {
704		return err
705	}
706	defer outFile.Close()
707
708	return c.GenBashCompletion(outFile)
709}