Differences between revisions 2 and 3
Revision 2 as of 2011-08-12 12:54:11
Size: 1657
Editor: GreyCat
Comment: bash 4.1 changed the answer to exercise 2
Revision 3 as of 2013-01-14 19:54:52
Size: 3583
Editor: GreyCat
Comment:
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
The correct answer to every exercise is actually "because `set -e` is crap".

However, some people want more detailed explanations. So, here you go:

------
Line 15: Line 20:
''Exercise 2: why does this one appear to work?'' ''Exercise 2: why does '''this''' one sometimes appear to work? In which versions of bash does it work, and in which versions does it fail?''
Line 29: Line 34:

------
''Exercise 3: why aren't these two scripts identical?''
{{{#!highlight bash
#!/bin/bash
set -e
test -d nosuchdir && echo no dir
echo survived
}}}

{{{#!highlight bash
#!/bin/bash
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
}}}

In the first script, the `test` command is "part of any command executed in a && or || list except the command following the final && or ||" (Bash 4.2 man page), so it does not cause the shell to exit.

In the second script, that is also true, so the shell does not exit immediately after the `test ...&&` command. However, the function `f` returns 1 (failure) because that was the exit status of the last command executed in the function. The simple command `f` in the main body of the script therefore returns 1 (failure), which causes the shell to exit.

------
''Exercise 4: why aren't '''these''' two scripts identical?''
{{{#!highlight bash
set -e
f() { test -d nosuchdir && echo no dir; }
f
echo survived
}}}

{{{#!highlight bash
set -e
f() { if test -d nosuchdir; then echo no dir; fi; }
f
echo survived
}}}

The first script above is the same as the second script from exercise 3. See previous answer for an explanation of that one.

In the second script, we observe one of the ways in which `if` and `&&` are not the same. In the manual, under Compound Commands, we find this sentence in the definition of `if`:

 . The exit status is the exit status of the last command executed, or zero if no condition tested true.

Since the `test` is not true, and no commands are executed, `if` must return 0. This means `f` returns 0, and the shell does not exit.

The correct answer to every exercise is actually "because set -e is crap".

However, some people want more detailed explanations. So, here you go:


Exercise 1: why doesn't this example print anything?

   1 #!/bin/bash
   2 set -e
   3 i=0
   4 let i++
   5 echo "i is $i"

According to the manual, set -e exits "if a simple command (see SHELL GRAMMAR above) exits with a non-zero status. 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 a if statement, part of an && or || list, or if the command's return value is being inverted via !".

The let command is a simple command, and it doesn't qualify for any of the exceptions in the above list. Moreover, help let tells us "If the last ARG evaluates to 0, let returns 1; 0 is returned otherwise." i++ evaluates to 0, so let i++ returns 1 and trips the set -e. The script aborts. Because we added 1 to a variable.


Exercise 2: why does this one sometimes appear to work? In which versions of bash does it work, and in which versions does it fail?

   1 #!/bin/bash
   2 set -e
   3 i=0
   4 ((i++))
   5 echo "i is $i"

((...)) does not qualify as a simple command according to the shell grammar. So it is not eligible to trigger a set -e abort, even though it still returns 1 in this particular instance (because i++ evaluates to 0 while setting i to 1, and because 0 is considered false in a math context).

However, this behavior changed in bash 4.1. Exercise 2 works only in bash 4.0 and earlier! In bash 4.1, ((...)) qualifies for set -e abortion, and this exercise will print nothing, the same as Exercise 1.

This reinforces my point about how unreliable set -e is. You can't even count on it to behave consistently across point-releases of a shell.


Exercise 3: why aren't these two scripts identical?

   1 #!/bin/bash
   2 set -e
   3 test -d nosuchdir && echo no dir
   4 echo survived

   1 #!/bin/bash
   2 set -e
   3 f() { test -d nosuchdir && echo no dir; }
   4 f
   5 echo survived

In the first script, the test command is "part of any command executed in a && or || list except the command following the final && or ||" (Bash 4.2 man page), so it does not cause the shell to exit.

In the second script, that is also true, so the shell does not exit immediately after the test ...&& command. However, the function f returns 1 (failure) because that was the exit status of the last command executed in the function. The simple command f in the main body of the script therefore returns 1 (failure), which causes the shell to exit.


Exercise 4: why aren't these two scripts identical?

   1 set -e
   2 f() { test -d nosuchdir && echo no dir; }
   3 f
   4 echo survived

   1 set -e
   2 f() { if test -d nosuchdir; then echo no dir; fi; }
   3 f
   4 echo survived

The first script above is the same as the second script from exercise 3. See previous answer for an explanation of that one.

In the second script, we observe one of the ways in which if and && are not the same. In the manual, under Compound Commands, we find this sentence in the definition of if:

  • The exit status is the exit status of the last command executed, or zero if no condition tested true.

Since the test is not true, and no commands are executed, if must return 0. This means f returns 0, and the shell does not exit.

BashFAQ/105/Answers (last edited 2022-08-26 20:03:23 by 138)