Substitution double et triple en bash et zsh

23

Suivi de la partie historique de cette question .

En bashje peux utiliser ${!FOO}pour la double substitution, en zsh ${(P)FOO}. Dans les deux, la vieille école (hack-y) eval \$$FOOfonctionne.

Donc, la chose la plus intelligente et la plus logique pour moi serait la ${${FOO}}, ${${${FOO}}}…substitution double / triple / n. Pourquoi cela ne fonctionne-t-il pas comme prévu?

Deuxièmement: que fait \la evaldéclaration? Je pense que c'est une évasion, rendant quelque chose comme eval \$$$FOOimpossible. Comment faire une substitution triple / n avec celle qui fonctionne dans chaque shell?

Profpatsch
la source

Réponses:

15

Le \doit être utilisé pour empêcher l'expansion de $$(identifiant de processus actuel). Pour une triple substitution, vous avez besoin d'un double eval, donc aussi plus d'échappements pour éviter les extensions indésirables dans chaque eval:

#! /bin/bash
l0=value
l1=l0
l2=l1
l3=l2
l4=l3

echo $l0
eval echo \$$l1
eval eval echo \\$\$$l2
eval eval eval echo \\\\$\\$\$$l3
eval eval eval eval echo \\\\\\\\$\\\\$\\$\$$l4
choroba
la source
Pourtant: l3=l2; eval eval eval echo \\\$\\$\$$l353294donc pas exactement modulaire.
Profpatsch
2
Pourquoi ne puis-je pas faire quelque chose comme ça eval $(eval echo \$$l2)? Je déteste bash, ça n'a absolument aucun sens.
Profpatsch
@Profpatsch: Ajout d'autres exemples.
choroba
Ah, c'est n². Je ne veux même pas essayer de comprendre pourquoi c'est comme ça.
Profpatsch
2
@Profpatsch: Facile. À chaque étape, le nombre de barres obliques inverses est divisé par deux (il est donc en réalité de 2ⁿ).
choroba
10
#!/bin/bash

hello=world
echo=hello

echo $echo ${!echo}
Christopher Abad
la source
6

Supposons que la valeur de FOOest un nom de variable valide (par exemple BAR), eval \$$FOOdivise la valeur de BARen mots séparés, traite chaque mot comme un motif générique et exécute le premier mot du résultat comme une commande, en passant les autres mots comme arguments. La barre oblique inverse devant le dollar le fait être traité littéralement, donc l'argument passé à la commande evalintégrée est la chaîne de quatre caractères $BAR.

${${FOO}}est une erreur de syntaxe. Il ne fait pas de «double substitution» car il n'y a aucune telle fonctionnalité dans aucun des shells communs (pas avec cette syntaxe de toute façon). Dans zsh, ${${FOO}} est valide et est une double substitution, mais il se comporte différemment de ce que vous aimeriez: il effectue deux transformations successives sur la valeur de FOO, qui sont toutes les deux la transformation d'identité, donc c'est juste une façon d'écrire de fantaisie ${FOO}.

Si vous souhaitez traiter la valeur d'une variable comme une variable, veillez à bien citer les choses. C'est beaucoup plus facile si vous définissez le résultat sur une variable:

eval "value=\${$FOO}"
Gilles 'SO- arrête d'être méchant'
la source
1
La définir comme variable, pour que le premier mot ne soit pas utilisé comme commande? Man, ce genre de logique est difficile à saisir. C'est le problème général que je semble avoir avec bash.
Profpatsch
4

{ba,z}sh Solution

Voici une fonction qui fonctionne dans les deux {ba,z}sh. Je pense qu'il est également conforme à POSIX.

Sans devenir fou avec la citation, vous pouvez l'utiliser pour de nombreux niveaux d'indirection comme ceci:

