Passando argumentos por referência
-
22-08-2019 - |
Pergunta
Eu quero perguntar se é possível passar argumentos para uma função de script por referência:
i. para fazer algo que seria parecido com isso em 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
}
Então em bash Quero fazer 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
Qualquer pensamento seria apreciado.
Solução
É 2018, e esta questão merece uma atualização. Pelo menos em Bash, a partir das Bash 4.3-alpha, você pode usar namerefs para passar argumentos de função por referência:
function boo()
{
local -n ref=$1
ref='new'
}
SOME_VAR='old'
echo $SOME_VAR # -> old
boo SOME_VAR
echo $SOME_VAR # -> new
As peças críticas aqui são:
-
Passando o nome da variável a vaia, não o seu valor:.
boo SOME_VAR
, nãoboo $SOME_VAR
-
Dentro da função, usando
local -n ref=$1
para declarar um nameref para a variável chamada por$1
, o que significa que não é uma referência a$1
si, mas sim a um variável cujo nome$1
detém, ou sejaSOME_VAR
no nosso caso. O valor no lado da mão direita deve ser apenas uma string com o nome de uma variável existente: não importa como você começa a corda, então coisas comolocal -n ref="my_var"
oulocal -n ref=$(get_var_name)
iria trabalhar muito.declare
também pode substituirlocal
em contextos que permitem / exigem que. Consulte capítulo sobre Shell Parâmetros no Manual Bash Reference para mais informações.
A vantagem dessa abordagem é (discutivelmente) melhor legibilidade e, mais importante, evitando eval
, cujo armadilhas de segurança são muitas e bem documentado.
Outras dicas
Do homem-página Bash (Parâmetro de Expansão):
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.
Portanto, uma referência é o nome da variável. Aqui é uma função swap
usando
indireto variável que não requer uma variável temporária:
function swap()
{ #
# @param VARNAME1 VARNAME2
#
eval "$1=${!2} $2=${!1}"
}
$ a=1 b=2
$ swap a b
$ echo $a $b
2 1
Use uma função auxiliar upvar
:
# 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
}
E usá-lo como este de dentro Newfun()
:
local "$1" && upvar $1 new
Para retornar múltiplas variáveis, use outra função auxiliar upvars
. Isto permite passar múltiplas variáveis ??dentro de uma chamada, evitando assim possíveis conflitos se uma chamada upvar
muda uma variável usada em outra chamada upvar
posterior.
Veja: http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference para upvars
função auxiliar e obter mais informações.
O problema com:
eval $1=new
é que não é seguro se $1
acontece para conter um comando:
set -- 'ls /;true'
eval $1=new # Oops
Seria melhor usar printf -v
:
printf -v "$1" %s new
Mas printf -v
não pode matrizes atribuir.
eval
Além disso, tanto printf
e local
não vai funcionar se a variável passa a ser declarado:
g() { local b; eval $1=bar; } # WRONG
g b # Conflicts with `local b'
echo $b # b is empty unexpected
As estadias de conflito lá mesmo se 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
Eu encontrei uma maneira de fazer isso, mas eu não sei como corrigir isto é:
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
Então, passamos o nome da variável ao contrário do valor e, em seguida, usar eval ...
Bash não tem nada parecido com referências incorporadas a ele, então, basicamente a única maneira que você seria capaz de fazer o que você quer é passar a função o nome da variável global que você quer modificar. E mesmo assim você vai precisar de uma declaração eval
:
boo() {
eval ${1}="new"
}
SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new
Eu não acho que você pode usar referências indiretas aqui porque Bash acessa automaticamente o valor da variável cujo nome é armazenado na referência indireta. Ele não lhe dá a oportunidade de defini-lo.
Ok, assim que esta questão tem sido esperando por uma solução 'real' já há algum tempo, e eu estou contente de dizer que agora podemos fazer isso sem o uso de eval em tudo.
O tecla a lembrar é para declarar uma referência, tanto o chamador como o chamado, pelo menos no meu exemplo:
#!/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 deve ser usado em uma cadeia de caracteres que um usuário pode definir, porque é perigoso. Algo como "string; rm -rf ~" vai ser ruim. Então, geralmente o seu melhor para encontrar soluções em que você não precisa se preocupar com isso.
No entanto, eval serão necessários para definir as variáveis ??passadas, como o comentário observou.
$ 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"
Output:
cat
cat|dog
cat|dog|hamster
Estrutura para chamar essa função é:
append_string name_of_var_to_update string_to_add name_of_var_containing_sep_char
Nome da variável é passado para fuction sobre animais de estimação e setembro, enquanto string para anexar é passado da maneira usual como valor. "$ {! 1}" refere-se ao conteúdo da variável global PETS. No início dessa variável é vazia e contens é adicionado cada vez que chamar a função. caractere separador podem ser selecionados conforme necessário. "Eval" linhas começando atualização PETS variável.
Este é o que funciona para mim no shell bash Ubuntu
#!/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