Este grupo redactor ha estado largas horas buscando para dar respuesta a tu duda, te regalamos la soluciones y esperamos que te sea de mucha apoyo.
Solución:
Cuando usa una sustitución de comando (es decir, el $(...)
construct), está creando una subcapa. Las subcapas heredan variables de sus capas principales, pero esto solo funciona de una manera: una subcapa no puede modificar el entorno de su capa principal.
Tu variable e
se establece dentro de una subcapa, pero no en la capa principal. Hay dos formas de pasar valores de una subcapa a su padre. Primero, puede enviar algo a la salida estándar, luego capturarlo con una sustitución de comando:
myfunc()
echo "Hello"
var="$(myfunc)"
echo "$var"
Los resultados anteriores:
Hello
Para un valor numérico en el rango de 0 a 255, puede usar return
para pasar el número como estado de salida:
mysecondfunc()
echo "Hello"
return 4
var="$(mysecondfunc)"
num_var=$?
echo "$var - num is $num_var"
Esto produce:
Hello - num is 4
Esto necesita bash 4.1 si usa
fd
olocal -n
.El resto debería funcionar en bash 3.x, espero. No estoy completamente seguro debido a
printf %q
– esta podría ser una característica 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 "[email protected]" "$?";
_capture() out="$("$@:2" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; 3>&1; echo "(exit $ret)";
capture() eval "$(_capture "[email protected]")";
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:
- Trabaja para
e=1000
, también. - Conservas
$?
si necesitas$?
Los únicos efectos secundarios negativos son:
- Necesita un moderno
bash
. - Se bifurca con bastante más frecuencia.
- Necesita la anotación (que lleva 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 reemplace todas las ocurrencias de3
con otro número (más alto).
- En
- Puede cambiarlo a otro FD si lo necesita.
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 secundarios, pero no al revés.
Si realiza la 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, se arriesga a muchas cosas malas. Pero si se hace correctamente, esto 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 es debido a printf '%q'
, que cita todo de modo que pueda 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 caparazón 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 el 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="$("[email protected]")"
Ahora, en lugar de escribir
d1=$(d)
podemos escribir
capture d1 d
Bueno, parece que no hemos cambiado mucho, ya que, de nuevo, las variables no se devuelven desde d
en 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
los 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 una llamada d
esta involucrado.
Así que deja el x
de distancia, para que podamos 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 común bash
3.x)
Evita cambiar d()
La última solución tiene grandes defectos:
d()
necesita ser alterado- Necesita utilizar algunos detalles internos de
xcapture
para pasar la salida.- Tenga en cuenta que esta sombrea (quema) una variable llamada
output
, por lo que nunca podremos devolver este.
- Tenga en cuenta que esta sombrea (quema) una variable llamada
- 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 a eval
Como puede ver, tenemos el 100% de control en esta ubicación. “Dentro de eval
estamos en un subshell, por lo que podemos hacer todo lo que queramos sin miedo a hacerle algo malo al shell parental.
Sí, bien, agreguemos otro contenedor, ahora directamente dentro del 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 "[email protected]")";
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 algunos inconvenientes importantes:
- los
!DO NOT USE!
Los marcadores están ahí, porque hay una condición de carrera muy mala en esto, que no se puede ver fácilmente:- los
>(printf ..)
es un trabajo de fondo. Por lo que aún podría ejecutarse mientras el_passback x
Esta corriendo. - Puede ver esto usted mismo si agrega un
sleep 1;
antes deprintf
o_passback
.
_xcapture a d; echo
luego salidasx
oa
primero, respectivamente.
- los
- los
_passback x
no 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 necesitamos necesariamente _xcapture
en absoluto, ya que podríamos haber escrito todo justo en el eval
.
Sin embargo, hacer esto generalmente no es muy legible. Y si vuelve a su guión en unos años, probablemente quiera poder leerlo de nuevo sin muchos problemas.
Arregla la carrera
Ahora arreglemos la condición de carrera.
El truco podría ser esperar hasta printf
ha cerrado su STDOUT, y luego la salida 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 canales diferentes, para generar la información y luego ensamblar la salida en una secuencia correcta.
Seguir el último camino podría verse así (tenga en cuenta que hace el printf
último 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 "[email protected]")";
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
. - los
$("$@:2" 3<&-; _passback x >&3)
termina después de la_passback
, cuando la subcapa cierra STDOUT. - Entonces el
printf
no puede suceder antes del_passback
, sin importar cuánto tiempo_passback
acepta. - Tenga en cuenta que el
printf
El comando no se ejecuta antes de ensamblar la línea de comandos completa, por lo que no podemos ver los artefactos deprintf
, independientemente de cómoprintf
está implementado.
Por lo tanto, primero _passback
ejecuta, entonces 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 el 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 la perspectiva de la 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 _
adjunto.
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 "[email protected]")";
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)
conjuntos $?
a qué fn
regresó. Así que probablemente también quieras esto. Sin embargo, necesita algunos ajustes más grandes:
# 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 "[email protected]" "$?";
_capture() out="$("$@:2" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; 3>&1; echo "(exit $ret)";
capture() eval "$(_capture "[email protected]")";
# 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 conpassback() set -- "[email protected]" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "$!1"; shift; done; return $1;
-
_capture()
se puede eliminar concapture() 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) al usarlo internamente. Debe tener esto en cuenta si pasa las FD.
Tenga en cuenta quebash
4.1 y superior tienefd
para usar algunos FD sin usar.
(Quizás agregue una solución aquí cuando vuelva).
Tenga en cuenta que esta es la razón por la que solía ponerlo en funciones separadas como_capture
, porque es posible agrupar todo esto en una sola línea, pero hace que sea cada vez más difícil 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 archivos 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. (Con
LD_PRELOAD=
¡Sin 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 que se presenta 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 trasladar 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.
Lo que estás haciendo, estás ejecutando test1
$(test1)
en un sub-shell (shell hijo) y Los shells secundarios no pueden modificar nada en el padre.
Lo puedes encontrar en bash manual
Por favor, compruebe: los resultados de las cosas en una subcapa aquí
Puntuaciones y reseñas
Si eres capaz, tienes la opción de dejar un tutorial acerca de qué le añadirías a este tutorial.