46

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//\'/\'\"\'\"\'}"'"
Charles
  • 50,943
  • 13
  • 104
  • 142
Eli Barzilay
  • 29,301
  • 3
  • 67
  • 110

7 Answers7

42

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 ';;;' '%s' foobar; echo $?
bash: printf: `;;;': not a valid identifier
2
David Foerster
  • 1,461
  • 1
  • 14
  • 23
  • Unlike `declare` and `typeset` (which prior to bash 4.2 can only declare the variable as local), with this solution the variable is not declared as local. Fixed my problem! – Law29 Aug 22 '18 at 07:57
  • I puzzled over the middle argument until I figured out that the `'%s'` is literal --which I found confusing because `'%s'` wasn't anywhere in the example given ... I suppose that the example works because, in addition to it being literal, it also appears to be optional, for the use-case of indirectly assigning variables. – Cognitiaclaeves Apr 24 '20 at 19:26
  • @Cognitiaclaeves: Thanks for the notice. I fixed the example to make it clearer. In practice, it shouldn’t make a difference since the formatting string doesn’t matter if the destination variable name is invalid. – David Foerster Apr 26 '20 at 08:12
41

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.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    This looks not portable to shells lesser than bash – MarcH Sep 16 '14 at 15:51
  • Indeed; while `declare` is an extension to the POSIX standard, it is also just a synonym for `typeset`, which *is* supported by other major shells (`ksh` and `zsh`, namely). Shells that don't support something similar must use `eval` with care. – chepner Sep 16 '14 at 15:55
  • 5
    `eval "$var='$val'"` is not nearly careful enough: If the contents contain literal single-quotes, they can easily escape. – Charles Duffy Apr 14 '15 at 01:36
  • 2
    Note that `typeset` and `declare`, when executed inside a bash function, define the variable as local. This made it useless for me as I was operating inside a bash function and wished to access the results from outside the function. David's "printf" solution below worked for me. – Law29 Aug 21 '18 at 16:34
  • 3
    Starting with `bash` 4.2, `declare` takes a `-g` option to force a global variable. – chepner Aug 21 '18 at 16:58
  • `declare` always creates a new variable. This answer is thus useless if the intent is to rather set an existing variable in some parent scope -- which is the primary use case for indirect variable assignment (to return value(s) from a function). – ivan_pozdeev Nov 07 '18 at 20:32
  • What about `eval "$var='${val//"'"/"'\''"}'"` ? ~ replacing all of the `'` by `'\''`. – Mathieu CAROFF Jan 07 '19 at 01:19
  • In addition, `export "$var=$val"` is also legal; I suspect that `local "$var=$val"` would also work. – zneak Aug 22 '19 at 00:33
  • @zneak They would, but `local` can only be used in a function, and `export` has the additional effect of adding the variable to the current environment. – chepner Aug 22 '19 at 01:50
  • Of course. Use appropriately if you are looking for either of these effects. – zneak Aug 22 '19 at 01:51
  • Does not work for me on `GNU bash, version 5.0.16(1)-release (x86_64-pc-linux-gnu)`. I'm getting `line 36: declare: =result-C: not a valid identifier`. It does not seem to split properly on the equation sign. – Akito Jun 22 '20 at 14:51
  • Thank you @chepner ! declare was successful to do an array assignment. This resolved my problem in bash trying array indirect assignment using a value from a dereferenced array as name for an associative array. The individual assignment can be done as declare ${basearray[0]}[sub0x]="${basearray[1]" with basearray[0] is an dereferenced array read in two lines before used to group items in a new array named by the entry. works4me ... man bash says: "The set and declare builtins display array values in a way that allows them to be reused as assignments." – opinion_no9 Mar 09 '21 at 15:35
19
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.

Generally, all expressions of the form $varname should be enclosed in double quotes, to prevent accidental expansion of filename patterns like *.c.

There are only two places where the quotes may be omitted since they are defined to not expand pathnames and split fields: variable assignments and case. POSIX 2018 says:

Each variable assignment shall be expanded for tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal prior to assigning the value.

This list of expansions is missing the parameter expansion and the field splitting. Sure, that's hard to see from reading this sentence alone, but that's the official definition.

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\""

Note that the second dollar is escaped using a backslash, to prevent it from being expanded in the first run. What happens is:

eval "$var=\"the value is \$val\""

The argument to the command eval is sent through parameter expansion and unescaping, resulting in:

varname="the value is $val"

This string is then evaluated as a variable assignment, which assigns the following value to the variable varname:

the value is value
Roland Illig
  • 40,703
  • 10
  • 88
  • 121
  • The indirection on the RHS is not what I'm looking for. – Eli Barzilay Mar 30 '12 at 12:42
  • 1
    (*forehead-slap*) Bah, I completely missed why I *do* want indirection on the RHS. Since your reply doesn't talk about it at all, I'll edit it now, instead of doing the answer-myself-and-pat-my-own-back... – Eli Barzilay Mar 30 '12 at 13:28
  • Fantastic! In the past I did some complicated `eval eval export` nonsense. Thank you ever so much. For the googlers, go with the answer above, not the eval eval export format. – bgStack15 Feb 09 '16 at 20:57
  • If anybody was confused by the above phrasing, perhaps `eval "$var"='$val'`makes it more clear. Or perhaps less clear, but now you have two phrasings to consider and compare to make sure you understood. :-) – clacke Feb 02 '18 at 08:24
  • @Eli There's nothing wrong with answering your own question. I have reverted your edit since it didn't match my style for answering. – Roland Illig Feb 02 '18 at 17:51
  • @RolandIllig *sigh* you waited 6 years for that? Anyway, done, since you insist. Also, "always be a single string enclosed in either single or double quotes" is not really related to either `eval` or my question (which doesn't really need the quotes since both variable names are know, and the contents of `$val` doesn't matter). – Eli Barzilay Feb 02 '18 at 20:14
17

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...

Eli Barzilay
  • 29,301
  • 3
  • 67
  • 110
  • The answer that DOES emphasize the main point to get: To have the right-side variable NOT re-evaluated by escaping the `$`. And always add quotes(keeps you very younger!). I think the `eval` version is better. – Small Boy Jun 17 '21 at 05:02
7

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}"
Darren Embry
  • 155
  • 1
  • 2
1

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")
0

Yet another way to accomplish this, without eval, is to use "read":

INDIRECT=foo
read -d '' -r "${INDIRECT}" <<<"$(( 2 * 2 ))"
echo "${foo}"  # outputs "4"
Jonathan Mayer
  • 1,432
  • 13
  • 17