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?

Quasímodo
  • 18,865
  • 4
  • 36
  • 73
  • 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). – ctrl-alt-delor Jul 22 '20 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 – Swaroop Joshi Jul 22 '20 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. – Charles Duffy Jul 23 '20 at 14:37
  • @CharlesDuffy haha, maybe. I just wanted to know what was going on behind the scenes with Linux. – Swaroop Joshi Jul 23 '20 at 14:40
  • 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". – Charles Duffy Jul 23 '20 at 14:40
  • 1
    BTW, you may find https://mywiki.wooledge.org/BashParser useful reading. – Charles Duffy Jul 23 '20 at 14:41

4 Answers4

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

fra-san
  • 10,205
  • 2
  • 22
  • 43
Quasímodo
  • 18,865
  • 4
  • 36
  • 73
  • 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} – Stéphane Chazelas Jul 22 '20 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.

xenoid
  • 8,888
  • 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 – Swaroop Joshi Jul 23 '20 at 01:12
  • See footnote in answer. – xenoid Jul 23 '20 at 07: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 – Swaroop Joshi Jul 23 '20 at 08: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. – A. R. Jul 23 '20 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. – Charles Duffy Jul 24 '20 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, ...).

fra-san
  • 10,205
  • 2
  • 22
  • 43
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
glenn jackman
  • 85,964