Чи є команда TRY CATCH у Bash


349

Я пишу сценарій оболонки і потрібно перевірити, чи встановлено додаток терміналу. Я хочу використовувати для цього команду TRY / CATCH, якщо немає більш акуратного способу.


1
Це може допомогти, якщо ви могли б розібратися, яку проблему ви намагаєтеся вирішити. Здається, ви тут не зовсім новачок, але все-таки можете завітати в Довідковий центр і дізнатися довідку про те, як задати гарне запитання.
devnull

Зважаючи на це, схоже, що приказка help testможе допомогти вам знайти рішення вашої проблеми.
devnull

2
try / catch / нарешті блок - це не команда, це конструкція
Бен


@LeeProbert: Оскільки у вас немає винятків у баші, мені цікаво, що ви хочете зловити. Найближчим, що йде в напрямку винятку, буде сигнал, і більшість (не всі) з них ви можете зловити за допомогою trapкоманди.
користувач1934428

Відповіді:


563

Чи є команда TRY CATCH у Bash?

Немає.

Bash не має такої кількості розкошів, як це можна знайти у багатьох мовах програмування.

Немає try/catchв баші; проте можна досягти подібної поведінки, використовуючи &&або ||.

Використання ||:

якщо command1невдача, то command2працює наступним чином

command1 || command2

Аналогічно, використання &&, command2буде запущено, якщо command1буде успішним

Найближчим наближенням try/catchє наступне

{ # try

    command1 &&
    #save your output

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

Також bash містить деякі механізми обробки помилок

set -e

він зупиняє ваш сценарій, якщо будь-яка проста команда не працює.

А також чому б і ні if...else. Це ваш найкращий друг.


18
З цим вам потрібно подбати про те, щоб код #save your outputне вийшов з ладу, інакше блок "catch" все одно буде виконуватися.
чепнер

7
Є пропозиція використовувати if...elseконструкцію. Чи означає це, що команди bash вирішуються як "truthy", якщо вони запускаються успішно, і "false", якщо вони не спрацьовують?
Люк Гріффітс

6
Для читачів цієї теми: здається, що set -eце не обов'язково найкращий спосіб робити речі; ось кілька контр-аргументів / особливих випадків: mywiki.wooledge.org/BashFAQ/105
Люк Девіс

2
Чи можу я знати, як записати виняток? Зазвичай у коді Java ми можемо використовувати system.out.log (e), а як щодо оболонки?
Panadol Chong

112

На основі деяких відповідей, які я знайшов тут, я створив собі невеликий файл-помічник для джерела своїх проектів:

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
}

ось приклад, як це виглядає у використанні:

#!/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
Чи можете ви показати, як ви імпортуєте функції пробування в інший приклад? (Я припускаю, що вони знаходяться в окремих файлах)
kilianc

1
@kilianc: Я просто його джерелом на зразок: source inc / trycatch.sh.
Mathias Henze

2
@MathiasHenze Спасибі людино, ваш код чортово крутий. Але навіщо вам потрібно ||після catchі перед {}блоком? Я б подумав, що це буде&&
Ремі Сан

(пізня відповідь для тих, хто це виявить) По суті, випадок помилки if False or run_if_failed()означає, що коротке замикання АБО спробувало перше твердження, яке не повернуло істину, і тепер переходить до наступного твердження. &&не буде працювати, оскільки перше твердження ( try) дало помилкове значення, а це означає, що catchтвердження не потрібно за правилом тавтології false&any equals false. Тільки не коротке замикання І / АБО виконає обидва.
ldmtwo

69

Я розробив майже бездоганну реалізацію спробувати і зловити в bash, що дозволяє писати код на зразок:

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

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

Ви навіть можете вкласти гнучкі блоки всередині себе!

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

Код є частиною моєї башти / рамки . Це також розширює ідею спробувати помилки з такими речами, як обробка помилок із зворотним зв'язком та винятками (плюс деякі інші приємні функції).

Ось код, який відповідає лише за спробу і ловлю:

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
}

Не соромтеся використовувати, роздрібнюватися та сприяти - це на GitHub .


