Bash l'espansione dell'array vuoto con `set -u`


103

Sto scrivendo uno script bash che ha set -ue ho un problema con l'espansione dell'array vuoto: bash sembra trattare un array vuoto come una variabile non impostata durante l'espansione:

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

( declare -a arrnon aiuta neanche.)

Una soluzione comune a questo è usare ${arr[@]-}invece, sostituendo così una stringa vuota invece dell'array vuoto ("non definito"). Tuttavia questa non è una buona soluzione, poiché ora non è possibile distinguere tra un array con una singola stringa vuota e un array vuoto. (@ -expansion è speciale in bash, si espande "${arr[@]}"in "${arr[0]}" "${arr[1]}" …, il che lo rende uno strumento perfetto per la creazione di righe di 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

Quindi c'è un modo per aggirare questo problema, oltre a controllare la lunghezza di un array in un if(vedere il codice di esempio di seguito) o disattivare l' -uimpostazione per quel pezzo breve?

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

Aggiornamento:bugs tag rimosso a causa della spiegazione di ikegami.

Risposte:


17

L' unico idioma sicuro è${arr[@]+"${arr[@]}"}

Questa è già la raccomandazione nella risposta di ikegami , ma in questo thread c'è molta disinformazione e congetture. Altri modelli, come ${arr[@]-}o ${arr[@]:0}, non sono sicuri in tutte le principali versioni di Bash.

Come mostra la tabella seguente, l'unica espansione affidabile in tutte le versioni di Bash moderne è ${arr[@]+"${arr[@]}"}(colonna +"). Da notare, molte altre espansioni falliscono in Bash 4.2, incluso (sfortunatamente) l' ${arr[@]:0}idioma più breve , che non solo produce un risultato errato ma in realtà fallisce. Se è necessario supportare versioni precedenti alla 4.4, e in particolare alla 4.2, questo è l'unico idioma funzionante.

Screenshot di diversi idiomi tra le versioni

Purtroppo altre +espansioni che, a prima vista, sembrano uguali, emettono effettivamente un comportamento diverso. :+L'espansione non è sicura, perché :-expansion tratta un array con un singolo elemento vuoto ( ('')) come "null" e quindi non si espande (in modo coerente) allo stesso risultato.

Citando l'espansione completa invece dell'array nidificato ( "${arr[@]+${arr[@]}}"), che mi sarei aspettato essere più o meno equivalente, è altrettanto pericoloso in 4.2.

Puoi vedere il codice che ha generato questi dati insieme ai risultati per diverse versioni aggiuntive di bash in questo gist .


1
Non ti vedo testare "${arr[@]}". Mi sto perdendo qualcosa? Da quello che posso vedere funziona almeno in 5.x.
x-yuri

1
@ x-yuri sì, Bash 4.4 ha risolto la situazione; non è necessario utilizzare questo modello se si sa che lo script verrà eseguito solo su 4.4+, ma molti sistemi sono ancora su versioni precedenti.
dimo414

Assolutamente. Nonostante siano belli (ad esempio la formattazione), gli spazi extra sono un grande male di bash, causando molti problemi
agg3l

81

Secondo la documentazione,

Una variabile array è considerata impostata se a un pedice è stato assegnato un valore. La stringa nulla è un valore valido.

A nessun pedice è stato assegnato un valore, quindi l'array non è impostato.

Ma mentre la documentazione suggerisce che un errore è appropriato qui, questo non è più il caso dalla 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: ''

C'è un condizionale che puoi usare inline per ottenere ciò che desideri nelle versioni precedenti: Usa ${arr[@]+"${arr[@]}"}invece di "${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

Testato con bash 4.2.25 e 4.3.11.


4
Qualcuno può spiegare come e perché funziona? Sono confuso su cosa [@]+fa effettivamente e perché il secondo ${arr[@]}non causerà un errore non associato.
Martin von Wittich

2
${parameter+word}si espande solo wordse parameternon è disattivato.
ikegami

