Esiste un comando TRY CATCH in Bash


351

Sto scrivendo uno script di shell e devo verificare che sia stata installata un'app terminale. Voglio usare un comando TRY / CATCH per fare questo a meno che non ci sia un modo più ordinato.


1
Potrebbe essere utile se potessi elaborare quale problema stai tentando di risolvere. Sembra che tu non sia esattamente nuovo qui, ma potresti comunque voler visitare il Centro assistenza e vedere aiuto su come porre una buona domanda.
Devnull,

Detto questo, sembra che dire help testpotrebbe aiutarti a trovare la soluzione al tuo problema.
Devnull,

2
try / catch / finally block non è un comando, è un costrutto
Ben


@LeeProbert: Dal momento che non hai eccezioni in bash, mi chiedo che cosa vuoi catturare. La cosa più vicina che va nella direzione di un'eccezione sarebbe un segnale, e la maggior parte (non tutte) di quelle che puoi catturare usando il trapcomando.
user1934428

Risposte:


563

Esiste un comando TRY CATCH in Bash?

No.

Bash non ha tutti i lussi che si possono trovare in molti linguaggi di programmazione.

Non c'è try/catchin bash; tuttavia, si può ottenere un comportamento simile usando &&o ||.

Utilizzando ||:

se command1fallisce, viene command2eseguito come segue

command1 || command2

Allo stesso modo, utilizzando &&, command2verrà eseguito se ha command1esito positivo

L'approssimazione più vicina di try/catchè la seguente

{ # try

    command1 &&
    #save your output

} || { # catch
    # save log for exception 
}

Anche bash contiene anche alcuni meccanismi di gestione degli errori

set -e

interrompe lo script se un comando semplice non riesce.

E anche perché no if...else. È il tuo migliore amico.


18
Con questo, è necessario assicurarsi che il codice #save your outputnon fallisca, altrimenti il ​​blocco "catch" verrà comunque eseguito.
Chepner,

7
C'è un suggerimento per usare un if...elsecostrutto. Ciò implica che i comandi bash si risolvono come "veritieri" se vengono eseguiti correttamente e "falsi" se falliscono?
Luke Griffiths,

6
Per i lettori di questa discussione: sembra che set -enon sia necessariamente il modo migliore per fare le cose; ecco alcuni contro-argomenti / casi speciali: mywiki.wooledge.org/BashFAQ/105
Luke Davis,

2
Posso sapere come registrare l'eccezione? Normalmente nel codice Java, possiamo usare system.out.log (e), ma che ne dici di shell?
Panadol Chong,

112

Sulla base di alcune risposte che ho trovato qui, mi sono creato un piccolo file di supporto per i miei progetti:

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
}

ecco un esempio di come appare in uso:

#!/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

    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
}

2
Potresti sho come importare le funzioni try catch nell'altro esempio? (
Suppongo

1
@kilianc: L'ho appena fatto come: source inc / trycatch.sh.
Mathias Henze,

2
@MathiasHenze Grazie amico, il tuo codice è dannatamente bello. Ma perché hai bisogno di un ||dopo catche prima del {}blocco? Avrei pensato che fosse un&&
Remy San

(risposta tardiva per chiunque lo trovi) In sostanza, il caso di errore if False or run_if_failed()significa che il corto circuito O ha provato la prima istruzione che non è tornata vera e ora passa alla frase successiva. &&non funzionerebbe perché la prima istruzione ( try) ha fornito false, il che significa che l' catchaffermazione non è necessaria per la regola di tautologia false&any equals false. Solo un corto circuito AND / OR eseguirà entrambi.
ldmtwo

69

Ho sviluppato un'implementazione try & catch quasi impeccabile in bash, che ti permette di scrivere codice come:

try 
    echo 'Hello'
    false
    echo 'This will not be displayed'

catch 
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

Puoi persino annidare i blocchi try-catch dentro di loro!

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__!"
}

