Questo richiede bash 4.1 se usi {fd}
o local -n
.
Il resto dovrebbe funzionare in bash 3.x, spero. Non sono completamente sicuro a causa di printf %q
- questa potrebbe essere una funzionalità di bash 4.
Sommario
Il tuo esempio può essere modificato come segue per archiviare l'effetto desiderato:
# 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"
stampa come desiderato:
hello
4
Nota che questa soluzione:
- Funziona anche per
e=1000
.
- Conserve
$?
se avete bisogno di$?
Gli unici effetti collaterali negativi sono:
- Ha bisogno di un moderno
bash
.
- Si biforca abbastanza più spesso.
- Ha bisogno dell'annotazione (dal nome della tua funzione, con un'aggiunta
_
)
- Sacrifica il descrittore di file 3.
- Puoi cambiarlo con un altro FD se ne hai bisogno.
- In
_capture
appena sostituire tutte le occorrenze di 3
con un altro (più alto) numero.
Si spera che quanto segue (che è abbastanza lungo, mi dispiace) spieghi come adattare questa ricetta anche ad altri script.
Il problema
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
uscite
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
mentre l'output desiderato è
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
La causa del problema
Le variabili di shell (o in generale, l'ambiente) vengono trasmesse dai processi parentali ai processi figli, ma non viceversa.
Se acquisisci l'output, di solito viene eseguito in una subshell, quindi restituire le variabili è difficile.
Alcuni addirittura ti dicono che è impossibile rimediare. Questo è sbagliato, ma è un problema noto da tempo difficile da risolvere.
Esistono diversi modi per risolverlo al meglio, questo dipende dalle tue esigenze.
Ecco una guida passo passo su come farlo.
Ritornare le variabili nella shell dei genitori
C'è un modo per restituire le variabili a una shell parentale. Tuttavia questo è un percorso pericoloso, perché utilizza eval
. Se fatto in modo improprio, rischi molte cose malvagie. Ma se fatto correttamente, questo è perfettamente sicuro, a condizione che non ci siano bug 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
stampe
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Nota che questo funziona anche per cose pericolose:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
stampe
; /bin/echo *
Ciò è dovuto printf '%q'
, che cita tutto ciò, che puoi riutilizzarlo in modo sicuro in un contesto di shell.
Ma questo è un dolore in a ..
Questo non solo sembra brutto, è anche molto da scrivere, quindi è soggetto a errori. Solo un singolo errore e sei condannato, giusto?
Bene, siamo a livello di shell, quindi puoi migliorarlo. Pensa solo a un'interfaccia che vuoi vedere e poi puoi implementarla.
Aumenta, come la shell elabora le cose
Facciamo un passo indietro e pensiamo ad alcune API che ci permettono di esprimere facilmente ciò che vogliamo fare.
Ebbene, cosa vogliamo fare con la d()
funzione?
Vogliamo catturare l'output in una variabile. OK, quindi implementiamo un'API esattamente per questo:
# 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="$("$@")"
}
Ora, invece di scrivere
d1=$(d)
possiamo scrivere
capture d1 d
Bene, sembra che non siamo cambiati molto, dato che, ancora una volta, le variabili non vengono restituite dalla d
shell genitrice e dobbiamo digitare un po 'di più.
Tuttavia ora possiamo utilizzare tutta la potenza della shell, poiché è ben racchiusa in una funzione.
Pensa a un'interfaccia facile da riutilizzare
Una seconda cosa è che vogliamo essere DRY (Don't Repeat Yourself). Quindi non vogliamo assolutamente digitare qualcosa di simile
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
Il x
qui non è solo ridondante, è soggetto a errori da ripetere sempre nel contesto corretto. E se lo usi 1000 volte in uno script e poi aggiungi una variabile? Non vuoi assolutamente modificare tutte le 1000 località in cui d
è coinvolta una chiamata .
Quindi lascia x
perdere, così possiamo scrivere:
_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
uscite
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Sembra già molto buono. (Ma c'è ancora il local -n
che non funziona in oder comune bash
3.x)
Evita di cambiare d()
L'ultima soluzione ha alcuni grossi difetti:
d()
deve essere modificato
- È necessario utilizzare alcuni dettagli interni
xcapture
per passare l'output.
- Nota che questa ombreggia (brucia) una variabile denominata
output
, quindi non possiamo mai restituirla.
- Ha bisogno di collaborare con
_passback
Possiamo sbarazzarci anche di questo?
Certo che possiamo! Siamo in un guscio, quindi c'è tutto ciò di cui abbiamo bisogno per farlo.
Se guardi un po 'più da vicino alla chiamata eval
puoi vedere che abbiamo il controllo al 100% in questa posizione. "Dentro" eval
siamo in una subshell, quindi possiamo fare tutto ciò che vogliamo senza paura di fare qualcosa di brutto al guscio dei genitori.
Sì, bello, quindi aggiungiamo un altro wrapper, ora direttamente all'interno di 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
stampe
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Tuttavia, questo, ancora una volta, presenta alcuni importanti inconvenienti:
- I
!DO NOT USE!
marker ci sono, perché c'è una condizione di gara molto brutta in questo, che non puoi vedere facilmente:
- Il
>(printf ..)
è un processo in background. Quindi potrebbe ancora essere eseguito mentre _passback x
è in esecuzione.
- Puoi vederlo tu stesso se aggiungi un file
sleep 1;
prima printf
o _passback
.
_xcapture a d; echo
quindi le uscite x
o a
first, rispettivamente.
- Il
_passback x
non dovrebbe farne parte _xcapture
, perché questo rende difficile riutilizzare quella ricetta.
- Inoltre abbiamo qualche bivio non segnato qui (il
$(cat)
), ma poiché questa soluzione è !DO NOT USE!
ho preso la via più breve.
Tuttavia, questo dimostra che possiamo farlo, senza modifiche a d()
(e senza local -n
)!
Tieni presente che non ne abbiamo assolutamente bisogno _xcapture
, poiché avremmo potuto scrivere tutto proprio nel file eval
.
Tuttavia, questa operazione di solito non è molto leggibile. E se torni al copione tra qualche anno, probabilmente vorrai poterlo leggere di nuovo senza troppi problemi.
Risolvi la gara
Ora sistemiamo la condizione della gara.
Il trucco potrebbe essere quello di aspettare fino a quando non printf
è stato chiuso è STDOUT, e quindi l'output x
.
Esistono molti modi per archiviarlo:
- Non è possibile utilizzare le pipe di shell, perché le pipe vengono eseguite in processi diversi.
- Si possono usare file temporanei,
- o qualcosa come un file di blocco o un fifo. Questo permette di aspettare la serratura o fifo,
- o canali diversi, per emettere le informazioni e quindi assemblare l'uscita in una sequenza corretta.
Seguire l'ultimo percorso potrebbe apparire come (nota che fa l' printf
ultimo perché funziona meglio qui):
_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
uscite
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Perché è corretto?
_passback x
parla direttamente con STDOUT.
- Tuttavia, poiché STDOUT deve essere catturato nel comando interno, prima lo "salviamo" in FD3 (puoi usarne altri, ovviamente) con '3> & 1' e poi lo riutilizziamo con
>&3
.
- Il
$("${@:2}" 3<&-; _passback x >&3)
finisce dopo _passback
, quando la subshell chiude STDOUT.
- Quindi
printf
non può accadere prima del _passback
, indipendentemente dal tempo _passback
impiegato.
- Nota che il
printf
comando non viene eseguito prima che l'intera riga di comando sia assemblata, quindi non possiamo vedere artefatti da printf
, indipendentemente da come printf
è implementato.
Quindi prima _passback
esegue, poi il file printf
.
Questo risolve la gara, sacrificando un descrittore di file fisso 3. Puoi, ovviamente, scegliere un altro descrittore di file nel caso in cui FD3 non sia libero nel tuo shellscript.
Si prega di notare anche il 3<&-
che protegge l'FD3 da passare alla funzione.
Rendilo più generico
_capture
contiene parti che appartengono a d()
, il che è negativo dal punto di vista della riutilizzabilità. Come risolverlo?
Bene, fallo in modo disperato introducendo un'altra cosa, una funzione aggiuntiva, che deve restituire le cose giuste, che prende il nome dalla funzione originale con _
allegato.
Questa funzione è chiamata dopo la funzione reale e può aumentare le cose. In questo modo, questo può essere letto come un'annotazione, quindi è molto leggibile:
_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
stampa ancora
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Consenti l'accesso al codice di ritorno
Manca solo un bit:
v=$(fn)
imposta $?
ciò che è fn
tornato. Quindi probabilmente lo vuoi anche tu. Tuttavia, ha bisogno di qualche ritocco più grande:
# 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
stampe
23 42 69 FAIL
C'è ancora molto margine di miglioramento
_passback()
può essere eliminato con passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
può essere eliminato con capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
La soluzione inquina un descrittore di file (qui 3) utilizzandolo internamente. Devi tenerlo a mente se ti capita di superare FD.
Nota che bash
4.1 e versioni successive devono {fd}
utilizzare alcuni FD inutilizzati.
(Forse aggiungerò una soluzione qui quando torno in giro.)
Nota che questo è il motivo per cui lo metto in funzioni separate come _capture
, perché è possibile inserire tutto in una riga, ma rende sempre più difficile leggere e capire
Forse vuoi catturare anche STDERR della funzione chiamata. Oppure vuoi anche passare dentro e fuori più di un descrittore di file da e per le variabili.
Non ho ancora una soluzione, tuttavia qui è un modo per catturare più di un FD , quindi probabilmente possiamo anche restituire le variabili in questo modo.
Inoltre, non dimenticare:
Questo deve chiamare una funzione di shell, non un comando esterno.
Non esiste un modo semplice per passare le variabili di ambiente dai comandi esterni. (Con LD_PRELOAD=
dovrebbe essere possibile, però!) Ma questo è qualcosa di completamente diverso.
Ultime parole
Questa non è l'unica soluzione possibile. È un esempio di una soluzione.
Come sempre hai molti modi per esprimere le cose nella shell. Quindi sentiti libero di migliorare e trovare qualcosa di meglio.
La soluzione qui presentata è piuttosto lontana dall'essere perfetta:
- Non era quasi affatto testato, quindi perdona gli errori di battitura.
- C'è molto margine di miglioramento, vedi sopra.
- Utilizza molte funzionalità dal moderno
bash
, quindi probabilmente è difficile da portare su altre shell.
- E potrebbero esserci alcune stranezze a cui non ho pensato.
Tuttavia penso che sia abbastanza facile da usare:
- Aggiungi solo 4 righe di "libreria".
- Aggiungi solo 1 riga di "annotazione" per la tua funzione di shell.
- Sacrifica temporaneamente un solo descrittore di file.
- E ogni passaggio dovrebbe essere facile da capire anche a distanza di anni.