Why doesn't Bash’s ‘set -e’ do what I expected? (2021)
mywiki.wooledge.org
mywiki.wooledge.org
This prints “a”, as you’d expect:
set -e
myfun() { printf a; false; printf b; }
myfun
printf c
...because the “set -e” terminates the whole script immediately after running the “false” command.But this script prints “a-b-True”, where some people (myself included) might have expected it to print “a-False”:
set -e
myfun() { printf a-; false; printf b-; }
if myfun; then
printf True
else
printf False
fi
The “set -e” is ignored completely. Putting another “set -e” inside the definition of myfun doesn’t make any difference.I encountered the "you can't enable -e" problem years ago in a complex build system that used Bash to build embedded sysem components. The scripts were all designed on the assumption that "set -eu -o pipefail" would cause early exit on any failing commands as if doing the equivalent of "exit $?" or "return $?", and of course it is not reliable, as your example shows.
The very surprising behaviour of "set -e" inside functions was not noticed for a long time while the system was being written and used. When it was noticed, "set -e" was added inside the functions because that seemed to work for the Bash version at the time, then later it stopped working.
This is harder than it sounds! :) There's arguably not a natural behavior for shell, because $? is overloaded in Unix. It can mean success/failure or it can mean true/false/error.
https://www.oilshell.org/release/latest/doc/error-handling.h...
Oil introduces some new idioms that led you distinguish between the two paradigms, so it's best to change the way you write scripts. It's not something bash can magically fix.
https://www.oilshell.org/release/latest/doc/idioms.html#erro...
That said, just running your scripts with Oil will flag problems, acting like a ShellCheck at runtime, catching things that ShellCheck can't.
See the sibling reply for a demo, and this comment:
https://www.oilshell.org/release/latest/doc/error-handling.h...
Here's a demo of how Oil fixes it, which I mentioned in this thread:
$ osh hn.sh
a-b-True
You can add shopt --set strict_errexit at the top of your script: $ osh -o strict_errexit hn.sh
if myfun; then
^~
hn.sh:3: errexit was disabled for this construct
if myfun; then
^~~~~
hn.sh:3: fatal: Can't run a proc while errexit is disabled. Use 'try' or wrap it in a process with $0 myproc
Or just use Oil: $ oil hn.sh
<same output>
So Oil is acting like a ShellCheck at runtime, to unconditionally flag this error. It cannot be caught using syntax (which ShellCheck is limited to), because function vs. external executable is a runtime decision.After a while I stumbled upon scsh [1], which at first didn't impress me because I ran it as an interactive shell, and from this point of view it was really ugly. But then I realized that scsh was primarily meant as a way to run shell scripts, and I immediately felt in love. I had the power of a Scheme interpreter, the ability to easily use mathematical expressions (the awesomeness of Scheme's numerical tower!) and macros, and a very effective way to redirect inputs and outputs and pipe commands that was embedded in the language [2]!
In those years I used scsh a lot and developed quite complex scripts using it, it was really a godsend. Unfortunately the program got abandoned around 2006 because it was not trivial to add support for 64-bit architectures. However, while writing this post I've just discovered that somebody has revived the project and enabled 64-bit compilation [3]. I would love to see a revamp! Nowadays I use Python for complex scripts, but it's not the same as a language with native support for redirection and pipes!
I have also been forced to revert to bash later so it is good to know that there might be an update for it.
While this is no doubt right, I think you could build a little abstraction that simplified piping syntax considerable. The subprocess module is not terribly convenient, although it has gotten less confusing over the years.
Python does have operator overloading, so you could even make it fancy.
This is an important point for scripting as nobody wants to setup a venv to run a simple script.
Zippapp are a solutions to that but most people don't know abot them. Also, it's not convenient enough to make at the moment.
That is excellent news, if it comes to anything. At present it looks like it's still dependent on the 32-bit Scheme48. Is there a roadmap for this project?
> Nowadays I use Python for complex scripts, but it's not the same as a language with native support for redirection and pipes!
Note that Guile has support for both these. But it doesn't have the shell-tool minilanguages in it that scsh does, like the one for awk.
For simplish parallel runner https://www.gnu.org/software/parallel/ is excellent, can even automate stuff like "copy a file to remote server then run command on it" so you can make simplistic cluster computing with it
e.g. `ls old/*.jpeg | parallel mv {} new/{/.}.jpg` is easier than a bash loop.[1]
[1] Yes, it should be `find -print0` or something, blah blah, it's an example.
It runs your bash scripts but you can also opt into correct error handling. The simple invariant is that it doesn't lose an exit code, and non-zero is fatal by default.
See https://www.oilshell.org/release/latest/doc/error-handling.h...
Oil 0.10.0 - Can Unix Shell Error Handling Be Fixed Once and For All?
https://www.oilshell.org/blog/2022/05/release-0.10.0.html
This took quite awhile, and I became aware of more error handling gotchas than I knew about when starting the project:
e.g. it's impossible in bash to even see the error status of a process sub like diff <(sort left.txt) <(sort OOPS)
If you have bash scripts that you don't want to rewrite, try
1) run them with OSH
2) Add shopt --set oil:upgrade at the top to get the error handling fixes.
Tell me what happens :) https://github.com/oilshell/oil
I spent a long time on that, but the post didn't get read much. I think it's because it takes a lot of bash experience to even understand what the problem is.
(rehash of https://news.ycombinator.com/item?id=33075915 which came up the other day :) )
> … and it's a new language for Python and JavaScript users who avoid shell!
Why not work towards using python/JavaScript as shell languages? There was a HN thread a few days ago on Xonsh, the main python attempt at this which looks nice to me, which naturally got a lot of anti-anti-bash energy. But if we want better shell ergonomics and are willing to ditch bash, why not take popular versatile and capable languages and get them shell-appropriate? It seems like a natural use case for these languages, and even the place where they could retire to peacefully in the future.
I suppose OSH is aiming at bash compatibility, but how good or viable is that really (not a rhetorical question)? Seems to me like an all or nothing thing as someone who recently ran into some bash-zsh incompatibility.
~~~
HN thread on Xonsh: https://news.ycombinator.com/item?id=33044772
Xonsh homepage: https://xon.sh/
https://www.oilshell.org/blog/2021/01/why-a-new-shell.html#i...
Note that osh is (really very nearly[1]) bash compatible by default, but you can opt in to incompatible parts. This lets you convert a large body of shell code gradually.
1: I think the only incompatibility is that in osh you can't index into associative arrays with unquoted words. IIRC this was to make parsing of osh decidable.
I always refer to this awesome answer to that question:
> I have the feeling that trying to address these points by bolting features or DSLs onto a general-purpose programming language doesn't work. At least, I have yet to see a convincing implementation of it.
… and what is worth doing, a new shell or new features in an old language, which isn’t addressed by that post AFAICT.
https://github.com/oilshell/oil/wiki/Internal-DSLs-for-Shell
https://github.com/oilshell/oil/wiki/Alternative-Shells
It doesn't mean they aren't useful, but we still need a better successor to Bourne shell / bash.
https://news.ycombinator.com/item?id=33122256
They're separate things because introducing MORE error handling can make your script incompatible with bash, for people who want to write code that's portable to 2 shells :-/
set -e comes from the POSIX shell language, descended from the Bourne Shell.
The examples you're using use obscure Bash features. For instance let turns an arithmetic result into a termination status, where 0 is fail (opposite to the POSIX convention and all).
set -e works to the extent that your commands have a sane termination status, which they generally do if they are standard built-ins or well-behaved utilities.
One Bash feature improves the effectiveness of set -e (or exit status testing in general). In a command pipe:
a | b | .. | z
the termination status is obtained from z. That's standard. If z indicates success, the pipeline is successful no matter how a through y terminate. Bash has a "pipefail" option to help with this.Is read(1) an obscure feature now?
Example here: https://news.ycombinator.com/item?id=33122256
Let's hope it's not bash scripts you're writing :-)
The answers there are just a few of the many great examples of why bash is best avoided in almost all cases.
If you are wondering what in the world I'm talking about and I've missed your question entirely, sorry about that. My feet are tired.
This is the expectation, and the article is about why it's not that simple.
On the one hand it is nice to have your interactive interface and your scripts be the same language. on the other it is a bit horrifying the way all those convenient interactive features make your programs so prone to failure.
I wouldn't mind it if all scripting in the shell had to be offloaded to a completely separate language that was less error prone and more consistent.
Seems kind of logical, the following c code behaves the same way.
#include <stdio.h>
int test(void) {
int i=0;
return i++;
}
void main(void) {
printf("i = %d\n", test());
}
'let i++' evaluates to / returns 0 before increasing i, just like c would. if cond; then x; else y; fi
does not do the same thing as if ! cond; then y; else x; fi
despite the the absence of any operator overloading. $ if false; then :; else (if [ $? -eq 0 ]; then echo msg; fi); fi
$ if ! false; then (if [ $? -eq 0 ]; then echo msg; fi); else :; fi
msgYou should do
if ARG=false
Then use $ARG not $? in the if statement.
I can't think of an example of the if statement being confusing that isn't relying on $?
error=0
if ! some_command; then error=$?; bar; fi
echo "blah"
return $errorStatus=0
Somecommand || status=$?
Then do logic based on status
false the command returns 1, ! the command turns a 0 return code into a 1 and anything other than a zero into a 0, if is vaguely backwards from most languages in that it deals in return codes a rc of 0 is true and anything else is false
in the first case false has a rc of 1 this will execute the else list then the test command 1 is equal to 0(rc = 1 false list) so "msg" does not print
in the second case false(rc 1), !(rc 0), if(true list) now you check if rc is 0, it is and "msg" is printed.
I don't know what kind of shell scripts the designers of this write, but the number of times I've wanted or found it helpful for ! to modify $? is has been exactly zero.
I am almost certain that >99% of people who write shell scripts do not find this intuitive.
I don't think that is true, consider:
# if false ; then echo "true $?" ; else echo "false $?" ; fi
false 1
# if ! false ; then echo "true $?" ; else echo "false $?" ; fi
true 0
# if ! ! false ; then echo "true $?" ; else echo "false $?" ; fi
false 1
Basically, $? evaluates to the full condition inside the "if" sentence.One of the first things I learned when starting to use $? was to assign it to a variable asap if I needed to use it more than once.
Of course
cond = false
if cond; then x; else (if cond; then echo msg); fi
does not do the same thing as cond = true
if cond; then (if cond; then echo msg); else x; fi
Bash is not somehow strange in this regard./joke
The page is probably oriented at situations where one needs to support any version of bash and any obscure/inadvisable functionality (mind that there are still devs that mix sh and bash syntax in the same script), which is very inconvenient and error prone, so manual error handling may make sense.
While there is always something insane behind the corner in Bash, with a restricted subdomain (e.g. bash 4.2+, strict shell options, and shellcheck) it's possible to progressively write reasonably solid shell scripts.
The document conclusion is somewhat biased. "to handle errors properly" implies that `-e` in inherently unreliable, which not fair - strict shell options do remove certain classes of errors, which doesn't hurt.
https://blog.habets.se/2021/06/The-uselessness-of-bash.html
C++ can be used correctly. It's easier since C++11, with smart pointers and stuff. I'm not going to argue that writing solid C++ is easy, but it IS possible.
Bash basically can't be used correctly. Even a simple command pipeline of three commands, with actual error handling, is probably more code in bash than in any other language.
> C++ can be used correctly. It's easier since C++11, with smart pointers and stuff. I'm not going to argue that writing solid C++ is easy, but it IS possible.
It seems this conflates two different aspects of programming languages - complexity and (memory) safety.
The analogy with Bash was in relation to the language complexity. By reducing the language features to a restricted, well-known (by the given team) scope, it's possible to use the language effectively. A very good example of this is the Doom 3 source code.
In relation to memory safety, Google hasn't been able to develop a safe enough Bluetooth stack, which has been a dumpster fire. Due to the small support windows of Android, as of today, a huge amount of devices with older Android versions are either in the garbage, or are extremely unsafe. If Google, with their engineering practices, can't develop a safe Bluetooth stack in an unsafe language, realistically, nobody can.
> https://blog.habets.se/2021/06/The-uselessness-of-bash.html
The topic in the post is very interesting (pipes error handling are no doubt a rough area of Bash), but the post takes the choice of being a rant rather than an informative research. The central thesis that "functionality of language X is broken in a 1-line program, but it works in a 83-lines program in language Y" doesn't have any meaningful conclusions. It would have been interesting and informative to compare against the correct version in language X, which in the case of Bash, it does exist (but in this case, it's advanced Bash, and sure, it's ugly).
There is a very wide gap been 1-, 10-, 100- and 1000- line programs, and very different requirements. Asserting that only the 1-line case is the only valid use case for shell scripting, it's inefficient system administration (assuming disciplined shell scripting).
Fair criticism. The difference though being that bash is implying a promise of being able to provide a solution in one line, but if you actually try to make bug free programs it'll end up being just as long as in Go. Without the readability, since you have to jump through crazy amounts of hoops since even the fixes/workarounds for the problems in bash don't have support in the language.
This makes bash's supposed strengths actually weaknesses, even for small programs.
> (pipes error handling are no doubt a rough area of Bash)
But that's my point when I condense this to calling bash useless. Pipes are maybe the core way that one ties together components in bash, and error handling is very important to not have bugs. Unhandled or mishandled errors are bugs.
> Asserting that only the 1-line case is the only valid use case for shell scripting
Well, I'd say that a 1-line case will not be correct, but will work in most day to day, if supervised.
And a correct shellscript can never be 1 line, and making it correct would make it 10s or 100s of lines of unreadable mess, that nobody would actually write or maintain.
Maybe there are wizards out there who write bug free bash. I've never met one. I've met many who think they are.
Most would probably be surprised that they'd have to write at least tens of lines of code to replace their use of the pipe operator, to have bug free code.
He put that into action by making a language so embroiled in its own implementation that cloning a new version would be a pointless exercise in failing to replicate the internal details of the original program. That nicely solved the problem of it being hard to port a script.
Then he made it incredibly hard to build. Cross-compiling it wasn't even a thing, and probably still isn't; we just use distro builds that have Qemu.
https://blog.habets.se/2021/06/The-uselessness-of-bash.html
I've reviewed a lot of code, bash and otherwise. I have never, not once, reviewed bash code that didn't have subtle bugs. And this is code written by smart people.
FTFY.
It really is absolutely terrible tool as programming language. If your script is more than "a bunch of pipes in a trench coat" just use something else. I'd unironically prefer Perl to it.
"Real" programming languages usually have painful-to-use APIs for calling commmands, redirecting output, piping one command to another, etc.
Build scripts that fail and return 0 are my nemesis.
I was also once party to an argument caused by a developer who thought that non-zero exit codes were an acceptable way to communicate success states. I suspect that he may have been trying to communicate different success conditions, but I couldn't tell you for certain.
(from the bash manual)
The ERR trap [same rules as set -e] is not executed if the
failed command is part of the command list immediately following
a while or until keyword, part of the test in an if statement,
part of a command executed in a && or || list except the command
following the final && or || [...]Makes things much saner.
This post is a better introduction than the submission: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_p...
`set -Eeuxo pipefail` will be my new default.
It’s still much better to use it than not to use it.
But I am wondering, does zsh fare better when it comes to writing more correct scripts or is it plagued with the same issues ?
The -e terminates the script at every failure not caught by a try...catch.
With this in mind, it's easier to predict bash's behavior.
and even then, shelling out from another language when absolutely necessary can be a better option
it does change, it just has outstanding backward compatibility and new feature (including parser-level) opt-in per module.
set -euo pipefail
This is how all of my shell scripts start.Yes, they are better than nothing. But as explained in the article it's still like wandering blindfolded into a minefield.
set -e
let 1
let 0
set -e does exactly what you'd expect, arguably, with the exception of subshells and conditions.
And those rules are extremely simple to learn too. If you understand when a statement which might be composed of other statements would have an error, you can predict what set -e will do
#!/usr/bin/env bash
set -e
i=0
let i++
echo "i is $i"Because, this example works exactly the way I would expect. The next one (using the double parentheses syntax) is more questionable, and I could see it going either way, and would either look it up in the manual, or put an `|| true` after it to be safe. The fact that it changed in a minor release is pretty bad though.
> If the last ARG evaluates to 0, let returns 1; let returns 0 otherwise.
Which allows you to do things like:
while let --i; do some command; done
or if let x > 4; then some command; fiI’d expect an arithmetic operation to fail if I, say, divide by 0. Not if I add 1 to zero!
And just because behavior is documented doesn’t mean it’s not surprising or complicated.
From help let:
Exit Status:
If the last ARG evaluates to 0, let returns 1; let returns 0 otherwise..
Since var++ is post-increment, I guess the last argument does evaluate to zero. Subtle...