22

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 ?

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175

5 Answers5

15

Don't use eval, use declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
glenn jackman
  • 84,176
  • 15
  • 116
  • 168
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,341
  • 1
  • 26
  • 27
  • 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.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • 2
    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
  • 57,448
  • 9
  • 113
  • 229
  • `;` 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'` with `bash` 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 evaluating `a,b1234=0` as an arithmetic expression and that `a` 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
0

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