10

Why does the following not work in Bash?

# Ensure TEST is unset
export TEST=''
echo "Hello world!" > test.txt && TEST="$(cat test.txt)" echo "$TEST"

It always returns a blank line for some reason.

Doing it on separate lines gives the expected result:

# Ensure TEST is unset
export TEST=''
echo "Hello world" > test.txt
TEST=$(cat test.txt)
echo "$TEST"
Hello world!

How can I make this work in a one-liner?

I need this to decrypt a GPG file containing a string with special characters which I then pass to Ansible.

ANSIBLE_VAULT_PASSWORD="$( gpg -d ~/gpg_encrypted_vault_password_file 2>/dev/null )" ansible-playbook etc etc etc

For some reason, Ansible developers think storing a vault password in a plain text file is secure and I don't like typing (or copy-pasting) long secure passwords every time I test a play, so using the GPG agent with an encrypted file is the only secure workaround that I can come up with.

5
  • 4
    "for some reason ansible devs think storing a vault-password in a plain text file is secure" – They give you another option. Use the "If executable, it will be run and the resulting stdout will be used as the password" description of ANSIBLE_VAULT_PASSWORD_FILE. This will be like ANSIBLE_VAULT_PASSWORD_FILE=/path/to/file ansible-playbook … where /path/to/file is an exectuable script that will run gpg -d ~/gpg_encrypted_vault_password_file 2>/dev/null. Not tested because I have not and I do not use ansible-playbook, I have just skimmed through the documentation. Commented Mar 27 at 12:58
  • 1
    On a side note: On Linux the initial environment for a process is visible to other processes under the same user through the /proc/ file system. A password passed this way is just as exposed as a if it were stored in a plaintext file with owner-only read permissions. Commented Mar 28 at 23:08
  • TEST="$(cat test.txt)" and echo "$TEST" are two separate commands. Try separating them with a semicolon.
    – Wastrel
    Commented Mar 29 at 13:44
  • *Hello, World! Commented Mar 30 at 11:27
  • @KamilMaciorowski using the gpg trick works nice for ansible-playbook, sadly this does not work for ansible-navigator, and the trick they mention using the ANSIBLE_VAULT_PASSWORD var does not work anymore. The devs finaly came to their senses that storing passwords in env vars is not really secure. (personally I think their security officer had a nice talk with them related to at least 4 CVE occurences when you do something like this), so I'm also reluctant to use the gpg trick with the script as it stores the password in an env var .
    – Rob
    Commented Apr 10 at 15:56

4 Answers 4

18

