Помилка керування в Bash


240

Який ваш улюблений метод обробляти помилки в Bash? Найкращий приклад поводження з помилками, які я знайшов в Інтернеті, написав Вільям Шоттс, молодший за адресою http://www.linuxcommand.org .

Він пропонує використовувати наступну функцію для обробки помилок у Bash:

#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>

PROGNAME=$(basename $0)

function error_exit
{

#   ----------------------------------------------------------------
#   Function for exit due to fatal program error
#       Accepts 1 argument:
#           string containing descriptive error message
#   ---------------------------------------------------------------- 

    echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

Чи є у вас краща процедура обробки помилок, яку ви використовуєте в сценаріях Bash?


1
Дивіться цю детальну відповідь: підвищити помилку в сценарії Bash .
codeforester

1
Дивіться реалізацію входу та керування помилками тут: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Відповіді:


154

Використовуйте пастку!

tempfiles=( )
cleanup() {
  rm -f "${tempfiles[@]}"
}
trap cleanup 0

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

... тоді, коли ви створюєте тимчасовий файл:

temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )

і $temp_fooбуде видалено при виході, а номер поточного рядка буде надрукований. ( set -eце також дасть вам поведінку під час помилки при виході, хоча це має серйозні застереження та послаблює передбачуваність та переносимість коду).

Ви можете дозволити виклику пастки errorдля вас (у такому випадку він використовує код виходу за замовчуванням 1 і не має повідомлення) або зателефонувати йому самостійно та надати явні значення; наприклад:

error ${LINENO} "the foobar failed" 2

вийде зі статусом 2 та надішле явне повідомлення.


4
@draemon змінна капіталізація є навмисною. All-caps є звичайним лише для вбудованих оболонок та змінних середовища - використання малих літер для всього іншого запобігає конфліктам у просторі імен. Дивіться також stackoverflow.com/questions/673055/…
Чарльз Даффі

1
перед тим, як розірвати його знову, протестуйте свої зміни. Конвенції - це хороша річ, але вони є вторинними щодо функціонуючого коду.
Draemon

3
@Draemon, я фактично не згоден. Очевидно, що зламаний код помічається та фіксується. Погані практики, але здебільшого працюючий код живе вічно (і поширюється).
Чарльз Даффі

1
але ти не помітив. Пошкоджений код можна помітити, оскільки функціонування коду є головною проблемою.
Draemon

5
це не зовсім безоплатно ( stackoverflow.com/a/10927223/26334 ), і якщо код уже не сумісний з POSIX, вилучення ключового слова функції не дозволяє більше працювати під POSIX sh, але головна моя думка полягала в тому, що ви ' ve (IMO) знецінив відповідь, послабивши рекомендацію використовувати set -e. Stackoverflow - це не "ваш" код, а найкращі відповіді.
Draemon

123

Це прекрасне рішення. Я просто хотів додати

set -e

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


29
set -eне обійшлося без gotchas: див. mywiki.wooledge.org/BashFAQ/105 для кількох.
Чарльз Даффі

3
@CharlesDuffy, деякі з цих проблем можна подолатиset -o pipefail
варильні панелі

7
@CharlesDuffy Дякую за вказівку на ґетчі; загалом, однак, я все ще думаю, set -eщо це велике співвідношення вигод та витрат.
Бруно Де Фрайн

3
@BrunoDeFraine я використовую set -eсам, але ряд інших постійних членів в irc.freenode.org # bash радять (досить сильно) проти цього. Як мінімум, питання, про які йдеться, слід добре розуміти.
Чарльз Даффі

3
встановити -e -o pipefail -u # і знати, що ти робиш
Сем Уоткінс

78

Читання всіх відповідей на цій сторінці мене дуже надихнуло.

Отже, ось моя підказка:

вміст файлу: lib.trap.sh

lib_name='trap'
lib_version=20121026

stderr_log="/dev/shm/stderr.log"

#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

if test "${g_libs[$lib_name]+_}"; then
    return 0
