Impression de tableaux associatifs BASH

17

Existe-t-il un moyen d'imprimer un tableau entier ([clé] = valeur) sans boucler sur tous les éléments?

Supposons que j'ai créé un tableau avec quelques éléments:

declare -A array
array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)

Je peux imprimer la totalité du tableau avec

for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done

Cependant, il semble que bash sache déjà comment obtenir tous les éléments du tableau en une seule fois - à la fois les clés ${!array[@]}et les valeurs ${array[@]}.

Existe-t-il un moyen de faire bash imprimer ces informations sans la boucle?

Edit:
typeset -p arrayfait ça!
Cependant, je ne peux pas supprimer le préfixe et le suffixe en une seule substitution:

a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"

Existe-t-il un moyen plus propre d'obtenir / imprimer uniquement la partie clé = valeur de la sortie?

Dani_l
la source

Réponses:

15

Je pense que vous demandez deux choses différentes.

Existe-t-il un moyen de faire bash imprimer ces informations sans la boucle?

Oui, mais ils ne sont pas aussi efficaces que l'utilisation de la boucle.

Existe-t-il un moyen plus propre d'obtenir / imprimer uniquement la partie clé = valeur de la sortie?

Oui, la forboucle. Il a l'avantage de ne pas nécessiter de programmes externes, est simple et facilite le contrôle du format de sortie exact sans surprise.


Toute solution qui essaie de gérer la sortie de declare -p( typeset -p) doit faire face à a) la possibilité que les variables elles-mêmes contiennent des parenthèses ou des crochets, b) la citation qui declare -pdoit être ajoutée pour que sa sortie soit valide pour le shell.

Par exemple, votre expansion b="${a##*(}"mange certaines des valeurs, si une clé / valeur contient une parenthèse ouvrante. En effet, vous avez utilisé ##, ce qui supprime le préfixe le plus long . Pareil pour c="${b%% )*}". Bien que vous puissiez bien sûr faire correspondre declareplus exactement le passe-partout imprimé , vous auriez encore du mal si vous ne vouliez pas tous les devis.

Cela n'a pas l'air très agréable sauf si vous en avez besoin.

$ declare -A array=([abc]="'foobar'" [def]='"foo bar"')
$ declare -p array
declare -A array='([def]="\"foo bar\"" [abc]="'\''foobar'\''" )'

Avec la forboucle, il est plus facile de choisir le format de sortie comme vous le souhaitez:

# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'

# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'

À partir de là, il est également simple de changer le format de sortie sinon (supprimez les crochets autour de la clé, mettez toutes les paires clé / valeur sur une seule ligne ...). Si vous avez besoin de citer autre chose que le shell lui-même, vous devrez toujours le faire vous-même, mais au moins vous avez les données brutes sur lesquelles travailler. (Si vous avez des sauts de ligne dans les clés ou les valeurs, vous aurez probablement besoin de quelques citations.)

Avec un Bash actuel (4.4, je pense), vous pouvez également utiliser à la printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"place de printf "%q=%q". Il produit un format cité un peu plus agréable, mais est bien sûr un peu plus de travail à retenir pour écrire. (Et il cite le cas du coin de la @clé de tableau, qui %qne cite pas.)

Si la boucle for semble trop fatiguée pour être écrite, enregistrez-la quelque part (sans citer ici):

printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%s\n" "$k" "${__p[$k]}" ; done ;  }  

Et puis il suffit de l'utiliser:

$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)")
$ printarr a
a=123
b=foo bar
c=(blah)

Fonctionne également avec les tableaux indexés:

