scripts script programas operaciones manejo español ejemplos comando cadenas aritmeticas bash variables global-variables eval

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



shell script linux español (7)

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 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 lo hice 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 función test1 en ret y modificar también la variable global?


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 según lo deseado:

hello 4

Tenga en cuenta que esta solución:

  • Funciona para e=1000 , también.
  • ¿Conserva $? si necesitas $?

Los únicos efectos secundarios negativos son:

  • Necesita un bash moderno.
  • Se bifurca con bastante más frecuencia.
  • Necesita la anotación (nombrada después de su función, con un _ añadido)
  • Sacrifica el descriptor de archivo 3.
    • Puedes cambiarlo a otro FD si lo necesitas.
      • En _capture simplemente reemplaza todas las ocurrencias de 3 con otro número (más alto).

Lo siguiente (que es bastante largo, lo siento) explica con suerte cómo agregar 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 el resultado deseado es

4 20171129-123521 20171129-123521 20171129-123521 20171129-123521

La causa del problema

Las variables de Shell (o, en términos generales, el entorno) se pasan de procesos parentales a procesos secundarios, pero no al revés.

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

Algunos incluso te dicen que es imposible de arreglar. Esto está mal, pero es un problema conocido y difícil de resolver.

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

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

Pasar las variables al shell parental

Hay una manera de devolver las variables a un shell parental. Sin embargo, este es un camino peligroso, porque esto utiliza eval . Si se hace incorrectamente, arriesgas muchas cosas malvadas. Pero si se realiza correctamente, esto es perfectamente seguro, siempre que no haya errores 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 lo cita todo de manera que puede volver a utilizarlo en un contexto de shell de forma segura.

Pero esto es un dolor en el a ...

Esto no solo parece feo, también es mucho para escribir, por lo que es propenso a errores. Solo un error y estás condenado, ¿verdad?

Bueno, estamos a nivel de caparazón, por lo que puede mejorarlo. Solo piensa en una interfaz que quieras ver y luego puedes implementarla.

Aumentar, cómo el caparazón procesa las cosas

Retrocedamos un poco y pensemos en alguna API que nos permita expresar fácilmente lo que queremos hacer.

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

Queremos capturar el resultado en una variable. OK, entonces implementemos una API para esto exactamente:

# 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

capture d1 d

Bueno, parece que no hemos cambiado mucho, ya que, una vez más, las variables no pasan de d al shell principal, y tenemos que escribir un poco más.

Sin embargo, ahora podemos tirar toda la potencia de la concha, ya que está muy bien envuelto en una función.

Piensa en una interfaz fácil de reutilizar

Una segunda cosa es que queremos estar SECOS (No repetir). Entonces 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

La x aquí no solo es redundante, sino que es propensa a errores y siempre se repite en el contexto correcto. ¿Qué sucede si lo usa 1000 veces en un script y luego agrega una variable? Definitivamente no quiere alterar todas las 1000 ubicaciones en las que está involucrada una llamada a d .

Así que deje la x , 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.

Evite cambiar d()

La última solución tiene algunos grandes defectos:

  • 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 , por lo que nunca podremos revertirla.
  • 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 te fijas un poco más cerca de la llamada a la eval , puedes ver que tenemos el 100% de control en esta ubicación. "Dentro" del eval estamos en una subcategoría, por lo que podemos hacer todo lo que queramos sin temor a hacer algo malo en el caparazón parental.

Sí, lindo, así que agreguemos otro envoltorio, 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 "$@")"; } 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 inconveniente importante:

  • El !DO NOT USE! los marcadores están ahí, porque hay una muy mala condición de carrera en esto, que no se puede ver fácilmente:
    • El >(printf ..) es un trabajo de fondo. Por lo tanto, aún se puede ejecutar mientras se está ejecutando _passback x .
    • Puede ver esto usted mismo si agrega un sleep 1; antes de printf o _passback . _xcapture ad; echo _xcapture ad; echo luego saca x o a primero, respectivamente.
  • El _passback x no debe ser parte de _xcapture , ya que esto dificulta la reutilización de esa receta.
  • También tenemos algo de bifurcación innecesaria aquí (el $(cat) ), 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, ya que podríamos haber escrito todo bien en la eval .