$ a=b
$ b=c
$ c=d
$ echo $(var_expand $(var_expand $a)
d

Ou si vous avez plus (!?!) De niveaux d'indirection, vous pouvez utiliser une boucle.

Il prévient lorsqu'il est donné:

  • Entrée nulle
  • Un nom de variable non défini

# Expand the variable named by $1 into its value. Works in both {ba,z}sh
# eg: a=HOME $(var_expand $a) == /home/me
var_expand() {
  if [ -z "${1-}" ] || [ $# -ne 1 ]; then
    printf 'var_expand: expected one argument\n' >&2;
    return 1;
  fi
  eval printf '%s' "\"\${$1?}\""
}
Tom Hale
la source
Une solution cross-shell fonctionnelle pour moi. Espérons que "l'expansion double variable compatible POSIX" redirige vers cette réponse à l'avenir.
BlueDrink9
2

Tout comme ${$foo}ne fonctionne pas à la place de ${(P)foo}dans zsh, ni ne fait ${${$foo}}. Il vous suffit de spécifier chaque niveau d'indirection:

$ foo=bar
$ bar=baz
$ baz=3
$ echo $foo
bar
$ echo ${(P)foo}
baz
$ echo ${(P)${(P)foo}}
3

Bien sûr, ${!${!foo}}ne fonctionne pas bash, car bashne permet pas les substitutions imbriquées.

chepner
la source
Merci, c'est bon à savoir. Dommage que ce soit uniquement zsh, donc vous ne pouvez pas vraiment le mettre dans un script (tout le monde a bash, mais ce n'est pas l'inverse).
Profpatsch
Je soupçonne qu'il zshest installé beaucoup plus souvent que vous ne le pensez. Il peut ne pas être lié à shcomme bashsouvent (mais pas toujours), mais il peut toujours être disponible pour les scripts shell. Pensez-y comme vous le faites Perl ou Python.
chepner
2

Pourquoi auriez-vous besoin de faire ça?

Vous pouvez toujours le faire en plusieurs étapes comme:

eval "l1=\${$var}"
eval "l2=\${$l1}"
...

Ou utilisez une fonction comme:

deref() {
  if [ "$1" -le 0 ]; then
    eval "$3=\$2"
  else
    eval "deref $(($1 - 1)) \"\${$2}\" \"\$3\""
  fi
}

Ensuite:

$ a=b b=c c=d d=e e=blah
$ deref 3 a res; echo "$res"
d
$ deref 5 a res; echo "$res"
blah

FWIW, dans zsh:

$ echo ${(P)${(P)${(P)${(P)a}}}}
blah
Stéphane Chazelas
la source
Huh, utiliser plusieurs étapes ne m'est pas venu à l'esprit jusqu'à présent… étrange.
Profpatsch
Il est évident, de mon point de vue, pourquoi @Profpatsch veut le faire . Parce que c'est la manière la plus intuitive de le faire. Il est difficile d'implémenter plusieurs substitutions dans bash.
Nikos Alexandris
2

Solution POSIX

Sans devenir fou avec la citation, vous pouvez utiliser cette fonction pour de nombreux niveaux d'indirection comme ceci:

$ a=b
$ b=c
$ c=d
$ echo $(var_expand $(var_expand $a)
d

Ou si vous avez plus (!?!) De niveaux d'indirection, vous pouvez utiliser une boucle.

Il prévient lorsqu'il est donné:

  • Entrée nulle
  • Plus d'un argument
  • Un nom de variable non défini

# Expand the variable named by $1 into its value. Works in both {ba,z}sh
# eg: a=HOME $(var_expand $a) == /home/me
function var_expand {
  if [ "$#" -ne 1 ] || [ -z "${1-}" ]; then
    printf 'var_expand: expected one argument\n' >&2;
    return 1;
  fi
  eval printf '%s' "\"\${$1?}\""
}
Tom Hale
la source
Y a-t-il une raison pour laquelle vous n'avez pas combiné cette réponse avec la précédente? En un coup d'œil, je ne vois pas la différence entre eux.
BlueDrink9