Expansion de tableau vide Bash avec `set -u`

103

J'écris un script bash qui a set -u, et j'ai un problème avec l'expansion de tableau vide: bash semble traiter un tableau vide comme une variable non définie pendant l'expansion:

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

( declare -a arrn'aide pas non plus.)

Une solution courante à cela est d'utiliser à la ${arr[@]-}place, substituant ainsi une chaîne vide au lieu du tableau vide ("indéfini"). Cependant, ce n'est pas une bonne solution, car maintenant vous ne pouvez pas discerner entre un tableau contenant une seule chaîne vide et un tableau vide. (@ -expansion est spécial en bash, il se dilate "${arr[@]}"dans "${arr[0]}" "${arr[1]}" …, ce qui en fait un outil idéal pour la construction de lignes de commande.)

$ 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

Existe-t-il donc un moyen de contourner ce problème, autre que de vérifier la longueur d'un tableau dans un if(voir l'exemple de code ci-dessous), ou de désactiver le -uparamètre pour ce petit morceau?

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

Mise à jour:bugs balise supprimée en raison de l'explication d'ikegami.

Ivan Tarasov
la source

Réponses:

17

Le seul idiome sûr est${arr[@]+"${arr[@]}"}

C'est déjà la recommandation de la réponse d'ikegami , mais il y a beaucoup de désinformation et de conjectures dans ce fil. D'autres modèles, tels que ${arr[@]-}ou ${arr[@]:0}, ne sont pas sûrs dans toutes les versions majeures de Bash.

