23

I want to dynamically assign values to variables using eval. The following dummy example works:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

However, when the variable value contains spaces, eval returns an error, even if $var_value is put between double quotes:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

Any way to circumvent this ?

6 Answers6

15

Don't use eval, use declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
glenn jackman
  • 85,964
12

Don't use eval for this; use declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Note that word-splitting is not an issue, because everything following the = is treated as the value by declare, not just the first word.

In bash 4.3, named references make this a little simpler.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

You can make eval work, but you still shouldn't :) Using eval is a bad habit to get into.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"
chepner
  • 7,501
  • 2
    Using eval that way is wrong. You're expanding $var_value before passing it to eval which means it's going to be interpreted as shell code! (try for instance with var_value="';:(){ :|:&};:'") – Stéphane Chazelas Sep 11 '14 at 21:57
  • 1
    Good point; there are some strings you can't safely assign using eval (which is one reason I said you shouldn't use eval). – chepner Sep 11 '14 at 21:59
  • @chepner - I do not believe that is true. maybe it is, but not this one at least. parameter substitutions allow for conditional expansion, and so you can expand only safe values in most cases, I think. still, your primary problem for $var_value is one of quote inversion - assuming a safe value for $var_name (which can be just as dangerous an assumption, really), then you should be enclosing the right-hand-side's double-quotes within single-quotes - not vice-versa. – mikeserv Sep 12 '14 at 08:51
  • I think I've fixed the eval, using printf and its bash-specific %q format. This is still not a recommendation to use eval, but I think it is safer than it was before. That fact that you need to go to this much effort to get it to work is proof that you should be using declare or named references instead. – chepner Sep 12 '14 at 12:00
  • Well, actually, in my opinion, named references are the problem. The best way to use it - in my experience - is like... set -- a bunch of args; eval "process2 $(process1 "$@")" where process1 just prints quoted numbers like "${1}" "${8}" "${138}". That's crazy simple - and as easy as '"${'$((i=$i+1))'}" ' in most cases. indexed references make it safe, robust, and fast. Still - I upvoted. – mikeserv Sep 12 '14 at 13:06
  • Nice hack with %q. I always feed dirty for receiving rep for "don't do this" answers. – glenn jackman Sep 12 '14 at 13:09
4

