0

Let me paint a picture for your. You write some deployment scripts or build scripts or somethings etc. which run commands on the remote to do things like e.g. create users, install/update package etc. and thus you might be worried that maybe you forgot to add that --gecos '' to your adduser or didn't provide that DEBIAN_FRONTEND=noninteractive for all of your apts or something that you might not even be aware of. Now, such scripts tend to be non-interactive. You don't want to be prompted for anything and in case for some reason prompting happens, you would like the whole operation to fail. However, what I'm seeing with bash in such scenarios is that instead of failing and returning a non 0 error code (even if I set -e), it does the worst possible thing. It interrupts and returns 0. This means the caller has no way of knowing that the script did not complete properly, but in fact was interrupted.

Here's a silly example to easily illustrate and toy with the problem. You can imagine bunch of stuff going on around this script. Maybe this bit code is actually executed using ssh on the remote, installing some packages instead of read line etc. and after this block you expect that all the code ran successfully.

set -e
# Do stuf...

/bin/bash <<SH
    echo gonna try to be interactive
    read line #tries to read a line from "user"
    echo you wont see me #Will not be executed as Bash will halt execution on previous failing line, this could be vital bit of code to execute, but it's now silently skipped
SH
echo return value $?
#I want to stop here if the above bit of bash failed because some code tried to get user input

Output

gonna try to be interactive
return value 0

I already noticed that if I use bin/sh instead of bash I get a different behaviour. It does not automatically immediately stop execution on the read and return 0. In fact by default it will just move on and I need to explicitly set -e to make it "short circuit" and with that it in fact does then give me return value 1. But is there any way to make this work sanely with bash?

4
  • 1
    Where do you think the input to read comes from when you've redirected the stdin of the shell from a here-doc? Try to add something like echo hello \$line as the last line before the SH delimiter.
    – ilkkachu
    Sep 6 '20 at 11:20
  • I don't understand the question. What do you mean by “try to be interactive”? Are you mixing up being interactive and reading data with read? read has nothing to do with user interaction: it reads a line from its input, which is usually a file or pipe, not something the user types. What are you trying to achieve? Sep 6 '20 at 11:35
  • Sorry for not being clear. I don't want to read anything. That's just an example of code which tries to get user input. I'll elaborate this a bit further in the question.
    – Timo
    Sep 6 '20 at 15:37
  • @Gilles'SO-stopbeingevil' if it makes it easier you can substitute read line with adduser testuser or something like that. It's just slightly inconvenient to have to clean up after testing adduser has created that user..
    – Timo
    Sep 11 '20 at 21:00
1

Like with any other command, you need to either explicitly check the return status of read or use set -e. set -e isn't a panacea, but it's better than not using it.

No language feature¹ will reliably protect you against code that is meaningful but doesn't do what you wanted it to do. Your code doesn't signal an error because the computer, like it always does, is doing what you told it to do, not what you wanted it to do.

/bin/bash <<SH

Bash's standard input is connected to a file that contains the here-document content (either a temporary file or a pipe depending on which shell is running).

read line #tries to read a line from "user"

No, it doesn't “read a line from user”. It reads a line from standard input. Which is the here-document. The effect depends on how the shell parses the script: that's why replacing /bin/bash by /bin/sh changed the behavior. With this particular script and this particular version of bash, bash only reads the script one line at a time. So when you tell it to read a line, it does this and sets line to echo you wont see me #Will not be executed as Bash will halt execution on previous failing line, this could be vital bit of code to execute, but it's now silently skipped. Once the read command has completed successfully, bash executes the next line. Since it's reached the end of the script, it returns the status of the last command, which is 0.

To confirm what is going on, add another line of code:

set -e
# Do stuf...

/bin/bash <<'SH'
    echo gonna try to be interactive
    read line #tries to read a line from "user"
    echo you wont see me #Will not be executed as Bash will halt execution on previous failing line, this could be vital bit of code to execute, but it's now silently skipped
    echo "Previous command='$_'; status=$?; line='$line'"
SH
echo return value $?

Output:

gonna try to be interactive
Previous command='line'; status=0; line='echo you wont see me #Will not be executed as Bash will halt execution on previous failing line, this could be vital bit of code to execute, but it's now silently skipped'
return value 0

The command is incorrect. The line did not fail and bash did not halt execution. The line was skipped because you told bash to read it as data.

You're observing a different behavior in dash because it read the whole script before executing it. You'd also observe that in bash if, for example, the code was in a function.

If you want the child shell to read from the parent's standard input, don't redirect the child shell's standard input, or make the parent's standard input available on another file descriptor.


You write some deployment scripts or build scripts or somethings etc. which run commands on the remote to do things like e.g. create users, install/update package etc. and thus you might be worried that maybe you forgot to add that --gecos '' to your adduser or didn't provide that DEBIAN_FRONTEND=noninteractive for all of your apts or something that you might not even be aware of. Now, such scripts tend to be non-interactive. You don't want to be prompted for anything and in case for some reason prompting happens, you would like the whole operation to fail.

This is a completely different problem. Make sure that your deployment script's standard input is connected to /dev/null. This way, if anything tries to read from standard input, the read will fail

If the deployment program attempts to read from the wrong place, or if it doesn't react correctly to a read failure, that's a bug in the deployment program which environmental preparation can't fix.

¹ Short of proving that the code meets a specification, but if you wanted to do that you wouldn't be using a scripting language.

6
  • I see, thank you for this insanely detailed explanation of what was going on with that example. It may not have been exactly what I was after, but certainly fascinating and greatly helpful. And I guess it does in fact explain the underlying root cause of my issues with adduser as well. It seems that reading the gecos for it causes the same thing to happen. It is reading the remaning heredoc code as an input for the command and depending on the content it may crash or it may pass, in which case it also skips executing the lines then, but gives 0 as return value.
    – Timo
    Sep 14 '20 at 6:54
  • I guess my follow up question then be that is there a way to make bash behave like dash in this regard, i.e. read the heredoc code in completely (I suppose even dash wont do this if the script is huge, but usually isn't or you'd probably want to put it into another file then)?
    – Timo
    Sep 14 '20 at 6:58
  • @Timo Don't pass a script to a nested shell as a here-document and you won't have this problem. If you really must pass a script to a nested shell as a here-document (and the only common reason to do that is that it's convenient when the nested shell is called via ssh), you can put the whole code in a function and make the last line call this function, so at least you'll be protected from commands that don't attempt to seek. But that's protecting you from yourself: the correct solution is to disconnect stdin. Put exec </dev/null at the top of the nested shell script. Sep 14 '20 at 7:15
  • Ooh, those sound like really good suggestions. I need to experiment with them. How about doing something like this, do you see any problems with this approach? code=$(cat <<SH \n set -e \n echo gonna read a line \n read line #should now fail 'cos stdin is not connected \n echo will not execute \n echo also wont execute \n SH \n ) \n /bin/bash -c "$code" < /dev/null \n echo retval $? Oof, sry bout the formatting..
    – Timo
    Sep 14 '20 at 7:29
  • @Timo That's a good way to avoid both quoting difficulties (which you'd run into if you put all your code in -c '…') and the pitfalls of passing code on stdin. Except make this cat <<'SH', otherwise the code isn't what it looks like. Sep 14 '20 at 7:30

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.