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


Estoy trabajando con esto:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

Tengo un script como el siguiente:

#!/bin/bash

e=2

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

test1 
echo "$e"

Que devuelve:

hello
4

Pero si se asigna 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"

Devuelve:

hello
2

He oído hablar de el 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 salvar el eco de la test1 función en ret y modificar la variable global también?

Author: Community, 2014-05-09

7 answers

Cuando se utiliza una sustitución de comandos (es decir, la construcción $(...)), se está creando una subcapa. Las subcells heredan variables de sus shell padre, pero esto solo funciona de una manera: una subcells no puede modificar el entorno de su shell padre. Su variable e se establece dentro de una subcapa, pero no en la shell principal. Hay dos maneras de pasar valores de una subcapa a su padre. Primero, puede enviar algo a stdout, luego capturarlo con un comando sustitución:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Da:

Hello

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

mysecondfunc() {
    echo "Hello"
    return 4
}

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

echo "$var - num is $num_var"

Da:

Hello - num is 4
 54
Author: Josh Jolly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-02 16:01:05

Lo que estás haciendo, estás ejecutando test1

$(test1)

En un sub-shell( shell hijo ) y los shell hijos no pueden modificar nada en el padre.

Se puede encontrar en bash manual

Por favor, Compruebe: Things results in a subshell aquí

 11
Author: PradyJord,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 05:17:25

Tal vez usted puede utilizar un archivo, escribir en el archivo dentro de la función, leer desde el archivo después de él. He cambiado e a una matriz. En este ejemplo, los espacios en blanco se utilizan como separador cuando se lee 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
 10
Author: Ashkan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-09 14:40:01

Tuve un problema similar, cuando quería 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, a la función. Por ejemplo,

#! /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 devuelto".

 4
Author: Elmar Zander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-15 21:47:31

Resumen

Su ejemplo se puede modificar 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:

  • También funciona para e=1000.
  • Conserva $? si necesita $?

Los únicos efectos secundarios malos son: {[89]]}

  • Necesita un bash moderno.
  • Se bifurca con bastante más frecuencia.
  • Necesita la anotación (nombrada como su función, con un añadido _)
  • Sacrifica el descriptor de fichero 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).

Lo siguiente (que es bastante largo, lo siento por eso) con suerte explica, cómo adpot 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 is

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 padres a los procesos hijos, pero no viceversa.

Si realiza la captura de salida, generalmente se ejecuta en una subcapa, por lo que es difícil devolver las variables.

Algunos incluso te dicen que es imposible arreglarlo. Esto está mal,pero es un problema difícil de resolver desde hace mucho tiempo.

Hay varias maneras de resolverlo mejor, esto depende de sus necesidades.

Aquí hay una guía paso a paso sobre cómo hacerlo.

Devolviendo variables al shell parental

Hay una manera de devolver variables a un shell parental. Sin embargo, este es un camino peligroso, porque utiliza eval. Si se hace incorrectamente, arriesgas 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

Impresiones

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945

Tenga en cuenta que esto funciona para peligrosos las cosas también:

danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"

Impresiones

; /bin/echo *

Esto se debe a printf '%q', que cita todo de tal manera, que se puede reutilizar en un contexto de shell de forma segura.

Pero esto es un dolor en la 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 shell, así que puedes mejorarlo. Solo piense en una interfaz que desea ver, y luego puede implementar se.

Aumentar, cómo el shell procesa las cosas

Vamos a dar un paso atrás y pensar en alguna API que nos permite expresar fácilmente, lo que queremos hacer.

Bueno, ¿qué queremos hacer con la función d()?

Queremos capturar la salida en una variable. OK, entonces vamos a implementar una API para exactamente esto:

# This needs a modern bash (see "help declare" if "-n" is present)
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}

Ahora, en lugar de escribir

d1=$(d)

Podemos escribir{[89]]}

capture d1 d

Bueno, esto parece que no hemos cambiado mucho, como, de nuevo, las variables no se devuelven desde d al shell padre, y necesitamos escribir un poco más.

Sin embargo, ahora podemos lanzar toda la potencia del shell, ya que está 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 errores siempre repetir en el contexto correcto. ¿Qué pasa si lo usas 1000 veces en un script y luego agregas una variable? Definitivamente no desea alterar todas las 1000 ubicaciones en las que está involucrada una llamada a d.

Así que deja el x lejos, para que podamos escribir:{[89]]}

_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.

Evite cambiar d()

La última solución tiene algunos grandes defectos: {[89]]}

  • d() necesita ser alterado
  • Necesita usar algunos detalles internos de xcapture para pasar la salida.
    • Tenga en cuenta que esta sombra (quema) una variable llamada output, así que nunca podremos devolverlo.
  • Necesita cooperar con {[42]]}

Podemos deshacernos de esto, también?

¡Por supuesto que podemos! Estamos en un caparazón, así que hay todo lo que necesitamos para hacer esto.

Si nos fijamos un poco más cerca de la llamada a eval se puede ver, que tenemos 100% control en esta ubicación. "Dentro" el eval estamos en una subcapa, así podemos hacer todo lo que queramos sin miedo a hacerle algo malo al caparazón parental.

