I'm writing a shell script and need to check that a terminal app has been installed. I want to use a TRY/CATCH command to do this unless there is a neater way.

4

15 Answers

Is there a TRY CATCH command in Bash?

No.

Bash doesn't have as many luxuries as one can find in many programming languages.

There is no try/catch in bash; however, one can achieve similar behavior using && or ||.

Using ||:

if command1 fails then command2 runs as follows

command1 || command2 

Similarly, using &&, command2 will run if command1 is successful

The closest approximation of try/catch is as follows

{ # try command1 && #save your output } || { # catch # save log for exception } 

Also bash contains some error handling mechanisms, as well

set -e 

it stops your script if any simple command fails.

And also why not if...else. It is your best friend.

13

Based on some answers I found here, I made myself a small helper file to source for my projects:

trycatch.sh

#!/bin/bash function try() { [[ $- = *e* ]]; SAVED_OPT_E=$? set +e } function throw() { exit $1 } function catch() { export ex_code=$? (( $SAVED_OPT_E )) && set +e return $ex_code } function throwErrors() { set -e } function ignoreErrors() { set +e } 

here is an example how it looks in use:

#!/bin/bash export AnException=100 export AnotherException=101 # start with a try try ( # open a subshell !!! echo "do something" [ someErrorCondition ] && throw $AnException echo "do something more" executeCommandThatMightFail || throw $AnotherException throwErrors # automaticatly end the try block, if command-result is non-null echo "now on to something completely different" executeCommandThatMightFail echo "it's a wonder we came so far" executeCommandThatFailsForSure || true # ignore a single failing command ignoreErrors # ignore failures of commands until further notice executeCommand1ThatFailsForSure local result = $(executeCommand2ThatFailsForSure) [ result != "expected error" ] && throw $AnException # ok, if it's not an expected error, we want to bail out! executeCommand3ThatFailsForSure # make sure to clear $ex_code, otherwise catch * will run # echo "finished" does the trick for this example echo "finished" ) # directly after closing the subshell you need to connect a group to the catch using || catch || { # now you can handle case $ex_code in $AnException) echo "AnException was thrown" ;; $AnotherException) echo "AnotherException was thrown" ;; *) echo "An unexpected exception was thrown" throw $ex_code # you can rethrow the "exception" causing the script to exit if not caught ;; esac } 
4

I've developed an almost flawless try & catch implementation in bash, that allows you to write code like:

try echo 'Hello' false echo 'This will not be displayed' catch echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!" 

You can even nest the try-catch blocks inside themselves!

try { echo 'Hello' try { echo 'Nested Hello' false echo 'This will not execute' } catch { echo "Nested Caught (@ $__EXCEPTION_LINE__)" } false echo 'This will not execute too' } catch { echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!" } 

The code is a part of my bash boilerplate/framework. It further extends the idea of try & catch with things like error handling with backtrace and exceptions (plus some other nice features).

Here's the code that's responsible just for try & catch:

set -o pipefail shopt -s expand_aliases declare -ig __oo__insideTryCatch=0 # if try-catch is nested, then set +e before so the parent handler doesn't catch us alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e; __oo__insideTryCatch+=1; ( set -e; trap \"Exception.Capture \${LINENO}; \" ERR;" alias catch=" ); Exception.Extract \$? || " Exception.Capture() { local script="${BASH_SOURCE[1]#./}" if [[ ! -f /tmp/stored_exception_source ]]; then echo "$script" > /tmp/stored_exception_source fi if [[ ! -f /tmp/stored_exception_line ]]; then echo "$1" > /tmp/stored_exception_line fi return 0 } Exception.Extract() { if [[ $__oo__insideTryCatch -gt 1 ]] then set -e fi __oo__insideTryCatch+=-1 __EXCEPTION_CATCH__=( $(Exception.GetLastException) ) local retVal=$1 if [[ $retVal -gt 0 ]] then # BACKWARDS COMPATIBILE WAY: # export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}" # export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}" export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}" export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}" export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}" return 1 # so that we may continue with a "catch" fi } Exception.GetLastException() { if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]] then cat /tmp/stored_exception cat /tmp/stored_exception_line cat /tmp/stored_exception_source else echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}" fi rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source return 0 } 

Feel free to use, fork and contribute - it's on GitHub.

5

bash does not abort the running execution in case something detects an error state (unless you set the -e flag). Programming languages which offer try/catch do this in order to inhibit a "bailing out" because of this special situation (hence typically called "exception").

In the bash, instead, only the command in question will exit with an exit code greater than 0, indicating that error state. You can check for that of course, but since there is no automatic bailing out of anything, a try/catch does not make sense. It is just lacking that context.

You can, however, simulate a bailing out by using sub shells which can terminate at a point you decide:

( echo "Do one thing" echo "Do another thing" if some_condition then exit 3 # <-- this is our simulated bailing out fi echo "Do yet another thing" echo "And do a last thing" ) # <-- here we arrive after the simulated bailing out, and $? will be 3 (exit code) if [ $? = 3 ] then echo "Bail out detected" fi 

Instead of that some_condition with an if you also can just try a command, and in case it fails (has an exit code greater than 0), bail out:

( echo "Do one thing" echo "Do another thing" some_command || exit 3 echo "Do yet another thing" echo "And do a last thing" ) ... 

Unfortunately, using this technique you are restricted to 255 different exit codes (1..255) and no decent exception objects can be used.

If you need more information to pass along with your simulated exception, you can use the stdout of the subshells, but that is a bit complicated and maybe another question ;-)

Using the above mentioned -e flag to the shell you can even strip that explicit exit statement:

( set -e echo "Do one thing" echo "Do another thing" some_command echo "Do yet another thing" echo "And do a last thing" ) ... 
3

You can use trap:

try { block A } catch { block B } finally { block C }

translates to:

( set -Ee function _catch { block B exit 0 # optional; use if you don't want to propagate (rethrow) error to outer shell } function _finally { block C } trap _catch ERR trap _finally EXIT block A ) 
1

There are so many similar solutions which probably work. Below is a simple and working way to accomplish try/catch, with explanation in the comments.

#!/bin/bash function a() { # do some stuff here } function b() { # do more stuff here } # this subshell is a scope of try # try ( # this flag will make to exit from current subshell on any error # inside it (all functions run inside will also break on any error) set -e a b # do more stuff here ) # and here we catch errors # catch errorCode=$? if [ $errorCode -ne 0 ]; then echo "We have an error" # We exit the all script with the same error, if you don't want to # exit it and continue, just delete this line. exit $errorCode fi 
2

As everybody says, bash doesn't have a proper language-supported try/catch syntax. You can launch bash with the -e argument or use set -e inside the script to abort the entire bash process if any command has a non-zero exit code. (You can also set +e to temporarily allow failing commands.)

So, one technique to simulate a try/catch block is to launch a sub-process to do the work with -e enabled. Then in the main process, check the return code of the sub-process.

Bash supports heredoc strings, so you don't have to write two separate files to handle this. In the below example, the TRY heredoc will run in a separate bash instance, with -e enabled, so the sub-process will crash if any command returns a non-zero exit code. Then, back in the main process, we can check the return code to handle a catch block.

#!/bin/bash set +e bash -e <<TRY echo hello cd /does/not/exist echo world TRY if [ $? -ne 0 ]; then echo caught exception fi 

It's not a proper language-supported try/catch block, but it may scratch a similar itch for you.

You can do:

#!/bin/bash if <command> ; then # TRY <do-whatever-you-want> else # CATCH echo 'Exception' <do-whatever-you-want> fi 

And you have traps which is not the same, but other technique you can use for this purpose

1

Assuming the terminal app in question is named 'app' (and you expect it to be in your $PATH):

if [[ ! `which app` ]]; then # run code if app not installed else # run code if app is installed fi 

I use something like this:

YOUR COMMAND HERE EXITCODE=$? if [ "$EXITCODE" -ne "0" ]; then #this is the catch part echo "uh oh" exit $EXITCODE fi 

this just checks the exit code of the command and compares it to zero (which indicates the command ran successfully)

A very simple thing I use:

try() { "$@" || (e=$?; echo "$@" > /dev/stderr; exit $e) } 
1

Below is an complete copy of the simplified script used in my other answer. Beyond additional error checking, there is an alias which allows the user to change the name of an existing alias. The syntax is given below. If the new_alias parameter is omitted, then the alias is removed.

ChangeAlias old_alias [new_alias] 

The complete script is given below.

common.GetAlias() { local "oldname=${1:-0}" if [[ $oldname =~ ^[0-9]+$ && oldname+1 -lt ${#FUNCNAME[@]} ]]; then oldname="${FUNCNAME[oldname + 1]}" fi name="common_${oldname#common.}" echo "${!name:-$oldname}" } common.Alias() { if [[ $# -ne 2 || -z $1 || -z $2 ]]; then echo "$(common.GetAlias): The must be only two parameters of nonzero length" >&2 return 1; fi eval "alias $1='$2'" local "f=${2##*common.}" f="${f%%;*}" local "v=common_$f" f="common.$f" if [[ -n ${!v:-} ]]; then echo "$(common.GetAlias): $1: Function \`$f' already paired with name \`${!v}'" >&2 return 1; fi shopt -s expand_aliases eval "$v=\"$1\"" } common.ChangeAlias() { if [[ $# -lt 1 || $# -gt 2 ]]; then echo "usage: $(common.GetAlias) old_name [new_name]" >&2 return "1" elif ! alias "$1" &>"/dev/null"; then echo "$(common.GetAlias): $1: Name not found" >&2 return 1; fi local "s=$(alias "$1")" s="${s#alias $1=\'}" s="${s%\'}" local "f=${s##*common.}" f="${f%%;*}" local "v=common_$f" f="common.$f" if [[ ${!v:-} != "$1" ]]; then echo "$(common.GetAlias): $1: Name not paired with a function \`$f'" >&2 return 1; elif [[ $# -gt 1 ]]; then eval "alias $2='$s'" eval "$v=\"$2\"" else unset "$v" fi unalias "$1" } common.Alias exception 'common.Exception' common.Alias throw 'common.Throw' common.Alias try '{ if common.Try; then' common.Alias yrt 'common.EchoExitStatus; fi; common.yrT; }' common.Alias catch '{ while common.Catch' common.Alias hctac 'common.hctaC -r; done; common.hctaC; }' common.Alias finally '{ if common.Finally; then' common.Alias yllanif 'fi; common.yllaniF; }' common.Alias caught 'common.Caught' common.Alias EchoExitStatus 'common.EchoExitStatus' common.Alias EnableThrowOnError 'common.EnableThrowOnError' common.Alias DisableThrowOnError 'common.DisableThrowOnError' common.Alias GetStatus 'common.GetStatus' common.Alias SetStatus 'common.SetStatus' common.Alias GetMessage 'common.GetMessage' common.Alias MessageCount 'common.MessageCount' common.Alias CopyMessages 'common.CopyMessages' common.Alias TryCatchFinally 'common.TryCatchFinally' common.Alias DefaultErrHandler 'common.DefaultErrHandler' common.Alias DefaultUnhandled 'common.DefaultUnhandled' common.Alias CallStack 'common.CallStack' common.Alias ChangeAlias 'common.ChangeAlias' common.Alias TryCatchFinallyAlias 'common.Alias' common.CallStack() { local -i "i" "j" "k" "subshell=${2:-0}" "wi" "wl" "wn" local "format= %*s %*s %-*s %s\n" "name" eval local "lineno=('' ${BASH_LINENO[@]})" for (( i=${1:-0},j=wi=wl=wn=0; i<${#FUNCNAME[@]}; ++i,++j )); do name="$(common.GetAlias "$i")" let "wi = ${#j} > wi ? wi = ${#j} : wi" let "wl = ${#lineno[i]} > wl ? wl = ${#lineno[i]} : wl" let "wn = ${#name} > wn ? wn = ${#name} : wn" done for (( i=${1:-0},j=0; i<${#FUNCNAME[@]}; ++i,++j )); do ! let "k = ${#FUNCNAME[@]} - i - 1" name="$(common.GetAlias "$i")" printf "$format" "$wi" "$j" "$wl" "${lineno[i]}" "$wn" "$name" "${BASH_SOURCE[i]}" done } common.Echo() { [[ $common_options != *d* ]] || echo "$@" >"$common_file" } common.DefaultErrHandler() { echo "Orginal Status: $common_status" echo "Exception Type: ERR" } common.Exception() { common.TryCatchFinallyVerify || return if [[ $# -eq 0 ]]; then echo "$(common.GetAlias): At least one parameter is required" >&2 return "1" elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ]]; then echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2 return "1" fi let "common_status = 10#$1" shift common_messages=() for message in "$@"; do common_messages+=("$message") done if [[ $common_options == *c* ]]; then echo "Call Stack:" >"$common_fifo" common.CallStack "2" >"$common_fifo" fi } common.Throw() { common.TryCatchFinallyVerify || return local "message" if ! common.TryCatchFinallyExists; then echo "$(common.GetAlias): No Try-Catch-Finally exists" >&2 return "1" elif [[ $# -eq 0 && common_status -eq 0 ]]; then echo "$(common.GetAlias): No previous unhandled exception" >&2 return "1" elif [[ $# -gt 0 && ( ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ) ]]; then echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2 return "1" fi common.Echo -n "In Throw ?=$common_status " common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL #=$#" if [[ $common_options == *k* ]]; then common.CallStack "2" >"$common_file" fi if [[ $# -gt 0 ]]; then let "common_status = 10#$1" shift for message in "$@"; do echo "$message" >"$common_fifo" done if [[ $common_options == *c* ]]; then echo "Call Stack:" >"$common_fifo" common.CallStack "2" >"$common_fifo" fi elif [[ ${#common_messages[@]} -gt 0 ]]; then for message in "${common_messages[@]}"; do echo "$message" >"$common_fifo" done fi chmod "0400" "$common_fifo" common.Echo "Still in Throw $=$common_status subshell=$BASH_SUBSHELL #=$# -=$-" exit "$common_status" } common.ErrHandler() { common_status=$? trap ERR common.Echo -n "In ErrHandler ?=$common_status debug=$common_options " common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL order=$common_order" if [[ -w "$common_fifo" ]]; then if [[ $common_options != *e* ]]; then common.Echo "ErrHandler is ignoring" common_status="0" return "$common_status" # value is ignored fi if [[ $common_options == *k* ]]; then common.CallStack "2" >"$common_file" fi common.Echo "Calling ${common_errHandler:-}" eval "${common_errHandler:-} \"${BASH_LINENO[0]}\" \"${BASH_SOURCE[1]}\" \"${FUNCNAME[1]}\" >$common_fifo <$common_fifo" if [[ $common_options == *c* ]]; then echo "Call Stack:" >"$common_fifo" common.CallStack "2" >"$common_fifo" fi chmod "0400" "$common_fifo" fi common.Echo "Still in ErrHandler $=$common_status subshell=$BASH_SUBSHELL -=$-" if [[ common_trySubshell -eq BASH_SUBSHELL ]]; then return "$common_status" # value is ignored else exit "$common_status" fi } common.Token() { local "name" case $1 in b) name="before";; t) name="$common_Try";; y) name="$common_yrT";; c) name="$common_Catch";; h) name="$common_hctaC";; f) name="$common_yllaniF";; l) name="$common_Finally";; *) name="unknown";; esac echo "$name" } common.TryCatchFinallyNext() { common.ShellInit local "previous=$common_order" "errmsg" common_order="$2" if [[ $previous != $1 ]]; then errmsg="${BASH_SOURCE[2]}: line ${BASH_LINENO[1]}: syntax error_near unexpected token \`$(common.Token "$2")'" echo "$errmsg" >&2 [[ /dev/fd/2 -ef $common_file ]] || echo "$errmsg" >"$common_file" kill -s INT 0 return "1" fi } common.ShellInit() { if [[ common_initSubshell -ne BASH_SUBSHELL ]]; then common_initSubshell="$BASH_SUBSHELL" common_order="b" fi } common.Try() { common.TryCatchFinallyVerify || return common.TryCatchFinallyNext "[byhl]" "t" || return common_status="0" common_subshell="$common_trySubshell" common_trySubshell="$BASH_SUBSHELL" common_messages=() common.Echo "-------------> Setting try=$common_trySubshell at subshell=$BASH_SUBSHELL" } common.yrT() { local "status=$?" common.TryCatchFinallyVerify || return common.TryCatchFinallyNext "[t]" "y" || return common.Echo -n "Entered yrT ?=$status status=$common_status " common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL" if [[ common_status -ne 0 ]]; then common.Echo "Build message array. ?=$common_status, subshell=$BASH_SUBSHELL" local "message=" "eof=TRY_CATCH_FINALLY_END_OF_MESSAGES_$RANDOM" chmod "0600" "$common_fifo" echo "$eof" >"$common_fifo" common_messages=() while read "message"; do common.Echo "----> $message" [[ $message != *$eof ]] || break common_messages+=("$message") done <"$common_fifo" fi common.Echo "In ytT status=$common_status" common_trySubshell="$common_subshell" } common.Catch() { common.TryCatchFinallyVerify || return common.TryCatchFinallyNext "[yh]" "c" || return [[ common_status -ne 0 ]] || return "1" local "parameter" "pattern" "value" local "toggle=true" "compare=p" "options=$-" local -i "i=-1" "status=0" set -f for parameter in "$@"; do if "$toggle"; then toggle="false" if [[ $parameter =~ ^-[notepr]$ ]]; then compare="${parameter#-}" continue fi fi toggle="true" while "true"; do eval local "patterns=($parameter)" if [[ ${#patterns[@]} -gt 0 ]]; then for pattern in "${patterns[@]}"; do [[ i -lt ${#common_messages[@]} ]] || break if [[ i -lt 0 ]]; then value="$common_status" else value="${common_messages[i]}" fi case $compare in [ne]) [[ ! $value == "$pattern" ]] || break 2;; [op]) [[ ! $value == $pattern ]] || break 2;; [tr]) [[ ! $value =~ $pattern ]] || break 2;; esac done fi if [[ $compare == [not] ]]; then let "++i,1" continue 2 else status="1" break 2 fi done if [[ $compare == [not] ]]; then status="1" break else let "++i,1" fi done [[ $options == *f* ]] || set +f return "$status" } common.hctaC() { common.TryCatchFinallyVerify || return common.TryCatchFinallyNext "[c]" "h" || return [[ $# -ne 1 || $1 != -r ]] || common_status="0" } common.Finally() { common.TryCatchFinallyVerify || return common.TryCatchFinallyNext "[ych]" "f" || return } common.yllaniF() { common.TryCatchFinallyVerify || return common.TryCatchFinallyNext "[f]" "l" || return [[ common_status -eq 0 ]] || common.Throw } common.Caught() { common.TryCatchFinallyVerify || return [[ common_status -eq 0 ]] || return 1 } common.EchoExitStatus() { return "${1:-$?}" } common.EnableThrowOnError() { common.TryCatchFinallyVerify || return [[ $common_options == *e* ]] || common_options+="e" } common.DisableThrowOnError() { common.TryCatchFinallyVerify || return common_options="${common_options/e}" } common.GetStatus() { common.TryCatchFinallyVerify || return echo "$common_status" } common.SetStatus() { common.TryCatchFinallyVerify || return if [[ $# -ne 1 ]]; then echo "$(common.GetAlias): $#: Wrong number of parameters" >&2 return "1" elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ]]; then echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2 return "1" fi let "common_status = 10#$1" } common.GetMessage() { common.TryCatchFinallyVerify || return local "upper=${#common_messages[@]}" if [[ upper -eq 0 ]]; then echo "$(common.GetAlias): $1: There are no messages" >&2 return "1" elif [[ $# -ne 1 ]]; then echo "$(common.GetAlias): $#: Wrong number of parameters" >&2 return "1" elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -ge upper ]]; then echo "$(common.GetAlias): $1: First parameter was an invalid index" >&2 return "1" fi echo "${common_messages[$1]}" } common.MessageCount() { common.TryCatchFinallyVerify || return echo "${#common_messages[@]}" } common.CopyMessages() { common.TryCatchFinallyVerify || return if [[ $# -ne 1 ]]; then echo "$(common.GetAlias): $#: Wrong number of parameters" >&2 return "1" elif [[ ${#common_messages} -gt 0 ]]; then eval "$1=(\"\${common_messages[@]}\")" else eval "$1=()" fi } common.TryCatchFinallyExists() { [[ ${common_fifo:-u} != u ]] } common.TryCatchFinallyVerify() { local "name" if ! common.TryCatchFinallyExists; then echo "$(common.GetAlias "1"): No Try-Catch-Finally exists" >&2 return "2" fi } common.GetOptions() { local "opt" local "name=$(common.GetAlias "1")" if common.TryCatchFinallyExists; then echo "$name: A Try-Catch-Finally already exists" >&2 return "1" fi let "OPTIND = 1" let "OPTERR = 0" while getopts ":cdeh:ko:u:v:" opt "$@"; do case $opt in c) [[ $common_options == *c* ]] || common_options+="c";; d) [[ $common_options == *d* ]] || common_options+="d";; e) [[ $common_options == *e* ]] || common_options+="e";; h) common_errHandler="$OPTARG";; k) [[ $common_options == *k* ]] || common_options+="k";; o) common_file="$OPTARG";; u) common_unhandled="$OPTARG";; v) common_command="$OPTARG";; \?) #echo "Invalid option: -$OPTARG" >&2 echo "$name: Illegal option: $OPTARG" >&2 return "1";; :) echo "$name: Option requires an argument: $OPTARG" >&2 return "1";; *) echo "$name: An error occurred while parsing options." >&2 return "1";; esac done shift "$((OPTIND - 1))" if [[ $# -lt 1 ]]; then echo "$name: The fifo_file parameter is missing" >&2 return "1" fi common_fifo="$1" if [[ ! -p $common_fifo ]]; then echo "$name: $1: The fifo_file is not an open FIFO" >&2 return "1" fi shift if [[ $# -lt 1 ]]; then echo "$name: The function parameter is missing" >&2 return "1" fi common_function="$1" if ! chmod "0600" "$common_fifo"; then echo "$name: $common_fifo: Can not change file mode to 0600" >&2 return "1" fi local "message=" "eof=TRY_CATCH_FINALLY_END_OF_FILE_$RANDOM" { echo "$eof" >"$common_fifo"; } 2>"/dev/null" if [[ $? -ne 0 ]]; then echo "$name: $common_fifo: Can not write" >&2 return "1" fi { while [[ $message != *$eof ]]; do read "message" done <"$common_fifo"; } 2>"/dev/null" if [[ $? -ne 0 ]]; then echo "$name: $common_fifo: Can not read" >&2 return "1" fi return "0" } common.DefaultUnhandled() { local -i "i" echo "-------------------------------------------------" echo "$(common.GetAlias "common.TryCatchFinally"): Unhandeled exception occurred" echo "Status: $(GetStatus)" echo "Messages:" for ((i=0; i<$(MessageCount); i++)); do echo "$(GetMessage "$i")" done echo "-------------------------------------------------" } common.TryCatchFinally() { local "common_file=/dev/fd/2" local "common_errHandler=common.DefaultErrHandler" local "common_unhandled=common.DefaultUnhandled" local "common_options=" local "common_fifo=" local "common_function=" local "common_flags=$-" local "common_trySubshell=-1" local "common_initSubshell=-1" local "common_subshell" local "common_status=0" local "common_order=b" local "common_command=" local "common_messages=()" local "common_handler=$(trap -p ERR)" [[ -n $common_handler ]] || common_handler="trap ERR" common.GetOptions "$@" || return "$?" shift "$((OPTIND + 1))" [[ -z $common_command ]] || common_command+="=$" common_command+='("$common_function" "$@")' set -E set +e trap "common.ErrHandler" ERR if true; then common.Try eval "$common_command" common.EchoExitStatus common.yrT fi while common.Catch; do "$common_unhandled" >&2 break common.hctaC -r done common.hctaC [[ $common_flags == *E* ]] || set +E [[ $common_flags != *e* ]] || set -e [[ $common_flags != *f* || $- == *f* ]] || set -f [[ $common_flags == *f* || $- != *f* ]] || set +f eval "$common_handler" return "$((common_status?2:0))" } 

I can recommend this in "bash -ue" mode:

set -ue false && RET=$? || RET=$? echo "expecting 1, got ${RET}" true && RET=$? || RET=$? echo "expecting 0, got ${RET}" echo "test try...catch" false && RET=$? || RET=$? if [ ${RET} -ne 0 ]; then echo "caught error ${RET}" fi echo "beware, using '||' before '&&' fails" echo " -> memory aid: [A]nd before [O]r in the alphabet" false || RET=$? && RET=$? echo "expecting 1, got ${RET}" true || RET=$? && RET=$? echo "expecting 0, got ${RET}" 

Below is an example of a script which implements try/catch/finally in bash.

Like other answers to this question, exceptions must be caught after exiting a subprocess.

The example scripts start by creating an anonymous fifo, which is used to pass string messages from a command exception or throw to end of the closest try block. Here the messages are removed from the fifo and placed in an array variable. The status is returned through return and exit commands and placed in a different variable. To enter a catch block, this status must not be zero. Other requirements to enter a catch block are passed as parameters. If the end of a catch block is reached, then the status is set to zero. If the end of the finally block is reached and the status is still nonzero, then an implicit throw containing the messages and status is executed. The script requires the calling of the function trycatchfinally which contains an unhandled exception handler.

The syntax for the trycatchfinally command is given below.

trycatchfinally [-cde] [-h ERR_handler] [-k] [-o debug_file] [-u unhandled_handler] [-v variable] fifo function 

The -c option adds the call stack to the exception messages.
The -d option enables debug output.
The -e option enables command exceptions.
The -h option allows the user to substitute their own command exception handler.
The -k option adds the call stack to the debug output.
The -o option replaces the default output file which is /dev/fd/2.
The -u option allows the user to substitute their own unhandled exception handler.
The -v option allows the user the option to pass back values though the use of Command Substitution.
The fifo is the fifo filename.
The function function is called by trycatchfinally as a subprocess.

Note: The cdko options were removed to simplify the script.

The syntax for the catch command is given below.

catch [[-enoprt] list ...] ... 

The options are defined below. The value for the first list is the status. Subsquent values are the messages. If the there are more messages than lists, then the remaining messages are ignored.

-e means [[ $value == "$string" ]] (the value has to match at least one string in the list)
-n means [[ $value != "$string" ]] (the value can not match any of the strings in the list)
-o means [[ $value != $pattern ]] (the value can not match any of the patterns in the list)
-p means [[ $value == $pattern ]] (the value has to match at least one pattern in the list)
-r means [[ $value =~ $regex ]] (the value has to match at least one extended regular expression in the list)
-t means [[ ! $value =~ $regex ]] (the value can not match any of the extended regular expressions in the list)

The try/catch/finally script is given below. To simplify the script for this answer, most of the error checking was removed. This reduced the size by 64%. A complete copy of this script can be found at my other answer.

shopt -s expand_aliases alias try='{ common.Try' alias yrt='EchoExitStatus; common.yrT; }' alias catch='{ while common.Catch' alias hctac='common.hctaC; done; }' alias finally='{ common.Finally' alias yllanif='common.yllaniF; }' DefaultErrHandler() { echo "Orginal Status: $common_status" echo "Exception Type: ERR" } exception() { let "common_status = 10#$1" shift common_messages=() for message in "$@"; do common_messages+=("$message") done } throw() { local "message" if [[ $# -gt 0 ]]; then let "common_status = 10#$1" shift for message in "$@"; do echo "$message" >"$common_fifo" done elif [[ ${#common_messages[@]} -gt 0 ]]; then for message in "${common_messages[@]}"; do echo "$message" >"$common_fifo" done fi chmod "0400" "$common_fifo" exit "$common_status" } common.ErrHandler() { common_status=$? trap ERR if [[ -w "$common_fifo" ]]; then if [[ $common_options != *e* ]]; then common_status="0" return fi eval "${common_errHandler:-} \"${BASH_LINENO[0]}\" \"${BASH_SOURCE[1]}\" \"${FUNCNAME[1]}\" >$common_fifo <$common_fifo" chmod "0400" "$common_fifo" fi if [[ common_trySubshell -eq BASH_SUBSHELL ]]; then return else exit "$common_status" fi } common.Try() { common_status="0" common_subshell="$common_trySubshell" common_trySubshell="$BASH_SUBSHELL" common_messages=() } common.yrT() { local "status=$?" if [[ common_status -ne 0 ]]; then local "message=" "eof=TRY_CATCH_FINALLY_END_OF_MESSAGES_$RANDOM" chmod "0600" "$common_fifo" echo "$eof" >"$common_fifo" common_messages=() while read "message"; do [[ $message != *$eof ]] || break common_messages+=("$message") done <"$common_fifo" fi common_trySubshell="$common_subshell" } common.Catch() { [[ common_status -ne 0 ]] || return "1" local "parameter" "pattern" "value" local "toggle=true" "compare=p" "options=$-" local -i "i=-1" "status=0" set -f for parameter in "$@"; do if "$toggle"; then toggle="false" if [[ $parameter =~ ^-[notepr]$ ]]; then compare="${parameter#-}" continue fi fi toggle="true" while "true"; do eval local "patterns=($parameter)" if [[ ${#patterns[@]} -gt 0 ]]; then for pattern in "${patterns[@]}"; do [[ i -lt ${#common_messages[@]} ]] || break if [[ i -lt 0 ]]; then value="$common_status" else value="${common_messages[i]}" fi case $compare in [ne]) [[ ! $value == "$pattern" ]] || break 2;; [op]) [[ ! $value == $pattern ]] || break 2;; [tr]) [[ ! $value =~ $pattern ]] || break 2;; esac done fi if [[ $compare == [not] ]]; then let "++i,1" continue 2 else status="1" break 2 fi done if [[ $compare == [not] ]]; then status="1" break else let "++i,1" fi done [[ $options == *f* ]] || set +f return "$status" } common.hctaC() { common_status="0" } common.Finally() { : } common.yllaniF() { [[ common_status -eq 0 ]] || throw } caught() { [[ common_status -eq 0 ]] || return 1 } EchoExitStatus() { return "${1:-$?}" } EnableThrowOnError() { [[ $common_options == *e* ]] || common_options+="e" } DisableThrowOnError() { common_options="${common_options/e}" } GetStatus() { echo "$common_status" } SetStatus() { let "common_status = 10#$1" } GetMessage() { echo "${common_messages[$1]}" } MessageCount() { echo "${#common_messages[@]}" } CopyMessages() { if [[ ${#common_messages} -gt 0 ]]; then eval "$1=(\"\${common_messages[@]}\")" else eval "$1=()" fi } common.GetOptions() { local "opt" let "OPTIND = 1" let "OPTERR = 0" while getopts ":cdeh:ko:u:v:" opt "$@"; do case $opt in e) [[ $common_options == *e* ]] || common_options+="e";; h) common_errHandler="$OPTARG";; u) common_unhandled="$OPTARG";; v) common_command="$OPTARG";; esac done shift "$((OPTIND - 1))" common_fifo="$1" shift common_function="$1" chmod "0600" "$common_fifo" } DefaultUnhandled() { local -i "i" echo "-------------------------------------------------" echo "TryCatchFinally: Unhandeled exception occurred" echo "Status: $(GetStatus)" echo "Messages:" for ((i=0; i<$(MessageCount); i++)); do echo "$(GetMessage "$i")" done echo "-------------------------------------------------" } TryCatchFinally() { local "common_errHandler=DefaultErrHandler" local "common_unhandled=DefaultUnhandled" local "common_options=" local "common_fifo=" local "common_function=" local "common_flags=$-" local "common_trySubshell=-1" local "common_subshell" local "common_status=0" local "common_command=" local "common_messages=()" local "common_handler=$(trap -p ERR)" [[ -n $common_handler ]] || common_handler="trap ERR" common.GetOptions "$@" shift "$((OPTIND + 1))" [[ -z $common_command ]] || common_command+="=$" common_command+='("$common_function" "$@")' set -E set +e trap "common.ErrHandler" ERR try eval "$common_command" yrt catch; do "$common_unhandled" >&2 hctac [[ $common_flags == *E* ]] || set +E [[ $common_flags != *e* ]] || set -e [[ $common_flags != *f* || $- == *f* ]] || set -f [[ $common_flags == *f* || $- != *f* ]] || set +f eval "$common_handler" } 

Below is an example, which assumes the above script is stored in the file named simple. The makefifo file contains the script described in this answer. The assumption is made that the file named 4444kkkkk does not exist, therefore causing an exception to occur. The error message output from the ls 4444kkkkk command is automatically suppressed until inside the appropriate catch block.

#!/bin/bash # if [[ $0 != ${BASH_SOURCE[0]} ]]; then bash "${BASH_SOURCE[0]}" "$@" return fi source simple source makefifo MyFunction3() { echo "entered MyFunction3" >&4 echo "This is from MyFunction3" ls 4444kkkkk echo "leaving MyFunction3" >&4 } MyFunction2() { echo "entered MyFunction2" >&4 value="$(MyFunction3)" echo "leaving MyFunction2" >&4 } MyFunction1() { echo "entered MyFunction1" >&4 local "flag=false" try ( echo "start of try" >&4 MyFunction2 echo "end of try" >&4 ) yrt catch "[1-3]" "*" "Exception\ Type:\ ERR"; do echo 'start of catch "[1-3]" "*" "Exception\ Type:\ ERR"' local -i "i" echo "-------------------------------------------------" echo "Status: $(GetStatus)" echo "Messages:" for ((i=0; i<$(MessageCount); i++)); do echo "$(GetMessage "$i")" done echo "-------------------------------------------------" break echo 'end of catch "[1-3]" "*" "Exception\ Type:\ ERR"' hctac >&4 catch "1 3 5" "*" -n "Exception\ Type:\ ERR"; do echo 'start of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"' echo "-------------------------------------------------" echo "Status: $(GetStatus)" [[ $(MessageCount) -le 1 ]] || echo "$(GetMessage "1")" echo "-------------------------------------------------" break echo 'end of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"' hctac >&4 catch; do echo 'start of catch' >&4 echo "failure" flag="true" echo 'end of catch' >&4 hctac finally echo "in finally" yllanif >&4 "$flag" || echo "success" echo "leaving MyFunction1" >&4 } 2>&6 ErrHandler() { echo "EOF" DefaultErrHandler "$@" echo "Function: $3" while read; do [[ $REPLY != *EOF ]] || break echo "$REPLY" done } set -u echo "starting" >&2 MakeFIFO "6" TryCatchFinally -e -h ErrHandler -o /dev/fd/4 -v result /dev/fd/6 MyFunction1 4>&2 echo "result=$result" exec >&6- 

The above script was tested using GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). The output, from running this script, is shown below.

starting entered MyFunction1 start of try entered MyFunction2 entered MyFunction3 start of catch "[1-3]" "*" "Exception\ Type:\ ERR" ------------------------------------------------- Status: 1 Messages: Orginal Status: 1 Exception Type: ERR Function: MyFunction3 ls: 4444kkkkk: No such file or directory ------------------------------------------------- start of catch end of catch in finally leaving MyFunction1 result=failure 

Another example which uses a throw can be created by replacing function MyFunction3 with the script shown below.

MyFunction3() { echo "entered MyFunction3" >&4 echo "This is from MyFunction3" throw "3" "Orginal Status: 3" "Exception Type: throw" echo "leaving MyFunction3" >&4 } 

The syntax for the throw command is given below. If no parameters are present, then status and messages stored in the variables are used instead.

throw [status] [message ...] 

The output, from executing the modified script, is shown below.

starting entered MyFunction1 start of try entered MyFunction2 entered MyFunction3 start of catch "1 3 5" "*" -n "Exception\ Type:\ ERR" ------------------------------------------------- Status: 3 Exception Type: throw ------------------------------------------------- start of catch end of catch in finally leaving MyFunction1 result=failure 

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy