Bash Error Handling
wizardzines.com
wizardzines.com
- Use shellcheck (static analysis/linter) https://www.shellcheck.net/
- Use shunit2 (unit tests) https://github.com/kward/shunit2
- Use 'local' or 'readonly' to annotate your variables
- Trap ctrl+c to gracefully exit (details here https://www.tothenew.com/blog/foolproof-your-bash-script-som...)
- Stick to long-form options for readability (--delete over -d for example)
- #!/usr/bin/env > #!/bin/bash for portability
- Consider setting variable values in a file and importing it at the top of your script to improve refactoring
- You will eventually forget how your scripts work - seriously consider if Bash is your best option for anything that needs to last a while!
https://www.cons.org/cracauer/sigint.html (section "How to be a proper program")
Most of the time, especially for short lived script, adding signal handlers is not worth the trouble.
If I need more power I usually switch to python. (Probably not relevant to a bash discussion though, sorry)
#!/usr/bin/env bash
is more portable than #!/bin/bash
Though personally if I was worried about portability then I’d just write the script in vanilla sh instead.If this method is superior, why hasn't it caught on with Bash I wonder (it's considered standard with Python and Node at least)?
> Though personally if I was worried about portability then I’d just write the script in vanilla sh instead.
Agreed.
Because if you need a portable bash then you target sh. But if you want a portable Python then you still target Python.
I am still supporting systems that came with Python 2. You get portable Python the same way you get portable bash: build and deploy the interpreter with your code.
The Python 2 problem is a whole other topic :)
> #!/usr/bin/env > #!/bin/bash for portability
Perhaps you misread it? env bash is greater for portability than bin/bash.
I'd rather that most devs don't touch that signal. Using that binding and having a GUI or CLI program continue hanging because the dev screwed up the cleanup is a real pain. And someone writing a Bash script is highly likely for doing something "very clever" with that signal to make my life harder.
Or if you're going to do something with it, at least make it clear you're trolling me. Show me a text add that forces me to choose my favorite Korean boy band before I can exit, or something in that vein.
Yes... but do not take that as a "cargo cult script shebang".
If you're a sysadmin writing a script for a company with 2k linux servers, that has a policy of "we only use linux version Foo X"... and we do not use other bash in the system than /bin/bash (no bash compiled by hand, no multiple versions of bash, etc)... then portability via "env" does not make sense.
If you have two laptops and a raspberry at home, with debian or arch, and you write a script for yourself... then portability via "env" does not make sense.
And last but not least... using env is slower.
See:
strace -fc /bin/bash -c ':'
Vs strace -fc /usr/bin/env bash -c ':'
On my system, that's 92 syscalls and 3 errors, Vs 152 syscalls and 8 errors.Just to start procesing.
Diferent levels of system bloat (environment, library paths, etc) can give different results than my example.
And as others said... if you're not using GNU/bash syntax and the script is really simple, the best for portbility is to go with /bin/sh.
strace -fc /bin/sh -c ':'
On my system 41 syscalls and 1 error... (and less RAM, CPU and pagefaults).If you're not using associative arrays, array indexes, non POSIX builtin options, and other bash extensions... if the script is just to join a few commands and variables... it pays the effort to write it in simple sh, both, for portability and performance.
- Do I trust my code to run on a machine where /bin/bash doesn't work?
- Do I trust my users to have their PATH configured correctly?
IME a user misconfiguring ~/.bashrc is about seven million times more likely than some theoretical argument about "portability", or even the idea that running my code on some unspecified version of bash that a mac user accidentally downloaded while screwing up a Homebrew copy/paste command is preferable to using the factory default that everyone has.- Do I respect people who have set up their PATH correctly (to prefer, by example, a newer /usr/local/bin/bash or $HOME/bin/bash than the standard /bin/bash)
https://github.com/oilshell/oil/issues/709
Summary of problems:
1. The "if myfunc" problem -- error checking is skipped. This is specified by POSIX, but it's undesirable.
2. The f() { test -d /tmp && echo "exists" } problem. The exit code of the function is probably not what you expect!
3. the local x=$(false) problem. This happens in all shells because of the definition of $?. (local is a shell builtin with its own exit code, not part of the language) This one is already fixed with shopt -s more_errexit in Oil.
4. the x=$(false) problem. This is a bash problem with command substitution. For example, dash and zsh don't have this problem. Test case:
bash -c 'set -e; x=$(false); echo should not get here'
Newer versions of bash fix it with inherit_errexit. Oil also implements inherit_errexit and turns it on by default! (in Oil, not OSH)-----
So 1 and 2 relate to the confusing combination of shell functions and errexit.
And 3 and 4 relate to command subs. Oil has fixed these with opt-in OSH shell options (on by default in Oil), but not 1 and 2.
If you know of any other problems, please chime in on the bug!
The exit code is 0 (assuming /tmp/, stdout, /bin/test and /bin/echo are all working correctly; with /tmp1 it's 1), as expected; is this referencing a bug in sh and/or bash that I've fixed locally and then forgotten about?
(Also, I'm pretty sure it should be:
f() { test -d /tmp && echo "exists"; }
unless the parse error for missing ';' was your point (I haven't bothered to fix that one, but maybe Oil has).)https://news.ycombinator.com/item?id=24738274
except with a shell function rather than a subshell.
Demo:
https://github.com/oilshell/blog-code/blob/master/errexit /demo.sh#L21
Run ./demo.sh subshell-compare and func-compare.
I have seen multiple people point it out as confusing. The problem is that in the first case, there are two exit codes:
1. false (suppressed on LHS of &&)
2. true
And in the second case, there are 3:
1. false (suppressed on LHS of &&)
2. true
3. the exit code of the function/subshell, which is the exit code of the last statement, which is nonzero. This then CAUSES THE PROGRAM TO FAIL, whereas it didn't before.
(edit: removed incorrect code)
-----
So it's kind of like a "ghost" exit code created by the subshell/function! Which has an unexpected interaction with errexit.
Does that make sense?
I am not sure exactly how to fix it, but it will probably involve limiting && to stuff like this:
if test -d /tmp && test -d /tmp/foo; then echo yes; fi
And disallow it when standing alone, because it doesn't make much sense there, if errexit is on.Feel free to chime in bug with any possible solutions or more problems.
----
(And yes Oil fixes the brace problem with shopt -s parse_brace, which is on by default in Oil! Try it out and let me know if you like it :) )
Not at all well, I'm afraid, but I think I understand:
f(){ false && true; } ; set -e ; f ; echo hi
# this works correctly (f returns false and errexit triggers)
set -e; false && true ; echo hi
# despite `false && true` failing, this doesn't trigger errexit
I was confused by the implication that the function version was what was wrong (rather than "false && true is broken, and you'd expect the function to work the same way"). Probably partly because I was confusing it with `command || true`, which is a idiomatic way to suppress errexit-like mechanisms by making the exit status always 0.Another way to look at it is the difference between exercise 4 and 5 here:
https://mywiki.wooledge.org/BashFAQ/105
So taking that concrete example
test -d nosuchdir && echo "warning: no directory";
instead of false && true
You didn't want the function to fail. You just wanted to print a warning if the directory doesn't exist. But the whole function fails, while it doesn't for the if statement.The whole thing is inherently confusing ... The language confuses success/failure and true/false. Both of those are stuffed into an exit code.
test -d is really for true/false, but then functions also have success/failure (did the last command succeed).
So there's no way for shell to really know what you want.
I think Oil might end up with something like "if catch foo" for success/fail, and "if boolean grep" for true/false.
That code prints a warning if the directory does exist, actually ("-d FILE FILE exists and is a directory"). Did you mean something like:
test -e outfile && echo "trying to overwrite preexisting outfile"
> You didn't want the function to fail.Uh, I kind of do, actually? If I wanted to discard the exit status I'd write:
test -e outfile && echo "trying to overwrite preexisting outfile" || true;
This is awkward, and suggests a proper if-then operator (perl-6-style `??` collides with globbing, but maybe `&?`?), but it's better then having something like: mkfoo -o foo && cp -T foo otherfoo
do-something foo bar
mostly-silently mix old and new data together because mkfoo failed and the error code was swallowed.> So there's no way for shell to really know what you want.
Yep, and if we have to pick one problem, things failing completely in a obvious and also easily-fixable way is usually a much less awful problem to have than silently corrupting data and state, especially when you've opted-in to a mechanism (like errexit) specifically designed to do the former.
Same theory for unset variable. Referencing an undefined variable shouldn’t break your session. Why initialize it anyway? It’s more code to change if you don’t use it if you have to initialize it when it might not be needed. And, you’d have to call the script with A= just to check A wasn’t defined, and in the process now you have A assigned to an empty string, instead of only defaulting to one when called, which uses more memory and execution time.
The pipeline doesn’t die because && and || and parens are seriously helpful for one-liners.
Don’t think of it as a script. Think of it as a script for a shell.
Bash is brilliantly useful for a lot of things. I’m not bashing it.
The fallacy is: "By posting comments on the Internet, I can prevent people from using a tool I'm frustrated with. And I can make millions of lines of existing programs that I rely on go away."
You have ${var:-} for doing that
shopt -s inherit_errexit 2>/dev/null || true
To make subprocesses inherit the errexit flag. inherit_errexit
If set, command substitution inherits the value of the errexit option,
instead of unsetting it in the subshell environment. This option is
enabled when posix mode is enabled.
So must I presume older versions were already doing that without needing an option set? function foo { false; true; }
foo || echo foo failed
prints nothing!From the bash manual page:
| If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit.
POSIX says the same thing, so this is true of all POSIX-y shells.
This means you really have to check for errors you care about, and `set -e` is useless. Ugh!
You can disable it again with set +x (same goes for +e, +u and afaik +o pipefail)
Also, please use shellcheck - https://www.shellcheck.net/
EDIT: Also, please don't modify scripts while they are running.
main() {
script goes here
exit
}
main "$@"A number of useful debugging tips are listed at <https://wiki.bash-hackers.org/scripting/debuggingtips>.
But I very much agree that lack of pipefail is painful. If I know that output on the left of the pipe is small, I read it into a variable and then use printf | right part. If the output can be big, I use a helper function to emulate it that I copy-paste.
Check out https://freebsdfrau.gitbook.io/serious-shell-programming/
There is a Facebook group and I have emailed the author as well with questions. Nice guy.
cleanup="true"
later:
if $cleanup; then a ; else b ; fi
or slightly uglier $cleanup && rm -f mytempfile
I like one-line functions: die() { echo "$@" 1>&2; exit 1; }I think this changed lately[1]. No clue where's implemented, though.
set -e; (false && true); echo hi
does nothing, but set -e; false && true ; echo hi
prints hi.My guess was that the () affects the order of operations between ; and &&, so the first line is three commands, while the second line is two.
"Placing a list of commands between parentheses causes a subshell environment to be created"
"Placing a list of commands between curly braces causes the list to be executed in the current shell context"
>My guess was that the () affects the order of operations between ; and &&, so the first line is three commands, while the second line is two.
Emphasis mine. My understanding was that the question is simply about whether () invokes a subshell or not (irrespective of set -e)
https://news.ycombinator.com/item?id=24740842
The problem actually has more to do with the definition of $? than the set -e behavior itself. And the fact that POSIX specifies that the error a the LHS of && is ignored (a fundamental confusion between true/false and success/error)
The exit code of the function is not what you expect, or the exit code of the subshell is not what you expect.
I made a note of it on the bug ... still thinking about what the solution to that one is.
(The other solutions are inherit_errexit, more_errexit, and a "catch" builtin.)
function foo { false; true; }
foo || echo foo failed
prints nothing.https://news.ycombinator.com/item?id=24740842
shopt -s strict_errexit in Oil disallows that (a runtime assertion 100% of the time). Feedback is welcome! https://github.com/oilshell/oil/issues/709
bash -eo pipefail -c "cat /etc/services | head -1; echo never got here"Where this will work as expected:
bash -eo pipefail -c "head < /etc/services -1|cat ; echo got here"I find set -e also has some gotchas. '|| die' added to the end of each line is uglier but more obvious in many cases.
I generally use them and think that overall, they have more benefits than drawbacks, but the odd time where I run into one of the pitfalls, debugging usually takes a while.
ShellCheck already does a pretty good job of pointing out incorrect variable names, too.
Common motifs, elsewhere, for a shell are a dollar sign and an underscore or a greater than sign and an underscore. (The latter is somewhat odd for a shell, given that it more resembles the prompts on Microsoft/IBM command interpreters, and not the PS1 prompts of Unix shells, which are commonly dollar symbols, hashes, or percent signs rather than greater than.)
* https://www.redbubble.com/i/sticker/zsh-by-zoerab/20363330.E...
* https://commons.wikimedia.org/wiki/File:Bash_Logo_black_and_...
* https://commons.wikimedia.org/wiki/File:PowerShell_5.0_icon....
* https://icon-library.net/icon/commands-icon-5.html
http://redsymbol.net/articles/unofficial-bash-strict-mode/
EDIT: The comic strip would be better in three rows of two panels - a row for each set flag.
> TIL that you can use the "DEBUG" trap to step through a bash script line by line
trap '(read -p "[$BASH_SOURCE:$LINENO] $BASH_COMMAND?")' DEBUG
> [...] it does something very different than sh -x — sh -x will just print out lines, this stops before* every single line and lets you confirm that you want to run that line*>> you can also customize the prompt with set -x
export PS4='+(${BASH_SOURCE}:${LINENO}) '
set -x
With a markdown_escape function, could this make for something like a notebook with ```bash fenced code blocks with syntax highlighting?First, because it behaves inconsistently across shells/versions [1] and second, because it doesn't always work as expected. For example, when you depend on the 'set -e' behavior in a function and call the function from within a condition, the 'set -e' has no effect at all. So you better don't count on 'set -e'.
But don't expect me to follow my own advice, as not using 'set -e' isn't a good option either...
Try it out on your shell scripts and let me know what happens :) OSH has the "broken" POSIX/bash behavior to maintain compatibility, while Oil opts you in to the better error semantics.
But then came the PowerShell people...
I dont understand its popularity. Normally sane people who like unit testing, CICD, SOLID principles, quality tools end up with a bunch of crappy scripts holding everything together. Please avoid.
I encourage all my teams to avoid built-in CI/CD features and plugins and just script what they want in a Docker container. It ends up being easier to maintain, breaks less often, and is more portable.
I think the only reason people use the shell for scripting is that
ls -lsa /tmp
is simpler to write than execute(["ls", "-lsa", "/tmp"])
or even execute("ls -lsa /tmp")
> Normally sane people who like unit testing, CICD, SOLID principles, quality tools end up with a bunch of crappy scripts holding everything together. Please avoid.Totally agree!