40

Follow-up to the background part in this question.

In bash I can use ${!FOO} for double substitution, in zsh ${(P)FOO}. In both, the old-school (hack-y) eval \$$FOO works.

So, the smartest and most logical thing for me would be ${${FOO}}, ${${${FOO}}}… for double/triple/n substitution. Why doesn’t this work as expected?

Second: What does the \ do in the eval statement? I reckon it’s an escape, making something like eval \$$$FOO impossible. How to do a triple/n substitution with that that works in every shell?

Profpatsch
  • 1,799

7 Answers7

25

The \ must be used to prevent the expansion of $$ (current process id). For triple substitution, you need double eval, so also more escapes to avoid the unwanted expansions in each eval:

#! /bin/bash
l0=value
l1=l0
l2=l1
l3=l2
l4=l3

echo $l0
eval echo \$$l1
eval eval echo \\$\$$l2
eval eval eval echo \\\\$\\$\$$l3
eval eval eval eval echo \\\\\\\\$\\\\$\\$\$$l4
choroba
  • 47,233
19
#!/bin/bash

hello=world
echo=hello

echo $echo ${!echo}
8

Supposing the value of FOO is a valid variable name (say BAR), eval \$$FOO splits the value of BAR into separate words, treats each word as a wildcard pattern, and executes the first word of the result as a command, passing the other words as arguments. The backslash in front of the dollar makes it be treated literally, so the argument passed to the eval builtin is the four-character string $BAR.

${${FOO}} is a syntax error. It doesn't do a “double substitution” because there's no such feature in any of the common shells (not with this syntax anyway). In zsh, ${${FOO}} is valid and is a double substitution, but it behaves differently from what you'd like: it performs two successive transformations on the value of FOO, both of which are the identity transformation, so it's just a fancy way of writing ${FOO}.

If you want to treat the value of a variable as a variable, be careful of quoting things properly. It's a lot easier if you set the result to a variable:

eval "value=\${$FOO}"
  • 2
    Setting it as variable, so that the first word isn’t used as command? Man, this sort of logic is hard to grasp. It’s the general problem I seem to have with bash. – Profpatsch Mar 19 '13 at 13:45
7

{ba,z}sh solution

Here's a function which works in both {ba,z}sh. I believe it's also POSIX compliant.

Without going mad with quoting, you can use it for many levels of indirection like so:

$ a=b
$ b=c
$ c=d
$ echo $(var_expand $(var_expand $a)
d

Or if you have more (!?!) levels of indirection you could use a loop.

It warns when given:

  • Null input
  • A variable name which isn't set
# Expand the variable named by $1 into its value. Works in both {ba,z}sh
# eg: a=HOME $(var_expand $a) == /home/me
var_expand() {
  if [ -z "${1-}" ] || [ $# -ne 1 ]; then
    printf 'var_expand: expected one argument\n' >&2;
    return 1;
  fi
  eval printf '%s' "\"\${$1?}\""
}
Tom Hale
  • 30,455
  • A working cross-shell solution for me. Hopefully "POSIX-compatible double variable expansion" redirects to this answer in the future. – silico-biomancer Dec 04 '19 at 00:28
4

Why would you need to do that?

You can always do it in several steps like:

eval "l1=\${$var}"
eval "l2=\${$l1}"
...

Or use a function like:

deref() {
  if [ "$1" -le 0 ]; then
    eval "$3=\$2"
  else
    eval "deref $(($1 - 1)) \"\${$2}\" \"\$3\""
  fi
}

Then:

$ a=b b=c c=d d=e e=blah
$ deref 3 a res; echo "$res"
d
$ deref 5 a res; echo "$res"
blah

FWIW, in zsh:

$ echo ${(P)${(P)${(P)${(P)a}}}}
blah
  • Huh, using several steps didn’t occur to me until now… strange. – Profpatsch Mar 24 '13 at 18:08
  • It's obvious, from my point of view, why @Profpatsch wants to do that. Because it is the most intuitive way to do it. Being hard to implement multiple substitutions in bash is another thing. – Nikos Alexandris Jul 26 '13 at 15:35
2

POSIX solution

Without going mad with quoting, you can use this function for many levels of indirection like so:

$ a=b
$ b=c
$ c=d
$ echo $(var_expand $(var_expand $a)
d

Or if you have more(!?!) levels of indirection you could use a loop.

It warns when given:

  • Null input
  • More than one argument
  • A variable name which isn't set

# Expand the variable named by $1 into its value. Works in both {ba,z}sh
# eg: a=HOME $(var_expand $a) == /home/me
function var_expand {
  if [ "$#" -ne 1 ] || [ -z "${1-}" ]; then
    printf 'var_expand: expected one argument\n' >&2;
    return 1;
  fi
  eval printf '%s' "\"\${$1?}\""
}
Tom Hale
  • 30,455
2

Just like ${$foo} doesn't work in place of ${(P)foo} in zsh, neither does ${${$foo}}. You just need to specify each level of indirection:

$ foo=bar
$ bar=baz
$ baz=3
$ echo $foo
bar
$ echo ${(P)foo}
baz
$ echo ${(P)${(P)foo}}
3

Of course, ${!${!foo}} doesn't work in bash, because bash doesn't allow nested substitutions.

chepner
  • 7,501
  • Thanks, that’s good to know. A shame that this is zsh-only, so you can’t really put it in a script (everyone has bash, but it’s not the other way around). – Profpatsch Mar 24 '13 at 16:08
  • I suspect zsh is installed far more often than you might assume. It may not be linked to sh like bash often (but not always) is, but it may still be available for shell scripts. Think of it like you do Perl or Python. – chepner Mar 24 '13 at 16:24