Comme le montre le tableau ci-dessous, la seule extension fiable dans toutes les versions modernes de Bash est ${arr[@]+"${arr[@]}"}(colonne +"). Il est à noter que plusieurs autres extensions échouent dans Bash 4.2, y compris (malheureusement) l' ${arr[@]:0}idiome plus court , qui ne produit pas seulement un résultat incorrect mais échoue en fait. Si vous avez besoin de prendre en charge les versions antérieures à 4.4, et en particulier 4.2, c'est le seul idiome qui fonctionne.

Capture d'écran de différents idiomes à travers les versions

Malheureusement, d'autres +extensions qui, en un coup d'œil, se ressemblent, émettent en effet un comportement différent. :+l'expansion n'est pas sûre, car :-expansion traite un tableau avec un seul élément vide ( ('')) comme "nul" et ne se développe donc pas (systématiquement) vers le même résultat.

Citant l'expansion complète au lieu du tableau imbriqué ( "${arr[@]+${arr[@]}}"), que j'aurais pensé être à peu près équivalent, est également dangereux dans 4.2.

Vous pouvez voir le code qui a généré ces données ainsi que les résultats de plusieurs versions supplémentaires de bash dans cet article .

dimo414
la source
1
Je ne vous vois pas tester "${arr[@]}". Est-ce que je manque quelque chose? D'après ce que je peux voir, cela fonctionne au moins 5.x.
x-yuri
1
@ x-yuri oui, Bash 4.4 a corrigé la situation; vous n'avez pas besoin d'utiliser ce modèle si vous savez que votre script ne fonctionnera que sur 4.4+, mais de nombreux systèmes sont encore sur des versions antérieures.
dimo414
Absolument. Malgré leur apparence agréable (par exemple, le formatage), les espaces supplémentaires sont un grand mal de bash, causant beaucoup de problèmes
agg3l
81

Selon la documentation,

Une variable de tableau est considérée comme définie si une valeur a été attribuée à un indice. La chaîne nulle est une valeur valide.

Aucun indice n'a reçu de valeur, le tableau n'est donc pas défini.

Mais alors que la documentation suggère qu'une erreur est appropriée ici, ce n'est plus le cas depuis 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: ''

Il existe une condition que vous pouvez utiliser en ligne pour obtenir ce que vous voulez dans les anciennes versions: utilisez à la ${arr[@]+"${arr[@]}"}place 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

Testé avec bash 4.2.25 et 4.3.11.

ikegami
la source
4
Quelqu'un peut-il expliquer comment et pourquoi cela fonctionne? Je ne sais pas ce qui [@]+fait réellement et pourquoi la seconde ${arr[@]}ne causera pas d'erreur non liée.
Martin von Wittich
2
${parameter+word}ne se développe que wordsi elle parametern'est pas désactivée.
ikegami
2
${arr+"${arr[@]}"}est plus court et semble fonctionner aussi bien.
Per Cederberg le
3
@Per Cerderberg, ça ne marche pas. unset arr, arr[1]=a, args ${arr+"${arr[@]}"}Contreargs ${arr[@]+"${arr[@]}"}
Ikegami
1
Pour être précis, dans les cas où l' +expansion ne se produit pas (à savoir, un tableau vide), l'expansion est remplacée par rien , ce qui est exactement ce à quoi un tableau vide se développe. :+est dangereux car il traite également un ('')tableau à un seul élément comme non défini et se développe de la même manière en rien, perdant la valeur.
dimo414
23

La réponse acceptée de @ ikegami est subtilement fausse! L'incantation correcte est ${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
la source
Cela ne fait plus de différence. bash-4.4.23: arr=('') && countArgs "${arr[@]:+${arr[@]}}"produit 1. Mais la ${arr[@]+"${arr[@]}"}forme permet de différencier les valeurs vides / non vides en ajoutant / ne ajoutant pas de deux-points.
x-yuri
arr=('') && countArgs ${arr[@]:+"${arr[@]}"}-> 0, arr=('') && countArgs ${arr[@]+"${arr[@]}"}-> 1.
x-yuri
1
Cela a été corrigé dans ma réponse il y a longtemps. (En fait, je suis sûr que j'ai déjà laissé un commentaire sur cette réponse à cet effet?!)
ikegami
16

Il s'avère que la gestion des tableaux a été modifiée dans la dernière version (2016/09/16) de bash 4.4 (disponible dans Debian Stretch, par exemple).

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

L'expansion des tableaux vides n'émet plus d'avertissement

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

$ # everything is fine
agg3l
la source
Je peux confirmer, bash-4.4.12 "${arr[@]}"cela suffirait.
x-yuri
14

cela peut être une autre option pour ceux qui préfèrent ne pas dupliquer arr [@] et sont d'accord pour avoir une chaîne vide

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

tester:

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
la source
10
Cela fonctionnera si vous interpolez simplement la variable, mais si vous voulez utiliser le tableau dans un, forcela se terminerait par une seule chaîne vide lorsque le tableau est indéfini / défini comme vide, où vous voudrez peut-être le corps de la boucle pour ne pas s'exécuter si le tableau n'est pas défini.
Ash Berlin-Taylor
merci @AshBerlin, j'ai ajouté une boucle for à ma réponse pour que les lecteurs le sachent
Jayen
-1 à cette approche, c'est tout simplement incorrect. Cela remplace un tableau vide par une seule chaîne vide, qui n'est pas la même chose. Le modèle suggéré dans la réponse acceptée ${arr[@]+"${arr[@]}"},, préserve correctement l'état du tableau vide.
dimo414
Voir aussi ma réponse montrant les situations dans lesquelles cette expansion échoue.
dimo414
ce n'est pas incorrect. il dit explicitement qu'il donnera une chaîne vide, et il y a même deux exemples où vous pouvez voir la chaîne vide.
Jayen
7

La réponse de @ ikegami est correcte, mais je considère que la syntaxe est ${arr[@]+"${arr[@]}"}épouvantable. Si vous utilisez de longs noms de variables de tableau, cela commence à sembler spaghetti plus rapidement que d'habitude.

Essayez plutôt ceci:

$ 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

Il semble que l'opérateur de tranche de tableau Bash soit très indulgent.

Alors pourquoi Bash a-t-il rendu la gestion du cas de bord des tableaux si difficile? Soupir. Je ne peux pas garantir que votre version autorisera un tel abus de l'opérateur de tranche de tableau, mais cela fonctionne parfaitement pour moi.

Attention: j'utilise GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) votre kilométrage peut varier.

kevinarpe
la source
9
ikegami avait à l'origine cela, mais l'a supprimé car il n'est pas fiable, à la fois en théorie (il n'y a aucune raison pour que cela fonctionne) et en pratique (la version OP de bash ne l'a pas accepté).
@hvd: Merci pour la mise à jour. Lecteurs: veuillez ajouter un commentaire si vous trouvez des versions de bash où le code ci-dessus ne fonctionne pas.
kevinarpe
hvp l'a déjà fait, et je vous le dirai aussi: "${arr[@]:0}"donne -bash: arr[@]: unbound variable.
ikegami
Une chose qui devrait fonctionner entre les versions est de définir une valeur de tableau par défaut arr=("_dummy_")et d'utiliser l'extension ${arr[@]:1}partout. Ceci est mentionné dans d'autres réponses, faisant référence aux valeurs sentinelles.
init_js
1
@init_js: Votre modification a malheureusement été rejetée. Je vous suggère d'ajouter une réponse distincte. (Réf: stackoverflow.com/review/suggested-edits/19027379 )
kevinarpe
6

Incohérence "intéressante" en effet.

En outre,

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

Bien que je sois d'accord pour dire que le comportement actuel n'est peut-être pas un bogue au sens où l'explique @ikegami, IMO nous pourrions dire que le bogue est dans la définition (de "set") elle-même, et / ou dans le fait qu'il est appliqué de manière incohérente. Le paragraphe précédent de la page de manuel dit

... ${name[@]}étend chaque élément du nom en un mot distinct. Lorsqu'il n'y a pas de membres du tableau, se ${name[@]}développe en rien.

ce qui est tout à fait cohérent avec ce qu'il dit sur l'expansion des paramètres de position dans "$@". Non pas qu'il n'y ait pas d'autres incohérences dans les comportements des tableaux et des paramètres de position ... mais pour moi, rien n'indique que ce détail devrait être incohérent entre les deux.

Continuant,

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

Alors arr[]n'est-il pas si indépendant que nous ne pouvons pas obtenir un nombre de ses éléments (0), ou une liste (vide) de ses clés? Pour moi, ils sont sensés et utiles - la seule valeur aberrante semble être l' expansion ${arr[@]}(et ${arr[*]}).

don311
la source
2

Je complète les réponses de @ ikegami (acceptées) et de @ kevinarpe (également bonnes).

Vous pouvez faire "${arr[@]:+${arr[@]}}"pour contourner le problème. Le côté droit (c'est-à-dire après :+) fournit une expression qui sera utilisée au cas où le côté gauche n'est pas défini / nul.

La syntaxe est obscure. Notez que le côté droit de l'expression subira une expansion des paramètres, donc une attention particulière doit être accordée à la cohérence des guillemets.

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

Comme le mentionne @kevinarpe, une syntaxe moins arcanique consiste à utiliser la notation de tranche de tableau ${arr[@]:0}(sur les versions Bash >= 4.4), qui s'étend à tous les paramètres, à partir de l'index 0. Elle ne nécessite pas non plus autant de répétition. Cette extension fonctionne indépendamment de set -u, vous pouvez donc l'utiliser à tout moment. La page de manuel indique (sous Extension des paramètres ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... Si le paramètre est un nom de tableau indexé indiqué par @ou *, le résultat est la longueur des membres du tableau commençant par ${parameter[offset]}. Un décalage négatif est pris par rapport à un supérieur à l'index maximum du tableau spécifié. Il s'agit d'une erreur d'expansion si la longueur est évaluée à un nombre inférieur à zéro.

Voici l'exemple fourni par @kevinarpe, avec une mise en forme alternative pour mettre la sortie en évidence:

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"

Ce comportement varie selon les versions de Bash. Vous avez peut-être également remarqué que l'opérateur de longueur ${#arr[@]}évaluera toujours à 0pour les tableaux vides, indépendamment de set -u, sans provoquer une 'erreur de variable non liée'.

init_js
la source
Malheureusement, l' :0idiome échoue dans Bash 4.2, ce n'est donc pas une approche sûre. Voyez ma réponse .
dimo414
1

Voici quelques façons de faire quelque chose comme ça, une en utilisant des sentinelles et une autre en utilisant des appendices conditionnels:

#!/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[@]}"
solide
la source
0

Incohérence intéressante; cela vous permet de définir quelque chose qui n'est "pas considéré comme défini" mais qui apparaît dans la sortie dedeclare -p

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

MISE À JOUR: comme d'autres l'ont mentionné, corrigé dans la version 4.4 publiée après la publication de cette réponse.

Mars
la source
C'est juste une syntaxe de tableau incorrecte; dont vous avez besoin echo ${arr[@]}(mais avant Bash 4.4, vous verrez toujours une erreur).
dimo414
Merci @ dimo414, la prochaine fois, suggérez une modification au lieu de voter contre. BTW si vous aviez essayé echo $arr[@]vous-même, vous auriez vu que le message d'erreur est différent.
MarcH
-2

Le moyen le plus simple et le plus compatible semble être:

$ set -u
$ arr=()
$ echo "foo: '${arr[@]-}'"
Nikolay
la source
1
Le PO lui-même a montré que cela ne fonctionne pas. Il se développe en une chaîne vide au lieu de rien.
ikegami
Bien, donc c'est OK pour l'interpolation de chaîne mais pas pour la boucle.
Craig Ringer