Sí, bien, así que vamos a añadir otra envoltura, ahora directamente dentro de la 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

Impresiones

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    

Sin embargo, esto, de nuevo, tiene algunos inconvenientes importantes:{[89]]}

  • Los !DO NOT USE! marcadores están allí, porque hay una muy mala condición de carrera en este, que no puedes ver fácilmente:
    • El >(printf ..) es un trabajo en segundo plano. Así que podría aun ejecutar mientras el _passback x se está ejecutando.
    • Puede ver esto usted mismo si agrega un sleep 1; antes de printf o _passback. _xcapture a d; echo luego sale x o a primero, respectivamente.
  • El _passback x no debe ser parte de _xcapture, porque esto hace que sea difícil reutilizar esa receta.
  • También tenemos un tenedor sin nombre aquí (el {[57]]}), pero como esta solución es !DO NOT USE! Tomé la ruta más corta.

Sin embargo, esto muestra, que podemos hacerlo, sin modificación a d()!

Tenga en cuenta que no necesitamos necesariamente _xcapture en absoluto, como podríamos haber escrito todo derecho en el eval.

Sin embargo, hacer esto generalmente no es muy legible. Y si vuelves a tu guión en unos años, probablemente quieras volver a leerlo sin muchos problemas.

Arreglar la carrera

Ahora vamos a arreglar la condición de carrera.

El truco podría ser esperar hasta que printf haya cerrado su salida estándar, y luego la salida x.

Hay muchas maneras de archivar esto:{[89]]}

  • No se pueden usar tuberías de shell, porque las tuberías se ejecutan en procesos diferentes.
  • Se pueden usar archivos temporales,
  • o algo así como un archivo de bloqueo o un fifo. Esto permite esperar la cerradura o fifo,
  • o diferentes canales, para generar la información, y luego ensamblar la salida en alguna secuencia correcta.

Siguiendo el último camino podría verse como (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 "$@")"; }

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 reutilizarlo con >&3.
  • El $("${@:2}" 3<&-; _passback x >&3) termina después del _passback, cuando la subcapa cierra STDOUT.
  • Así que el printf no puede suceder antes del {[42]]}, sin importar cuánto tiempo _passback toma.
  • Tenga en cuenta que el comando printf no se ejecuta antes de la línea de comandos está ensamblada, por lo que no podemos ver artefactos de printf, independientemente de cómo se implementa printf.

Por lo tanto primero _passback ejecuta, luego el printf.

Esto resuelve la raza, sacrificando un descriptor de archivo fijo 3. Por supuesto, puede elegir otro descriptor de archivo en el caso, que FD3 no está libre en su shellscript.

Tenga en cuenta también el 3<&- que protege FD3 para ser pasado a la función.

Hacerlo más genérico

_capture contiene partes, que pertenecen a d(), que es malo, desde una perspectiva de reutilización. Cómo resolver esto?

Bueno, hazlo de la manera desparate 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. Este manera, esto se puede leer como alguna 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 $? a lo que fn devolvió. 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 "$@" "$?"; }
_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=42; y=69; echo FAIL; return 23; }

# And now the code which uses it all
x=0
y=0
capture wtf fails
echo $? $x $y $wtf

Impresiones

23 42 69 FAIL

Todavía hay mucho margen de mejora{[159]]}
  • La solución contamina un descriptor de archivo al usarlo internamente. En caso de que lo necesite en su script, debe tener mucho cuidado de no usar esto. Tal vez hay una manera de deshacerse de esto y reemplazarlo con un descriptor de archivo dinámico (libre).

  • Tal vez desee capturar STDERR de la función llamada, también. O te quieres pasar y salir más de una filedescriptor desde y hacia variables.

Tampoco olvides: {[89]]}

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 a comandos externos. (Con LD_PRELOAD= debería ser posible, sin embargo!) Pero esto es algo completamente diferente.

Últimas palabras

Esta no es la única solución posible. Es un ejemplo de solución.

Como siempre tienes muchas maneras de expresar las cosas en la cáscara. Así que siéntete libre de mejorar y encontrar algo mejor.

La solución presentada aquí está bastante lejos de ser perfecto:

  • Casi no fue testet en absoluto, así que por favor perdone los errores tipográficos.
  • Hay mucho margen de mejora, véase más arriba.
  • Utiliza muchas características de modern bash, por lo que probablemente es difícil de portar a otros shells.
  • Y puede haber algunas peculiaridades 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 shell función.
  • Sacrifica solo un descriptor de archivo temporalmente.
  • Y cada paso debe ser fácil de entender incluso años después.
 4
Author: Tino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-29 15:27:30

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 termina la subcapa.

Referencia:

La substitución de órdenes , las órdenes agrupadas entre paréntesis y las órdenes asíncronas se invocan en un entorno subshell que es un duplicado del entorno shell

 1
Author: Some programmer dude,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-09 12:51:39

Siempre puedes usar un alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
 -1
Author: Dino Dini,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-27 16:26:17