$ b=(abba acdc)
$ printarr b
0=abba
1=acdc
ilkkachu
la source
Notez que la sortie de votre printf ...%q...variante ne convient pas pour une réintroduction dans le shell si le tableau a une @clé car% q ne la cite pas et a=([@]=value)est une erreur de syntaxe dans bash.
Stéphane Chazelas
@ StéphaneChazelas, apparemment. "${x@Q}"cite cela aussi, car il cite toutes les chaînes (et semble plus joli). ajouté une note sur l'utilisation de cela.
ilkkachu
Oui, copié de mksh. Un autre opérateur d'une forme encore différente qui ne peut pas être combinée avec la plupart des autres. Encore une fois, voyez zshavec ses indicateurs d'expansion variables (qui sont encore antérieurs aux bash par décennies et avec lesquels vous pouvez choisir le style de citation: $ {(q) var}, $ {(qq) var} ...) pour une meilleure conception. bash a le même problème que mksh en ce qu'il ne cite pas la chaîne vide (pas un problème ici car de toute façon bash ne prend pas en charge les clés vides). De plus, lorsque vous utilisez des styles de guillemets autres que des guillemets simples ( ${var@Q}recourt à $'...'pour certaines valeurs), il est important que le code soit réintroduit dans les mêmes paramètres régionaux.
Stéphane Chazelas
@ StéphaneChazelas, je pense que tu veux dire une valeur non définie, pas une chaîne vide? ( x=; echo "${x@Q}"ne donne '', unset x; echo "${x@Q}"ne donne rien.) Bash @Qsemble préférer $'\n'une nouvelle ligne littérale, qui peut en fait être bonne dans certaines situations (mais je ne peux pas dire ce que d'autres préfèrent). Bien sûr, avoir le choix ne serait pas mauvais.
ilkkachu
Oh oui désolé, je ne l'avais pas réalisé. C'est donc une différence avec mksh. La $'...'syntaxe est un problème potentiel dans des choses comme les LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'sorties $'\n<0xa3><0x5c>'et 0x5cseule la barre oblique inverse, vous auriez donc un problème si cette citation était interprétée dans un environnement local différent.
Stéphane Chazelas
9
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'

2 fourchettes

Peut être ça:

printf "%s\n" "${!array[@]}"
a2
a1
f50
zz
b1

printf "%s\n" "${array[@]}"
2
1
abcd
Hello World
bbb

printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t
a2                              2
a1                              1
f50                             abcd
zz                              Hello World
b1                              bbb

3 fourchettes

ou ca:

paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Pas de fourchette

être comparé à

for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Comparaison des temps d'exécution

Comme la dernière syntaxe n'utilise pas de fork, elles pourraient être plus rapides:

time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
      5      11      76
real    0m0.005s
user    0m0.000s
sys     0m0.000s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
      5       6      41
real    0m0.008s
user    0m0.000s
sys     0m0.000s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
      5       6      41
real    0m0.002s
user    0m0.000s
sys     0m0.001s

Mais cette affirmation ne reste pas vraie si le tableau devient grand; si la réduction des fourches est efficace pour les petits processus, l'utilisation d'outils dédiés est plus efficace pour les processus plus importants.

for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done


time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
  17581   35163  292941
real    0m0.150s
user    0m0.124s
sys     0m0.036s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
  17581   17582  169875
real    0m0.140s
user    0m0.000s
sys     0m0.004s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
  17581   17582  169875
real    0m0.312s
user    0m0.268s
sys     0m0.076s

Remarque

Comme les deux solutions ( fourchues ) utilisent l' alignement , aucune d'entre elles ne fonctionnera si une variable contient une nouvelle ligne . Dans ce cas, le seul moyen est une forboucle.

F. Hauri
la source
Tout en ayant l'air intelligent, les deux méthodes sont moins efficaces que a for. Ce qui est vraiment dommage.
Satō Katsura
@SatoKatsura Je suis d'accord, mais si elle est plus lente, l'utilisation de la syntaxe prest plus courte ... Je ne suis pas sûr que la prsyntaxe reste plus lente, même avec de grands tableaux!
F.Hauri
2
@MiniMax Parce qu'il ne produit pas le bon résultat (mêmes éléments, mauvais ordre). Vous devez compresser les tableaux ${!array[@]}et d' ${array[@]}abord pour que cela fonctionne.
Satō Katsura
1
Ce dernier extrait avec pasteest plus long que la forboucle de la question écrite sur une ligne for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, mais nécessite deux sous-coquilles et un programme externe. Comment est-ce plus propre? La solution avec prcasse également s'il y a beaucoup d'éléments, car elle essaie de paginer la sortie. Vous auriez besoin d'utiliser quelque chose comme | pr -2t -l"${#array[@]}"qui commence à devenir difficile à retenir par rapport à la boucle simple, et encore une fois, c'est plus long que cela.
ilkkachu
1
Dans bash, cmd1 | cmd2signifie 2 fourches, même si cmd1 ou cmd2 ou les deux sont intégrés.
Stéphane Chazelas
2

Si vous recherchez un shell avec une meilleure prise en charge des tableaux associatifs, essayez zsh.

Dans zsh(où des tableaux associatifs ont été ajoutés en 1998, par rapport à 1993 pour ksh93 et ​​2009 pour bash), $varou se ${(v)var}développe aux valeurs (non vides) du hachage, ${(k)var}aux clés (non vides) (dans le même ordre), et ${(kv)var}aux clés et valeurs.

Pour conserver les valeurs vides, comme pour les tableaux, vous devez citer et utiliser l' @indicateur.

Donc, pour imprimer les clés et les valeurs, c'est juste une question de

printf '%s => %s\n' "${(@kv)var}"

Bien que pour tenir compte d'un hachage éventuellement vide, vous devez faire:

(($#var)) &&  printf '%s => %s\n' "${(@kv)var}"

Notez également que zsh utilise une syntaxe de définition de tableau beaucoup plus sensible et utile que celle ksh93(copiée par bash):

typeset -A var
var=(k1 v1 k2 v2 '' empty '*' star)

Ce qui facilite grandement la copie ou la fusion de tableaux associatifs:

var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")

(vous ne pouvez pas facilement copier un hachage sans boucle avec bash, et notez que bashactuellement ne prend pas en charge les clés vides ou les clés / valeurs avec des octets NUL).

Voir également les zshfonctionnalités de compression de tableau dont vous aurez généralement besoin pour travailler avec des tableaux associatifs:

keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})
Stéphane Chazelas
la source
1

Puisque la composition fait ce que vous voulez, pourquoi ne pas simplement éditer sa sortie?

typeset -p array | sed s/^.*\(// | tr -d ")\'\""  | tr "[" "\n" | sed s/]=/' = '/

donne

a2 = 2  
a1 = 1  
b1 = bbb 

array='([a2]="2" [a1]="1" [b1]="bbb" )'

Verbose mais il est assez facile de voir comment fonctionne le formatage: il suffit d'exécuter le pipeline avec progressivement plus de commandes sed et tr . Modifiez-les pour convenir à de jolis goûts d'impression.

Nadreck
la source
Ce type de pipeline est voué à l'échec au moment où certaines des clés ou valeurs du tableau contiennent l'un des caractères que vous remplacez, comme les parenthèses, les crochets ou les guillemets. Et un pipeline de seds et trn'est pas encore plus simple qu'unfor boucle avec printf.
ilkkachu
De plus, vous savez que trtraduire caractère par caractère, cela ne correspond pas aux chaînes? tr "]=" " ="change "]" en espace et =en en =, quelle que soit la position. Vous pourriez donc probablement combiner les trois trà un.
ilkkachu
Très vrai pour certains des caractères non alphanumériques qui gomment ça. Cependant, tout ce qui doit les traiter devient un ordre de grandeur plus complexe et moins lisible, à moins qu'il n'y ait une très bonne raison de les avoir dans votre flux de données et cela est indiqué dans la question, je suppose qu'ils sont filtrés avant notre arrivée. Devrait toujours avoir votre mise en garde explicite. Je trouve que ces pipelines sont plus simples, par exemple et à des fins de débogage, qu'un glob printf qui fonctionne parfaitement ou explose sur votre visage. Ici, vous effectuez un changement simple par élément, testez-le, puis ajoutez-en 1 de plus.
Nadreck
Ma faute! J'ai mes _tr_s et _sed_s totalement mélangés! Corrigé dans la dernière modification.
Nadreck
1

Une autre option consiste à répertorier toutes les variables et grep pour celle que vous souhaitez.

set | grep -e '^aa='

J'utilise cela pour le débogage. Je doute qu'il soit très performant puisqu'il répertorie toutes les variables.

Si vous faisiez cela souvent, vous pourriez en faire une fonction comme celle-ci:

aap() { set | grep -e "^$1="; }

Malheureusement, lorsque nous vérifions les performances en utilisant le temps:

$ time aap aa aa=([0]="abc") . real 0m0.014s user 0m0.003s sys 0m0.006s

Par conséquent, si vous faisiez cela très souvent, vous voudriez la version NO FORKS de @ F.Hauri car elle est tellement plus rapide.

xer0x
la source