Di seguito è riportato un esempio di uno script che implementa try/catch/finally
in bash.
Come altre risposte a questa domanda, le eccezioni devono essere rilevate dopo essere usciti da un sottoprocesso.
Gli script di esempio iniziano creando un Fifo anonimo, che viene utilizzato per passare i messaggi di stringa da command exception
o throw
alla fine del try
blocco più vicino . Qui i messaggi vengono rimossi dal FIFO e inseriti in una variabile di matrice. Lo stato viene restituito tramite return
e exit
comandi e inserito in una variabile diversa. Per inserire un catch
blocco, questo stato non deve essere zero. Altri requisiti per inserire un catch
blocco vengono passati come parametri. Se catch
viene raggiunta la fine di un blocco, lo stato viene impostato su zero. Se finally
viene raggiunta la fine del blocco e lo stato è ancora diverso da zero, viene eseguito un lancio implicito contenente i messaggi e lo stato. Lo script richiede la chiamata della funzione trycatchfinally
che contiene un gestore di eccezioni non gestito.
La sintassi per il trycatchfinally
comando è riportata di seguito.
trycatchfinally [-cde] [-h ERR_handler] [-k] [-o debug_file] [-u unhandled_handler] [-v variable] fifo function
L' -c
opzione aggiunge lo stack di chiamate ai messaggi di eccezione.
L' -d
opzione abilita l'output di debug.
L' -e
opzione abilita le eccezioni di comando.
L' -h
opzione consente all'utente di sostituire il proprio gestore delle eccezioni ai comandi.
L' -k
opzione aggiunge lo stack di chiamate all'output di debug.
L' -o
opzione sostituisce il file di output predefinito che è /dev/fd/2
.
L' -u
opzione consente all'utente di sostituire il proprio gestore di eccezioni non gestito.
L' -v
opzione consente all'utente di restituire valori tramite l'uso della sostituzione dei comandi.
La fifo
è il nome del file FIFO.
La funzione function
viene chiamata trycatchfinally
come sottoprocesso.
Nota: le cdko
opzioni sono state rimosse per semplificare lo script.
La sintassi per il catch
comando è riportata di seguito.
catch [[-enoprt] list ...] ...
Le opzioni sono definite di seguito. Il valore per il primo elenco è lo stato. I valori secondari sono i messaggi. Se ci sono più messaggi che elenchi, i messaggi rimanenti vengono ignorati.
-e
significa [[ $value == "$string" ]]
(il valore deve corrispondere ad almeno una stringa nell'elenco)
-n
significa [[ $value != "$string" ]]
(il valore non può corrispondere a nessuna delle stringhe nell'elenco)
-o
significa [[ $value != $pattern ]]
(il valore non può corrispondere a nessuno dei modelli nell'elenco)
-p
significa [[ $value == $pattern ]]
(il valore ha per corrispondere ad almeno un modello nell'elenco)
-r
significa [[ $value =~ $regex ]]
(il valore deve corrispondere ad almeno un'espressione regolare estesa nell'elenco)
-t
significa [[ ! $value =~ $regex ]]
(il valore non può corrispondere a nessuna delle espressioni regolari estese nell'elenco)
Lo try/catch/finally
script è riportato di seguito. Per semplificare lo script per questa risposta, la maggior parte del controllo degli errori è stata rimossa. Ciò ha ridotto le dimensioni del 64%. Una copia completa di questo script può essere trovata nella mia altra risposta .
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"
}
Di seguito è riportato un esempio, che presuppone che lo script sopra sia memorizzato nel file denominato simple
. Il makefifo
file contiene lo script descritto in questa risposta . Si presume che il file denominato 4444kkkkk
non esista, causando quindi un'eccezione. Il messaggio di errore emesso dal ls 4444kkkkk
comando viene automaticamente eliminato fino a quando non si trova all'interno del catch
blocco appropriato .
#!/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-
Lo script sopra è stato testato usando GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
. L'output, dall'esecuzione di questo script, è mostrato di seguito.
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
Un altro esempio che utilizza un throw
può essere creato sostituendo la funzione MyFunction3
con lo script mostrato di seguito.
MyFunction3() {
echo "entered MyFunction3" >&4
echo "This is from MyFunction3"
throw "3" "Orginal Status: 3" "Exception Type: throw"
echo "leaving MyFunction3" >&4
}
La sintassi per il throw
comando è riportata di seguito. Se non sono presenti parametri, vengono invece utilizzati lo stato e i messaggi memorizzati nelle variabili.
throw [status] [message ...]
L'output, dall'esecuzione dello script modificato, è mostrato di seguito.
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