Есть ли в Bash команда TRY CATCH?

351

Я пишу сценарий оболочки, и мне нужно проверить, установлено ли терминальное приложение. Я хочу использовать команду TRY / CATCH, чтобы сделать это, если нет более аккуратного способа.

Ли Проберт
источник
1
Это может помочь, если вы сможете уточнить, какую проблему вы пытаетесь решить. Кажется, вы здесь не совсем новичок, но вы все равно можете посетить Справочный центр и посмотреть справку о том, как задать хороший вопрос.
devnull
Тем не менее, кажется, что высказывание help testможет помочь вам найти решение вашей проблемы.
devnull
2
Блок try / catch / finally - это не команда, это конструкция
Бен
@ LeeProbert: Поскольку у вас нет исключений в bash, мне интересно, что вы хотите поймать. Ближайшая вещь, идущая в направлении исключения, будет сигналом, и большинство (не все) из них вы можете поймать с помощью trapкоманды.
user1934428

Ответы:

563

Есть ли в Bash команда TRY CATCH?

Нет.

Bash не так много предметов роскоши, как можно найти во многих языках программирования.

Нет try/catchв bash; однако, можно добиться аналогичного поведения, используя &&или ||.

Использование ||:

если 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» все равно будет выполняться.
chepner
7
Есть предложение использовать if...elseконструкцию. Означает ли это, что команды bash считаются «правдивыми», если они выполняются успешно, и «ложными», если они терпят неудачу?
Люк Гриффитс
6
Для читателей этой темы: кажется, что set -eэто не обязательно лучший способ сделать что-то; Вот некоторые контраргументы / особые случаи: mywiki.wooledge.org/BashFAQ/105
Люк Дэвис,
2
Могу ли я узнать, как записать исключение? Обычно в Java-коде мы можем использовать system.out.log (e), но как насчет оболочки?
Панадол Чонг
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
Не могли бы вы показать, как импортировать функции try catch в другой пример? (Я предполагаю, что они находятся в отдельных файлах)
kilianc
1
@kilianc: я просто поставил его так: source inc / trycatch.sh.
Матиас Хенце
2
@MathiasHenze Спасибо, чувак, твой код чертовски крут. Но зачем вам ||после catchи до {}блока? Я бы подумал, что это&&
Реми Сан
(поздний ответ для любого, кто найдет это) По существу, случай ошибки if False or run_if_failed()означает, что короткое замыкание ИЛИ попыталось выполнить первое утверждение, которое не вернуло истину, и теперь переходит к следующему утверждению. &&не будет работать, потому что первый оператор ( try) выдал false, что означает, что catchпо правилу тавтологии утверждение не является необходимым false&any equals false. Только не короткое замыкание И / ИЛИ будет выполнять оба.
2
69

Я разработал почти безупречную реализацию try & catch в bash, которая позволяет вам писать такой код:

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

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

Вы можете даже вложить блоки try-catch внутри себя!

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

Код является частью моего bash шаблон / рамки . Это дополнительно расширяет идею try & catch с такими вещами, как обработка ошибок с обратным следом и исключениями (плюс некоторые другие приятные функции).

Вот код, который отвечает только за 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
}

Не стесняйтесь использовать, раскошелиться и внести свой вклад - это на GitHub .

niieani
источник
@ erm3nda Рад это слышать! Я думаю, что я убил несколько ошибок после того, как опубликовал это, так что посмотрите на GitHub для обновлений (вам нужно включить 03_exception.sh и 04_try_catch.sh). Насколько я знаю, текущая версия в значительной степени пуленепробиваема.
niieani
Очень хорошо! Я собираюсь использовать в моем проекте. Я положил на работу через 5 минут, и мои centos уже с Bash 4.2.46
Фелипе
1
Здесь есть фундаментальная проблема: если вы измените переменную в блоке try, она не будет видна снаружи, потому что она работает в под-оболочке.
Кан Ли
1
@KanLi правильно. Если вам my_output=$(try { code...; } catch { code...; })
небезразличен
В последней версии похоже, что EXCEPTION_LINE был переименован в BACKTRACE_LINE github.com/niieani/bash-oo-framework#using-try--catch
Ben Creasy,
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
Kostanos
источник
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с ifвы также можете просто попробовать команду, и в случае , если он не может (имеет Greater выход кода , чем 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), и никакие достойные объекты исключений не могут быть использованы.

Если вам нужно больше информации для передачи с вашим смоделированным исключением, вы можете использовать стандартный вывод подоболочек, но это немного сложнее и, возможно, другой вопрос ;-)

Используя вышеупомянутый -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
Это действительно должен быть принятый ответ, поскольку он наиболее близок к логике try / catch, которую вы можете получить с помощью shell.
Трент
13

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

Итак, один из методов для имитации блока try / catch - запуск подпроцесса для выполнения работы с -eвключенным. Затем в основном процессе проверьте код возврата подпроцесса.

Bash поддерживает строки heredoc, поэтому вам не нужно писать два отдельных файла для этого. В приведенном ниже примере TRY heredoc будет выполняться в отдельном экземпляре bash с -eвключенным, поэтому подпроцесс завершится сбоем, если какая-либо команда вернет ненулевой код завершения. Затем, вернувшись в основной процесс, мы можем проверить код возврата для обработки блока 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

Это не правильный поддерживаемый языком блок try / catch, но он может поцарапать подобный зуд для вас.

Дан Фабулич
источник
6

Ты можешь сделать:

#!/bin/bash
if <command> ; then # TRY
    <do-whatever-you-want>
else # CATCH
    echo 'Exception'
    <do-whatever-you-want>
fi
Дэвидсон Лима
источник
4

И у вас есть ловушки http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html, который отличается от других, но другой метод, который вы можете использовать для этой цели

Эран Бен-Натан
источник
Сигналы действительно связаны очень тонкой нитью с концепцией исключений и try / catch, поскольку они не являются частью обычного потока управления программой. Но можно упомянуть об этом здесь.
Alfe
0

Очень простая вещь, которую я использую:

try() {
    "$@" || (e=$?; echo "$@" > /dev/stderr; exit $e)
}
syberghost
источник
1
Так как правая часть ||находится внутри (), она будет работать в подоболочке и выходить, не вызывая выхода из основной оболочки. { }Вместо этого используйте группировку.
codeforester
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.

Как и другие ответы на этот вопрос, исключения должны быть перехвачены после выхода из подпроцесса.

Примеры сценариев начинаются с создания анонимного fifo, который используется для передачи строковых сообщений от 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Опция добавляет стек вызовов для сообщений исключений. Опция позволяет отладочный вывод. Опция позволяет исключения команд. Опция позволяет пользователю заменить свой собственный обработчик команды исключения. Опция добавляет стек вызовов к выходу отладки. Опция заменяет выходной файл по умолчанию , который . Опция позволяет пользователю заменить свое собственное необработанное обработчик исключений. Опция позволяет пользователю возможности передавать обратно значение , хотя использование команды подстановки. Это имя файла 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
Дэвид Андерсон
источник