6 ms·
One of the most useful tricks I've learned and now apply to every non-throwaway shell script is the use of "||" for guard clauses to catch failing operations, a
by gavinray 6y ago
One of the most useful tricks I've learned and now apply to every non-throwaway shell script is the use of "||" for guard clauses to catch failing operations, along with the use of a block statement for running multiple operations "{ }".
Like so:
# Download the binary
wget --quiet --output-document /usr/local/bin/mybinary "$download_url" || {
error 'Failed downloading the CLI'
exit 1
}
debug "Making CLI executable"
# Make it executable
chmod +x /usr/local/bin/mybinary || {
error 'Failed making CLI executable'
exit 1
}
This has been completely transformative for me and has made writing maintainable + debuggable scripts so much easier.
- LukeShu 6y agoIs that really better than if ! wget --quiet --output-document /usr/local/bin/mybinary "$download_url"; then error 'Failed downloading the CLI' exit 1 fi ? I know someone who wrote most of his Bash that way, using `cmd && { ...; }` instead of `if cmd; then ...; fi` and `cmd || { ...; }` instead of `if ! cmd; then ...; fi`. I never thought it was particularly clear or maintainable--that it was more on the "clever" side than on the "clear" side. Sure, it's a little clunky to have an `if` for everything that might fail, but uh... these days I write Go for a living.
- saurik 6y agoMy brain parses that || as quickly as it verifies the !, and having the actual command at the left of the line is helpful for clarity; FWIW, this is actually a pretty common paradigm in many languages including Ruby--with "or die"--which people generally liked for cute syntax.
- castillar76 6y agoYep, this was a common Perl construct as well, since you could follow "or die" with a custom error string. I used to amuse myself (way back in the Stone Age, when I was in college and didn't know better) by using amusing and unique invectives for error messages like 'or die "you son of a motherless goat"'. Not only did they make me giggle a little, they made searching code for the line that failed a little easier...
- saurik 6y agoYeah yeah yeah, but citing Perl to defend that something is readable doesn't win arguments ;P.
- kergonath 6y agoWell, some parts of Perl actually make a lot of sense.
- ycombobreaker 6y agoWhat did Larry Wall do to deserve that? Ruby cribbed the "or die" idiom (and others) from Perl because they were useful. At least let him own his good ideas along with the rest of the language.
- LukeShu 6y agoI mentioned that these days I write Go for a living, which involves a lot of: if err := thingThatMightFail(); err != nil { ... } so I was saying that of course I think lots of `if` statements for error handling are reasonable. (Hmm, thinking back, I learned Go in 2012, and my Bash probably peaked in 2013, I wonder if having learned Go made me more amenable to this in Bash.) I think a single-line `cmd || die "msg"` like in Perl or Ruby is handy and fine. Or maybe even a two-line some command that might fail || die "Some message that is long enough that it wants to be its own line" But once it starts getting to have multiple statements that you're grouping with `{ }`, just use an `if` statement. If I were reviewing someone's Ruby that had multiple lines after the "or" in "or die", I'd tell them to just use unless thingThatMightFail ... end
- yesenadam 6y agoLooks a lot better to me! "if not" is already off to a bad start.
- deleted 6y ago[deleted]
- rcoveson 6y agoI think you answered your own question. Those `if` constructs are indeed clunky. In fact I'd venture to say it's really just "C-like language Stockholm syndrome" that makes them feel more "maintainable" or "clear". The bash "or" syntax reads very clearly. Do X or do Y. Do X or fail like so. It's very English-like and intuitive. And understanding why it works is just a matter of understanding logical operator short circuiting, which you have to understand to work in C-like languages anyway.
- kazinator 6y agoProblem is you have to fork subshells if here is any sequencing involved. Compare if condition ; then command1 command2 else command3 command4 fi versus: condition && (command1; command2) || (command3; command4) GNU Bash has a fix for that: non-forking braces.
- deleted 6y ago[deleted]
- cassianoleal 6y agoI start all/most of my shell scripts with #!/usr/bin/env bash set -xeuo pipefael -x to print the commands -e to exit on error -u to error out if there are unbound variables -o pipefail to exit if a command that's not the last in a pipeline fails No need to || {}. Only word of warning here is that -x can print out secrets if you're not careful.
- pmahoney 6y agoI stopped using `set -e`. It is disabled if the function you're running is part of an `if` statement, for example: thingThatCanFail() { echo "step one succeeded" echo "step two failed" false echo "step three was run too" } if ! thingThatCanFail; then echo "thingThatCanFail failed!" fi With or without `set -e`, step three is run, and the function returns success, even though you might expect the failure of step two to prevent step three from running. If "thingThatCanFail" is called _outside_ of an if statement, then `set -e` causes different behavior (i.e. step three _is_ skipped). I instead use lots of chaining with && (as in the article), or explicit checks after each command. I have two utility functions I define in nearly every script: warn() { >&2 printf "%s\\n" "$*"; } abort() { warn "$@"; exit 1; } Then I do lots of: stepOne || abort "step one failed" stepTwo || abort "step two failed" ... It can get a little verbose, but much better than trying to reason about `set -e` in my opinion.
- ktpsns 6y agoThat's the same approach I am using in all my scripts. You can even combine that with `set -e` and run some commandThatMayFail || true in order to continue the script if some optional step fails. Bonus note: The notation "do something else die 'with message'" stems from Perl, AFAIK. See for instance https://perldoc.perl.org/Carp https://perldoc.perl.org/Carp
- deleted 6y ago[deleted]
- drran 6y agoI developed this style at Bazarvoice about 15 years ago. :-)
- waterhouse 6y agoProtip: die() { echo "$@" >&2; exit 1; } wget --quiet --output-document /usr/local/bin/mybinary "$download_url" || die 'Failed downloading the CLI' chmod +x /usr/local/bin/mybinary || die 'Failed making CLI executable' If you need more complex cleanup, that doesn't work as well... but (a) hopefully you can avoid that cleanup and just use a plain "|| die", and (b) if you need cleanup, hopefully you can put that into one function (possibly "die" itself) and have its cleanup work no matter where it's called from, and still run everything as "|| cleanup_and_die" or whatever you'd call it.
- neves 6y agoproprotip: in the beginning of your scripts put: set -e # stop script if error set -u # stop with undefined var set -o pipefail # stop with pipe fail Now your scripts will have a sane default behavior
- wahern 6y agoPro-protip: don't use echo. # panic STRING-FORMAT [...] # # write printf-formatted message to stderr, prepending # script name, and exit failure panic() { printf "%s: %.0s${1}\n" "${0##*/}" "$@" >&2 exit 1 } Prepending the script name is immensely helpful when diagnostic messages are mixed on stderr. It's also standard style for Unix utilities in general. The BSD extensions err(3) and warn(3), provided by all modern Unix libc implementations and commonly use by shell utilities, prepend the program name. Another common style guideline is to include context for the diagnostic, often prepended. For example, wget --quiet --output-document /usr/local/bin/mybinary "$download_url" \ || panic '%s: unable to download' "$download_url"
- cat199 6y agowhy is printf preferable to echo here other than formatting?
- deleted 6y ago[deleted]
- 6y ago