Not to conflict with any of the other pre-existing answers, but I thought a step-by-step analysis may help. (u1686_grawity's answer looks accurate, but I thought this might be more clear...)

# ensure TEST is unset export TEST=''

Good news: This likely works as you expected. The # turns the first line into a comment, and then the next line makes the TEST variable have an empty string (zero bytes).

echo "Hello world!" > test.txt && TEST="$(cat test.txt)" echo "$TEST"

The shell notices the &&. It will run the first portion of the code...

echo "Hello world!" > test.txt

Then, it processes the &&. It looks at the return value of the echo command, and sees that it is zero. Therefore, it can proceed to run the second command. The second command is:

TEST="$(cat test.txt)" echo "$TEST"

So, the first thing the shell is going to do here is to replace the variables. The "command substitution" acts like a variable here, so the "cat text.txt" command runs. In the shell's perspective, the command now looks more like:

TEST="Hello world!" echo "$TEST"

Now, that all probably looks good so far, but here's the big issue. The shell doesn't start running the 'TEST="Hello world!"' command yet. Instead, the shell notices that there is still a reference to $TEST, so the shell replaces that.

Now, the command that the shell plans to run looks something like this:

TEST="Hello world!" echo ""

I suppose that if I'm going to go into some full detail, I will point out that the shell does perform the act of breaking this command into pieces. This action removes the quotation marks. So the individual pieces look like this:

  1. TEST
  2. =
  3. Hello world!
  4. echo
  5. (empty value)

Okay, now the shell finally is done replacing things, so it can go onto the next task, which is to find what executable code to run.

It notices that the command line starts with a form of A=B C (where A represents a variable name, the equal sign specifies that a variable will be assigned a value, and B represents the value to be assigned, and then C is an optional portion which represents a command to run.

When the shell notices the command line fits this general pattern, the executable code that it is going to run is the code that handles the equal sign. Basically, the equal sign is similar to an executable filename, in the sense that the equal sign ends up controlling what code the shell is going to be executing. The shell proceeds to execute the code to assign a variable named TEST to have a value of Hello world!.

Then, after that is done, the shell will start the requested command, which uses the 4th and 5th potions identified above (where the 4th portion is the echo command, and the 5th portion is the empty string).

The reason that i's answer (err... I mean, sqrt-1's answer) works is that the second && causes the shell to run the TEST="$(cat test.txt)" code's return value is, notice that is zero, and only then proceed to process what comes after the second && (so then the echo "$TEST" portion will have the $TEST variable processed, and by now it all works).

Note that sqrt-1's provided answer shows some code that is a little bit wasteful, although understandably so because it does directly answer the asked question. What it basically does is:

First, splits the command into three parts:

  • echo "Hello world!" > test.txt
  • TEST="$(cat test.txt)"
  • echo "$TEST"
  • The third part only runs if the second part (runs and) succeeds, and the second part only runs if the first command succeeds, because of the two && operators.

    Another variation could be:

    echo "Hello world!" > test.txt && ( TEST="$(cat test.txt)" ; echo "$TEST" )

    With that semi-colon, the results of the 'TEST="$(cat test.txt)"' will not be evaluated to determine whether the 'echo "$TEST"' will be run. When I see a &&, I tend to try to figure out what is going to be evaluated, and why that would matter. So, despite the added complexity of needing to use parenthesis, this variation with a semi-colon actually seems to me like it may be a bit simpler to mentally process.

    3
    • As a potentially fun/enlightening excercise, consider this: mkdir -p t/u/v ; cd t/u ; echo t2.txt >> v/t2.txt ; echo From $(pwd) ; find . -iname t*.txt ; echo t1.txt >> t1.txt ; echo From $(pwd) ; find . -iname t*.txt ; cd .. ; echo From $(pwd) ; find . -iname t*.txt ; # and then figure out, why doesn't the t2.txt show up when you run the "find" command from the t/u sub-directory? And then, what causes the file to re-appear in the final "find" command?
      – TOOGAM
      Commented Mar 28 at 10:40
    • 1
      Note that your use of (...) will run the variable assignment in a subshell, and the result will not be visible afterwards, while using {...;} would allow the variable to be assigned in the same shell, although in this simple case it won't make any difference.
      – Neil
      Commented Mar 29 at 13:02
    • You might want to mention set -x as a way for the user to see all this happening.
      – terdon
      Commented Mar 30 at 14:29
    25

    It doesn't work because $var expansions are performed by the shell before the command line is run, i.e. not by the 'echo' command.

    Meanwhile, VAR=value somecommand is not a regular assignment – it is only meant to inject an environment variable into the new process, but has no effect on shell variables before that process is created, i.e. it is completely different from VAR=value; somecommand.

    So in other words, it happens in the opposite order than what your post's title describes. First the (stil-empty) "$TEST" is expanded into "", then echo "" is run with an additional TEST=... environment variable which it does not care about.

    Since your test commands are supposed to imitate the behavior of Ansible, they should be actually looking at their environment variables and not relying on pre-expansion by the shell; something like VAR=value env or VAR=value printenv VAR would be a much better fit.

    20

    Why does the following not work in bash?

    echo "Hello world!" > test.txt && TEST="$(cat test.txt)" echo "$TEST"
    

    If you run it though ShellCheck – shell script analysis tool it will tell you why:

    $ shellcheck myscript
     
    Line 2:
    echo "Hello world!" > test.txt && TEST="$(cat test.txt)" echo "$TEST"
                                      ^-- SC2097 (warning): This assignment is only seen by the forked process.
    >>                                                             ^-- SC2098 (warning): This expansion will not see the mentioned assignment.
    

    See ShellCheck: SC2097 – This assignment is only seen by the forked process.

    0
    -2

    Based on the link provided by @DavidPostill, I would use

    echo "Hello world!" > test.txt && TEST="$(cat test.txt)" && echo "$TEST"
    

    DUE EDIT
    As many users rightly pointed out, mine is far to be a useful answer: It allows no one to learn anything about the topic.
    Please, refer instead to @TOOGAM's post below, which is the best answer (my personal opinion) and fully exposes why @Peter Mortensen's code doesn't work, how bash variabile expansion works and even why my use of a second && Is not the optimal choice.

    6
    • 1
      That works.....
      – DavidPostill
      Commented Mar 27 at 13:02
    • 9
      While this works, it doesn't actually answer the question... The question is why it doesn't work rather than how to make it work Commented Mar 28 at 11:33
    • 9
      Don't just post code. Explain what was wrong and how your solution fixes it.
      – Barmar
      Commented Mar 28 at 15:13
    • 1
      @ScottishTapWater and @ Barmar, you're absolutely right, my bad. Fortunately, @ TOOGAM posted an outstanding answer !!! By far the best one (my opinion)
      – sqrt-1
      Commented Mar 28 at 15:29
    • 4
      Yep, and unfortunately SO still lacks a way for the community to override the accepted answer Commented Mar 28 at 16:20

    You must log in to answer this question.

    Not the answer you're looking for? Browse other questions tagged .