Cela nécessite bash 4.1 si vous utilisez {fd}
ou local -n
.
Le reste devrait fonctionner dans bash 3.x j'espère. Je ne suis pas complètement sûr à cause de printf %q
- cela pourrait être une fonctionnalité bash 4.
Résumé
Votre exemple peut être modifié comme suit pour archiver l'effet souhaité:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
imprime comme vous le souhaitez:
hello
4
Notez que cette solution:
- Fonctionne aussi pour
e=1000
.
- Se conserve
$?
si vous avez besoin$?
Les seuls effets secondaires négatifs sont:
- Il a besoin d'un moderne
bash
.
- Il fourche plus souvent.
- Il a besoin de l'annotation (nommée d'après votre fonction, avec un ajouté
_
)
- Il sacrifie le descripteur de fichier 3.
- Vous pouvez le changer en un autre FD si vous en avez besoin.
- En
_capture
simplement remplacer toutes les occurences de 3
avec un autre numéro (plus élevé).
Ce qui suit (qui est assez long, désolé pour cela) explique, espérons-le, comment ajouter cette recette à d'autres scripts également.
Le problème
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
les sorties
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
tandis que la sortie souhaitée est
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
La cause du problème
Les variables shell (ou en général, l'environnement) sont transmises des processus parentaux aux processus enfants, mais pas l'inverse.
Si vous effectuez une capture de sortie, celle-ci est généralement exécutée dans un sous-shell, il est donc difficile de renvoyer des variables.
Certains vous disent même que c'est impossible à réparer. C'est faux, mais c'est un problème difficile à résoudre depuis longtemps.
Il existe plusieurs façons de le résoudre au mieux, cela dépend de vos besoins.
Voici un guide étape par étape sur la façon de procéder.
Renvoyer des variables dans le shell parental
Il existe un moyen de renvoyer des variables à un shell parental. Cependant, c'est un chemin dangereux, car cela utilise eval
. Si cela est mal fait, vous risquez beaucoup de mauvaises choses. Mais si cela est fait correctement, c'est parfaitement sûr, à condition qu'il n'y ait pas de bogue dans bash
.
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
impressions
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Notez que cela fonctionne également pour les choses dangereuses:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
impressions
; /bin/echo *
Cela est dû au fait printf '%q'
que vous pouvez le réutiliser en toute sécurité dans un contexte shell.
Mais c'est une douleur dans le a ..
Cela n'a pas seulement l'air moche, c'est aussi beaucoup à taper, donc c'est sujet aux erreurs. Juste une seule erreur et vous êtes condamné, non?
Eh bien, nous sommes au niveau du shell, vous pouvez donc l'améliorer. Pensez simplement à une interface que vous souhaitez voir, puis vous pourrez l'implémenter.
Augmenter, comment le shell traite les choses
Revenons en arrière et réfléchissons à une API qui nous permet d'exprimer facilement ce que nous voulons faire.
Eh bien, que voulons-nous faire avec la d()
fonction?
Nous voulons capturer la sortie dans une variable. OK, alors implémentons une API pour exactement cela:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Maintenant, au lieu d'écrire
d1=$(d)
nous pouvons écrire
capture d1 d
Eh bien, cela semble que nous n'avons pas beaucoup changé, car, encore une fois, les variables ne sont pas renvoyées depuis d
le shell parent, et nous devons taper un peu plus.
Cependant, maintenant, nous pouvons y jeter toute la puissance du shell, car il est joliment enveloppé dans une fonction.
Pensez à une interface facile à réutiliser
Une deuxième chose est que nous voulons être SEC (Don't Repeat Yourself). Donc, nous ne voulons définitivement pas taper quelque chose comme
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
Le x
ici n'est pas seulement redondant, il est sujet aux erreurs de toujours se répéter dans le bon contexte. Que faire si vous l'utilisez 1000 fois dans un script, puis ajoutez une variable? Vous ne souhaitez définitivement pas modifier tous les 1000 emplacements où un appel d
est impliqué.
Alors laissez de x
côté, afin que nous puissions écrire:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
les sorties
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Cela semble déjà très bien. (Mais il y a toujours le local -n
qui ne fonctionne pas dans l'odeur commune bash
3.x)
Évitez de changer d()
La dernière solution présente de gros défauts:
d()
doit être modifié
- Il doit utiliser certains détails internes de
xcapture
pour transmettre la sortie.
- Notez que cela ombres (brûle) une variable nommée
output
, donc nous ne pouvons jamais la renvoyer.
- Il doit coopérer avec
_passback
Pouvons-nous nous en débarrasser également?
Bien sûr on peut! Nous sommes dans une coquille, il y a donc tout ce dont nous avons besoin pour y parvenir.
Si vous regardez un peu plus près de l'appel, eval
vous pouvez voir que nous avons un contrôle à 100% à cet endroit. «À l'intérieur», eval
nous sommes dans un sous-shell, nous pouvons donc faire tout ce que nous voulons sans craindre de faire quelque chose de mal à la coquille parentale.
Ouais, bien, ajoutons donc un autre wrapper, maintenant directement dans le eval
:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
impressions
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Cependant, cela présente encore une fois un inconvénient majeur:
- Les
!DO NOT USE!
marqueurs sont là, car il y a une très mauvaise condition de course là-dedans, que vous ne pouvez pas voir facilement:
- C'est
>(printf ..)
un travail d'arrière-plan. Il peut donc continuer à s'exécuter pendant l' _passback x
exécution de.
- Vous pouvez le voir vous-même si vous ajoutez un
sleep 1;
avant printf
ou un _passback
.
_xcapture a d; echo
puis sorties x
ou d' a
abord, respectivement.
- Le
_passback x
ne devrait pas en faire partie _xcapture
, car cela rend difficile la réutilisation de cette recette.
- Nous avons également une fourche non avertie ici (la
$(cat)
), mais comme cette solution est, !DO NOT USE!
j'ai pris le chemin le plus court.
Cependant, cela montre que nous pouvons le faire, sans modification de d()
(et sans local -n
)!
Veuillez noter que nous n'avons pas nécessairement besoin _xcapture
du tout, car nous aurions pu tout écrire directement dans le eval
.
Cependant, cela n'est généralement pas très lisible. Et si vous revenez à votre script dans quelques années, vous voudrez probablement pouvoir le relire sans trop de problèmes.
Fixez la course
Maintenant, corrigeons la condition de concurrence.
L'astuce pourrait être d'attendre la printf
fermeture de STDOUT, puis de sortir x
.
Il existe de nombreuses façons d'archiver ceci:
- Vous ne pouvez pas utiliser de tubes shell, car les tubes s'exécutent dans des processus différents.
- On peut utiliser des fichiers temporaires,
- ou quelque chose comme un fichier de verrouillage ou un fifo. Cela permet d'attendre la serrure ou le fifo,
- ou différents canaux, pour sortir les informations, puis assembler la sortie dans un ordre correct.
Suivre le dernier chemin pourrait ressembler à (notez qu'il fait le printf
dernier car cela fonctionne mieux ici):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
les sorties
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Pourquoi est-ce correct?
_passback x
parle directement à STDOUT.
- Cependant, comme STDOUT doit être capturé dans la commande interne, nous le «sauvegardons» d'abord dans FD3 (vous pouvez en utiliser d'autres, bien sûr) avec '3> & 1', puis nous le réutilisons avec
>&3
.
- Le se
$("${@:2}" 3<&-; _passback x >&3)
termine après le _passback
, lorsque le sous-shell ferme STDOUT.
- Donc,
printf
cela ne peut pas arriver avant le _passback
, quel que soit le temps qu'il _passback
faut.
- Notez que la
printf
commande n'est pas exécutée avant que la ligne de commande complète ne soit assemblée, nous ne pouvons donc pas voir les artefacts à partir de printf
, indépendamment de la manière dont printf
est implémentée.
Par conséquent _passback
, exécute d' abord , puis le printf
.
Cela résout la course, sacrifiant un descripteur de fichier fixe 3. Vous pouvez, bien sûr, choisir un autre descripteur de fichier dans le cas où FD3 n'est pas libre dans votre shellscript.
Veuillez également noter le 3<&-
qui protège FD3 pour être passé à la fonction.
Rendez-le plus générique
_capture
contient des pièces qui appartiennent à d()
, ce qui est mauvais, du point de vue de la réutilisabilité. Comment résoudre ça?
Eh bien, faites-le de manière désespérée en introduisant une dernière chose, une fonction supplémentaire, qui doit renvoyer les bonnes choses, qui est nommée d'après la fonction d'origine avec _
attaché.
Cette fonction est appelée après la fonction réelle et peut augmenter les choses. De cette façon, cela peut être lu comme une annotation, donc c'est très lisible:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
imprime encore
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Autoriser l'accès au code de retour
Il n'y a qu'un peu manquant:
v=$(fn)
définit $?
ce qui fn
est retourné. Donc, vous le voulez probablement aussi. Il faut cependant quelques ajustements plus importants:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
impressions
23 42 69 FAIL
Il y a encore beaucoup à faire
_passback()
peut être éliminé avec passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
peut être éliminé avec capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
La solution pollue un descripteur de fichier (ici 3) en l'utilisant en interne. Vous devez garder cela à l'esprit si vous réussissez les FD.
Notez que les bash
versions 4.1 et supérieures {fd}
doivent utiliser des FD inutilisées.
(Peut-être ajouterai-je une solution ici quand je reviendrai.)
Notez que c'est pourquoi j'utilise pour le mettre dans des fonctions distinctes comme _capture
, parce que tout cela dans une seule ligne est possible, mais il est de plus en plus difficile à lire et à comprendre
Vous souhaitez peut-être également capturer STDERR de la fonction appelée. Ou vous voulez même transmettre et sortir plus d'un descripteur de fichier depuis et vers des variables.
Je n'ai pas encore de solution, mais voici un moyen d'attraper plus d'un FD , donc nous pouvons probablement renvoyer les variables de cette façon aussi.
N'oubliez pas non plus:
Cela doit appeler une fonction shell, pas une commande externe.
Il n'y a pas de moyen facile de passer des variables d'environnement hors de commandes externes. (Avec LD_PRELOAD=
cela devrait être possible, cependant!) Mais c'est alors quelque chose de complètement différent.
Derniers mots
Ce n’est pas la seule solution possible. C'est un exemple de solution.
Comme toujours, vous avez de nombreuses façons d'exprimer les choses dans le shell. Alors n'hésitez pas à vous améliorer et à trouver quelque chose de mieux.
La solution présentée ici est loin d'être parfaite:
- Ce n'était presque pas du tout testet, alors pardonnez les fautes de frappe.
- Il y a encore beaucoup à faire, voir ci-dessus.
- Il utilise de nombreuses fonctionnalités modernes
bash
, il est donc probablement difficile à porter sur d'autres shells.
- Et il pourrait y avoir des bizarreries auxquelles je n'ai pas pensé.
Cependant je pense que c'est assez facile à utiliser:
- Ajoutez seulement 4 lignes de "bibliothèque".
- Ajoutez juste 1 ligne "d'annotation" pour votre fonction shell.
- Sacrifie temporairement un seul descripteur de fichier.
- Et chaque étape devrait être facile à comprendre, même des années plus tard.