Les variables d'environnement ne sont pas définies lorsque ma fonction est appelée dans un pipeline

10

J'ai la fonction récursive suivante pour définir les variables d'environnement:

function par_set {
  PAR=$1
  VAL=$2
  if [ "" != "$1" ]
  then
    export ${PAR}=${VAL}
    echo ${PAR}=${VAL}
    shift
    shift
    par_set $*
  fi
}

Si je l'appelle par lui-même, il définit à la fois la variable et fait écho à stdout:

$ par_set FN WORKS
FN=WORKS
$ echo "FN = "$FN
FN = WORKS

La redirection de stdout vers un fichier fonctionne également:

$ par_set REDIR WORKS > out
cat out
REDIR=WORKS
$ echo "REDIR = "$REDIR
REDIR = WORKS

Mais, si je redirige stdout vers une autre commande, la variable n'est pas définie:

$ par_set PIPE FAILS |sed -e's/FAILS/BARFS/'
PIPE=BARFS
$ echo "PIPE = "$PIPE
PIPE =

Pourquoi le canal empêche-t-il la fonction d'exporter la variable? Existe-t-il un moyen de résoudre ce problème sans recourir à des fichiers temporaires ou à des canaux nommés?

Résolu:

Code de travail grâce à Gilles:

par_set $(echo $*|tr '=' ' ') > >(sed -e's/^/  /' >> ${LOG})

Cela permet au script d'être appelé ainsi:

$ . ./script.sh PROCESS_SUB ROCKS PIPELINES=NOGOOD
$ echo $PROCESS_SUB
ROCKS
$ echo $PIPELINES
NOGOOD
$ cat log
7:20140606155622162731431:script.sh:29581:Parse Command Line parameters.  Params must be in matched pairs separated by one or more '=' or ' '.
  PROCESS_SUB=ROCKS
  PIPELINES=NOGOOD

Projet hébergé sur bitbucket https://bitbucket.org/adalby/monitor-bash si intéressé par le code complet.

Andrew
la source

Réponses:

8

Chaque partie d'un pipeline (c'est-à-dire chaque côté du tuyau) s'exécute dans un processus distinct (appelé sous-shell, lorsqu'un shell forks un sous-processus pour exécuter une partie du script). Dans par_set PIPE FAILS |sed -e's/FAILS/BARFS/', la PIPEvariable est définie dans le sous-processus qui exécute le côté gauche du tuyau. Cette modification n'est pas reflétée dans le processus parent (les variables d'environnement ne sont pas transférées entre les processus, elles ne sont héritées que par des sous-processus.

Le côté gauche d'un tuyau s'exécute toujours en sous-couche. Certains shells (ATT ksh, zsh) courent du côté droit dans les shells parents; la plupart courent également du côté droit en sous-coquille.

Si vous souhaitez à la fois rediriger la sortie d'une partie du script et exécuter cette partie dans le shell parent, dans ksh / bash / zsh, vous pouvez utiliser la substitution de processus .

par_set PROCESS SUBSTITUTION > >(sed s/ION/ED/)

Avec n'importe quel shell POSIX, vous pouvez rediriger la sortie vers un canal nommé.

mkfifo f
<f grep NAMED= &
par_set NAMED PIPE >f

Oh, et vous manquez des guillemets sur les substitutions de variables , votre code rompt sur des choses comme par_set name 'value with spaces' star '*'.

export "${PAR}=${VAL}"

par_set "$@"
Gilles 'SO- arrête d'être méchant'
la source
Substitution de processus pour la victoire! Je savais que je pouvais utiliser un tube nommé ou un fichier temporaire, mais ceux-ci sont moches, ont une mauvaise concurrence et laissent un gâchis si le script meurt (le piège aide avec le dernier). La chose spatiale est intentionnelle. Par convention, les variables passées sur la ligne de commande sont en paires nom / valeur et séparées par '=' et / ou ''.
Andrew
3

Cela ne fonctionne pas car chaque côté du tuyau s'exécute dans un sous-shell bashet les variables définies dans un sous-shell sont locales à ce sous-shell.

Mettre à jour:

Il semble qu'il soit facile de passer des variables du parent au shell enfant, mais vraiment difficile de le faire dans l'autre sens. Certaines solutions de contournement sont nommées tubes, fichiers temporaires, écriture dans stdout et lecture dans le parent, etc.

