[HN Gopher] Elegant Bash Conditionals
___________________________________________________________________
Elegant Bash Conditionals
Author : timvisee
Score : 66 points
Date : 2021-03-02 12:16 UTC (10 hours ago)
(HTM) web link (timvisee.com)
(TXT) w3m dump (timvisee.com)
| the_jeremy wrote:
| Strongly disagree on elegance of [[ condition ]] || cmd and [[
| condition ]] && cmd.
|
| Every developer has read enough if statements to understand if
| then else at a glance. Few people write enough bash scripts to
| have that same instant parsing of [[ condition ]] && cmd or [[
| condition ]] || cmd, especially when intermingled.
|
| I like the idea of cmd || { raise error; } that others have
| mentioned, but I hate the idea of using these in place of an if
| statement to save 2 lines.
| dragonwriter wrote:
| condition && action condition || action
|
| (or the equivalent for languages where the logical operators
| are written differently) are not bash-exclusive idioms; I've
| seen them in a number of expression-oriented languages (less
| frequently on statement-oriented languages where the particular
| action is a function rather than a statement, but the fact that
| it isn't usable consistently in those. languages tends to make
| it less idiomatic there.)
| mmastrac wrote:
| I prefer || and && because I can never seem to remember the
| bash `if` syntax.
| OskarS wrote:
| I always found it delightful how `if [ condition ]` works in
| bash, which, in case you don't know: the way `if <something>`
| works in bash is that <something> is a command that runs like
| everything else in bash. If the command succeeds (i.e. if the
| return code for the process is 0), the body of the if executes.
|
| So how does `if [ <condition> ]` work then? Is it some kind of
| special case? No! The way it works is that there's an executable
| named `[` in UNIX that that takes the condition expression as
| arguments, and returns "success" or "failure" depending on if the
| condition evaluates to true. It's right there in the filesystem
| at /bin/[
|
| Now, some sticklers might argue that maybe not the greatest idea
| to spawn a process every time you need to evaluate an if
| statement, but I really do admire the UNIX purity of it: of
| COURSE that's how bash conditionals work! This is UNIX, after
| all! Why add an expression parser/evaluator to bash when you can
| just have it's own process for that? Do one thing and do it well!
| mr-wendel wrote:
| Actually... it usually _is_ a special case. For performance
| reasons there are some sneaky things Bash tends to do to avoid
| spawning lots of extra processes. In this case the `[` binary
| is more for backwards compatibility.
|
| You can verify this by writing a simple test script and doing
| (at least on Linux) "strace ./test.sh 2>&1 | grep exec" and
| observe no explicit call to that binary.
| tjoff wrote:
| It is neat, but it also trips up a lot of us in the beginning.
| Because at one point you instead write: ' _if [condition]_ '
| and get weird errors. And you can't understand how suddenly you
| can't even do a simple if-statement.
|
| And there are tons of gotchas like that that made me terrified
| of bash. Even the simplest construct can attack you from any
| angle and the trial and error and distrust is everywhere. I'm
| getting better at it but for me and I guess many others, I just
| don't use bash enough to remember these quirks and the
| resulting experience is rather poor. Which is a shame!
|
| The savior for me was _shellcheck_. Instead of needing
| character-perfect memory of each construct and common snippet I
| can run shellcheck on it and it will tell me what common
| pitfall I might have stumbled upon and most importantly, why it
| is an issue. For a beginner I think it is an absolute must.
| cle wrote:
| Because Bash already has an expression parser/evaluator that it
| uses for this case, so now there isn't just one thing doing it,
| there are two, and you have to know when they are used and when
| they aren't.
|
| Honestly the idea that a certain benign-looking syntax could
| (but maybe not!) spawn some external subprocess is terrifying
| to me, and I don't find it delightful at all.
| rgrau wrote:
| There is some special casing in the ifs conditions though.
|
| Even if you `set -e`, the command in the test can fail and your
| script won't exit.
| wkz wrote:
| While it might not be a special case exactly, you most likely
| won't see /bin/[ being executed either. Typically your shell
| will implement it as a built in command. Because as you say,
| spawning processes is expensive.
| memco wrote:
| > [ $USER != "root" ] && echo You must be root && exit 1
|
| I get that the authors point was more focused on the conditional
| usage, but this seems like a pretty bad way to check if the user
| is root and isn't really any more complicated than checking EUID
| = 0 or some of the other methods.
| joana035 wrote:
| > Ughh. Let's improve!
|
| I really don't understand what is the problem with that simple if
| statement to receive this kind of reaction. :shrug:
| pizza234 wrote:
| The traditional format of Bash's if/then has `if` and `then` on
| separate lines: if command_or_condition
| then branch fi
|
| I suppose that to many, this looks wasteful and consequently
| ugly, so one looks for alternatives.
|
| I always inline the `then`, which requires a semicolon:
| if command_or_condition; then branch fi
|
| If I was forced to use the traditional format, I'd be
| definitely annoyed :)
| a1369209993 wrote:
| I use: if command_or_condition then
| branch more_branch fi
|
| Given the relationship between ; and newline, and the
| positions of required ;s in if/then/fi, I assumed that was
| intended.
| waynesonfire wrote:
| I agree. Also, not sure what's being improved. I also would
| have expected to run into a reference of double and single
| brackets used in if statements, e.g. [[ ]] and [ ] -- which I
| always seem to find confusing.
| prussian wrote:
| [[ ]] is effectively an entirely different parser than the
| surrounding shell, similar to (( )).
|
| so you can do things like [[ 1 < 2 ]] &&
| echo true # where < is now not redirect, like to (( 1 < 2 ))
| [[ "x" == 'y' || x == x ]] # note how || is not behaving the
| same as [ '' || 'y' ]
|
| `man 1 test` explains the more conventional `[ ]` where the
| bash man page would better explain the [[ case.
| stonesweep wrote:
| The [ character is actually the program called "test"[1], so
| you can think of it as just running grep or sed or any other
| external binary/program, whereas the [[ replacement is shell-
| specific builtin; this blog is not using "bash-ism" but
| instead the external test operator from `coreutils` which
| generally would work from sh, zsh and whatever but on zsh, it
| doesn't work the same (kinda, it's more that the comparison
| EQUALS operator doesn't work well with [ but it's fine with
| [[).
|
| Here it is in pdksh: # [ "Z" == "z" ] &&
| echo Bob # [ "Z" == "Z" ] && echo Bob
| Bob
|
| I chose pdksh on purpose - because _it_ also supports [[
| (builtin like bash)[2], so while [[ is a "bash-ism" it's
| actually present in some/many other shells as well. However,
| the operational aspect is not identical between [ and [[,
| both in the bash and pdksh implementations and let's add zsh
| to show the same line of shell break: # [[
| "Z" == "Z" && "A" == "A" ]] && echo Bob
| Bob # [ "Z" == "Z" && "A" == "A" ] && echo Bob
| pdksh: [: missing ] # [[ "Z" == "Z" && "A" ==
| "A" ]] && echo Bob Bob # [ "Z" == "Z" && "A"
| == "A" ] && echo Bob -bash: [: missing `]'
| # [[ "Z" == "Z" && "A" == "A" ]] && echo Bob Bob
| # [ "Z" == "Z" && "A" == "A" ] && echo Bob zsh: = not
| found
|
| The [ operator requires that you use the bash/pdksh method to
| combine them with && "outside the brackets" in a more POSIX
| like use, like so: # if ([ "Z" == "Z" ] &&
| [ "A" == "A" ]); then echo Bob; fi;
| Bob
|
| ...except on zsh, because (surprise!) zsh has decided to
| internalize the [ command rather than use the external one
| (which works OK); but it also has a problem with the "=" sign
| being used like this[3] with the test operator. zsh requires
| we then handle the == sign by quoting it (or unsetting a
| value internally): # if [ "Z" == "Z" ] && [
| "A" == "A" ]; then echo Bob; fi;
| zsh: = not found # if [ "Z" '==' "Z" ] && [ "A"
| '==' "A" ]; then echo Bob; fi; Bob
|
| This last example is a subtle point - the use of [[ is
| actually _more_ compatible between bash/pdksh/zsh than the
| use of [ due to the way zsh handles the input which is
| different than bash/pdksh and even old school POSIX Bourne
| shell (/bin/sh).
|
| This got kinda long sorry, hope it helps.
|
| [1] there's actually a binary "[" and a binary "test" in the
| `coreutils` package on most systems, however I'm not sure why
| they have different binary file sizes to be honest
|
| [2] https://linux.die.net/man/1/pdksh search "[["
|
| [3] https://www.zsh.org/mla/users/2011/msg00161.html
| its-summertime wrote:
| From personal messing around in the past, ||/&& generally don't
| seem as performant as using plain statements, by a fair margin
| (I'm guessing its causing subshells to be spawned for whatever
| reason, but I didn't bother digging further)
| gavinray wrote:
| 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.
| cassianoleal wrote:
| I 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 wrote:
| I 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 wrote:
| That'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
| drran wrote:
| I developed this style at Bazarvoice about 15 years ago. :-)
| LukeShu wrote:
| Is 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 wrote:
| My 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 wrote:
| Yep, 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...
| LukeShu wrote:
| I 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
| sharley wrote:
| 'cmd && do_sth || do_sth_else' is not equivalent to if else
|
| https://mywiki.wooledge.org/BashPitfalls#cmd1_.26.26_cmd2_.7...
| mr-wendel wrote:
| Do yourself a favor and read the linked section. The "elegant"
| method sure can be nice, but comes with a downside you really
| need to be aware of.
|
| I _love_ shell scripting and this highlights one of the major
| problems with bash: small changes like this can appear
| completely interchangeable with other mechanisms for doing the
| same thing but introduce edge cases that can unexpectedly bite
| you.
|
| For example... consider what happens if "[ $foo -eq 0 ] &&
| /bin/do-something" is the last statement within a function or
| the end of your script.
| e40 wrote:
| I came here to make this comment. It really needs to be a
| top-level comment and at the top.
|
| I have wasted many hours of my life tracking down buts where
| the idiom in the OP was used in a function. It leads to
| really hard to find bugs.
|
| Beware!!!
| cb321 wrote:
| Original article:
|
| >The echo command _always_ exists [sic] with 0, so this
| propagates to exit if the first expression is truthful. (emphasis
| his)
|
| In both bash and dash (and probably most), echo will fail if
| there is no stdout. E.g.: j.sh: echo
| hello && exit 2 $ bash j.sh >&- j.sh: line
| 1: echo: write error: Bad file descriptor $ echo $?
| 1
|
| Note the exit value ($?) is 1, not 2.
|
| EDIT: Also note that not exiting the script could cause a flurry
| of cascading errors (likely permission errors in the article's
| example).
| hvdijk wrote:
| It will also fail if there is a stdout, but echo cannot write
| to it, for instance because you are redirecting to a file and
| have run out of disk space.
| cb321 wrote:
| Yeppers. This can be a kind of subtle footgun.
| twic wrote:
| I use these, but only as what are effectively assertions:
| [[ -f $input_file ]] || { echo "${input_file} does not exist";
| exit 1; }
|
| So you can read the left-hand side bit as an assertion that
| something is true in the following part of the script, and skim
| over the right-hand side.
| 0xbadcafebee wrote:
| You should not overuse chaining of commands. It obscures
| individual exit status, it can fail in subtle ways (esp. with
| features like _set -e_ or _pipefail_ ), eventually you'll need to
| refactor it to do something more complicated, and it can hide
| important behavior from the casual reader.
|
| Parameter expansion is a great way to simplify your code, but it
| can be obtuse to the casual reader. Setting a default variable
| using _DEFAULT= "${DEFAULT:-myvalue}"_ is a bit easier to
| understand than just _${DEFAULT:=myvalue}_.
|
| Read the _dash_ manual and try to stick to just those features as
| it 's almost entirely POSIX. Most scripts do not need to rely on
| _bash_ functionality. https://linux.die.net/man/1/dash
|
| If you find it will really simplify your life to have arrays,
| hashes/maps/dicts, a while loop reading from a subshell's output,
| etc, then use Bash and use those features. Otherwise, stick to
| POSIX semantics. You can do almost everything you need with
| parameter expansion, _expr_ and other unix tools.
|
| With any programming language, you should try to use the least
| amount of syntax and functionality possible to accomplish your
| goal, as long as it is readable, maintainable, and does not hide
| tricky behavior. Being verbose is always preferable to being hard
| to maintain; verbose and uncomplicated things can easily be
| simplified later.
| lhorie wrote:
| Something I've come to internalize over the years is to always
| try to avoid being clever especially with lingua francas like
| Bash.
|
| There _will_ be people - seasoned professional developers, even -
| reading these languages that don 't know the first thing about
| them!
|
| `[` is one of those things that are second nature for seasoned
| bash people but that are utterly ungoogleable by a bash beginner.
| Beginners don't know `[` is an executable, and it would never
| occur to them to `man [`. It can become quite the rabbit hole to
| figure out that there's also `[[` (which is NOT an executable),
| that `[ a > b ]` is _very very_ different from `[[ a > b ]]`,
| etc.
|
| So do these people a favor and put the `if` there so they at
| least have some hope of stumbling upon a stack overflow post
| talking about some of this stuff.
| l0b0 wrote:
| $ type -a [ [ is a shell builtin [ is /usr/bin/[
| $ type -a [[ [[ is a shell keyword
| kwhitefoot wrote:
| '[' is a Bash builtin. See
| https://www.computerhope.com/unix/bash/index.htm.
|
| It is usually also a symlink to the 'test' executable but not
| in all Unix-like systems.
| js2 wrote:
| "test" and its alias "[" are actually builtins:
|
| https://www.gnu.org/software/bash/manual/html_node/Bourne-Sh...
|
| They also exist as executables for ancient backwards
| compatibility.
|
| "[[" is a so-called "conditional construct":
|
| https://www.gnu.org/software/bash/manual/html_node/Condition...
|
| I have a somewhat quirky style and almost always use "if test
| ..." since I find it clearer than "[" and usually don't need
| the additional functionality that "[[" provides.
| hagg3n wrote:
| I honestly thought I was the only one, how cool is that? I
| too find `test` preferable over `[` since it leaves no doubt
| we're calling a command just like any other and the same
| assumptions still apply. I also avoid `[[` since it isn't
| posix and won't work in simpler shells.
| _joel wrote:
| Mentioned in the artice is:
|
| cat ~/.profile && echo This is your profile || echo Failed to
| read profile
|
| I think these are commonly called short-circuits. Using these
| with brackets too, in order to gain greater control or more
| complex comparisions is super useful if not aware of it.
|
| A simple example would be something like:
|
| # (foo && bar) && (bish || bash) && echo 0 || (echo "not bosh" &&
| exit 1)
|
| I'm no bash expert, just a sysadmin with a little dangerous
| knowledge, but constructing things that way just feels more
| natural (to me, at least, it's probably 'teaching your
| grandmother to suck eggs' to a proper bash hacker).
| mr-wendel wrote:
| You can group with curley braces too. This can be especially
| useful for input/output redirection over a chunk of commands.
|
| Be careful with using parenthesis -- this means you're now in a
| subshell! That "exit 1" will NOT terminate the entire script,
| but instead set "$?" to 1 for the next command. Additionally,
| any variable or environmental changes will be lost (which may
| be desirable) once you're outside of that block.
| _joel wrote:
| Thanks for the tip! I knew what I was doing was probably not
| right, but it worked when I needed it in the past (probably
| because of setting pipefail). It's very much welcome to know
| how to do it better :)
| cb321 wrote:
| Curly braces will fail in `dash` which may be what your
| /bin/sh is (or could become).
| jitl wrote:
| This is bad advice if you want to write reliable and maintainable
| shell scripts.
| kludgeon wrote:
| just use zsh. `if [[ -f file ]] {run this command} else {run that
| command}`
|
| no closing `fi`, use multiline or single line, add an arbitrary
| number of elif statements. this only requires the short option is
| set in your z shell.
| kludgeon wrote:
| and I'd call most of this discussion flow control, not
| conditionals. Strictly speaking, conditionals are one of the
| primary benefits of zsh over bash imo.
| musicale wrote:
| I'm not entirely sure what I think of "x or y" replacing "if not
| x then y", but it's a common enough idiom that I don't have
| trouble reading or writing it.
|
| I have tended to use it more recently for vertical compactness,
| and in python because pylint doesn't like single-line if
| statements but happily tolerates "x or y".
___________________________________________________________________
(page generated 2021-03-02 23:02 UTC)