39

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,749
  • 4
  • 20
  • 23

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
  • 45,735
  • 7
  • 84
  • 110
18
#!/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}"
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • 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
  • 28,728
  • 32
  • 139
  • 229
  • 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
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • 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
  • 28,728
  • 32
  • 139
  • 229
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,341
  • 1
  • 26
  • 27
  • 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