可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Seems that the recommended way of doing indirect variable setting in bash is to use eval
:
var=x; val=foo
eval $var=$val
echo $x # --> foo
The problem is the usual one with eval
:
var=x; val=1$'\n'pwd
eval $var=$val # bad output here
(and since it is recommended in many places, I wonder just how many scripts are vulnerable because of this...)
In any case, the obvious solution of using (escaped) quotes doesn't really work:
var=x; val=1\"$'\n'pwd\"
eval $var=\"$val\" # fail with the above
The thing is that bash has indirect variable reference baked in (with ${!foo}
), but I don't see any such way to do indirect assignment -- is there any sane way to do this?
For the record, I did find a solution, but this is not something that I'd consider "sane"...:
eval "$var='"${val//\'/\'\"\'\"\'}"'"
回答1:
A slightly better way, avoiding the possible security implications of using eval
, is
declare "$var=$val"
Note that declare
is a synonym for typeset
in bash
. The typeset
command is more widely supported (ksh
and zsh
also use it):
typeset "$var=$val"
In modern versions of bash
, one should use a nameref.
declare -n var=x
x=$val
It's safer than eval
, but still not perfect.
回答2:
Bash has an extension to printf
that saves its result into a variable:
printf -v "${VARNAME}" '%s' "${VALUE}"
This prevents all possible escaping issues.
If you use an invalid identifier for $VARNAME
, the command will fail and return status code 2:
$ printf -v ';;;' foobar; echo $?
bash: printf: `;;;': not a valid identifier
2
回答3:
eval "$var=\$val"
The argument to eval
should always be a single string enclosed in either single or double quotes. All code that deviates from this pattern has some unintended behavior in edge cases, such as file names with special characters.
When the argument to eval
is expanded by the shell, the $var
is replaced with the variable name, and the \$
is replaced with a simple dollar. The string that is evaluated therefore becomes:
varname=$value
This is exactly what you want.
Usually all expressions of the form $varname
should be enclosed in double quotes. There are only two places where the quotes may be omitted: variable assignments and case
. Since this is a variable assignment, the quotes are not needed here. They don't hurt, though, so you could also write the original code as:
eval "$var=\"the value is $val\""
回答4:
The main point is that the recommended way to do this is:
eval "$var=\$val"
with the RHS done indirectly too. Since eval
is used in the same
environment, it will have $val
bound, so deferring it works, and since
now it's just a variable. Since the $val
variable has a known name,
there are no issues with quoting, and it could have even been written as:
eval $var=\$val
But since it's better to always add quotes, the former is better, or
even this:
eval "$var=\"\$val\""
A better alternative in bash that was mentioned for the whole thing that
avoids eval
completely (and is not as subtle as declare
etc):
printf -v "$var" "%s" "$val"
Though this is not a direct answer what I originally asked...
回答5:
Newer versions of bash support something called "parameter transformation", documented in a section of the same name in bash(1).
"${value@Q}"
expands to a shell-quoted version of "${value}"
that you can re-use as input.
Which means the following is a safe solution:
eval="${varname}=${value@Q}"
回答6:
Just for completeness I also want to suggest the possible use of the bash built in read. I've also made corrections regarding -d'' based on socowi's comments.
But much care needs to be exercised when using read to ensure the input is sanitized (-d'' reads until null termination and printf "...\0" terminates the value with a null), and that read itself is executed in the main shell where the variable is needed and not a sub-shell (hence the < <( ... ) syntax).
var=x; val=foo0shouldnotterminateearly
read -d'' -r "$var" < <(printf "$val\0")
echo $x # --> foo0shouldnotterminateearly
echo ${!var} # --> foo0shouldnotterminateearly
I tested this with \n \t \r spaces and 0, etc it worked as expected on my version of bash.
The -r will avoid escaping \, so if you had the characters "\" and "n" in your value and not an actual newline, x will contain the two characters "\" and "n" also.
This method may not be aesthetically as pleasing as the eval or printf solution, and would be more useful if the value is coming in from a file or other input file descriptor
read -d'' -r "$var" < <( cat $file )
And here are some alternative suggestions for the < <() syntax
read -d'' -r "$var" <<< "$val"$'\0'
read -d'' -r "$var" < <(printf "$val") #Apparently I didn't even need the \0, the printf process ending was enough to trigger the read to finish.
read -d'' -r "$var" <<< $(printf "$val")
read -d'' -r "$var" <<< "$val"
read -d'' -r "$var" < <(printf "$val")