Sin embargo, hacer esto generalmente no es muy legible. Y si vuelves a tu script en unos pocos años, probablemente quieras volver a leerlo sin demasiados problemas.

Arregla la carrera

Ahora arreglemos la condición de carrera.

El truco podría ser esperar hasta que printf haya cerrado su STDOUT, y luego sacar x .

Hay muchas formas de archivar esto:

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

Seguir la última ruta podría ser similar (tenga en cuenta que la última impresión es 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) finaliza después del _passback , cuando la subshell cierra STDOUT.
  • Por lo tanto, el printf no puede ocurrir antes del _passback , independientemente de cuánto tiempo _passback tome.
  • Tenga en cuenta que el comando printf no se ejecuta antes de ensamblar la línea de comando completa, por lo que no podemos ver artefactos de printf , independientemente de cómo se implemente printf .

Por lo tanto, primero se ejecuta _passback , luego printf .

Esto resuelve la carrera, 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 pasar a la función.

Hazlo 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 una manera despiadada 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 las 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

sigue imprimiendo

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Permitir el acceso al código de retorno

Solo falta un poco:

v=$(fn) establece $? a lo que fn regresó. Entonces 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

huellas dactilares

23 42 69 FAIL

Todavía hay mucho margen de mejora

  • La solución contamina un descriptor de archivo usándolo internamente. En caso de que lo necesite en su secuencia de comandos, debe tener mucho cuidado de no usar esto. Quizás haya una manera de deshacerse de esto y reemplazarlo con un descriptor de archivo dinámico (gratuito).

  • Quizás también desee capturar STDERR de la función llamada. O desea incluso ingresar y sacar más de un descriptor de archivo desde y hacia variables.

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

Ultimas palabras

Esta no es la única solución posible. Es un ejemplo de una 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 fue testet en absoluto, así que por favor perdone los errores tipográficos.
  • Hay mucho margen de mejora, ver arriba.
  • Utiliza muchas características del bash moderno, por lo que probablemente sea difícil portarlo a otros proyectiles.
  • Y puede haber algunos caprichos en los 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 un solo descriptor de archivo temporalmente.
  • Y cada paso debería ser fácil de entender, incluso años después.

Cuando utiliza una sustitución de comando (es decir, el constructo $(...) ), está creando una subshell. Las subcapas heredan las variables de sus shells primarios, pero esto solo funciona de una manera: una subshell no puede modificar el entorno de su shell primario. Su variable e se establece dentro de una subcadena, pero no el shell primario. Hay dos formas de pasar valores de una subshell a su principal. En primer lugar, 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 a 255, puede usar return para pasar el número como estado de salida:

myotherfunc() { echo "Hello" return 4 } var="$(myotherfunc)" num_var=$? echo "$var - num is $num_var"

Da:

Hello - num is 4


Es porque la sustitución del comando se realiza en una subcadena, de modo que mientras la subshell hereda las variables, los cambios en ellas se pierden cuando la subshell termina.

Reference :

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


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

$(test1)

en un subconjunto (shell secundario) y las shells Child no pueden modificar nada en el elemento primario .

Puedes encontrarlo en el manual bash

Por favor, compruebe: las cosas resultan en una subshell here


Siempre puedes usar un alias:

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


Tal vez puedas usar un archivo, escribir en el archivo dentro de la función, leer de un archivo después de él. He cambiado e a 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


Tuve un problema similar cuando quería eliminar automáticamente los archivos temporales que había creado. La solución que se me ocurrió fue no utilizar la sustitución de comandos, sino pasar el nombre de la variable, que debería llevar el resultado final, a 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 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".