101

我第一次尝试编写 bash 补全,我对取消引用 bash 数组 (${array[@]}${array[*]}) 的两种方法有点困惑。

这是相关的代码块(顺便说一句,它可以工作,但我想更好地理解它):

_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 的文档说

可以使用 ${name[subscript]} 引用数组的任何元素。需要大括号以避免与 shell 的文件名扩展运算符冲突。如果下标是“@”或“*”,则单词将扩展为数组名称的所有成员。只有当单词出现在双引号中时,这些下标才会有所不同。如果单词是双引号,则 ${name[*]} 扩展为单个单词,每个数组成员的值由 IFS 变量的第一个字符分隔,${name[@]} 扩展 name 的每个元素一个单独的词。

现在我想我明白这compgen -W需要一个包含可能替代词列表的字符串,但在这种情况下,我不明白“${name[@]} 将名称的每个元素扩展为一个单独的词”是什么意思。

长话短说:${array[*]}作品;${array[@]}没有。我想知道为什么,我想更好地理解究竟${array[@]}扩展成什么。

4

2 回答 2

138

(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.

于 2010-07-28T17:00:17.613 回答
74

您的标题询问了${array[@]}${array[*]}(均在 内{}),但随后您询问了$array[*]$array[@](均无{}),这有点令人困惑。我会回答两个(内{}):

当您引用数组变量并@用作下标时,数组的每个元素都将扩展为其完整内容,而不管该$IFS内容中可能存在的空格(实际上是其中一个)。当您使用星号 ( *) 作为下标时(不管它是否被引用),它可能会扩展为通过在 处分解每个数组元素的内容而创建的新内容$IFS

Here's the example script:

#!/bin/sh

myarray[0]="one"
myarray[1]="two"
myarray[3]="three four"

echo "with quotes around myarray[*]"
for x in "${myarray[*]}"; do
        echo "ARG[*]: '$x'"
done

echo "with quotes around myarray[@]"
for x in "${myarray[@]}"; do
        echo "ARG[@]: '$x'"
done

echo "without quotes around myarray[*]"
for x in ${myarray[*]}; do
        echo "ARG[*]: '$x'"
done

echo "without quotes around myarray[@]"
for x in ${myarray[@]}; do
        echo "ARG[@]: '$x'"
done

And here's it's output:

with quotes around myarray[*]
ARG[*]: 'one two three four'
with quotes around myarray[@]
ARG[@]: 'one'
ARG[@]: 'two'
ARG[@]: 'three four'
without quotes around myarray[*]
ARG[*]: 'one'
ARG[*]: 'two'
ARG[*]: 'three'
ARG[*]: 'four'
without quotes around myarray[@]
ARG[@]: 'one'
ARG[@]: 'two'
ARG[@]: 'three'
ARG[@]: 'four'

I personally usually want "${myarray[@]}". Now, to answer the second part of your question, ${array[@]} versus $array[@].

Quoting the bash docs, which you quoted:

The braces are required to avoid conflicts with the shell's filename expansion operators.

$ myarray=
$ myarray[0]="one"
$ myarray[1]="two"
$ echo ${myarray[@]}
one two

But, when you do $myarray[@], the dollar sign is tightly bound to myarray so it is evaluated before the [@]. For example:

$ ls $myarray[@]
ls: cannot access one[@]: No such file or directory

But, as noted in the documentation, the brackets are for filename expansion, so let's try this:

$ touch one@
$ ls $myarray[@]
one@

Now we can see that the filename expansion happened after the $myarray exapansion.

And one more note, $myarray without a subscript expands to the first value of the array:

$ myarray[0]="one four"
$ echo $myarray[5]
one four[5]
于 2010-07-27T22:53:28.243 回答