From b529cee550fa20678ecd7c92ed575f3342db1d49 Mon Sep 17 00:00:00 2001 From: Freddy Vulto Date: Sun, 7 Feb 2010 15:18:58 +0100 Subject: Added _get_comp_words_by_ref() This solves the following problems: - now one function call suffices instead of two (_get_cword; _get_pword) if subsequent words need to be retrieved. Also more than two words can be retrieved at once, e.g.: _get_comp_words_by_ref cur prev prev2 prev3 Also this prevents passing of `wordbreakchars' to differ in calls to `_get_cword' and `_get_pword', e.g.: _get_comp_words_by_ref -n : cur prev - passing by reference, no subshell call necessary anymore - _get_pword now also takes into account the cursor position Added testsuite proc `assert_no_output()' Word of caution: The passing-arguments-by-ref system in bash doesn't work if the new variable is also declared local. For example: t() { local a # ... eval $1=b } a=c; t a; echo $a # Outputs "c", should be "b" # Variable "a" is 'forbidden' To make name collissions like this less likely to happen, but make the real function still use readable variables, I've wrapped the `*_by_ref' functions within an additional layer using variables prefixed with double underscores (__). For example: _t() { # Readable variables can still be used here local a # ... eval $1=b } t() { local __a _t __a eval $1=\$__a } a=c; t a; echo $a # Outputs "b" # Variable "__a" is 'forbidden' Now only more obfuscated variables (starting with double prefix (__)) are forbidden to use. --- bash_completion | 136 ++++++++++++++ test/lib/library.exp | 46 +++-- test/unit/_get_comp_words_by_ref.exp | 339 +++++++++++++++++++++++++++++++++++ 3 files changed, 510 insertions(+), 11 deletions(-) create mode 100644 test/unit/_get_comp_words_by_ref.exp diff --git a/bash_completion b/bash_completion index d59ef6f2..4dc09ea1 100644 --- a/bash_completion +++ b/bash_completion @@ -253,6 +253,142 @@ __reassemble_comp_words_by_ref() { } # __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 +# 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 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 ___get_cword_at_cursor_by_ref() +__get_cword_at_cursor_by_ref() { + # NOTE: The call to the main function ___get_cword_at_cursor_by_ref() is + # wrapped to make collisions with local variable names less likely. + local __words __cword __cur + ___get_cword_at_cursor_by_ref "$1" __words __cword __cur + + eval $2=\( \"\${__words[@]}\" \) + eval $3=\$__cword + eval $4=\$__cur +} + + +# @param $1 exclude +# @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 +# @note Do not call this function directly but call +# `__get_cword_at_cursor_by_ref()' instead to make variable name collisions +# less likely +# @see __get_cword_at_cursor_by_ref() +___get_cword_at_cursor_by_ref() { + local cword words + __reassemble_comp_words_by_ref "$1" words cword + + local i + local cur="$COMP_LINE" + local index="$COMP_POINT" + 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 + ((index--)) + done + + # Does found word matches 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 + + if [[ "${words[cword]:0:${#cur}}" != "$cur" ]]; then + # We messed up. At least return the whole word so things keep working + eval $4=\"\${words[cword]}\" + else + eval $4=\"\${cur:0:\$index}\" + fi + + eval $2=\( \"\${words[@]}\" \) + eval $3=\$cword +} + + +# 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] VAR1 [VAR2 [VAR3]] +# Example usage: +# +# $ _get_comp_words_by_ref -n : cur prev +# +# 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. Bash-3 doesn't do word splitting, +# so this ensures we get the same word on both bash-3 and bash-4. +# @see __get_comp_words_by_ref +_get_comp_words_by_ref() { + # NOTE: The call to the main function __get_comp_words_by_ref() is wrapped + # to make collisions with local variable name less likely. + local __words __cword __cur __var __vars + __get_comp_words_by_ref __words __cword __cur __vars "$@" + set -- "${__vars[@]}" + eval $1=\$__cur + shift + for __var; do + ((__cword--)) + [[ ${__words[__cword]} ]] && eval $__var=\${__words[__cword]} + done +} + + +# @param $1 words Name of variable to return words to +# @param $2 cword Name of variable to return cword to +# @param $3 cur Name of variable to return current word to complete to +# @param $4 varnames Name of variable to return array of variable names to +# @param $@ Arguments to _get_comp_words_by_ref() +# @note Do not call this function directly but call `_get_comp_words_by_ref()' +# instead to make variable name collisions less likely +# @see _get_comp_words_by_ref() +__get_comp_words_by_ref() +{ + local exclude flag i OPTIND=5 # Skip first four arguments + local cword words cur varnames=() + while getopts "n:" flag "$@"; do + case $flag in + n) exclude=$OPTARG ;; + esac + done + varnames=( ${!OPTIND} ) + let "OPTIND += 1" + while [[ $# -ge $OPTIND ]]; do + varnames+=( ${!OPTIND} ) + let "OPTIND += 1" + done + + __get_cword_at_cursor_by_ref "$exclude" words cword cur + + eval $1=\( \"\${words[@]}\" \) + eval $2=\$cword + eval $3=\$cur + eval $4=\( \"\${varnames[@]}\" \) +} + + # 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. diff --git a/test/lib/library.exp b/test/lib/library.exp index 387b40b7..a7a376e9 100644 --- a/test/lib/library.exp +++ b/test/lib/library.exp @@ -79,18 +79,22 @@ proc assert_bash_type {command} { # @result boolean True if successful, False if not proc assert_bash_list {expected cmd {test ""} {prompt /@} {size 20}} { if {$test == ""} {set test "$cmd should show expected output"} - send "$cmd\r" - expect -ex "$cmd\r\n" - - if {[match_items $expected $test $prompt $size]} { - expect { - -re $prompt { pass "$test" } - -re eof { unresolved "eof" } - }; # expect + if {[llength $expected] == 0} { + assert_no_output $cmd $test $prompt } else { - fail "$test" - }; # if -}; # assert_bash_list() + send "$cmd\r" + expect -ex "$cmd\r\n" + + if {[match_items $expected $test $prompt $size]} { + expect { + -re $prompt { pass "$test" } + -re eof { unresolved "eof" } + } + } else { + fail "$test" + } + } +} proc assert_bash_list_dir {expected cmd dir {test ""} {prompt /@} {size 20}} { @@ -451,6 +455,26 @@ proc assert_no_complete {{cmd} {test ""}} { }; # assert_no_complete() +# Check that no output is generated on a certain command. +# @param string $cmd The command to attempt to complete. +# @param string $test Optional parameter with test name. +# @param string $prompt (optional) Bash prompt. Default is "/@" +proc assert_no_output {{cmd} {test ""} {prompt /@}} { + if {[string length $test] == 0} { + set test "$cmd shouldn't generate output" + } + + send "$cmd\r" + expect -ex "$cmd" + + expect { + -re "^\r\n$prompt$" { pass "$test" } + default { fail "$test" } + timeout { fail "$test" } + } +} + + # Source/run file with additional tests if completion for the specified command # is installed in bash. # @param string $command Command to check completion availability for. diff --git a/test/unit/_get_comp_words_by_ref.exp b/test/unit/_get_comp_words_by_ref.exp new file mode 100644 index 00000000..67862b3a --- /dev/null +++ b/test/unit/_get_comp_words_by_ref.exp @@ -0,0 +1,339 @@ +proc setup {} { + assert_bash_exec {unset COMP_CWORD COMP_LINE COMP_POINT COMP_WORDS} + save_env +}; # setup() + + +proc teardown {} { + assert_bash_exec {unset COMP_CWORD COMP_LINE COMP_POINT COMP_WORDS cur prev prev2} + # Delete 'COMP_WORDBREAKS' occupying two lines + assert_env_unmodified { + /COMP_WORDBREAKS=/{N + d + } + } +}; # teardown() + + +setup + + +set test "_get_comp_words_by_ref should run without errors" +assert_bash_exec {_get_comp_words_by_ref cur > /dev/null} $test + + +sync_after_int + + +# See also ./lib/completions/alias.exp. Here `_get_cword' is actually tested +# by moving the cursor left into the current word. + + +set test "a b|"; # | = cursor position +set cmd {COMP_WORDS=(a b); COMP_CWORD=1; COMP_LINE='a b'; COMP_POINT=3; _get_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list {"b a"} $cmd $test + + +sync_after_int + + +set test "a |"; # | = cursor position +set cmd {COMP_WORDS=(a); COMP_CWORD=1; COMP_LINE='a '; COMP_POINT=2; _get_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list {" a"} $cmd $test + + +sync_after_int + + +set test "a b |"; # | = cursor position +set cmd {COMP_WORDS=(a b ''); COMP_CWORD=2; COMP_LINE='a b '; COMP_POINT=4; _get_comp_words_by_ref cur prev prev2; echo "$cur $prev $prev2"} +assert_bash_list {" b a"} $cmd $test + + +sync_after_int + + +set test "a b | with WORDBREAKS -= :"; # | = cursor position +set cmd {COMP_WORDS=(a b ''); COMP_CWORD=2; COMP_LINE='a b '; COMP_POINT=4; _get_comp_words_by_ref -n : cur; printf %s "$cur"} +assert_bash_list {} $cmd $test + + +sync_after_int + + +set test "a b|c"; # | = cursor position +set cmd {COMP_WORDS=(a bc); COMP_CWORD=1; COMP_LINE='a bc'; COMP_POINT=3; _get_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list {"b a"} $cmd $test + + +sync_after_int + + +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_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list {"b\\ c a"} $cmd $test + + +sync_after_int + + +set test {a b\| c should return b\ }; # | = cursor position +set cmd {COMP_WORDS=(a 'b\ c'); COMP_CWORD=1; COMP_LINE='a b\ c'; COMP_POINT=4; _get_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list {"b\\ a"} $cmd $test + + +sync_after_int + + +set test {a "b\|}; #"# | = cursor position +set cmd {COMP_WORDS=(a '"b\'); COMP_CWORD=1; COMP_LINE='a "b\'; COMP_POINT=5; _get_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list {"\"b\\ a"} $cmd $test + + +sync_after_int + + +set test {a 'b c|}; # | = cursor position +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_comp_words_by_ref cur prev; echo "$cur $prev"} +send "$cmd\r" +expect -ex "$cmd\r\n" +expect { + -ex "'b c a\r\n/@" { pass "$test" } + -ex "c b\r\n/@" { + 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|}; #"# | = cursor position +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} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur prev; echo "$cur $prev"}; +send "$cmd\r" +expect -ex "$cmd\r\n" +expect { + -ex "\"b c a\r\n/@" { pass "$test" } + -ex "c b\r\n/@" { + 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| with WORDBREAKS += :}; # | = cursor position +if {[lindex $::BASH_VERSINFO 0] <= 3} { + set cmd {COMP_WORDS=(a "b:c"); COMP_CWORD=1} + set expected {"b:c a"} +} else { + 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} +# NOTE: Split-send cmd to prevent backspaces (\008) in output +assert_bash_exec $cmd $test +set cmd {_get_comp_words_by_ref cur prev; echo "$cur $prev"} +assert_bash_list $expected $cmd $test + + +sync_after_int + + +set test {a b:c| with WORDBREAKS -= :}; # | = cursor position +if {[lindex $::BASH_VERSINFO 0] <= 3} { + set cmd {COMP_WORDS=(a "b:c"); COMP_CWORD=1} +} else { + set cmd {COMP_WORDS=(a b : c); COMP_CWORD=3} +}; # if +append cmd {; COMP_LINE='a b:c'; COMP_POINT=5} +assert_bash_exec $cmd $test +set cmd {_get_comp_words_by_ref -n : cur prev; echo "$cur $prev"} +assert_bash_list {"b:c a"} $cmd $test + + +sync_after_int + + +set test {a b c:| with WORDBREAKS -= :}; # | = cursor position +if {[lindex $::BASH_VERSINFO 0] <= 3} { + set cmd {COMP_WORDS=(a b c:); COMP_CWORD=2} +} else { + set cmd {COMP_WORDS=(a b c :); COMP_CWORD=3} +}; # if +append cmd {; COMP_LINE='a b c:'; COMP_POINT=6} +assert_bash_exec $cmd $test +set cmd {_get_comp_words_by_ref -n : cur prev; echo "$cur $prev $prev2"} +assert_bash_list {"c: b a"} $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} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref -n : cur prev; echo "$cur $prev"} +assert_bash_list {": a"} $cmd $test + + +sync_after_int + + +set test {a b::| with WORDBREAKS -= : should return b::}; # | = cursor position +if {[lindex $::BASH_VERSINFO 0] <= 3} { + set cmd {COMP_WORDS=(a "b::"); COMP_CWORD=1} +} else { + set cmd {COMP_WORDS=(a b ::); COMP_CWORD=2} +}; # if +append cmd {; COMP_LINE='a b::'; COMP_POINT=5} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref -n : cur prev; echo "$cur $prev"} +assert_bash_list {"b:: a"} $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 +set cmd {COMP_WORDS=(a -n); COMP_CWORD=1; COMP_LINE='a -n'; COMP_POINT=4} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur; printf %s $cur} +assert_bash_list -n $cmd $test + + +sync_after_int + + +set test {a b>c| should return c}; # | = cursor position +set cmd {COMP_WORDS=(a b \> c); COMP_CWORD=3; COMP_LINE='a b>c'; COMP_POINT=5} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur prev; echo "$cur"} +assert_bash_list c $cmd $test + + +sync_after_int + + +set test {a b=c| should return b=c (bash-3) or c (bash-4)}; # | = cursor position +if {[lindex $::BASH_VERSINFO] <= 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 expected c +}; # if +append cmd {; COMP_LINE='a b=c'; COMP_POINT=5} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur prev; echo "$cur"} +assert_bash_list $expected $cmd $test + + +sync_after_int + + +set test {a *| should return *}; # | = cursor position +set cmd {COMP_WORDS=(a \*); COMP_CWORD=1; COMP_LINE='a *'; COMP_POINT=4} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur; echo "$cur"} +assert_bash_list * $cmd $test + + +sync_after_int + + +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=7} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur; printf %s "$cur"} +send "$cmd\r" +expect -ex "$cmd\r\n" +expect { + -ex "\$(b c/@" { pass "$test" } + # Expected failure on bash-4 + -ex "c/@" { xfail "$test" } +}; # expect + + +sync_after_int + + +set test {a $(b c\ d| should return $(b c\ d}; # | = cursor position +set cmd {COMP_WORDS=(a '$(b c\ d'); COMP_CWORD=1; COMP_LINE='a $(b c\ d'; COMP_POINT=10} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur; printf %s "$cur"} +#assert_bash_list {{$(b\ c\\\ d}} $cmd $test +send "$cmd\r" +expect -ex "$cmd\r\n" +expect { + -ex "\$(b c\\ d/@" { pass "$test" } + # Expected failure on bash-4 + -ex "c\\ d/@" { xfail "$test" } +}; # expect + + +sync_after_int + + +set test {a 'b&c| should return 'b&c}; # | = cursor position +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=4} +} else { + set cmd {COMP_WORDS=(a "'b&c"); COMP_CWORD=1} +}; # if +append cmd {; COMP_LINE="a 'b&c"; COMP_POINT=6} +assert_bash_exec $cmd +set cmd {_get_comp_words_by_ref cur prev; printf %s "$cur"} +send "$cmd\r" +expect -ex "$cmd\r\n" +expect { + -ex "'b&c/@" { pass "$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 + + +teardown -- cgit v1.2.1