Il codice fa parte della mia bash boilerplate / framework . Estende ulteriormente l'idea di provare e catturare con cose come la gestione degli errori con backtrace ed eccezioni (oltre ad alcune altre belle funzioni).

Ecco il codice responsabile solo di 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
}

Sentiti libero di usare, fork e contribuire - è su GitHub .


@ erm3nda Lieto di sentirlo! Penso di aver eliminato alcuni bug dopo averlo pubblicato, quindi dai un'occhiata a GitHub per gli aggiornamenti (dovrai includere 03_exception.sh e 04_try_catch.sh). Per quanto ne so, la versione attuale è praticamente a prova di proiettile.
niieani,

Molto bella! Ho intenzione di utilizzare nel mio progetto. Mi sono messo al lavoro in 5 minuti e il mio centos è già con bash 4.2.46
Felipe

1
C'è un problema fondamentale qui: se si modifica una variabile nel blocco try, non verrà visualizzata all'esterno perché è in esecuzione in una sotto-shell.
Kan Li,

1
@KanLi corretto. Se ti interessa l'output del try / catch puoi semplicemente catturarlo come:my_output=$(try { code...; } catch { code...; })
niieani

Nell'ultima versione, sembra che EXCEPTION_LINE sia stata rinominata in BACKTRACE_LINE github.com/niieani/bash-oo-framework#using-try--catch
Ben Creasy il

19

Puoi usare trap:

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

si traduce in:

(
  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
)

Anche tu vuoi -Ebandiera, penso, quindi la trappola si propaga alle funzioni
Mark K Cowan,

17

Ci sono così tante soluzioni simili che probabilmente funzionano. Di seguito è riportato un modo semplice e efficace per eseguire try / catch, con spiegazione nei commenti.

#!/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

15

bashnon interrompe l'esecuzione in esecuzione nel caso in cui qualcosa rilevi uno stato di errore (a meno che non si imposti il -eflag). I linguaggi di programmazione che offrono lo try/catchfanno al fine di inibire un "salvataggio" a causa di questa situazione speciale (quindi tipicamente chiamata "eccezione").

Nel bash, invece, solo il comando in questione uscirà con un codice di uscita maggiore di 0, che indica quello stato di errore. Ovviamente puoi verificarlo, ma poiché non esiste alcun salvataggio automatico di nulla, un tentativo / cattura non ha senso. Manca solo quel contesto.

Puoi, tuttavia, simulare un salvataggio usando sottotitoli che possono terminare in un punto che decidi:

(
  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

Invece di quello some_conditioncon un ifpuoi anche solo provare un comando, e nel caso fallisca (ha un codice di uscita maggiore di 0), salva:

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

Sfortunatamente, usando questa tecnica sei limitato a 255 diversi codici di uscita (1..255) e non puoi usare oggetti di eccezione decenti.

Se hai bisogno di maggiori informazioni per passare con la tua eccezione simulata, puoi usare lo stdout dei subshells, ma è un po 'complicato e forse un'altra domanda ;-)

Usando il -eflag sopra menzionato sulla shell puoi persino eliminare quella exitdichiarazione esplicita :

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

1
Questa dovrebbe davvero essere la risposta accettata in quanto è la più logica da provare / catturare come puoi ottenere con la shell.
Trento,

13

Come dicono tutti, bash non ha una corretta sintassi try / catch supportata dal linguaggio. È possibile avviare bash con l' -eargomento o utilizzare set -eall'interno dello script per interrompere l'intero processo bash se un comando ha un codice di uscita diverso da zero. (È inoltre possibile set +econsentire temporaneamente comandi non riusciti.)

Pertanto, una tecnica per simulare un blocco try / catch consiste nell'avvio di un processo secondario con cui -eabilitare il lavoro . Quindi, nel processo principale, controlla il codice di ritorno del processo secondario.

