4

Say:

variable="Something that it holds"

then echo "$variable" will output: Something that it holds

But say I also do:

var2="variable";  
echo "\$$(echo $var2)"

will just output: $variable
And not: Something that it holds

Can anyone tell me about what feature of Unix is in play, here?

6
  • Which bit are you struggling with? Are you OK with echo "$(echo $var2)"? Incidentally you should also quote the $var2. So it becomes echo "$(echo "$var2")". However in this example it won't matter (the bug is latent). Commented Jul 22, 2020 at 19:14
  • Sorry, it's just that,"\$$(echo $var2)" would evaluate to '$variable'. So why doesn't UNIX understand that I'm trying to do echo $variable (indirectly), and print out whatever is stored in variable. I'm referring to the second part Commented Jul 22, 2020 at 19:44
  • 1
    You should be very glad that the shell works the way it does -- it would be impossible to write a shell script that securely handled untrusted data if data were silently treated as code. Commented Jul 23, 2020 at 14:37
  • 2
    Sure. But the answer is just that there's less going on behind the scenes that you expect; whatever extra step might have to happen to make the second $variable be replaced with a value just doesn't happen, because it's not part of the execution model to re-evaluate expansion results as code (unless the user runs eval or such). So we can't really give you an answer that says "it doesn't parse as code because thing-X happens", when the reality is more like "it doesn't parse as code because in order for that to happen, there would need to be step-Y, and that just isn't part of the model". Commented Jul 23, 2020 at 14:40
  • 1
    BTW, you may find mywiki.wooledge.org/BashParser useful reading. Commented Jul 23, 2020 at 14:41

4 Answers 4

9
variable="Something"
var2="variable";
echo "\$$(echo $var2)"

In the last line, you expect "\$$(echo $var2)"$variableSomething. This would require the shell to perform two parameter expansions. It does not, but rather simply prints the result of the command substitution $(echo $var2) prepended by a $.

In principle, eval helps you to get what you want. After the shell performs the first step, "\$$(echo $var2)"$variable, eval performs the second step, $variableSomething.

$ eval echo "\$$(echo $var2)"
Something

Although in our particular case the above command is OK, that still lacks a correct quoting, and printf is to be favored over echo,

$ eval 'printf "%s\n" "${'"$var2"'}"'
Something

However, eval raises security concerns with untrusted data. Suppose var2="variable;rm importantFile". In that case, eval passes

echo $variable;rm importantFile

to the shell, which happily removes importantFile, if it exists.

In some shells (e.g.: Bash, ksh, Zsh) you can also do it using indirection. The syntax of indirect expansion in Bash is:

$ echo "${!var2}"
Something

var2="variable;rm importantFile" is not a problem anymore, but var2='a[$(rm importantFile)]' still is.

Read more about indirection in the Bash manual.

If the first character of parameter is an exclamation point (!), and parameter is not a nameref, it introduces a level of indirection. Bash uses the value formed by expanding the rest of parameter as the new parameter; this is then expanded and that value is used in the rest of the expansion, rather than the expansion of the original parameter

1
  • bash's ${!var} is doing almost the opposite of what ${!var} was doing in ksh93. In ksh93, to do indirection, you'd use namerefs (not ${!var} which is to prevent indirection when var is a nameref, try ksh93 -c 'a=value; nameref b=a; echo "$b" "${!b}"'). In zsh, it's ${(P)var} Commented Jul 22, 2020 at 22:04
8

Smells like an anti-pattern(*). Many shells have string-indexed arrays/dictionaries. In ksh93 (where that syntax comes from), bash or zsh:

typeset -A dictionary
x="keyX"
y="keyY"
dictionary[keyX]="valueX"
dictionary[$y]="valueY"

printf '%s\n' "${dictionary[$x]}"
printf '%s\n' "${dictionary[keyY]}"

(*) Nothing to do with Linux per se. The variable-name-in-a-variable is a very general "anti-pattern", something that shouldn't be done, and if you think you need it, there is a problem with your design (XY problem)? Most uses of a variable-name-in-a-variable are better replaced with a dictionary when that exists.

5
  • I'm sorry, I just started learning Unix last week, and didn't completely grasp what you're trying to say. Are you telling about the inner working of UNIX? Though, I got the dictionary part Commented Jul 23, 2020 at 1:12
  • See footnote in answer. Commented Jul 23, 2020 at 7:45
  • Right, actually, maybe you're right. I wasn't trying to solve a problem. It's just that I wanted to know what is going on, behind the scenes, with Linux's interpretation and all that. But thank you for mentioning that it's not usually necessary to do so Commented Jul 23, 2020 at 8:35
  • It's not always an anti-pattern. For example, M (a non-relational database language) and its descendant ObjectScript offer @variable syntax to do just this, which is very useful for passing around references to database nodes. The alternative would be Mergeing the entire data tree into an in-memory node, editing it and Mergeing it back into the database, which for a large data node is hugely inefficient. Commented Jul 23, 2020 at 16:29
  • Also, in bash, indirect references are genuinely essential in some cases. You can't pass an associative array as a function argument without one, for example. Particularly in bash releases which have formally adopted the ksh nameref feature, I absolutely disagree with the antipattern claim. Commented Jul 24, 2020 at 14:00
5

What you are observing is the standard behavior of a POSIX shell: in general, it

  1. reads its input;

  2. breaks the input into tokens: words and operators (token recognition);

    • during this step, any time it encounters an unquoted $ (or `), it recursively determines the type of expansion and the token to be expanded, reading all the needed input;
  3. parses the input into simple commands and compound commands;

  4. performs various expansions (separately) on different parts of each command, resulting in a list of pathnames and fields to be treated as a command and arguments;

  5. performs redirection and removes redirection operators and their operands from the parameter list;

  6. executes a function, built-in, executable file, or script;

  7. optionally waits for the command to complete and collects the exit status.

When parsing echo "\$$(echo $var2)", the shell detects two expansions (step 2): the double-quoted command substitution $(echo $var2) and the unquoted parameter expansion $var2. The escaped $ in \$ is taken as a literal dollar sign because a double-quoted \ retains its role as an escape character when followed by $.

No further detection of expansions happens at later stages. Specifically, there is no further parsing of the result of expansions performed in step 4 ("\$$(echo $var2)""\$$(echo variable)""\$variable"$variable) that could detect expansion-triggering characters.

Also note that, while the $ symbol is used to replace the name of a variable with its content in the context of parameter expansion, it has not been designed as a general dereference operator.

In standard parameter expansion, whose simplest form is ${parameter}, the parameter specification is only allowed to be a variable name, a positional parameter or a special parameter (see the definition of "parameter"). Strictly speaking, parameter expansions can not be nested (an expansion expression is only allowed as word in the various ${parameter<symbols>[word]} forms).

You can easily verify that, with the exception of the Z shell, ${${foo}} is not a valid expression and that $$ expands to the shell's PID (thus, $foo expands to the value of foo, $$foo expands to the concatenation of the shell's PID and the literal "foo", $$$foo expands to the concatenation of the shell's PID and the value of foo, ...).

0
1

In addition to indirect variable expansion, in bash (starting from version 4.3) you can use a nameref

declare -n var2="variable"  # "var2" is a _reference_ to "variable"

variable="Something"
echo "$var2"     # => Something

variable="something else"
echo "$var2"     # => something else

unset variable
echo "$var2"     # => ""

I don't know how this is implemented, but this is an interesting tidbit: you can find out what a nameref refers to using indirection:

echo ${!var2}    # => variable

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.