Uma confusão sobre $ {array [*]} versus $ {array [@]} no contexto de conclusão de um bash

90

Estou tentando escrever uma conclusão de bash pela primeira vez e estou um pouco confuso sobre as duas maneiras de desreferenciar matrizes de bash ( ${array[@]}e${array[*]} ).

Aqui está o trecho de código relevante (funciona, a propósito, mas gostaria de entendê-lo melhor):

_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} ) )
}

A documentação do bash diz :

Qualquer elemento de um array pode ser referenciado usando $ {name [subscript]}. As chaves são necessárias para evitar conflitos com os operadores de expansão de nome de arquivo do shell. Se o subscrito for '@' ou '*', a palavra se expandirá para todos os membros do nome da matriz. Esses subscritos diferem apenas quando a palavra aparece entre aspas duplas. Se a palavra estiver entre aspas duplas, $ {name [*]} se expande para uma única palavra com o valor de cada membro da matriz separado pelo primeiro caractere da variável IFS, e $ {name [@]} expande cada elemento do nome para uma palavra separada.

Agora acho que entendo que compgen -W espera uma string contendo uma lista de palavras de alternativas possíveis, mas neste contexto eu não entendo o que "$ {name [@]} expande cada elemento do nome para uma palavra separada" significa.

Resumindo: ${array[*]}obras; ${array[@]}não. Eu gostaria de saber por que e de entender melhor o que exatamente se ${array[@]}expande.

Telêmaco
fonte

Respostas:

120

(Esta é uma expansão do meu comentário sobre a resposta de Kaleb Pederson - veja essa resposta para um tratamento mais geral do [@]vs. [*])

Quando o bash (ou qualquer shell semelhante) analisa uma linha de comando, ele a divide em uma série de "palavras" (que chamarei de "palavras de shell" para evitar confusão posteriormente). Geralmente, as palavras de shell são separadas por espaços (ou outros espaços em branco), mas os espaços podem ser incluídos em uma palavra de shell escapando ou citando-os. A diferença entre [@]e [*]-expanded arrays em aspas duplas é que "${myarray[@]}"faz com que cada elemento do array seja tratado como uma shell-word separada, enquanto "${myarray[*]}"resulta em uma única shell-word com todos os elementos do array separados por espaços (ou qualquer que seja o primeiro personagem deIFS ).

Normalmente, o [@]comportamento é o que você deseja. Suponha que temos perls=(perl-one perl-two)e usamos ls "${perls[*]}"- isso é equivalente a ls "perl-one perl-two", que procurará por um único arquivo nomeado perl-one perl-two, que provavelmente não é o que você queria. ls "${perls[@]}"é equivalente als "perl-one" "perl-two" , que tem muito mais probabilidade de fazer algo útil.

Fornecer uma lista de palavras de conclusão (que chamarei de palavras comp para evitar confusão com palavras shell) compgené diferente; a-W opção leva uma lista de palavras-comp, mas deve estar na forma de uma única palavra-shell com as palavras-comp separadas por espaços. Observe que as opções de comando que levam argumentos sempre (pelo menos até onde eu sei) levam uma única palavra de shell - caso contrário, não haveria nenhuma maneira de dizer quando os argumentos para a opção terminam e os argumentos regulares do comando (/ outros sinalizadores de opção) começam.

Em mais detalhes:

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

é equivalente a:

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

... que faz o que você quer. Por outro lado,

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

é equivalente a:

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

... o que é um absurdo completo: "perl-one" é a única palavra-comp anexada ao sinalizador -W, e o primeiro argumento real - que o compgen tomará como a string a ser completada - é "perl-dois / usr / bin / perl ". Eu esperaria compgen reclamar que recebeu argumentos extras ("-" e o que quer que esteja em $ cur), mas aparentemente ele simplesmente os ignora.

Gordon Davisson
fonte
3
Isto e excelente; obrigado. Eu realmente queria que explodisse mais alto, mas isso pelo menos esclarece por que não funcionou.
Telêmaco
61

Seu título pergunta sobre ${array[@]}versus, ${array[*]}mas você pergunta sobre $array[*]versus, o $array[@]que é um pouco confuso. Vou responder a ambos:

Quando você cita uma variável de matriz e usa @como um subscrito, cada elemento da matriz é expandido para seu conteúdo completo, independentemente do espaço em branco (na verdade, um de $IFS) que possa estar presente nesse conteúdo. Quando você usa o asterisco ( *) como o subscrito (independentemente de estar entre aspas ou não), ele pode se expandir para um novo conteúdo criado dividindo o conteúdo de cada elemento da matriz em $IFS.

Aqui está o script de exemplo:

#!/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

E aqui está o resultado:

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'

Eu pessoalmente geralmente quero "${myarray[@]}". Agora, para responder à segunda parte de sua pergunta, ${array[@]}versus $array[@].

Citando os documentos do bash, que você citou:

As chaves são necessárias para evitar conflitos com os operadores de expansão de nome de arquivo do shell.

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

Mas, quando você faz isso $myarray[@], o cifrão está fortemente vinculado a, myarrayportanto, é avaliado antes de [@]. Por exemplo:

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

Mas, conforme observado na documentação, os colchetes são para expansão de nome de arquivo, então vamos tentar isso:

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

Agora podemos ver que a expansão do nome do arquivo aconteceu após a $myarrayexapansão.

E mais uma nota, $myarraysem um subscrito se expande para o primeiro valor da matriz:

$ myarray[0]="one four"
$ echo $myarray[5]
one four[5]
Kaleb Pederson
fonte
1
Veja também esta a respeito de como IFSafeta a saída de forma diferente dependendo @vs. *e citou vs. unquoted.
Dennis Williamson
Peço desculpas, já que é muito importante neste contexto, mas sempre quis dizer ${array[*]}ou ${array[@]}. A falta de aparelho ortodôntico era simplesmente descuido. Além disso, você pode explicar o que ${array[*]}se expandiria no compgencomando? Ou seja, nesse contexto, o que significa expandir a matriz em cada um de seus elementos separadamente?
Telêmaco
Colocando de outra forma, você (como quase todas as fontes) diz que ${array[@]}geralmente é o caminho a seguir. O que estou tentando entender é por que nesse caso ${array[*]} funciona.
Telêmaco
1
É porque a lista de palavras fornecida com a opção -W deve ser fornecida como uma única palavra (que compgen então divide com base no IFS). Se for dividido em palavras separadas antes de ser entregue a compgen (que é o que [@] faz), compgen pensará que apenas o primeiro vem com -W, e o resto são argumentos regulares (e eu acho que espera apenas um argumento, e, portanto, vomitará).
Gordon Davisson
@Gordon: Mova isso para uma resposta e eu aceitarei. Isso é o que eu realmente queria saber. Obrigado. (Aliás, não vomita de uma maneira óbvia. Ele vomita silenciosamente - o que torna difícil saber o que deu errado.)
Telêmaco