Pasar argumentos por referencia
-
22-08-2019 - |
Pregunta
Quiero preguntar si es posible pasar argumentos a una función de script por referencia:
es decir. hacer algo que se vería así en C ++:
void boo(int &myint) { myint = 5; }
int main() {
int t = 4;
printf("%d\n", t); // t->4
boo(t);
printf("%d\n", t); // t->5
}
Así que en BASH quiero hacer algo como:
function boo ()
{
var1=$1 # now var1 is global to the script but using it outside
# this function makes me lose encapsulation
local var2=$1 # so i should use a local variable ... but how to pass it back?
var2='new' # only changes the local copy
#$1='new' this is wrong of course ...
# ${!1}='new' # can i somehow use indirect reference?
}
# call boo
SOME_VAR='old'
echo $SOME_VAR # -> old
boo "$SOME_VAR"
echo $SOME_VAR # -> new
Cualquier idea sería apreciada.
Solución
Es 2018, y esta cuestión merece una actualización. Por lo menos en Bash, a partir del golpe de 4,3-alfa, puede utilizar namerefs para pasar argumentos por referencia:
function boo()
{
local -n ref=$1
ref='new'
}
SOME_VAR='old'
echo $SOME_VAR # -> old
boo SOME_VAR
echo $SOME_VAR # -> new
Las piezas críticas aquí son:
-
Al pasar el nombre de la variable a abuchear, no su valor:.
boo SOME_VAR
, noboo $SOME_VAR
-
Dentro de la función, usando
local -n ref=$1
para declarar un nameref para la variable nombrada por$1
, lo que significa que no es una referencia a$1
en sí, sino más bien a una variables cuyo nombre$1
se mantiene, es decirSOME_VAR
en nuestro caso. El valor en el lado derecho debe ser sólo una cadena con el nombre de una variable existente: no importa cómo se obtiene la cadena, así que cosas comolocal -n ref="my_var"
olocal -n ref=$(get_var_name)
funcionarían también.declare
también puede reemplazarlocal
en contextos que permiten / requerir que. Ver capítulo sobre Parámetros de Shell en el Manual de Referencia de Bash para más información.
La ventaja de este enfoque es (posiblemente) mejor legibilidad y, lo más importante, evitando eval
, cuyas trampas de seguridad son muchos y bien documentada.
Otros consejos
Desde la página de manual Bash (expansión de parámetros):
If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion.
Por lo tanto, una referencia es el nombre de la variable. Aquí es una función utilizando swap
indirección variable que no requiere una variable temporal:
function swap()
{ #
# @param VARNAME1 VARNAME2
#
eval "$1=${!2} $2=${!1}"
}
$ a=1 b=2
$ swap a b
$ echo $a $b
2 1
Utilice un upvar
función auxiliar:
# Assign variable one scope above the caller.
# Usage: local "$1" && upvar $1 value [value ...]
# Param: $1 Variable name to assign value to
# Param: $* Value(s) to assign. If multiple values, an array is
# assigned, otherwise a single value is assigned.
# NOTE: For assigning multiple variables, use 'upvars'. Do NOT
# use multiple 'upvar' calls, since one 'upvar' call might
# reassign a variable to be used by another 'upvar' call.
# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
upvar() {
if unset -v "$1"; then # Unset & validate varname
if (( $# == 2 )); then
eval $1=\"\$2\" # Return single value
else
eval $1=\(\"\${@:2}\"\) # Return array
fi
fi
}
Y utilizar de esta manera desde dentro Newfun()
:
local "$1" && upvar $1 new
Para devolver múltiples variables, utilizar otra función auxiliar upvars
. Esto permite pasar múltiples variables dentro de una llamada, evitando así posibles conflictos si una llamada upvar
cambia una variable utilizada en otra llamada upvar
posterior.
Ver: http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference para upvars
función de ayuda y más información.
El problema:
eval $1=new
es que no es seguro si $1
pasa a contener un comando:
set -- 'ls /;true'
eval $1=new # Oops
Sería mejor utilizar printf -v
:
printf -v "$1" %s new
Pero printf -v
no puede asignar matrices.
Por otra parte, tanto eval
y printf
no funcionarán si la variable pasa a ser declarado local
:
g() { local b; eval $1=bar; } # WRONG
g b # Conflicts with `local b'
echo $b # b is empty unexpected
El conflicto permanece allí incluso si es local b
unset
:
g() { local b; unset b; eval $1=bar; } # WRONG
g b # Still conflicts with `local b'
echo $b # b is empty unexpected
He encontrado una manera de hacer esto, pero no estoy seguro de cómo corregir esto es:
Newfun()
{
local var1="$1"
eval $var1=2
# or can do eval $1=2 if no local var
}
var=1
echo var is $var # $var = 1
newfun 'var' # pass the name of the variable…
echo now var is $var # $var = 2
Así que pasamos el nombre de la variable en comparación con el valor y luego utilizar eval ...
Bash no tiene nada como referencias construido en él, así que básicamente la única manera de que sería capaz de hacer lo que quiere es pasar la función del nombre de la variable global que desea que modificar. E incluso entonces se necesita de una declaración eval
:
boo() {
eval ${1}="new"
}
SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new
No creo que se puede utilizar referencias indirectas aquí porque Bash accede automáticamente el valor de la variable cuyo nombre se almacena en la referencia indirecta. Que no le da la oportunidad de configurarlo.
Ok, por lo que esta cuestión ha estado esperando una solución 'real' desde hace algún tiempo, y me alegra decir que ahora podemos lograr esto sin usar eval en absoluto.
La clave para recordar es declarar una referencia tanto en la persona que llama como el destinatario de la llamada, por lo menos en mi ejemplo:
#!/bin/bash
# NOTE this does require a bash version >= 4.3
set -o errexit -o nounset -o posix -o pipefail
passedByRef() {
local -n theRef
if [ 0 -lt $# ]; then
theRef=$1
echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theRef}"
# now that we have a reference, we can assign things to it
theRef="some other value"
echo -e "${FUNCNAME}:\n\tvalue of my reference set to:\n\t\t${theRef}"
else
echo "Error: missing argument"
exit 1
fi
}
referenceTester() {
local theVariable="I am a variable"
# note the absence of quoting and escaping etc.
local -n theReference=theVariable
echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theReference}"
passedByRef theReference
echo -e "${FUNCNAME}:\n\tthe value of my reference is now:\n\t\t${theReference},\n\tand the pointed to variable:\n\t\t${theVariable}"
}
# run it
referenceTester
Eval nunca debe ser usado en una cadena de caracteres que un usuario puede establecer porque su peligroso. Algo así como "cadena; rm -rf ~" será malo. Por lo general todo lo posible para encontrar soluciones en las que usted no tiene que preocuparse por ello.
Sin embargo, será necesario eval para establecer las variables pasadas, como el comentario señaló.
$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4
#!/bin/bash
append_string()
{
if [ -z "${!1}" ]; then
eval "${1}='$2'"
else
eval "${1}='${!1}''${!3}''$2'"
fi
}
PETS=''
SEP='|'
append_string "PETS" "cat" "SEP"
echo "$PETS"
append_string "PETS" "dog" "SEP"
echo "$PETS"
append_string "PETS" "hamster" "SEP"
echo "$PETS"
Salida:
cat
cat|dog
cat|dog|hamster
Estructura para llamar esa función es:
append_string name_of_var_to_update string_to_add name_of_var_containing_sep_char
Nombre de la variable se pasa a fuction acerca de las mascotas y la SEP, mientras que la cadena para anexar se pasa de la forma habitual como valor. "$ {! 1}" se refiere al contenido de la variable global PETS. En el principio de que la variable está vacía y se añade contens cada vez que llamamos a la función. carácter separador se puede seleccionar según sea necesario. "Eval" líneas que comienzan actualizar la variable PETS.
Esto es lo que funciona para mí en Ubuntu shell bash
#!/bin/sh
iteration=10
increment_count()
{
local i
i=$(($1+1))
eval ${1}=\$i
}
increment_count iteration
echo $iteration #prints 11
increment_count iteration
echo $iteration #prints 12