7

I have a complex script where I generally test each command individually for success. However, there are cases where I have to carry out a sequence of simple operations, and I'd prefer to get an exit status from the entire sequence, without bothering to check each command individually.

This is my first attempt, with some arbitrary commands:

#!/bin/bash
if ! (
    set -e
    cd test
    touch foo
    chown root:root foo)
then
    echo "subshell failed"
else
    echo "subshell completed"
fi

The set -e was meant to ensure that the subshell exited with failure when the first failure was encountered, but this doesn't work. In this case, if directory test doesn't exist, the script simply creates foo in the current directory, and then fails on the chown.

What's the right way to do this? In other words, echo 'subshell completed' only if all 3 commands completed without error?

3
  • 1
    This is one of the (many!) serious pitfalls with set -e covered in BashFAQ #105. Commented Jun 6 at 3:12
  • 3
    BTW, as opposed to a && b && c, consider a || exit; b || exit; c || exit -- that way you can decide to modify behavior for any particular element without needing to worry about breaking a chain. (When in a function but not in a subshell, replace || exit with || return as appropriate). Commented Jun 6 at 3:13
  • Easier for others to reproduce as if (set -e; false; true); then echo success; else echo failed; fi. Commented Jun 6 at 12:08

2 Answers 2

11

The right way is to check the exit status of each command and do whatever cleanup is appropriate at that point. (Along with printing an appropriate error message, but the tools themselves likely do that already.) For example, if the file gets created, but the permissions can't be set properly, do you want to leave the file there or should you (try to) remove it?

(Granted, this particular case would be a bit unexpected, but anyway.)

If you don't want to be that fancy, then just say you want to run each command only if the previous one succeeds, i.e. join them with &&:

file=foo
if  cd test &&
    touch "$file" &&
    chown root:root "$file"
then
    echo ok.
else
    echo something failed.
fi

Note that that would leave the script running in the newly-created directory. Which may or may not be what you want to do, depending on what the script does next. Of course you could still use a subshell to isolate the effects of the cd:

if  ( cd test &&
      touch "$file" &&
      chown root:root "$file" )
then ...

or just do away with the cd entirely:

filepath="test/$file"
if  touch "$filepath" &&
    chown root:root "$filepath" )
then ...

(don't use path as a variable name if you think you'll ever use zsh; it'll blow up hilariously.)

In any case, forget about set -e. It doesn't do what you want and is confusing enough to likely be worse than useless. See e.g. BashFAQ 105

1
  • 1
    These seem to be the only options - though it's easy to forget && or || exit at the end of any given line and inadvertently allow a command to fail. I don't have a solution for that other than good code reviewers. Commented Jun 7 at 7:34
5

Re: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html

The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.

Emphasis mine.

8
  • 1
    That's not what we're talking about here - the subshell itself is the test part of the if, and we want to test its exit status. However, the commands within the subshell aren't in such a context, and we want failure of any of them to cause the subshell to return exit status (to be tested by if). Commented Jun 6 at 12:03
  • Oh, it seems I'm mistaken - the sub-shell inherits that context from its parent, probably just via fork(). Commented Jun 6 at 12:06
  • 1
    Yeah, I thought that, too. But when I thought about it it seemed that in order for the parent shell to "know" it has to pass this context in. But rules around subshells are... interesting.
    – user612597
    Commented Jun 6 at 14:46
  • 2
    Now, one could do if sh -c 'set -e; false; echo hi'; then true; fi and there, the child shell would not know or care anything about the surrounding test context, so that might do what one wants. But rather than running nested shells, it's likely better to just forget about set -e and be done with it.
    – ilkkachu
    Commented Jun 6 at 19:56
  • 1
    @TobySpeight, yes, it's quite possible it's just an accident of the original / some early implementation. But by now the behaviour is such in every shell, and it's enshrined in the POSIX text, so we're forever stuck with it. ":)" (Somewhat similarly to word splitting. And the ls behaviour on dotfiles also comes to mind as something described a sort of an accident originally.) In the case of set -e, even zsh doesn't seem to have tried to make it more sensible.
    – ilkkachu
    Commented Jun 7 at 9:16

You must log in to answer this question.

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