Bash expansión de matriz vacía con `set -u`

103

Estoy escribiendo un script de bash que tiene set -u, y tengo un problema con la expansión de matriz vacía: bash parece tratar una matriz vacía como una variable no configurada durante la expansión:

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

( declare -a arrtampoco ayuda)

Una solución común a esto es usar ${arr[@]-}en su lugar, sustituyendo así una cadena vacía en lugar de la matriz vacía ("indefinida"). Sin embargo, esta no es una buena solución, ya que ahora no puede distinguir entre una matriz con una sola cadena vacía y una matriz vacía. (@ -Ampliación es especial en bash, se expande "${arr[@]}"en "${arr[0]}" "${arr[1]}" …, lo que hace que sea una herramienta perfecta para la construcción de líneas 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

Entonces, ¿hay alguna forma de solucionar ese problema, además de verificar la longitud de una matriz en un if(ver ejemplo de código a continuación), o desactivar la -uconfiguración para esa pieza corta?

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

Actualización:bugs etiqueta eliminada debido a la explicación de ikegami.

Ivan Tarasov
fuente

Respuestas:

17

El único modismo seguro es${arr[@]+"${arr[@]}"}

Esta ya es la recomendación en la respuesta de ikegami , pero hay mucha información errónea y conjeturas en este hilo. Otros patrones, como ${arr[@]-}o ${arr[@]:0}, no son seguros en todas las versiones principales de Bash.

Como muestra la siguiente tabla, la única expansión que es confiable en todas las versiones modernas de Bash es ${arr[@]+"${arr[@]}"}(columna +"). Es de destacar que varias otras expansiones fallan en Bash 4.2, incluido (desafortunadamente) el ${arr[@]:0}modismo más corto , que no solo produce un resultado incorrecto sino que en realidad falla. Si necesita admitir versiones anteriores a la 4.4, y en particular a la 4.2, este es el único idioma que funciona.

Captura de pantalla de diferentes modismos entre versiones

Desafortunadamente, otras +expansiones que, a simple vista, parecen iguales, emiten un comportamiento diferente. :+la expansión no es segura, porque :-expansion trata una matriz con un solo elemento vacío ( ('')) como "nulo" y, por lo tanto, no se expande (consistentemente) al mismo resultado.

Citar la expansión completa en lugar de la matriz anidada ( "${arr[@]+${arr[@]}}"), que habría esperado que fuera aproximadamente equivalente, es igualmente inseguro en 4.2.

Puede ver el código que generó estos datos junto con los resultados de varias versiones adicionales de bash en esta esencia .

dimo414
fuente
1
No te veo probando "${arr[@]}". ¿Me estoy perdiendo de algo? Por lo que puedo ver, funciona al menos en 5.x.
x-yuri
1
@ x-yuri sí, Bash 4.4 arregló la situación; no necesita usar este patrón si sabe que su script solo se ejecutará en 4.4+, pero muchos sistemas todavía tienen versiones anteriores.
dimo414
Absolutamente. A pesar de verse bien (por ejemplo, formatear), los espacios extra son un gran mal de bash, causando muchos problemas
agg3l
81

Según la documentación,

Una variable de matriz se considera establecida si a un subíndice se le ha asignado un valor. La cadena nula es un valor válido.

No se ha asignado un valor a ningún subíndice, por lo que la matriz no está configurada.

Pero aunque la documentación sugiere que aquí es apropiado un error, este ya no es el caso desde 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: ''

Hay un condicional que puede usar en línea para lograr lo que desea en versiones anteriores: use en ${arr[@]+"${arr[@]}"}lugar 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

Probado con bash 4.2.25 y 4.3.11.

ikegami
fuente
4
¿Alguien puede explicar cómo y por qué funciona esto? Estoy confundido acerca de lo que [@]+realmente hace y por qué el segundo ${arr[@]}no causará un error desatado.
Martin von Wittich
2
${parameter+word}solo se expande wordsi parameterno está desarmado.
ikegami
2
${arr+"${arr[@]}"}es más corto y parece funcionar igual de bien.
Per Cederberg
3
@Per Cerderberg, no funciona. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
Ikegami
1
Para ser precisos, en los casos en los que la +expansión no ocurre (es decir, una matriz vacía), la expansión se reemplaza por nada , que es exactamente a lo que se expande una matriz vacía. :+no es seguro porque también trata una ('')matriz de un solo elemento como no configurada y de manera similar se expande a nada, perdiendo el valor.
dimo414
23

¡La respuesta aceptada de @ikegami es sutilmente incorrecta! El encantamiento correcto es ${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
fuente
Ya no marca la diferencia. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"produce 1. Pero la ${arr[@]+"${arr[@]}"}forma permite diferenciar entre valor vacío / no vacío al agregar / no agregar dos puntos.
x-yuri
arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
x-yuri
1
Esto se ha solucionado en mi respuesta hace mucho tiempo. (¡De hecho, estoy seguro de que anteriormente dejé un comentario en esta respuesta en ese sentido!)
ikegami
16

Resulta que el manejo de matrices ha sido cambiado en bash 4.4 recientemente lanzado (2016/09/16) (disponible en Debian stretch, por ejemplo).

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

Ahora la expansión de matrices vacías no emite advertencia

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

$ # everything is fine
agg3l
fuente
Puedo confirmarlo, con bash-4.4.12 "${arr[@]}"sería suficiente.
x-yuri
14

esta puede ser otra opción para aquellos que prefieren no duplicar arr [@] y están bien tener una cadena vacía

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

Probar:

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
fuente
10
Esto funcionará si solo está interpolando la variable, pero si desea usar la matriz en una, foresto terminaría con una sola cadena vacía cuando la matriz no esté definida / definida como vacía, donde, como podría querer, el cuerpo del bucle para que no se ejecute si la matriz no está definida.
Ash Berlin-Taylor
gracias @AshBerlin, agregué un bucle for a mi respuesta para que los lectores estén al tanto
Jayen
-1 a este enfoque, es simplemente incorrecto. Esto reemplaza una matriz vacía con una sola cadena vacía, que no es lo mismo. El patrón sugerido en la respuesta aceptada ${arr[@]+"${arr[@]}"}conserva correctamente el estado de matriz vacía.
dimo414
Vea también mi respuesta que muestra las situaciones en las que esta expansión se rompe.
dimo414
no es incorrecto. dice explícitamente que dará una cadena vacía, e incluso hay dos ejemplos donde puede ver la cadena vacía.
Jayen
7

La respuesta de @ikegami es correcta, pero considero que la sintaxis es ${arr[@]+"${arr[@]}"}terrible. Si usa nombres de variables de matriz largos, comenzará a verse más rápido de lo habitual.

Prueba esto en su lugar:

$ 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 el operador de corte de matriz Bash es muy indulgente.

Entonces, ¿por qué Bash hizo que el manejo del caso extremo de las matrices fuera tan difícil? Suspiro. No puedo garantizar que su versión permita tal abuso del operador de corte de matriz, pero funciona a la perfección para mí.

Advertencia: estoy usando GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) Su kilometraje puede variar.

kevinarpe
fuente
9
ikegami originalmente tenía esto, pero lo eliminó porque no es confiable, tanto en teoría (no hay ninguna razón por la que esto debería funcionar) como en la práctica (la versión de OP de bash no lo aceptó).
@hvd: Gracias por la actualización. Lectores: agregue un comentario si encuentra versiones de bash donde el código anterior no funciona.
kevinarpe
hvp ya lo hizo, y yo también le diré: "${arr[@]:0}"da -bash: arr[@]: unbound variable.
ikegami
Una cosa que debería funcionar en todas las versiones es establecer un valor de matriz predeterminado arr=("_dummy_")y usar la expansión en ${arr[@]:1}todas partes. Esto se menciona en otras respuestas, refiriéndose a los valores centinela.
init_js
1
@init_js: Su edición fue lamentablemente rechazada. Le sugiero que agregue como una respuesta separada. (Ref: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe
6

Incoherencia "interesante" de hecho.

Además,

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

Si bien estoy de acuerdo en que el comportamiento actual puede no ser un error en el sentido que explica @ikegami, en mi opinión podríamos decir que el error está en la definición (de "conjunto") en sí, y / o en el hecho de que se aplica de manera inconsistente. El párrafo anterior en la página del manual dice

... ${name[@]}expande cada elemento del nombre a una palabra separada. Cuando no hay miembros de la matriz, se ${name[@]}expande a nada.

que es totalmente coherente con lo que dice sobre la expansión de los parámetros posicionales en "$@". No es que no haya otras inconsistencias en el comportamiento de las matrices y los parámetros posicionales ... pero para mí no hay indicios de que este detalle deba ser inconsistente entre los dos.

Continuo,

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

Entonces, arr[]¿no es tan independiente que no podemos obtener un recuento de sus elementos (0) o una lista (vacía) de sus claves? Para mí, estos son razonables y útiles; el único valor atípico parece ser la ${arr[@]}(y ${arr[*]}) expansión.

don311
fuente
2

Estoy complementando las respuestas de @ ikegami (aceptadas) y de @ kevinarpe (también buenas).

Puede hacer "${arr[@]:+${arr[@]}}"para solucionar el problema. El lado derecho (es decir, después :+) proporciona una expresión que se utilizará en caso de que el lado izquierdo no esté definido / nulo.

La sintaxis es misteriosa. Tenga en cuenta que el lado derecho de la expresión sufrirá una expansión de parámetros, por lo que se debe prestar especial atención a tener citas 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 menciona @kevinarpe, una sintaxis menos arcana es usar la notación de corte de matriz ${arr[@]:0}(en las versiones Bash >= 4.4), que se expande a todos los parámetros, comenzando desde el índice 0. Tampoco requiere tanta repetición. Esta expansión funciona independientemente de set -u, por lo que puede usarla en todo momento. La página del manual dice (en Expansión de parámetros ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Si el parámetro es un nombre de matriz indexado subindicado por @o *, el resultado son los miembros de longitud de la matriz que comienzan con ${parameter[offset]}. Se toma un desplazamiento negativo relativo a uno mayor que el índice máximo de la matriz especificada. Es un error de expansión si la longitud se evalúa como un número menor que cero.

Este es el ejemplo proporcionado por @kevinarpe, con formato alternativo para poner la salida en evidencia:

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"

Este comportamiento varía con las versiones de Bash. También puede haber notado que el operador de longitud ${#arr[@]}siempre evaluará 0para matrices vacías, independientemente de set -u, sin causar un 'error de variable no vinculada'.

init_js
fuente
Desafortunadamente, el :0idioma falla en Bash 4.2, por lo que este no es un enfoque seguro. Mira mi respuesta .
dimo414
1

Aquí hay un par de formas de hacer algo como esto, una usando centinelas y otra usando anexos condicionales:

#!/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[@]}"
solidsnack
fuente
0

Incoherencia interesante; esto le permite definir algo que "no se considera configurado" pero que aparece en la salida dedeclare -p

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

ACTUALIZACIÓN: como mencionaron otros, se corrigió en 4.4 lanzado después de que se publicó esta respuesta.

Marzo
fuente
Eso es solo una sintaxis de matriz incorrecta; necesita echo ${arr[@]}(pero antes de Bash 4.4 todavía verá un error).
dimo414
Gracias @ dimo414, la próxima vez sugiera una edición en lugar de votar negativamente. Por cierto, si lo hubiera intentado echo $arr[@]usted mismo, habría visto que el mensaje de error es diferente.
MarcH
-2

La forma más sencilla y compatible parece ser:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"
Nikolai
fuente
1
Los propios OP demostraron que esto no funciona. Se expande a una cadena vacía en lugar de nada.
ikegami
Bien, entonces está bien para la interpolación de cadenas pero no para bucles.
Craig Ringer