Bash leere Array-Erweiterung mit `set -u`

103

Ich schreibe ein Bash-Skript, das hat set -u, und ich habe ein Problem mit der Erweiterung des leeren Arrays: Bash scheint ein leeres Array während der Erweiterung als nicht gesetzte Variable zu behandeln:

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

( declare -a arrhilft auch nicht.)

Eine übliche Lösung hierfür besteht darin, ${arr[@]-}stattdessen eine leere Zeichenfolge anstelle des ("undefinierten") leeren Arrays zu verwenden. Dies ist jedoch keine gute Lösung, da Sie jetzt nicht zwischen einem Array mit einer einzelnen leeren Zeichenfolge und einem leeren Array unterscheiden können. (@ -expansion ist etwas Besonderes in Bash, es erweitert sich "${arr[@]}"in "${arr[0]}" "${arr[1]}" …, was es zu einem perfekten Werkzeug zum Erstellen von Befehlszeilen macht.)

$ 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

Gibt es also einen Weg, um dieses Problem zu umgehen, außer die Länge eines Arrays in einem zu überprüfen if(siehe Codebeispiel unten) oder die -uEinstellung für dieses kurze Stück zu deaktivieren?

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

Update: Entfernt - bugsTag aufgrund Erklärung von Ikegami.

Ivan Tarasov
quelle

Antworten:

16

Die einzig sichere Redewendung ist${arr[@]+"${arr[@]}"}

Dies ist bereits die Empfehlung in Ikegamis Antwort , aber in diesem Thread gibt es viele Fehlinformationen und Vermutungen. Andere Muster wie ${arr[@]-}oder ${arr[@]:0}sind nicht in allen Hauptversionen von Bash sicher.

