From d866854066fb3b6711cf6fc92ff648a0a12ee9d8 Mon Sep 17 00:00:00 2001 From: Freddy Vulto Date: Fri, 29 Jan 2010 23:23:30 +0100 Subject: Fix _usergroup, cpio and chown completions Improve test suite. Thanks to Leonard Crestez (Alioth: #311396, Debian: #511788). `assert_complete' is improved. It proved difficult to tell tcl to ignore backslash escapes, e.g. the `\b' is no BACKSPACE but a literal `b'. The added function `split_words_bash' should to the trick now. Added function `assert_no_complete' which can also be reached by calling `assert_complete' with an empty `expected' argument: assert_complete "" qwerty --- CHANGES | 1 + bash_completion | 60 +++++++++----- contrib/cpio | 6 +- test/completion/chown.exp | 3 + test/lib/completions/chown.exp | 70 ++++++++++++++++ test/lib/library.exp | 177 +++++++++++++++++++++++++++++------------ 6 files changed, 242 insertions(+), 75 deletions(-) create mode 100644 test/completion/chown.exp create mode 100644 test/lib/completions/chown.exp diff --git a/CHANGES b/CHANGES index dc626b1b..bb9f57d9 100644 --- a/CHANGES +++ b/CHANGES @@ -67,6 +67,7 @@ bash-completion (2.x) [ Leonard Crestez ] * Improve ssh -o suboption completion (Alioth: #312122). * Fix NFS mounts completion (Alioth: #312285). + * Fix completion of usernames (Alioth: #311396, Debian: #511788). [ Raphaƫl Droz ] * Add xsltproc completion (Alioth: #311843). diff --git a/bash_completion b/bash_completion index b01d4955..57284a45 100644 --- a/bash_completion +++ b/bash_completion @@ -771,20 +771,38 @@ _installed_modules() awk '{if (NR != 1) print $1}' )" -- "$1" ) ) } -# This function completes on user:group format +# This function completes on user or user:group format; as for chown and cpio. # +# The : must be added manually; it will only complete usernames initially. +# The legacy user.group format is not supported. +# +# It assumes compopt -o filenames; but doesn't touch it. _usergroup() { local IFS=$'\n' - cur=${cur//\\\\ / } - if [[ $cur = *@(\\:|.)* ]]; then - user=${cur%%*([^:.])} - COMPREPLY=( $(compgen -P ${user/\\\\} -g -- ${cur##*[.:]}) ) + if [[ $cur = *\\\\* || $cur = *:*:* ]]; then + # Give up early on if something seems horribly wrong. + return + elif [[ $cur = *\\:* ]]; then + # Completing group after 'user\:gr'. + # Reply with a list of groups prefixed with 'user:', readline will + # escape to the colon. + local prefix + prefix=${cur%%*([^:])} + prefix=${prefix//\\} + COMPREPLY=( $( compgen -P "$prefix" -g -- "${cur#*[:]}" ) ) elif [[ $cur = *:* ]]; then - COMPREPLY=( $( compgen -g -- ${cur##*[.:]} ) ) + # Completing group after 'user:gr'. + # Reply with a list of unprefixed groups since readline with split on : + # and only replace the 'gr' part + COMPREPLY=( $( compgen -g -- "${cur#*:}" ) ) else - type compopt &>/dev/null && compopt -o nospace - COMPREPLY=( $( compgen -S : -u -- "$cur" ) ) + # Completing a partial 'usernam'. + # + # Don't suffix with a : because readline will escape it and add a + # slash. It's better to complete into 'chown username ' than 'chown + # username\:'. + COMPREPLY=( $( compgen -u -- "$cur" ) ) fi } @@ -908,8 +926,10 @@ complete -F _service service _chown() { local cur prev split=false - cur=`_get_cword` - prev=${COMP_WORDS[COMP_CWORD-1]} + + # Get cur and prev words; but don't treat user:group as separate words. + cur=`_get_cword :` + prev=`_get_pword :` _split_longopt && split=true @@ -926,22 +946,22 @@ _chown() $split && return 0 - # options completion if [[ "$cur" == -* ]]; then + # Complete -options COMPREPLY=( $( compgen -W '-c -h -f -R -v --changes --dereference \ --no-dereference --from --silent --quiet --reference --recursive \ --verbose --help --version' -- "$cur" ) ) else - _count_args + local args - case $args in - 1) - _usergroup - ;; - *) - _filedir - ;; - esac + # The first argument is an usergroup; the rest are filedir. + _count_args : + + if [[ $args == 1 ]]; then + _usergroup + else + _filedir + fi fi } # _chown() complete -F _chown -o filenames chown diff --git a/contrib/cpio b/contrib/cpio index e8e4a5a5..dddfc190 100644 --- a/contrib/cpio +++ b/contrib/cpio @@ -11,8 +11,8 @@ _cpio() local cur prev split=false COMPREPLY=() - cur=`_get_cword` - prev=${COMP_WORDS[COMP_CWORD-1]} + cur=`_get_cword :` + prev=`_get_pword :` _split_longopt && split=true @@ -91,7 +91,7 @@ _cpio() esac fi } -complete -F _cpio cpio +complete -F _cpio -o filenames cpio } # Local variables: diff --git a/test/completion/chown.exp b/test/completion/chown.exp new file mode 100644 index 00000000..05ad2300 --- /dev/null +++ b/test/completion/chown.exp @@ -0,0 +1,3 @@ +if {[assert_bash_type chown]} { + source "lib/completions/chown.exp" +}; # if diff --git a/test/lib/completions/chown.exp b/test/lib/completions/chown.exp new file mode 100644 index 00000000..cc56149b --- /dev/null +++ b/test/lib/completions/chown.exp @@ -0,0 +1,70 @@ +proc setup {} { + save_env +}; # setup() + +proc teardown {} { + assert_env_unmodified +}; # teardown() + + +setup + + +assert_complete_any "chown " +sync_after_int + + +# All the tests use the root:root user and group. They're assumed to exist. +set fulluser "root" +set fullgroup "root" + +# Partial username is assumed to be unambiguous. +set partuser "roo" +set partgroup "roo" + +# Skip tests if root:root not available or if roo:roo matches multiple +# users/groups +if {[exec bash -c "compgen -A user $partuser" | wc -l] > 1 || + [exec bash -c "compgen -A user $fulluser" | wc -l] != 1 || + [exec bash -c "compgen -A group $partgroup" | wc -l] > 1 || + [exec bash -c "compgen -A group $fullgroup" | wc -l] != 1} { + untested "Not running complex chown tests." +} else { + assert_complete $fulluser "chown $partuser" + sync_after_int + + assert_complete $fulluser:$fullgroup "chown $fulluser:$partgroup" + sync_after_int + + # One slash should work correctly (doubled here for tcl). + assert_complete $fulluser\\:$fullgroup "chown $fulluser\\:$partgroup" + sync_after_int + + foreach prefix { + "funky\\ user:" "funky\\ user\\:" "funky.user:" "funky\\.user:" "fu\\ nky.user\\:" + "f\\ o\\ o\\.\\bar:" "foo\\_b\\ a\\.r\\ :" + } { + set test "Check preserve special chars in $prefix$partgroup" + #assert_complete_into "chown $prefix$partgroup" "chown $prefix$fullgroup " $test + assert_complete $prefix$fullgroup "chown $prefix$partgroup" $test + sync_after_int + } + + # Check that we give up in degenerate cases instead of spewing various junk. + + assert_no_complete "chown $fulluser\\\\:$partgroup" + sync_after_int + + assert_no_complete "chown $fulluser\\\\\\:$partgroup" + sync_after_int + + assert_no_complete "chown $fulluser\\\\\\\\:$partgroup" + sync_after_int + + # Colons in user/groupnames are not usually allowed. + assert_no_complete "chown foo:bar:$partgroup" + sync_after_int +} + + +teardown diff --git a/test/lib/library.exp b/test/lib/library.exp index ade7873d..6b56e492 100644 --- a/test/lib/library.exp +++ b/test/lib/library.exp @@ -104,7 +104,7 @@ proc assert_bash_list_dir {expected cmd dir {test ""} {prompt /@} {size 20}} { # Make sure the expected items are returned by TAB-completing the specified # command. -# @param list $expected +# @param list $expected Expected completions. # @param string $cmd Command given to generate items # @param string $test (optional) Test title. Default is "$cmd should show completions" # @param string $prompt (optional) Bash prompt. Default is "/@" @@ -113,66 +113,72 @@ proc assert_bash_list_dir {expected cmd dir {test ""} {prompt /@} {size 20}} { # argument-to-complete and to be replaced with the longest common prefix # of $expected. If empty string (default), `assert_complete' autodetects # if the last argument is an argument-to-complete by checking if $cmd -# doesn't end with whitespace. Specifying `cword' is only necessary if -# this autodetection fails, e.g. when the last whitespace is escaped or +# doesn't end with whitespace. Specifying `cword' should only be necessary +# if this autodetection fails, e.g. when the last whitespace is escaped or # quoted, e.g. "finger foo\ " or "finger 'foo " # @param list $filters (optional) List of filters to apply to this function to tweak # the expected completions and argument-to-complete. Possible values: # - "ltrim_colon_completions" # @result boolean True if successful, False if not proc assert_complete {expected cmd {test ""} {prompt /@} {size 20} {cword ""} {filters ""}} { - if {$test == ""} {set test "$cmd should show completions"} - send "$cmd\t" - if {[llength $expected] == 1} { - expect -ex "$cmd" - if {[lsearch -exact $filters "ltrim_colon_completions"] == -1} { - set cmds [split $cmd] - set cur ""; # Default to empty word to complete on - if {[llength $cmds] > 1} { - # Assume last word of `$cmd' is word to complete on. - set cur [lindex $cmds [expr [llength $cmds] - 1]] - }; # if - # Remove second word from beginning of single item $expected - if {[string first $cur $expected] == 0} { - set expected [string range $expected [string length $cur] end] - }; # if - }; # if + if {[llength $expected] == 0} { + assert_no_complete $cmd $test } else { - expect -ex "$cmd\r\n" - # Make sure expected items are unique - set expected [lsort -unique $expected] - }; # if - - if {[lsearch -exact $filters "ltrim_colon_completions"] != -1} { - # If partial contains colon (:), remove partial from begin of items - # See also: bash_completion.__ltrim_colon_completions() - _ltrim_colon_completions cword expected - }; # if - - if {[match_items $expected $test $prompt $size]} { + if {$test == ""} {set test "$cmd should show completions"} + send "$cmd\t" if {[llength $expected] == 1} { - pass "$test" + expect -ex "$cmd" + + if {[lsearch -exact $filters "ltrim_colon_completions"] == -1} { + set cur ""; # Default to empty word to complete on + set words [split_words_bash $cmd] + if {[llength $words] > 1} { + # Assume last word of `$cmd' is word to complete on. + set index [expr [llength $words] - 1] + set cur [lindex $words $index] + }; # if + # Remove second word from beginning of single item $expected + if {[string first $cur $expected] == 0} { + set expected [list [string range $expected [string length $cur] end]] + }; # if + }; # if } else { - # Remove optional (partial) last argument-to-complete from `cmd', - # E.g. "finger test@" becomes "finger" + expect -ex "$cmd\r\n" + # Make sure expected items are unique + set expected [lsort -unique $expected] + }; # if + + if {[lsearch -exact $filters "ltrim_colon_completions"] != -1} { + # If partial contains colon (:), remove partial from begin of items + # See also: bash_completion.__ltrim_colon_completions() + _ltrim_colon_completions cword expected + }; # if - if {[lsearch -exact $filters "ltrim_colon_completions"] != -1} { - set cmd2 $cmd + if {[match_items $expected $test $prompt $size]} { + if {[llength $expected] == 1} { + pass "$test" } else { - set cmd2 [_remove_cword_from_cmd $cmd $cword] + # Remove optional (partial) last argument-to-complete from `cmd', + # E.g. "finger test@" becomes "finger" + + if {[lsearch -exact $filters "ltrim_colon_completions"] != -1} { + set cmd2 $cmd + } else { + set cmd2 [_remove_cword_from_cmd $cmd $cword] + }; # if + + # Determine common prefix of completions + set common [::textutil::string::longestCommonPrefixList $expected] + #if {[string length $common] > 0} {set common " $common"} + expect { + -ex "$prompt$cmd2$common" { pass "$test" } + -re $prompt { unresolved "$test at prompt" } + -re eof { unresolved "eof" } + }; # expect }; # if - - # Determine common prefix of completions - set common [::textutil::string::longestCommonPrefixList $expected] - #if {[string length $common] > 0} {set common " $common"} - expect { - -ex "$prompt$cmd2$common" { pass "$test" } - -re $prompt { unresolved "$test at prompt" } - -re eof { unresolved "eof" } - }; # expect + } else { + fail "$test" }; # if - } else { - fail "$test" }; # if }; # assert_complete() @@ -213,13 +219,18 @@ proc _remove_cword_from_cmd {cmd {cword ""}} { }; # _remove_cword_from_cmd() +# Escape regexp special characters +proc _escape_regexp_chars {var} { + upvar $var str + regsub -all {([\^$+*?.|(){}[\]\\])} $str {\\\1} str +} + # Make sure any completions are returned proc assert_complete_any {cmd {test ""} {prompt /@}} { if {$test == ""} {set test "$cmd should show completions"} send "$cmd\t" expect -ex "$cmd" - # Escape special regexp characters - regsub -all {([\^$+*?.|(){}[\]\\])} $cmd {\\\1} cmd + _escape_regexp_chars cmd expect { -timeout 1 # Match completions, multiple words @@ -415,6 +426,29 @@ proc assert_exec {cmd {stdout ''} {test ''}} { }; # assert_exec() +# Check that no completion is attempted on a certain command. +# Params: +# @cmd The command to attempt to complete. +# @test Optional parameter with test name. +proc assert_no_complete {{cmd} {test ""}} { + if {[string length $test] == 0} { + set test "$cmd shouldn't complete" + }; # if + + send "$cmd\t" + expect -ex "$cmd" + + # We can't anchor on $, simulate typing a magical string instead. + set endguard "Magic End Guard" + send "$endguard" + expect { + -re "^$endguard$" { pass "$test" } + default { fail "$test" } + timeout { fail "$test" } + }; # expect +}; # assert_no_complete() + + # Sort list. # `exec sort' is used instead of `lsort' to achieve exactly the # same sort order as in bash. @@ -515,8 +549,7 @@ proc match_items {items test {prompt /@} {size 20}} { if {$i > $size} { set expected "\\s*" } else { set expected "" } for {set j 0} {$j < $size && $i + $j < [llength $items]} {incr j} { set item "[lindex $items [expr {$i + $j}]]" - # Escape special regexp characters - regsub -all {([\^$+*?.|(){}[\]\\])} $item {\\\1} item + _escape_regexp_chars item append expected $item if {[llength $items] > 1} {append expected {\s+}}; }; # for @@ -609,6 +642,46 @@ proc source_bash_completion {} { }; # source_bash_completion() +# Split line into words, disregarding backslash escapes (e.g. \b (backspace), +# \g (bell)), but taking backslashed spaces into account. +# Aimed for simulating bash word splitting. +# Example usage: +# +# % set a {f cd\ \be} +# % split_words $a +# f {cd\ \be} +# +# @param string Line to split +# @return list Words +proc split_words_bash {line} { + set words {} + set glue false + foreach part [split $line] { + set glue_next false + # Does `part' end with a backslash (\)? + if {[string last "\\" $part] == [string length $part] - [string length "\\"]} { + # Remove end backslash + set part [string range $part 0 [expr [string length $part] - [string length "\\"] - 1]] + # Indicate glue on next run + set glue_next true + }; # if + # Must `part' be appended to latest word (= glue)? + if {[llength $words] > 0 && [string is true $glue]} { + # Yes, join `part' to latest word; + set zz [lindex $words [expr [llength $words] - 1]] + # Separate glue with backslash-space (\ ); + lset words [expr [llength $words] - 1] "$zz\\ $part" + } else { + # No, don't append word to latest word; + # Append `part' as separate word + lappend words $part + }; # if + set glue $glue_next + }; # foreach + return $words +}; # split_words_bash() + + # Start bash running as test environment. proc start_bash {} { global TESTDIR TOOL_EXECUTABLE spawn_id -- cgit v1.2.1