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.

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 .


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.


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}só 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...

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

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

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.


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}"dá -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.


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


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[@]}"

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.


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[@]-}'"

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.