Approach Bash Like a Developer - Part 15 - Strict Mode Caveats
This is part fifteen of a series on how to approach bash programming in a way that’s safer and more structured than your basic script.
See part 1 if you want to catch the series from the start.
Last time, we updated our outline script with strict mode. This time, let’s discuss a couple more caveats about strict mode.
As mentioned in the discussion on strict mode, the left-hand side of a boolean && will not trigger errexit. However, there can still be an issue with errexit if the last line in your function is a boolean && expression.
When the left-hand of the expression returns false, the right-hand portion will not be executed. Since it is the last line in your function, the function will then return, and it will return the value of the last command executed. That’s the conditional which returned false.
Even though the function will have executed correctly, it will return false, and that return will cause errexit to trigger.
For this reason, you’ll want to add a final true to the function if you have a boolean && as your last line. Usually I just add a semicolon and colon to the end of that line:
: is a bashism which is the same as true. It’s appropriate here since we’re keeping the code cleaner.
Another alternative is to use an if then statement instead of a boolean &&. The if then never returns the value of the condition as its own return value.
Crack the Return Code
Saving the return code of a command which has failed is also a challenge with strict mode. You can always toggle strict mode, but you can also use boolean suspension. There are two other options.
One trick is to cover both alternatives:
Whether command succeeds or fails, the result code will be captured, and errexit is suspended by the &&.
The other option is to use negation, which also defeats errexit:
In this case, $? will be the opposite of the actual return code, i.e. it will be 0 if command threw an error and 1 if it didn’t. That means you lose the actual error code returned, if it was an error, but if all you care about is whether an error was thrown or not, it works.
Return Codes on Your Functions
The command && rc=$? || rc=$? trick works well for external and builtin commands, since they don’t need errexit to detect when one of their own internal steps goes wrong. Your functions, however, do.
If you are coding your functions conscientiously, that means they are written to detect their own error conditions and return an appropriate code as the return value, rather than stop the script.
However, you still want errexit to work correctly so that the error cases you haven’t detected with your code still stops the script. This allows you to debug the script, and to prevent it from continuing with faulty assumptions about the state of things.
In the past, I used to write functions so that they returned error codes when appropriate, and then used an || to take action if they did:
That method suspends errexit, which causes the issues I just mentioned.
Instead, I now write the function to return an error code in a designated global variable rather than return an actual error code.
I use _err_ for that purpose. This means that the function doesn’t have to be tested with a boolean ||. Instead I check the variable after the function has finished:
So in detectable error scenarios, I write the function to return a 0 return code and instead set _err_, which thus doesn’t trip errexit.
In order for this to work, you have to remember to set _err_=0 at the beginning of your function so you don’t accidentally get the last function’s value for _err_.
I also pretty it up with an alias:
The (exit $_err_) is to reset the error value to the one passed via _err_, which can then be picked up by die or whatever function/command you choose to use on the right-hand side of the ||.
Set on You
Aside from errexit, nounset can be tricky as well. Any reference to an unset variable will exit. If you can’t be sure that the variable will exist, you can always use bash’s default value parameter expansion like so:
A default value (after :- above) will prevent bash from flagging the variable as unset and stopping the script.
Hopefully your variables will exist most of the time, so the need for this technique should be minimal.
It can, however, be useful when you are assigning positional arguments to variable names in a function:
This way, arg1 is guaranteed to exist and can be referenced without fear of triggering nounset, even if no argument was supplied in $1.
Continue with part 16 - recap