Pourquoi ne puis-je pas spécifier une variable d'environnement et la faire écho dans la même ligne de commande?

90

Considérez cet extrait:

$ SOMEVAR=AAA
$ echo zzz $SOMEVAR zzz
zzz AAA zzz

Ici , je me suis fixé $SOMEVARà AAAla première ligne - et quand je fais l' écho sur la deuxième ligne, je reçois le AAAcontenu comme prévu.

Mais alors, si j'essaie de spécifier la variable sur la même ligne de commande que le echo:

$ SOMEVAR=BBB echo zzz $SOMEVAR zzz
zzz AAA zzz

... Je n'obtiens pas BBBcomme prévu - j'obtiens l'ancienne valeur ( AAA).

Est-ce que c'est ainsi que les choses sont censées être? Si tel est le cas, comment se fait-il que vous puissiez spécifier des variables comme LD_PRELOAD=/... program args ...et que cela fonctionne? Qu'est-ce que je rate?

sdaau
la source
2
Cela fonctionne lorsque vous faites de l'affectation une instruction distincte ou lorsque vous appelez un script avec son propre environnement, mais pas lorsque vous préférez une commande dans l'environnement actuel. Intéressant!
Todd A. Jacobs
1
La raison LD_PRELOADfonctionne est que la variable est définie dans l' environnement du programme - pas sur sa ligne de commande.
Suspendu jusqu'à nouvel ordre.

Réponses:

102

Ce que vous voyez est le comportement attendu. Le problème est que le shell parent évalue $SOMEVARsur la ligne de commande avant d'appeler la commande avec l'environnement modifié. Vous devez obtenir l'évaluation de $SOMEVARdifféré jusqu'à ce que l'environnement soit défini.

Vos options immédiates comprennent:

  1. SOMEVAR=BBB eval echo zzz '$SOMEVAR' zzz.
  2. SOMEVAR=BBB sh -c 'echo zzz $SOMEVAR zzz'.

Les deux utilisent des guillemets simples pour empêcher le shell parent d'évaluer $SOMEVAR; il n'est évalué qu'après avoir été défini dans l'environnement (temporairement, pour la durée de la commande unique).

Une autre option consiste à utiliser la notation sous-shell (comme l'a également suggéré Marcus Kuhn dans sa réponse ):

(SOMEVAR=BBB; echo zzz $SOMEVAR zzz)

La variable est définie uniquement dans le sous-shell

Jonathan Leffler
la source
Génial, @JonathanLeffler - merci beaucoup pour l'explication; à votre santé!
sdaau
L'ajout de @ markus-kuhn est difficile à surestimer.
Alex Che
37

Le problème, revisité

Franchement, le manuel est déroutant sur ce point. Le manuel GNU Bash dit:

L'environnement de toute commande ou fonction simple [notez que cela exclut les fonctions internes] peut être temporairement augmenté en le préfixant avec des affectations de paramètres, comme décrit dans Paramètres du shell. Ces instructions d'affectation n'affectent que l'environnement vu par cette commande.

Si vous analysez vraiment la phrase, ce qu'elle dit, c'est que l' environnement de la commande / fonction est modifié, mais pas l'environnement du processus parent. Donc, cela fonctionnera:

$ TESTVAR=bbb env | fgrep TESTVAR
TESTVAR=bbb

car l'environnement de la commande env a été modifié avant son exécution. Cependant, cela ne fonctionnera pas:

$ set -x; TESTVAR=bbb echo aaa $TESTVAR ccc
+ TESTVAR=bbb
+ echo aaa ccc
aaa ccc

à cause du moment où l'expansion des paramètres est effectuée par le shell.

Étapes de l'interprète

Une autre partie du problème est que Bash définit ces étapes pour son interpréteur:

  1. Lit son entrée à partir d'un fichier (voir Scripts Shell), d'une chaîne fournie comme argument à l'option d'invocation -c (voir Invocation de Bash) ou depuis le terminal de l'utilisateur.
  2. Décompose l'entrée en mots et opérateurs, en respectant les règles de citation décrites dans Quoting. Ces jetons sont séparés par des métacaractères. L'expansion des alias est effectuée par cette étape (voir Alias).
  3. Analyse les jetons en commandes simples et composées (voir Commandes Shell).
  4. Exécute les différentes extensions du shell (voir Expansions du shell), divisant les jetons développés en listes de noms de fichiers (voir Extension de nom de fichier) et commandes et arguments.
  5. Effectue toutes les redirections nécessaires (voir Redirections) et supprime les opérateurs de redirection et leurs opérandes de la liste d'arguments.
  6. Exécute la commande (voir Exécution de commandes).
  7. Attend éventuellement la fin de la commande et récupère son statut de sortie (voir Statut de sortie).

Ce qui se passe ici, c'est que les fonctions intégrées n'ont pas leur propre environnement d'exécution, donc elles ne voient jamais l'environnement modifié. En outre, les commandes simples (par exemple / bin / echo) n'obtenir un ennvironment modifié ( ce qui explique pourquoi l'exemple env a travaillé) mais l'expansion de la coquille se déroule dans le courant environnement à l' étape 4.