Quelques références:

http://mywiki.wooledge.org/BashFAQ/024
/programming//q/15541321/3565972
/programming//a/15383353/3565972
http://forums.opensuse.org/showthread .php / 458979-How-export-variable-in-subshell-back-out-to-parent

savanto
la source
Je pensais que c'était une possibilité, mais la fonction devrait s'exécuter dans le shell actuel. J'ai testé en ajoutant un "écho $$" à la fonction. par_set STILL FAILS | sed -e "s / ^ / sedpid = $$, fnpid = /" sorties sedpid = 15957, fnpid = 15957.
Andrew
@Andrew Voir ce stackoverflow.com/a/20726041/3565972 . Apparemment, $$c'est la même chose pour les coquilles des parents et des enfants. Vous pouvez utiliser $BASHPIDpour obtenir le pid du sous-shell. Quand je suis à l' echo $$ $BASHPIDintérieur, par_setje reçois différents pids.
savanto
@Andrew Toujours en train de trouver une solution de contournement, mais sans succès! =)
savanto
@ Savanto-Merci. Je ne savais pas que sur $$ vs $ BASHPID ou les sous-coquilles de forçage de tuyaux.
Andrew
0

Vous signalez les sous-coquilles - qui peuvent être contournées avec quelques agissements dans le shell à l'extérieur d'un pipeline - mais la partie la plus difficile du problème a à voir avec la concurrence du pipeline .

Tous les membres du processus du pipeline démarrent en même temps , et donc le problème pourrait être plus facile à comprendre si vous le regardez comme ceci:

{ sleep 1 ; echo $((f=1+2)) >&2 ; } | echo $((f))
###OUTPUT
0
...
3

Les processus de pipeline ne peuvent pas hériter des valeurs de variable car ils sont déjà désactivés et en cours d'exécution avant que la variable soit définie.

Je ne peux pas vraiment comprendre à quoi sert votre fonction - à quoi sert-elle qui exportne l'est pas déjà? Ou même juste var=val? Par exemple, voici à nouveau presque le même pipeline:

pipeline() { 
    { sleep 1
      echo "f=$((f=f+1+2))" >&3
    } | echo $((f)) >&2
} 3>&1

f=4 pipeline

###OUTPUT

4
...
f=7

Et avec export:

export $(f=4 pipeline) ; pipeline

###OUTPUT:

4
7
...
f=10

Donc, votre truc pourrait fonctionner comme:

par_set $(echo PIPE FAILS | 
    sed 's/FAIL/WORK/;/./w /path/to/log')

Qui se connecterait à sedla sortie d'un fichier et le livrerait à votre fonction en tant que shell-split "$@".

Ou bien:

$ export $(echo PIPE FAILS | sed 's/ FAIL/=WORK/')
$ par_set $PIPE TWICE_REMOVED
$ echo "WORKS = "$WORKS
WORKS = TWICE_REMOVED

Si je devais écrire votre fonction, cela ressemblerait probablement à ceci:

_env_eval() { 
    while ${2+:} false ; do
       ${1:+export} ${1%%["${IFS}"]*}="${2}" || :
       shift 2
    done
}
mikeserv
la source
C'est vrai, mais sans importance: Andrew essaie d'utiliser les variables après la fin du pipeline, pas de l'autre côté du tuyau.
Gilles 'SO- arrête d'être méchant'
@ Gilles - eh bien, il peut le faire. |pipeline | sh
mikeserv
@Gilles - en fait, vous n'en avez même pas besoin sh. Il utilise déjà export.
mikeserv
L'objectif global est les scripts de surveillance du système. Voudrait pouvoir les appeler de manière interactive, à partir d'autres scripts ou de cron. Vous voulez pouvoir passer des paramètres en définissant des vars env, en passant sur la ligne de commande ou un fichier de configuration. La fonction existe pour prendre l'entrée d'une ou plusieurs sources et définir l'environnement correctement. Cependant, je veux également pouvoir éventuellement enregistrer la sortie (après avoir parcouru sed au format), d'où la nécessité de canaliser.
Andrew
1
@ mikeserv- Merci pour tous les commentaires. Je suis allé avec la suggestion de Gilles de substitution de processus, mais avoir à argumenter pour mon code fait de moi un meilleur programmeur.
Andrew