else
    if test ${#g_libs[@]} == 0; then
        declare -A g_libs
    fi
    g_libs[$lib_name]=$lib_version
fi


#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

exec 2>"$stderr_log"


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function exit_handler ()
{
    local error_code="$?"

    test $error_code == 0 && return;

    #
    # LOCAL VARIABLES:
    # ------------------------------------------------------------------
    #    
    local i=0
    local regex=''
    local mem=''

    local error_file=''
    local error_lineno=''
    local error_message='unknown'

    local lineno=''


    #
    # PRINT THE HEADER:
    # ------------------------------------------------------------------
    #
    # Color the output if it's an interactive terminal
    test -t 1 && tput bold; tput setf 4                                 ## red bold
    echo -e "\n(!) EXIT HANDLER:\n"


    #
    # GETTING LAST ERROR OCCURRED:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    #
    # Read last file from the error log
    # ------------------------------------------------------------------
    #
    if test -f "$stderr_log"
        then
            stderr=$( tail -n 1 "$stderr_log" )
            rm "$stderr_log"
    fi

    #
    # Managing the line to extract information:
    # ------------------------------------------------------------------
    #

    if test -n "$stderr"
        then        
            # Exploding stderr on :
            mem="$IFS"
            local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
            IFS=':'
            local stderr_parts=( $shrunk_stderr )
            IFS="$mem"

            # Storing information on the error
            error_file="${stderr_parts[0]}"
            error_lineno="${stderr_parts[1]}"
            error_message=""

            for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
                do
                    error_message="$error_message "${stderr_parts[$i-1]}": "
            done

            # Removing last ':' (colon character)
            error_message="${error_message%:*}"

            # Trim
            error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
    fi

    #
    # GETTING BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
    _backtrace=$( backtrace 2 )


    #
    # MANAGING THE OUTPUT:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    local lineno=""
    regex='^([a-z]{1,}) ([0-9]{1,})$'

    if [[ $error_lineno =~ $regex ]]

        # The error line was found on the log
        # (e.g. type 'ff' without quotes wherever)
        # --------------------------------------------------------------
        then
            local row="${BASH_REMATCH[1]}"
            lineno="${BASH_REMATCH[2]}"

            echo -e "FILE:\t\t${error_file}"
            echo -e "${row^^}:\t\t${lineno}\n"

            echo -e "ERROR CODE:\t${error_code}"             
            test -t 1 && tput setf 6                                    ## white yellow
            echo -e "ERROR MESSAGE:\n$error_message"


        else
            regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
            if [[ "$_backtrace" =~ $regex ]]

                # The file was found on the log but not the error line
                # (could not reproduce this case so far)
                # ------------------------------------------------------
                then
                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    echo -e "ERROR MESSAGE:\n${stderr}"

                # Neither the error line nor the error file was found on the log
                # (e.g. type 'cp ffd fdf' without quotes wherever)
                # ------------------------------------------------------
                else
                    #
                    # The error file is the first on backtrace list:

                    # Exploding backtrace on newlines
                    mem=$IFS
                    IFS='
                    '
                    #
                    # Substring: I keep only the carriage return
                    # (others needed only for tabbing purpose)
                    IFS=${IFS:0:1}
                    local lines=( $_backtrace )

                    IFS=$mem

                    error_file=""

                    if test -n "${lines[1]}"
                        then
                            array=( ${lines[1]} )

                            for (( i=2; i<${#array[@]}; i++ ))
                                do
                                    error_file="$error_file ${array[$i]}"
                            done

                            # Trim
                            error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
                    fi

                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    if test -n "${stderr}"
                        then
                            echo -e "ERROR MESSAGE:\n${stderr}"
                        else
                            echo -e "ERROR MESSAGE:\n${error_message}"
                    fi
            fi
    fi

    #
    # PRINTING THE BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 7                                            ## white bold
    echo -e "\n$_backtrace\n"

    #
    # EXITING:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 4                                            ## red bold
    echo "Exiting!"

    test -t 1 && tput sgr0 # Reset terminal

    exit "$error_code"
}
trap exit_handler EXIT                                                  # ! ! ! TRAP EXIT ! ! !
trap exit ERR                                                           # ! ! ! TRAP ERR ! ! !


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function backtrace
{
    local _start_from_=0

    local params=( "$@" )
    if (( "${#params[@]}" >= "1" ))
        then
            _start_from_="$1"
    fi

    local i=0
    local first=false
    while caller $i > /dev/null
    do
        if test -n "$_start_from_" && (( "$i" + 1   >= "$_start_from_" ))
            then
                if test "$first" == false
                    then
                        echo "BACKTRACE IS:"
                        first=true
                fi
                caller $i
        fi
        let "i=i+1"
    done
}

return 0



Приклад використання:
вміст файлу: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

echo "doing something wrong now .."
echo "$foo"

exit 0


Запуск:

bash trap-test.sh

Вихід:

doing something wrong now ..

(!) EXIT HANDLER:

FILE:       trap-test.sh
LINE:       6

ERROR CODE: 1
ERROR MESSAGE:
foo:   unassigned variable

BACKTRACE IS:
1 main trap-test.sh

Exiting!


Як видно із скріншоту нижче, вихід виводиться кольоровим, а повідомлення про помилку надходить на використаній мові.

введіть тут опис зображення


3
ця річ приголомшлива. Ви повинні створити для неї проект github, щоб люди могли легко вдосконалитись та зробити свій внесок. Я поєднав його з log4bash і разом це створює потужне середовище для створення хороших скриптів bash.
Домінік Дорн

1
FYI - test ${#g_libs[@]} == 0не сумісний з POSIX (тест POSIX підтримує =порівняння рядків або -eqчислові порівняння, але ні ==, не кажучи вже про відсутність масивів в POSIX), і якщо ви не намагаєтесь бути сумісним з POSIX, чому в Ви використовуєте світ testвзагалі, а не математичний контекст? (( ${#g_libs[@]} == 0 ))зрештою, легше читати.
Чарльз Даффі

2
@Luca - це справді чудово! Ваша картина надихнула мене на створення власної реалізації цього, що робить це ще на кілька кроків далі. Я розмістив це у своїй відповіді нижче .
niieani

3
Бравісімо !! Це відмінний спосіб налагодження сценарію. Grazie mille Єдине, що я додав, це перевірка на OS X так: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy

1
Трохи безсоромний самовідвід, але ми взяли цей фрагмент, очистили його, додали більше функцій, покращили формат виводу та зробили його більш сумісним з POSIX (працює як на Linux, так і на OSX). Він опублікований як частина Privex ShellCore на Github: github.com/Privex/shell-core
Someguy123

22

Еквівалентною альтернативою "set -e" є

set -o errexit

Це робить значення прапора дещо чіткішим, ніж просто "-е".

Випадкове додавання: щоб тимчасово відключити прапор і повернутися до типового (продовження виконання незалежно від кодів виходу), просто використовуйте

set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e

Це виключає правильне поводження з помилками, згадане в інших відповідях, але є швидким та ефективним (як і баш).


1
використання $(foo)на голій лінії, а не простоfoo , як правило , не те. Навіщо рекламувати його, подаючи його як приклад?
Чарльз Даффі

20

Натхнений ідеями, представленими тут, я розробив читабельний та зручний спосіб вирішення помилок у скриптах bash в моєму проекті bash kotplate. .

Просто постачає бібліотеку, ви отримаєте наступне з коробки (тобто він буде зупинити виконання на будь-яку помилку, як при використанні set -eзавдяки trapна ERRдеякі Баш-фу ):

керування помилками bash-oo-frame

Є деякі додаткові функції, які допомагають впоратися з помилками, наприклад, спроба лову або кидка ключове слово , що дозволяє перервати виконання в точці, щоб побачити відставання. Плюс до цього, якщо термінал підтримує його, він випльовує смайли Emojis, забарвлює частини виводу для великої читабельності та підкреслює метод, що спричинив виняток у контексті рядка коду.

Недоліком є ​​- це не портативно - код працює лише в bash, ймовірно,> = 4 (але я думаю, що його можна перенести з деяким зусиллям, щоб переграти 3).

Код розділений на декілька файлів для кращого керування, але мене надихнула ідея зворотного зв'язку з відповіді вище Луки Борріоне .

Щоб прочитати більше або подивитися джерело, див. GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw


Це всередині проекту Bash Object Oriented Framework . ... На щастя, у нього лише 7,4 К LOC (за даними GLOC ). OOP - Об'єктно-орієнтований біль?
ingyhere

@ingyее дуже модульне (і зручне для видалення), тому ви можете використовувати лише частину винятків, якщо саме так ви і прийшли;)
niieani

11

Я вважаю за краще щось дійсно легко зателефонувати. Тому я використовую те, що виглядає трохи складно, але просте у використанні. Зазвичай я просто копіюю і вставляю код нижче в свої сценарії. Пояснення слід за кодом.

#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
    echo
    echo "$@"
    exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM

#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'

Зазвичай я ставлю виклик функції очищення поруч із функцією error_exit, але це змінюється від сценарію до сценарію, тому я його покинув. Пастки вловлюють загальні закінчуючі сигнали і стежать, щоб все очистилося. Псевдонім - це те, що робить справжню магію. Мені подобається перевіряти все на відмову. Тож взагалі я називаю програми "якщо!" заява типу. Віднімаючи 1 від номера рядка, псевдонім підкаже мені, де стався збій. Це також мертвий простий дзвінок, і це майже ідіотський доказ. Нижче наводиться приклад (просто замініть / bin / false на все, що ви збираєтесь зателефонувати).

#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
    die "Who knew false is false."
fi

2
Чи можете ви поширитись на твердження "Ми мусимо прямо дозволяти псевдоніми" ? Я б хвилювався, що може призвести до несподіваної поведінки. Чи є спосіб досягти того ж, що має менший вплив?
вибух

Мені це не потрібно $LINENO - 1. Покажіть правильно без нього.
киб

Коротший приклад використання в bash і zshfalse || die "hello death"
kyb

6

Ще один розгляд - вихідний код для повернення. Просто " 1" є досить стандартним, хоча є кілька зарезервованих кодів виходу, які використовує сам bash , і ця сама сторінка стверджує, що визначені користувачем коди повинні бути в діапазоні 64-113, щоб відповідати стандартам C / C ++.

Ви також можете розглянути бітовий векторний підхід, який mountвикористовує для своїх вихідних кодів:

 0  success
 1  incorrect invocation or permissions
 2  system error (out of memory, cannot fork, no more loop devices)
 4  internal mount bug or missing nfs support in mount
 8  user interrupt
16  problems writing or locking /etc/mtab
32  mount failure
64  some mount succeeded

OR-введення кодів разом дозволяє вашому сценарію надсилати декілька одночасних помилок.


4

Я використовую наступний код пастки, він також дозволяє відстежувати помилки через труби та команди 'time'

#!/bin/bash
set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
function error() {
    JOB="$0"              # job name
    LASTLINE="$1"         # line of error occurrence
    LASTERR="$2"          # error code
    echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
    exit 1
}
trap 'error ${LINENO} ${?}' ERR

5
functionКлючове слово безпричинно POSIX-несумісне. Подумайте зробити свою декларацію просто error() {, не functionперед цим.
Чарльз Даффі

5
${$?}слід просто бути $?або ${?}якщо ви наполягаєте на використанні непотрібних брекетів; внутрішня $неправильна.
Чарльз Даффі

3
@CharlesDuffy на даний момент, POSIX безкоштовно несумісний з GNU / Linux (все-таки, я вважаю, що ви хочете)
Croad Langshan

3

Я звик

die() {
        echo $1
        kill $$
}

перед; я думаю, тому що "вихід" чомусь не був для мене. Наведені вище значення за замовчуванням здаються гарною ідеєю.


Краще надіслати повідомлення про помилку на STDERR, ні?
анкостіс

3

Це вже деякий час служило мені. Він друкує повідомлення про помилки або попереджувальні повідомлення червоним кольором, один рядок на параметр, і дозволяє необов'язковий код виходу.

# Custom errors
EX_UNKNOWN=1

warning()
{
    # Output warning messages
    # Color the output red if it's an interactive terminal
    # @param $1...: Messages

    test -t 1 && tput setf 4

    printf '%s\n' "$@" >&2

    test -t 1 && tput sgr0 # Reset terminal
    true
}

error()
{
    # Output error messages with optional exit code
    # @param $1...: Messages
    # @param $N: Exit code (optional)

    messages=( "$@" )

    # If the last parameter is a number, it's not part of the messages
    last_parameter="${messages[@]: -1}"
    if [[ "$last_parameter" =~ ^[0-9]*$ ]]
    then
        exit_code=$last_parameter
        unset messages[$((${#messages[@]} - 1))]
    fi

    warning "${messages[@]}"

    exit ${exit_code:-$EX_UNKNOWN}
}

3

Не впевнений, чи буде це корисним для вас, але я змінив деякі запропоновані тут функції, щоб включити до нього перевірку на помилку (код виходу з попередньої команди). Під час кожного "перевірки" я також передаю як параметр "повідомлення" про те, що помилка в цілях реєстрації.

#!/bin/bash

error_exit()
{
    if [ "$?" != "0" ]; then
        log.sh "$1"
        exit 1
    fi
}

Тепер, щоб викликати його в межах одного сценарію (або в іншому, якщо я використовую export -f error_exit), я просто записую ім'я функції і передаю повідомлення як параметр, як це:

#!/bin/bash

cd /home/myuser/afolder
error_exit "Unable to switch to folder"

rm *
error_exit "Unable to delete all files"

Використовуючи це, я зміг створити дійсно надійний файл bash для якогось автоматизованого процесу, і він зупиниться у випадку помилок і сповістить мене ( log.shзробимо це)


2
Подумайте про використання синтаксису POSIX для визначення функцій - немає functionключового слова, просто error_exit() {.
Чарльз Даффі

2
є причина, чому ви не просто так робите cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
П’єр-Олів'є Варес

@ Pierre-OlivierVares Немає конкретних причин щодо використання || Це був лише уривок існуючого коду, і я щойно додав рядки "поводження з помилками" після кожного, що стосується рядка. Деякі дуже довгі, і просто чіткіше було розмістити його на окремій (негайній) лінії
Нельсон Родрігес

Схоже, чистий розчин, однак, перевірка оболонки скаржиться: github.com/koalaman/shellcheck/wiki/SC2181
mhulse

1

Цей трюк корисний для відсутніх команд чи функцій. Ім'я відсутньої функції (або виконується) буде передано у $ _

function handle_error {
    status=$?
    last_call=$1

    # 127 is 'command not found'
    (( status != 127 )) && return

    echo "you tried to call $last_call"
    return
}

# Trap errors.
trap 'handle_error "$_"' ERR

Не $_буде доступним у функції такий самий, як $?? Я не впевнений, що є якась причина використовувати одне у функції, а не інше.
ingyhere

1

Ця функція останнім часом мені добре служить:

action () {
    # Test if the first parameter is non-zero
    # and return straight away if so
    if test $1 -ne 0
    then
        return $1
    fi

    # Discard the control parameter
    # and execute the rest
    shift 1
    "$@"
    local status=$?

    # Test the exit status of the command run
    # and display an error message on failure
    if test ${status} -ne 0
    then
        echo Command \""$@"\" failed >&2
    fi

    return ${status}
}

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

command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...

Стає таким:

action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...

<<<Error-handling code here>>>

Якщо будь-яка з команд виходить з ладу, код помилки просто передається до кінця блоку. Мені здається корисним, коли ви не хочете, щоб наступні команди виконувались, якщо попередній не вдався, але ви також не хочете, щоб сценарій вийшов відразу (наприклад, всередині циклу).


0

Використання пастки - це не завжди варіант. Наприклад, якщо ви пишете якусь функцію повторного використання, яка потребує обробки помилок, і яку можна викликати з будь-якого сценарію (після отримання файлу допоміжними функціями), ця функція не може припускати нічого про час виходу зовнішнього скрипту, що робить використання пасток дуже важким. Ще одним недоліком використання пасток є погана компостування, оскільки ви ризикуєте перезаписати попередню пастку, яка може бути встановлена ​​раніше у ланцюжку абонентів.

Існує невелика хитрість, яку можна використовувати для правильного поводження з помилками без пасток. Як ви вже можете дізнатися з інших відповідей, set -eце не працює всередині команд, якщо ви використовуєте ||оператор після них, навіть якщо ви запускаєте їх у підшалі; наприклад, це не працює:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Але ||оператор потрібен для запобігання поверненню із зовнішньої функції перед очищенням. Хитрість полягає в тому, щоб запустити внутрішню команду у фоновому режимі, а потім негайно дочекатися її. waitВбудований повертають код завершення внутрішньої команди, і тепер ви використовуєте ||після того wait, а не внутрішньою функції, тому set -eпрацює належним чином всередині останній:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Ось загальна функція, яка ґрунтується на цій ідеї. Він повинен працювати у всіх сумісних з POSIX оболонках, якщо ви вилучите localключові слова, тобто замініть всі local x=yпросто x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Приклад використання:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Запуск прикладу:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

Єдине, про що потрібно знати, використовуючи цей метод, - це те, що всі модифікації змінних Shell, виконані з команди, яку ви передаєте, runне поширюватимуться на функцію виклику, оскільки команда працює в підзарядці.

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