A good way to work with eval is to replace it with echo for testing. echo and eval work the same (if we set aside the \x expansion done by some echo implementations like bash's under some conditions).

Both commands join their arguments with one space in between. The difference is that echo displays the result while eval evaluates/interprets as shell code the result.

So, to see what shell code

eval $(echo $var_name=$var_value)

would evaluate, you can run:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

That's not what you want, what you want is:

fruit=$var_value

Also, using $(echo ...) here doesn't make sense.

To output the above, you'd run:

$ echo "$var_name=\$var_value"
fruit=$var_value

So, to interpret it, that's simply:

eval "$var_name=\$var_value"

Note that it can also be used to set individual array elements:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

As others have said, if you don't care your code being bash specific, you can use declare as:

declare "$var_name=$var_value"

However note that it has some side effects.

It limits the scope of the variable to the function where it's run in. So you can't use it for instance in things like:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Because that would declare a foo variable local to setvar so would be useless.

bash-4.2 added a -g option for declare to declare a global variable, but that's not what we want either as our setvar would set a global var as opposed to that of the caller if the caller was a function, like in:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

which would output:

1:
2: some value

Also, note that while declare is called declare (actually bash borrowed the concept from the Korn shell's typeset builtin), if the variable is already set, declare doesn't declare a new variable and the way the assignment is done depends on the type of the variable.

For instance:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

will produce a different result (and potentially have nasty side effects) if varname was previously declared as a scalar, array or associative array.

Also note that declare is not any safer than eval, if the contents of $varname is not tightly controlled.

For instance, both eval "$varname=\$varvalue" and declare "$varname=$varvalue" would reboot the system if $varname contained a[$(reboot)1] for instance.

  • 3
    What's wrong with being bash specific? OP put the bash tag on the question, so he's using bash. Providing alternates is good, but I think telling someone not to use a feature of a shell because it's not portable is silly. – phemmer Sep 12 '14 at 00:38
  • @Patrick, seen the smiley? Having said that, using portable syntax means less effort when you need to port your code to another system where bash is not available (or when you realise you need a better/faster shell). The eval syntax works in all Bourne-like shells and is POSIX so all systems will have a sh where that works. (that also means my answer applies to all shells, and sooner or later, as happens often here, you'll see a non-bash specific question closed as a duplicate of this one. – Stéphane Chazelas Sep 12 '14 at 07:08
  • but what if $var_name contains tokens? ...like ;? – mikeserv Sep 12 '14 at 08:00
  • @mikeserv, then it's not a variable name. If you can't trust its content, then you need to sanitize it with both eval and declare (think of PATH, TMOUT, PS4, SECONDS...). – Stéphane Chazelas Sep 12 '14 at 08:47
  • but on the first pass its always a variable expansion and never a variable name until the second. in my answer i sanitize it with a parameter expansion, but if youre implying doing the sanitization in a subshell on the first pass, that could as well be done portably w/ export. i dont follow the parenthetical bit on the end though. – mikeserv Sep 12 '14 at 09:03
  • @mikeserv, what I'm saying is that if varname is meant to contain a variable name and varvalue is meant to contain a variable value (so any string), then $varvalue may contain a ;, but $varname may not. If you don't have control over the content of $varname (but I can't think of a context where that makes sense) and you want to avoid it having nasty side effects, then you need to sanitize it anyway whether you use eval or declare. – Stéphane Chazelas Sep 12 '14 at 10:23
  • i get that - but you always have control over expansions. one scenario where it might make sense would be parsing set or export -p- like splitting them with IFS==. in a situation like that you would very likely have a valid varname, but edge cases can wipe a disk. in any case - you had a good point about my post - i made a tiny update and must do better - but yours is completely fixed. also... unicycling? – mikeserv Sep 12 '14 at 10:45
1

If you do:

eval "$name=\$val"

...and $name contains a ; - or any of several other tokens the shell might interpret as delimiting a simple command - preceded by proper shell syntax, that will be executed.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

OUTPUT

hi
be careful with eval

It can sometimes be possible to separate the evaluation and execution of such statements, though. For example, alias can be used to pre-evaluate a command. In the following example the variable definition is saved to an alias that can only be successfully declared if the $nm variable it is evaluating contains no bytes that do not match ASCII alphanumerics or _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

eval is used here to handle invoking the new alias from a varname. But it is only called at all if the previous alias definition is successful, and while I know a lot of different implementations will accept a lot of different kinds of values for alias names, I haven't yet run into one that will accept a completely empty one.

The definition within the alias is for _$nm, however, and this is to ensure that no significant environment values are written over. I don't know of any noteworthy environment values beginning with a _ and it is usually a safe bet for semi-private declaration.

Anyway, if the alias definition is successful it will declare an alias named for $nm's value. And eval will only call that alias if also does not start with a number - else eval gets only a null argument. So if both conditions are met eval calls the alias and the variable definition saved in the alias is made, after which the new alias is promptly removed from the hash table.

mikeserv
  • 58,310
  • ; is not allowed in variable names. If you don't have control over the content of $name, then you need to sanitize it for export/declare as well. While export doesn't execute code, setting some variables like PATH, PS4 and many of those at info -f bash -n 'Bash Variables' have equally dangerous side effects. – Stéphane Chazelas Sep 12 '14 at 09:04
  • @StéphaneChazelas - of course it is not allowed, but, as before, its not a variable name on eval's first pass - it is a variable expansion. As you said elsewhere, in that context it is very much allowed. Still the $PATH argument is a very good one - i made a small edit and will add some later. – mikeserv Sep 12 '14 at 10:54
  • @StéphaneChazelas - better late than never...? – mikeserv Oct 28 '14 at 13:23
  • In practice, zsh, pdksh, mksh, yash don't complain on unset 'a;b'. – Stéphane Chazelas Oct 28 '14 at 13:30
  • You'll want unset -v -- ... as well. – Stéphane Chazelas Oct 28 '14 at 13:31
  • @StéphaneChazelas - That's very useful information - I only came back to this because I was thinking on how to do it at least cost for my own - purportedly - portable project. Anyway, I just made another edit to offer a different option. – mikeserv Oct 28 '14 at 13:38
  • Try with varname='a[\echo rm -rf / >&2`],a'withbash` for instance. – Stéphane Chazelas Oct 28 '14 at 13:43
  • @StephaneChezales - I'd rather not. Forgot about backquotes - should filter for them too. Thanks again. – mikeserv Oct 28 '14 at 13:44
  • Still doesn't exclude invalid variables like (rm -rf / 1)+a or a,b, and can have nasty side effects like with PATH=42,a or execute arbitrary code like with a='a[$(id >&2)]' varname='a,b' – Stéphane Chazelas Oct 28 '14 at 13:52
  • @StephaneChezales - What? How can it do any of those things? In the case a ) or don't know how to put a backquote in a comment exists in $varname then the resulting arithmetic is op is "$(($$=0))" - which is a syntax error. I don't understand how any of those things can happen. – mikeserv Oct 28 '14 at 14:04
  • Sorry, (rm -rf / 1)+a is wrong since you strip the )+a. In the case of a='b[$(id >&2)]' varname='a,b' bash -c ': "$((${varname%%*[\)]*}$$=0))"', you're evaluatinga,b1234=0as an arithmetic expression and thata` is recursively evaluated which is where you've got the code injection (that is why it's known (not as widely as one would like) that you mustn't use external data in arithmetic evaluations). – Stéphane Chazelas Oct 28 '14 at 14:10
  • @StephaneChazelas - I've never seen that comma thing before - and my next step is to look it up. In any case the test is now explicit in case I've missed anything else. Dang, man, you're good. I really appreciate it. My little project involves recursively building arrays based on evaluated names - that could have been bad. – mikeserv Oct 28 '14 at 14:13
  • Now, with the latest edit, you might as well do a case $varname in ("" | *[!_[:alnum:]]* | [[:digit:]]*) invalid; esac Or even ("" | [!_[:alnum:]]) since you're adding a _ prefix anyway. – Stéphane Chazelas Oct 28 '14 at 14:16
  • , is just the , operator. +, -.... would work as well. – Stéphane Chazelas Oct 28 '14 at 14:19
  • @StéphaneChazelas - I was just thinking the same, but now I'm not so sure if I shouldn't go back to the other - I guess the comma is pretty cool. But, understanding it a little better, I think it might be down to those three. The = assignment shouldn't work after an add. Those should require a separate $(( )) statement of their own. – mikeserv Oct 28 '14 at 14:20
  • @StéphaneChazelas - but I guess that is a shell dependent thing and maybe the portable app focuses a little harder on what is than what should be, I guess. – mikeserv Oct 28 '14 at 14:25
  • Also note that the check for [:alnum:] only works in yash, ksh93 and zsh. It only works in bash in single-byte locales, and in other shells in ASCII locales, so you may want to check for ASCII alnums only. – Stéphane Chazelas Oct 28 '14 at 14:25
  • @StéphaneChazelas - it's stuff like this that makes me wish -t had stuck - and had a real meaning. It would have been pretty handy for the bash devs that exploit thing you found, too. Maybe I could use IFS and look for extra fields... – mikeserv Oct 28 '14 at 14:30
  • @StéphaneChazelas - hopefully you'll let me know if you can break it now? – mikeserv Oct 28 '14 at 19:28
-1

use dead quote ie. apstroph.
eval $(echo $var_name='$var_value')
it worked with me.

-1

First, double quote to prevent globbing and word splitting. $var_name should be quoted, for best habit.

In normal shell script, variables turn into values, which is called expansion. When eval is in use, in addition to the normal phrase one expansion, variable are expanded inside eval also. This is what makes eval so powerful (not evil if used with care).

Solution one:

Suppress the expansion of $var_value outside eval by single quotes. eval takes one argument fruit=$var_value. Note that this is no different from eval 'fruit=$var_value' in plain text. Beware that the value in var_name should not contain any spaces. All three examples are the same, as long as they suppress the expansion. $var_value is treated as plain text.

var_name="fruit" ; var_value="blue orange" ; eval "$var_name"='$var_value' ; echo "$fruit"

var_name="fruit" ; var_value="blue orange" ; eval "$var_name"='$'var_value ; echo "$fruit"

var_name="fruit" ; var_value="blue orange" ; eval "$var_name"=$var_value ; echo "$fruit"

Solution two:

Give eval the quotes. In these cases, both $var_name and $var_value get expanded outside eval. eval gets fruit='blue orange' or fruit="blue orange".

var_name="fruit" ; var_value="blue orange" ; eval "$var_name"=\'"$var_value"\' ; echo "$fruit"

var_name="fruit" ; var_value="blue orange" ; eval "$var_name"=&quot;"$var_value"&quot; ; echo "$fruit"

var_name="fruit" ; var_value="blue orange" ; eval "$var_name=&quot;$var_value&quot;" ; echo "$fruit"

Single quotes and double quotes make no different in the above case. But when it comes to indirect variables, it makes a different.

basket='$var_value' ; var_name="fruit" ; var_value="blue orange" ; eval "$var_name"=\'"$basket"\' ; echo "$fruit"
$var_value

basket='$var_value' ; var_name="fruit" ; var_value="blue orange" ; eval "$var_name"=&quot;"$basket"&quot; ; echo "$fruit" blue orange


In your example, the use of echo is not necessary.

echo $var_name="$var_value" expands to fruit=blue orange. The problem is this string is not quoted. eval takes two arguments, fruit=blue and orange, separately. To fix it, either suppress the expansion of $var_value, or give it the proper quotes.

var_name="fruit" ; var_value="blue orange" ; eval $(echo "$var_name"='$var_value') ; echo "$fruit"

var_name="fruit" ; var_value="blue orange" ; eval $(echo "$var_name"='"$var_value"') ; echo "$fruit"

This use of echo is redundant. Try to avoid it as much as possible.

midnite
  • 423
  • What's the point of $(echo ...) in that last part there? Also, consider what happens with solution two if var_value contains a single or double quotes. – ilkkachu Dec 05 '23 at 21:19
  • echo is redundant, just for illustration purpose. If var_value contains quotes, the quotes should be properly escaped. eval is not expected to take arbitrary input. eval is powerful when we have whole control of the input, e.g. internal program. Arbitrary input must be sterilised. – midnite Dec 05 '23 at 21:25
  • eval is pretty the only solution when accessing indirect variables, e.g. var01 var02 var03, and need POSIX compliant. – midnite Dec 05 '23 at 21:28
  • And also the use of getopt cannot avoid eval. – midnite Dec 05 '23 at 21:34
  • Don't you think mentioning any limitations and requirements of the code you're showing might be useful for the reader? Like the one you just mentioned in the comment there. In any case, I can't really tell what that $(echo ...) is illustrating there. Usually, putting an echo in a command substitution is worse than useless, e.g. in that last one, it mangles the value if it contains whitespace other than single spaces (and simply escaping doesn't help). – ilkkachu Dec 05 '23 at 21:34
  • Anyway, as long as the value to be set is in a variable (with a known name), you seem to already have a working solution in solution one, so I'm just wondering how useful the broken ones are. – ilkkachu Dec 05 '23 at 21:36
  • 1
    what does getopt have to do with any of this? – ilkkachu Dec 05 '23 at 21:36