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

104

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 eno 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 test1función rety modificar la variable global también?

harrison4
fuente
¿Necesitas devolver el saludo? Podrías hacer eco de $ e para que vuelva. ¿O repetir todo lo que desea y luego analizar el resultado?

Respuestas:

98

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 variable ese 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:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Da:

Hello

Para un valor numérico de 0-255, puede usar returnpara pasar el número como estado de salida:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Da:

Hello - num is 4
Josh Jolly
fuente
Gracias por el punto, pero tengo que devolver una matriz de cadenas, y dentro de la función tengo que agregar elementos a dos matrices de cadenas globales.
harrison 4 de
3
Te das cuenta de que si simplemente ejecutas la función sin asignarla a una variable, todas las variables globales que contiene se actualizarán. En lugar de devolver una matriz de cadenas, ¿por qué no actualizar la matriz de cadenas en la función y luego asignarla a otra variable después de que la función haya finalizado?
@JohnDoe: no puede devolver una "matriz de cadenas" de una función. Todo lo que puede hacer es imprimir una cadena. Sin embargo, puede hacer algo como esto:setarray() { declare -ag "$1=(a b c)"; }
rici
34

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 %qesta 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=1000Tambié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 _capturesimplemente reemplazar todas las ocurrencias de 3otro 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 del 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 xaquí 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 destá involucrada una llamada .

Así que deja la xdistancia, 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 -nque no funciona en oder common bash3.x)

Evita cambiar d()

La última solución tiene grandes defectos:

  • d() necesita ser alterado
  • Necesita usar algunos detalles internos de xcapturepara 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" evalestamos 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 xestá ejecutando.
    • Puede ver esto usted mismo si agrega un sleep 1;antes printfo _passback. _xcapture a d; echoluego salidas xo aprimero, respectivamente.
  • No _passback xdeberí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 printfhaya 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 _passbacktarde.
  • Tenga en cuenta que el printfcomando 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 printfse implemente.

Por lo tanto, primero se _passbackejecuta, 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

_capturecontiene 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 fnregresó. 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 bash4.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.
Tino
fuente
2
eres increíble
Eliran Malka
14

Tal vez pueda usar un archivo, escribir en el archivo dentro de la función, leer desde el archivo posterior. He cambiado ea una matriz. En este ejemplo, los espacios en blanco se utilizan como separadores al leer la matriz.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Salida:

hi
first second third
first
second
third
Ashkan
fuente
13

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í

PradyJord
fuente
7

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

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

Entonces, en tu caso eso sería:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Funciona y no tiene restricciones sobre el "valor de retorno".

Elmar Zander
fuente
1

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 :

La sustitución de comandos, los comandos agrupados entre paréntesis y los comandos asincrónicos se invocan en un entorno de subcapa que es un duplicado del entorno de shell

Algún tipo programador
fuente
@JohnDoe No estoy seguro de que sea posible. Es posible que deba reconsiderar el diseño del guión.
Un tipo programador
Oh, pero necesito asignar una matriz global dentro de una función, si no, tendría que repetir mucho código (repetir el código de la función -30 líneas- 15 veces -una por llamada-). No hay otra forma, ¿no?
harrison 4 de
1

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:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

El inconveniente es que es posible que necesite varios archivos temporales para diferentes variables. Y también es posible que deba emitir un synccomando para conservar el contenido en el disco entre una operación de escritura y lectura.

Fabio
fuente
-1

Siempre puedes usar un alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Dino Dini
fuente