views:

866

answers:

1

What is the correct/best way of handling spaces and quotes in bash completion?

Here’s a simple example. I have a command called words (e.g., a dictionary lookup program) that takes various words as arguments. The supported ‘words’ may actually contain spaces, and are defined in a file called words.dat:

foo
bar one
bar two

Here’s my first suggested solution:

_find_words()
{
search="$cur"
grep -- "^$search" words.dat
}

_words_complete()
{
local IFS=$'\n'

COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"

COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

}
complete -F _words_complete words

Typing ‘words f<tab>’ correctly completes the command to ‘words foo ’ (with a trailing space), which is nice, but for ‘words b<tab>’ it suggests ‘words bar ’. The correct completion would be ‘words bar\ ’. And for ‘words "b<tab>’ and ‘words 'b<tab>’ it offers no suggestions.

This last part I have been able to solve. It’s possible to use eval to properly parse the (escaped) characters. However, eval is not fond of missing quotes, so to get everything to work, I had to change the search="$cur" to

search=$(eval echo "$cur" 2>/dev/null ||
eval echo "$cur'" 2>/dev/null ||
eval echo "$cur\"" 2>/dev/null || "")

This actually works. Both ‘words "b<tab>’ and ‘words 'b<tab>’ correctly autocompletes, and if I add a ‘o’ and press <tab> again, it actually completes the word and adds the correct closing quote. However, if I try to complete ‘words b<tab>’ or even ‘words bar\ <tab>’, it is autocompleted to ‘words bar ’ instead of ‘words bar\ ’, and adding for instance ‘one’ would fail when the words program is run.

Now, obviously it is possible to handle this correctly. For instance, the ls command can do it for files namned ‘foo’ ‘bar one’ and ‘bar two’ (though it does have problems with some ways of expressing the filenames when one uses a (valid) combination of both ", ' and various escapes). However, I couldn’t figure out how ls does it by reading the bash completion code.

So, does anybody know of how properly handle this? The actual input quotes need not be preserved; I would be happy with a solution that changes ‘words "b<tab>’, ‘words 'b<tab>’ and ‘words b<tab>’ to ‘words bar\ ’, for instance, (though I would prefer stripping of quotes, like in this example, instead of adding them).

+2  A: 

This not too elegant postprocessing solution seems to work for me (GNU bash, version 3.1.17(6)-release (i686-pc-cygwin)). (Unless I didn't test some border case as usual :))

Don't need to eval things, there are only 2 kinds of quotes.

Since compgen doesn't want to escape spaces for us, we will escape them ourselves (only if word didn't start with a quote). This has a side effect of full list (on double tab) having escaped values as well. Not sure if that's good or not, since ls doesn't do it...

EDIT: Fixed to handle single and double qoutes inside the words. Essentially we have to pass 3 unescapings :). First for grep, second for compgen, and last for words command itself when autocompletion is done.

_find_words()
{
    search=$(eval echo "$cur" 2>/dev/null || eval echo "$cur'" 2>/dev/null || eval echo "$cur\"" 2>/dev/null || "")
    grep -- "^$search" words.dat | sed -e "{" -e 's#\\#\\\\#g' -e "s#'#\\\'#g" -e 's#"#\\\"#g' -e "}"
}

_words_complete()
{
    local IFS=$'\n'

    COMPREPLY=()
    local cur="${COMP_WORDS[COMP_CWORD]}"

    COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

    local escaped_single_qoute="'\''"
    local i=0
    for entry in ${COMPREPLY[*]}
    do
     if [[ "${cur:0:1}" == "'" ]] 
     then
      # started with single quote, escaping only other single quotes
      # [']bla'bla"bla\bla bla --> [']bla'\''bla"bla\bla bla
      COMPREPLY[$i]="${entry//\'/${escaped_single_qoute}}" 
     elif [[ "${cur:0:1}" == "\"" ]] 
     then
      # started with double quote, escaping all double quotes and all backslashes
      # ["]bla'bla"bla\bla bla --> ["]bla'bla\"bla\\bla bla
      entry="${entry//\\/\\\\}" 
      COMPREPLY[$i]="${entry//\"/\\\"}" 
     else 
      # no quotes in front, escaping _everything_
      # [ ]bla'bla"bla\bla bla --> [ ]bla\'bla\"bla\\bla\ bla
      entry="${entry//\\/\\\\}" 
      entry="${entry//\'/\'}" 
      entry="${entry//\"/\\\"}" 
      COMPREPLY[$i]="${entry// /\\ }"
     fi
     (( i++ ))
    done
}
Eugene
Thanks. This solution works for the original examples, but if I add ‘rock 'n roll’ to words.dat, it fails. My real-life use of the autocompletion actually involves words with apostrophes, and that’s the reason I originally used `eval`. It’s easy enough (though not very elegant) to fix, by adding an extra ‘search and replace’ to the `for loop, and then add another for loop for strings beginning with '. The only remaining problem, as far as I can see, is that the autocompletion does not advance one cursor position if you have written an entire word, including any closing quotes.
Karl Ove Hufthammer
Regarding my comment above. It looks like the situation is slightly worse than I thought. Auto-completion inside a word containing apostrophes (e.g., trying to autocomplete ‘rock 'n ro’, either written using escaped spaces and apostrophe, or single or double quotes) doesn’t work. The reason is that the `search` variable is not in its correct expanded form. Some extra substitutions seems possible, but I haven’t been able to get this to work correctly for all the three different ways of escaping.
Karl Ove Hufthammer
Yes, compgen seems to be stripping all qoutes... Which means they must be escaped inside _find_words
Eugene
If you need to handle some other special characters ($, `, {, ( or whatever else bash might take offence on -- didn't test those), just escape them in similar way -- first backslash, then everything else in 2 or 3 places.
Eugene
There are still some things that doesn’t work, e.g. completing `"rock 'n`. Most of these can be fixed by removing `"$cur"` from the `_find_words` call. Then everything works for strings that starts with a double quote or nothing, except when ‘completing’ an already complete word starting with a double quote (since the last double quote incorrectly gets quotes?). And there are still some problems with completing words beginning with a single quote (only the last part of the word seems to be recognised when the completion suggestion is inserted). But all in all, this solution works OK. Thanks!
Karl Ove Hufthammer