2
${arr+"${arr[@]}"}è più corto e sembra funzionare altrettanto bene.
Secondo Cederberg

3
@Per Cerderberg, non funziona. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
Ikegami

1
Per essere precisi, nei casi in cui l' +espansione non si verifica (ovvero, un array vuoto) l'espansione viene sostituita con nulla , che è esattamente ciò in cui si espande un array vuoto. :+non è sicuro perché tratta anche un ('')array a elemento singolo come non impostato e allo stesso modo si espande a zero, perdendo il valore.
dimo414

23

La risposta accettata di @ ikegami è sottilmente sbagliata! L'incantesimo corretto è ${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...

Non fa più differenza. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"produce 1. Ma il ${arr[@]+"${arr[@]}"}modulo consente di distinguere tra valori vuoti / non vuoti aggiungendo / non aggiungendo due punti.
x-yuri

arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
x-yuri

1
Questo è stato risolto nella mia risposta molto tempo fa. (In effetti, sono sicuro di aver precedentemente lasciato un commento su questa risposta in tal senso ?!)
ikegami

16

Si scopre che la gestione degli array è stata cambiata nella bash 4.4 rilasciata di recente (2016/09/16) (disponibile in Debian stretch, per esempio).

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

Ora l'espansione di array vuoti non emette alcun avviso

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

$ # everything is fine

Posso confermare, con bash-4.4.12 "${arr[@]}"sarebbe sufficiente.
x-yuri

14

questa potrebbe essere un'altra opzione per coloro che preferiscono non duplicare arr [@] e possono avere una stringa vuota

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

testare:

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
Funzionerà se stai solo interpolando la variabile, ma se vuoi usare l'array in un forquesto finirebbe con una singola stringa vuota quando l'array è indefinito / definito come vuoto, dove come potresti volere il corpo del ciclo da non eseguire se l'array non è definito.
Ash Berlin-Taylor

grazie @AshBerlin, ho aggiunto un ciclo for alla mia risposta in modo che i lettori siano consapevoli
Jayen

-1 a questo approccio, è semplicemente errato. Questo sostituisce un array vuoto con una singola stringa vuota, che non è la stessa. Il modello suggerito nella risposta accettata ${arr[@]+"${arr[@]}"},, preserva correttamente lo stato di matrice vuota.
dimo414

Vedi anche la mia risposta che mostra le situazioni in cui questa espansione si interrompe.
dimo414

non è sbagliato. dice esplicitamente che darà una stringa vuota, e ci sono anche due esempi in cui puoi vedere la stringa vuota.
Jayen

7

La risposta di @ ikegami è corretta, ma considero la sintassi ${arr[@]+"${arr[@]}"}terribile. Se usi nomi di variabili di array lunghi, inizia a sembrare spaghetti più velocemente del solito.

Prova questo invece:

$ 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

Sembra che l'operatore slice dell'array Bash sia molto indulgente.

Allora perché Bash ha reso così difficile la gestione del caso limite degli array? Sospiro. Non posso garantire che la vostra versione consentirà tale abuso dell'operatore slice di array, ma per me funziona benissimo.

Avvertenza: sto usando il GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) tuo chilometraggio può variare.


