1

I'm using WSL (Ubuntu 18.04) on Windows 10 and bash.

I have a file filename.gpg with the content:

export SOME_ENV_VAR='123'

Now I run the following commands:

$ $(gpg -d filename.gpg)
$ echo $SOME_ENV_VAR
'123' <-- with quotes

However, if I run it directly in the shell:

$ export SOME_ENV_VAR='123'
$ echo $SOME_ENV_VAR
123 < -- without quotes

Why does it behave like this? Why is there a difference between running a command using $() and running it directly?

Aside: I got it working using eval $(gpg -d filename), I have no idea why this works.

Leo Jiang
  • 24,497
  • 49
  • 154
  • 284
  • 2
    This is another form of https://stackoverflow.com/questions/12136948/why-does-shell-ignore-quotes-in-arguments-passed-to-it-through-variables – that other guy Jan 13 '19 at 18:32
  • Ubuntu uses dash by default don't? Are you invoking scripts with bash instead? – geckos Jan 13 '19 at 19:08
  • 2
    Answer is here: https://unix.stackexchange.com/questions/38440/why-does-bash-variable-expansion-retain-quotes – hek2mgl Jan 13 '19 at 19:21
  • 1
    Quotes behave exactly the same in shell scripts as in interactive commands. The problems is that you are *not* running `filename` as a script. Hint: `$(cat filename)` is the same as `$(echo "export SOME_ENV_VAR='123'")`. – AlexP Jan 13 '19 at 19:21
  • 1
    This is also [BashFAQ #50](http://mywiki.wooledge.org/BashFAQ/050). BTW, using `eval` the way you are is inherently buggy. At bare minimum, add more quotes: `eval "$(gpg -d filename)"` -- that way you aren't string-splittting and glob-expanding the decrypted content before concatenating the results of those operations together and feeding that result to `eval` to parse. – Charles Duffy Jan 14 '19 at 01:35
  • For a concrete example of using `eval` unwisely causing bugs, compare `printf '%s\n' "first line" "second line"` to `eval printf '%s\n' "first line" "second line"`. And that's an *extremely* tame example; more interesting ones would be security-impacting. For example, if you run `var='*.txt'; eval echo $var`, you'd better hope nobody created a file with a command like `touch $'$(rm -rf ~)\'$(rm -rf ~)\'.txt'`. See also [BashFAQ #48](https://mywiki.wooledge.org/BashFAQ/048). – Charles Duffy Jan 14 '19 at 01:40

1 Answers1

4

Quotes in shell scripts do not behave differently from quotes in shell commands.

With the $(gpg -d filename.gpg) syntax, you are not executing a shell script, but a regular single command.

What your command does

  1. It executes gpg -d filename.gpg
  2. From the result, it takes the first (IFS-separated) word as the command to execute
  3. It takes every other (IFS-separated) words, including words from additional lines, as its parameters
  4. It executs the command

From the following practical examples, you can see how it differs from executing a shell script:

  1. Remove the word export from filename.gpg: the command is then SOME_ENV_VAR='123' which is not understood as a variable assignment (you will get SOME_ENV_VAR='123': command not found).
  2. If you add several lines, they won't be understood as separated command lines, but as parameters to the very first command (export).
  3. If you change export SOME_ENV_VAR='123' to export SOME_ENV_VAR=$PWD, SOME_ENV_VAR will not contain the content of variable PWD, but the string $var

Why is it so?

See how bash performs expansion when analyzing a command.

There are many steps. $(...) is called "command substitution" and is the fourth step. When it is done, none of the previous steps will be performed again. This explains why your command does not work when you remove the export word, and why variables are not substituted in the result.

Moreover "quote Removal" is the last step and the manual reads:

all unquoted occurrences of the characters ‘\’, ‘'’, and ‘"’ that did not result from one of the above expansions are removed

Since the single quotes resulted from the "command substitution" expansion, they were not removed. That's why the content of SOME_ENV_VAR is '123' and not 123.

Why does eval work?

Because eval triggers another complete parsing of its parameters. The whole set of expansions is run again.

From the manual:

The arguments are concatenated together into a single command, which is then read and executed

Note that this means that you are still running one single command, and not a shell script. If your filename.gpg script has several lines, subsequent lines will be added to the argument list of the first (and only) command.

What should I do then?

Just use source along with process substitution.

source <(gpg -d filename.gpg)

Contrary to eval, source is used to execute a shell script in the current context. Process substitution provides a pseudo-filename that contains the result of the substitution (i.e. the output of gpg).

xhienne
  • 5,738
  • 1
  • 15
  • 34
  • `source <(...)` doesn't work on all widely-used versions of bash. See https://stackoverflow.com/questions/32596123/why-source-command-doesnt-work-with-process-substitution-in-bash-3-2 – Charles Duffy Jan 14 '19 at 01:40