diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:03:19 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:03:19 +0000 |
commit | 6c09f2a45c5541e9c207d14fc7aa21a4a0066bde (patch) | |
tree | 0221189d367bf661f6f9493c4f17a03f0dd4b7d2 /bash_completion | |
parent | Releasing progress-linux version 1:2.11-8~progress7.99u1. (diff) | |
download | bash-completion-6c09f2a45c5541e9c207d14fc7aa21a4a0066bde.tar.xz bash-completion-6c09f2a45c5541e9c207d14fc7aa21a4a0066bde.zip |
Merging upstream version 1:2.12.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'bash_completion')
-rw-r--r-- | bash_completion | 3211 |
1 files changed, 2147 insertions, 1064 deletions
diff --git a/bash_completion b/bash_completion index 1a7f563..e245cc5 100644 --- a/bash_completion +++ b/bash_completion @@ -23,12 +23,16 @@ # # https://github.com/scop/bash-completion -BASH_COMPLETION_VERSINFO=(2 11) +BASH_COMPLETION_VERSINFO=( + 2 # x-release-please-major + 12 # x-release-please-minor + 0 # x-release-please-patch +) if [[ $- == *v* ]]; then - BASH_COMPLETION_ORIGINAL_V_VALUE="-v" + _comp__init_original_set_v="-v" else - BASH_COMPLETION_ORIGINAL_V_VALUE="+v" + _comp__init_original_set_v="+v" fi if [[ ${BASH_COMPLETION_DEBUG-} ]]; then @@ -37,13 +41,60 @@ else set +v fi -# Blacklisted completions, causing problems with our code. -# -_blacklist_glob='@(acroread.sh)' - # Turn on extended globbing and programmable completion shopt -s extglob progcomp +# Declare a compatibility function name +# @param $1 Version of bash-completion where the deprecation occurred +# @param $2 Old function name +# @param $3 New function name +# @since 2.12 +_comp_deprecate_func() +{ + if (($# != 3)); then + printf 'bash_completion: %s: usage: %s DEPRECATION_VERSION OLD_NAME NEW_NAME\n' "$FUNCNAME" "$FUNCNAME" + return 2 + fi + if [[ $2 != [a-zA-Z_]*([a-zA-Z_0-9]) ]]; then + printf 'bash_completion: %s: %s\n' "$FUNCNAME" "\$2: invalid function name '$1'" >&2 + return 2 + elif [[ $3 != [a-zA-Z_]*([a-zA-Z_0-9]) ]]; then + printf 'bash_completion: %s: %s\n' "$FUNCNAME" "\$3: invalid function name '$2'" >&2 + return 2 + fi + eval -- "$2() { $3 \"\$@\"; }" +} + +# Declare a compatibility variable name. +# For bash 4.3+, a real name alias is created, allowing value changes to +# "apply through" when the variables are set later. For bash versions earlier +# than that, the operation is once-only; the value of the new variable +# (if it's unset) is set to that of the old (if set) at call time. +# +# @param $1 Version of bash-completion where the deprecation occurred +# @param $2 Old variable name +# @param $3 New variable name +# @since 2.12 +_comp_deprecate_var() +{ + if (($# != 3)); then + printf 'bash_completion: %s: usage: %s DEPRECATION_VERSION OLD_NAME NEW_NAME\n' "$FUNCNAME" "$FUNCNAME" + return 2 + fi + if [[ $2 != [a-zA-Z_]*([a-zA-Z_0-9]) ]]; then + printf 'bash_completion: %s: %s\n' "$FUNCNAME" "\$2: invalid variable name '$1'" >&2 + return 2 + elif [[ $3 != [a-zA-Z_]*([a-zA-Z_0-9]) ]]; then + printf 'bash_completion: %s: %s\n' "$FUNCNAME" "\$3: invalid variable name '$2'" >&2 + return 2 + fi + if ((BASH_VERSINFO[0] >= 5 || BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 3)); then + eval "declare -gn $2=$3" + elif [[ -v $2 && ! -v $3 ]]; then + printf -v "$3" %s "$2" + fi +} + # A lot of the following one-liners were taken directly from the # completion examples provided with the bash 2.04 source distribution @@ -67,9 +118,6 @@ complete -A setopt set # shopt completes with shopt options complete -A shopt shopt -# helptopics -complete -A helptopic help - # unalias completes with aliases complete -a unalias @@ -83,7 +131,8 @@ complete -b builtin # Check if we're running on the given userland # @param $1 userland to check for -_userland() +# @since 2.12 +_comp_userland() { local userland=$(uname -s) [[ $userland == @(Linux|GNU/*) ]] && userland=GNU @@ -92,94 +141,135 @@ _userland() # This function sets correct SysV init directories # -_sysvdirs() +# @since 2.12 +_comp_sysvdirs() { sysvdirs=() [[ -d /etc/rc.d/init.d ]] && sysvdirs+=(/etc/rc.d/init.d) [[ -d /etc/init.d ]] && sysvdirs+=(/etc/init.d) # Slackware uses /etc/rc.d [[ -f /etc/slackware-version ]] && sysvdirs=(/etc/rc.d) - return 0 + ((${#sysvdirs[@]})) } # This function checks whether we have a given program on the system. # -_have() +# @since 2.12 +_comp_have_command() { # Completions for system administrator commands are installed as well in # case completion is attempted via `sudo command ...'. - PATH=$PATH:/usr/sbin:/sbin:/usr/local/sbin type $1 &>/dev/null -} - -# Backwards compatibility for compat completions that use have(). -# @deprecated should no longer be used; generally not needed with dynamically -# loaded completions, and _have is suitable for runtime use. -have() -{ - unset -v have - _have $1 && have=yes + PATH=$PATH:/usr/sbin:/sbin:/usr/local/sbin type "$1" &>/dev/null } # This function checks whether a given readline variable # is `on'. # -_rl_enabled() +# @since 2.12 +_comp_readline_variable_on() { - [[ "$(bind -v)" == *$1+([[:space:]])on* ]] + [[ $(bind -v) == *$1+([[:space:]])on* ]] } # This function shell-quotes the argument -quote() +# @param $1 String to be quoted +# @var[out] REPLY Resulting string +# @since 2.12 +_comp_quote() { - local quoted=${1//\'/\'\\\'\'} - printf "'%s'" "$quoted" + REPLY=\'${1//\'/\'\\\'\'}\' } -# @see _quote_readline_by_ref() -quote_readline() -{ - local ret - _quote_readline_by_ref "$1" ret - printf %s "$ret" -} # quote_readline() - -# This function shell-dequotes the argument -dequote() +# shellcheck disable=SC1003 +_comp_dequote__initialize() { - eval printf %s "$1" 2>/dev/null + unset -f "$FUNCNAME" + local regex_param='\$([_a-zA-Z][_a-zA-Z0-9]*|[-*@#?$!0-9_])|\$\{[!#]?([_a-zA-Z][_a-zA-Z0-9]*(\[([0-9]+|[*@])\])?|[-*@#?$!0-9_])\}' + local regex_quoted='\\.|'\''[^'\'']*'\''|\$?"([^\"$`!]|'$regex_param'|\\.)*"|\$'\''([^\'\'']|\\.)*'\''' + _comp_dequote__regex_safe_word='^([^\'\''"$`;&|<>()!]|'$regex_quoted'|'$regex_param')*$' +} +_comp_dequote__initialize + +# This function expands a word using `eval` in a safe way. This function can +# be typically used to get the expanded value of `${word[i]}` as +# `_comp_dequote "${word[i]}"`. When the word contains unquoted shell special +# characters, command substitutions, and other unsafe strings, the function +# call fails before applying `eval`. Otherwise, `eval` is applied to the +# string to generate the result. +# +# @param $1 String to be expanded. A safe word consists of the following +# sequence of substrings: +# +# - Shell non-special characters: [^\'"$`;&|<>()!]. +# - Parameter expansions of the forms $PARAM, ${!PARAM}, +# ${#PARAM}, ${NAME[INDEX]}, ${!NAME[INDEX]}, ${#NAME[INDEX]} +# where INDEX is an integer, `*` or `@`, NAME is a valid +# variable name [_a-zA-Z][_a-zA-Z0-9]*, and PARAM is NAME or a +# parameter [-*@#?$!0-9_]. +# - Quotes \?, '...', "...", $'...', and $"...". In the double +# quotations, parameter expansions are allowed. +# +# @var[out] REPLY Array that contains the expanded results. Multiple words or +# no words may be generated through pathname expansions. +# +# Note: This function allows parameter expansions as safe strings, which might +# cause unexpected results: +# +# * This allows execution of arbitrary commands through extra expansions of +# array subscripts in name references. For example, +# +# declare -n v='dummy[$(echo xxx >/dev/tty)]' +# echo "$v" # This line executes the command 'echo xxx'. +# _comp_dequote '"$v"' # This line also executes it. +# +# * This may change the internal state of the variable that has side effects. +# For example, the state of the random number generator of RANDOM can change: +# +# RANDOM=1234 # Set seed +# echo "$RANDOM" # This produces 30658. +# RANDOM=1234 # Reset seed +# _comp_dequote '"$RANDOM"' # This line changes the internal state. +# echo "$RANDOM" # This fails to reproduce 30658. +# +# We allow these parameter expansions as a part of safe strings assuming the +# referential transparency of the simple parameter expansions and the sane +# setup of the variables by the user or other frameworks that the user loads. +# @since 2.12 +_comp_dequote() +{ + REPLY=() # fallback value for unsafe word and failglob + [[ $1 =~ $_comp_dequote__regex_safe_word ]] || return 1 + eval "REPLY=($1)" 2>/dev/null # may produce failglob } -# Assign variable one scope above the caller -# Usage: local "$1" && _upvar $1 "value(s)" -# Param: $1 Variable name to assign value to -# Param: $* Value(s) to assign. If multiple values, an array is -# assigned, otherwise a single value is assigned. -# NOTE: For assigning multiple variables, use '_upvars'. Do NOT -# use multiple '_upvar' calls, since one '_upvar' call might -# reassign a variable to be used by another '_upvar' call. -# See: https://fvue.nl/wiki/Bash:_Passing_variables_by_reference -_upvar() -{ - echo "bash_completion: $FUNCNAME: deprecated function," \ - "use _upvars instead" >&2 - if unset -v "$1"; then # Unset & validate varname - if (($# == 2)); then - eval $1=\"\$2\" # Return single value - else - eval $1=\(\"\$"{@:2}"\"\) # Return array - fi +# Unset the given variables across a scope boundary. Useful for unshadowing +# global scoped variables. Note that simply calling unset on a local variable +# will not unshadow the global variable. Rather, the result will be a local +# variable in an unset state. +# Usage: local IFS='|'; _comp_unlocal IFS +# @param $* Variable names to be unset +# @since 2.12 +_comp_unlocal() +{ + if ((BASH_VERSINFO[0] >= 5)) && shopt -q localvar_unset; then + shopt -u localvar_unset + unset -v "$@" + shopt -s localvar_unset + else + unset -v "$@" fi } # Assign variables one scope above the caller # Usage: local varname [varname ...] && -# _upvars [-v varname value] | [-aN varname [value ...]] ... +# _comp_upvars [-v varname value] | [-aN varname [value ...]] ... # Available OPTIONS: # -aN Assign next N values to varname as array # -v Assign single value to varname -# Return: 1 if error occurs -# See: https://fvue.nl/wiki/Bash:_Passing_variables_by_reference -_upvars() +# @return 1 if error occurs +# @see https://fvue.nl/wiki/Bash:_Passing_variables_by_reference +# @since 2.12 +_comp_upvars() { if ! (($#)); then echo "bash_completion: $FUNCNAME: usage: $FUNCNAME" \ @@ -201,7 +291,8 @@ _upvars() return 1 } # Assign array of -aN elements - [[ "$2" ]] && unset -v "$2" && eval $2=\(\"\$"{@:3:${1#-a}}"\"\) && + # shellcheck disable=SC2015,SC2140 # TODO + [[ $2 ]] && unset -v "$2" && eval "$2"=\(\"\$"{@:3:${1#-a}}"\"\) && shift $((${1#-a} + 2)) || { echo bash_completion: \ "$FUNCNAME: \`$1${2+ }$2': missing argument(s)" \ @@ -211,7 +302,8 @@ _upvars() ;; -v) # Assign single value - [[ "$2" ]] && unset -v "$2" && eval $2=\"\$3\" && + # shellcheck disable=SC2015 # TODO + [[ $2 ]] && unset -v "$2" && eval "$2"=\"\$3\" && shift 3 || { echo "bash_completion: $FUNCNAME: $1:" \ "missing argument(s)" >&2 @@ -226,6 +318,482 @@ _upvars() done } +# Get the list of filenames that match with the specified glob pattern. +# This function does the globbing in a controlled environment, avoiding +# interference from user's shell options/settings or environment variables. +# @param $1 array_name Array name +# The array name should not start with an underscore "_", which is internally +# used. The array name should not be "GLOBIGNORE". +# @param $2 pattern Pattern string to be evaluated. +# This pattern string will be evaluated using "eval", so brace expansions, +# parameter expansions, command substitutions, and other expansions will be +# processed. The user-provided strings should not be directly specified to +# this argument. +# @return 0 if at least one path is generated, 1 if no path is generated, or 2 +# if the usage is incorrect. +# @since 2.12 +_comp_expand_glob() +{ + if (($# != 2)); then + printf 'bash-completion: %s: unexpected number of arguments\n' "$FUNCNAME" >&2 + printf 'usage: %s ARRAY_NAME PATTERN\n' "$FUNCNAME" >&2 + return 2 + elif [[ $1 == @(GLOBIGNORE|_*|*[^_a-zA-Z0-9]*|[0-9]*|'') ]]; then + printf 'bash-completion: %s: invalid array name "%s"\n' "$FUNCNAME" "$1" >&2 + return 2 + fi + + # Save and adjust the settings. + local _original_opts=$SHELLOPTS:$BASHOPTS + set +o noglob + shopt -s nullglob + shopt -u failglob dotglob + + # Also the user's GLOBIGNORE may affect the result of pathname expansions. + local GLOBIGNORE= + + eval -- "$1=()" # a fallback in case that the next line fails. + eval -- "$1=($2)" + + # Restore the settings. Note: Changing GLOBIGNORE affects the state of + # "shopt -q dotglob", so we need to explicitly restore the original state + # of "shopt -q dotglob". + _comp_unlocal GLOBIGNORE + if [[ :$_original_opts: == *:dotglob:* ]]; then + shopt -s dotglob + else + shopt -u dotglob + fi + [[ :$_original_opts: == *:nullglob:* ]] || shopt -u nullglob + [[ :$_original_opts: == *:failglob:* ]] && shopt -s failglob + [[ :$_original_opts: == *:noglob:* ]] && set -o noglob + eval "((\${#$1[@]}))" +} + +# Split a string and assign to an array. This function basically performs +# `IFS=<sep>; <array_name>=(<text>)` but properly handles saving/restoring the +# state of `IFS` and the shell option `noglob`. A naive splitting by +# `arr=(...)` suffers from unexpected IFS and pathname expansions, so one +# should prefer this function to such naive splitting. +# OPTIONS +# -a Append to the array +# -F sep Set a set of separator characters (used as IFS). The default +# separator is $' \t\n' +# -l The same as -F $'\n' +# @param $1 array_name The array name +# The array name should not start with an underscores "_", which is +# internally used. The array name should not be either "IFS" or +# "OPT{IND,ARG,ERR}". +# @param $2 text The string to split +# @return 2 when the usage is wrong, 0 when one or more completions are +# generated, or 1 when the execution succeeds but no candidates are +# generated. +# @since 2.12 +_comp_split() +{ + local _append="" IFS=$' \t\n' + + local OPTIND=1 OPTARG="" OPTERR=0 _opt + while getopts ':alF:' _opt "$@"; do + case $_opt in + a) _append=set ;; + l) IFS=$'\n' ;; + F) IFS=$OPTARG ;; + *) + echo "bash_completion: $FUNCNAME: usage error" >&2 + return 2 + ;; + esac + done + shift "$((OPTIND - 1))" + if (($# != 2)); then + printf '%s\n' "bash_completion: $FUNCNAME: unexpected number of arguments" >&2 + printf '%s\n' "usage: $FUNCNAME [-al] [-F SEP] ARRAY_NAME TEXT" >&2 + return 2 + elif [[ $1 == @(*[^_a-zA-Z0-9]*|[0-9]*|''|_*|IFS|OPTIND|OPTARG|OPTERR) ]]; then + printf '%s\n' "bash_completion: $FUNCNAME: invalid array name '$1'" >&2 + return 2 + fi + + local _original_opts=$SHELLOPTS + set -o noglob + + local _old_size _new_size + if [[ $_append ]]; then + eval "$1+=()" # in case $1 is unset + eval "_old_size=\${#$1[@]}" + eval "$1+=(\$2)" + else + _old_size=0 + eval "$1=(\$2)" + fi + eval "_new_size=\${#$1[@]}" + + [[ :$_original_opts: == *:noglob:* ]] || set +o noglob + ((_new_size > _old_size)) +} + +# Helper function for _comp_compgen +# @var[in] $? +# @var[in] _var +# @var[in] _append +# @return original $? +_comp_compgen__error_fallback() +{ + local _status=$? + if [[ $_append ]]; then + # make sure existence of variable + eval -- "$_var+=()" + else + eval -- "$_var=()" + fi + return "$_status" +} + +# Provide a common interface to generate completion candidates in COMPREPLY or +# in a specified array. +# OPTIONS +# -a Append to the array +# -v arr Store the results to the array ARR. The default is `COMPREPLY`. +# The array name should not start with an underscores "_", which is +# internally used. The array name should not be any of "cur", "IFS" +# or "OPT{IND,ARG,ERR}". +# -U var Unlocalize VAR before performing the assignments. This option can +# be specified multiple times to register multiple variables. This +# option is supposed to be used in implementing a generator (G1) when +# G1 defines a local variable name that does not start with `_`. In +# such a case, when the target variable specified to G1 by `-v VAR1` +# conflicts with the local variable, the assignment to the target +# variable fails to propagate outside G1. To avoid such a situation, +# G1 can call `_comp_compgen` with `-U VAR` to unlocalize `VAR` +# before accessing the target variable. For a builtin compgen call +# (i.e., _comp_compgen [options] -- options), VAR is unlocalized +# after calling the builtin `compgen` but before assigning results to +# the target array. For a generator call (i.e., _comp_compgen +# [options] G2 ...), VAR is unlocalized before calling the child +# generator function `_comp_compgen_G2`. +# -c cur Set a word used as a prefix to filter the completions. The default +# is ${cur-}. +# -R The same as -c ''. Use raw outputs without filtering. +# -C dir Evaluate compgen/generator in the specified directory. +# @var[in,opt] cur Used as the default value of a prefix to filter the +# completions. +# +# Usage #1: _comp_compgen [-alR|-F sep|-v arr|-c cur|-C dir] -- options... +# Call `compgen` with the specified arguments and store the results in the +# specified array. This function essentially performs arr=($(compgen args...)) +# but properly handles shell options, IFS, etc. using _comp_split. This +# function is equivalent to `_comp_split [-a] -l arr "$(IFS=sep; compgen +# args... -- cur)"`, but this pattern is frequent in the codebase and is good +# to separate out as a function for the possible future implementation change. +# OPTIONS +# -F sep Set a set of separator characters (used as IFS in evaluating +# `compgen'). The default separator is $' \t\n'. Note that this is +# not the set of separators to delimit output of `compgen', but the +# separators in evaluating the expansions of `-W '...'`, etc. The +# delimiter of the output of `compgen` is always a newline. +# -l The same as -F $'\n'. Use lines as words in evaluating compgen. +# @param $1... options Arguments that are passed to compgen (if $1 starts with +# a hyphen `-`). +# +# Note: References to positional parameters $1, $2, ... (such as -W '$1') +# will not work as expected because these reference the arguments of +# `_comp_compgen' instead of those of the caller function. When there are +# needs to reference them, save the arguments to an array and reference the +# array instead. +# +# Note: The array option `-V arr` in bash >= 5.3 should be instead specified +# as `-v arr` as a part of the `_comp_compgen` options. +# @return True (0) if at least one completion is generated, False (1) if no +# completion is generated, or 2 with an incorrect usage. +# +# Usage #2: _comp_compgen [-aR|-v arr|-c cur|-C dir|-i cmd|-x cmd] name args... +# Call the generator `_comp_compgen_NAME ARGS...` with the specified options. +# This provides a common interface to call the functions `_comp_compgen_NAME`, +# which produce completion candidates, with custom options [-alR|-v arr|-c +# cur]. The option `-F sep` is not used with this usage. +# OPTIONS +# -x cmd Call exported generator `_comp_xfunc_CMD_compgen_NAME` +# -i cmd Call internal generator `_comp_cmd_CMD__compgen_NAME` +# @param $1... name args Calls the function _comp_compgen_NAME with the +# specified ARGS (if $1 does not start with a hyphen `-`). The options +# [-alR|-v arr|-c cur] are inherited by the child calls of `_comp_compgen` +# inside `_comp_compgen_NAME` unless the child call `_comp_compgen` receives +# overriding options. +# @var[in,opt,internal] _comp_compgen__append +# @var[in,opt,internal] _comp_compgen__var +# @var[in,opt,internal] _comp_compgen__cur +# These variables are internally used to pass the effect of the options +# [-alR|-v arr|-c cur] to the child calls of `_comp_compgen` in +# `_comp_compgen_NAME`. +# @return Exit status of the generator. +# +# @remarks When no options are supplied to _comp_compgen, `_comp_compgen NAME +# args` is equivalent to the direct call `_comp_compgen_NAME args`. As the +# direct call is slightly more efficient, the direct call is preferred over +# calling it through `_comp_compgen`. +# +# @remarks Design `_comp_compgen_NAME`: a function that produce completions can +# be defined with the name _comp_compgen_NAME. The function is supposed to +# generate completions by calling `_comp_compgen`. To reflect the options +# specified to the outer calls of `_comp_compgen`, the function should not +# directly modify `COMPREPLY`. To add words, one can call +# +# _comp_compgen -- -W '"${words[@]}"' +# +# To directly add words without filtering by `cur`, one can call +# +# _comp_compgen -R -- -W '"${words[@]}"' +# +# or use the utility `_comp_compgen_set`: +# +# _comp_compgen_set "${words[@]}" +# +# Other nested calls of _comp_compgen can also be used. The function is +# supposed to replace the existing content of the array by default to allow the +# caller control whether to replace or append by the option `-a`. +# +# @since 2.12 +_comp_compgen() +{ + local _append= + local _var= + local _cur=${_comp_compgen__cur-${cur-}} + local _dir="" + local _ifs=$' \t\n' _has_ifs="" + local _icmd="" _xcmd="" + local -a _upvars=() + + local _old_nocasematch="" + if shopt -q nocasematch; then + _old_nocasematch=set + shopt -u nocasematch + fi + local OPTIND=1 OPTARG="" OPTERR=0 _opt + while getopts ':av:U:Rc:C:lF:i:x:' _opt "$@"; do + case $_opt in + a) _append=set ;; + v) + if [[ $OPTARG == @(*[^_a-zA-Z0-9]*|[0-9]*|''|_*|IFS|OPTIND|OPTARG|OPTERR|cur) ]]; then + printf 'bash_completion: %s: -v: invalid array name `%s'\''\n' "$FUNCNAME" "$OPTARG" >&2 + return 2 + fi + _var=$OPTARG + ;; + U) + if [[ $OPTARG == @(*[^_a-zA-Z0-9]*|[0-9]*|'') ]]; then + printf 'bash_completion: %s: -U: invalid variable name `%s'\''\n' "$FUNCNAME" "$OPTARG" >&2 + return 2 + elif [[ $OPTARG == @(_*|IFS|OPTIND|OPTARG|OPTERR|cur) ]]; then + printf 'bash_completion: %s: -U: unnecessary to mark `%s'\'' as upvar\n' "$FUNCNAME" "$OPTARG" >&2 + return 2 + fi + _upvars+=("$OPTARG") + ;; + c) _cur=$OPTARG ;; + R) _cur="" ;; + C) + if [[ ! $OPTARG ]]; then + printf 'bash_completion: %s: -C: invalid directory name `%s'\''\n' "$FUNCNAME" "$OPTARG" >&2 + return 2 + fi + _dir=$OPTARG + ;; + l) _has_ifs=set _ifs=$'\n' ;; + F) _has_ifs=set _ifs=$OPTARG ;; + [ix]) + if [[ ! $OPTARG ]]; then + printf 'bash_completion: %s: -%s: invalid command name `%s'\''\n' "$FUNCNAME" "$_opt" "$OPTARG" >&2 + return 2 + elif [[ $_icmd ]]; then + printf 'bash_completion: %s: -%s: `-i %s'\'' is already specified\n' "$FUNCNAME" "$_opt" "$_icmd" >&2 + return 2 + elif [[ $_xcmd ]]; then + printf 'bash_completion: %s: -%s: `-x %s'\'' is already specified\n' "$FUNCNAME" "$_opt" "$_xcmd" >&2 + return 2 + fi + ;;& + i) _icmd=$OPTARG ;; + x) _xcmd=$OPTARG ;; + *) + printf 'bash_completion: %s: usage error\n' "$FUNCNAME" >&2 + return 2 + ;; + esac + done + [[ $_old_nocasematch ]] && shopt -s nocasematch + shift "$((OPTIND - 1))" + if (($# == 0)); then + printf 'bash_completion: %s: unexpected number of arguments\n' "$FUNCNAME" >&2 + printf 'usage: %s [-alR|-F SEP|-v ARR|-c CUR] -- ARGS...' "$FUNCNAME" >&2 + return 2 + fi + if [[ ! $_var ]]; then + # Inherit _append and _var only when -v var is unspecified. + _var=${_comp_compgen__var-COMPREPLY} + [[ $_append ]] || _append=${_comp_compgen__append-} + fi + + if [[ $1 != -* ]]; then + # usage: _comp_compgen [options] NAME args + if [[ $_has_ifs ]]; then + printf 'bash_completion: %s: `-l'\'' and `-F sep'\'' are not supported for generators\n' "$FUNCNAME" >&2 + return 2 + fi + + local -a _generator + if [[ $_icmd ]]; then + _generator=("_comp_cmd_${_icmd//[^a-zA-Z0-9_]/_}__compgen_$1") + elif [[ $_xcmd ]]; then + _generator=(_comp_xfunc "$_xcmd" "compgen_$1") + else + _generator=("_comp_compgen_$1") + fi + if ! declare -F "${_generator[0]}" &>/dev/null; then + printf 'bash_completion: %s: unrecognized generator `%s'\'' (function %s not found)\n' "$FUNCNAME" "$1" "${_generator[0]}" >&2 + return 2 + fi + + ((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}" + + if [[ $_dir ]]; then + local _original_pwd=$PWD + local PWD=${PWD-} OLDPWD=${OLDPWD-} + # Note: We also redirect stdout because `cd` may output the target + # directory to stdout when CDPATH is set. + command cd -- "$_dir" &>/dev/null || + { + _comp_compgen__error_fallback + return + } + fi + + local _comp_compgen__append=$_append + local _comp_compgen__var=$_var + local _comp_compgen__cur=$_cur cur=$_cur + # Note: we use $1 as a part of a function name, and we use $2... as + # arguments to the function if any. + # shellcheck disable=SC2145 + "${_generator[@]}" "${@:2}" + local _status=$? + + # Go back to the original directory. + # Note: Failure of this line results in the change of the current + # directory visible to the user. We intentionally do not redirect + # stderr so that the error message appear in the terminal. + # shellcheck disable=SC2164 + [[ $_dir ]] && command cd -- "$_original_pwd" + + return "$_status" + fi + + # usage: _comp_compgen [options] -- [compgen_options] + if [[ $_icmd || $_xcmd ]]; then + printf 'bash_completion: %s: generator name is unspecified for `%s'\''\n' "$FUNCNAME" "${_icmd:+-i $_icmd}${_xcmd:+x $_xcmd}" >&2 + return 2 + fi + + # Note: $* in the below checks would be affected by uncontrolled IFS in + # bash >= 5.0, so we need to set IFS to the normal value. The behavior in + # bash < 5.0, where unquoted $* in conditional command did not honor IFS, + # was a bug. + # Note: Also, ${_cur:+-- "$_cur"} and ${_append:+-a} would be affected by + # uncontrolled IFS. + local IFS=$' \t\n' + # Note: extglob *\$?(\{)[0-9]* can be extremely slow when the string + # "${*:2:_nopt}" becomes longer, so we test \$[0-9] and \$\{[0-9] + # separately. + if [[ $* == *\$[0-9]* || $* == *\$\{[0-9]* ]]; then + printf 'bash_completion: %s: positional parameter $1, $2, ... do not work inside this function\n' "$FUNCNAME" >&2 + return 2 + fi + + local _result + _result=$( + if [[ $_dir ]]; then + # Note: We also redirect stdout because `cd` may output the target + # directory to stdout when CDPATH is set. + command cd -- "$_dir" &>/dev/null || return + fi + IFS=$_ifs compgen "$@" ${_cur:+-- "$_cur"} + ) || { + _comp_compgen__error_fallback + return + } + + ((${#_upvars[@]})) && _comp_unlocal "${_upvars[@]}" + _comp_split -l ${_append:+-a} "$_var" "$_result" +} + +# usage: _comp_compgen_set [words...] +# Reset COMPREPLY with the specified WORDS. If no arguments are specified, the +# array is cleared. +# +# When an array name is specified by `-v VAR` in a caller _comp_compgen, the +# array is reset instead of COMPREPLY. When the `-a` flag is specified in a +# caller _comp_compgen, the words are appended to the existing elements of the +# array instead of replacing the existing elements. This function ignores +# ${cur-} or the prefix specified by `-v CUR`. +# @return 0 if at least one completion is generated, or 1 otherwise. +# @since 2.12 +_comp_compgen_set() +{ + local _append=${_comp_compgen__append-} + local _var=${_comp_compgen__var-COMPREPLY} + eval -- "$_var${_append:++}=(\"\$@\")" + (($#)) +} + +# Simply split the text and generate completions. This function should be used +# instead of `_comp_compgen -- -W "$(command)"`, which is vulnerable because +# option -W evaluates the shell expansions included in the option argument. +# Options: +# -F sep Specify the separators. The default is $' \t\n' +# -l The same as -F $'\n' +# -X arg The same as the compgen option -X. +# -S arg The same as the compgen option -S. +# -P arg The same as the compgen option -P. +# -o arg The same as the compgen option -o. +# @param $1 String to split +# @return 0 if at least one completion is generated, or 1 otherwise. +# @since 2.12 +_comp_compgen_split() +{ + local _ifs=$' \t\n' + local -a _compgen_options=() + + local OPTIND=1 OPTARG="" OPTERR=0 _opt + while getopts ':lF:X:S:P:o:' _opt "$@"; do + case $_opt in + l) _ifs=$'\n' ;; + F) _ifs=$OPTARG ;; + [XSPo]) _compgen_options+=("-$_opt" "$OPTARG") ;; + *) + printf 'bash_completion: usage: %s [-l|-F sep] [--] str\n' "$FUNCNAME" >&2 + return 2 + ;; + esac + done + shift "$((OPTIND - 1))" + if (($# != 1)); then + printf 'bash_completion: %s: unexpected number of arguments.\n' "$FUNCNAME" >&2 + printf 'usage: %s [-l|-F sep] [--] str' "$FUNCNAME" >&2 + return 2 + fi + + local input=$1 IFS=$' \t\n' + _comp_compgen -F "$_ifs" -U input -- ${_compgen_options[@]+"${_compgen_options[@]}"} -W '$input' +} + +# Check if the argument looks like a path. +# @param $1 thing to check +# @return True (0) if it does, False (> 0) otherwise +# @since 2.12 +_comp_looks_like_path() +{ + [[ ${1-} == @(*/|[.~])* ]] +} + # Reassemble command line words, excluding specified characters from the # list of word completion separators (COMP_WORDBREAKS). # @param $1 chars Characters out of $COMP_WORDBREAKS which should @@ -235,9 +803,9 @@ _upvars() # @param $2 words Name of variable to return words to # @param $3 cword Name of variable to return cword to # -__reassemble_comp_words_by_ref() +_comp__reassemble_words() { - local exclude i j line ref + local exclude="" i j line ref # Exclude word separator characters? if [[ $1 ]]; then # Yes, exclude word separator characters; @@ -248,7 +816,7 @@ __reassemble_comp_words_by_ref() # Default to cword unchanged printf -v "$3" %s "$COMP_CWORD" # Are characters excluded which were former included? - if [[ -v exclude ]]; then + if [[ $exclude ]]; then # Yes, list of word completion separators has shrunk; line=$COMP_LINE # Re-assemble words to complete @@ -269,12 +837,16 @@ __reassemble_comp_words_by_ref() ((i == COMP_CWORD)) && printf -v "$3" %s "$j" # Remove optional whitespace + word separator from line copy line=${line#*"${COMP_WORDS[i]}"} + # Indicate next word if available, else end *both* while and + # for loop + if ((i < ${#COMP_WORDS[@]} - 1)); then + ((i++)) + else + break 2 + fi # Start new word if word separator in original line is # followed by whitespace. [[ $line == [[:blank:]]* ]] && ((j++)) - # Indicate next word if available, else end *both* while and - # for loop - ((i < ${#COMP_WORDS[@]} - 1)) && ((i++)) || break 2 done # Append word to current word ref="$2[$j]" @@ -291,7 +863,7 @@ __reassemble_comp_words_by_ref() printf -v "$2[i]" %s "${COMP_WORDS[i]}" done fi -} # __reassemble_comp_words_by_ref() +} # @param $1 exclude Characters out of $COMP_WORDBREAKS which should NOT be # considered word breaks. This is useful for things like scp where @@ -300,22 +872,22 @@ __reassemble_comp_words_by_ref() # @param $2 words Name of variable to return words to # @param $3 cword Name of variable to return cword to # @param $4 cur Name of variable to return current word to complete to -# @see __reassemble_comp_words_by_ref() -__get_cword_at_cursor_by_ref() +# @see _comp__reassemble_words() +_comp__get_cword_at_cursor() { local cword words=() - __reassemble_comp_words_by_ref "$1" words cword + _comp__reassemble_words "$1" words cword local i cur="" index=$COMP_POINT lead=${COMP_LINE:0:COMP_POINT} - # Cursor not at position 0 and not leaded by just space(s)? + # Cursor not at position 0 and not led by just space(s)? if [[ $index -gt 0 && ($lead && ${lead//[[:space:]]/}) ]]; then cur=$COMP_LINE for ((i = 0; i <= cword; ++i)); do # Current word fits in $cur, and $cur doesn't match cword? - while [[ ${#cur} -ge ${#words[i]} && \ + while [[ ${#cur} -ge ${#words[i]} && ${cur:0:${#words[i]}} != "${words[i]-}" ]]; do # Strip first character - cur="${cur:1}" + cur=${cur:1} # Decrease cursor position, staying >= 0 ((index > 0)) && ((index--)) done @@ -324,7 +896,7 @@ __get_cword_at_cursor_by_ref() if ((i < cword)); then # No, cword lies further; local old_size=${#cur} - cur="${cur#"${words[i]}"}" + cur=${cur#"${words[i]}"} local new_size=${#cur} ((index -= old_size - new_size)) fi @@ -335,8 +907,9 @@ __get_cword_at_cursor_by_ref() ((index < 0)) && index=0 fi - local "$2" "$3" "$4" && _upvars -a${#words[@]} $2 ${words+"${words[@]}"} \ - -v $3 "$cword" -v $4 "${cur:0:index}" + local IFS=$' \t\n' + local "$2" "$3" "$4" && _comp_upvars -a"${#words[@]}" "$2" ${words[@]+"${words[@]}"} \ + -v "$3" "$cword" -v "$4" "${cur:0:index}" } # Get the word to complete and optional previous words. @@ -345,7 +918,7 @@ __get_cword_at_cursor_by_ref() # (For example, if the line is "ls foobar", # and the cursor is here --------> ^ # Also one is able to cross over possible wordbreak characters. -# Usage: _get_comp_words_by_ref [OPTIONS] [VARNAMES] +# Usage: _comp_get_words [OPTIONS] [VARNAMES] # Available VARNAMES: # cur Return cur via $cur # prev Return prev via $prev @@ -364,16 +937,23 @@ __get_cword_at_cursor_by_ref() # # Example usage: # -# $ _get_comp_words_by_ref -n : cur prev +# $ _comp_get_words -n : cur prev # -_get_comp_words_by_ref() +# @since 2.12 +_comp_get_words() { - local exclude flag i OPTIND=1 + local exclude="" flag i OPTIND=1 local cur cword words=() - local upargs=() upvars=() vcur vcword vprev vwords + local upargs=() upvars=() vcur="" vcword="" vprev="" vwords="" while getopts "c:i:n:p:w:" flag "$@"; do case $flag in + [cipw]) + if [[ $OPTARG != [a-zA-Z_]*([a-zA-Z_0-9])?(\[*\]) ]]; then + echo "bash_completion: $FUNCNAME: -$flag: invalid variable name \`$OPTARG'" >&2 + return 1 + fi + ;;& c) vcur=$OPTARG ;; i) vcword=$OPTARG ;; n) exclude=$OPTARG ;; @@ -400,108 +980,44 @@ _get_comp_words_by_ref() ((OPTIND += 1)) done - __get_cword_at_cursor_by_ref "${exclude-}" words cword cur + _comp__get_cword_at_cursor "${exclude-}" words cword cur - [[ -v vcur ]] && { + [[ $vcur ]] && { upvars+=("$vcur") - upargs+=(-v $vcur "$cur") + upargs+=(-v "$vcur" "$cur") } - [[ -v vcword ]] && { + [[ $vcword ]] && { upvars+=("$vcword") - upargs+=(-v $vcword "$cword") + upargs+=(-v "$vcword" "$cword") } - [[ -v vprev && $cword -ge 1 ]] && { + [[ $vprev ]] && { + local value="" + ((cword >= 1)) && value=${words[cword - 1]} upvars+=("$vprev") - upargs+=(-v $vprev "${words[cword - 1]}") + upargs+=(-v "$vprev" "$value") } - [[ -v vwords ]] && { + [[ $vwords ]] && { + # Note: bash < 4.4 has a bug that all the elements are connected with + # ${v+"$@"} when IFS does not contain whitespace. + local IFS=$' \t\n' upvars+=("$vwords") - upargs+=(-a${#words[@]} $vwords ${words+"${words[@]}"}) + upargs+=(-a"${#words[@]}" "$vwords" ${words+"${words[@]}"}) } - ((${#upvars[@]})) && local "${upvars[@]}" && _upvars "${upargs[@]}" + ((${#upvars[@]})) && local "${upvars[@]}" && _comp_upvars "${upargs[@]}" } -# Get the word to complete. -# This is nicer than ${COMP_WORDS[COMP_CWORD]}, since it handles cases -# where the user is completing in the middle of a word. -# (For example, if the line is "ls foobar", -# and the cursor is here --------> ^ -# @param $1 string Characters out of $COMP_WORDBREAKS which should NOT be -# considered word breaks. This is useful for things like scp where -# we want to return host:path and not only path, so we would pass the -# colon (:) as $1 in this case. -# @param $2 integer Index number of word to return, negatively offset to the -# current word (default is 0, previous is 1), respecting the exclusions -# given at $1. For example, `_get_cword "=:" 1' returns the word left of -# the current word, respecting the exclusions "=:". -# @deprecated Use `_get_comp_words_by_ref cur' instead -# @see _get_comp_words_by_ref() -_get_cword() -{ - local LC_CTYPE=C - local cword words - __reassemble_comp_words_by_ref "${1-}" words cword - - # return previous word offset by $2 - if [[ ${2-} && ${2//[^0-9]/} ]]; then - printf "%s" "${words[cword - $2]}" - elif ((${#words[cword]} == 0 && COMP_POINT == ${#COMP_LINE})); then - : # nothing - else - local i - local cur="$COMP_LINE" - local index="$COMP_POINT" - for ((i = 0; i <= cword; ++i)); do - # Current word fits in $cur, and $cur doesn't match cword? - while [[ ${#cur} -ge ${#words[i]} && \ - ${cur:0:${#words[i]}} != "${words[i]}" ]]; do - # Strip first character - cur="${cur:1}" - # Decrease cursor position, staying >= 0 - ((index > 0)) && ((index--)) - done - - # Does found word match cword? - if ((i < cword)); then - # No, cword lies further; - local old_size="${#cur}" - cur="${cur#${words[i]}}" - local new_size="${#cur}" - ((index -= old_size - new_size)) - fi - done - - if [[ ${words[cword]:0:${#cur}} != "$cur" ]]; then - # We messed up! At least return the whole word so things - # keep working - printf "%s" "${words[cword]}" - else - printf "%s" "${cur:0:index}" - fi - fi -} # _get_cword() - -# Get word previous to the current word. -# This is a good alternative to `prev=${COMP_WORDS[COMP_CWORD-1]}' because bash4 -# will properly return the previous word with respect to any given exclusions to -# COMP_WORDBREAKS. -# @deprecated Use `_get_comp_words_by_ref cur prev' instead -# @see _get_comp_words_by_ref() +# Generate the specified items after left-trimming with the word-to-complete +# containing a colon (:). If the word-to-complete does not contain a colon, +# this generates the specified items without modifications. +# @param $@ items to generate +# @var[in] cur current word to complete +# +# @remarks In Bash, with a colon in COMP_WORDBREAKS, words containing colons +# are always completed as entire words if the word to complete contains a +# colon. This function fixes this behavior by removing the +# colon-containing-prefix from the items. # -_get_pword() -{ - if ((COMP_CWORD >= 1)); then - _get_cword "${@:-}" 1 - fi -} - -# If the word-to-complete contains a colon (:), left-trim COMPREPLY items with -# word-to-complete. -# With a colon in COMP_WORDBREAKS, words containing -# colons are always completed as entire words if the word to complete contains -# a colon. This function fixes this, by removing the colon-containing-prefix -# from COMPREPLY items. # The preferred solution is to remove the colon (:) from COMP_WORDBREAKS in # your .bashrc: # @@ -510,24 +1026,37 @@ _get_pword() # # See also: Bash FAQ - E13) Why does filename completion misbehave if a colon # appears in the filename? - https://tiswww.case.edu/php/chet/bash/FAQ +# +# @since 2.12 +_comp_compgen_ltrim_colon() +{ + (($#)) || return 0 + local -a _tmp + _tmp=("$@") + if [[ $cur == *:* && $COMP_WORDBREAKS == *:* ]]; then + # Remove colon-word prefix from items + local _colon_word=${cur%"${cur##*:}"} + _tmp=("${_tmp[@]#"$_colon_word"}") + fi + _comp_compgen_set "${_tmp[@]}" +} + +# If the word-to-complete contains a colon (:), left-trim COMPREPLY items with +# word-to-complete. +# # @param $1 current word to complete (cur) -# @modifies global array $COMPREPLY +# @var[in,out] COMPREPLY # -__ltrim_colon_completions() +# @since 2.12 +_comp_ltrim_colon_completions() { - if [[ $1 == *:* && $COMP_WORDBREAKS == *:* ]]; then - # Remove colon-word prefix from COMPREPLY items - local colon_word=${1%"${1##*:}"} - local i=${#COMPREPLY[*]} - while ((i-- > 0)); do - COMPREPLY[i]=${COMPREPLY[i]#"$colon_word"} - done - fi -} # __ltrim_colon_completions() + ((${#COMPREPLY[@]})) || return 0 + _comp_compgen -c "$1" ltrim_colon "${COMPREPLY[@]}" +} # This function quotes the argument in a way so that readline dequoting # results in the original argument. This is necessary for at least -# `compgen' which requires its arguments quoted/escaped: +# `compgen` which requires its arguments quoted/escaped: # # $ ls "a'b/" # c @@ -538,87 +1067,90 @@ __ltrim_colon_completions() # See also: # - https://lists.gnu.org/archive/html/bug-bash/2009-03/msg00155.html # - https://www.mail-archive.com/bash-completion-devel@lists.alioth.debian.org/msg01944.html -# @param $1 Argument to quote -# @param $2 Name of variable to return result to -_quote_readline_by_ref() +# @param $1 Argument to quote +# @var[out] REPLY Quoted result is stored in this variable +# @since 2.12 +# shellcheck disable=SC2178 # The assignment is not intended for the global "REPLY" +_comp_quote_compgen() { if [[ $1 == \'* ]]; then # Leave out first character - printf -v $2 %s "${1:1}" + REPLY=${1:1} else - printf -v $2 %q "$1" + printf -v REPLY %q "$1" + + # If result becomes quoted like this: $'string', re-evaluate in order + # to drop the additional quoting. See also: + # https://www.mail-archive.com/bash-completion-devel@lists.alioth.debian.org/msg01942.html + if [[ $REPLY == \$\'*\' ]]; then + local value=${REPLY:2:-1} # Strip beginning $' and ending '. + value=${value//'%'/%%} # Escape % for printf format. + # shellcheck disable=SC2059 + printf -v REPLY "$value" # Decode escape sequences of \.... + fi fi - - # If result becomes quoted like this: $'string', re-evaluate in order to - # drop the additional quoting. See also: - # https://www.mail-archive.com/bash-completion-devel@lists.alioth.debian.org/msg01942.html - [[ ${!2} == \$* ]] && eval $2=${!2} -} # _quote_readline_by_ref() +} # This function performs file and directory completion. It's better than # simply using 'compgen -f', because it honours spaces in filenames. # @param $1 If `-d', complete only on directories. Otherwise filter/pick only # completions with `.$1' and the uppercase version of it as file # extension. +# @return 0 if at least one completion is generated, or 1 otherwise. # -_filedir() +# @since 2.12 +_comp_compgen_filedir() { - local IFS=$'\n' - - _tilde "${cur-}" || return + _comp_compgen_tilde && return local -a toks - local reset arg=${1-} - - if [[ $arg == -d ]]; then - reset=$(shopt -po noglob) - set -o noglob - toks=($(compgen -d -- "${cur-}")) - IFS=' ' - $reset - IFS=$'\n' + local _arg=${1-} + + if [[ $_arg == -d ]]; then + _comp_compgen -v toks -- -d else - local quoted - _quote_readline_by_ref "${cur-}" quoted + local REPLY + _comp_quote_compgen "${cur-}" + local _quoted=$REPLY + _comp_unlocal REPLY + + # work around bash-4.2 where compgen -f "''" produces nothing. + [[ $_quoted == "''" ]] && _quoted="" # Munge xspec to contain uppercase version too # https://lists.gnu.org/archive/html/bug-bash/2010-09/msg00036.html # news://news.gmane.io/4C940E1C.1010304@case.edu - local xspec=${arg:+"!*.@($arg|${arg^^})"} plusdirs=() + local _xspec=${_arg:+"!*.@($_arg|${_arg^^})"} _plusdirs=() # Use plusdirs to get dir completions if we have a xspec; if we don't, # there's no need, dirs come along with other completions. Don't use # plusdirs quite yet if fallback is in use though, in order to not ruin # the fallback condition with the "plus" dirs. - local opts=(-f -X "$xspec") - [[ $xspec ]] && plusdirs=(-o plusdirs) - [[ ${COMP_FILEDIR_FALLBACK-} || -z ${plusdirs-} ]] || - opts+=("${plusdirs[@]}") - - reset=$(shopt -po noglob) - set -o noglob - toks+=($(compgen "${opts[@]}" -- $quoted)) - IFS=' ' - $reset - IFS=$'\n' + local _opts=(-f -X "$_xspec") + [[ $_xspec ]] && _plusdirs=(-o plusdirs) + [[ ${BASH_COMPLETION_FILEDIR_FALLBACK-} || ! ${_plusdirs-} ]] || + _opts+=("${_plusdirs[@]}") + + _comp_compgen -v toks -c "$_quoted" -- "${_opts[@]}" # Try without filter if it failed to produce anything and configured to - [[ -n ${COMP_FILEDIR_FALLBACK-} && -n $arg && ${#toks[@]} -lt 1 ]] && { - reset=$(shopt -po noglob) - set -o noglob - toks+=($(compgen -f ${plusdirs+"${plusdirs[@]}"} -- $quoted)) - IFS=' ' - $reset - IFS=$'\n' - } + [[ ${BASH_COMPLETION_FILEDIR_FALLBACK-} && + $_arg && ${#toks[@]} -lt 1 ]] && + _comp_compgen -av toks -c "$_quoted" -- \ + -f ${_plusdirs+"${_plusdirs[@]}"} fi if ((${#toks[@]} != 0)); then - # 2>/dev/null for direct invocation, e.g. in the _filedir unit test + # 2>/dev/null for direct invocation, e.g. in the _comp_compgen_filedir + # unit test compopt -o filenames 2>/dev/null - COMPREPLY+=("${toks[@]}") fi -} # _filedir() + + # Note: bash < 4.4 has a bug that all the elements are connected with + # ${v+"${a[@]}"} when IFS does not contain whitespace. + local IFS=$' \t\n' + _comp_compgen -U toks set ${toks[@]+"${toks[@]}"} +} # This function splits $cur=--foo=bar into $prev=--foo, $cur=bar, making it # easier to support both "--foo bar" and "--foo=bar" style completions. @@ -626,13 +1158,13 @@ _filedir() # this to be useful. # Returns 0 if current option was split, 1 otherwise. # -_split_longopt() +_comp__split_longopt() { if [[ $cur == --?*=* ]]; then # Cut also backslash before '=' in case it ended up there # for some reason. - prev="${cur%%?(\\)=*}" - cur="${cur#*=}" + prev=${cur%%?(\\)=*} + cur=${cur#*=} return 0 fi @@ -642,51 +1174,140 @@ _split_longopt() # Complete variables. # @return True (0) if variables were completed, # False (> 0) if not. -_variables() +# @since 2.12 +_comp_compgen_variables() { if [[ $cur =~ ^(\$(\{[!#]?)?)([A-Za-z0-9_]*)$ ]]; then # Completing $var / ${var / ${!var / ${#var if [[ $cur == '${'* ]]; then local arrs vars - vars=($(compgen -A variable -P ${BASH_REMATCH[1]} -S '}' -- ${BASH_REMATCH[3]})) - arrs=($(compgen -A arrayvar -P ${BASH_REMATCH[1]} -S '[' -- ${BASH_REMATCH[3]})) + _comp_compgen -v vars -c "${BASH_REMATCH[3]}" -- -A variable -P "${BASH_REMATCH[1]}" -S '}' + _comp_compgen -v arrs -c "${BASH_REMATCH[3]}" -- -A arrayvar -P "${BASH_REMATCH[1]}" -S '[' if ((${#vars[@]} == 1 && ${#arrs[@]} != 0)); then # Complete ${arr with ${array[ if there is only one match, and that match is an array variable compopt -o nospace - COMPREPLY+=(${arrs[*]}) + _comp_compgen -U vars -U arrs -R -- -W '"${arrs[@]}"' else # Complete ${var with ${variable} - COMPREPLY+=(${vars[*]}) + _comp_compgen -U vars -U arrs -R -- -W '"${vars[@]}"' fi else # Complete $var with $variable - COMPREPLY+=($(compgen -A variable -P '$' -- "${BASH_REMATCH[3]}")) + _comp_compgen -ac "${BASH_REMATCH[3]}" -- -A variable -P '$' fi return 0 elif [[ $cur =~ ^(\$\{[#!]?)([A-Za-z0-9_]*)\[([^]]*)$ ]]; then # Complete ${array[i with ${array[idx]} - local IFS=$'\n' - COMPREPLY+=($(compgen -W '$(printf %s\\n "${!'${BASH_REMATCH[2]}'[@]}")' \ - -P "${BASH_REMATCH[1]}${BASH_REMATCH[2]}[" -S ']}' -- "${BASH_REMATCH[3]}")) + local vars + _comp_compgen -v vars -c "${BASH_REMATCH[3]}" -- -W '"${!'"${BASH_REMATCH[2]}"'[@]}"' \ + -P "${BASH_REMATCH[1]}${BASH_REMATCH[2]}[" -S ']}' # Complete ${arr[@ and ${arr[* if [[ ${BASH_REMATCH[3]} == [@*] ]]; then - COMPREPLY+=("${BASH_REMATCH[1]}${BASH_REMATCH[2]}[${BASH_REMATCH[3]}]}") + vars+=("${BASH_REMATCH[1]}${BASH_REMATCH[2]}[${BASH_REMATCH[3]}]}") + fi + # array indexes may have colons + if ((${#vars[@]})); then + _comp_compgen -U vars -c "$cur" ltrim_colon "${vars[@]}" + else + _comp_compgen_set fi - __ltrim_colon_completions "$cur" # array indexes may have colons return 0 elif [[ $cur =~ ^\$\{[#!]?[A-Za-z0-9_]*\[.*\]$ ]]; then # Complete ${array[idx] with ${array[idx]} - COMPREPLY+=("$cur}") - __ltrim_colon_completions "$cur" + _comp_compgen -c "$cur" ltrim_colon "$cur}" return 0 + fi + return 1 +} + +# Complete a delimited value. +# +# Usage: [-k] DELIMITER COMPGEN_ARG... +# -k: do not filter out already present tokens in value +# @since 2.12 +_comp_delimited() +{ + local prefix="" delimiter=$1 deduplicate=set + shift + if [[ $delimiter == -k ]]; then + deduplicate="" + delimiter=$1 + shift + fi + [[ $cur == *"$delimiter"* ]] && prefix=${cur%"$delimiter"*}$delimiter + + if [[ $deduplicate ]]; then + # We could construct a -X pattern to feed to compgen, but that'd + # conflict with possibly already set -X in $@, as well as have + # glob char escaping issues to deal with. Do removals by hand instead. + _comp_compgen -R -- "$@" + local -a existing + _comp_split -F "$delimiter" existing "$cur" + # Do not remove the last from existing if it's not followed by the + # delimiter so we get space appended. + [[ ! $cur || $cur == *"$delimiter" ]] || unset -v "existing[${#existing[@]}-1]" + if ((${#COMPREPLY[@]})); then + local x i + for x in ${existing+"${existing[@]}"}; do + for i in "${!COMPREPLY[@]}"; do + if [[ $x == "${COMPREPLY[i]}" ]]; then + unset -v 'COMPREPLY[i]' + continue 2 # assume no dupes in COMPREPLY + fi + done + done + ((${#COMPREPLY[@]})) && + _comp_compgen -c "${cur##*"$delimiter"}" -- -W '"${COMPREPLY[@]}"' + fi else - case ${prev-} in - TZ) - cur=/usr/share/zoneinfo/$cur - _filedir + _comp_compgen -c "${cur##*"$delimiter"}" -- "$@" + fi + + # It would seem that in some specific cases we could avoid adding the + # prefix to all completions, thereby making the list of suggestions + # cleaner, and only adding it when there's exactly one completion. + # The cases where this opportunity has been observed involve having + # `show-all-if-ambiguous` on, but even that has cases where it fails + # and the last separator including everything before it is lost. + # https://github.com/scop/bash-completion/pull/913#issuecomment-1490140309 + local i + for i in "${!COMPREPLY[@]}"; do + COMPREPLY[i]="$prefix${COMPREPLY[i]}" + done + + [[ $delimiter != : ]] || _comp_ltrim_colon_completions "$cur" +} + +# Complete assignment of various known environment variables. +# +# The word to be completed is expected to contain the entire assignment, +# including the variable name and the "=". Some known variables are completed +# with colon separated values; for those to work, colon should not have been +# used to split words. See related parameters to _comp_initialize. +# +# @param $1 variable assignment to be completed +# @return True (0) if variable value completion was attempted, +# False (> 0) if not. +# @since 2.12 +_comp_variable_assignments() +{ + local cur=${1-} i + + if [[ $cur =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + prev=${BASH_REMATCH[1]} + cur=${BASH_REMATCH[2]} + else + return 1 + fi + + case $prev in + TZ) + cur=/usr/share/zoneinfo/$cur + _comp_compgen_filedir + if ((${#COMPREPLY[@]})); then for i in "${!COMPREPLY[@]}"; do if [[ ${COMPREPLY[i]} == *.tab ]]; then - unset 'COMPREPLY[i]' + unset -v 'COMPREPLY[i]' continue elif [[ -d ${COMPREPLY[i]} ]]; then COMPREPLY[i]+=/ @@ -694,20 +1315,24 @@ _variables() fi COMPREPLY[i]=${COMPREPLY[i]#/usr/share/zoneinfo/} done - return 0 - ;; - TERM) - _terms - return 0 - ;; - LANG | LC_*) - COMPREPLY=($(compgen -W '$(locale -a 2>/dev/null)' \ - -- "$cur")) - return 0 - ;; - esac - fi - return 1 + fi + ;; + TERM) + _comp_compgen_terms + ;; + LANG | LC_*) + _comp_compgen_split -- "$(locale -a 2>/dev/null)" + ;; + LANGUAGE) + _comp_delimited : -W '$(locale -a 2>/dev/null)' + ;; + *) + _comp_compgen_variables && return 0 + _comp_compgen -a filedir + ;; + esac + + return 0 } # Initialize completion and deal with various general things: do file @@ -717,18 +1342,37 @@ _variables() # cur, prev, words, and cword are local, ditto split if you use -s. # # Options: -# -n EXCLUDE Passed to _get_comp_words_by_ref -n with redirection chars -# -e XSPEC Passed to _filedir as first arg for stderr redirections -# -o XSPEC Passed to _filedir as first arg for other output redirections -# -i XSPEC Passed to _filedir as first arg for stdin redirections -# -s Split long options with _split_longopt, implies -n = +# -n EXCLUDE Passed to _comp_get_words -n with redirection chars +# -e XSPEC Passed to _comp_compgen_filedir as first arg for stderr +# redirections +# -o XSPEC Passed to _comp_compgen_filedir as first arg for other output +# redirections +# -i XSPEC Passed to _comp_compgen_filedir as first arg for stdin +# redirections +# -s Split long options with _comp__split_longopt, implies -n = +# @param $1...$3 args Original arguments specified to the completion function. +# The first argument $1 is command name. The second +# argument $2 is the string before the cursor in the +# current word. The third argument $3 is the previous +# word. +# @var[out] cur Reconstructed current word +# @var[out] prev Reconstructed previous word +# @var[out] words Reconstructed words +# @var[out] cword Current word index in `words` +# @var[out] comp_args Original arguments specified to the completion +# function are saved in this array, if the arguments +# $1...$3 is specified. +# @var[out,opt] was_split When "-s" is specified, `"set"/""` is set depending +# on whether the split happened. # @return True (0) if completion needs further processing, # False (> 0) no further processing is necessary. # -_init_completion() +# @since 2.12 +_comp_initialize() { - local exclude="" flag outx errx inx OPTIND=1 + local exclude="" opt_split="" outx="" errx="" inx="" + local flag OPTIND=1 OPTARG="" OPTERR=0 while getopts "n:e:o:i:s" flag "$@"; do case $flag in n) exclude+=$OPTARG ;; @@ -736,8 +1380,9 @@ _init_completion() o) outx=$OPTARG ;; i) inx=$OPTARG ;; s) - split=false - exclude+== + opt_split="set" + was_split="" + exclude+="=" ;; *) echo "bash_completion: $FUNCNAME: usage error" >&2 @@ -745,13 +1390,15 @@ _init_completion() ;; esac done + shift "$((OPTIND - 1))" + (($#)) && comp_args=("$@") COMPREPLY=() - local redir="@(?([0-9])<|?([0-9&])>?(>)|>&)" - _get_comp_words_by_ref -n "$exclude<>&" cur prev words cword + local redir='@(?(+([0-9])|{[a-zA-Z_]*([a-zA-Z_0-9])})@(>?([>|&])|<?([>&])|<<?([-<]))|&>?(>))' + _comp_get_words -n "$exclude<>&" cur prev words cword # Complete variable names. - _variables && return 1 + _comp_compgen_variables && return 1 # Complete on files if current is a redirect possibly followed by a # filename, e.g. ">foo", or previous is a "bare" redirect, e.g. ">". @@ -770,8 +1417,9 @@ _init_completion() esac ;; esac - cur="${cur##$redir}" - _filedir $xspec + # shellcheck disable=SC2295 # redir is a pattern + cur=${cur##$redir} + _comp_compgen_filedir "$xspec" return 1 fi @@ -792,259 +1440,342 @@ _init_completion() ((cword <= 0)) && return 1 prev=${words[cword - 1]} - [[ ${split-} ]] && _split_longopt && split=true + [[ $opt_split ]] && _comp__split_longopt && was_split="set" return 0 } -# Helper function for _parse_help and _parse_usage. -__parse_options() +# Helper function for _comp_compgen_help and _comp_compgen_usage. +# Obtain the help output based on the arguments. +# @param $@ args Arguments specified to the caller. +# @var[out] _lines +# @return 2 if the usage is wrong, 1 if no output is obtained, or otherwise 0. +_comp_compgen_help__get_help_lines() +{ + local -a help_cmd + case ${1-} in + -) + if (($# > 1)); then + printf 'bash_completion: %s -: extra arguments for -\n' "${FUNCNAME[1]}" >&2 + printf 'usage: %s -\n' "${FUNCNAME[1]}" >&2 + printf 'usage: %s -c cmd args...\n' "${FUNCNAME[1]}" >&2 + printf 'usage: %s [-- args...]\n' "${FUNCNAME[1]}" >&2 + return 2 + fi + help_cmd=(exec cat) + ;; + -c) + if (($# < 2)); then + printf 'bash_completion: %s -c: no command is specified\n' "${FUNCNAME[1]}" >&2 + printf 'usage: %s -\n' "${FUNCNAME[1]}" >&2 + printf 'usage: %s -c cmd args...\n' "${FUNCNAME[1]}" >&2 + printf 'usage: %s [-- args...]\n' "${FUNCNAME[1]}" >&2 + return 2 + fi + help_cmd=("${@:2}") + ;; + --) shift 1 ;& + *) + local REPLY + _comp_dequote "${comp_args[0]-}" || REPLY=${comp_args[0]-} + help_cmd=("${REPLY:-false}" "$@") + ;; + esac + + local REPLY + _comp_split -l REPLY "$(LC_ALL=C "${help_cmd[@]}" 2>&1)" && + _lines=("${REPLY[@]}") +} + +# Helper function for _comp_compgen_help and _comp_compgen_usage. +# @var[in,out] options Add options +# @return True (0) if an option was found, False (> 0) otherwise +_comp_compgen_help__parse() { - local option option2 i IFS=$' \t\n,/|' + local option option2 i # Take first found long option, or first one (short) if not found. option= - local -a array=($1) - for i in "${array[@]}"; do - case "$i" in - ---*) break ;; - --?*) - option=$i - break - ;; - -?*) [[ $option ]] || option=$i ;; - *) break ;; - esac - done - [[ $option ]] || return 0 - - IFS=$' \t\n' # affects parsing of the regexps below... + local -a array + if _comp_split -F $' \t\n,/|' array "$1"; then + for i in "${array[@]}"; do + case "$i" in + ---*) break ;; + --?*) + option=$i + break + ;; + -?*) [[ $option ]] || option=$i ;; + *) break ;; + esac + done + fi + [[ $option ]] || return 1 # Expand --[no]foo to --foo and --nofoo etc if [[ $option =~ (\[((no|dont)-?)\]). ]]; then option2=${option/"${BASH_REMATCH[1]}"/} option2=${option2%%[<{().[]*} - printf '%s\n' "${option2/=*/=}" + options+=("${option2/=*/=}") option=${option/"${BASH_REMATCH[1]}"/"${BASH_REMATCH[2]}"} fi - option=${option%%[<{().[]*} - printf '%s\n' "${option/=*/=}" + [[ $option =~ ^([^=<{().[]|\.[A-Za-z0-9])+=? ]] && + options+=("$BASH_REMATCH") } -# Parse GNU style help output of the given command. -# @param $1 command; if "-", read from stdin and ignore rest of args -# @param $2 command options (default: --help) +# Parse GNU style help output of the given command and generate and store +# completions in an array. The help output is produced in the way depending on +# the usage: +# usage: _comp_compgen_help - # read from stdin +# usage: _comp_compgen_help -c cmd args... # run "cmd args..." +# usage: _comp_compgen_help [[--] args...] # run "${comp_args[0]} args..." +# When no arguments are specified, `--help` is assumed. # -_parse_help() -{ - eval local cmd="$(quote "$1")" - local line - { - case $cmd in - -) cat ;; - *) LC_ALL=C "$(dequote "$cmd")" ${2:---help} 2>&1 ;; - esac - } | - while read -r line; do - - [[ $line == *([[:blank:]])-* ]] || continue - # transform "-f FOO, --foo=FOO" to "-f , --foo=FOO" etc - while [[ $line =~ \ - ((^|[^-])-[A-Za-z0-9?][[:space:]]+)\[?[A-Z0-9]+([,_-]+[A-Z0-9]+)?(\.\.+)?\]? ]]; do - line=${line/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}"} - done - __parse_options "${line// or /, }" - +# @var[in] comp_args[0] +# @since 2.12 +_comp_compgen_help() +{ + (($#)) || set -- -- --help + + local -a _lines + _comp_compgen_help__get_help_lines "$@" || return "$?" + + local -a options=() + local _line + for _line in "${_lines[@]}"; do + [[ $_line == *([[:blank:]])-* ]] || continue + # transform "-f FOO, --foo=FOO" to "-f , --foo=FOO" etc + while [[ $_line =~ ((^|[^-])-[A-Za-z0-9?][[:space:]]+)\[?[A-Z0-9]+([,_-]+[A-Z0-9]+)?(\.\.+)?\]? ]]; do + _line=${_line/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}"} done + _comp_compgen_help__parse "${_line// or /, }" + done + ((${#options[@]})) || return 1 + + _comp_compgen -U options -- -W '"${options[@]}"' + return 0 } -# Parse BSD style usage output (options in brackets) of the given command. -# @param $1 command; if "-", read from stdin and ignore rest of args -# @param $2 command options (default: --usage) +# Parse BSD style usage output (options in brackets) of the given command. The +# help output is produced in the way depending on the usage: +# usage: _comp_compgen_usage - # read from stdin +# usage: _comp_compgen_usage -c cmd args... # run "cmd args..." +# usage: _comp_compgen_usage [[--] args...] # run "${comp_args[0]} args..." +# When no arguments are specified, `--usage` is assumed. # -_parse_usage() -{ - eval local cmd="$(quote "$1")" - local line match option i char - { - case $cmd in - -) cat ;; - *) LC_ALL=C "$(dequote "$cmd")" ${2:---usage} 2>&1 ;; - esac - } | - while read -r line; do - - while [[ $line =~ \[[[:space:]]*(-[^]]+)[[:space:]]*\] ]]; do - match=${BASH_REMATCH[0]} - option=${BASH_REMATCH[1]} - case $option in - -?(\[)+([a-zA-Z0-9?])) - # Treat as bundled short options - for ((i = 1; i < ${#option}; i++)); do - char=${option:i:1} - [[ $char != '[' ]] && printf '%s\n' -$char - done - ;; - *) - __parse_options "$option" - ;; - esac - line=${line#*"$match"} - done - +# @var[in] comp_args[0] +# @since 2.12 +_comp_compgen_usage() +{ + (($#)) || set -- -- --usage + + local -a _lines + _comp_compgen_help__get_help_lines "$@" || return "$?" + + local -a options=() + local _line _match _option _i _char + for _line in "${_lines[@]}"; do + while [[ $_line =~ \[[[:space:]]*(-[^]]+)[[:space:]]*\] ]]; do + _match=${BASH_REMATCH[0]} + _option=${BASH_REMATCH[1]} + case $_option in + -?(\[)+([a-zA-Z0-9?])) + # Treat as bundled short options + for ((_i = 1; _i < ${#_option}; _i++)); do + _char=${_option:_i:1} + [[ $_char != '[' ]] && options+=("-$_char") + done + ;; + *) + _comp_compgen_help__parse "$_option" + ;; + esac + _line=${_line#*"$_match"} done + done + ((${#options[@]})) || return 1 + + _comp_compgen -U options -- -W '"${options[@]}"' + return 0 } # This function completes on signal names (minus the SIG prefix) # @param $1 prefix -_signals() +# +# @since 2.12 +_comp_compgen_signals() { - local -a sigs=($(compgen -P "${1-}" -A signal "SIG${cur#${1-}}")) - COMPREPLY+=("${sigs[@]/#${1-}SIG/${1-}}") + local -a sigs + _comp_compgen -v sigs -c "SIG${cur#"${1-}"}" -- -A signal && + _comp_compgen -RU sigs -- -P "${1-}" -W '"${sigs[@]#SIG}"' } # This function completes on known mac addresses # -_mac_addresses() +# @since 2.12 +_comp_compgen_mac_addresses() { - local re='\([A-Fa-f0-9]\{2\}:\)\{5\}[A-Fa-f0-9]\{2\}' + local _re='\([A-Fa-f0-9]\{2\}:\)\{5\}[A-Fa-f0-9]\{2\}' local PATH="$PATH:/sbin:/usr/sbin" + local -a addresses # Local interfaces # - ifconfig on Linux: HWaddr or ether # - ifconfig on FreeBSD: ether # - ip link: link/ether - COMPREPLY+=($( + _comp_compgen -v addresses split -- "$( { - LC_ALL=C ifconfig -a || ip link show + LC_ALL=C ifconfig -a || ip -c=never link show || ip link show } 2>/dev/null | command sed -ne \ - "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($re\)[[:space:]].*/\1/p" -ne \ - "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($re\)[[:space:]]*$/\1/p" -ne \ - "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($re\)[[:space:]].*|\2|p" -ne \ - "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($re\)[[:space:]]*$|\2|p" - )) + "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($_re\)[[:space:]].*/\1/p" -ne \ + "s/.*[[:space:]]HWaddr[[:space:]]\{1,\}\($_re\)[[:space:]]*$/\1/p" -ne \ + "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($_re\)[[:space:]].*|\2|p" -ne \ + "s|.*[[:space:]]\(link/\)\{0,1\}ether[[:space:]]\{1,\}\($_re\)[[:space:]]*$|\2|p" + )" # ARP cache - COMPREPLY+=($({ - arp -an || ip neigh show - } 2>/dev/null | command sed -ne \ - "s/.*[[:space:]]\($re\)[[:space:]].*/\1/p" -ne \ - "s/.*[[:space:]]\($re\)[[:space:]]*$/\1/p")) + _comp_compgen -av addresses split -- "$( + { + arp -an || ip -c=never neigh show || ip neigh show + } 2>/dev/null | command sed -ne \ + "s/.*[[:space:]]\($_re\)[[:space:]].*/\1/p" -ne \ + "s/.*[[:space:]]\($_re\)[[:space:]]*$/\1/p" + )" # /etc/ethers - COMPREPLY+=($(command sed -ne \ - "s/^[[:space:]]*\($re\)[[:space:]].*/\1/p" /etc/ethers 2>/dev/null)) + _comp_compgen -av addresses split -- "$(command sed -ne \ + "s/^[[:space:]]*\($_re\)[[:space:]].*/\1/p" /etc/ethers 2>/dev/null)" - COMPREPLY=($(compgen -W '${COMPREPLY[@]}' -- "$cur")) - __ltrim_colon_completions "$cur" + _comp_compgen -U addresses ltrim_colon "${addresses[@]}" } # This function completes on configured network interfaces # -_configured_interfaces() +# @since 2.12 +_comp_compgen_configured_interfaces() { + local -a files if [[ -f /etc/debian_version ]]; then # Debian system - COMPREPLY=($(compgen -W "$(command sed -ne 's|^iface \([^ ]\{1,\}\).*$|\1|p' \ - /etc/network/interfaces /etc/network/interfaces.d/* 2>/dev/null)" \ - -- "$cur")) + _comp_expand_glob files '/etc/network/interfaces /etc/network/interfaces.d/*' || return 0 + _comp_compgen -U files split -- "$(command sed -ne \ + 's|^iface \([^ ]\{1,\}\).*$|\1|p' "${files[@]}" 2>/dev/null)" elif [[ -f /etc/SuSE-release ]]; then # SuSE system - COMPREPLY=($(compgen -W "$(printf '%s\n' \ - /etc/sysconfig/network/ifcfg-* | - command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")) + _comp_expand_glob files '/etc/sysconfig/network/ifcfg-*' || return 0 + _comp_compgen -U files split -- "$(printf '%s\n' "${files[@]}" | + command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" elif [[ -f /etc/pld-release ]]; then # PLD Linux - COMPREPLY=($(compgen -W "$(command ls -B \ - /etc/sysconfig/interfaces | - command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")) + _comp_compgen -U files split -- "$(command ls -B /etc/sysconfig/interfaces | + command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" else # Assume Red Hat - COMPREPLY=($(compgen -W "$(printf '%s\n' \ - /etc/sysconfig/network-scripts/ifcfg-* | - command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" -- "$cur")) + _comp_expand_glob files '/etc/sysconfig/network-scripts/ifcfg-*' || return 0 + _comp_compgen -U files split -- "$(printf '%s\n' "${files[@]}" | + command sed -ne 's|.*ifcfg-\([^*].*\)$|\1|p')" fi } # Local IP addresses. +# If producing IPv6 completions, `_comp_initialize` with `-n :`. +# # -4: IPv4 addresses only (default) # -6: IPv6 addresses only # -a: All addresses # -_ip_addresses() +# @since 2.12 +_comp_compgen_ip_addresses() { - local n + local _n case ${1-} in - -a) n='6\?' ;; - -6) n='6' ;; - *) n= ;; + -a) _n='6\{0,1\}' ;; + -6) _n='6' ;; + *) _n= ;; esac local PATH=$PATH:/sbin - local addrs=$({ - LC_ALL=C ifconfig -a || ip addr show + local addrs + _comp_compgen -v addrs split -- "$({ + LC_ALL=C ifconfig -a || ip -c=never addr show || ip addr show } 2>/dev/null | command sed -e 's/[[:space:]]addr:/ /' -ne \ - "s|.*inet${n}[[:space:]]\{1,\}\([^[:space:]/]*\).*|\1|p") - COMPREPLY+=($(compgen -W "$addrs" -- "${cur-}")) + "s|.*inet${_n}[[:space:]]\{1,\}\([^[:space:]/]*\).*|\1|p")" || + return + + if [[ ! $_n ]]; then + _comp_compgen -U addrs set "${addrs[@]}" + else + _comp_compgen -U addrs ltrim_colon "${addrs[@]}" + fi } -# This function completes on available kernels +# This function completes on available kernel versions # -_kernel_versions() +# @since 2.12 +_comp_compgen_kernel_versions() { - COMPREPLY=($(compgen -W '$(command ls /lib/modules)' -- "$cur")) + _comp_compgen_split -- "$(command ls /lib/modules)" } # This function completes on all available network interfaces # -a: restrict to active interfaces only # -w: restrict to wireless interfaces only # -_available_interfaces() +# @since 2.12 +_comp_compgen_available_interfaces() { local PATH=$PATH:/sbin - - COMPREPLY=($({ - if [[ ${1:-} == -w ]]; then + local generated + _comp_compgen -v generated split -- "$({ + if [[ ${1-} == -w ]]; then iwconfig - elif [[ ${1:-} == -a ]]; then - ifconfig || ip link show up + elif [[ ${1-} == -a ]]; then + ifconfig || ip -c=never link show up || ip link show up else - ifconfig -a || ip link show + ifconfig -a || ip -c=never link show || ip link show fi - } 2>/dev/null | awk \ - '/^[^ \t]/ { if ($1 ~ /^[0-9]+:/) { print $2 } else { print $1 } }')) - - COMPREPLY=($(compgen -W '${COMPREPLY[@]/%[[:punct:]]/}' -- "$cur")) + } 2>/dev/null | _comp_awk \ + '/^[^ \t]/ { if ($1 ~ /^[0-9]+:/) { print $2 } else { print $1 } }')" && + _comp_compgen -U generated set "${generated[@]}" } # Echo number of CPUs, falling back to 1 on failure. -_ncpus() +# @var[out] REPLY +# @return 0 if it successfully obtained the number of CPUs, or otherwise 1 +# @since 2.12 +_comp_get_ncpus() { local var=NPROCESSORS_ONLN - [[ $OSTYPE == *linux* ]] && var=_$var - local n=$(getconf $var 2>/dev/null) - printf %s ${n:-1} + [[ $OSTYPE == *@(linux|msys|cygwin)* ]] && var=_$var + if REPLY=$(getconf $var 2>/dev/null) && ((REPLY >= 1)); then + return 0 + else + REPLY=1 + return 1 + fi } # Perform tilde (~) completion -# @return True (0) if completion needs further processing, -# False (> 0) if tilde is followed by a valid username, completions -# are put in COMPREPLY and no further processing is necessary. -_tilde() +# @return False (1) if completion needs further processing, +# True (0) if tilde is followed by a valid username, completions are +# put in COMPREPLY and no further processing is necessary. +# @since 2.12 +_comp_compgen_tilde() { - local result=0 - if [[ ${1-} == \~* && $1 != */* ]]; then + if [[ ${cur-} == \~* && $cur != */* ]]; then # Try generate ~username completions - COMPREPLY=($(compgen -P '~' -u -- "${1#\~}")) - result=${#COMPREPLY[@]} - # 2>/dev/null for direct invocation, e.g. in the _tilde unit test - ((result > 0)) && compopt -o filenames 2>/dev/null + if _comp_compgen -c "${cur#\~}" -- -P '~' -u; then + # 2>/dev/null for direct invocation, e.g. in the + # _comp_compgen_tilde unit test + compopt -o filenames 2>/dev/null + return 0 + fi fi - return $result + return 1 } -# Expand variable starting with tilde (~) +# Expand string starting with tilde (~) # We want to expand ~foo/... to /home/foo/... to avoid problems when # word-to-complete starting with a tilde is fed to commands and ending up # quoted instead of expanded. @@ -1053,11 +1784,11 @@ _tilde() # a dollar sign variable ($) or asterisk (*) is not expanded. # Example usage: # -# $ v="~"; __expand_tilde_by_ref v; echo "$v" +# $ _comp_expand_tilde "~"; echo "$REPLY" # # Example output: # -# v output +# $1 REPLY # -------- ---------------- # ~ /home/user # ~foo/bar /home/foo/bar @@ -1065,17 +1796,22 @@ _tilde() # ~foo/a b /home/foo/a b # ~foo/* /home/foo/* # -# @param $1 Name of variable (not the value of the variable) to expand -__expand_tilde_by_ref() -{ - if [[ ${!1-} == \~* ]]; then - eval $1="$(printf ~%q "${!1#\~}")" +# @param $1 Value to expand +# @var[out] REPLY Expanded result +# @since 2.12 +_comp_expand_tilde() +{ + REPLY=$1 + if [[ $1 == \~* ]]; then + printf -v REPLY '~%q' "${1#\~}" + eval "REPLY=$REPLY" fi -} # __expand_tilde_by_ref() +} # This function expands tildes in pathnames # -_expand() +# @since 2.12 +_comp_expand() { # Expand ~username type directory specifications. We want to expand # ~foo/... to /home/foo/... to avoid problems when $cur starting with @@ -1083,58 +1819,67 @@ _expand() case ${cur-} in ~*/*) - __expand_tilde_by_ref cur + local REPLY + _comp_expand_tilde "$cur" + cur=$REPLY ;; ~*) - _tilde "$cur" || - eval COMPREPLY[0]="$(printf ~%q "${COMPREPLY[0]#\~}")" - return ${#COMPREPLY[@]} + _comp_compgen -v COMPREPLY tilde && + eval "COMPREPLY[0]=$(printf ~%q "${COMPREPLY[0]#\~}")" && + return 1 ;; esac + return 0 } # Process ID related functions. # for AIX and Solaris we use X/Open syntax, BSD for others. +# +# @since 2.12 if [[ $OSTYPE == *@(solaris|aix)* ]]; then # This function completes on process IDs. - _pids() + _comp_compgen_pids() { - COMPREPLY=($(compgen -W '$(command ps -efo pid | command sed 1d)' -- "$cur")) + _comp_compgen_split -- "$(command ps -efo pid | command sed 1d)" } - _pgids() + _comp_compgen_pgids() { - COMPREPLY=($(compgen -W '$(command ps -efo pgid | command sed 1d)' -- "$cur")) + _comp_compgen_split -- "$(command ps -efo pgid | command sed 1d)" } - _pnames() + _comp_compgen_pnames() { - COMPREPLY=($(compgen -X '<defunct>' -W '$(command ps -efo comm | \ - command sed -e 1d -e "s:.*/::" -e "s/^-//" | sort -u)' -- "$cur")) + _comp_compgen_split -X '<defunct>' -- "$(command ps -efo comm | + command sed -e 1d -e 's:.*/::' -e 's/^-//' | sort -u)" } else - _pids() + _comp_compgen_pids() { - COMPREPLY=($(compgen -W '$(command ps axo pid=)' -- "$cur")) + _comp_compgen_split -- "$(command ps ax -o pid=)" } - _pgids() + _comp_compgen_pgids() { - COMPREPLY=($(compgen -W '$(command ps axo pgid=)' -- "$cur")) + _comp_compgen_split -- "$(command ps ax -o pgid=)" } # @param $1 if -s, don't try to avoid truncated command names - _pnames() + _comp_compgen_pnames() { - local -a procs + local -a procs=() if [[ ${1-} == -s ]]; then - procs=($(command ps axo comm | command sed -e 1d)) + _comp_split procs "$(command ps ax -o comm | command sed -e 1d)" else - local line i=-1 ifs=$IFS - IFS=$'\n' - local -a psout=($(command ps axo command=)) - IFS=$ifs + # Some versions of ps don't support "command", but do "comm", e.g. + # some busybox ones. Fall back + local -a psout + _comp_split -l psout "$({ + command ps ax -o command= || command ps ax -o comm= + } 2>/dev/null)" + local line i=-1 for line in "${psout[@]}"; do if ((i == -1)); then - # First line, see if it has COMMAND column header. For example - # the busybox ps does that, i.e. doesn't respect axo command= + # First line, see if it has COMMAND column header. For + # example some busybox ps versions do that, i.e. don't + # respect command= if [[ $line =~ ^(.*[[:space:]])COMMAND([[:space:]]|$) ]]; then # It does; store its index. i=${#BASH_REMATCH[1]} @@ -1146,147 +1891,165 @@ else # line=${line:i} # take command starting from found index line=${line%% *} # trim arguments - procs+=($line) + [[ $line ]] && procs+=("$line") fi done if ((i == -1)); then - # Regular axo command= parsing + # Regular command= parsing for line in "${psout[@]}"; do if [[ $line =~ ^[[(](.+)[])]$ ]]; then - procs+=(${BASH_REMATCH[1]}) + procs+=("${BASH_REMATCH[1]}") else line=${line%% *} # trim arguments line=${line##@(*/|-)} # trim leading path and - - procs+=($line) + [[ $line ]] && procs+=("$line") fi done fi fi - COMPREPLY=($(compgen -X "<defunct>" -W '${procs[@]}' -- "$cur")) + ((${#procs[@]})) && + _comp_compgen -U procs -- -X "<defunct>" -W '"${procs[@]}"' } fi # This function completes on user IDs # -_uids() +# @since 2.12 +_comp_compgen_uids() { if type getent &>/dev/null; then - COMPREPLY=($(compgen -W '$(getent passwd | cut -d: -f3)' -- "$cur")) + _comp_compgen_split -- "$(getent passwd | cut -d: -f3)" elif type perl &>/dev/null; then - COMPREPLY=($(compgen -W '$(perl -e '"'"'while (($uid) = (getpwent)[2]) { print $uid . "\n" }'"'"')' -- "$cur")) + _comp_compgen_split -- "$(perl -e 'while (($uid) = (getpwent)[2]) { print $uid . "\n" }')" else # make do with /etc/passwd - COMPREPLY=($(compgen -W '$(cut -d: -f3 /etc/passwd)' -- "$cur")) + _comp_compgen_split -- "$(cut -d: -f3 /etc/passwd)" fi } # This function completes on group IDs # -_gids() +# @since 2.12 +_comp_compgen_gids() { if type getent &>/dev/null; then - COMPREPLY=($(compgen -W '$(getent group | cut -d: -f3)' -- "$cur")) + _comp_compgen_split -- "$(getent group | cut -d: -f3)" elif type perl &>/dev/null; then - COMPREPLY=($(compgen -W '$(perl -e '"'"'while (($gid) = (getgrent)[2]) { print $gid . "\n" }'"'"')' -- "$cur")) + _comp_compgen_split -- "$(perl -e 'while (($gid) = (getgrent)[2]) { print $gid . "\n" }')" else # make do with /etc/group - COMPREPLY=($(compgen -W '$(cut -d: -f3 /etc/group)' -- "$cur")) + _comp_compgen_split -- "$(cut -d: -f3 /etc/group)" fi } # Glob for matching various backup files. # -_backup_glob='@(#*#|*@(~|.@(bak|orig|rej|swp|dpkg*|rpm@(orig|new|save))))' +_comp_backup_glob='@(#*#|*@(~|.@(bak|orig|rej|swp|@(dpkg|ucf)-*|rpm@(orig|new|save))))' # Complete on xinetd services # -_xinetd_services() +# @since 2.12 +_comp_compgen_xinetd_services() { - local xinetddir=${BASHCOMP_XINETDDIR:-/etc/xinetd.d} + local xinetddir=${_comp__test_xinetd_dir:-/etc/xinetd.d} if [[ -d $xinetddir ]]; then - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - local -a svcs=($(printf '%s\n' $xinetddir/!($_backup_glob))) - $reset - ((!${#svcs[@]})) || - COMPREPLY+=($(compgen -W '${svcs[@]#$xinetddir/}' -- "${cur-}")) + local -a svcs + if _comp_expand_glob svcs '$xinetddir/!($_comp_backup_glob)'; then + _comp_compgen -U svcs -U xinetddir -- -W '"${svcs[@]#$xinetddir/}"' + fi fi } # This function completes on services # -_services() +# @since 2.12 +_comp_compgen_services() { local sysvdirs - _sysvdirs + _comp_sysvdirs || return 1 - local IFS=$' \t\n' reset=$(shopt -p nullglob) - shopt -s nullglob - COMPREPLY=( - $(printf '%s\n' ${sysvdirs[0]}/!($_backup_glob|functions|README))) - $reset + local services + _comp_expand_glob services '${sysvdirs[0]}/!($_comp_backup_glob|functions|README)' - COMPREPLY+=($({ + local _generated=$({ systemctl list-units --full --all || systemctl list-unit-files } 2>/dev/null | - awk '$1 ~ /\.service$/ { sub("\\.service$", "", $1); print $1 }')) + _comp_awk '$1 ~ /\.service$/ { sub("\\.service$", "", $1); print $1 }') + _comp_split -la services "$_generated" if [[ -x /sbin/upstart-udev-bridge ]]; then - COMPREPLY+=($(initctl list 2>/dev/null | cut -d' ' -f1)) + _comp_split -la services "$(initctl list 2>/dev/null | cut -d' ' -f1)" fi - COMPREPLY=($(compgen -W '${COMPREPLY[@]#${sysvdirs[0]}/}' -- "$cur")) + ((${#services[@]})) || return 1 + _comp_compgen -U services -U sysvdirs -- -W '"${services[@]#${sysvdirs[0]}/}"' } # This completes on a list of all available service scripts for the # 'service' command and/or the SysV init.d directory, followed by # that script's available commands +# This function is in the main bash_completion file rather than in a separate +# one, because we set it up eagerly as completer for scripts in sysv init dirs +# below. # -_service() +# @since 2.12 +_comp_complete_service() { - local cur prev words cword - _init_completion || return + local cur prev words cword comp_args + _comp_initialize -- "$@" || return # don't complete past 2nd token ((cword > 2)) && return if [[ $cword -eq 1 && $prev == ?(*/)service ]]; then - _services - [[ -e /etc/mandrake-release ]] && _xinetd_services + _comp_compgen_services + [[ -e /etc/mandrake-release ]] && _comp_compgen_xinetd_services else local sysvdirs - _sysvdirs - COMPREPLY=($(compgen -W '`command sed -e "y/|/ /" \ - -ne "s/^.*\(U\|msg_u\)sage.*{\(.*\)}.*$/\2/p" \ - ${sysvdirs[0]}/${prev##*/} 2>/dev/null` start stop' -- "$cur")) + _comp_sysvdirs || return 1 + _comp_compgen_split -l -- "$(command sed -e 'y/|/ /' \ + -ne 's/^.*\(U\|msg_u\)sage.*{\(.*\)}.*$/\2/p' \ + "${sysvdirs[0]}/${prev##*/}" 2>/dev/null) start stop" fi } && - complete -F _service service -_sysvdirs -for svcdir in "${sysvdirs[@]}"; do - for svc in $svcdir/!($_backup_glob); do - [[ -x $svc ]] && complete -F _service $svc - done -done -unset svc svcdir sysvdirs + complete -F _comp_complete_service service -# This function completes on modules +_comp__init_set_up_service_completions() +{ + local sysvdirs svc svcdir svcs + _comp_sysvdirs && + for svcdir in "${sysvdirs[@]}"; do + if _comp_expand_glob svcs '"$svcdir"/!($_comp_backup_glob)'; then + for svc in "${svcs[@]}"; do + [[ -x $svc ]] && complete -F _comp_complete_service "$svc" + done + fi + done + unset -f "$FUNCNAME" +} +_comp__init_set_up_service_completions + +# This function completes on kernel modules +# @param $1 kernel version # -_modules() +# @since 2.12 +_comp_compgen_kernel_modules() { - local modpath - modpath=/lib/modules/$1 - COMPREPLY=($(compgen -W "$(command ls -RL $modpath 2>/dev/null | - command sed -ne 's/^\(.*\)\.k\{0,1\}o\(\.[gx]z\)\{0,1\}$/\1/p')" -- "$cur")) + local _modpath=/lib/modules/$1 + _comp_compgen_split -- "$(command ls -RL "$_modpath" 2>/dev/null | + command sed -ne 's/^\(.*\)\.k\{0,1\}o\(\.[gx]z\)\{0,1\}$/\1/p' \ + -e 's/^\(.*\)\.ko\.zst$/\1/p')" } -# This function completes on installed modules +# This function completes on inserted kernel modules +# @param $1 prefix to filter with, default $cur # -_installed_modules() +# @since 2.12 +_comp_compgen_inserted_kernel_modules() { - COMPREPLY=($(compgen -W "$(PATH="$PATH:/sbin" lsmod | - awk '{if (NR != 1) print $1}')" -- "$1")) + _comp_compgen -c "${1:-$cur}" split -- "$(PATH="$PATH:/sbin" lsmod | + _comp_awk '{if (NR != 1) print $1}')" } # This function completes on user or user:group format; as for chown and cpio. @@ -1296,7 +2059,9 @@ _installed_modules() # # @param $1 If -u, only return users/groups the user has access to in # context of current completion. -_usergroup() +# +# @since 2.12 +_comp_compgen_usergroups() { if [[ $cur == *\\\\* || $cur == *:*:* ]]; then # Give up early on if something seems horribly wrong. @@ -1305,27 +2070,26 @@ _usergroup() # Completing group after 'user\:gr<TAB>'. # Reply with a list of groups prefixed with 'user:', readline will # escape to the colon. - local prefix - prefix=${cur%%*([^:])} - prefix=${prefix//\\/} - local mycur="${cur#*[:]}" + local tmp if [[ ${1-} == -u ]]; then - _allowed_groups "$mycur" + _comp_compgen -v tmp -c "${cur#*:}" allowed_groups else - local IFS=$'\n' - COMPREPLY=($(compgen -g -- "$mycur")) + _comp_compgen -v tmp -c "${cur#*:}" -- -g + fi + if ((${#tmp[@]})); then + local _prefix=${cur%%*([^:])} + _prefix=${_prefix//\\/} + _comp_compgen -Rv tmp -- -P "$_prefix" -W '"${tmp[@]}"' + _comp_compgen -U tmp set "${tmp[@]}" fi - COMPREPLY=($(compgen -P "$prefix" -W "${COMPREPLY[@]}")) elif [[ $cur == *:* ]]; then # Completing group after 'user:gr<TAB>'. # Reply with a list of unprefixed groups since readline with split on : # and only replace the 'gr' part - local mycur="${cur#*:}" if [[ ${1-} == -u ]]; then - _allowed_groups "$mycur" + _comp_compgen -c "${cur#*:}" allowed_groups else - local IFS=$'\n' - COMPREPLY=($(compgen -g -- "$mycur")) + _comp_compgen -c "${cur#*:}" -- -g fi else # Completing a partial 'usernam<TAB>'. @@ -1334,168 +2098,274 @@ _usergroup() # slash. It's better to complete into 'chown username ' than 'chown # username\:'. if [[ ${1-} == -u ]]; then - _allowed_users "$cur" + _comp_compgen_allowed_users else - local IFS=$'\n' - COMPREPLY=($(compgen -u -- "$cur")) + _comp_compgen -- -u fi fi } -_allowed_users() +# @since 2.12 +_comp_compgen_allowed_users() { - if _complete_as_root; then - local IFS=$'\n' - COMPREPLY=($(compgen -u -- "${1:-$cur}")) + if _comp_as_root; then + _comp_compgen -- -u else - local IFS=$'\n ' - COMPREPLY=($(compgen -W \ - "$(id -un 2>/dev/null || whoami 2>/dev/null)" -- "${1:-$cur}")) + _comp_compgen_split -- "$(id -un 2>/dev/null || whoami 2>/dev/null)" fi } -_allowed_groups() +# @since 2.12 +_comp_compgen_allowed_groups() { - if _complete_as_root; then - local IFS=$'\n' - COMPREPLY=($(compgen -g -- "$1")) + if _comp_as_root; then + _comp_compgen -- -g else - local IFS=$'\n ' - COMPREPLY=($(compgen -W \ - "$(id -Gn 2>/dev/null || groups 2>/dev/null)" -- "$1")) + _comp_compgen_split -- "$(id -Gn 2>/dev/null || groups 2>/dev/null)" fi } +# @since 2.12 +_comp_compgen_selinux_users() +{ + _comp_compgen_split -- "$(semanage user -nl 2>/dev/null | + _comp_awk '{ print $1 }')" +} + # This function completes on valid shells +# @param $1 chroot to search from # -_shells() -{ - local shell rest - while read -r shell rest; do - [[ $shell == /* && $shell == "$cur"* ]] && COMPREPLY+=($shell) - done 2>/dev/null </etc/shells +# @since 2.12 +_comp_compgen_shells() +{ + local -a shells=() + local _shell _rest + while read -r _shell _rest; do + [[ $_shell == /* ]] && shells+=("$_shell") + done 2>/dev/null <"${1-}"/etc/shells + _comp_compgen -U shells -- -W '"${shells[@]}"' } # This function completes on valid filesystem types # -_fstypes() +# @since 2.12 +_comp_compgen_fstypes() { - local fss + local _fss if [[ -e /proc/filesystems ]]; then # Linux - fss="$(cut -d$'\t' -f2 /proc/filesystems) - $(awk '! /\*/ { print $NF }' /etc/filesystems 2>/dev/null)" + _fss="$(cut -d$'\t' -f2 /proc/filesystems) + $(_comp_awk '! /\*/ { print $NF }' /etc/filesystems 2>/dev/null)" else # Generic - fss="$(awk '/^[ \t]*[^#]/ { print $3 }' /etc/fstab 2>/dev/null) - $(awk '/^[ \t]*[^#]/ { print $3 }' /etc/mnttab 2>/dev/null) - $(awk '/^[ \t]*[^#]/ { print $4 }' /etc/vfstab 2>/dev/null) - $(awk '{ print $1 }' /etc/dfs/fstypes 2>/dev/null) + _fss="$(_comp_awk '/^[ \t]*[^#]/ { print $3 }' /etc/fstab 2>/dev/null) + $(_comp_awk '/^[ \t]*[^#]/ { print $3 }' /etc/mnttab 2>/dev/null) + $(_comp_awk '/^[ \t]*[^#]/ { print $4 }' /etc/vfstab 2>/dev/null) + $(_comp_awk '{ print $1 }' /etc/dfs/fstypes 2>/dev/null) + $(lsvfs 2>/dev/null | _comp_awk '$1 !~ /^(Filesystem|[^a-zA-Z])/ { print $1 }') $([[ -d /etc/fs ]] && command ls /etc/fs)" fi - [[ -n $fss ]] && COMPREPLY+=($(compgen -W "$fss" -- "$cur")) + [[ $_fss ]] && _comp_compgen_split -- "$_fss" +} + +# Get absolute path to a file, with rudimentary canonicalization. +# No symlink resolution or existence checks are done; +# see `_comp_realcommand` for those. +# @param $1 The file +# @var[out] REPLY The path +# @since 2.12 +_comp_abspath() +{ + REPLY=$1 + case $REPLY in + /*) ;; + ../*) REPLY=$PWD/${REPLY:3} ;; + *) REPLY=$PWD/$REPLY ;; + esac + while [[ $REPLY == */./* ]]; do + REPLY=${REPLY//\/.\//\/} + done + REPLY=${REPLY//+(\/)/\/} } # Get real command. -# - arg: $1 Command -# - stdout: Filename of command in PATH with possible symbolic links resolved. -# Empty string if command not found. -# - return: True (0) if command found, False (> 0) if not. -_realcommand() -{ - type -P "$1" >/dev/null && { - if type -p realpath >/dev/null; then - realpath "$(type -P "$1")" - elif type -p greadlink >/dev/null; then - greadlink -f "$(type -P "$1")" - elif type -p readlink >/dev/null; then - readlink -f "$(type -P "$1")" - else - type -P "$1" - fi - } +# Command is the filename of command in PATH with possible symlinks resolved +# (if resolve tooling available), empty string if command not found. +# @param $1 Command +# @var[out] REPLY Resulting string +# @return True (0) if command found, False (> 0) if not. +# @since 2.12 +_comp_realcommand() +{ + REPLY="" + local file + file=$(type -P "$1") || return $? + if type -p realpath >/dev/null; then + REPLY=$(realpath "$file") + elif type -p greadlink >/dev/null; then + REPLY=$(greadlink -f "$file") + elif type -p readlink >/dev/null; then + REPLY=$(readlink -f "$file") + else + _comp_abspath "$file" + fi } -# This function returns the first argument, excluding options -# @param $1 chars Characters out of $COMP_WORDBREAKS which should -# NOT be considered word breaks. See __reassemble_comp_words_by_ref. -_get_first_arg() -{ - local i +# This function returns the position of the first argument, excluding options +# +# Options: +# -a GLOB Pattern of options that take an option argument +# +# @var[out] REPLY Position of the first argument before the current one being +# completed if any, or otherwise an empty string +# @return True (0) if any argument is found, False (> 0) otherwise. +# @since 2.12 +_comp_locate_first_arg() +{ + local has_optarg="" + local OPTIND=1 OPTARG="" OPTERR=0 _opt + while getopts ':a:' _opt "$@"; do + case $_opt in + a) has_optarg=$OPTARG ;; + *) + echo "bash_completion: $FUNCNAME: usage error" >&2 + return 2 + ;; + esac + done + shift "$((OPTIND - 1))" - arg= - for ((i = 1; i < COMP_CWORD; i++)); do - if [[ ${COMP_WORDS[i]} != -* ]]; then - arg=${COMP_WORDS[i]} + local i + REPLY= + for ((i = 1; i < cword; i++)); do + # shellcheck disable=SC2053 + if [[ $has_optarg && ${words[i]} == $has_optarg ]]; then + ((i++)) + elif [[ ${words[i]} != -?* ]]; then + REPLY=$i + return 0 + elif [[ ${words[i]} == -- ]]; then + ((i + 1 < cword)) && REPLY=$((i + 1)) && return 0 break fi done + return 1 } -# This function counts the number of args, excluding options -# @param $1 chars Characters out of $COMP_WORDBREAKS which should -# NOT be considered word breaks. See __reassemble_comp_words_by_ref. -# @param $2 glob Options whose following argument should not be counted -# @param $3 glob Options that should be counted as args -_count_args() +# This function returns the first argument, excluding options +# +# Options: +# -a GLOB Pattern of options that take an option argument +# +# @var[out] REPLY First argument before the current one being completed if any, +# or otherwise an empty string +# @return True (0) if any argument is found, False (> 0) otherwise. +# @since 2.12 +_comp_get_first_arg() { - local i cword words - __reassemble_comp_words_by_ref "${1-}" words cword + _comp_locate_first_arg "$@" && REPLY=${words[REPLY]} +} - args=1 +# This function counts the number of args, excluding options +# +# Options: +# -n CHARS Characters out of $COMP_WORDBREAKS which should +# NOT be considered word breaks. See +# _comp__reassemble_words. +# -a GLOB Options whose following argument should not be counted +# -i GLOB Options that should be counted as args +# +# @var[out] REPLY Return the number of arguments +# @since 2.12 +_comp_count_args() +{ + local has_optarg="" has_exclude="" exclude="" glob_include="" + local OPTIND=1 OPTARG="" OPTERR=0 _opt + while getopts ':a:n:i:' _opt "$@"; do + case $_opt in + a) has_optarg=$OPTARG ;; + n) has_exclude=set exclude+=$OPTARG ;; + i) glob_include=$OPTARG ;; + *) + echo "bash_completion: $FUNCNAME: usage error" >&2 + return 2 + ;; + esac + done + shift "$((OPTIND - 1))" + + if [[ $has_exclude ]]; then + local cword words + _comp__reassemble_words "$exclude<>&" words cword + fi + + local i + REPLY=1 for ((i = 1; i < cword; i++)); do # shellcheck disable=SC2053 - if [[ ${words[i]} != -* && ${words[i - 1]} != ${2-} || \ - ${words[i]} == ${3-} ]]; then - ((args++)) + if [[ $has_optarg && ${words[i]} == $has_optarg ]]; then + ((i++)) + elif [[ ${words[i]} != -?* || $glob_include && ${words[i]} == $glob_include ]]; then + ((REPLY++)) + elif [[ ${words[i]} == -- ]]; then + ((REPLY += cword - i - 1)) + break fi done } # This function completes on PCI IDs # -_pci_ids() +# @since 2.12 +_comp_compgen_pci_ids() { - COMPREPLY+=($(compgen -W \ - "$(PATH="$PATH:/sbin" lspci -n | awk '{print $3}')" -- "$cur")) + _comp_compgen_split -- "$(PATH="$PATH:/sbin" lspci -n | _comp_awk '{print $3}')" } # This function completes on USB IDs # -_usb_ids() +# @since 2.12 +_comp_compgen_usb_ids() { - COMPREPLY+=($(compgen -W \ - "$(PATH="$PATH:/sbin" lsusb | awk '{print $6}')" -- "$cur")) + _comp_compgen_split -- "$(PATH="$PATH:/sbin" lsusb | _comp_awk '{print $6}')" } # CD device names -_cd_devices() +# +# @since 2.12 +_comp_compgen_cd_devices() { - COMPREPLY+=($(compgen -f -d -X "!*/?([amrs])cd*" -- "${cur:-/dev/}")) + _comp_compgen -c "${cur:-/dev/}" -- -f -d -X "!*/?([amrs])cd!(c-*)" } # DVD device names -_dvd_devices() +# +# @since 2.12 +_comp_compgen_dvd_devices() { - COMPREPLY+=($(compgen -f -d -X "!*/?(r)dvd*" -- "${cur:-/dev/}")) + _comp_compgen -c "${cur:-/dev/}" -- -f -d -X "!*/?(r)dvd*" } # TERM environment variable values -_terms() +# +# @since 2.12 +_comp_compgen_terms() { - COMPREPLY+=($(compgen -W "$({ + _comp_compgen_split -- "$({ command sed -ne 's/^\([^[:space:]#|]\{2,\}\)|.*/\1/p' /etc/termcap { toe -a || toe - } | awk '{ print $1 }' - find /{etc,lib,usr/lib,usr/share}/terminfo/? -type f -maxdepth 1 | - awk -F/ '{ print $NF }' - } 2>/dev/null)" -- "$cur")) + } | _comp_awk '{ print $1 }' + _comp_expand_glob dirs '/{etc,lib,usr/lib,usr/share}/terminfo/?' && + find "${dirs[@]}" -type f -maxdepth 1 | + _comp_awk -F / '{ print $NF }' + } 2>/dev/null)" } -_bashcomp_try_faketty() +# @since 2.12 +_comp_try_faketty() { if type unbuffer &>/dev/null; then unbuffer -p "$@" @@ -1514,95 +2384,122 @@ _bashcomp_try_faketty() # This function provides simple user@host completion # -_user_at_host() +# @since 2.12 +_comp_complete_user_at_host() { - local cur prev words cword - _init_completion -n : || return + local cur prev words cword comp_args + _comp_initialize -n : -- "$@" || return if [[ $cur == *@* ]]; then - _known_hosts_real "$cur" + _comp_compgen_known_hosts "$cur" else - COMPREPLY=($(compgen -u -S @ -- "$cur")) + _comp_compgen -- -u -S @ compopt -o nospace fi } -shopt -u hostcomplete && complete -F _user_at_host talk ytalk finger +shopt -u hostcomplete && complete -F _comp_complete_user_at_host talk ytalk finger # NOTE: Using this function as a helper function is deprecated. Use -# `_known_hosts_real' instead. -_known_hosts() -{ - local cur prev words cword - _init_completion -n : || return - - # NOTE: Using `_known_hosts' as a helper function and passing options - # to `_known_hosts' is deprecated: Use `_known_hosts_real' instead. - local options - [[ ${1-} == -a || ${2-} == -a ]] && options=-a - [[ ${1-} == -c || ${2-} == -c ]] && options+=" -c" - _known_hosts_real ${options-} -- "$cur" -} # _known_hosts() +# `_comp_compgen_known_hosts' instead. +# @since 2.12 +_comp_complete_known_hosts() +{ + local cur prev words cword comp_args + _comp_initialize -n : -- "$@" || return + + # NOTE: Using `_known_hosts' (the old name of `_comp_complete_known_hosts') + # as a helper function and passing options to `_known_hosts' is + # deprecated: Use `_comp_compgen_known_hosts' instead. + local -a options=() + [[ ${1-} == -a || ${2-} == -a ]] && options+=(-a) + [[ ${1-} == -c || ${2-} == -c ]] && options+=(-c) + local IFS=$' \t\n' # Workaround for connected ${v+"$@"} in bash < 4.4 + _comp_compgen_known_hosts ${options[@]+"${options[@]}"} -- "$cur" +} # Helper function to locate ssh included files in configs # This function looks for the "Include" keyword in ssh config files and # includes them recursively, adding each result to the config variable. -_included_ssh_config_files() +_comp__included_ssh_config_files() { (($# < 1)) && echo "bash_completion: $FUNCNAME: missing mandatory argument CONFIG" >&2 - local configfile i f + local configfile i files f REPLY configfile=$1 - local reset=$(shopt -po noglob) - set -o noglob - local included=($(command sed -ne 's/^[[:blank:]]*[Ii][Nn][Cc][Ll][Uu][Dd][Ee][[:blank:]]\(.*\)$/\1/p' "${configfile}")) - $reset - - [[ ${included-} ]] || return - for i in "${included[@]}"; do - # Check the origin of $configfile to complete relative included paths on included - # files according to ssh_config(5): - # "[...] Files without absolute paths are assumed to be in ~/.ssh if included in a user - # configuration file or /etc/ssh if included from the system configuration file.[...]" - if ! [[ $i =~ ^\~.*|^\/.* ]]; then - if [[ $configfile =~ ^\/etc\/ssh.* ]]; then - i="/etc/ssh/$i" - else - i="$HOME/.ssh/$i" + # From man ssh_config: + # "Files without absolute paths are assumed to be in ~/.ssh if included + # in a user configuration file or /etc/ssh if included from the system + # configuration file." + # This behavior is not affected by the the including file location - + # if the system configuration file is included from the user's config, + # relative includes are still resolved in the user's ssh config directory. + local relative_include_base + if [[ $configfile == /etc/ssh* ]]; then + relative_include_base="/etc/ssh" + else + relative_include_base="$HOME/.ssh" + fi + + local depth=1 + local -a included + local -a include_files + included=("$configfile") + + # Max recursion depth per openssh's READCONF_MAX_DEPTH: + # https://github.com/openssh/openssh-portable/blob/5ec5504f1d328d5bfa64280cd617c3efec4f78f3/readconf.c#L2240 + local max_depth=16 + while ((${#included[@]} > 0 && depth++ < max_depth)); do + _comp_split include_files "$(command sed -ne 's/^[[:blank:]]*[Ii][Nn][Cc][Ll][Uu][Dd][Ee][[:blank:]]\(.*\)$/\1/p' "${included[@]}")" || return + included=() + for i in "${include_files[@]}"; do + if [[ $i != [~/]* ]]; then + i="${relative_include_base}/${i}" fi - fi - __expand_tilde_by_ref i - # In case the expanded variable contains multiple paths - set +o noglob - for f in $i; do - if [[ -r $f ]]; then - config+=("$f") - # The Included file is processed to look for Included files in itself - _included_ssh_config_files $f + _comp_expand_tilde "$i" + if _comp_expand_glob files '$REPLY'; then + # In case the expanded variable contains multiple paths + for f in "${files[@]}"; do + if [[ -r $f && ! -d $f ]]; then + config+=("$f") + included+=("$f") + fi + done fi done - $reset done -} # _included_ssh_config_files() +} -# Helper function for completing _known_hosts. +# Helper function for completing _comp_complete_known_hosts. # This function performs host completion based on ssh's config and known_hosts # files, as well as hostnames reported by avahi-browse if -# COMP_KNOWN_HOSTS_WITH_AVAHI is set to a non-empty value. Also hosts from -# HOSTFILE (compgen -A hostname) are added, unless -# COMP_KNOWN_HOSTS_WITH_HOSTFILE is set to an empty value. -# Usage: _known_hosts_real [OPTIONS] CWORD -# Options: -a Use aliases from ssh config files -# -c Use `:' suffix -# -F configfile Use `configfile' for configuration settings -# -p PREFIX Use PREFIX -# -4 Filter IPv6 addresses from results -# -6 Filter IPv4 addresses from results -# Return: Completions, starting with CWORD, are added to COMPREPLY[] -_known_hosts_real() -{ - local configfile flag prefix="" ifs=$IFS - local cur suffix="" aliases i host ipv4 ipv6 +# BASH_COMPLETION_KNOWN_HOSTS_WITH_AVAHI is set to a non-empty value. +# Also hosts from HOSTFILE (compgen -A hostname) are added, unless +# BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE is set to an empty value. +# Usage: _comp_compgen_known_hosts [OPTIONS] CWORD +# Options: +# -a Use aliases from ssh config files +# -c Use `:' suffix +# -F configfile Use `configfile' for configuration settings +# -p PREFIX Use PREFIX +# -4 Filter IPv6 addresses from results +# -6 Filter IPv4 addresses from results +# @var[out] COMPREPLY Completions, starting with CWORD, are added +# @return True (0) if one or more completions are generated, or otherwise False +# (1). +# @since 2.12 +_comp_compgen_known_hosts() +{ + local known_hosts + _comp_compgen_known_hosts__impl "$@" || return "$?" + _comp_compgen -U known_hosts set "${known_hosts[@]}" +} +_comp_compgen_known_hosts__impl() +{ + known_hosts=() + + local configfile="" flag prefix="" + local cur suffix="" aliases="" i host ipv4="" ipv6="" local -a kh tmpkh=() khd=() config=() # TODO remove trailing %foo from entries @@ -1610,21 +2507,27 @@ _known_hosts_real() local OPTIND=1 while getopts "ac46F:p:" flag "$@"; do case $flag in - a) aliases='yes' ;; + a) aliases=set ;; c) suffix=':' ;; - F) configfile=$OPTARG ;; + F) + if [[ ! $OPTARG ]]; then + echo "bash_completion: $FUNCNAME: -F: an empty filename is specified" >&2 + return 2 + fi + configfile=$OPTARG + ;; p) prefix=$OPTARG ;; - 4) ipv4=1 ;; - 6) ipv6=1 ;; + 4) ipv4=set ;; + 6) ipv6=set ;; *) echo "bash_completion: $FUNCNAME: usage error" >&2 - return 1 + return 2 ;; esac done if (($# < OPTIND)); then echo "bash_completion: $FUNCNAME: missing mandatory argument CWORD" >&2 - return 1 + return 2 fi cur=${!OPTIND} ((OPTIND += 1)) @@ -1634,68 +2537,63 @@ _known_hosts_real() printf '%s ' ${!OPTIND} shift done)" >&2 - return 1 + return 2 fi [[ $cur == *@* ]] && prefix=$prefix${cur%@*}@ && cur=${cur#*@} kh=() # ssh config files - if [[ -v configfile ]]; then - [[ -r $configfile ]] && config+=("$configfile") + if [[ $configfile ]]; then + [[ -r $configfile && ! -d $configfile ]] && config+=("$configfile") else for i in /etc/ssh/ssh_config ~/.ssh/config ~/.ssh2/config; do - [[ -r $i ]] && config+=("$i") + [[ -r $i && ! -d $i ]] && config+=("$i") done fi - local reset=$(shopt -po noglob) - set -o noglob - # "Include" keyword in ssh config files if ((${#config[@]} > 0)); then for i in "${config[@]}"; do - _included_ssh_config_files "$i" + _comp__included_ssh_config_files "$i" done fi # Known hosts files from configs if ((${#config[@]} > 0)); then - local IFS=$'\n' # expand paths (if present) to global and user known hosts files # TODO(?): try to make known hosts files with more than one consecutive # spaces in their name work (watch out for ~ expansion # breakage! Alioth#311595) - tmpkh=($(awk 'sub("^[ \t]*([Gg][Ll][Oo][Bb][Aa][Ll]|[Uu][Ss][Ee][Rr])[Kk][Nn][Oo][Ww][Nn][Hh][Oo][Ss][Tt][Ss][Ff][Ii][Ll][Ee][ \t]+", "") { print $0 }' "${config[@]}" | sort -u)) - IFS=$ifs - fi - if ((${#tmpkh[@]} != 0)); then - local j - for i in "${tmpkh[@]}"; do - # First deal with quoted entries... - while [[ $i =~ ^([^\"]*)\"([^\"]*)\"(.*)$ ]]; do - i=${BASH_REMATCH[1]}${BASH_REMATCH[3]} - j=${BASH_REMATCH[2]} - __expand_tilde_by_ref j # Eval/expand possible `~' or `~user' - [[ -r $j ]] && kh+=("$j") - done - # ...and then the rest. - for j in $i; do - __expand_tilde_by_ref j # Eval/expand possible `~' or `~user' - [[ -r $j ]] && kh+=("$j") + if _comp_split -l tmpkh "$(_comp_awk 'sub("^[ \t]*([Gg][Ll][Oo][Bb][Aa][Ll]|[Uu][Ss][Ee][Rr])[Kk][Nn][Oo][Ww][Nn][Hh][Oo][Ss][Tt][Ss][Ff][Ii][Ll][Ee][ \t=]+", "") { print $0 }' "${config[@]}" | sort -u)"; then + local tmpkh2 j REPLY + for i in "${tmpkh[@]}"; do + # First deal with quoted entries... + while [[ $i =~ ^([^\"]*)\"([^\"]*)\"(.*)$ ]]; do + i=${BASH_REMATCH[1]}${BASH_REMATCH[3]} + _comp_expand_tilde "${BASH_REMATCH[2]}" # Eval/expand possible `~' or `~user' + [[ -r $REPLY ]] && kh+=("$REPLY") + done + # ...and then the rest. + _comp_split tmpkh2 "$i" || continue + for j in "${tmpkh2[@]}"; do + _comp_expand_tilde "$j" # Eval/expand possible `~' or `~user' + [[ -r $REPLY ]] && kh+=("$REPLY") + done done - done + fi fi - if [[ ! -v configfile ]]; then + if [[ ! $configfile ]]; then # Global and user known_hosts files for i in /etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2 \ /etc/known_hosts /etc/known_hosts2 ~/.ssh/known_hosts \ ~/.ssh/known_hosts2; do - [[ -r $i ]] && kh+=("$i") + [[ -r $i && ! -d $i ]] && kh+=("$i") done for i in /etc/ssh2/knownhosts ~/.ssh2/hostkeys; do - [[ -d $i ]] && khd+=("$i"/*pub) + [[ -d $i ]] || continue + _comp_expand_glob tmpkh '"$i"/*.pub' && khd+=("${tmpkh[@]}") done fi @@ -1706,27 +2604,27 @@ _known_hosts_real() for i in "${kh[@]}"; do while read -ra tmpkh; do ((${#tmpkh[@]} == 0)) && continue - set -- "${tmpkh[@]}" # Skip entries starting with | (hashed) and # (comment) - [[ $1 == [\|\#]* ]] && continue + [[ ${tmpkh[0]} == [\|\#]* ]] && continue # Ignore leading @foo (markers) - [[ $1 == @* ]] && shift + local host_list=${tmpkh[0]} + [[ ${tmpkh[0]} == @* ]] && host_list=${tmpkh[1]-} # Split entry on commas - local IFS=, - for host in $1; do - # Skip hosts containing wildcards - [[ $host == *[*?]* ]] && continue - # Remove leading [ - host="${host#[}" - # Remove trailing ] + optional :port - host="${host%]?(:+([0-9]))}" - # Add host to candidates - COMPREPLY+=($host) - done - IFS=$ifs + local -a hosts + if _comp_split -F , hosts "$host_list"; then + for host in "${hosts[@]}"; do + # Skip hosts containing wildcards + [[ $host == *[*?]* ]] && continue + # Remove leading [ + host=${host#[} + # Remove trailing ] + optional :port + host=${host%]?(:+([0-9]))} + # Add host to candidates + [[ $host ]] && known_hosts+=("$host") + done + fi done <"$i" done - COMPREPLY=($(compgen -W '${COMPREPLY[@]}' -- "$cur")) fi if ((${#khd[@]} > 0)); then # Needs to look for files called @@ -1734,213 +2632,233 @@ _known_hosts_real() # dont fork any processes, because in a cluster environment, # there can be hundreds of hostkeys for i in "${khd[@]}"; do - if [[ $i == *key_22_$cur*.pub && -r $i ]]; then + if [[ $i == *key_22_*.pub && -r $i ]]; then host=${i/#*key_22_/} host=${host/%.pub/} - COMPREPLY+=($host) + [[ $host ]] && known_hosts+=("$host") fi done fi # apply suffix and prefix - for i in ${!COMPREPLY[*]}; do - COMPREPLY[i]=$prefix${COMPREPLY[i]}$suffix - done + ((${#known_hosts[@]})) && + _comp_compgen -v known_hosts -- -W '"${known_hosts[@]}"' -P "$prefix" -S "$suffix" fi # append any available aliases from ssh config files - if [[ ${#config[@]} -gt 0 && -v aliases ]]; then - local -a hosts=($(command sed -ne 's/^[[:blank:]]*[Hh][Oo][Ss][Tt][[:blank:]]\(.*\)$/\1/p' "${config[@]}")) - if ((${#hosts[@]} != 0)); then - COMPREPLY+=($(compgen -P "$prefix" \ - -S "$suffix" -W '${hosts[@]%%[*?%]*}' -X '\!*' -- "$cur")) + if [[ ${#config[@]} -gt 0 && $aliases ]]; then + local -a hosts + if _comp_split hosts "$(command sed -ne 's/^[[:blank:]]*[Hh][Oo][Ss][Tt][[:blank:]=]\{1,\}\(.*\)$/\1/p' "${config[@]}")"; then + _comp_compgen -av known_hosts -- -P "$prefix" \ + -S "$suffix" -W '"${hosts[@]%%[*?%]*}"' -X '@(\!*|)' fi fi # Add hosts reported by avahi-browse, if desired and it's available. - if [[ ${COMP_KNOWN_HOSTS_WITH_AVAHI-} ]] && + if [[ ${BASH_COMPLETION_KNOWN_HOSTS_WITH_AVAHI-} ]] && type avahi-browse &>/dev/null; then - # The original call to avahi-browse also had "-k", to avoid lookups - # into avahi's services DB. We don't need the name of the service, and - # if it contains ";", it may mistify the result. But on Gentoo (at - # least), -k wasn't available (even if mentioned in the manpage) some - # time ago, so... - COMPREPLY+=($(compgen -P "$prefix" -S "$suffix" -W \ - "$(avahi-browse -cpr _workstation._tcp 2>/dev/null | - awk -F';' '/^=/ { print $7 }' | sort -u)" -- "$cur")) + # Some old versions of avahi-browse reportedly didn't have -k + # (even if mentioned in the manpage); those we do not support any more. + local generated=$(avahi-browse -cprak 2>/dev/null | _comp_awk -F ';' \ + '/^=/ && $5 ~ /^_(ssh|workstation)\._tcp$/ { print $7 }' | + sort -u) + _comp_compgen -av known_hosts -- -P "$prefix" -S "$suffix" -W '$generated' fi # Add hosts reported by ruptime. if type ruptime &>/dev/null; then - COMPREPLY+=($(compgen -W \ - "$(ruptime 2>/dev/null | awk '!/^ruptime:/ { print $1 }')" \ - -- "$cur")) + local generated=$(ruptime 2>/dev/null | _comp_awk '!/^ruptime:/ { print $1 }') + _comp_compgen -av known_hosts -- -W '$generated' fi # Add results of normal hostname completion, unless - # `COMP_KNOWN_HOSTS_WITH_HOSTFILE' is set to an empty value. - if [[ -n ${COMP_KNOWN_HOSTS_WITH_HOSTFILE-1} ]]; then - COMPREPLY+=( - $(compgen -A hostname -P "$prefix" -S "$suffix" -- "$cur")) + # `BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE' is set to an empty value. + if [[ ${BASH_COMPLETION_KNOWN_HOSTS_WITH_HOSTFILE-set} ]]; then + _comp_compgen -av known_hosts -- -A hostname -P "$prefix" -S "$suffix" fi - $reset + ((${#known_hosts[@]})) || return 1 - if [[ -v ipv4 ]]; then - COMPREPLY=("${COMPREPLY[@]/*:*$suffix/}") + if [[ $ipv4 ]]; then + known_hosts=("${known_hosts[@]/*:*$suffix/}") fi - if [[ -v ipv6 ]]; then - COMPREPLY=("${COMPREPLY[@]/+([0-9]).+([0-9]).+([0-9]).+([0-9])$suffix/}") + if [[ $ipv6 ]]; then + known_hosts=("${known_hosts[@]/+([0-9]).+([0-9]).+([0-9]).+([0-9])$suffix/}") fi - if [[ -v ipv4 || -v ipv6 ]]; then - for i in "${!COMPREPLY[@]}"; do - [[ ${COMPREPLY[i]} ]] || unset -v "COMPREPLY[i]" + if [[ $ipv4 || $ipv6 ]]; then + for i in "${!known_hosts[@]}"; do + [[ ${known_hosts[i]} ]] || unset -v 'known_hosts[i]' done fi + ((${#known_hosts[@]})) || return 1 - __ltrim_colon_completions "$prefix$cur" - -} # _known_hosts_real() -complete -F _known_hosts traceroute traceroute6 \ - fping fping6 telnet rsh rlogin ftp dig mtr ssh-installkeys showmount - -# This meta-cd function observes the CDPATH variable, so that cd additionally -# completes on directories under those specified in CDPATH. + _comp_compgen -v known_hosts -c "$prefix$cur" ltrim_colon "${known_hosts[@]}" +} +complete -F _comp_complete_known_hosts traceroute traceroute6 \ + fping fping6 telnet rsh rlogin ftp dig drill mtr ssh-installkeys showmount + +# Convert the word index in `words` to the index in `COMP_WORDS`. +# @param $1 Index in the array WORDS. +# @var[in,opt] words Words that contain reassmbled words. +# @var[in,opt] cword Current word index in WORDS. +# WORDS and CWORD, if any, are expected to be created by +# _comp__reassemble_words. # -_cd() +_comp__find_original_word() { - local cur prev words cword - _init_completion || return + REPLY=$1 - local IFS=$'\n' i j k + # If CWORD or WORDS are undefined, we return the first argument without any + # processing. + [[ -v cword && -v words ]] || return 0 - compopt -o filenames - - # Use standard dir completion if no CDPATH or parameter starts with /, - # ./ or ../ - if [[ -z ${CDPATH:-} || $cur == ?(.)?(.)/* ]]; then - _filedir -d - return - fi - - local -r mark_dirs=$(_rl_enabled mark-directories && echo y) - local -r mark_symdirs=$(_rl_enabled mark-symlinked-directories && echo y) - - # we have a CDPATH, so loop on its contents - for i in ${CDPATH//:/$'\n'}; do - # create an array of matched subdirs - k="${#COMPREPLY[@]}" - for j in $(compgen -d -- $i/$cur); do - if [[ ($mark_symdirs && -L $j || $mark_dirs && ! -L $j) && ! -d ${j#$i/} ]]; then - j+="/" - fi - COMPREPLY[k++]=${j#$i/} + local reassembled_offset=$1 i=0 j + for ((j = 0; j < reassembled_offset; j++)); do + local word=${words[j]} + while [[ $word && i -lt ${#COMP_WORDS[@]} && $word == *"${COMP_WORDS[i]}"* ]]; do + word=${word#*"${COMP_WORDS[i++]}"} done done - - _filedir -d - - if ((${#COMPREPLY[@]} == 1)); then - i=${COMPREPLY[0]} - if [[ $i == "$cur" && $i != "*/" ]]; then - COMPREPLY[0]="${i}/" - fi - fi - - return + REPLY=$i } -if shopt -q cdable_vars; then - complete -v -F _cd -o nospace cd pushd -else - complete -F _cd -o nospace cd pushd -fi - -# A _command_offset wrapper function for use when the offset is unknown. -# Only intended to be used as a completion function directly associated -# with a command, not to be invoked from within other completion functions. -# -_command() -{ - local offset i - - # find actual offset, as position of the first non-option - offset=1 - for ((i = 1; i <= COMP_CWORD; i++)); do - if [[ ${COMP_WORDS[i]} != -* ]]; then - offset=$i - break - fi - done - _command_offset $offset -} - # A meta-command completion function for commands like sudo(8), which need to # first complete on a command, then complete according to that command's own # completion definition. # -_command_offset() +# @since 2.12 +_comp_command_offset() { # rewrite current completion context before invoking # actual command completion + # obtain the word index in COMP_WORDS + local REPLY + _comp__find_original_word "$1" + local word_offset=$REPLY + + # make changes to COMP_* local. Note that bash-4.3..5.0 have a + # bug that `local -a arr=("${arr[@]}")` fails. We instead first + # assign the values of `COMP_WORDS` to another array `comp_words`. + local COMP_LINE=$COMP_LINE COMP_POINT=$COMP_POINT COMP_CWORD=$COMP_CWORD + local -a comp_words=("${COMP_WORDS[@]}") + local -a COMP_WORDS=("${comp_words[@]}") + # find new first word position, then # rewrite COMP_LINE and adjust COMP_POINT - local word_offset=$1 i j + local i tail for ((i = 0; i < word_offset; i++)); do - for ((j = 0; j <= ${#COMP_LINE}; j++)); do - [[ $COMP_LINE == "${COMP_WORDS[i]}"* ]] && break - COMP_LINE=${COMP_LINE:1} - ((COMP_POINT--)) - done - COMP_LINE=${COMP_LINE#"${COMP_WORDS[i]}"} - ((COMP_POINT -= ${#COMP_WORDS[i]})) + tail=${COMP_LINE#*"${COMP_WORDS[i]}"} + ((COMP_POINT -= ${#COMP_LINE} - ${#tail})) + COMP_LINE=$tail done # shift COMP_WORDS elements and adjust COMP_CWORD - for ((i = 0; i <= COMP_CWORD - word_offset; i++)); do - COMP_WORDS[i]=${COMP_WORDS[i + word_offset]} - done - for ((i; i <= COMP_CWORD; i++)); do - unset 'COMP_WORDS[i]' - done + COMP_WORDS=("${COMP_WORDS[@]:word_offset}") ((COMP_CWORD -= word_offset)) COMPREPLY=() local cur - _get_comp_words_by_ref cur + _comp_get_words cur if ((COMP_CWORD == 0)); then - local IFS=$'\n' - compopt -o filenames - COMPREPLY=($(compgen -d -c -- "$cur")) + _comp_compgen_commands else - local cmd=${COMP_WORDS[0]} compcmd=${COMP_WORDS[0]} - local cspec=$(complete -p $cmd 2>/dev/null) + _comp_dequote "${COMP_WORDS[0]}" || REPLY=${COMP_WORDS[0]} + local cmd=$REPLY compcmd=$REPLY + local cspec=$(complete -p "$cmd" 2>/dev/null) # If we have no completion for $cmd yet, see if we have for basename if [[ ! $cspec && $cmd == */* ]]; then - cspec=$(complete -p ${cmd##*/} 2>/dev/null) + cspec=$(complete -p "${cmd##*/}" 2>/dev/null) [[ $cspec ]] && compcmd=${cmd##*/} fi # If still nothing, just load it for the basename if [[ ! $cspec ]]; then compcmd=${cmd##*/} - _completion_loader $compcmd - cspec=$(complete -p $compcmd 2>/dev/null) + _comp_load -D -- "$compcmd" + cspec=$(complete -p "$compcmd" 2>/dev/null) fi - if [[ -n $cspec ]]; then - if [[ ${cspec#* -F } != "$cspec" ]]; then - # complete -F <function> + local retry_count=0 + while true; do # loop for the retry request by status 124 + local args original_cur=${comp_args[1]-$cur} + if ((${#COMP_WORDS[@]} >= 2)); then + args=("$cmd" "$original_cur" "${COMP_WORDS[-2]}") + else + args=("$cmd" "$original_cur") + fi + + if [[ ! $cspec ]]; then + if ((${#COMPREPLY[@]} == 0)); then + # XXX will probably never happen as long as completion loader loads + # *something* for every command thrown at it ($cspec != empty) + _comp_complete_minimal "${args[@]}" + fi + elif [[ $cspec == *\ -[CF]\ * ]]; then + if [[ $cspec == *' -F '* ]]; then + # complete -F <function> + + # get function name + local func=${cspec#* -F } + func=${func%% *} + $func "${args[@]}" + + # restart completion (once) if function exited with 124 + if (($? == 124 && retry_count++ == 0)); then + # Note: When the completion function returns 124, the + # state of COMPREPLY is discarded. + COMPREPLY=() - # get function name - local func=${cspec#*-F } - func=${func%% *} + cspec=$(complete -p "$compcmd" 2>/dev/null) - if ((${#COMP_WORDS[@]} >= 2)); then - $func $cmd "${COMP_WORDS[-1]}" "${COMP_WORDS[-2]}" + # Note: When completion spec is removed after 124, we + # do not generate any completions including the default + # ones. This is the behavior of the original Bash + # progcomp. + [[ $cspec ]] || break + + continue + fi else - $func $cmd "${COMP_WORDS[-1]}" + # complete -C <command> + + # get command name + local completer=${cspec#* -C \'} + + # completer commands are always single-quoted + if ! _comp_dequote "'$completer"; then + _minimal "${args[@]}" + break + fi + completer=${REPLY[0]} + + local -a suggestions + + local IFS=$' \t\n' + local reset_monitor=$(shopt -po monitor) reset_lastpipe=$(shopt -p lastpipe) reset_noglob=$(shopt -po noglob) + set +o monitor + shopt -s lastpipe + set -o noglob + + COMP_KEY="$COMP_KEY" COMP_LINE="$COMP_LINE" \ + COMP_POINT="$COMP_POINT" COMP_TYPE="$COMP_TYPE" \ + $completer "${args[@]}" | mapfile -t suggestions + + $reset_monitor + $reset_lastpipe + $reset_noglob + _comp_unlocal IFS + + local suggestion + local i=0 + COMPREPLY=() + for suggestion in "${suggestions[@]}"; do + COMPREPLY[i]+=${COMPREPLY[i]+$'\n'}$suggestion + + if [[ $suggestion != *\\ ]]; then + ((i++)) + fi + done fi # restore initial compopts @@ -1949,53 +2867,93 @@ _command_offset() # FIXME: should we take "+o opt" into account? cspec=${cspec#*-o } opt=${cspec%% *} - compopt -o $opt - cspec=${cspec#$opt} + compopt -o "$opt" + cspec=${cspec#"$opt"} done else cspec=${cspec#complete} - cspec=${cspec%%$compcmd} - COMPREPLY=($(eval compgen "$cspec" -- '$cur')) + cspec=${cspec%%@("$compcmd"|"'${compcmd//\'/\'\\\'\'}'")} + eval "_comp_compgen -- $cspec" fi - elif ((${#COMPREPLY[@]} == 0)); then - # XXX will probably never happen as long as completion loader loads - # *something* for every command thrown at it ($cspec != empty) - _minimal - fi + break + done fi } -complete -F _command aoss command "do" else eval exec ltrace nice nohup padsp \ + +# A _comp_command_offset wrapper function for use when the offset is unknown. +# Only intended to be used as a completion function directly associated +# with a command, not to be invoked from within other completion functions. +# +# @since 2.12 +_comp_command() +{ + # We unset the shell variable `words` locally to tell + # `_comp_command_offset` that the index is intended to be that in + # `COMP_WORDS` instead of `words`. + local words + unset -v words + + local offset i + + # find actual offset, as position of the first non-option + offset=1 + for ((i = 1; i <= COMP_CWORD; i++)); do + if [[ ${COMP_WORDS[i]} != -* ]]; then + offset=$i + break + fi + done + _comp_command_offset $offset +} +complete -F _comp_command aoss command "do" else eval exec ltrace nice nohup padsp \ "then" time tsocks vsound xargs -_root_command() +# @since 2.12 +_comp_root_command() { local PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin - local root_command=$1 - _command + local _comp_root_command=$1 + _comp_command } -complete -F _root_command fakeroot gksu gksudo kdesudo really +complete -F _comp_root_command fakeroot gksu gksudo kdesudo really # Return true if the completion should be treated as running as root -_complete_as_root() +# +# @since 2.12 +_comp_as_root() { - [[ $EUID -eq 0 || ${root_command:-} ]] + [[ $EUID -eq 0 || ${_comp_root_command-} ]] } -_longopt() +# Complete on available commands, subject to `no_empty_cmd_completion`. +# @return True (0) if one or more completions are generated, or otherwise False +# (1). Note that it returns 1 even when the completion generation is canceled +# by `shopt -s no_empty_cmd_completion`. +# +# @since 2.12 +_comp_compgen_commands() { - local cur prev words cword split - _init_completion -s || return + [[ ! ${cur-} ]] && shopt -q no_empty_cmd_completion && return 1 + # -o filenames for e.g. spaces in paths to and in command names + _comp_compgen -- -c -o plusdirs && compopt -o filenames +} + +# @since 2.12 +_comp_complete_longopt() +{ + local cur prev words cword was_split comp_args + _comp_initialize -s -- "$@" || return case "${prev,,}" in --help | --usage | --version) return ;; --!(no-*)dir*) - _filedir -d + _comp_compgen -a filedir -d return ;; --!(no-*)@(file|path)*) - _filedir + _comp_compgen -a filedir return ;; --+([-a-z0-9_])) @@ -2003,60 +2961,65 @@ _longopt() "s|.*$prev\[\{0,1\}=[<[]\{0,1\}\([-A-Za-z0-9_]\{1,\}\).*|\1|p") case ${argtype,,} in *dir*) - _filedir -d + _comp_compgen -a filedir -d return ;; *file* | *path*) - _filedir + _comp_compgen -a filedir return ;; esac ;; esac - $split && return + [[ $was_split ]] && return if [[ $cur == -* ]]; then - COMPREPLY=($(compgen -W "$(LC_ALL=C $1 --help 2>&1 | + _comp_compgen_split -- "$(LC_ALL=C $1 --help 2>&1 | while read -r line; do [[ $line =~ --[A-Za-z0-9]+([-_][A-Za-z0-9]+)*=? ]] && - printf '%s\n' ${BASH_REMATCH[0]} - done)" -- "$cur")) + printf '%s\n' "${BASH_REMATCH[0]}" + done)" [[ ${COMPREPLY-} == *= ]] && compopt -o nospace elif [[ $1 == *@(rmdir|chroot) ]]; then - _filedir -d + _comp_compgen -a filedir -d else [[ $1 == *mkdir ]] && compopt -o nospace - _filedir + _comp_compgen -a filedir fi } # makeinfo and texi2dvi are defined elsewhere. -complete -F _longopt a2ps awk base64 bash bc bison cat chroot colordiff cp \ +complete -F _comp_complete_longopt \ + a2ps awk base64 bash bc bison cat chroot colordiff cp \ csplit cut date df diff dir du enscript env expand fmt fold gperf \ - grep grub head irb ld ldd less ln ls m4 md5sum mkdir mkfifo mknod \ + grep grub head irb ld ldd less ln ls m4 mkdir mkfifo mknod \ mv netstat nl nm objcopy objdump od paste pr ptx readelf rm rmdir \ - sed seq sha{,1,224,256,384,512}sum shar sort split strip sum tac tail tee \ + sed seq shar sort split strip sum tac tail tee \ texindex touch tr uname unexpand uniq units vdir wc who -declare -Ag _xspecs +# @since 2.12 +declare -Ag _comp_xspecs -_filedir_xspec() +# @since 2.12 +_comp_complete_filedir_xspec() { - local cur prev words cword - _init_completion || return + local cur prev words cword comp_args + _comp_initialize -- "$@" || return + _comp_compgen_filedir_xspec "$1" +} - _tilde "$cur" || return +# @since 2.12 +_comp_compgen_filedir_xspec() +{ + _comp_compgen_tilde && return - local IFS=$'\n' xspec=${_xspecs[${1##*/}]} tmp - local -a toks + local REPLY + _comp_quote_compgen "$cur" + local quoted=$REPLY - toks=($( - compgen -d -- "$(quote_readline "$cur")" | { - while read -r tmp; do - printf '%s\n' $tmp - done - } - )) + local xspec=${_comp_xspecs[${1##*/}]-${_xspecs[${1##*/}]-}} + local -a toks + _comp_compgen -v toks -c "$quoted" -- -d # Munge xspec to contain uppercase version too # https://lists.gnu.org/archive/html/bug-bash/2010-09/msg00036.html @@ -2069,198 +3032,318 @@ _filedir_xspec() fi xspec="$matchop($xspec|${xspec^^})" - toks+=($( - eval compgen -f -X "'!$xspec'" -- '$(quote_readline "$cur")' | { - while read -r tmp; do - [[ -n $tmp ]] && printf '%s\n' $tmp - done - } - )) + _comp_compgen -av toks -c "$quoted" -- -f -X "@(|!($xspec))" # Try without filter if it failed to produce anything and configured to - [[ -n ${COMP_FILEDIR_FALLBACK:-} && ${#toks[@]} -lt 1 ]] && { - local reset=$(shopt -po noglob) - set -o noglob - toks+=($(compgen -f -- "$(quote_readline "$cur")")) - IFS=' ' - $reset - IFS=$'\n' - } + [[ ${BASH_COMPLETION_FILEDIR_FALLBACK-} && ${#toks[@]} -lt 1 ]] && + _comp_compgen -av toks -c "$quoted" -- -f - if ((${#toks[@]} != 0)); then - compopt -o filenames - COMPREPLY=("${toks[@]}") - fi + ((${#toks[@]})) || return 1 + + compopt -o filenames + _comp_compgen -RU toks -- -W '"${toks[@]}"' } -_install_xspec() +_comp__init_install_xspec() { local xspec=$1 cmd shift for cmd in "$@"; do - _xspecs[$cmd]=$xspec + _comp_xspecs[$cmd]=$xspec done } # bzcmp, bzdiff, bz*grep, bzless, bzmore intentionally not here, see Debian: #455510 -_install_xspec '!*.?(t)bz?(2)' bunzip2 bzcat pbunzip2 pbzcat lbunzip2 lbzcat -_install_xspec '!*.@(zip|[aegjswx]ar|exe|pk3|wsz|zargo|xpi|s[tx][cdiw]|sx[gm]|o[dt][tspgfc]|od[bm]|oxt|epub|apk|aab|ipa|do[ct][xm]|p[op]t[mx]|xl[st][xm]|pyz|whl)' unzip zipinfo -_install_xspec '*.Z' compress znew +_comp__init_install_xspec '!*.?(t)bz?(2)' bunzip2 bzcat pbunzip2 pbzcat lbunzip2 lbzcat +_comp__init_install_xspec '!*.@(zip|[aegjkswx]ar|exe|pk3|wsz|zargo|xpi|s[tx][cdiw]|sx[gm]|o[dt][tspgfc]|od[bm]|oxt|?(o)xps|epub|cbz|apk|aab|ipa|do[ct][xm]|p[op]t[mx]|xl[st][xm]|pyz|whl|[Ff][Cc][Ss]td)' unzip zipinfo +_comp__init_install_xspec '*.Z' compress znew # zcmp, zdiff, z*grep, zless, zmore intentionally not here, see Debian: #455510 -_install_xspec '!*.@(Z|[gGd]z|t[ag]z)' gunzip zcat -_install_xspec '!*.@(Z|[gGdz]z|t[ag]z)' unpigz -_install_xspec '!*.Z' uncompress +_comp__init_install_xspec '!*.@(Z|[gGd]z|t[ag]z)' gunzip zcat +_comp__init_install_xspec '!*.@(Z|[gGdz]z|t[ag]z)' unpigz +_comp__init_install_xspec '!*.Z' uncompress # lzcmp, lzdiff intentionally not here, see Debian: #455510 -_install_xspec '!*.@(tlz|lzma)' lzcat lzegrep lzfgrep lzgrep lzless lzmore unlzma -_install_xspec '!*.@(?(t)xz|tlz|lzma)' unxz xzcat -_install_xspec '!*.lrz' lrunzip -_install_xspec '!*.@(gif|jp?(e)g|miff|tif?(f)|pn[gm]|p[bgp]m|bmp|xpm|ico|xwd|tga|pcx)' ee -_install_xspec '!*.@(gif|jp?(e)g|tif?(f)|png|p[bgp]m|bmp|x[bp]m|rle|rgb|pcx|fits|pm|svg)' qiv -_install_xspec '!*.@(gif|jp?(e)g?(2)|j2[ck]|jp[2f]|tif?(f)|png|p[bgp]m|bmp|x[bp]m|rle|rgb|pcx|fits|pm|?(e)ps)' xv -_install_xspec '!*.@(@(?(e)ps|?(E)PS|pdf|PDF)?(.gz|.GZ|.bz2|.BZ2|.Z))' gv ggv kghostview -_install_xspec '!*.@(dvi|DVI)?(.@(gz|Z|bz2))' xdvi kdvi -_install_xspec '!*.dvi' dvips dviselect dvitype dvipdf advi dvipdfm dvipdfmx -_install_xspec '!*.[pf]df' acroread gpdf xpdf -_install_xspec '!*.@(?(e)ps|pdf)' kpdf -_install_xspec '!*.@(okular|@(?(e|x)ps|?(E|X)PS|[pf]df|[PF]DF|dvi|DVI|cb[rz]|CB[RZ]|djv?(u)|DJV?(U)|dvi|DVI|gif|jp?(e)g|miff|tif?(f)|pn[gm]|p[bgp]m|bmp|xpm|ico|xwd|tga|pcx|GIF|JP?(E)G|MIFF|TIF?(F)|PN[GM]|P[BGP]M|BMP|XPM|ICO|XWD|TGA|PCX|epub|EPUB|odt|ODT|fb?(2)|FB?(2)|mobi|MOBI|g3|G3|chm|CHM)?(.?(gz|GZ|bz2|BZ2|xz|XZ)))' okular -_install_xspec '!*.pdf' epdfview pdfunite -_install_xspec '!*.@(cb[rz7t]|djv?(u)|?(e)ps|pdf)' zathura -_install_xspec '!*.@(?(e)ps|pdf)' ps2pdf ps2pdf12 ps2pdf13 ps2pdf14 ps2pdfwr -_install_xspec '!*.texi*' makeinfo texi2html -_install_xspec '!*.@(?(la)tex|texi|dtx|ins|ltx|dbj)' tex latex slitex jadetex pdfjadetex pdftex pdflatex texi2dvi xetex xelatex luatex lualatex -_install_xspec '!*.mp3' mpg123 mpg321 madplay -_install_xspec '!*@(.@(mp?(e)g|MP?(E)G|wm[av]|WM[AV]|avi|AVI|asf|vob|VOB|bin|dat|divx|DIVX|vcd|ps|pes|fli|flv|FLV|fxm|FXM|viv|rm|ram|yuv|mov|MOV|qt|QT|web[am]|WEB[AM]|mp[234]|MP[234]|m?(p)4[av]|M?(P)4[AV]|mkv|MKV|og[agmv]|OG[AGMV]|t[ps]|T[PS]|m2t?(s)|M2T?(S)|mts|MTS|wav|WAV|flac|FLAC|asx|ASX|mng|MNG|srt|m[eo]d|M[EO]D|s[3t]m|S[3T]M|it|IT|xm|XM)|+([0-9]).@(vdr|VDR))?(.@(crdownload|part))' xine aaxine fbxine -_install_xspec '!*@(.@(mp?(e)g|MP?(E)G|wm[av]|WM[AV]|avi|AVI|asf|vob|VOB|bin|dat|divx|DIVX|vcd|ps|pes|fli|flv|FLV|fxm|FXM|viv|rm|ram|yuv|mov|MOV|qt|QT|web[am]|WEB[AM]|mp[234]|MP[234]|m?(p)4[av]|M?(P)4[AV]|mkv|MKV|og[agmv]|OG[AGMV]|t[ps]|T[PS]|m2t?(s)|M2T?(S)|mts|MTS|wav|WAV|flac|FLAC|asx|ASX|mng|MNG|srt|m[eo]d|M[EO]D|s[3t]m|S[3T]M|it|IT|xm|XM|iso|ISO)|+([0-9]).@(vdr|VDR))?(.@(crdownload|part))' kaffeine dragon totem -_install_xspec '!*.@(avi|asf|wmv)' aviplay -_install_xspec '!*.@(rm?(j)|ra?(m)|smi?(l))' realplay -_install_xspec '!*.@(mpg|mpeg|avi|mov|qt)' xanim -_install_xspec '!*.@(og[ag]|m3u|flac|spx)' ogg123 -_install_xspec '!*.@(mp3|ogg|pls|m3u)' gqmpeg freeamp -_install_xspec '!*.fig' xfig -_install_xspec '!*.@(mid?(i)|cmf)' playmidi -_install_xspec '!*.@(mid?(i)|rmi|rcp|[gr]36|g18|mod|xm|it|x3m|s[3t]m|kar)' timidity -_install_xspec '!*.@(669|abc|am[fs]|d[bs]m|dmf|far|it|mdl|m[eo]d|mid?(i)|mt[2m]|oct|okt?(a)|p[st]m|s[3t]m|ult|umx|wav|xm)' modplugplay modplug123 -_install_xspec '*.@([ao]|so|so.!(conf|*/*)|[rs]pm|gif|jp?(e)g|mp3|mp?(e)g|avi|asf|ogg|class)' vi vim gvim rvim view rview rgvim rgview gview emacs xemacs sxemacs kate kwrite -_install_xspec '!*.@(zip|z|gz|tgz)' bzme +_comp__init_install_xspec '!*.@(tlz|lzma)' lzcat lzegrep lzfgrep lzgrep lzless lzmore unlzma +_comp__init_install_xspec '!*.@(?(t)xz|tlz|lzma)' unxz xzcat +_comp__init_install_xspec '!*.lrz' lrunzip +_comp__init_install_xspec '!*.@(gif|jp?(e)g|miff|tif?(f)|pn[gm]|p[bgp]m|bmp|xpm|ico|xwd|tga|pcx)' ee +_comp__init_install_xspec '!*.@(gif|jp?(e)g|tif?(f)|png|p[bgp]m|bmp|x[bp]m|rle|rgb|pcx|fits|pm|svg)' qiv +_comp__init_install_xspec '!*.@(gif|jp?(e)g?(2)|j2[ck]|jp[2f]|tif?(f)|png|p[bgp]m|bmp|x[bp]m|rle|rgb|pcx|fits|pm|?(e)ps)' xv +_comp__init_install_xspec '!*.@(@(?(e)ps|?(E)PS|pdf|PDF)?(.gz|.GZ|.bz2|.BZ2|.Z))' gv ggv kghostview +_comp__init_install_xspec '!*.@(dvi|DVI)?(.@(gz|Z|bz2))' xdvi kdvi +_comp__init_install_xspec '!*.dvi' dvips dviselect dvitype dvipdf advi dvipdfm dvipdfmx +_comp__init_install_xspec '!*.[pf]df' acroread gpdf xpdf +_comp__init_install_xspec '!*.@(?(e)ps|pdf)' kpdf +_comp__init_install_xspec '!*.@(okular|@(?(e|x)ps|?(E|X)PS|[pf]df|[PF]DF|dvi|DVI|cb[rz]|CB[RZ]|djv?(u)|DJV?(U)|dvi|DVI|gif|jp?(e)g|miff|tif?(f)|pn[gm]|p[bgp]m|bmp|xpm|ico|xwd|tga|pcx|GIF|JP?(E)G|MIFF|TIF?(F)|PN[GM]|P[BGP]M|BMP|XPM|ICO|XWD|TGA|PCX|epub|EPUB|odt|ODT|fb?(2)|FB?(2)|mobi|MOBI|g3|G3|chm|CHM|md|markdown)?(.?(gz|GZ|bz2|BZ2|xz|XZ)))' okular +_comp__init_install_xspec '!*.pdf' epdfview pdfunite +_comp__init_install_xspec '!*.@(cb[rz7t]|djv?(u)|?(e)ps|pdf)' zathura +_comp__init_install_xspec '!*.@(?(e)ps|pdf)' ps2pdf ps2pdf12 ps2pdf13 ps2pdf14 ps2pdfwr +_comp__init_install_xspec '!*.texi*' makeinfo texi2html +_comp__init_install_xspec '!*.@(?(la)tex|texi|dtx|ins|ltx|dbj)' tex latex slitex jadetex pdfjadetex pdftex pdflatex texi2dvi xetex xelatex luatex lualatex +_comp__init_install_xspec '!*.mp3' mpg123 mpg321 madplay +_comp__init_install_xspec '!*@(.@(mp?(e)g|MP?(E)G|wm[av]|WM[AV]|avi|AVI|asf|vob|VOB|bin|dat|divx|DIVX|vcd|ps|pes|fli|flv|FLV|fxm|FXM|viv|rm|ram|yuv|mov|MOV|qt|QT|web[am]|WEB[AM]|mp[234]|MP[234]|m?(p)4[av]|M?(P)4[AV]|mkv|MKV|og[agmv]|OG[AGMV]|t[ps]|T[PS]|m2t?(s)|M2T?(S)|mts|MTS|wav|WAV|flac|FLAC|asx|ASX|mng|MNG|srt|m[eo]d|M[EO]D|s[3t]m|S[3T]M|it|IT|xm|XM)|+([0-9]).@(vdr|VDR))?(.@(crdownload|part))' xine aaxine cacaxine fbxine +_comp__init_install_xspec '!*@(.@(mp?(e)g|MP?(E)G|wm[av]|WM[AV]|avi|AVI|asf|vob|VOB|bin|dat|divx|DIVX|vcd|ps|pes|fli|flv|FLV|fxm|FXM|viv|rm|ram|yuv|mov|MOV|qt|QT|web[am]|WEB[AM]|mp[234]|MP[234]|m?(p)4[av]|M?(P)4[AV]|mkv|MKV|og[agmv]|OG[AGMV]|opus|OPUS|t[ps]|T[PS]|m2t?(s)|M2T?(S)|mts|MTS|wav|WAV|flac|FLAC|asx|ASX|mng|MNG|srt|m[eo]d|M[EO]D|s[3t]m|S[3T]M|it|IT|xm|XM|iso|ISO)|+([0-9]).@(vdr|VDR))?(.@(crdownload|part))' kaffeine dragon totem +_comp__init_install_xspec '!*.@(avi|asf|wmv)' aviplay +_comp__init_install_xspec '!*.@(rm?(j)|ra?(m)|smi?(l))' realplay +_comp__init_install_xspec '!*.@(mpg|mpeg|avi|mov|qt)' xanim +_comp__init_install_xspec '!*.@(og[ag]|m3u|flac|spx)' ogg123 +_comp__init_install_xspec '!*.@(mp3|ogg|pls|m3u)' gqmpeg freeamp +_comp__init_install_xspec '!*.fig' xfig +_comp__init_install_xspec '!*.@(mid?(i)|cmf)' playmidi +_comp__init_install_xspec '!*.@(mid?(i)|rmi|rcp|[gr]36|g18|mod|xm|it|x3m|s[3t]m|kar)' timidity +_comp__init_install_xspec '!*.@(669|abc|am[fs]|d[bs]m|dmf|far|it|mdl|m[eo]d|mid?(i)|mt[2m]|oct|okt?(a)|p[st]m|s[3t]m|ult|umx|wav|xm)' modplugplay modplug123 +_comp__init_install_xspec '*.@([ao]|so|so.!(conf|*/*)|[rs]pm|gif|jp?(e)g|mp3|mp?(e)g|avi|asf|ogg|class)' vi vim gvim rvim view rview rgvim rgview gview emacs xemacs sxemacs kate kwrite +_comp__init_install_xspec '!*.@(zip|z|gz|tgz)' bzme # konqueror not here on purpose, it's more than a web/html browser -_install_xspec '!*.@(?([xX]|[sS])[hH][tT][mM]?([lL]))' netscape mozilla lynx galeon dillo elinks amaya epiphany -_install_xspec '!*.@(sxw|stw|sxg|sgl|doc?([mx])|dot?([mx])|rtf|txt|htm|html|?(f)odt|ott|odm|pdf)' oowriter lowriter -_install_xspec '!*.@(sxi|sti|pps?(x)|ppt?([mx])|pot?([mx])|?(f)odp|otp)' ooimpress loimpress -_install_xspec '!*.@(sxc|stc|xls?([bmx])|xlw|xlt?([mx])|[ct]sv|?(f)ods|ots)' oocalc localc -_install_xspec '!*.@(sxd|std|sda|sdd|?(f)odg|otg)' oodraw lodraw -_install_xspec '!*.@(sxm|smf|mml|odf)' oomath lomath -_install_xspec '!*.odb' oobase lobase -_install_xspec '!*.[rs]pm' rpm2cpio -_install_xspec '!*.aux' bibtex -_install_xspec '!*.po' poedit gtranslator kbabel lokalize -_install_xspec '!*.@([Pp][Rr][Gg]|[Cc][Ll][Pp])' harbour gharbour hbpp -_install_xspec '!*.[Hh][Rr][Bb]' hbrun -_install_xspec '!*.ly' lilypond ly2dvi -_install_xspec '!*.@(dif?(f)|?(d)patch)?(.@([gx]z|bz2|lzma))' cdiff -_install_xspec '!@(*.@(ks|jks|jceks|p12|pfx|bks|ubr|gkr|cer|crt|cert|p7b|pkipath|pem|p10|csr|crl)|cacerts)' portecle -_install_xspec '!*.@(mp[234c]|og[ag]|@(fl|a)ac|m4[abp]|spx|tta|w?(a)v|wma|aif?(f)|asf|ape)' kid3 kid3-qt -unset -f _install_xspec - -# Minimal completion to use as fallback in _completion_loader. -_minimal() -{ - local cur prev words cword split - _init_completion -s || return - $split && return - _filedir +_comp__init_install_xspec '!*.@(?([xX]|[sS])[hH][tT][mM]?([lL]))' netscape mozilla lynx galeon dillo elinks amaya epiphany +_comp__init_install_xspec '!*.@(sxw|stw|sxg|sgl|doc?([mx])|dot?([mx])|rtf|txt|htm|html|?(f)odt|ott|odm|pdf)' oowriter lowriter +_comp__init_install_xspec '!*.@(sxi|sti|pps?(x)|ppt?([mx])|pot?([mx])|?(f)odp|otp)' ooimpress loimpress +_comp__init_install_xspec '!*.@(sxc|stc|xls?([bmx])|xlw|xlt?([mx])|[ct]sv|?(f)ods|ots)' oocalc localc +_comp__init_install_xspec '!*.@(sxd|std|sda|sdd|?(f)odg|otg)' oodraw lodraw +_comp__init_install_xspec '!*.@(sxm|smf|mml|odf)' oomath lomath +_comp__init_install_xspec '!*.odb' oobase lobase +_comp__init_install_xspec '!*.[rs]pm' rpm2cpio +_comp__init_install_xspec '!*.aux' bibtex +_comp__init_install_xspec '!*.po' poedit gtranslator kbabel lokalize +_comp__init_install_xspec '!*.@([Pp][Rr][Gg]|[Cc][Ll][Pp])' harbour gharbour hbpp +_comp__init_install_xspec '!*.[Hh][Rr][Bb]' hbrun +_comp__init_install_xspec '!*.ly' lilypond ly2dvi +_comp__init_install_xspec '!*.@(dif?(f)|?(d)patch)?(.@([gx]z|bz2|lzma))' cdiff +_comp__init_install_xspec '!@(*.@(ks|jks|jceks|p12|pfx|bks|ubr|gkr|cer|crt|cert|p7b|pkipath|pem|p10|csr|crl)|cacerts)' portecle +_comp__init_install_xspec '!*.@(mp[234c]|og[ag]|@(fl|a)ac|m4[abp]|spx|tta|w?(a)v|wma|aif?(f)|asf|ape)' kid3 kid3-qt +unset -f _comp__init_install_xspec + +# Minimal completion to use as fallback in _comp_complete_load. +# TODO:API: rename per conventions +_comp_complete_minimal() +{ + local cur prev words cword comp_args + _comp_initialize -- "$@" || return + compopt -o bashdefault -o default } # Complete the empty string to allow completion of '>', '>>', and '<' on < 4.3 # https://lists.gnu.org/archive/html/bug-bash/2012-01/msg00045.html -complete -F _minimal '' +complete -F _comp_complete_minimal '' -__load_completion() +# @since 2.12 +_comp_load() { - local -a dirs=(${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions) - local ifs=$IFS IFS=: dir cmd="${1##*/}" compfile - [[ -n $cmd ]] || return 1 - for dir in ${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; do - dirs+=($dir/bash-completion/completions) + local flag_fallback_default="" IFS=$' \t\n' + local OPTIND=1 OPTARG="" OPTERR=0 opt + while getopts ':D' opt "$@"; do + case $opt in + D) flag_fallback_default=set ;; + *) + echo "bash_completion: $FUNCNAME: usage error" >&2 + return 2 + ;; + esac done - IFS=$ifs + shift "$((OPTIND - 1))" - if [[ $BASH_SOURCE == */* ]]; then - dirs+=("${BASH_SOURCE%/*}/completions") - else - dirs+=(./completions) - fi + local cmd=$1 cmdname=${1##*/} dir compfile + local -a paths + [[ $cmdname ]] || return 1 local backslash= if [[ $cmd == \\* ]]; then - cmd="${cmd:1}" + cmd=${cmd:1} # If we already have a completion for the "real" command, use it $(complete -p "$cmd" 2>/dev/null || echo false) "\\$cmd" && return 0 backslash=\\ fi - for dir in "${dirs[@]}"; do - [[ -d $dir ]] || continue - for compfile in "$cmd" "$cmd.bash" "_$cmd"; do - compfile="$dir/$compfile" - # Avoid trying to source dirs; https://bugzilla.redhat.com/903540 - if [[ -f $compfile ]] && . "$compfile" &>/dev/null; then - [[ $backslash ]] && $(complete -p "$cmd") "\\$cmd" - return 0 + # Resolve absolute path to $cmd + local REPLY pathcmd origcmd=$cmd + if pathcmd=$(type -P "$cmd"); then + _comp_abspath "$pathcmd" + cmd=$REPLY + fi + + local -a dirs=() + + # Lookup order: + # 1) From BASH_COMPLETION_USER_DIR (e.g. ~/.local/share/bash-completion): + # User installed completions. + if [[ ${BASH_COMPLETION_USER_DIR-} ]]; then + _comp_split -F : paths "$BASH_COMPLETION_USER_DIR" && + dirs+=("${paths[@]/%//completions}") + else + dirs=("${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions") + fi + + # 2) From the location of bash_completion: Completions relative to the main + # script. This is primarily for run-in-place-from-git-clone setups, where + # we want to prefer in-tree completions over ones possibly coming with a + # system installed bash-completion. (Due to usual install layouts, this + # often hits the correct completions in system installations, too.) + if [[ $BASH_SOURCE == */* ]]; then + dirs+=("${BASH_SOURCE%/*}/completions") + else + dirs+=(./completions) + fi + + # 3) From bin directories extracted from the specified path to the command, + # the real path to the command, and $PATH + paths=() + [[ $cmd == /* ]] && paths+=("${cmd%/*}") + _comp_realcommand "$cmd" && paths+=("${REPLY%/*}") + _comp_split -aF : paths "$PATH" + for dir in "${paths[@]%/}"; do + [[ $dir == ?*/@(bin|sbin) ]] && + dirs+=("${dir%/*}/share/bash-completion/completions") + done + + # 4) From XDG_DATA_DIRS or system dirs (e.g. /usr/share, /usr/local/share): + # Completions in the system data dirs. + _comp_split -F : paths "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" && + dirs+=("${paths[@]/%//bash-completion/completions}") + + # Set up default $IFS in case loaded completions depend on it, + # as well as for $compspec invocation below. + local IFS=$' \t\n' + + # Look up and source + shift + local i prefix compspec + for prefix in "" _; do # Regular from all dirs first, then fallbacks + for i in ${!dirs[*]}; do + dir=${dirs[i]} + if [[ ! -d $dir ]]; then + unset -v 'dirs[i]' + continue fi + for compfile in "$prefix$cmdname" "$prefix$cmdname.bash"; do + compfile="$dir/$compfile" + # Avoid trying to source dirs as long as we support bash < 4.3 + # to avoid an fd leak; https://bugzilla.redhat.com/903540 + if [[ -d $compfile ]]; then + # Do not warn with . or .. (especially the former is common) + [[ $compfile == */.?(.) ]] || + echo "bash_completion: $compfile: is a directory" >&2 + elif [[ -e $compfile ]] && . "$compfile" "$cmd" "$@"; then + # At least $cmd is expected to have a completion set when + # we return successfully; see if it already does + if compspec=$(complete -p "$cmd" 2>/dev/null); then + # $cmd is the case in which we do backslash processing + [[ $backslash ]] && eval "$compspec \"\$backslash\$cmd\"" + # If invoked without path, that one should be set, too + # ...but let's not overwrite an existing one, if any + [[ $origcmd != */* ]] && + ! complete -p "$origcmd" &>/dev/null && + eval "$compspec \"\$origcmd\"" + return 0 + fi + # If not, see if we got one for $cmdname + if [[ $cmdname != "$cmd" ]] && compspec=$(complete -p "$cmdname" 2>/dev/null); then + # Use that for $cmd too, if we have a full path to it + [[ $cmd == /* ]] && eval "$compspec \"\$cmd\"" + return 0 + fi + # Nothing expected was set, continue lookup + fi + done done done # Look up simple "xspec" completions - [[ -v _xspecs[$cmd] ]] && - complete -F _filedir_xspec "$cmd" "$backslash$cmd" && return 0 + [[ -v _comp_xspecs[$cmdname] || -v _xspecs[$cmdname] ]] && + complete -F _comp_complete_filedir_xspec "$cmdname" "$backslash$cmdname" && return 0 + + if [[ $flag_fallback_default ]]; then + complete -F _comp_complete_minimal -- "$origcmd" && return 0 + fi return 1 } # set up dynamic completion loading -_completion_loader() +# @since 2.12 +_comp_complete_load() { # $1=_EmptycmD_ already for empty cmds in bash 4.3, set to it for earlier - local cmd="${1:-_EmptycmD_}" + local cmd=${1:-_EmptycmD_} - __load_completion "$cmd" && return 124 - - # Need to define *something*, otherwise there will be no completion at all. - complete -F _minimal -- "$cmd" && return 124 + # Pass -D to define *something*, or otherwise there will be no completion + # at all. + _comp_load -D -- "$cmd" && return 124 } && - complete -D -F _completion_loader + complete -D -F _comp_complete_load # Function for loading and calling functions from dynamically loaded # completion files that may not have been sourced yet. # @param $1 completion file to load function from in case it is missing -# @param $2... function and its arguments -_xfunc() -{ - set -- "$@" - local srcfile=$1 - shift - declare -F $1 &>/dev/null || __load_completion "$srcfile" - "$@" +# @param $2 the xfunc name. When it does not start with `_', +# `_comp_xfunc_${1//[^a-zA-Z0-9_]/_}_$2' is used for the actual name of the +# shell function. +# @param $3... if any, specifies the arguments that are passed to the xfunc. +# @since 2.12 +_comp_xfunc() +{ + local xfunc_name=$2 + [[ $xfunc_name == _* ]] || + xfunc_name=_comp_xfunc_${1//[^a-zA-Z0-9_]/_}_$xfunc_name + declare -F "$xfunc_name" &>/dev/null || _comp_load "$1" + "$xfunc_name" "${@:3}" } +# Call a POSIX-compatible awk. Solaris awk is not POSIX-compliant, but Solaris +# provides a POSIX-compatible version through /usr/xpg4/bin/awk. We switch the +# implementation to /usr/xpg4/bin/awk in Solaris if any. +# @since 2.12 +if [[ $OSTYPE == *solaris* && -x /usr/xpg4/bin/awk ]]; then + _comp_awk() + { + /usr/xpg4/bin/awk "$@" + } +else + _comp_awk() + { + command awk "$@" + } +fi + # source compat completion directory definitions -compat_dir=${BASH_COMPLETION_COMPAT_DIR:-/etc/bash_completion.d} -if [[ -d $compat_dir && -r $compat_dir && -x $compat_dir ]]; then - for i in "$compat_dir"/*; do - [[ ${i##*/} != @($_backup_glob|Makefile*|$_blacklist_glob) && -f \ - $i && -r $i ]] && . "$i" - done +_comp__init_compat_dirs=() +if [[ ${BASH_COMPLETION_COMPAT_DIR-} ]]; then + _comp__init_compat_dirs+=("$BASH_COMPLETION_COMPAT_DIR") +else + _comp__init_compat_dirs+=(/etc/bash_completion.d) + # Similarly as for the "completions" dir, look up from relative to + # bash_completion, primarily for installed-with-prefix and + # run-in-place-from-git-clone setups. Notably we do it after the system + # location here, in order to prefer in-tree variables and functions. + if [[ ${BASH_SOURCE%/*} == */share/bash-completion ]]; then + _comp__init_compat_dir=${BASH_SOURCE%/share/bash-completion/*}/etc/bash_completion.d + elif [[ $BASH_SOURCE == */* ]]; then + _comp__init_compat_dir="${BASH_SOURCE%/*}/bash_completion.d" + else + _comp__init_compat_dir=./bash_completion.d + fi + [[ ${_comp__init_compat_dirs[0]} == "$_comp__init_compat_dir" ]] || + _comp__init_compat_dirs+=("$_comp__init_compat_dir") fi -unset compat_dir i _blacklist_glob +for _comp__init_compat_dir in "${_comp__init_compat_dirs[@]}"; do + [[ -d $_comp__init_compat_dir && -r $_comp__init_compat_dir && -x $_comp__init_compat_dir ]] || continue + for _comp__init_file in "$_comp__init_compat_dir"/*; do + [[ ${_comp__init_file##*/} != @($_comp_backup_glob|Makefile*|${BASH_COMPLETION_COMPAT_IGNORE-}) && + -f $_comp__init_file && -r $_comp__init_file ]] && . "$_comp__init_file" + done +done +unset -v _comp__init_compat_dirs _comp__init_compat_dir _comp__init_file # source user completion file -user_completion=${BASH_COMPLETION_USER_FILE:-~/.bash_completion} -[[ ${BASH_SOURCE[0]} != "$user_completion" && -r $user_completion && -f $user_completion ]] && - . $user_completion -unset user_completion +# +# Remark: We explicitly check that $user_completion is not '/dev/null' since +# /dev/null may be a regular file in broken systems and can contain arbitrary +# garbages of suppressed command outputs. +_comp__init_user_file=${BASH_COMPLETION_USER_FILE:-~/.bash_completion} +[[ $_comp__init_user_file != "${BASH_SOURCE[0]}" && $_comp__init_user_file != /dev/null && -r $_comp__init_user_file && -f $_comp__init_user_file ]] && + . "$_comp__init_user_file" +unset -v _comp__init_user_file unset -f have -unset have +unset -v have -set $BASH_COMPLETION_ORIGINAL_V_VALUE -unset BASH_COMPLETION_ORIGINAL_V_VALUE +set $_comp__init_original_set_v +unset -v _comp__init_original_set_v # ex: filetype=sh |