@ erm3nda Радий почути це! Я думаю, що я вбив кілька помилок після того, як я опублікував це, тому подивіться на GitHub для оновлень (вам потрібно буде включити 03_exception.sh та 04_try_catch.sh). На сьогоднішній день, наскільки я знаю, це версія набагато захищена від куль.
niieani

Дуже хороший! Я збираюся використовувати в своєму проекті. Я почав працювати за 5 хвилин, і мої центри вже з басом 4.2.46
Феліпе

1
Тут є принципова проблема: якщо ви зміните змінну в блоці спробу, вона не буде видно зовні, оскільки вона працює в підколонці.
Кан Лі

1
@KanLi правильно. Якщо ви дбаєте про вихід спробу / лову, ви можете просто зафіксувати його так:my_output=$(try { code...; } catch { code...; })
niieani

В останній версії схоже, що EXCEPTION_LINE було перейменовано на BACKTRACE_LINE github.com/niieani/bash-oo-framework#using-try--catch
Бен

19

Ви можете використовувати trap:

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

перекладається на:

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

Ви також хочете, щоб -Eпрапор я думаю, тому пастка поширюється на функції
Марк К Коуан

17

Є так багато подібних рішень, які, ймовірно, спрацьовують. Нижче наводиться простий і робочий спосіб здійснити спробу / ловити, з поясненнями в коментарях.

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

bashне скасовує запущене виконання у випадку, якщо щось виявить стан помилки (якщо ви не встановите -eпрапор). Мови програмування, які пропонують, try/catchроблять це для того, щоб гальмувати "випадання" через цю особливу ситуацію (тому зазвичай називають "винятком").

В bash, натомість, тільки команда в питанні вийде з кодом виходу Великого ніж 0, що вказує на стан помилки. Ви можете перевірити це, звичайно, але оскільки немає автоматичного врятування нічого, спробувати / улов не має сенсу. Просто не вистачає цього контексту.

Однак ви можете імітувати випадок, використовуючи додаткові оболонки, які можуть закінчитися в обраній вами точці:

(
  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

Замість цього some_conditionз an ifви також можете просто спробувати команду, і якщо вона не вдасться (має вихідний код більше 0), виконайте:

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

На жаль, за допомогою цієї методики ви можете обмежитись 255 різними кодами виходу (1..255), і жодні гідні об'єкти виключень не можна використовувати.

Якщо вам потрібна додаткова інформація, а також ваш модельований виняток, ви можете використовувати stdout допоміжних оболонок, але це трохи складніше і, можливо, інше питання ;-)

Використовуючи вищезгаданий -eпрапор до оболонки, ви навіть можете викреслити це явне exitтвердження:

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

1
Це дійсно має бути прийнятою відповіддю, оскільки це найближче спробувати ловити логіку, як можна отримати з оболонкою.
Трент

13

Як всі кажуть, bash не має належного мовного синтаксису try / catch. Ви можете запустити bash з -eаргументом або використовувати set -eвсередині скрипту, щоб перервати весь процес bash, якщо будь-яка команда має ненульовий код виходу. (Ви також set +eможете тимчасово дозволити збій команди.)

Отже, одна з методик моделювання блоку спробу / лову - запустити підпроцес, щоб виконати роботу з -eувімкненою функцією. Потім у головному процесі перевірте код повернення підпроцесу.

Bash підтримує рядки heredoc, тому вам не потрібно писати два окремі файли, щоб впоратися з цим. У наведеному нижче прикладі TRY heredoc запуститься в окремому екземплярі bash, з -eувімкненим, тому підпроцес завершиться збоєм, якщо будь-яка команда поверне ненульовий код виходу. Потім, повертаючись до основного процесу, ми можемо перевірити код повернення для обробки блоку лову.

#!/bin/bash

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

Це не належний мовний блок "try / catch", але він може подряпати подібний свербіж.



4

І у вас є пастки http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html, що не те саме, але інша техніка, яку ви можете використовувати для цього


Сигнали насправді пов'язані лише дуже тонкою ниткою з поняттям винятків та спроби / лову, оскільки вони не є частиною нормального керуючого потоку програми. Але добре це згадати тут.
Альфе

0

Я використовую дуже просту річ:

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

1
Оскільки права сторона ||знаходиться в (), вона буде працювати в нижній частині корпусу та виходити, не викликаючи вихід основної оболонки. Використовуйте { }натомість групування.
кодовий ліс

