Expansão da matriz vazia do Bash com `set -u`

103

Estou escrevendo um script bash que tem set -u, e tenho um problema com a expansão da matriz vazia: o bash parece tratar uma matriz vazia como uma variável não definida durante a expansão:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]}'"
bash: arr[@]: unbound variable

( declare -a arrtambém não ajuda.)

Uma solução comum para isso é usar em ${arr[@]-}vez disso, substituindo assim uma string vazia em vez do array vazio ("indefinido"). No entanto, esta não é uma boa solução, pois agora você não pode discernir entre um array com uma única string vazia e um array vazio. (@ -expansion é especial no bash, ele se expande "${arr[@]}"para "${arr[0]}" "${arr[1]}" …, o que o torna uma ferramenta perfeita para construir linhas de comando.)

$ countArgs() { echo $#; }
$ countArgs a b c
3
$ countArgs
0
$ countArgs ""
1
$ brr=("")
$ countArgs "${brr[@]}"
1
$ countArgs "${arr[@]-}"
1
$ countArgs "${arr[@]}"
bash: arr[@]: unbound variable
$ set +u
$ countArgs "${arr[@]}"
0

Portanto, há uma maneira de contornar esse problema, além de verificar o comprimento de uma matriz em um if(veja o exemplo de código abaixo), ou desativar a -uconfiguração para esse pequeno pedaço?

if [ "${#arr[@]}" = 0 ]; then
   veryLongCommandLine
else
   veryLongCommandLine "${arr[@]}"
fi

Atualização:bugs Tag removida devido à explicação de ikegami.

Ivan Tarasov
fonte

Respostas:

17

O único idioma seguro é${arr[@]+"${arr[@]}"}

Esta já é a recomendação na resposta de ikegami , mas há muita desinformação e suposições neste tópico. Outros padrões, como ${arr[@]-}ou ${arr[@]:0}, não são seguros em todas as versões principais do Bash.

Como mostra a tabela abaixo, a única expansão confiável em todas as versões modernas do Bash é ${arr[@]+"${arr[@]}"}(coluna +"). É importante notar que várias outras expansões falham no Bash 4.2, incluindo (infelizmente) o ${arr[@]:0}idioma mais curto , que não apenas produz um resultado incorreto, mas também falha. Se você precisa oferecer suporte a versões anteriores a 4.4 e, em particular, 4.2, este é o único idioma funcional.

Captura de tela de diferentes idiomas nas versões

Infelizmente outro + expansões que, à primeira vista, parecem iguais, de fato emitem comportamentos diferentes. :+a expansão não é segura, porque :-expansion trata uma matriz com um único elemento vazio ( ('')) como "nulo" e, portanto, não se expande (de forma consistente) para o mesmo resultado.

Citar a expansão completa em vez do array aninhado ( "${arr[@]+${arr[@]}}"), que eu esperava ser aproximadamente equivalente, é igualmente inseguro em 4.2.

Você pode ver o código que gerou esses dados junto com os resultados de várias versões adicionais do bash nesta essência .

dimo414
fonte
1
Eu não vejo você testando "${arr[@]}". Estou esquecendo de algo? Pelo que posso ver, funciona pelo menos em 5.x.
x-yuri
1
@ x-yuri sim, o Bash 4.4 corrigiu a situação; você não precisa usar esse padrão se sabe que seu script só será executado no 4.4+, mas muitos sistemas ainda estão em versões anteriores.
dimo414
Absolutamente. Apesar de ter uma boa aparência (por exemplo, formatação), os espaços extras são um grande mal do bash, causando muitos problemas
agg3l
81

De acordo com a documentação,

Uma variável de matriz é considerada definida se um valor subscrito foi atribuído. A string nula é um valor válido.

Nenhum subscrito foi atribuído a um valor, então a matriz não está definida.

Mas, embora a documentação sugira que um erro é apropriado aqui, esse não é mais o caso desde o 4.4 .

$ bash --version | head -n 1
GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)

$ set -u

$ arr=()

$ echo "foo: '${arr[@]}'"
foo: ''

Há uma condição que você pode usar embutida para conseguir o que deseja nas versões anteriores: Use em ${arr[@]+"${arr[@]}"}vez de "${arr[@]}".

$ function args { perl -E'say 0+@ARGV; say "$_: $ARGV[$_]" for 0..$#ARGV' -- "$@" ; }

$ set -u

$ arr=()

$ args "${arr[@]}"
-bash: arr[@]: unbound variable

$ args ${arr[@]+"${arr[@]}"}
0

$ arr=("")

$ args ${arr[@]+"${arr[@]}"}
1
0: 

$ arr=(a b c)

$ args ${arr[@]+"${arr[@]}"}
3
0: a
1: b
2: c

Testado com bash 4.2.25 e 4.3.11.

ikegami
fonte
4
Alguém pode explicar como e por que isso funciona? Estou confuso sobre o que [@]+realmente faz e por que o segundo ${arr[@]}não causa um erro ilimitado.
Martin von Wittich
2
${parameter+word}wordse expande se parameternão estiver desarmado.
ikegami
2
${arr+"${arr[@]}"}é mais curto e parece funcionar tão bem.
Per Cederberg
3
@Per Cerderberg, não funciona. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
Ikegami
1
Para ser preciso, nos casos em que a +expansão não ocorre (ou seja, uma matriz vazia), a expansão é substituída por nada , que é exatamente o que uma matriz vazia se expande. :+não é seguro porque também trata uma ('')matriz de elemento único como indefinida e de forma semelhante se expande para nada, perdendo o valor.
dimo414
23

A resposta aceita de @ikegami está sutilmente errada! O encantamento correto é ${arr[@]+"${arr[@]}"}:

$ countArgs () { echo "$#"; }
$ arr=('')
$ countArgs "${arr[@]:+${arr[@]}}"
0   # WRONG
$ countArgs ${arr[@]+"${arr[@]}"}
1   # RIGHT
$ arr=()
$ countArgs ${arr[@]+"${arr[@]}"}
0   # Let's make sure it still works for the other case...
ijs
fonte
Não faz mais diferença. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"produz 1. Mas a ${arr[@]+"${arr[@]}"}forma permite diferenciar entre valor vazio / não vazio adicionando / não adicionando dois pontos.
x-yuri
arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
x-yuri
1
Isso foi corrigido em minha resposta há muito tempo. (Na verdade, tenho certeza de que deixei um comentário sobre essa resposta anteriormente ?!)
ikegami
16

Acontece que o manuseio do array foi alterado no recentemente lançado (2016/09/16) bash 4.4 (disponível no Debian stretch, por exemplo).

$ bash --version | head -n1
bash --version | head -n1
GNU bash, version 4.4.0(1)-release (x86_64-pc-linux-gnu)

Agora, a expansão de matrizes vazias não emite aviso

$ set -u
$ arr=()
$ echo "${arr[@]}"

$ # everything is fine
agg3l
fonte
Posso confirmar, com bash-4.4.12 "${arr[@]}"seria suficiente.
x-yuri
14

esta pode ser outra opção para aqueles que preferem não duplicar arr [@] e estão bem em ter uma string vazia

echo "foo: '${arr[@]:-}'"

testar:

set -u
arr=()
echo a "${arr[@]:-}" b # note two spaces between a and b
for f in a "${arr[@]:-}" b; do echo $f; done # note blank line between a and b
arr=(1 2)
echo a "${arr[@]:-}" b
for f in a "${arr[@]:-}" b; do echo $f; done
Jayen
fonte
10
Isso funcionará se você estiver apenas interpolando a variável, mas se quiser usar a matriz em um, forisso acabará com uma única string vazia quando a matriz for indefinida / definida como vazia, onde como você pode querer o corpo do loop para não ser executado se a matriz não estiver definida.
Ash Berlin-Taylor
obrigado @AshBerlin, adicionei um loop for à minha resposta para que os leitores fiquem sabendo
Jayen
-1 para esta abordagem, é simplesmente incorreto. Isso substitui uma matriz vazia por uma única string vazia, que não é a mesma. O padrão sugerido na resposta aceita ${arr[@]+"${arr[@]}"}, preserva corretamente o estado de matriz vazia.
dimo414
Veja também minha resposta mostrando as situações em que essa expansão falha.
dimo414
não está incorreto. ele diz explicitamente que fornecerá uma string vazia, e há até dois exemplos onde você pode ver a string vazia.
Jayen
7

A resposta de @ikegami está correta, mas considero a sintaxe ${arr[@]+"${arr[@]}"}terrível. Se você usar nomes de variáveis ​​de matriz longa, começa a parecer espaguete mais rápido do que o normal.

Em vez disso, tente isto:

$ set -u

$ count() { echo $# ; } ; count x y z
3

$ count() { echo $# ; } ; arr=() ; count "${arr[@]}"
-bash: abc[@]: unbound variable

$ count() { echo $# ; } ; arr=() ; count "${arr[@]:0}"
0

$ count() { echo $# ; } ; arr=(x y z) ; count "${arr[@]:0}"
3

Parece que o operador de fatia da matriz Bash é muito complacente.

Então, por que Bash dificultou tanto o tratamento do caso extremo dos arrays? Suspiro. Não posso garantir que sua versão permitirá tal abuso do operador de fatia de array, mas funciona muito bem para mim.

Advertência: Estou usando GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) sua milhagem pode variar.

Kevinarpe
fonte
9
ikegami originalmente tinha isso, mas o removeu porque não é confiável, tanto em teoria (não há razão para que funcione) e na prática (a versão do OP do bash não o aceitava).
@hvd: Obrigado pela atualização. Leitores: por favor, adicionem um comentário se você encontrar versões do bash onde o código acima não funciona.
kevinarpe
hvp ja fez, e vou te dizer tambem: "${arr[@]:0}"-bash: arr[@]: unbound variable.
ikegami
Uma coisa que deve funcionar em todas as versões é definir um valor de array padrão arr=("_dummy_")e usar a expansão em ${arr[@]:1}todos os lugares. Isso é citado em outras respostas, referindo-se aos valores sentinela.
init_js
1
@init_js: Infelizmente sua edição foi rejeitada. Eu sugiro que você adicione como uma resposta separada. (Ref: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe
6

De fato, inconsistência "interessante".

Além disso,

$ set -u
$ echo $#
0
$ echo "$1"
bash: $1: unbound variable   # makes sense (I didn't set any)
$ echo "$@" | cat -e
$                            # blank line, no error

Embora eu concorde que o comportamento atual pode não ser um bug no sentido que @ikegami explica, IMO podemos dizer que o bug está na definição (de "set") em si, e / ou no fato de que é aplicado de forma inconsistente. O parágrafo anterior na página de manual diz

... ${name[@]}expande cada elemento do nome para uma palavra separada. Quando não há membros da matriz, se ${name[@]}expande para nada.

o que é inteiramente consistente com o que diz sobre a expansão dos parâmetros posicionais em "$@". Não que não haja outras inconsistências nos comportamentos de matrizes e parâmetros posicionais ... mas para mim não há indícios de que esse detalhe seja inconsistente entre os dois.

Continuando,

$ arr=()
$ echo "${arr[@]}"
bash: arr[@]: unbound variable   # as we've observed.  BUT...
$ echo "${#arr[@]}"
0                                # no error
$ echo "${!arr[@]}" | cat -e
$                                # no error

Portanto, arr[]não é tão desvinculado que não podemos obter uma contagem de seus elementos (0) ou uma lista (vazia) de suas chaves? Para mim, eles são sensatos e úteis - o único valor atípico parece ser o${arr[@]} (e ${arr[*]}) expansão.

don311
fonte
2

Estou complementando no @ ikegami (aceito) e no @kevinarpe (também bom).

Você pode fazer "${arr[@]:+${arr[@]}}"para contornar o problema. O lado direito (ou seja, depois:+ ) fornece uma expressão que será usada caso o lado esquerdo não seja definido / nulo.

A sintaxe é misteriosa. Observe que o lado direito da expressão sofrerá expansão de parâmetros, portanto, atenção extra deve ser dada para ter citações consistentes.

: example copy arr into arr_copy
arr=( "1 2" "3" )
arr_copy=( "${arr[@]:+${arr[@]}}" ) # good. same quoting. 
                                    # preserves spaces

arr_copy=( ${arr[@]:+"${arr[@]}"} ) # bad. quoting only on RHS.
                                    # copy will have ["1","2","3"],
                                    # instead of ["1 2", "3"]

Como @kevinarpe menciona, uma sintaxe menos misteriosa é usar a notação de fatia de array ${arr[@]:0}(nas versões Bash >= 4.4), que se expande para todos os parâmetros, começando do índice 0. Também não requer tanta repetição. Essa expansão funciona independentemente set -u, então você pode usar isso a qualquer momento. A página do manual diz (em Expansão de parâmetro ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Se o parâmetro for um nome de matriz indexado com subscrito por @ou *, o resultado será o comprimento dos membros da matriz começando com ${parameter[offset]}. Um deslocamento negativo é obtido em relação a um maior que o índice máximo da matriz especificada. É um erro de expansão se o comprimento for avaliado como um número menor que zero.

Este é o exemplo fornecido por @kevinarpe, com formatação alternativa para colocar a saída em evidência:

set -u
function count() { echo $# ; };
(
    count x y z
)
: prints "3"

(
    arr=()
    count "${arr[@]}"
)
: prints "-bash: arr[@]: unbound variable"

(
    arr=()
    count "${arr[@]:0}"
)
: prints "0"

(
    arr=(x y z)
    count "${arr[@]:0}"
)
: prints "3"

Esse comportamento varia com as versões do Bash. Você também deve ter notado que o operador de comprimento ${#arr[@]}sempre avaliará como 0para matrizes vazias, independentemente de set -u, sem causar um 'erro de variável não acoplada'.

init_js
fonte
Infelizmente, o :0idioma falha no Bash 4.2, então essa não é uma abordagem segura. Veja minha resposta .
dimo414
1

Aqui estão algumas maneiras de fazer algo assim, uma usando sentinelas e outra usando anexos condicionais:

#!/bin/bash
set -o nounset -o errexit -o pipefail
countArgs () { echo "$#"; }

arrA=( sentinel )
arrB=( sentinel "{1..5}" "./*" "with spaces" )
arrC=( sentinel '$PWD' )
cmnd=( countArgs "${arrA[@]:1}" "${arrB[@]:1}" "${arrC[@]:1}" )
echo "${cmnd[@]}"
"${cmnd[@]}"

arrA=( )
arrB=( "{1..5}" "./*"  "with spaces" )
arrC=( '$PWD' )
cmnd=( countArgs )
# Checks expansion of indices.
[[ ! ${!arrA[@]} ]] || cmnd+=( "${arrA[@]}" )
[[ ! ${!arrB[@]} ]] || cmnd+=( "${arrB[@]}" )
[[ ! ${!arrC[@]} ]] || cmnd+=( "${arrC[@]}" )
echo "${cmnd[@]}"
"${cmnd[@]}"
saco sólido
fonte
0

Inconsistência interessante; isso permite que você defina algo que "não é considerado definido", mas aparece na saída dedeclare -p

arr=()
set -o nounset
echo ${arr[@]}
 =>  -bash: arr[@]: unbound variable
declare -p arr
 =>  declare -a arr='()'

ATUALIZAÇÃO: conforme outros mencionados, corrigido no 4.4 divulgado após a publicação desta resposta.

Março
fonte
Isso é apenas sintaxe de array incorreta; você precisa echo ${arr[@]}(mas antes do Bash 4.4 você ainda verá um erro).
dimo414
Obrigado @ dimo414, da próxima vez sugira uma edição em vez de downvoting. BTW, se você tivesse tentado echo $arr[@]você mesmo, teria visto que a mensagem de erro é diferente.
MarcH
-2

A forma mais simples e compatível parece ser:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"
Nikolay
fonte
1
O próprio OP mostrou que isso não funciona. Ele se expande para uma string vazia em vez de nada.
ikegami
Certo, então está tudo bem para interpolação de string, mas não em loop.
Craig Ringer