Enregistrer la sortie de la commande qui modifie l'environnement en une variable

8

Comment enregistrer la sortie d'une commande qui modifie l'environnement en une variable?

J'utilise bash shell.

Supposons que j'ai:

function f () { a=3; b=4 ; echo "`date`: $a $b"; }

Et maintenant, je peux utiliser des commandes pour exécuter f:

$ a=0; b=0; f; echo $a; echo $b; echo $c
Sat Jun 28 21:27:08 CEST 2014: 3 4
3
4

mais je voudrais enregistrer la sortie de la fvariable c, j'ai donc essayé:

a=0; b=0; c=""; c=$(f); echo $a; echo $b; echo $c

mais malheureusement, j'ai:

0
0
Sat Jun 28 21:28:03 CEST 2014: 3 4

donc je n'ai pas de changement d'environnement ici.

Comment enregistrer la sortie de la commande (pas seulement la fonction) dans une variable et enregistrer les changements environnementaux?

Je sais que cela $(...)ouvre un nouveau sous-shell et c'est le problème, mais est-il possible de contourner ce problème?

faramir
la source
Droit, de sorte que le problème est que $aet $bsont des variables locales dans votre ffonction. Vous pourriez exportles faire, mais cela semble sommaire.
HalosGhost du
1
@HalosGhost: Non, je ne pense pas. Regardez le premier exemple: …; f; echo $a; …entraîne 3un écho, tout fcomme la modification d'une variable shell (et pas seulement sa propre variable locale).
Scott

Réponses:

2

Si vous utilisez Bash 4 ou une version ultérieure, vous pouvez utiliser des coprocessus :

function f () { a=3; b=4 ; echo "`date`: $a $b"; }
coproc cat
f >&${COPROC[1]}
exec {COPROC[1]}>&-
read c <&${COPROC[0]}
echo a $a
echo b $b
echo c $c

affichera

a 3
b 4
c Sun Jun 29 10:08:15 NZST 2014: 3 4

coproccrée un nouveau processus exécutant une commande donnée (ici, cat). Il enregistre le PID COPROC_PIDet les descripteurs de fichiers d'entrée / sortie standard dans un tableau COPROC(comme pipe(2), ou voir ici ou ici ).

Ici, nous exécutons la fonction avec une sortie standard pointée vers notre coprocessus en cours d'exécution cat, puis à readpartir de celle-ci. Puisque catjuste crache son entrée, nous obtenons la sortie de la fonction dans notre variable. exec {COPROC[1]}>&-ferme simplement le descripteur de fichier afin de catne pas attendre indéfiniment.


Notez que cela readne prend qu'une seule ligne à la fois. Vous pouvez utiliser mapfilepour obtenir un tableau de lignes, ou simplement utiliser le descripteur de fichier comme vous le souhaitez d'une manière différente.

exec {COPROC[1]}>&-travaux dans les versions actuelles de Bash, mais les versions antérieures de la série 4 nécessitent d'enregistrer le descripteur de fichier dans une variable simple première: fd=${COPROC[1]}; exec {fd}>&-. Si votre variable n'est pas définie, elle fermera la sortie standard.


Si vous utilisez une version 3 de Bash, vous pouvez obtenir le même effet mkfifo, mais ce n'est pas beaucoup mieux que d'utiliser un fichier réel à ce stade.

Michael Homer
la source
Je ne peux utiliser que bash 3.1. Les tuyaux sont difficiles à utiliser. Est-ce que mktemp et est la seule option? N'y a-t-il pas ici une syntaxe ou une option spéciale pour exécuter la commande et obtenir la sortie mais avec des changements d'environnement?
faramir
Vous pouvez utiliser mkfifo pour créer un canal nommé en remplacement de coproc, ce qui n'implique pas réellement d'écrire les données sur le disque. Avec cela, vous redirigez la sortie vers le fifo et entrez à partir de celui-ci au lieu du coprocessus. Sinon non.
Michael Homer
+1 pour avoir adapté ma réponse pour ne pas utiliser de fichier. Mais, dans mes tests, la exec {COPROC[1]}>&-commande (au moins parfois) termine immédiatement le coprocessus , donc sa sortie n'est plus disponible pour être lue. coproc { cat; sleep 1; }semble fonctionner mieux. (Vous devrez peut-être augmenter la valeur 1si vous lisez plusieurs lignes.)
Scott
1
Il y a ici une signification conventionnelle et bien comprise de «fichier réel» que vous ignorez volontairement.
Michael Homer
1
"Fichier réel" signifiait un fichier sur disque , tel que compris par tout le monde ici sauf, prétendument, vous. Personne n'a jamais laissé entendre ou permis à un lecteur raisonnable de déduire que l'un ou l'autre était le même que des arguments ou des variables d'environnement. Je ne suis pas intéressé à poursuivre cette discussion.
Michael Homer
2

Si vous comptez déjà sur le corps de l' fexécution dans le même shell que l'appelant, et donc être en mesure de modifier des variables comme aet b, pourquoi ne pas définir la fonction caussi bien? En d'autres termes:

$ function f () { a=3; b=4 ; c=$(echo "$(date): $a $b"); }
$ a=0; b=0; f; echo $a; echo $b; echo $c
3
4
Mon Jun 30 14:32:00 EDT 2014: 3 4

