9

${!FOO} performs a double substitution in bash, meaning it takes the (string) value of FOO and uses it as a variable name.
zsh doesn’t support this feature.

Is there a way to make this work the same in bash and zsh?

Background:

I’ve got a list of environment variables, like

PATH MAIL EDITOR

and want to first print the variable names and afterwards their values.

This works in bash but not zsh:

for VAR in LIST
do
        echo $VAR
        echo ${!VAR}
done

It should be somehow possible “the old way” with eval, but I can’t get it to work:

for VAR in LIST
do
        echo $VAR
        echo `eval \$$VAR`
done

I’m never going to understand why I can’t simply do arbitrary deep substitutions like ${${VAR}} or even ${${${VAR}}} if need be, so an explanation for that would be nice, too.

  • 1
    HAH! I thought zsh was supposed to have more features. – Ярослав Рахматуллин Mar 18 '13 at 9:14
  • 3
    No need to brag with bash, it indeed has the same function (and a lot more), only uses a different pattern, namely ${(p)FOO}. – Profpatsch Mar 19 '13 at 13:46
  • 1
    @ЯрославРахматуллин, zsh's (e) parameter expansion flag was added in zsh-2.6-beta17 (May 1996), the (P) flag in 3.1.5-pws-7 (1999). bash's ${!var} in 2.0 (December 1996). – Stéphane Chazelas Oct 25 '16 at 22:14
13

Both bash and zsh have a way to perform indirect expansion, but they use different syntax.

It's easy enough to perform indirect expansion using eval; this works in all POSIX and most Bourne shells. Take care to quote properly in case the value contains characters that have a special meaning in the shell.

eval "value=\"\${$VAR}\""
echo "$VAR"
echo "$value"

${${VAR}} doesn't work because it's not a feature that any shell implements. The thing inside the braces must conform to syntax rules which do not include ${VAR}. (In zsh, this is supported syntax, but does something different: nested substitutions perform successive transformations on the same value; ${${VAR}} is equivalent to $VAR since this performs the identity transformation twice on the value.)

14

The comment, under the original question, by Profpatsch gives the example ${(p)FOO} to output the content of an indirect variable reference. The flag is incorrect or a typo, it should be a capital P and not a lower case p. Use: ${(P)FOO}.

The following should produce the desired output:

#! /bin/zsh
LIST=(PATH MAIL EDITOR)
for VAR in ${LIST}
do
    print "${VAR}:  ${(P)VAR}"
done

From the zshexpn man page; section - Parameter Expansion Flags:

P

  This forces the value of the parameter name to be interpreted as a further
  parameter name,  whose  value  will  be used where appropriate.  Note that
  flags set with one of the typeset family of commands (in particular case
  trans‐formations) are not applied to the value of name used in this fashion.

  If used with a nested parameter or command substitution, the result of that
  will be taken as a parameter  name  in the  same  way.   For  example,  if
  you  have  `foo=bar'  and `bar=baz', the strings ${(P)foo}, ${(P)${foo}}, and
  ${(P)$(echo bar)} will be expanded to `baz'

At one time I read why ${${${VAR}}} does not produce the output you expected, but at this time I can't find it. You can do something like the following:

first="second" ; second="third" ; third="fourth" ; fourth="fifth"
print ${(P)${(P)${(P)first}}}
fifth
3

You are not using eval correctly. In your example value of $VAR preceded with a "$" (i.e `$VALUE') would be executed as a command. That's not what you want. You want to evaluate the expansion of a variable whose name is taken from another variable.

$ for i in `echo PATH MAIL EDITOR`; 
    do eval moo="\${$i}" 
    echo $moo 
done
/usr/local/sbin:/usr/local/bin:/usr/sbin:/u (...)
/var/mail/root
nano
0

{ba,z}sh solution

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

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
var_expand() {
  if [ "$#" -ne 1 ] || [ -z "${1-}" ]; then
    printf 'var_expand: expected one non-empty argument\n' >&2;
    return 1;
  fi
  eval printf '%s' "\"\${$1?}\""
}
  • There's no function nor [[...]] in POSIX sh. – Stéphane Chazelas Sep 28 '18 at 12:44
  • The check for no argument should be with [ "$#" -eq 0 ] (or probably [ "$#" -ne 1 ] as you're expecting only one argument). – Stéphane Chazelas Sep 28 '18 at 12:46
  • @StéphaneChazelas Thanks, updated. Is there a reference shell for POSIX compliance? I'm thinking shell-check might help too. – Tom Hale Sep 28 '18 at 13:18
  • @StéphaneChazelas Re the test, I also want to want to fail when given a single null argument. I've added your 2nd suggestion. Cheers! – Tom Hale Sep 28 '18 at 13:25
  • I agree with quoting ${1-} given a='"" -o -n 1' [ -z $a ] == 0. But won't $# always be a single token? – Tom Hale Sep 28 '18 at 13:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.