Esto necesita bash 4.1 si usa {fd}
o local -n
.
El resto debería funcionar en bash 3.x, espero. No estoy completamente seguro debido a que printf %q
esta podría ser una función de bash 4.
Resumen
Su ejemplo puede modificarse de la siguiente manera para archivar el efecto deseado:
# 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 como desee:
hello
4
Tenga en cuenta que esta solución:
e=1000
También funciona para .
- Conserva
$?
si lo necesitas$?
Los únicos efectos secundarios negativos son:
- Necesita un moderno
bash
.
- Se bifurca con bastante más frecuencia.
- Necesita la anotación (el nombre de su función, con un agregado
_
)
- Sacrifica el descriptor de archivo 3.
- Puede cambiarlo a otro FD si lo necesita.
- En
_capture
simplemente reemplazar todas las ocurrencias de 3
otro número (superior).
Con suerte, lo siguiente (que es bastante largo, lo siento) explica cómo adaptar esta receta a otros scripts también.
El 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
salidas
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
mientras que la salida deseada es
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
La causa del problema
Las variables de shell (o en general, el entorno) se pasan de los procesos parentales a los procesos hijos, pero no al revés.
Si realiza una captura de salida, esto generalmente se ejecuta en una subcapa, por lo que devolver las variables es difícil.
Algunos incluso te dicen que es imposible de arreglar. Esto está mal, pero es un problema difícil de resolver desde hace mucho tiempo.
Hay varias formas de cómo solucionarlo mejor, esto depende de tus necesidades.
Aquí hay una guía paso a paso sobre cómo hacerlo.
Pasando variables al shell parental
Hay una forma de devolver variables a un shell parental. Sin embargo, este es un camino peligroso, porque utiliza eval
. Si se hace incorrectamente, corre el riesgo de muchas cosas malas. Pero si se hace correctamente, es perfectamente seguro, siempre que no haya ningún error en 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
huellas dactilares
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Tenga en cuenta que esto también funciona para cosas peligrosas:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
huellas dactilares
; /bin/echo *
Esto se debe a printf '%q'
, que cita todo eso, que puede reutilizarlo en un contexto de shell de forma segura.
Pero esto es un dolor en el a ..
Esto no solo se ve feo, también es mucho para escribir, por lo que es propenso a errores. Un solo error y estás condenado, ¿verdad?
Bueno, estamos a nivel de caparazón, así que puedes mejorarlo. Solo piense en una interfaz que desee ver y luego podrá implementarla.
Aumento, cómo el shell procesa las cosas.
Demos un paso atrás y pensemos en alguna API que nos permita expresar fácilmente lo que queremos hacer.
Bueno, ¿qué queremos hacer con la d()
función?
Queremos capturar la salida en una variable. Bien, entonces implementemos una API exactamente para esto:
# 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="$("$@")"
}
Ahora, en lugar de escribir
d1=$(d)
podemos escribir
capture d1 d
Bueno, esto parece que no hemos cambiado mucho, ya que, de nuevo, las variables no se devuelven desde d
el shell padre y necesitamos escribir un poco más.
Sin embargo, ahora podemos lanzarle toda la potencia del shell, ya que está muy bien envuelto en una función.
Piense en una interfaz fácil de reutilizar
Una segunda cosa es que queremos estar SECOS (No te repitas). Así que definitivamente no queremos escribir algo como
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
El x
aquí no solo es redundante, es propenso a cometer errores siempre en el contexto correcto. ¿Qué pasa si lo usa 1000 veces en un script y luego agrega una variable? Definitivamente no desea alterar todas las 1000 ubicaciones a las que d
está involucrada una llamada .
Así que deja la x
distancia, así podemos escribir:
_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
salidas
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Esto ya se ve muy bien. (Pero todavía existe el local -n
que no funciona en oder common bash
3.x)
Evita cambiar d()
La última solución tiene grandes defectos:
d()
necesita ser alterado
- Necesita usar algunos detalles internos de
xcapture
para pasar la salida.
- Tenga en cuenta que esto sombrea (quema) una variable nombrada
output
, por lo que nunca podremos devolver esta.
- Necesita cooperar con
_passback
¿Podemos deshacernos de esto también?
¡Por supuesto que podemos! Estamos en un caparazón, por lo que hay todo lo que necesitamos para hacer esto.
Si miras un poco más cerca de la llamada eval
, puedes ver que tenemos un control del 100% en esta ubicación. "Dentro" eval
estamos en un subshell, por lo que podemos hacer todo lo que queramos sin miedo a hacerle algo malo al caparazón parental.
Sí, bien, agreguemos otro contenedor, ahora directamente dentro de 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
huellas dactilares
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Sin embargo, esto, nuevamente, tiene un gran inconveniente:
- Los
!DO NOT USE!
marcadores están ahí, porque hay una condición de carrera muy mala en esto, que no se puede ver fácilmente:
- El
>(printf ..)
es un trabajo en segundo plano. Por lo tanto, es posible que aún se ejecute mientras se _passback x
está ejecutando.
- Puede ver esto usted mismo si agrega un
sleep 1;
antes printf
o _passback
.
_xcapture a d; echo
luego salidas x
o a
primero, respectivamente.
- No
_passback x
debería ser parte de _xcapture
, porque esto dificulta la reutilización de esa receta.
- También tenemos una bifurcación innecesaria aquí (la
$(cat)
), pero como esta solución es !DO NOT USE!
, tomé la ruta más corta.
Sin embargo, esto demuestra que podemos hacerlo, ¡sin modificar d()
(y sin local -n
)!
Tenga en cuenta que no es necesario que lo necesitemos _xcapture
, ya que podríamos haber escrito todo directamente en el eval
.
Sin embargo, hacer esto generalmente no es muy legible. Y si vuelve a su guión dentro de unos años, probablemente quiera poder leerlo de nuevo sin muchos problemas.
Arreglar la carrera
Ahora arreglemos la condición de carrera.
El truco podría ser esperar hasta que printf
haya cerrado su STDOUT y luego salir x
.
Hay muchas formas de archivar esto:
- No puede utilizar tuberías de shell, porque las tuberías se ejecutan en diferentes procesos.
- Uno puede usar archivos temporales,
- o algo así como un archivo de bloqueo o un quince. Esto permite esperar la cerradura o quince,
- o diferentes canales, para dar salida a la información y luego ensamblar la salida en una secuencia correcta.
Seguir la última ruta podría verse así (tenga en cuenta que hace la printf
última porque esto funciona mejor aquí):
_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
salidas
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
¿Por qué es esto correcto?
_passback x
habla directamente con STDOUT.
- Sin embargo, como STDOUT necesita ser capturado en el comando interno, primero lo "guardamos" en FD3 (puede usar otros, por supuesto) con '3> & 1' y luego lo reutilizamos con
>&3
.
- El
$("${@:2}" 3<&-; _passback x >&3)
termina después de _passback
, cuando la subcapa cierra STDOUT.
- Por lo tanto
printf
, no puede suceder antes del _passback
, independientemente del tiempo que _passback
tarde.
- Tenga en cuenta que el
printf
comando no se ejecuta antes de ensamblar la línea de comandos completa, por lo que no podemos ver los artefactos de printf
, independientemente de cómo printf
se implemente.
Por lo tanto, primero se _passback
ejecuta, luego el printf
.
Esto resuelve la carrera, sacrificando un descriptor de archivo fijo 3. Puede, por supuesto, elegir otro descriptor de archivo en el caso de que FD3 no esté libre en su shellscript.
Tenga en cuenta también 3<&-
que protege FD3 para pasar a la función.
Hazlo más genérico
_capture
contiene partes, que pertenecen a d()
, lo cual es malo, desde una perspectiva de reutilización. ¿Cómo solucionar esto?
Bueno, hágalo de la manera desesperada introduciendo una cosa más, una función adicional, que debe devolver las cosas correctas, que lleva el nombre de la función original con _
adjunta.
Esta función se llama después de la función real y puede aumentar cosas. De esta manera, esto se puede leer como una anotación, por lo que es muy legible:
_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
todavía imprime
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Permitir el acceso al código de retorno
Solo falta un bit:
v=$(fn)
establece $?
lo que fn
regresó. Así que probablemente también quieras esto. Sin embargo, necesita algunos ajustes más importantes:
# 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
huellas dactilares
23 42 69 FAIL
Todavía hay mucho margen de mejora
_passback()
se puede eliminar con passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
se puede eliminar con capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
La solución contamina un descriptor de archivo (aquí 3) usándolo internamente. Debe tener eso en cuenta si pasa las FD.
Tenga en cuenta que bash
4.1 y superior tiene {fd}
que utilizar algunos FD no utilizados.
(Quizás agregue una solución aquí cuando vuelva).
Tenga en cuenta que es por eso que solía ponerlo en funciones separadas como _capture
, porque es posible colocar todo esto en una línea, pero hace que sea cada vez más difícil de leer y comprender
Quizás también desee capturar STDERR de la función llamada. O incluso desea pasar más de un descriptor de archivo desde y hacia variables.
Todavía no tengo una solución, sin embargo, aquí hay una forma de capturar más de un FD , por lo que probablemente también podamos devolver las variables de esta manera.
Además, no olvides:
Esto debe llamar a una función de shell, no a un comando externo.
No hay una manera fácil de pasar variables de entorno de comandos externos. (¡Sin LD_PRELOAD=
embargo, debería ser posible!) Pero esto es algo completamente diferente.
Ultimas palabras
Ésta no es la única solución posible. Es un ejemplo de solución.
Como siempre, tienes muchas formas de expresar las cosas en el caparazón. Así que siéntete libre de mejorar y encontrar algo mejor.
La solución presentada aquí está bastante lejos de ser perfecta:
- Casi no se ha probado en absoluto, así que perdone los errores tipográficos.
- Hay mucho margen de mejora, véase más arriba.
- Utiliza muchas características de las modernas
bash
, por lo que probablemente sea difícil de migrar a otros shells.
- Y puede haber algunas peculiaridades en las que no he pensado.
Sin embargo, creo que es bastante fácil de usar:
- Agregue solo 4 líneas de "biblioteca".
- Agregue solo 1 línea de "anotación" para su función de shell.
- Sacrifica solo un descriptor de archivo temporalmente.
- Y cada paso debería ser fácil de entender incluso años después.