Une raison possible peut être que la variable de sortie doit avoir des noms différents (c1, c2, etc.) lorsqu'elle est appelée à différents endroits, mais vous pouvez résoudre ce problème en définissant la fonction c_TEMP et en demandant aux appelants de le faire, c1=$c_TEMPetc.

godlygeek
la source
1

C'est un kluge, mais essayez

f > c.file
c=$(cat c.file)

(et, éventuellement, rm c.file).

Scott
la source
Je vous remercie. C'est une réponse simple et qui fonctionne, mais qui présente certains inconvénients. Je sais que je peux utiliser mktemp, mais je ne voudrais pas créer de fichiers supplémentaires.
faramir
1

Attribuez simplement toutes les variables et écrivez la sortie en même temps.

f() { c= ; echo "${c:=$(date): $((a=3)) $((b=4))}" ; }

Maintenant, si vous le faites:

f ; echo "$a $b $c"

Votre sortie est:

Tue Jul  1 04:58:17 PDT 2014: 3 4
3 4 Tue Jul  1 04:58:17 PDT 2014: 3 4

Notez qu'il s'agit entièrement de code portable POSIX. J'ai initialement défini c=la ''chaîne nulle car l'expansion des paramètres limite l'affectation + l'évaluation simultanée des variables à des valeurs uniquement numériques (comme $((var=num))) ou à des valeurs nulles ou inexistantes - ou, en d'autres termes, vous ne pouvez pas définir et évaluer simultanément une variable en une chaîne arbitraire si cette variable est déjà affectée à une valeur. Je m'assure donc qu'il est vide avant d'essayer. Si je ne vidais pas cavant d'essayer de l'assigner, l'expansion ne retournerait que l'ancienne valeur.

Juste pour démontrer:

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2

newvaln'est pas affecté à $cinline car oldvalest développé dans ${word}, tandis que l' affectation $((arithmétique inline se produit toujours. Mais si n'a pas et est vide ou non défini ...=))$c oldval

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a)) 
    c= a=$((a+a))
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2
newval 8

... newvalest alors à la fois affecté et développé $c.

Toutes les autres façons de procéder impliquent une forme d'évaluation secondaire. Par exemple, supposons que je souhaite affecter la sortie de f()à une variable nommée nameà un moment et varà un autre. Tel qu'il est actuellement écrit, cela ne fonctionnera pas sans définir la var dans la portée de l'appelant. Une manière différente pourrait ressembler à ceci, cependant:

f(){ fout_name= fout= set -- "${1##[0-9]*}" "${1%%*[![:alnum:]]*}"
    (: ${2:?invalid or unspecified param - name set to fout}) || set --
    export "${fout_name:=${1:-fout}}=${fout:=$(date): $((a=${a:-50}+1)) $((b=${b:-100}-4))}"
    printf %s\\n "$fout"
}

f &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$a" "$b"

J'ai fourni un meilleur exemple formaté ci-dessous, mais, appelé comme ci-dessus, la sortie est:

sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
51
96

Ou avec des $ENVarguments différents ou:

b=9 f myvar &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$myvar" \
        "$a" "$b"

###OUTPUT###

Tue Jul  1 19:56:42 PDT 2014: 52 5
myvar
Tue Jul  1 19:56:42 PDT 2014: 52 5
Tue Jul  1 19:56:42 PDT 2014: 52 5
52
5

La chose la plus délicate à faire pour évaluer deux fois est probablement de s'assurer que les variables ne cassent pas les guillemets et exécutent du code aléatoire. Plus une variable est évaluée, plus elle devient difficile. L'extension des paramètres aide beaucoup ici, et l'utilisation exportpar opposition à evalest beaucoup plus sûre.

Dans l'exemple ci-dessus, f()attribue d'abord $foutla ''chaîne nulle, puis définit les paramètres de position pour tester les noms de variables valides. Si les deux tests ne réussissent pas, un message est émis stderret la valeur par défaut de foutest affectée à $fout_name. Indépendamment des tests, cependant, $fout_nameest toujours affecté à l'un foutou au nom que vous spécifiez $foutet, éventuellement, votre nom spécifié reçoit toujours la valeur de sortie de la fonction. Pour illustrer cela, j'ai écrit cette petite forboucle:

for v in var '' "wr;\' ong"
    do sleep 10 &&
        a=${a:+$((a*2))} f "$v" || break
    echo "${v:-'' #null}" 
    printf '#\t"$%s" = '"'%s'\n" \
        a "$a" b "$b" \
        fout_name "$fout_name" \
        fout "$fout" \
        '(eval '\''echo "$'\''"$fout_name"\")' \
            "$(eval 'echo "$'"$fout_name"\")"
 done

Il joue avec certains noms de variables et extensions de paramètres. Si vous avez une question, il suffit de la poser. Cela n'exécute que les mêmes lignes dans la fonction déjà représentée ici. Il convient de mentionner au moins que les variables $aet $bse comportent différemment selon qu'elles sont définies lors de l'invocation ou déjà définies. Pourtant, le forne fait presque rien d'autre que formater l'ensemble de données et fourni par f(). Regarde:

###OUTPUT###

Wed Jul  2 02:50:17 PDT 2014: 51 96
var
#       "$a" = '51'
#       "$b" = '96'
#       "$fout_name" = 'var'
#       "$fout" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:27 PDT 2014: 103 92
'' #null
#       "$a" = '103'
#       "$b" = '92'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:37 PDT 2014: 207 88
wr;\' ong
#       "$a" = '207'
#       "$b" = '88'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
mikeserv
la source