Saltar al contenido

¿Cómo modificar una variable global dentro de una función en bash?

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 o local -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 de 3 con otro número (más alto).

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.
  • 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 de printf o _passback.
      _xcapture a d; echo luego salidas x o a primero, respectivamente.
  • 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 de printf, independientemente de cómo printf 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 con passback() set -- "[email protected]" "$?"; 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) al usarlo internamente. Debe tener esto en cuenta si pasa las FD.
    Tenga en cuenta quebash 4.1 y superior tiene fd 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.

¡Haz clic para puntuar esta entrada!
(Votos: 2 Promedio: 3.5)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *