可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I want to ask if it is possible to pass arguments to a script function by reference:
i.e. to do something that would look like this in C:
Void boo (int & myint) { myint= 5; }
main (){
int t= 4;
printf t; // t->4
boo (t);
printf t; // t ->5
}
So then in BASH I want to do something like:
Function boo ()
{
var1=$1 # now var1 is global to the scrip but using it outside
# this function makes me loose 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
Any thoughts would be appreciated.
回答1:
It's 2018, and this question deserves an update. At least in Bash, as of Bash 4.3-alpha, you can use namerefs to pass function arguments by reference:
function boo()
{
local -n ref=$1
ref='new'
}
SOME_VAR='old'
echo $SOME_VAR # -> old
boo SOME_VAR
echo $SOME_VAR # -> new
The critical pieces here are:
Passing the variable's name to boo, not its value: boo SOME_VAR
, not boo $SOME_VAR
.
Inside the function, using local -n=$1
to declare a nameref to the variable named by $1
, meaning it's not a reference to $1
itself, but rather to a variable whose name $1
holds, i.e. SOME_VAR
in our case. The value on the right-hand side should just be a string naming an existing variable: it doesn't matter how you get the string, so things like local -n ref="my_var"
or local -n ref=$(get_var_name)
would work too. declare
can also replace local
in contexts that allow/require that. See chapter on Shell Parameters in Bash Reference Manual for more information.
The advantage of this approach is (arguably) better readability and, most importantly, avoiding eval
, whose security pitfalls are many and well-documented.
回答2:
From the Bash man-page (Parameter Expansion):
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.
Therefore a reference is the variable's name. Here is a swap
function using
variable indirection that does not require a temporary variable:
function swap()
{ #
# @param VARNAME1 VARNAME2
#
eval "$1=${!2} $2=${!1}"
}
$ a=1 b=2
$ swap a b
$ echo $a $b
2 1
回答3:
Use a helper function 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
}
And use it like this from within Newfun()
:
local "$1" && upvar $1 new
For returning multiple variables, use another helper function upvars
. This allows passing multiple variables within one call, thus avoiding possible conflicts if one upvar
call changes a variable used in another subsequent upvar
call.
See: http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference for helper function upvars
and more information.
The problem with:
eval $1=new
is that it's not safe if $1
happens to contain a command:
set -- 'ls /;true'
eval $1=new # Oops
It would be better to use printf -v
:
printf -v "$1" %s new
But printf -v
cannot assign arrays.
Moreover, both eval
and printf
won't work if the variable happens to be declared local
:
g() { local b; eval $1=bar; } # WRONG
g b # Conflicts with `local b'
echo $b # b is empty unexpected
The conflict stays there even if local b
is unset
:
g() { local b; unset b; eval $1=bar; } # WRONG
g b # Still conflicts with `local b'
echo $b # b is empty unexpected
回答4:
I have found a way to do this but I am not sure how correct this is:
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
So we pass the variable name as opposed to the value and then use eval ...
回答5:
Bash doesn't have anything like references built into it, so basically the only way you would be able to do what you want is to pass the function the name of the global variable you want it to modify. And even then you'll need an eval
statement:
boo() {
eval ${1}="new"
}
SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new
I don't think you can use indirect references here because Bash automatically accesses the value of the variable whose name is stored in the indirect reference. It doesn't give you the chance to set it.
回答6:
Ok, so this question has been waiting for a 'real' solution for some time now, and I am glad to say that we can now accomplish this without using eval at all.
The key to remember is to declare a reference in both the caller as the callee, at least in my example:
#!/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
回答7:
Eval should never be used on a string that a user can set because its dangerous. Something like "string; rm -rf ~" will be bad. So generally its best to find solutions where you don't have to worry about it.
However, eval will be needed to set the passed variables, as the comment noted.
$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4
回答8:
#!/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
Structure for calling that function is:
append_string name_of_var_to_update string_to_add name_of_var_containing_sep_char
Name of variable is passed to fuction about PETS and SEP while string to append is passed the usual way as value. "${!1}" refers to contents of global PETS variable. In the beginning that variable is empty and contens is added each time we call the function. Separator character can be selected as needed. "eval" starting lines update PETS variable.
回答9:
This is what works for me on Ubuntu bash shell
#!/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