Wie die folgende Tabelle zeigt, ist ${arr[@]+"${arr[@]}"}(Spalte +") die einzige Erweiterung, die für alle modernen Bash-Versionen zuverlässig ist . Bemerkenswert ist, dass einige andere Erweiterungen in Bash 4.2 fehlschlagen, einschließlich (leider) der kürzeren Sprache ${arr[@]:0}, die nicht nur ein falsches Ergebnis liefert, sondern tatsächlich fehlschlägt. Wenn Sie Versionen vor 4.4 und insbesondere 4.2 unterstützen müssen, ist dies die einzige funktionierende Sprache.

Screenshot verschiedener Redewendungen in verschiedenen Versionen

Leider zeigen andere +Erweiterungen, die auf den ersten Blick gleich aussehen, tatsächlich ein anderes Verhalten. :+Die Erweiterung ist nicht sicher, da :-expansion ein Array mit einem einzelnen leeren Element ( ('')) als "null" behandelt und daher nicht (konsistent) auf dasselbe Ergebnis erweitert.

Das Zitieren der vollständigen Erweiterung anstelle des verschachtelten Arrays ( "${arr[@]+${arr[@]}}"), von dem ich erwartet hätte, dass es ungefähr gleichwertig ist, ist in 4.2 ähnlich unsicher.

Sie können den Code, der diese Daten generiert hat, zusammen mit den Ergebnissen für mehrere zusätzliche Versionen von Bash in dieser Übersicht sehen .

dimo414
quelle
1
Ich sehe dich nicht testen "${arr[@]}". Vermisse ich etwas Soweit ich sehen kann, funktioniert es zumindest in 5.x.
x-yuri
1
@ x-yuri ja, Bash 4.4 hat die Situation behoben; Sie müssen dieses Muster nicht verwenden, wenn Sie wissen, dass Ihr Skript nur unter 4.4+ ausgeführt wird, aber viele Systeme noch frühere Versionen haben.
dimo414
Absolut. Obwohl sie gut aussehen (z. B. Formatierung), sind zusätzliche Leerzeichen ein großes Übel und verursachen viele Probleme
agg3l
81

Laut Dokumentation,

Eine Array-Variable gilt als gesetzt, wenn einem Index ein Wert zugewiesen wurde. Die Nullzeichenfolge ist ein gültiger Wert.

Es wurde keinem Index ein Wert zugewiesen, daher ist das Array nicht festgelegt.

Obwohl die Dokumentation darauf hinweist, dass hier ein Fehler angebracht ist, ist dies seit 4.4 nicht mehr der Fall .

$ 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: ''

Es gibt eine Bedingung, die Sie inline verwenden können, um das zu erreichen, was Sie in älteren Versionen wollen: Verwenden Sie ${arr[@]+"${arr[@]}"}anstelle von "${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

Getestet mit Bash 4.2.25 und 4.3.11.

Ikegami
quelle
4
Kann jemand erklären, wie und warum das funktioniert? Ich bin verwirrt darüber, was [@]+tatsächlich funktioniert und warum der zweite ${arr[@]}keinen ungebundenen Fehler verursacht.
Martin von Wittich
2
${parameter+word}Erweitert sich nur, wordwenn parameteres nicht deaktiviert ist.
Ikegami
2
${arr+"${arr[@]}"}ist kürzer und scheint genauso gut zu funktionieren.
Per Cederberg
3
@Per Cerderberg, funktioniert nicht. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Vsargs ${arr[@]+"${arr[@]}"}
ikegami
1
Um genau zu sein, wird in Fällen, in denen die +Erweiterung nicht auftritt (nämlich ein leeres Array), die Erweiterung durch nichts ersetzt , was genau das ist, worauf ein leeres Array erweitert wird. :+ist unsicher, da es auch ein Einzelelement- ('')Array als nicht gesetzt behandelt und sich in ähnlicher Weise zu nichts erweitert, wodurch der Wert verloren geht.
dimo414
23

@ ikegamis akzeptierte Antwort ist auf subtile Weise falsch! Die richtige Beschwörung ist ${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
quelle
Macht keinen Unterschied mehr. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"produziert 1. Mit ${arr[@]+"${arr[@]}"}form können Sie jedoch zwischen leeren und nicht leeren Werten unterscheiden, indem Sie einen Doppelpunkt hinzufügen oder nicht hinzufügen.
X-Yuri
arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
X-Yuri
1
Dies wurde in meiner Antwort vor langer Zeit behoben. (Tatsächlich bin ich sicher, dass ich zuvor einen Kommentar zu dieser Antwort hinterlassen habe
?!
15

Es stellte sich heraus, dass die Array-Behandlung in der kürzlich veröffentlichten Bash 4.4 (16.09.2016) geändert wurde (zum Beispiel in Debian Stretch verfügbar).

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

Jetzt gibt die Erweiterung für leere Arrays keine Warnung aus

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

$ # everything is fine
agg3l
quelle
Ich kann bestätigen, mit bash-4.4.12 "${arr[@]}"würde ausreichen.
X-Yuri
13

Dies kann eine weitere Option für diejenigen sein, die es vorziehen, arr [@] nicht zu duplizieren, und in Ordnung sind, eine leere Zeichenfolge zu haben

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

zu testen:

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
quelle
10
Dies funktioniert, wenn Sie nur die Variable interpolieren. Wenn Sie das Array jedoch in einem verwenden formöchten, wird dies zu einer einzelnen leeren Zeichenfolge führen, wenn das Array undefiniert / als leer definiert ist, wo Sie möglicherweise den Schleifenkörper möchten nicht ausgeführt werden, wenn das Array nicht definiert ist.
Ash Berlin-Taylor
danke @AshBerlin, ich habe meiner Antwort eine for-Schleife hinzugefügt, damit die Leser darauf aufmerksam werden
Jayen
-1 zu diesem Ansatz ist es einfach falsch. Dies ersetzt ein leeres Array durch eine einzelne leere Zeichenfolge, die nicht identisch ist. Das in der akzeptierten Antwort vorgeschlagene Muster behält den ${arr[@]+"${arr[@]}"}Status des leeren Arrays korrekt bei.
dimo414
Siehe auch meine Antwort, die die Situationen zeigt, in denen diese Erweiterung zusammenbricht.
dimo414
es ist nicht falsch. Es wird ausdrücklich angegeben, dass eine leere Zeichenfolge angegeben wird, und es gibt sogar zwei Beispiele, in denen Sie die leere Zeichenfolge sehen können.
Jayen
7

@ ikegamis Antwort ist richtig, aber ich halte die Syntax für ${arr[@]+"${arr[@]}"}schrecklich. Wenn Sie lange Array-Variablennamen verwenden, sieht es schneller als gewöhnlich spaghettiartig aus.

Versuchen Sie stattdessen Folgendes:

$ 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

Es sieht so aus, als ob der Bash-Array-Slice-Operator sehr nachsichtig ist.

Warum hat Bash die Behandlung des Randfalls von Arrays so schwierig gemacht? Seufzer. Ich kann nicht garantieren, dass Ihre Version einen solchen Missbrauch des Array-Slice-Operators zulässt, aber es funktioniert gut für mich.

Vorsichtsmaßnahme: Ich verwende GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) Ihre Laufleistung kann variieren.

Kevinarpe
quelle
9
ikegami hatte dies ursprünglich, entfernte es jedoch, weil es sowohl theoretisch (es gibt keinen Grund, warum dies funktionieren sollte) als auch in der Praxis (die OP-Version von bash hat es nicht akzeptiert) unzuverlässig ist.
@hvd: Danke für das Update. Leser: Bitte fügen Sie einen Kommentar hinzu, wenn Sie Versionen von bash finden, bei denen der obige Code nicht funktioniert.
Kevinarpe
hvp hat es schon getan, und ich sage dir auch: "${arr[@]:0}"gibt -bash: arr[@]: unbound variable.
Ikegami
Eine Version, die versionübergreifend funktionieren sollte, besteht darin, einen Standard-Array-Wert auf festzulegen arr=("_dummy_")und die Erweiterung ${arr[@]:1}überall zu verwenden. Dies wird in anderen Antworten erwähnt, die sich auf Sentinel-Werte beziehen.
init_js
1
@init_js: Ihre Bearbeitung wurde leider abgelehnt. Ich schlage vor, Sie als separate Antwort hinzufügen. (Ref: stackoverflow.com/review/suggested-edits/19027379 )
Kevinarpe
6

"Interessante" Inkonsistenz in der Tat.

Außerdem,

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

Obwohl ich damit einverstanden bin, dass das aktuelle Verhalten möglicherweise kein Fehler in dem Sinne ist, wie @ikegami es erklärt, können wir IMO sagen, dass der Fehler in der Definition (von "set") selbst liegt und / oder in der Tatsache, dass er inkonsistent angewendet wird. Der vorhergehende Absatz in der Manpage sagt

... ${name[@]}erweitert jedes Element des Namens zu einem eigenen Wort. Wenn keine Array-Mitglieder vorhanden sind, wird ${name[@]}auf nichts erweitert.

Das stimmt völlig mit dem überein, was es über die Erweiterung von Positionsparametern in sagt "$@" . Nicht, dass es keine anderen Inkonsistenzen im Verhalten von Arrays und Positionsparametern gibt ... aber für mich gibt es keinen Hinweis darauf, dass dieses Detail zwischen den beiden inkonsistent sein sollte.

Auch weiterhin,

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

Ist arr[]es also nicht so ungebunden, dass wir keine Zählung seiner Elemente (0) oder eine (leere) Liste seiner Schlüssel erhalten können? Für mich sind diese sinnvoll und nützlich - der einzige Ausreißer scheint die ${arr[@]}(und ${arr[*]}) Erweiterung zu sein.

don311
quelle
2

Ich ergänze @ ikegami (akzeptiert) und @ kevinarpe Antworten (auch gut).

Sie können "${arr[@]:+${arr[@]}}"das Problem umgehen. Die rechte Seite (dh danach:+ ) stellt einen Ausdruck bereit, der verwendet wird, falls die linke Seite nicht definiert / null ist.

Die Syntax ist arkan. Beachten Sie, dass die rechte Seite des Ausdrucks einer Parametererweiterung unterzogen wird. Daher sollte besonders auf ein konsistentes Anführungszeichen geachtet werden.

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

Wie bei @kevinarpe erwähnt, besteht eine weniger arkane Syntax darin, die Array-Slice-Notation ${arr[@]:0}(bei Bash-Versionen >= 4.4) zu verwenden, die ab Index 0 auf alle Parameter erweitert wird. Außerdem sind weniger Wiederholungen erforderlich. Diese Erweiterung funktioniert unabhängig davon set -u, sodass Sie sie jederzeit verwenden können. Die Manpage sagt (unter Parametererweiterung ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Wenn der Parameter ein indizierter Array-Name ist , der durch @oder tiefgestellt *ist, sind die Längenelemente des Arrays beginnend mit ${parameter[offset]}. Ein negativer Versatz wird relativ zu einem Versatz genommen, der größer als der maximale Index des angegebenen Arrays ist. Es ist ein Erweiterungsfehler, wenn die Länge eine Zahl kleiner als Null ergibt.

Dies ist das von @kevinarpe bereitgestellte Beispiel mit alternativer Formatierung, um die Ausgabe als Beweis zu platzieren:

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"

Dieses Verhalten variiert je nach Version von Bash. Möglicherweise haben Sie auch bemerkt, dass der Längenoperator unabhängig davon ${#arr[@]}immer nach 0leeren Arrays auswertet set -u, ohne einen "ungebundenen Variablenfehler" zu verursachen.

init_js
quelle
Leider :0schlägt die Redewendung in Bash 4.2 fehl, sodass dies kein sicherer Ansatz ist. Siehe meine Antwort .
dimo414
1

Hier sind einige Möglichkeiten, um so etwas zu tun: eine mit Sentinels und eine mit bedingten Anhängen:

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

Interessante Inkonsistenz; Auf diese Weise können Sie etwas definieren, das "nicht als gesetzt betrachtet" wird und dennoch in der Ausgabe von angezeigt wirddeclare -p

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

UPDATE: Wie bereits erwähnt, wurde in 4.4 behoben, nachdem diese Antwort veröffentlicht wurde.

März
quelle
Das ist nur eine falsche Array-Syntax. Sie benötigen echo ${arr[@]}(aber vor Bash 4.4 wird immer noch ein Fehler angezeigt).
dimo414
Danke @ dimo414, schlagen Sie das nächste Mal eine Bearbeitung vor, anstatt eine Abstimmung vorzunehmen. Übrigens, wenn Sie es selbst versucht echo $arr[@]hätten, hätten Sie gesehen, dass die Fehlermeldung anders ist.
MarcH
-2

Der einfachste und kompatibelste Weg scheint zu sein:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"
Nikolay
quelle
1
Das OP selbst hat gezeigt, dass dies nicht funktioniert. Es wird zu einer leeren Zeichenfolge anstatt zu nichts erweitert.
Ikegami
Richtig, es ist also in Ordnung für die String-Interpolation, aber nicht für die Schleife.
Craig Ringer