A confusion about ${array[*]} versus ${array[@]} in the context of a bash completion

arrays, bash, bash-completion

Solution

(This is an expansion of my comment on Kaleb Pederson's answer -- see that answer for a more general treatment of `[@]` vs `[*]`.)

When bash (or any similar shell) parses a command line, it splits it into a series of "words" (which I will call "shell-words" to avoid confusion later). Generally, shell-words are separated by spaces (or other whitespace), but spaces can be included in a shell-word by escaping or quoting them. The difference between `[@]` and `[*]`-expanded arrays in double-quotes is that `"${myarray[@]}"` leads to each element of the array being treated as a separate shell-word, while `"${myarray[*]}"` results in a single shell-word with all of the elements of the array separated by spaces (or whatever the first character of `IFS` is).

Usually, the `[@]` behavior is what you want. Suppose we have `perls=(perl-one perl-two)` and use `ls "${perls[*]}"` -- that's equivalent to `ls "perl-one perl-two"`, which will look for single file named `perl-one perl-two`, which is probably not what you wanted. `ls "${perls[@]}"` is equivalent to `ls "perl-one" "perl-two"`, which is much more likely to do something useful.

Providing a list of completion words (which I will call comp-words to avoid confusion with shell-words) to `compgen` is different; the `-W` option takes a list of comp-words, but it must be in the form of a single shell-word with the comp-words separated by spaces. Note that command options that take arguments always (at least as far as I know) take a single shell-word -- otherwise there'd be no way to tell when the arguments to the option end, and the regular command arguments (/other option flags) begin.

In more detail:

perls=(perl-one perl-two)
compgen -W "${perls[*]} /usr/bin/perl" -- ${cur}

is equivalent to:

compgen -W "perl-one perl-two /usr/bin/perl" -- ${cur}

...which does what you want. On the other hand,

perls=(perl-one perl-two)
compgen -W "${perls[@]} /usr/bin/perl" -- ${cur}

is equivalent to:

compgen -W "perl-one" "perl-two /usr/bin/perl" -- ${cur}

...which is complete nonsense: "perl-one" is the only comp-word attached to the -W flag, and the first real argument -- which compgen will take as the string to be completed -- is "perl-two /usr/bin/perl". I'd expect compgen to complain that it's been given extra arguments ("--" and whatever's in $cur), but apparently it just ignores them.

Problem

I'm taking a stab at writing a bash completion for the first time, and I'm a bit confused about about the two ways of dereferencing bash arrays (`${array[@]}` and `${array[*]}`). Here's the relevant chunk of code (it works, but I would like to understand it better): ``` _switch() { local cur perls local ROOT=${PERLBREW_ROOT:-$HOME/perl5/perlbrew} COMPREPLY=() cur=${COMP_WORDS[COMP_CWORD]} perls=($ROOT/perls/perl-*) # remove all but the final part of the name perls=(${perls[*]##*/}) COMPREPLY=( $( compgen -W "${perls[*]} /usr/bin/perl" -- ${cur} ) ) } ``` bash's documentation says: Any element of an array may be referenced using ${name[subscript]}. The braces are required to avoid conflicts with the shell's filename expansion operators. If the subscript is ‘@’ or ‘*’, the word expands to all members of the array name. These subscripts differ only when the word appears within double quotes. If the word is double-quoted, ${name[*]} expands to a single word with the value of each array member separated by the first character of the IFS variable, and ${name[@]} expands each element of name to a separate word. Now I think I understand that `compgen -W` expects a string containing a wordlist of possible alternatives, but in this context I don't understand what "${name[@]} expands each element of name to a separate word" means. Long story short: `${array[*]}` works; `${array[@]}` doesn't. I would like to know why, and I would like to understand better what exactly `${array[@]}` expands into.

Original source

Related problems