Bash supporta stringhe ereditarie, quindi non è necessario scrivere due file separati per gestirlo. Nell'esempio seguente, l'ereditarietà TRY verrà eseguita in un'istanza bash separata, con -eabilitato, quindi il processo secondario si arresterà in modo anomalo se un comando restituisce un codice di uscita diverso da zero. Quindi, nel processo principale, possiamo controllare il codice di ritorno per gestire un blocco catch.

#!/bin/bash

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

Non è un blocco try / catch corretto supportato dalla lingua, ma potrebbe graffiare un prurito simile per te.


6

Tu puoi fare:

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


0

Una cosa molto semplice che uso:

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

1
Dato che il lato destro di ||è dentro (), verrebbe eseguito in una subshell e uscirà, senza causare l'uscita della shell principale. Usa { }invece il raggruppamento.
codeforester,

0

Di seguito è una copia completa dello script semplificato utilizzato nell'altra mia risposta . Oltre al controllo degli errori aggiuntivo, esiste un alias che consente all'utente di modificare il nome di un alias esistente. La sintassi è riportata di seguito. Se il new_aliasparametro viene omesso, l'alias viene rimosso.

ChangeAlias old_alias [new_alias]

Lo script completo è riportato di seguito.

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))"
}

0

Di seguito è riportato un esempio di uno script che implementa try/catch/finallyin 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 exceptiono throwalla fine del tryblocco più vicino . Qui i messaggi vengono rimossi dal FIFO e inseriti in una variabile di matrice. Lo stato viene restituito tramite returne exitcomandi e inserito in una variabile diversa. Per inserire un catchblocco, questo stato non deve essere zero. Altri requisiti per inserire un catchblocco vengono passati come parametri. Se catchviene raggiunta la fine di un blocco, lo stato viene impostato su zero. Se finallyviene 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 trycatchfinallyche contiene un gestore di eccezioni non gestito.

La sintassi per il trycatchfinallycomando è riportata di seguito.

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

L' -copzione aggiunge lo stack di chiamate ai messaggi di eccezione.
L' -dopzione abilita l'output di debug.
L' -eopzione abilita le eccezioni di comando.
L' -hopzione consente all'utente di sostituire il proprio gestore delle eccezioni ai comandi.
L' -kopzione aggiunge lo stack di chiamate all'output di debug.
L' -oopzione sostituisce il file di output predefinito che è /dev/fd/2.
L' -uopzione consente all'utente di sostituire il proprio gestore di eccezioni non gestito.
L' -vopzione consente all'utente di restituire valori tramite l'uso della sostituzione dei comandi.
La fifoè il nome del file FIFO.
La funzione functionviene chiamata trycatchfinallycome sottoprocesso.

Nota: le cdkoopzioni sono state rimosse per semplificare lo script.

La sintassi per il catchcomando è 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.

-esignifica [[ $value == "$string" ]](il valore deve corrispondere ad almeno una stringa nell'elenco)
-nsignifica [[ $value != "$string" ]](il valore non può corrispondere a nessuna delle stringhe nell'elenco)
-osignifica [[ $value != $pattern ]](il valore non può corrispondere a nessuno dei modelli nell'elenco)
-psignifica [[ $value == $pattern ]](il valore ha per corrispondere ad almeno un modello nell'elenco)
-rsignifica [[ $value =~ $regex ]](il valore deve corrispondere ad almeno un'espressione regolare estesa nell'elenco)
-tsignifica [[ ! $value =~ $regex ]](il valore non può corrispondere a nessuna delle espressioni regolari estese nell'elenco)

Lo try/catch/finallyscript è 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 makefifofile contiene lo script descritto in questa risposta . Si presume che il file denominato 4444kkkkknon esista, causando quindi un'eccezione. Il messaggio di errore emesso dal ls 4444kkkkkcomando viene automaticamente eliminato fino a quando non si trova all'interno del catchblocco 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 throwpuò essere creato sostituendo la funzione MyFunction3con 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 throwcomando è 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.