Skip to content

Instantly share code, notes, and snippets.

@trolzen
Forked from stecman/_README.md
Last active October 30, 2021 00:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trolzen/68b1c5037414a71f5decbf557bc5168d to your computer and use it in GitHub Desktop.
Save trolzen/68b1c5037414a71f5decbf557bc5168d to your computer and use it in GitHub Desktop.
bash-completion extract for dealing with colons (fixed)

Extract from bash-completion for working with stecman/symfony-console-completion

This is an example extract of the minimum amount of the bash-completion project required to use these two functions:

  • __ltrim_colon_completions
  • _get_comp_words_by_ref

The functions here are copied verbatim from the bash_completion source. I recommend not using this extract unless there are no other alternatives. Instead, prefer installing the bash-completion package from your package manager, or install bash-completion from source.

If you really are going to use this, download it and source it in your shell profile:

curl https://gist.githubusercontent.com/trolzen/68b1c5037414a71f5decbf557bc5168d/raw/bash_completion_fill.sh -O
source bash_completion_fill.sh

You can also just source the main BASH script from the bash-completion package, if you're more comfortable with doing that. I've tested this briefly and it works with symfony-console-completion, but no guarantees it won't break, sicne there are compiled components to bash-completion and I haven't looked at how they're hooked into the BASH side of it:

curl http://anonscm.debian.org/cgit/bash-completion/bash-completion.git/plain/bash_completion
source bash_completion

See issue #36 in stecman/symfony-console-completion for more context.

