14

the following script with debug option 'set -e -v' fails at the increment operator only when the variable has a prior value of zero.

#!/bin/bash
set -e -v
i=1; let i++; echo "I am still here"
i=0; let i++; echo "I am still here"

i=0; ((i++)); echo "I am still here"

bash (GNU bash, version 4.0.33(1)-release (x86_64-apple-darwin10) but also GNU bash, version 4.2.4(1)-release (x86_64-unknown-linux-gnu))

any ideas?

Arnaud Le Blanc
  • 98,321
  • 23
  • 206
  • 194
bliako
  • 977
  • 1
  • 5
  • 16
  • 2
    I never would have guessed an innocuous looking increment would have had this unintended interaction with `set -e`. Lucky I found this question. – Bryce Thomas Jun 28 '14 at 17:12

3 Answers3

23

the answer to my question is not to use let (or shift, or...) but to use

i=$((i+1))

when trying to check a bash script by setting 'exit on non-zero status code' with

set -e

The bash manual states that set -e has the effect of 'Exit immediately if a simple command exits with a non-zero status.'.

Unfortunately let (and shift and ...) return the result of the computation ('If the last arg evaluates to 0, let returns 1; 0 is returned otherwise'). So instead of a status code one gets a return value of some sort. And sometimes this return value will be zero and sometimes one depending on the computation. Therefore set -e will cause the script to exit depending on the result of your computation!!! and there is nothing to do about it unless either you don't use it ever or resort to

let i++ || true

as pointed by arnaud576875 which btw adds extra CPU burden.

Using

let ++i

works only for the specific case that i is not -1, as with let i++ which works only for when i is not 0. Therefore half-solutions.

I love Unix though, I wouldn't have it any other way.

bliako
  • 977
  • 1
  • 5
  • 16
  • +1 This answer is the correct one. I have made a test script to check the other increment options I found on internet and SO (about 10). An online script is located here: http://goo.gl/XYzrK2 – user9869932 Dec 29 '15 at 00:48
10

If the last argument of let evaluates to 0, let returns 1 (so, a non-zero status):

From the manual:

   let arg [arg ...]

Each arg is an arithmetic expression to be evaluated. If the last arg evaluates to 0, let returns 1; 0 is returned otherwise.

i++ evaluates to zero when i is 0 (because it's a post-increment, so the previous value of i is returned), so let returns 1, and due to set -e, bash exists.

Here are some solutions:

let ++i         # pre-increment, if you expect `i` to never be -1
let i++ 1       # add an expression evaluating to non-zero
let i++ || true # call true if let returns non-zero
Community
  • 1
  • 1
Arnaud Le Blanc
  • 98,321
  • 23
  • 206
  • 194
1

Looking at the BASH manpage on the set -e:

Exit immediately if a simple command (see SHELL GRAMMAR above) exits with a non-zero status. [...]

So, if any statement returns a non-zero exit code, the shell will exit.

Taking a look at the BASH manpage, on the let command:

If the last arg evaluates to 0, let returns 1; 0 is returned otherwise.

But wait! The answer to i++ is a one and not a zero! It should have worked!

Again, the answer is with the BASH manpage on the increment operator:

id++ id--: variable post-increment and post-decrement

Okay, not so clear. Try this shell script:

#!/bin/bash
set -e -v
i=1; let ++i; echo "I am still here"
i=0; let ++i; echo "I am still here"

i=0; ((++i)); echo "I am still here"

Hmmm... that works as expected, and all I did was change i++ to ++i in each line.

The i++ is a post-increment operator. That means, it increments i after the let statement returns a value. Since i was zero before being incremented, the let statement returns a non-zero value.

However, the ++i is a pre-increment operator. That means it increments i before returning the exit status. Since i is incremented to a 1, the exit status becomes a zero.

I hope this makes sense.

David W.
  • 105,218
  • 39
  • 216
  • 337
  • 1
    what it doesn't make sense is that let, set -e and reasonable humans are not meant to live together, as no one will write 'let i++ || true' unless as an email signature. – bliako Aug 30 '11 at 18:37
  • It's the fact that `i++` isn't an increment operator, but a ***post-increment*** operator that throws people. Most people don't know it unless they're C programmers and they've seen this: `foo = bar + i++;` or `oldfoo = foo++` enough times to give them a brain tumor. – David W. Aug 30 '11 at 19:21
  • No is not that, it's the fact that a status code, which 'set -e' uses for checking health, whether the operation was completed successful or not, is used for relaying information about the result of the computation. Developers thought it a feature not a bug to introduce, a la: let i++ || echo 'look no hands', but the end result is that we need to rewrite code in order to debug - in fact i don't trust this anymore because a lot more operations are following this. And by the way, as a matter of consistency should the bash for((i=0;i<10;i++)) have the same BOFH effect? sneaky man, sneaky. – bliako Aug 30 '11 at 19:44
  • Right, it's a status code! The status code is returned _before_ the increment is done since this is a _post-increment_ operator. Since `i` is zero, the status code is `1`. Makes perfect sense -- especially after a bout of heavy drinking. I agree it is confusing and causes lots of errors. It's why I tell people not to use `i++`, but `++i` instead because it has none of these interesting _side effects_. Or even better `i=$i+i` which probably is just as efficient and is crystal clear what you want. – David W. Aug 30 '11 at 20:21