diff options
27 files changed, 771 insertions, 85 deletions
@@ -68,6 +68,7 @@ bash-completion (2.x) * Improve ssh -o suboption completion (Alioth: #312122). * Fix NFS mounts completion (Alioth: #312285). * Fix completion of usernames (Alioth: #311396, Debian: #511788). + * Fix chown test crashing on systems with no root group (Alioth: #312306). * Fixed tests when BASH_COMPLETION or TESTDIR contain spaces. [ Raphaƫl Droz ] diff --git a/bash_completion b/bash_completion index 8467272a..904a8be2 100644 --- a/bash_completion +++ b/bash_completion @@ -90,7 +90,7 @@ complete -f -X '!*.@(@(?(e)ps|?(E)PS|pdf|PDF|dvi|DVI)?(.gz|.GZ|.bz2|.BZ2)|cb[rz] complete -f -X '!*.@(okular|@(?(e|x)ps|?(E|X)PS|pdf|PDF|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|FB|mobi|MOBI|g3|G3|chm|CHM|fdf|FDF)?(.?(gz|GZ|bz2|BZ2)))' okular complete -f -X '!*.@(?(e)ps|?(E)PS|pdf|PDF)' ps2pdf ps2pdf12 ps2pdf13 ps2pdf14 ps2pdfwr complete -f -X '!*.texi*' makeinfo texi2html -complete -f -X '!*.@(?(la)tex|?(LA)TEX|texi|TEXI|dtx|DTX|ins|INS)' tex latex slitex jadetex pdfjadetex pdftex pdflatex texi2dvi +complete -f -X '!*.@(?(la)tex|?(LA)TEX|texi|TEXI|dtx|DTX|ins|INS|ltx|LTX)' tex latex slitex jadetex pdfjadetex pdftex pdflatex texi2dvi complete -f -X '!*.@(mp3|MP3)' mpg123 mpg321 madplay complete -f -X '!*@(.@(mp?(e)g|MP?(E)G|wma|avi|AVI|asf|vob|VOB|bin|dat|divx|DIVX|vcd|ps|pes|fli|flv|FLV|viv|rm|ram|yuv|mov|MOV|qt|QT|wmv|mp[234]|MP[234]|m4[pv]|M4[PV]|mkv|MKV|og[gmv]|OG[GMV]|wav|WAV|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))' xine aaxine fbxine kaffeine dragon complete -f -X '!*.@(avi|asf|wmv)' aviplay @@ -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/doc/styleguide.txt b/doc/styleguide.txt index 39aedf82..4dbd5732 100644 --- a/doc/styleguide.txt +++ b/doc/styleguide.txt @@ -37,22 +37,33 @@ be more efficient in some cases and may reduce need for nesting conditions, and it's cleaner to write for example [[ ... && ... ]] than [ ... ] && [ ... ], and in general [[ ]] has more features. +Line wrapping +------------- + +Try to wrap lines at 79 characters. Never go past this limit, unless +you absolutely need to (example: a long sed regular expression, or the +like). This also holds true for the documentation and the testsuite. +Other files, like ChangeLog, or COPYING, are exempt from this rule. + +$(...) vs `...` +--------------- + +When you need to do some code substitution in your completion script, +you *MUST* use the $(...) construct, rather than the `...`. The former +is preferable because anyone, with any keyboard layout, is able to +type it. Backticks aren't always available, without doing strange +key combinations. + ///////////////////////////////////////// case/esac vs if --------------- -line wrapping -------------- - quoting ------- awk vs cut for simple cases --------------------------- -$(...) vs `...` ---------------- - variable and function naming ---------------------------- diff --git a/doc/testing.txt b/doc/testing.txt index f836c494..505d0848 100644 --- a/doc/testing.txt +++ b/doc/testing.txt @@ -13,6 +13,11 @@ written in http://expect.nist.gov[Expect], which in turn uses http://tcl.sourceforge.net[Tcl] -- Tool command language. +Coding Style Guide +------------------ + +The bash-completion test suite tries to adhere to this +http://wiki.tcl.tk/708[Tcl Style Guide]. Installing dependencies @@ -62,6 +67,50 @@ Each tool has a slightly different way of loading the test fixtures, see <<Test_context,Test context>> below. +Completion +~~~~~~~~~~ + +Completion tests are spread over two directories: `completion/\*.exp` calls +completions in `lib/completions/\*.exp`. This two-file system stems from +bash-completion-lib (http://code.google.com/p/bash-completion-lib/, containing +dynamic loading of completions) where tests are run twice per completion; once +before dynamic loading and a second time after to confirm that all dynamic +loading has gone well. + +For example: + +---- +set test "Completion via comp_load() should be installed" +set cmd "complete -p awk" +send "$cmd\r" +expect { + -re "^$cmd\r\ncomplete -o filenames -F comp_load awk\r\n/@$" { pass "$test" } + -re /@ { fail "$test at prompt" } +} + + +source "lib/completions/awk.exp" + + +set test "Completion via _longopt() should be installed" +set cmd "complete -p awk" +send "$cmd\r" +expect { + -re "^$cmd\r\ncomplete -o filenames -F _longopt awk\r\n/@$" { pass "$test" } + -re /@ { fail "$test at prompt" } +} + + +source "lib/completions/awk.exp" +---- + +Looking to the completion tests from a broader perspective, every test for a +command has two stages which are now reflected in the two files: + +. Tests concerning the command completions' environment (typically in +`test/completion/foo`) +. Tests invoking actual command completion (typically in +`test/lib/completions/foo`) Running the tests diff --git a/test/Makefile.am b/test/Makefile.am index e05abf5f..d955be8a 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -3,7 +3,6 @@ AM_RUNTESTFLAGS = --outdir log EXTRA_DIST = completion \ config \ - fixture1 \ fixtures \ lib \ unit diff --git a/test/completion/perldoc.exp b/test/completion/perldoc.exp index e06e594f..d0d49b1a 100644 --- a/test/completion/perldoc.exp +++ b/test/completion/perldoc.exp @@ -1,3 +1 @@ -if {[assert_bash_type perl]} { - source "lib/completions/perldoc.exp" -}; # if +assert_source_completions perldoc diff --git a/test/fixtures/lftp/.lftp/bookmarks b/test/fixtures/lftp/.lftp/bookmarks new file mode 100644 index 00000000..31ec9303 --- /dev/null +++ b/test/fixtures/lftp/.lftp/bookmarks @@ -0,0 +1 @@ +lftptest ftp://ftp.funet.fi/ diff --git a/test/fixture1/bar b/test/fixtures/shared/default/bar index e69de29b..e69de29b 100644 --- a/test/fixture1/bar +++ b/test/fixtures/shared/default/bar diff --git a/test/fixture1/bar bar.d/foo b/test/fixtures/shared/default/bar bar.d/foo index e69de29b..e69de29b 100644 --- a/test/fixture1/bar bar.d/foo +++ b/test/fixtures/shared/default/bar bar.d/foo diff --git a/test/fixture1/foo b/test/fixtures/shared/default/foo index 257cc564..257cc564 100644 --- a/test/fixture1/foo +++ b/test/fixtures/shared/default/foo diff --git a/test/fixture1/foo.d/foo b/test/fixtures/shared/default/foo.d/foo index e69de29b..e69de29b 100644 --- a/test/fixture1/foo.d/foo +++ b/test/fixtures/shared/default/foo.d/foo diff --git a/test/generate b/test/generate index 5c184d56..aa282a2c 100755 --- a/test/generate +++ b/test/generate @@ -14,9 +14,7 @@ generate_test_completion() { #if [ ! -f "$path" ]; then # No, file doesn't exist; generate file cat <<EXPECT > "$path" -if {[assert_bash_type $1]} { - source "lib/completions/$1.exp" -}; # if +assert_source_completions $1 EXPECT #fi } # generate_test_completion() diff --git a/test/lib/completions/acroread.exp b/test/lib/completions/acroread.exp index d21d0f55..4f11f905 100644 --- a/test/lib/completions/acroread.exp +++ b/test/lib/completions/acroread.exp @@ -1,11 +1,11 @@ proc setup {} { save_env - assert_bash_exec "touch fixture1/t.pdf"; # Create temporary files + assert_bash_exec "touch fixtures/shared/default/t.pdf"; # Create temporary files }; # setup() proc teardown {} { - assert_bash_exec "rm fixture1/t.pdf"; # Remove temporary files + assert_bash_exec "rm fixtures/shared/default/t.pdf"; # Remove temporary files assert_env_unmodified }; # teardown() @@ -13,7 +13,7 @@ proc teardown {} { setup -assert_complete {"bar bar.d/" foo.d/ t.pdf} "acroread fixture1/" +assert_complete {"bar bar.d/" foo.d/ t.pdf} "acroread fixtures/shared/default/" sync_after_int diff --git a/test/lib/completions/cancel.exp b/test/lib/completions/cancel.exp index ac41c282..764497c3 100644 --- a/test/lib/completions/cancel.exp +++ b/test/lib/completions/cancel.exp @@ -12,7 +12,7 @@ setup # Adding a print job is successful? -if {[assert_exec {lp -H hold fixture1/foo} job]} { +if {[assert_exec {lp -H hold fixtures/shared/default/foo} job]} { # Yes, adding a print-job is successful; # Retrieve job-id, so we can cancel the job after the test set job_id [lindex [split $job] 3] diff --git a/test/lib/completions/cd.exp b/test/lib/completions/cd.exp index d4c06d9d..58233575 100644 --- a/test/lib/completions/cd.exp +++ b/test/lib/completions/cd.exp @@ -12,7 +12,7 @@ setup set test "Tab should complete" -assert_complete {"bar bar.d/" foo.d/} "cd fixture1/" $test +assert_complete {"bar bar.d/" foo.d/} "cd fixtures/shared/default/" $test sync_after_int @@ -20,13 +20,13 @@ sync_after_int set test "Tab should complete cd at cursor position" # Try completion -set cmd "cd fixture1/foo" +set cmd "cd fixtures/shared/default/foo" append cmd \002\002\002; # \002 = ^B = Move cursor left in bash emacs mode #append cmd \033\0133D; # Escape-[-D = Cursor left send "$cmd\t" expect { - -re "cd fixture1/foo\b\b\b\r\n(\.svn/ +|)bar bar.d/ +foo.d/ *(\.svn/ *|)\r\n/@cd fixture1/foo\b\b\b$" { pass "$test" } - -re "^cd fixture1/foo\b\b\bfoo.d/foo\b\b\b$" { fail "$test: Wrong cursor position" } + -re "cd fixtures/shared/default/foo\b\b\b\r\n(\.svn/ +|)bar bar.d/ +foo.d/ *(\.svn/ *|)\r\n/@cd fixtures/shared/default/foo\b\b\b$" { pass "$test" } + -re "^cd fixtures/shared/default/foo\b\b\bfoo.d/foo\b\b\b$" { fail "$test: Wrong cursor position" } -re /@ { unresolved "$test at prompt" } default { unresolved "$test" } }; # expect @@ -38,7 +38,7 @@ sync_after_int set test "Tab should complete CDPATH" # Set CDPATH assert_bash_exec "CDPATH=\$PWD"; -assert_complete "fixture1/foo.d/" "cd fixture1/fo" $test +assert_complete "fixtures/shared/default/foo.d/" "cd fixtures/shared/default/fo" $test sync_after_int # Reset CDPATH assert_bash_exec "unset CDPATH" diff --git a/test/lib/completions/chown.exp b/test/lib/completions/chown.exp index 480f6743..953b2b02 100644 --- a/test/lib/completions/chown.exp +++ b/test/lib/completions/chown.exp @@ -10,30 +10,28 @@ proc teardown {} { setup -assert_complete_any "chown " +set users [exec bash -c "compgen -A user"] +assert_complete $users "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 -# -# compgen -A is used because it's a bash builtin and available everywhere. -# The || true part prevents exec from throwing an exception if nothing is -# found. -if {[exec bash -c "compgen -A user $partuser || true" | wc -l] > 1 || - [exec bash -c "compgen -A user $fulluser || true" | wc -l] != 1 || - [exec bash -c "compgen -A group $partgroup || true" | wc -l] > 1 || - [exec bash -c "compgen -A group $fullgroup || true" | wc -l] != 1} { - untested "Not running complex chown tests." -} else { +# Find user/group suitable for testing. +set failed_find_unique_completion 0 +foreach ug {user group} { + # compgen -A is used because it's a bash builtin and available everywhere. + # The || true part prevents exec from throwing an exception if nothing is + # found (very very unlikely). + set list [split [exec bash -c "compgen -A $ug || true"] "\n"] + if {![find_unique_completion_pair $list part$ug full$ug]} { + untested "Not running complex chown tests; no suitable test $ug found." + set failed_find_unique_completion 1 + } +} + +# These tests require an unique completion. +if {!$failed_find_unique_completion} { assert_complete $fulluser "chown $partuser" sync_after_int diff --git a/test/lib/completions/find.exp b/test/lib/completions/find.exp index 0fb5c62c..8009fddc 100644 --- a/test/lib/completions/find.exp +++ b/test/lib/completions/find.exp @@ -29,7 +29,7 @@ sync_after_int set test "-wholename should complete files/dirs" -set dir fixture1 +set dir fixtures/shared/default set files [split [exec bash -c "cd $dir && ls -p"] "\n"] assert_complete_dir $files "find -wholename " $dir diff --git a/test/lib/completions/lftp.exp b/test/lib/completions/lftp.exp index 4bdde376..a49a1935 100644 --- a/test/lib/completions/lftp.exp +++ b/test/lib/completions/lftp.exp @@ -1,4 +1,5 @@ proc setup {} { + assert_bash_exec {HOME=$TESTDIR/fixtures/lftp} save_env }; # setup() @@ -11,7 +12,10 @@ proc teardown {} { setup -assert_complete_any "lftp " +set expected [get_hosts] +# `lftptest' is defined in ./fixtures/lftp/.lftp/bookmarks +lappend expected lftptest +assert_complete $expected "lftp " sync_after_int diff --git a/test/lib/completions/perl.exp b/test/lib/completions/perl.exp index 4b463383..2199c994 100644 --- a/test/lib/completions/perl.exp +++ b/test/lib/completions/perl.exp @@ -18,7 +18,7 @@ sync_after_int set test "Second argument should file complete" -set cmd "perl foo fixture1/f" +set cmd "perl foo fixtures/shared/default/f" send "$cmd\t" expect { -re "^$cmd\r\nfoo +foo.d/ *\r\n/@${cmd}oo$" { pass "$test" } @@ -31,7 +31,7 @@ sync_after_int set test "-I without space should complete directories" -set cmd "perl -Ifixture1/" +set cmd "perl -Ifixtures/shared/default/" send "$cmd\t" expect { -re "^$cmd\r\nbar bar.d/ +foo.d/ *\r\n/@$cmd$" { pass "$test" } @@ -44,7 +44,7 @@ sync_after_int set test "-I with space should complete directories" -set cmd "perl -I fixture1/" +set cmd "perl -I fixtures/shared/default/" send "$cmd\t" expect { -re "^$cmd\r\nbar bar.d/ +foo.d/ *\r\n/@$cmd$" { pass "$test" } @@ -57,7 +57,7 @@ sync_after_int set test "-x without space should complete directories" -set cmd "perl -xfixture1/b" +set cmd "perl -xfixtures/shared/default/b" send "$cmd\t" expect { -re "^${cmd}ar\\\\ bar.d/ *$" { pass "$test" } @@ -70,7 +70,7 @@ sync_after_int set test "-x with space should complete directories" -set cmd "perl -x fixture1/b" +set cmd "perl -x fixtures/shared/default/b" send "$cmd\t" expect { -re "^${cmd}ar\\\\ bar.d/ *$" { pass "$test" } diff --git a/test/lib/completions/sbcl-mt.exp b/test/lib/completions/sbcl-mt.exp index 1188f197..45e81f79 100644 --- a/test/lib/completions/sbcl-mt.exp +++ b/test/lib/completions/sbcl-mt.exp @@ -11,7 +11,7 @@ proc teardown {} { setup -assert_complete {bar "bar bar.d/" foo foo.d/} "sbcl-mt fixture1/" +assert_complete {bar "bar bar.d/" foo foo.d/} "sbcl-mt fixtures/shared/default/" sync_after_int diff --git a/test/lib/completions/sbcl.exp b/test/lib/completions/sbcl.exp index d5f2c8c2..b68b2dce 100644 --- a/test/lib/completions/sbcl.exp +++ b/test/lib/completions/sbcl.exp @@ -11,7 +11,7 @@ proc teardown {} { setup -assert_complete {bar "bar bar.d/" foo foo.d/} "sbcl fixture1/" +assert_complete {bar "bar bar.d/" foo foo.d/} "sbcl fixtures/shared/default/" sync_after_int diff --git a/test/lib/completions/screen.exp b/test/lib/completions/screen.exp index f486b463..62c57362 100644 --- a/test/lib/completions/screen.exp +++ b/test/lib/completions/screen.exp @@ -18,7 +18,7 @@ sync_after_int set test "-c should complete files/dirs" -set dir fixture1 +set dir fixtures/shared/default set prompt "/$dir/@" assert_bash_exec "cd $dir" "" $prompt set cmd "screen -c " diff --git a/test/lib/completions/ssh.exp b/test/lib/completions/ssh.exp index 8a0d88d2..91955c1d 100644 --- a/test/lib/completions/ssh.exp +++ b/test/lib/completions/ssh.exp @@ -52,30 +52,17 @@ sync_after_int set test "First argument shouldn't complete with commands" -# NOTE: This test assumes the machine running this test has a command "bash" -# but no host named "bash" ... +# NOTE: This test assumes there's a command "bash" and no host named "bash" set cmd "ssh bas" -send "$cmd\t" -expect -ex "$cmd" -expect { - -timeout 1 - # In case multiple commands `bas*' - besides `bash' - are completed - -re "^\r\n.*bash.*\r\n/@$cmd$" { fail "$test" } - # In case the single command `bash' is completed - -re "h $" { fail "$test" } - # In case the hostname `bash_completion' is completed. - # See `scp' tests in `lib/completions/scp.exp' - -re "h_completion $" { pass "$test" } - -re ".+" { unresolved "$test" } - timeout { pass "$test" } -}; # expect +assert_complete [get_known_hosts "bas"] $cmd $test sync_after_int set test "First argument should complete partial hostname" -assert_complete_partial [get_hosts] ssh "" $test /@ 20 [list "ltrim_colon_completions"] +assert_complete_partial [get_hosts] ssh "" $test /@ 20 \ + [list "ltrim_colon_completions"] sync_after_int diff --git a/test/lib/completions/sudo.exp b/test/lib/completions/sudo.exp index 3dc98da3..1299a6da 100644 --- a/test/lib/completions/sudo.exp +++ b/test/lib/completions/sudo.exp @@ -11,7 +11,7 @@ proc teardown {} { setup -assert_complete "fixture1/foo.d/" "sudo cd fixture1/fo" +assert_complete "fixtures/shared/default/foo.d/" "sudo cd fixtures/shared/default/fo" sync_after_int diff --git a/test/lib/library.exp b/test/lib/library.exp index fa554c73..00dd8469 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,43 @@ 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. +# @param string $file (optional) File to source/run. Default is +# "lib/completions/$cmd.exp". +proc assert_source_completions {command {file ""}} { + if {[is_bash_completion_installed_for $command]} { + if {[string length $file] == 0} { + set file "lib/completions/$command.exp" + } + source $file + } else { + untested $command + } +}; # assert_source_completions() + + # Sort list. # `exec sort' is used instead of `lsort' to achieve exactly the # same sort order as in bash. @@ -462,10 +503,12 @@ proc bash_sort {items} { # Get 'known' hostnames. Looks also in ssh's 'known_hosts' files. +# @param string cword (optional) Word, hosts should start with. # @return list Hostnames # @see get_hosts() -proc get_known_hosts {} { - assert_bash_exec {_known_hosts_real ''; echo_array COMPREPLY} {} /@ result +proc get_known_hosts {{cword ''}} { + assert_bash_exec "_known_hosts_real '$cword'; echo_array COMPREPLY" \ + {} /@ result return $result }; # get_known_hosts() @@ -532,6 +575,24 @@ proc init_tcl_bash_globals {} { }; # init_tcl_bash_globals() +# Check whether completion is installed for the specified command by executing +# `complete -p ...' in bash. +# @param string $command Command to check completion availability for. +# @return boolean True (1) if completion is installed, False (0) if not. +proc is_bash_completion_installed_for {command} { + set test "$command should have completion installed in bash" + set cmd "complete -p $command &> /dev/null && echo -n 0 || echo -n 1" + send "$cmd\r" + expect "$cmd\r\n" + expect { + -ex 0 { set result true } + -ex 1 { set result false } + } + expect "/@" + return $result +}; # is_bash_completion_installed_for() + + # Detect if test suite is running under Cygwin/Windows proc is_cygwin {} { expr {[string first [string tolower [exec uname -s]] cygwin] >= 0} @@ -684,6 +745,73 @@ proc split_words_bash {line} { }; # split_words_bash() +# Given a list of items this proc finds a (part, full) pair so that when +# completing from $part $full will be the only option. +# +# Arguments: +# list The list of full completions. +# partName Output parameter for the partial string. +# fullName Output parameter for the full string, member of item. +# +# Results: +# 1, or 0 if no suitable result was found. +proc find_unique_completion_pair {{list} {partName} {fullName}} { + upvar $partName part + upvar $fullName full + set bestscore 0 + set list [lsort $list] + set n [llength $list] + for {set i 0} {$i < $n} {incr i} { + set cur [lindex $list $i] + set curlen [string length $cur] + + set prev [lindex $list [expr {$i - 1}]] + set next [lindex $list [expr {$i + 1}]] + set diffprev [expr {$prev == ""}] + set diffnext [expr {$next == ""}] + + # Analyse each item of the list and look for the minimum length of the + # partial prefix which is distinct from both $next and $prev. The list + # is sorted so the prefix will be unique in the entire list. + # + # In the worst case we analyse every character in the list 3 times. + # That's actually very fast, sorting could take more. + for {set j 0} {$j < $curlen} {incr j} { + set curchar [string index $cur $j] + if {!$diffprev && [string index $prev $j] != $curchar} { + set diffprev 1 + } + if {!$diffnext && [string index $next $j] != $curchar} { + set diffnext 1 + } + if {$diffnext && $diffprev} { + break + } + } + + # At the end of the loop $j is the index of last character of + # the unique partial prefix. The length is one plus that. + set parlen [expr {$j + 1}] + if {$parlen >= $curlen} { + continue + } + + # Try to find the most "readable pair"; look for a long pair where + # $part is about half of $full. + if {$parlen < $curlen / 2} { + set parlen [expr {$curlen / 2}] + } + set score [expr {$curlen - $parlen}] + if {$score > $bestscore} { + set bestscore $score + set part [string range $cur 0 [expr {$parlen - 1}]] + set full $cur + } + } + return [expr {$bestscore != 0}] +} + + # Start bash running as test environment. proc start_bash {} { global TESTDIR TOOL_EXECUTABLE spawn_id 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 diff --git a/test/unit/find_unique_completion_pair.exp b/test/unit/find_unique_completion_pair.exp new file mode 100644 index 00000000..ec7f040d --- /dev/null +++ b/test/unit/find_unique_completion_pair.exp @@ -0,0 +1,37 @@ +# Note: This test actually tests a function in the test library. It doesn't +# need bash running; but it doesn't hurt either. + +# Run one test. Look below for usage. +proc test_find_ucp {{list} {epart} {econt} {eret 1}} { + set efull "$epart$econt" + set rret [find_unique_completion_pair $list rpart rfull] + if {$eret != $rret} { + if {$eret} { + fail "find_unique_completion_pair: Nothing found for {$list}" + } else { + fail "find_unique_completion_pair: Expected failure for {$list}" + } + } elseif {!$eret} { + pass "find_unique_completion_pair: No results for list {$list}" + } elseif {$rpart != $epart || $rfull != $efull} { + fail "find_unique_completion_pair: Got \"$rpart\", \"$rfull\" \ + instead of \"$epart\", \"$efull\" for list {$list}" + } else { + pass "find_unique_completion_pair: Got \"$epart\", \"$efull\" \ + for list {$list}" + } +} + +test_find_ucp {a} 0 0 0 +test_find_ucp {ab} a b +test_find_ucp {a ab abcd abc} 0 0 0 +test_find_ucp {a ab abcde abc} abcd e +test_find_ucp {user1 user2} 0 0 0 +test_find_ucp {root username2 username1} ro ot +test_find_ucp {root username21 username2} ro ot +test_find_ucp {long_user_name lang_user_name long_usor_name} lang_us er_name +test_find_ucp {lang_user_name1 long_user_name lang_user_name long_usor_name} \ + long_use r_name +test_find_ucp {root username} user name +test_find_ucp {a aladin} ala din +test_find_ucp {ala aladin} alad in |