Estoy trabajando con esto:
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Tengo un guión como el siguiente:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
test1
echo "$e"
Que devuelve:
hello
4
Pero si asigno el resultado de la función a una variable, la variable global e
no se modifica:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
ret=$(test1)
echo "$ret"
echo "$e"
Devoluciones:
hello
2
He oído hablar del uso de eval en este caso, así que hice esto en test1
:
eval 'e=4'
Pero el mismo resultado.
¿Podría explicarme por qué no se modifica? ¿Cómo podría guardar el eco de la test1
función ret
y modificar la variable global también?
bash
variables
global-variables
eval
harrison4
fuente
fuente
Respuestas:
Cuando usa una sustitución de comando (es decir, la
$(...)
construcción), está creando una subcapa. Las subcapas heredan las variables de sus capas principales, pero esto solo funciona de una manera: una subcapa no puede modificar el entorno de su capa principal. Su variablee
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 stdout, luego capturarlo con una sustitución de comando:Da:
Para un valor numérico de 0-255, puede usar
return
para pasar el número como estado de salida:Da:
fuente
setarray() { declare -ag "$1=(a b c)"; }
Resumen
Su ejemplo puede modificarse de la siguiente manera para archivar el efecto deseado:
imprime como desee:
Tenga en cuenta que esta solución:
e=1000
También funciona para .$?
si lo necesitas$?
Los únicos efectos secundarios negativos son:
bash
._
)_capture
simplemente reemplazar todas las ocurrencias de3
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
salidas
mientras que la salida deseada es
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 enbash
.huellas dactilares
Tenga en cuenta que esto también funciona para cosas peligrosas:
huellas dactilares
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:
Ahora, en lugar de escribir
podemos escribir
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
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 qued
está involucrada una llamada .Así que deja la
x
distancia, así podemos escribir:salidas
Esto ya se ve muy bien. (Pero todavía existe el
local -n
que no funciona en oder commonbash
3.x)Evita cambiar
d()
La última solución tiene grandes defectos:
d()
necesita ser alteradoxcapture
para pasar la salida.output
, por lo que nunca podremos devolver esta._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
:huellas dactilares
Sin embargo, esto, nuevamente, tiene un gran inconveniente:
!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:>(printf ..)
es un trabajo en segundo plano. Por lo tanto, es posible que aún se ejecute mientras se_passback x
está ejecutando.sleep 1;
antesprintf
o_passback
._xcapture a d; echo
luego salidasx
oa
primero, respectivamente._passback x
debería ser parte de_xcapture
, porque esto dificulta la reutilización de esa receta.$(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 sinlocal -n
)!Tenga en cuenta que no es necesario que lo necesitemos
_xcapture
, ya que podríamos haber escrito todo directamente en eleval
.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 salirx
.Hay muchas formas de archivar esto:
Seguir la última ruta podría verse así (tenga en cuenta que hace la
printf
última porque esto funciona mejor aquí):salidas
¿Por qué es esto correcto?
_passback x
habla directamente con STDOUT.>&3
.$("${@:2}" 3<&-; _passback x >&3)
termina después de_passback
, cuando la subcapa cierra STDOUT.printf
, no puede suceder antes del_passback
, independientemente del tiempo que_passback
tarde.printf
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
se implemente.Por lo tanto, primero se
_passback
ejecuta, luego elprintf
.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 ad()
, 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:
todavía imprime
Permitir el acceso al código de retorno
Solo falta un bit:
v=$(fn)
establece$?
lo quefn
regresó. Así que probablemente también quieras esto. Sin embargo, necesita algunos ajustes más importantes:huellas dactilares
Todavía hay mucho margen de mejora
_passback()
se puede eliminar conpassback() { set -- "$@" "$?"; 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) 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 comprenderQuizá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.
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:
bash
, por lo que probablemente sea difícil de migrar a otros shells.Sin embargo, creo que es bastante fácil de usar:
fuente
Tal vez pueda usar un archivo, escribir en el archivo dentro de la función, leer desde el archivo posterior. He cambiado
e
a una matriz. En este ejemplo, los espacios en blanco se utilizan como separadores al leer la matriz.Salida:
fuente
Lo que estás haciendo, estás ejecutando test1
$(test1)
en un sub-shell (shell secundario) y los shells secundarios no pueden modificar nada en el padre .
Puedes encontrarlo en el manual de bash
Por favor, compruebe: los resultados de las cosas en un subshell aquí
fuente
Tuve un problema similar cuando quise eliminar automáticamente los archivos temporales que había creado. La solución que se me ocurrió no fue usar la sustitución de comandos, sino pasar el nombre de la variable, que debería tomar el resultado final, en la función. P.ej
Entonces, en tu caso eso sería:
Funciona y no tiene restricciones sobre el "valor de retorno".
fuente
Esto se debe a que la sustitución de comandos se realiza en una subcapa, por lo que mientras la subcapa hereda las variables, los cambios en ellas se pierden cuando finaliza la subcapa.
Referencia :
fuente
Una solución a este problema, sin tener que introducir funciones complejas y modificar en gran medida la original, es almacenar el valor en un archivo temporal y leerlo / escribirlo cuando sea necesario.
Este enfoque me ayudó mucho cuando tuve que simular una función de bash llamada varias veces en un caso de prueba de murciélagos.
Por ejemplo, podrías tener:
El inconveniente es que es posible que necesite varios archivos temporales para diferentes variables. Y también es posible que deba emitir un
sync
comando para conservar el contenido en el disco entre una operación de escritura y lectura.fuente
Siempre puedes usar un alias:
fuente