9
ikegami originariamente lo aveva, ma lo ha rimosso perché non è affidabile, sia in teoria (non c'è motivo per cui dovrebbe funzionare) che in pratica (la versione OP di bash non lo accettava).

@hvd: grazie per l'aggiornamento. Lettori: si prega di aggiungere un commento se si trovano versioni di bash in cui il codice sopra non funziona.
kevinarpe

hvp lo ha già fatto, e lo dirò anche a te: "${arr[@]:0}"-bash: arr[@]: unbound variable.
ikegami

Una cosa che dovrebbe funzionare in tutte le versioni è impostare un valore di array predefinito su arr=("_dummy_")e utilizzare l'espansione ${arr[@]:1}ovunque. Questo è menzionato in altre risposte, riferendosi ai valori sentinella.
init_js

1
@init_js: la tua modifica è stata purtroppo rifiutata. Ti suggerisco di aggiungere come risposta separata. (Ref: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe

6

Incoerenza "interessante" davvero.

Inoltre,

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

Anche se sono d'accordo sul fatto che il comportamento corrente potrebbe non essere un bug nel senso che spiega @ikegami, IMO potremmo dire che il bug è nella definizione (di "set") stessa e / o nel fatto che è applicato in modo incoerente. Il paragrafo precedente nella pagina man dice

... ${name[@]}espande ogni elemento del nome in una parola separata. Quando non ci sono membri dell'array, si ${name[@]}espande in nulla.

che è del tutto coerente con ciò che dice sull'espansione dei parametri posizionali in "$@". Non che non ci siano altre incongruenze nei comportamenti degli array e dei parametri posizionali ... ma per me non c'è alcun accenno che questo dettaglio debba essere incoerente tra i due.

Continuando,

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

Quindi arr[]non è così svincolato da non poter ottenere un conteggio dei suoi elementi (0) o un elenco (vuoto) delle sue chiavi? Per me questi sono sensati e utili: l'unico valore anomalo sembra essere l' espansione ${arr[@]}(e ${arr[*]}).


2

Sto completando le risposte di @ ikegami (accettate) e di @ kevinarpe (anche buone).

Puoi fare "${arr[@]:+${arr[@]}}"per risolvere il problema. Il lato destro (cioè, dopo :+) fornisce un'espressione che verrà utilizzata nel caso in cui il lato sinistro non sia definito / nullo.

La sintassi è arcana. Si noti che il lato destro dell'espressione subirà l'espansione dei parametri, quindi è necessario prestare particolare attenzione ad avere citazioni coerenti.

: 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"]

Come cita @kevinarpe, una sintassi meno arcana consiste nell'usare la notazione di slice dell'array ${arr[@]:0}(sulle versioni Bash >= 4.4), che si espande a tutti i parametri, a partire dall'indice 0. Inoltre, non richiede tante ripetizioni. Questa espansione funziona indipendentemente set -u, quindi puoi usarla in ogni momento. La pagina man dice (sotto Parameter Expansion ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Se il parametro è un nome di array indicizzato sottoscritto da @o *, il risultato sono i membri di lunghezza dell'array che iniziano con ${parameter[offset]}. Viene considerato un offset negativo relativo a uno maggiore dell'indice massimo della matrice specificata. È un errore di espansione se la lunghezza restituisce un numero inferiore a zero.

Questo è l'esempio fornito da @kevinarpe, con formattazione alternativa per mettere in evidenza l'output:

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"

Questo comportamento varia con le versioni di Bash. Potresti anche aver notato che l'operatore di lunghezza ${#arr[@]}valuterà sempre 0per array vuoti, a prescindere set -u, senza causare un "errore di variabile non associata".


Sfortunatamente l' :0idioma fallisce in Bash 4.2, quindi questo non è un approccio sicuro. Vedi la mia risposta .
dimo414

1

Ecco un paio di modi per fare qualcosa di simile, uno usando le sentinelle e un altro usando le aggiunte condizionali:

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

Incoerenza interessante; questo ti permette di definire qualcosa che è "non considerato impostato" ma che appare nell'output dideclare -p

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

AGGIORNAMENTO: come altri menzionati, corretto nella versione 4.4 rilasciata dopo che questa risposta è stata pubblicata.


Questa è solo una sintassi dell'array errata; necessario echo ${arr[@]}(ma prima di Bash 4.4 vedrai ancora un errore).
dimo414

Grazie @ dimo414, la prossima volta suggerisci una modifica invece di downvoting. A proposito, se avessi provato echo $arr[@]tu stesso avresti visto che il messaggio di errore è diverso.
MarcH

-2

Il modo più semplice e compatibile sembra essere:

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

1
Gli stessi OP hanno dimostrato che questo non funziona. Si espande in una stringa vuota invece di niente.
ikegami

Bene, quindi va bene per l'interpolazione di stringhe ma non per il loop.
Craig Ringer
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.