0

Нижче наведена повна копія спрощеного сценарію, що використовується в іншій моїй відповіді . Крім додаткової перевірки помилок, існує псевдонім, який дозволяє користувачеві змінити ім’я існуючого псевдоніма. Синтаксис подано нижче. Якщо new_aliasпараметр опущено, то псевдонім видаляється.

ChangeAlias old_alias [new_alias]

Повний сценарій подано нижче.

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

Нижче наводиться приклад сценарію, який реалізується try/catch/finallyв bash.

Як і інші відповіді на це питання, винятки повинні бути вилучені після виходу з підпроцесу.

Приклади скриптів починаються зі створення анонімного файлу, який використовується для передачі рядкових повідомлень від command exceptionабо throwдо кінця найближчого tryблоку. Тут повідомлення видаляються з файлу fifo та розміщуються у змінній масиві. Статус повертається через returnта exitкоманд і поміщені в інший змінної. Для введення catchблоку цей статус не повинен дорівнювати нулю. Інші вимоги до введення catchблоку передаються як параметри. Якщо кінець catchблоку досягнуто, то стан встановлюється на нуль. Якщо кінець finallyблоку досягнуто, а статус все ще є ненульовим, виконується неявний кидок, що містить повідомлення та статус. Сценарій вимагає виклику функції, trycatchfinallyяка містить необроблений обробник винятків.

Синтаксис trycatchfinallyкоманди наведено нижче.

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

-cОпція додає стек викликів для повідомлень винятків. Опція дозволяє зневаджувальної. Опція дозволяє виключення команд. Ця опція дозволяє користувачеві замінити власний обробник винятків команд. Опція додає стек викликів до виходу налагодження. Опція замінює вихідний файл за умовчанням , який . Ця опція дозволяє користувачеві замінити власний оброблений обробник винятків. Ця опція дозволяє користувачеві можливість передавати зворотні значення за допомогою використання Command Substitution. Ім'я файлу fifo. Функція викликається як підпроцес.
-d
-e
-h
-k
-o/dev/fd/2
-u
-v
fifo
functiontrycatchfinally

Примітка. cdkoПараметри було видалено для спрощення сценарію.

Синтаксис catchкоманди наведено нижче.

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

Параметри визначені нижче. Значення першого списку - це статус. Підрядними значеннями є повідомлення. Якщо повідомлень більше, ніж списків, інші повідомлення ігноруються.

-eозначає [[ $value == "$string" ]](значення має відповідати щонайменше одному рядку в списку)
-nозначає [[ $value != "$string" ]](значення не може відповідати жодному з рядків у списку)
-oозначає [[ $value != $pattern ]](значення не може відповідати жодному з шаблонів у списку)
-pозначає [[ $value == $pattern ]](значення має відповідати щонайменше одному шаблону у списку)
-rозначає [[ $value =~ $regex ]](значення має відповідати щонайменше одному розширеному регулярному виразу у списку)
-tозначає [[ ! $value =~ $regex ]](значення не може відповідати жодному з розширених регулярних виразів у списку)

try/catch/finallyСценарій наводиться нижче. Для спрощення сценарію для цієї відповіді більшість перевірок помилок було видалено. Це зменшило розмір на 64%. Повну копію цього сценарію можна знайти в іншій моїй відповіді .

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

Нижче наведено приклад, який передбачає, що вищезазначений скрипт зберігається у файлі з назвою simple. makefifoФайл містить скрипт , описаний в цій відповіді . Зроблено припущення, що названого файлу 4444kkkkkне існує, тому викликає виняток. Повідомлення про помилку, що виводиться з ls 4444kkkkkкоманди, автоматично придушується, поки всередині не буде відповідного catchблоку.

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

Вищеописаний сценарій був протестований за допомогою GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). Результат із запуску цього сценарію показаний нижче.

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

Інший приклад, який використовує a, throwможна створити, замінивши функцію MyFunction3скриптом, показаним нижче.

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

Синтаксис throwкоманди наведено нижче. Якщо ніяких параметрів немає, то замість цього використовуються стан та повідомлення, що зберігаються у змінних.

throw [status] [message ...]

Висновок від виконання модифікованого сценарію наведено нижче.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.