hhlohmann
Posted on December 10, 2021
Or at least some insights into Command Substitution, Side Effects, Short-Circuiting, and Guard clauses
Basically only numerical error "messages"
The error handlling concept of Bash (resp. shell scripting in general) consists in return
ing or exit
ing with a number that might help you to look up details in some list. It is not possible to have an informative message as eg. in JavaScript:
function JavaScriptReturn() {
return 'Oh, is this about Bash?'
}
But it is possible if you use Command Substitution (see the Bash manual on it before heading to Stack Overflow).
Put side effects to numerical errors
Instead of
if [ "$1" = "" ] ; then return 1 ; fi
you can say
if [ "$1" = "" ] ; then $(fErr) ; fi
where "fErr" is a function like this:
fErr(){
1>&2 printf "No arg!"
printf "return 1"
}
You surely remember that functions in Bash are just command groups, so command substitution with a function will result in the defined command group in place of the substitution, so the line above executes as if it was:
if [ "$1" = "" ] ; then printf "No arg!" ; return 1 ; fi
-- but wait: why not 1>&2 printf "No arg!"
and printf "return 1"
?
Note that the line before is as if, actually executed is:
if [ "$1" = "" ] ; then return 1 ; fi
since the outcome of the command substitution is just return 1
.
The printf "No arg!"
is not substituted, but executed inside "fErr" before substitution takes place, you could (or must) say as a side effect of the command substitution. It would have been substituted together with return 1
if we would not redirect stdout to stderr by 1>&2
for printf "No arg!"
.
Remember that stdout and stderr normally are both shown to you on the same visual output screen, but they are two different channels and in scripting you can and often must address them separately.
Redirecting to stderr should be natural for error messages, here it is a big help in distinguishing the immediately control-flow critical action "return / exit with error" from "additional" information.
Powerful error handling in short-circuiting
Calling a function before throwing a "return" statement might not be that big sensation in "if ... else ... fi", but things look different with short-circuiting.
Sometimes using
[ "$1" = "" ] && printf "No arg!" && return 1
instead of
if [ "$1" = "" ] ; then printf "No arg!" ; return 1 ; fi
is indeed just Code golf, but short-circuits are very handy for Guard clauses.
Imagine something like this
[ "$1" = "" ] && $(fErr1)
[ ! -d "$1" ] && ! transmogrify "$1" && $(fErr2)
[ "$2" = "$3" ] && unhandle "$2" || deep_unhandle "$2" && $(fErr3) || $(ferr4)
and try to imagine it - whatever it means - with "if" clauses.
A slight problem with short-circuits is that
A || B
means do A, in case of error do B, but
A || B && C
does not mean do A, in case of error do B & C, but do A, in case of error do B, and then - in any case - do C, so that a simple
findHolyGrail || printf "Failed to find holy grail" && return 1
tweet "Come party!"
would exit with error before "tweet", regardless if "findHolyGrail" succeeded or not, but
findHolyGrail || $(fErr)
tweet "Come party!"
would "tweet" on success for "findHolyGrail" and exit else.
Keep in mind that "fErr" might be much more than just a short message, but eg.
fErr(){
healWounds
cancelHotelBookings
fireLameFellows
searchNewChallenge
thinkOverCurrentJob
1>&2 printf "$messageOfSorrow"
printf "return 1"
}
Using Command substitution correctly
Note that only exactly
$(fErr)
will be substituted with the commands of "fErr".
Do not put double quotes around the substitution expression. With quotes you would get and try to a string, not a command, i.e.
"return 1"
instead of
return 1
Backquotes vs. Dollar
While we use
$(fErr)
here for command substitution, you will often see a variant with backquotes (backticks), ie.
`fErr`
There are contexts in which the backquote variant is more compact and readable, and it looks more modern, but indeed it is "old-style" and runs in many contexts into problems because backquotes are syntactically less clearer than the "$()" construct.
You should prefer "$()" because it resembles not without reason the difference of "()" and "{}" for Grouping commands, not at least for Shell functions where the curlies "{}" execute the commands in place while the parens "()" put the result of executing the commands into place.
Single vs. multiple commands as substitutes
Since command substitution is nothing more than calling one or more commands and substitute the outcomes of that calls at the place of the call, you could also use
! findHolyGrail && $(fErrMsg ; fErrReturn)
with separate functions for an error message and return 1
, but you cannot use
! findHolyGrail && $(fErrMsg ; return 1)
since this would not be excecuted like
! findHolyGrail && fErrMsg && return 1
but like
! findHolyGrail && fErrMsg
i.e. without returning, but proceeding despite error.
This is because in
$(fErrMsg ; return 1)
the return 1
part is an instruction to return with "1" from the subshell in which the execution of the command(s) happens whose outcome should be substituted, but itself is no outcome of that execution. You could catch by usual means the return value "1", but that's not the outcome that is used for substitution; you might say that the subshell execution have two results, one a substitutable outcome and one an exit state.
What you had to write is
! findHolyGrail && $(fErrMsg ; printf "return 1")
and this invites to integrate the printf "return 1"
part into fErrMsg
at least for clearer code.
Posted on December 10, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024