En d'autres termes, vous ne passez pas 'aaa $ TESTVAR ccc' à / bin / echo; vous passez la chaîne interpolée (telle que développée dans l'environnement actuel) à / bin / echo. Dans ce cas, puisque l'environnement actuel n'a pas de TESTVAR , vous passez simplement «aaa ccc» à la commande.

Résumé

La documentation pourrait être beaucoup plus claire. Heureusement qu'il y a Stack Overflow!

Voir également

http://www.gnu.org/software/bash/manual/bashref.html#Command-Execution-Environment

Todd A. Jacobs
la source
J'avais déjà voté pour cela - mais je viens de revenir sur cette question, et ce post contient exactement les pointeurs dont j'ai besoin; merci beaucoup, @CodeGnome!
sdaau
Je ne sais pas si Bash a changé dans ce domaine depuis cette réponse a été publiée, mais les affectations de variables préfixées ne travail avec builtins maintenant. Par exemple, FOO=foo eval 'echo $FOO'imprime foocomme prévu. Cela signifie que vous pouvez faire des choses comme IFS="..." read ....
Will Vousden
Je pense que ce qui se passe, c'est que Bash modifie temporairement son propre environnement et le restaure une fois la commande terminée, ce qui peut avoir des effets secondaires étranges.
Will Vousden
22

Pour réaliser ce que vous voulez, utilisez

( SOMEVAR=BBB; echo zzz $SOMEVAR zzz )

Raison:

  • Vous devez séparer l'affectation par un point-virgule ou une nouvelle ligne de la commande suivante, sinon elle n'est pas exécutée avant le développement des paramètres pour la commande suivante (écho).

  • Vous devez effectuer l'affectation dans un environnement de sous - shell , pour vous assurer qu'elle ne persiste pas au-delà de la ligne actuelle.

Cette solution est plus courte, plus propre et plus efficace que certaines des autres suggérées, en particulier elle ne crée pas de nouveau procédé.

Markus Kuhn
la source
3
Pour les futurs googleurs qui se retrouvent ici: c'est probablement la meilleure réponse à cette question. Pour compliquer davantage les choses, si vous souhaitez que l'affectation soit disponible dans l'environnement de la commande, vous devez l'exporter. Le sous-shell empêche toujours l'affectation de persister. (export SOMEVAR=BBB; python -c "from os import getenv; print getenv('SOMEVAR')")
eaj
@eaj Pour exporter une variable shell vers un seul appel de programme externe, comme dans votre exemple, utilisez simplementSOMEVAR=BBB python -c "from os import getenv; print getenv('SOMEVAR')"
Markus Kuhn
10

La raison en est que cela définit une variable d'environnement pour une ligne. Mais, echone fait pas l'expansion, bashfait. Par conséquent, votre variable est en fait développée avant que la commande ne soit exécutée, même si elle SOME_VARestBBB dans le contexte de la commande echo.

Pour voir l'effet, vous pouvez faire quelque chose comme:

$ SOME_VAR=BBB bash -c 'echo $SOME_VAR'
BBB

Ici, la variable n'est pas développée tant que le processus enfant n'est pas exécuté, vous voyez donc la valeur mise à jour. si vous vérifiez à SOME_VARIABLEnouveau dans le shell parent, c'est toujours AAA, comme prévu.

Erreur fatale
la source
1
+1 pour une explication correcte de la raison pour laquelle cela ne fonctionne pas comme écrit et pour une solution de contournement viable.
Jonathan Leffler
1
SOMEVAR=BBB; echo zzz $SOMEVAR zzz

Utiliser un ; pour séparer les instructions qui sont sur la même ligne.

Kyros
la source
1
Cela fonctionne, mais ce n'est pas tout à fait le point. L'idée est de définir l'environnement pour une seule commande, pas de manière permanente comme le fait votre solution.
Jonathan Leffler
Merci pour ça @Kyros; Je ne sais pas comment ça se fait que j'ai raté ça maintenant :) Toujours en train d'errer comment LD_PRELOADet tel peut fonctionner devant un exécutable sans point-virgule, cependant ... Merci encore une fois - bravo!
sdaau
@JonathanLeffler - en effet, c'était l'idée; Je n'avais pas réalisé que le point-virgule rend le changement permanent - merci de l'avoir noté!
sdaau
1

Voici une alternative:

SOMEVAR=BBB && echo zzz $SOMEVAR zzz
Brian
la source
Que vous utilisiez &&ou ;sépariez les commandes, l'affectation persiste, ce qui n'est pas le comportement souhaité par OP. Markus Kuhn a la version correcte de cette réponse.
eaj