#!/bin/bash
# Get the word to complete and optional previous words.
# 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 --------> ^
# Also one is able to cross over possible wordbreak characters.
# Usage: _get_comp_words_by_ref [OPTIONS] [VARNAMES]
# Available VARNAMES:
# cur Return cur via $cur
# prev Return prev via $prev
# words Return words via $words
# cword Return cword via $cword
#
# Available OPTIONS:
# -n EXCLUDE 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 -n option in this case.
# -c VARNAME Return cur via $VARNAME
# -p VARNAME Return prev via $VARNAME
# -w VARNAME Return words via $VARNAME
# -i VARNAME Return cword via $VARNAME
#
# Example usage:
#
# $ _get_comp_words_by_ref -n : cur prev
#
_get_comp_words_by_ref()
{
local exclude flag i OPTIND=1
local cur cword words=()
local upargs=() upvars=() vcur vcword vprev vwords
while getopts "c:i:n:p:w:" flag "$@"; do
case $flag in
c) vcur=$OPTARG ;;
i) vcword=$OPTARG ;;
n) exclude=$OPTARG ;;
p) vprev=$OPTARG ;;
w) vwords=$OPTARG ;;
esac
done
while [[ $# -ge $OPTIND ]]; do
case ${!OPTIND} in
cur) vcur=cur ;;
prev) vprev=prev ;;
cword) vcword=cword ;;
words) vwords=words ;;
*) echo "bash: $FUNCNAME(): \`${!OPTIND}': unknown argument" \
1>&2; return 1
esac
let "OPTIND += 1"
done
__get_cword_at_cursor_by_ref "$exclude" words cword cur
[[ $vcur ]] && { upvars+=("$vcur" ); upargs+=(-v $vcur "$cur" ); }
[[ $vcword ]] && { upvars+=("$vcword"); upargs+=(-v $vcword "$cword"); }
[[ $vprev && $cword -ge 1 ]] && { upvars+=("$vprev" ); upargs+=(-v $vprev
"${words[cword - 1]}"); }
[[ $vwords ]] && { upvars+=("$vwords"); upargs+=(-a${#words[@]} $vwords
"${words[@]}"); }
(( ${#upvars[@]} )) && local "${upvars[@]}" && _upvars "${upargs[@]}"
}
# Assign variables one scope above the caller
# Usage: local varname [varname ...] &&
# _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()
{
if ! (($#)); then
echo "bash_completion: $FUNCNAME: usage: $FUNCNAME" \
"[-v varname value] | [-aN varname [value ...]] ..." >&2
return 2
fi
while (($#)); do
case $1 in
-a*)
# Error checking
[[ ${1#-a} ]] || {
echo "bash_completion: $FUNCNAME:" \
"\`$1': missing number specifier" >&2
return 1
}
printf %d "${1#-a}" &>/dev/null || {
echo bash_completion: \
"$FUNCNAME: \`$1': invalid number specifier" >&2
return 1
}
# Assign array of -aN elements
[[ "$2" ]] && unset -v "$2" && eval $2=\(\"\$"{@:3:${1#-a}}"\"\) &&
shift $((${1#-a} + 2)) || {
echo bash_completion: \
"$FUNCNAME: \`$1${2+ }$2': missing argument(s)" \
>&2
return 1
}
;;
-v)
# Assign single value
[[ "$2" ]] && unset -v "$2" && eval $2=\"\$3\" &&
shift 3 || {
echo "bash_completion: $FUNCNAME: $1:" \
"missing argument(s)" >&2
return 1
}
;;
*)
echo "bash_completion: $FUNCNAME: $1: invalid option" >&2
return 1
;;
esac
done
}
# @param $1 exclude 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 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()
{
local cword words=()
__reassemble_comp_words_by_ref "$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)?
if [[ $index -gt 0 && ( $lead && ${lead//[[:space:]]} ) ]]; then
cur=$COMP_LINE
for (( i = 0; i <= cword; ++i )); do
while [[
# Current word fits in $cur?
${#cur} -ge ${#words[i]} &&
# $cur doesn't match cword?
"${cur:0:${#words[i]}}" != "${words[i]}"
]]; do
# Strip first character
cur="${cur:1}"
# Decrease cursor position, staying >= 0
[[ $index -gt 0 ]] && ((index--))
done
# Does found word match cword?
if [[ $i -lt $cword ]]; then
# No, cword lies further;
local old_size=${#cur}
cur="${cur#"${words[i]}"}"
local new_size=${#cur}
index=$(( index - old_size + new_size ))
fi
done
# Clear $cur if just space(s)
[[ $cur && ! ${cur//[[:space:]]} ]] && cur=
# Zero $index if negative
[[ $index -lt 0 ]] && index=0
fi
local "$2" "$3" "$4" && _upvars -a${#words[@]} $2 "${words[@]}" \
-v $3 "$cword" -v $4 "${cur:0:$index}"
}
# 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
# 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 here.
# @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()
{
local exclude i j line ref
# Exclude word separator characters?
if [[ $1 ]]; then
# Yes, exclude word separator characters;
# Exclude only those characters, which were really included
exclude="${1//[^$COMP_WORDBREAKS]}"
fi
# Default to cword unchanged
printf -v "$3" %s "$COMP_CWORD"
# Are characters excluded which were former included?
if [[ $exclude ]]; then
# Yes, list of word completion separators has shrunk;
line=$COMP_LINE
# Re-assemble words to complete
for (( i=0, j=0; i < ${#COMP_WORDS[@]}; i++, j++)); do
# Is current word not word 0 (the command itself) and is word not
# empty and is word made up of just word separator characters to
# be excluded and is current word not preceded by whitespace in
# original line?
while [[ $i -gt 0 && ${COMP_WORDS[$i]} == +([$exclude]) ]]; do
# Is word separator not preceded by whitespace in original line
# and are we not going to append to word 0 (the command
# itself), then append to current word.
[[ $line != [$' \t']* ]] && (( j >= 2 )) && ((j--))
# Append word separator to current or new word
ref="$2[$j]"
printf -v "$ref" %s "${!ref}${COMP_WORDS[i]}"
# Indicate new cword
[[ $i == $COMP_CWORD ]] && printf -v "$3" %s "$j"
# Remove optional whitespace + word separator from line copy
line=${line#*"${COMP_WORDS[$i]}"}
# Start new word if word separator in original line is
# followed by whitespace.
[[ $line == [$' \t']* ]] && ((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]"
printf -v "$ref" %s "${!ref}${COMP_WORDS[i]}"
# Remove optional whitespace + word from line copy
line=${line#*"${COMP_WORDS[i]}"}
# Indicate new cword
[[ $i == $COMP_CWORD ]] && printf -v "$3" %s "$j"
done
[[ $i == $COMP_CWORD ]] && printf -v "$3" %s "$j"
else
# No, list of word completions separators hasn't changed;
for i in ${!COMP_WORDS[@]}; do
printf -v "$2[i]" %s "${COMP_WORDS[i]}"
done
fi
} # __reassemble_comp_words_by_ref()
# 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:
#
# # Remove colon (:) from list of word completion separators
# COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
#
# See also: Bash FAQ - E13) Why does filename completion misbehave if a colon
# appears in the filename? - http://tiswww.case.edu/php/chet/bash/FAQ
# @param $1 current word to complete (cur)
# @modifies global array $COMPREPLY
#
__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)) -ge 0 ]]; do
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
done
fi
} # __ltrim_colon_completions()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment