Merged __get_cword3 & __get_cword4 to _get_cword
Actually enhanced __get_cword3 to _get_cword, and removed __get_cword4. __get_cword4 could handle chars to exclude from COMP_WORDBREAKS, but failed with partial quoted arguments (e.g. "a 'b c|", | = cursor position). This was no problem till bash-4.0.35, because bash < 4.0.35 also returned partial quoted arguments incorrectly. See also: http://www.mail-archive.com/bug-bash@gnu.org/msg06094.html Now that bash-4.0.35 returns quoted arguments ok, __get_cword3 is enhanced to also handle chars to exclude from COMP_WORDBREAKS. Because __get_cword3 also handles partial quoted arguments correctly, this makes __get_cword3 suitable for bash-4 as well.
This commit is contained in:
parent
8a70568066
commit
08c5878483
236
bash_completion
236
bash_completion
@ -208,178 +208,129 @@ dequote()
|
||||
eval echo "$1" 2> /dev/null
|
||||
}
|
||||
|
||||
|
||||
# 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 ref
|
||||
# On bash-3, `COMP_WORDBREAKS' is empty which is ok; no additional
|
||||
# word breaking is done on bash-3.
|
||||
local wordbreaks="$COMP_WORDBREAKS"
|
||||
# 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
|
||||
|
||||
# Are characters excluded which were former included?
|
||||
if [[ $exclude ]]; then
|
||||
# Yes, list of word completion separators has shrunk;
|
||||
# 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 of
|
||||
# length 1 and is word newly excluded from being word separator?
|
||||
while [[ $i -gt 0 && ${#COMP_WORDS[$i]} == 1 && ${COMP_WORDS[$i]//[^$exclude]} ]]; do
|
||||
[ $j -ge 2 ] && ((j--))
|
||||
# Append word separator to current word
|
||||
ref="$2[$j]"
|
||||
eval $2[$j]=\""${!ref}${COMP_WORDS[$i]}"\"
|
||||
# Indicate new cword
|
||||
[ $i = $COMP_CWORD ] && eval $3=$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]"
|
||||
eval $2[$j]=\""${!ref}${COMP_WORDS[$i]}"\"
|
||||
# Indicate new cword
|
||||
[ $i = $COMP_CWORD ] && eval $3=$j
|
||||
done
|
||||
else
|
||||
# No, list of word completions separators hasn't changed;
|
||||
eval $2=\( \"\${COMP_WORDS[@]}\" \)
|
||||
eval $3=$COMP_CWORD
|
||||
fi
|
||||
} # __reassemble_comp_words_by_ref()
|
||||
|
||||
|
||||
# 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 --------> ^
|
||||
# it will complete just "foo", not "foobar", which is what the user wants.)
|
||||
# @param $1 string (optional) 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.
|
||||
# NOTE: This parameter only applies to bash-4.
|
||||
|
||||
# @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. Bash-3 doesn't do word splitting, so this
|
||||
# ensures we get the same word on both bash-3 and bash-4.
|
||||
# @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_cword4 "=:" 1' returns the word left of
|
||||
# the current word, respecting the exclusions "=:".
|
||||
#
|
||||
_get_cword()
|
||||
{
|
||||
if [ ${BASH_VERSINFO[0]} -ge 4 ] ; then
|
||||
__get_cword4 "$@"
|
||||
else
|
||||
__get_cword3 "$2"
|
||||
fi
|
||||
} # _get_cword()
|
||||
local cword words
|
||||
__reassemble_comp_words_by_ref "$1" words cword
|
||||
|
||||
# Get word previous to the current word;
|
||||
# Accepts the same arguments as _get_cword()
|
||||
#
|
||||
# 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.
|
||||
_get_pword() { _get_cword "${@:-}" 1; }
|
||||
|
||||
# Get the word to complete on bash-3, where words are not broken by
|
||||
# COMP_WORDBREAKS characters and the COMP_CWORD variables look like this, for
|
||||
# example:
|
||||
#
|
||||
# $ a b:c<TAB>
|
||||
# COMP_CWORD: 1
|
||||
# COMP_CWORDS:
|
||||
# 0: a
|
||||
# 1: b:c
|
||||
#
|
||||
# See also:
|
||||
# _get_cword, main routine
|
||||
# __get_cword4, bash-4 variant
|
||||
#
|
||||
[ ${BASH_VERSINFO[0]} -lt 4 ] &&
|
||||
__get_cword3()
|
||||
{
|
||||
# return previous word offset by $1
|
||||
if [[ ${1//[^0-9]/} ]]; then
|
||||
printf "%s" "${COMP_WORDS[COMP_CWORD-$1]}"
|
||||
elif [[ "${#COMP_WORDS[COMP_CWORD]}" -eq 0 ]] || [[ "$COMP_POINT" == "${#COMP_LINE}" ]]; then
|
||||
printf "%s" "${COMP_WORDS[COMP_CWORD]}"
|
||||
# return previous word offset by $2
|
||||
if [[ ${2//[^0-9]/} ]]; then
|
||||
printf "%s" "${words[cword-$2]}"
|
||||
elif [[ "${#words[cword]}" -eq 0 ]] || [[ "$COMP_POINT" == "${#COMP_LINE}" ]]; then
|
||||
printf "%s" "${words[cword]}"
|
||||
else
|
||||
local i
|
||||
local cur="$COMP_LINE"
|
||||
local index="$COMP_POINT"
|
||||
for (( i = 0; i <= COMP_CWORD; ++i )); do
|
||||
for (( i = 0; i <= cword; ++i )); do
|
||||
while [[
|
||||
# Current COMP_WORD fits in $cur?
|
||||
"${#cur}" -ge ${#COMP_WORDS[i]} &&
|
||||
# $cur doesn't match COMP_WORD?
|
||||
"${cur:0:${#COMP_WORDS[i]}}" != "${COMP_WORDS[i]}"
|
||||
# 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
|
||||
index="$(( index - 1 ))"
|
||||
((index--))
|
||||
done
|
||||
|
||||
# Does found COMP_WORD matches COMP_CWORD?
|
||||
if [[ "$i" -lt "$COMP_CWORD" ]]; then
|
||||
# No, COMP_CWORD lies further;
|
||||
# Does found word matches cword?
|
||||
if [[ "$i" -lt "$cword" ]]; then
|
||||
# No, cword lies further;
|
||||
local old_size="${#cur}"
|
||||
cur="${cur#${COMP_WORDS[i]}}"
|
||||
cur="${cur#${words[i]}}"
|
||||
local new_size="${#cur}"
|
||||
index="$(( index - old_size + new_size ))"
|
||||
index=$(( index - old_size + new_size ))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${COMP_WORDS[COMP_CWORD]:0:${#cur}}" != "$cur" ]]; then
|
||||
if [[ "${words[cword]:0:${#cur}}" != "$cur" ]]; then
|
||||
# We messed up! At least return the whole word so things
|
||||
# keep working
|
||||
printf "%s" "${COMP_WORDS[COMP_CWORD]}"
|
||||
printf "%s" "${words[cword]}"
|
||||
else
|
||||
printf "%s" "${cur:0:$index}"
|
||||
fi
|
||||
fi
|
||||
} # __get_cword3()
|
||||
} # _get_cword()
|
||||
|
||||
|
||||
# Get the word to complete on bash-4, where words are splitted by
|
||||
# COMP_WORDBREAKS characters (default is " \t\n\"'><=;|&(:") and the COMP_CWORD
|
||||
# variables look like this, for example:
|
||||
# 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.
|
||||
# @see _get_cword()
|
||||
#
|
||||
# $ a b:c<TAB>
|
||||
# COMP_CWORD: 3
|
||||
# COMP_CWORDS:
|
||||
# 0: a
|
||||
# 1: b
|
||||
# 2: :
|
||||
# 3: c
|
||||
#
|
||||
# @param $1 string
|
||||
# $1 string (optional) 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.
|
||||
# @param $2 integer
|
||||
# $2 integer (optional) Return word according to $COMP_WORDBREAKS, negatively
|
||||
# offset by the value. For example, `__get_cword4 "=:" -1' returns the word
|
||||
# left of the current word, respecting the exclusions given at $1
|
||||
# See also:
|
||||
# _get_cword, main routine
|
||||
# __get_cword3, bash-3 variant
|
||||
#
|
||||
[ ${BASH_VERSINFO[0]} -ge 4 ] && {
|
||||
# return index of first occuring break character in $1; return 0 if none
|
||||
__break_index() {
|
||||
if [[ $1 == *[$WORDBREAKS]* ]]; then
|
||||
local w="${1%[$WORDBREAKS]*}"
|
||||
echo $((${#w}+1))
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
} # __break_index()
|
||||
|
||||
# return the index of the start of the last word in $@
|
||||
__word_start() {
|
||||
local buf="$@"
|
||||
local start="$(__break_index "$buf")"
|
||||
while [[ $start -ge 2 ]]; do
|
||||
# Get character before $start
|
||||
local char="${cur:$(( start - 2 )):1}"
|
||||
# If the WORDBREAK character isn't escaped, exit loop
|
||||
[[ $char != \\ ]] && break
|
||||
# The WORDBREAK character is escaped; recalculate $start
|
||||
buf="${COMP_LINE:0:$(( start - 2 ))}"
|
||||
start=$(__break_index "$buf")
|
||||
done
|
||||
echo $start
|
||||
} # __word_start()
|
||||
|
||||
__get_cword4()
|
||||
{
|
||||
local exclude="$1" n_idx="${2:-0}"
|
||||
local i
|
||||
local LC_CTYPE=C
|
||||
local WORDBREAKS="$COMP_WORDBREAKS"
|
||||
# Strip single quote (') and double quote (") from WORDBREAKS to
|
||||
# workaround a bug in bash-4.0, where quoted words are split
|
||||
# unintended, see:
|
||||
# http://www.mail-archive.com/bug-bash@gnu.org/msg06095.html
|
||||
# This fixes simple quoting (e.g. $ a "b<TAB> returns "b instead of b)
|
||||
# but still fails quoted spaces (e.g. $ a "b c<TAB> returns c instead
|
||||
# of "b c).
|
||||
WORDBREAKS="${WORDBREAKS//[\"\']/}"
|
||||
if [[ $exclude ]]; then
|
||||
for (( i=0; i<${#exclude}; ++i )); do
|
||||
local char="${exclude:$i:1}"
|
||||
WORDBREAKS="${WORDBREAKS//$char/}"
|
||||
done
|
||||
fi
|
||||
local cur="${COMP_LINE:0:$COMP_POINT}"
|
||||
local tmp="$cur"
|
||||
|
||||
# calculate current word, negatively offset by n_idx
|
||||
cur="${tmp:$(__word_start "$tmp")}"
|
||||
while [[ $n_idx -gt 0 ]]; do
|
||||
local tmp="${tmp%[$WORDBREAKS]$cur}" # truncate passed string
|
||||
local cur="${tmp:$(__word_start "$tmp")}" # then recalculate
|
||||
((--n_idx))
|
||||
done
|
||||
printf "%s" "$cur"
|
||||
} # __get_cword4()
|
||||
} # [ ${BASH_VERSINFO[0]} -ge 4 ]
|
||||
_get_pword() { _get_cword "${@:-}" 1; }
|
||||
|
||||
|
||||
# If the word-to-complete contains a colon (:), left-trim COMPREPLY items with
|
||||
@ -388,10 +339,17 @@ __get_cword4()
|
||||
# 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 word-to-complete contains a colon,
|
||||
# and bash-version < 4,
|
||||
|
@ -1,10 +1,19 @@
|
||||
# Bash library for bash-completion DejaGnu testsuite
|
||||
|
||||
|
||||
# @param $1 Char to add to $COMP_WORDBREAKS
|
||||
# @see remove_comp_wordbreak_char()
|
||||
add_comp_wordbreak_char() {
|
||||
if [ ${BASH_VERSINFO[0]} -ge 4 ]; then
|
||||
[[ "${COMP_WORDBREAKS//[^$1]}" ]] || COMP_WORDBREAKS=$COMP_WORDBREAKS$1
|
||||
fi
|
||||
} # add_comp_wordbreak_char()
|
||||
|
||||
|
||||
# Diff environment files to detect if environment is unmodified
|
||||
# @param $1 File 1
|
||||
# @param $2 File 2
|
||||
# @param $1 Additional sed script
|
||||
# @param $3 Additional sed script
|
||||
diff_env() {
|
||||
diff "$1" "$2" | sed -e "
|
||||
/^[0-9,]\{1,\}[acd]/d # Remove diff line indicators
|
||||
@ -19,9 +28,9 @@ diff_env() {
|
||||
# Unset variable after outputting.
|
||||
# @param $1 Name of array variable to process
|
||||
echo_array() {
|
||||
local IFS=$'\n'
|
||||
eval printf "%s" \"\${$1[*]}\" | sort
|
||||
}
|
||||
local name=$1[@]
|
||||
printf "%s\n" "${!name}" | sort
|
||||
} # echo_array()
|
||||
|
||||
|
||||
# Check if current bash version meets specified minimum
|
||||
@ -43,6 +52,16 @@ is_bash_version_minimal() {
|
||||
]]
|
||||
} # is_bash_version_minimal()
|
||||
|
||||
|
||||
# @param $1 Char to remove from $COMP_WORDBREAKS
|
||||
# @see add_comp_wordbreak_char()
|
||||
remove_comp_wordbreak_char() {
|
||||
if [ ${BASH_VERSINFO[0]} -ge 4 ]; then
|
||||
COMP_WORDBREAKS=${COMP_WORDBREAKS//$1}
|
||||
fi
|
||||
} # remove_comp_wordbreak_char()
|
||||
|
||||
|
||||
# Local variables:
|
||||
# mode: shell-script
|
||||
# sh-basic-offset: 4
|
||||
|
@ -79,47 +79,68 @@ assert_bash_list {"\"b\\"} $cmd $test
|
||||
sync_after_int
|
||||
|
||||
|
||||
# See http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=474094 for useful ideas
|
||||
# to make this test pass.
|
||||
set test {a 'b c| should return 'b c}; # | = cursor position
|
||||
if {[lindex $::BASH_VERSINFO 0] <= 3} {
|
||||
set cmd {COMP_WORDS=(a "'b c"); COMP_CWORD=1}
|
||||
} else {
|
||||
if {
|
||||
[lindex $::BASH_VERSINFO 0] == 4 &&
|
||||
[lindex $::BASH_VERSINFO 1] == 0 &&
|
||||
[lindex $::BASH_VERSINFO 2] < 35
|
||||
} {
|
||||
set cmd {COMP_WORDS=(a "'" b c); COMP_CWORD=3}
|
||||
} else {
|
||||
set cmd {COMP_WORDS=(a "'b c"); COMP_CWORD=1}
|
||||
}; # if
|
||||
append cmd {; COMP_LINE="a 'b c"; COMP_POINT=6; _get_cword}
|
||||
send "$cmd\r"
|
||||
expect -ex "$cmd\r\n"
|
||||
expect {
|
||||
-ex "'b c/@" { pass "$test" }
|
||||
-ex "c/@" { xfail "$test" }
|
||||
-ex "c/@" {
|
||||
if {
|
||||
[lindex $::BASH_VERSINFO 0] == 4 &&
|
||||
[lindex $::BASH_VERSINFO 1] == 0 &&
|
||||
[lindex $::BASH_VERSINFO 2] < 35
|
||||
} {xfail "$test"} {fail "$test"}
|
||||
}
|
||||
}; # expect
|
||||
|
||||
|
||||
sync_after_int
|
||||
|
||||
|
||||
# See http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=474094 for useful ideas
|
||||
# to make this test pass.
|
||||
set test {a "b c| should return "b c}; # | = cursor position
|
||||
set cmd {COMP_WORDS=(a "\"b c"); COMP_CWORD=1; COMP_LINE="a \"b c"; COMP_POINT=6; _get_cword};
|
||||
if {
|
||||
[lindex $::BASH_VERSINFO 0] == 4 &&
|
||||
[lindex $::BASH_VERSINFO 1] == 0 &&
|
||||
[lindex $::BASH_VERSINFO 2] < 35
|
||||
} {
|
||||
set cmd {COMP_WORDS=(a "\"" b c); COMP_CWORD=3}
|
||||
} else {
|
||||
set cmd {COMP_WORDS=(a "\"b c"); COMP_CWORD=1}
|
||||
}; # if
|
||||
append cmd {; COMP_LINE="a \"b c"; COMP_POINT=6; _get_cword};
|
||||
send "$cmd\r"
|
||||
expect -ex "$cmd\r\n"
|
||||
expect {
|
||||
-ex "\"b c/@" { pass "$test" }
|
||||
-ex "c/@" { xfail "$test" }
|
||||
-ex "c/@" {
|
||||
if {
|
||||
[lindex $::BASH_VERSINFO 0] == 4 &&
|
||||
[lindex $::BASH_VERSINFO 1] == 0 &&
|
||||
[lindex $::BASH_VERSINFO 2] < 35
|
||||
} {xfail "$test"} {fail "$test"}
|
||||
}
|
||||
}; # expect
|
||||
|
||||
|
||||
sync_after_int
|
||||
|
||||
|
||||
set test {a b:c| should return b:c (bash-3) or c (bash-4)}; # | = cursor position
|
||||
set test {a b:c| with WORDBREAKS += : should return b:c (bash-3) or c (bash-4)}; # | = cursor position
|
||||
if {[lindex $::BASH_VERSINFO 0] <= 3} {
|
||||
set cmd {COMP_WORDS=(a "b:c"); COMP_CWORD=1}
|
||||
set expected b:c
|
||||
} else {
|
||||
set cmd {COMP_WORDS=(a b : c); COMP_CWORD=3}
|
||||
set cmd {add_comp_wordbreak_char :; COMP_WORDS=(a b : c); COMP_CWORD=3}
|
||||
set expected c
|
||||
}; # if
|
||||
append cmd {; COMP_LINE='a b:c'; COMP_POINT=5; _get_cword}
|
||||
@ -142,6 +163,14 @@ assert_bash_list b:c $cmd $test
|
||||
sync_after_int
|
||||
|
||||
|
||||
set test {a :| with WORDBREAKS -= : should return :}; # | = cursor position
|
||||
set cmd {COMP_WORDS=(a :); COMP_CWORD=1; COMP_LINE='a :'; COMP_POINT=3; _get_cword :}
|
||||
assert_bash_list : $cmd $test
|
||||
|
||||
|
||||
sync_after_int
|
||||
|
||||
|
||||
# This test makes sure `_get_cword' doesn't use `echo' to return it's value,
|
||||
# because -n might be interpreted by `echo' and thus will not be returned.
|
||||
set test "a -n| should return -n"; # | = cursor position
|
||||
|
Loading…
x
Reference in New Issue
Block a user