Bash завершение скрипта при ошибке

Инструменты автоматизации и мониторинга удобны тем, что разработчик может взять готовые скрипты, при необходимости адаптировать и использовать в своём проекте. Можно заметить, что в некоторых скриптах используются коды завершения (exit codes), а в других нет. О коде завершения легко забыть, но это очень полезный инструмент. Особенно важно использовать его в скриптах командной строки.

Что такое коды завершения

В Linux и других Unix-подобных операционных системах программы во время завершения могут передавать значение родительскому процессу. Это значение называется кодом завершения или состоянием завершения. В POSIX по соглашению действует стандарт: программа передаёт 0 при успешном исполнении и 1 или большее число при неудачном исполнении.

Почему это важно? Если смотреть на коды завершения в контексте скриптов для командной строки, ответ очевиден. Любой полезный Bash-скрипт неизбежно будет использоваться в других скриптах или его обернут в однострочник Bash. Это особенно актуально при использовании инструментов автоматизации типа SaltStack или инструментов мониторинга типа Nagios. Эти программы исполняют скрипт и проверяют статус завершения, чтобы определить, было ли исполнение успешным.

Кроме того, даже если вы не определяете коды завершения, они всё равно есть в ваших скриптах. Но без корректного определения кодов выхода можно столкнуться с проблемами: ложными сообщениями об успешном исполнении, которые могут повлиять на работу скрипта.

Что происходит, когда коды завершения не определены

В Linux любой код, запущенный в командной строке, имеет код завершения. Если код завершения не определён, Bash-скрипты используют код выхода последней запущенной команды. Чтобы лучше понять суть, обратите внимание на пример.

#!/bin/bash
touch /root/test
echo created file

Этот скрипт запускает команды touch и echo. Если запустить этот скрипт без прав суперпользователя, команда touch не выполнится. В этот момент мы хотели бы получить информацию об ошибке с помощью соответствующего кода завершения. Чтобы проверить код выхода, достаточно ввести в командную строку специальную переменную $?. Она печатает код возврата последней запущенной команды.

$ ./tmp.sh 
touch: cannot touch '/root/test': Permission denied
created file
$ echo $?
0

Как видно, после запуска команды ./tmp.sh получаем код завершения 0. Этот код говорит об успешном выполнении команды, хотя на самом деле команда не выполнилась. Скрипт из примера выше исполняет две команды: touch и echo. Поскольку код завершения не определён, получаем код выхода последней запущенной команды. Это команда echo, которая успешно выполнилась.

Скрипт:

#!/bin/bash
touch /root/test

Если убрать из скрипта команду echo, можно получить код завершения команды touch.

$ ./tmp.sh 
touch: cannot touch '/root/test': Permission denied
$ echo $?
1

Поскольку touch в данном случае — последняя запущенная команда, и она не выполнилась, получаем код возврата 1.

Как использовать коды завершения в Bash-скриптах

Удаление из скрипта команды echo позволило нам получить код завершения. Что делать, если нужно сделать разные действия в случае успешного и неуспешного выполнения команды touch? Речь идёт о печати stdout в случае успеха и stderr в случае неуспеха.

Проверяем коды завершения

Выше мы пользовались специальной переменной $?, чтобы получить код завершения скрипта. Также с помощью этой переменной можно проверить, выполнилась ли команда touch успешно.

#!/bin/bash
touch /root/test 2> /dev/null
if [ $? -eq 0 ]
then
  echo "Successfully created file"
else
  echo "Could not create file" >&2
fi

После рефакторинга скрипта получаем такое поведение:

  • Если команда touch выполняется с кодом 0, скрипт с помощью echo сообщает об успешно созданном файле.
  • Если команда touch выполняется с другим кодом, скрипт сообщает, что не смог создать файл.

Любой код завершения кроме 0 значит неудачную попытку создать файл. Скрипт с помощью echo отправляет сообщение о неудаче в stderr.

Выполнение:

$ ./tmp.sh
Could not create file

Создаём собственный код завершения

Наш скрипт уже сообщает об ошибке, если команда touch выполняется с ошибкой. Но в случае успешного выполнения команды мы всё также получаем код 0.

$ ./tmp.sh
Could not create file
$ echo $?
0

Поскольку скрипт завершился с ошибкой, было бы не очень хорошей идеей передавать код успешного завершения в другую программу, которая использует этот скрипт. Чтобы добавить собственный код завершения, можно воспользоваться командой exit.

#!/bin/bash
touch /root/test 2> /dev/null
if [ $? -eq 0 ]
then
  echo "Successfully created file"
  exit 0
else
  echo "Could not create file" >&2
  exit 1
fi

Теперь в случае успешного выполнения команды touch скрипт с помощью echo сообщает об успехе и завершается с кодом 0. В противном случае скрипт печатает сообщение об ошибке при попытке создать файл и завершается с кодом 1.

Выполнение:

$ ./tmp.sh
Could not create file
$ echo $?
1

Как использовать коды завершения в командной строке

Скрипт уже умеет сообщать пользователям и программам об успешном или неуспешном выполнении. Теперь его можно использовать с другими инструментами администрирования или однострочниками командной строки.

Bash-однострочник:

$ ./tmp.sh && echo "bam" || (sudo ./tmp.sh && echo "bam" || echo "fail")
Could not create file
Successfully created file
bam

В примере выше && используется для обозначения «и», а || для обозначения «или». В данном случае команда выполняет скрипт ./tmp.sh, а затем выполняет echo "bam", если код завершения 0. Если код завершения 1, выполняется следующая команда в круглых скобках. Как видно, в скобках для группировки команд снова используются && и ||.

Скрипт использует коды завершения, чтобы понять, была ли команда успешно выполнена. Если коды завершения используются некорректно, пользователь скрипта может получить неожиданные результаты при неудачном выполнении команды.

Дополнительные коды завершения

Команда exit принимает числа от 0 до 255. В большинстве случаев можно обойтись кодами 0 и 1. Однако есть зарезервированные коды, которые обозначают конкретные ошибки. Список зарезервированных кодов можно посмотреть в документации.

Адаптированный перевод статьи Understanding Exit Codes and how to use them in bash scripts by Benjamin Cane. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

One point missed in the existing answers is show how to inherit the error traps. The bash shell provides one such option for that using set

-E

If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.


Adam Rosenfield’s answer recommendation to use set -e is right in certain cases but it has its own potential pitfalls. See GreyCat’s BashFAQ — 105 — Why doesn’t set -e (or set -o errexit, or trap ERR) do what I expected?

According to the manual, set -e exits

if a simple commandexits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in a if statement, part of an && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return value is being inverted via !«.

which means, set -e does not work under the following simple cases (detailed explanations can be found on the wiki)

  1. Using the arithmetic operator let or $((..)) ( bash 4.1 onwards) to increment a variable value as

    #!/usr/bin/env bash
    set -e
    i=0
    let i++                   # or ((i++)) on bash 4.1 or later
    echo "i is $i" 
    
  2. If the offending command is not part of the last command executed via && or ||. For e.g. the below trap wouldn’t fire when its expected to

    #!/usr/bin/env bash
    set -e
    test -d nosuchdir && echo no dir
    echo survived
    
  3. When used incorrectly in an if statement as, the exit code of the if statement is the exit code of the last executed command. In the example below the last executed command was echo which wouldn’t fire the trap, even though the test -d failed

    #!/usr/bin/env bash
    set -e
    f() { if test -d nosuchdir; then echo no dir; fi; }
    f 
    echo survived
    
  4. When used with command-substitution, they are ignored, unless inherit_errexit is set with bash 4.4

    #!/usr/bin/env bash
    set -e
    foo=$(expr 1-1; true)
    echo survived
    
  5. when you use commands that look like assignments but aren’t, such as export, declare, typeset or local. Here the function call to f will not exit as local has swept the error code that was set previously.

    set -e
    f() { local var=$(somecommand that fails); }        
    g() { local var; var=$(somecommand that fails); }
    
  6. When used in a pipeline, and the offending command is not part of the last command. For e.g. the below command would still go through. One options is to enable pipefail by returning the exit code of the first failed process:

    set -e
    somecommand that fails | cat -
    echo survived
    

The ideal recommendation is to not use set -e and implement an own version of error checking instead. More information on implementing custom error handling on one of my answers to Raise error in a Bash script

When writing Bash scripts, it’s essential to include error handling to ensure that the script exits gracefully in the event of an error. In this article, we’ll cover all the possible techniques for exiting when errors occur in Bash scripts, including the use of exit codes, the “set -e” option, the “trap” command, and command substitution. In this article, we will discuss the various techniques and methods to handle errors and exit gracefully in Bash scripts. Error handling is an essential aspect of writing scripts, as it helps to ensure that the script exits in an appropriate manner, even when an error occurs. Without proper error handling, a script might continue to execute even when an error has occurred, leading to unexpected results or even system crashes.

We will start by discussing the use of exit codes, which are used to indicate the status of the script’s execution. We will then move on to the “set -e” option, which causes the script to exit immediately if any command exits with a non-zero exit code. Next, we will cover the “trap” command, which allows you to specify a command or set of commands to be executed when the script exits, whether it’s due to an error or not. Then, we will look at command substitution, which allows you to capture the output of a command and assign it to a variable. Finally, we will discuss the technique of suppressing error messages.

Exiting when errors occur in Bash scripts is an important aspect of script development, as it helps to ensure that the script stops running and exits cleanly in the event of an error. There are several ways to exit a Bash script when an error occurs, including using the exit command, using the return command, and using the trap command. It is also important to include error handling and error messages in your script to provide useful information to the user in the event of an error.

Exit Codes

Exit codes are used to indicate the status of the script’s execution, with a zero exit code indicating success and non-zero codes indicating an error. One of the simplest ways to exit a Bash script when an error occurs is to use the “exit” command with a non-zero exit code. For example, the following script exits with a code of 1 if the “mkdir” command fails:

Script:

#!/bin/bash
mkdir /tmp/example
if [ $? -ne 0 ]; then
  exit 1
fi

Output:

Exit codes

Another way to check the exit code of a command is by using the $? variable. For example, the following script also exits with a code of 1 if the “mkdir” command fails:

Script:

#!/bin/bash

mkdir /home/lucifer/yash/first

if [ $? -ne 0 ]; then

  echo “Error: Failed to create directory”

  exit 1

fi

Output:

Exit Codes

Set -e Option

Another way to exit a Bash script when an error occurs is to use the “set -e” option. This option causes the script to exit immediately if any command exits with a non-zero exit code. For example, the following script exits immediately if the “mkdir” command fails:

Script:

#!/bin/bash
set -e
mkdir /tmp/example

Output:

Using -e option

Trap Command

The trap command allows you to specify a command or set of commands to be executed when the script exits, whether it’s due to an error or not. For example, the following script uses the trap command to delete the directory if the script exits with a non-zero exit code:

Script:

#!/bin/bash
trap 'rm -rf /tmp/example' EXIT
mkdir /tmp/example
# some commands here
exit 0

Output:

Using trap command

Command Substitution

Another way to handle errors in Bash scripts is by using command substitution. This technique allows you to capture the output of a command and assign it to a variable. For example, the following script captures the output of the “mkdir” command and assigns it to the “result” variable. If the command fails, the script exits with a code of 1:

Script:

#!/bin/bash
result=$(mkdir /tmp/example)
if [ $? -ne 0 ]; then
  echo "Error: $result"
  exit 1
fi

Output:

Command Substitution

In this example, if the mkdir command fails to create the directory, the error message will be assigned to the “result” variable and displayed on the console.

Suppressing Error Messages

It is also possible to suppress the error message generated by a command by adding 2> /dev/null to the command.

Script:

#!/bin/bash
mkdir /tmp/example 2> /dev/null
if [ $? -ne 0 ]; then
  exit 1
fi

Output:

Suppressing Error Messages

In this example, if the mkdir command fails to create the directory, the error message will not be displayed on the console.

Exit When Any Command Fails

One way to exit a Bash script when any command fails is to use the set -e option. This option causes the script to exit immediately if any command returns a non-zero exit status.
For example, the following script will exit if the ls command fails to list the contents of the specified directory:

Script:

#!/bin/bash
set -e
ls /nonexistent_directory
echo "This line will not be executed"

If the directory /nonexistent_directory does not exist, the script will exit with an error message like this:

Output:

Exiting when command fails

And it will not execute the second line.

Another way to exit if any command fails is to use the || operator after each command, for example:

Script:

#!/bin/bash

ls /nonexistent_directory || exit 1

echo “This line will not be executed”This script will also exit with an error message

Output:

Exiting when command fails

And it will not execute the second line.

You can also check the exit status of the last command with $? variable and use the if condition with the exit command to exit if any command fails.

Script:

#!/bin/bash
ls /nonexistent_directory
if [ $? -ne 0 ]
then
  echo "Command failed"
  exit 1
else
  echo "Command Successful"
fi

This script will also exit with an error message:

Output:

Exiting when command fails

And it will not execute the second line.

It is important to note that, when using the set -e option or || operator, the script will exit even if the failure is in a command inside a function or a subshell. To prevent this behavior, you can use set +e before the function or subshell and set -e after it.

Exit Only When Specific Commands Fail

To exit a Bash script only when specific commands fail, you can use the || operator in conjunction with the exit command. The || operator is used to execute a command only if the preceding command fails (returns a non-zero exit status).
For example, the following script will exit if the ls command fails to list the contents of the specified directory, but will continue to execute subsequent commands even if they fail:

Script:

#!/bin/bash
ls /nonexistent_directory || exit 1
echo "This line will be executed"
rm /nonexistent_file || true
echo "This line will also be executed"

If the directory /nonexistent_directory does not exist, the script will exit with an error message like this:

Output:

Exiting when specific command fails

And it will not execute the second and third lines.

It is important to note that, the true command is used to prevent the rm command from causing the script to exit if it fails. Without the true command, the script would exit if the file /nonexistent_file does not exist.

Another way to achieve this is by using an if statement and checking the exit status of the command with $? variable, like:

Script:

#!/bin/bash
ls /nonexistent_directory
if [ $? -ne 0 ]
then
  echo "Command failed"
  exit 1
else
  echo "Command Successful"
fi
echo "This line will be executed"
rm /nonexistent_file
if [ $? -ne 0 ]
then
  echo "Command failed"
else
  echo "Command Successful"
fi

This script will also exit with an error message:

Output:

Exiting when specific command fails

And it will not execute the second and third lines.

It is important to note that, the exit status of the last executed command is stored in the $? variable. And you can use it to check if the command is successful or not.

Conclusion

There are multiple ways to handle errors and exit gracefully in Bash scripts. By using exit codes, the “set -e” option, the “trap” command, command substitution, and suppressing error messages, you can ensure that your script exits in an appropriate manner and provides appropriate error handling for your script. It’s essential to test your script with different inputs and error conditions to ensure that it is handling errors as expected. It’s also important to include clear and descriptive error messages that indicate the cause of the error and provide guidance on how to resolve it. With these techniques, you can write robust and reliable Bash scripts that can handle errors and exit gracefully.

attempt=0
until gksu command; do
  attempt=$((attempt + 1))
  if [ "$attempt" -gt 5 ]; then
    exit 1
  fi
done

exit exits the script unless it’s called in a subshell. If that part of the script is in a subshell, for instance because it’s within (...) or $(...) or part of a pipe-line, then it will only exit that subshell.

In that case, if you want the script to exit in addition to the subshell, then you’ll need to call exit upon that subshell exiting.

For instance, here with 2 nested levels of subshells:

(
  life=hard
  output=$(
    echo blah
    [ "$life" = easy ] || exit 1 # exit subshell
    echo blih not run
  ) || exit # if the subshell exits with a non-zero exit status,
            # exit as well with the same exit status

  echo not run either
) || exit # if the subshell exits with a non-zero exit status,
          # exit as well with the same exit status

It can become trickier if the subshell is part of a pipeline. bash has a special $PIPESTATUS array, similar to zsh‘s $pipestatus one that can help you here:

{
   echo foo
   exit 1
   echo bar
} | wc -c
subshell_ret=${PIPESTATUS[0]}
if [ "$subshell_ret" -ne 0 ]; then
  exit "$subshell_ret"
fi

Написание надежного, без ошибок сценария bash всегда является сложной задачей. Даже если вы написать идеальный сценарий bash, он все равно может не сработать из-за внешних факторов, таких как некорректный ввод или проблемы с сетью.

В оболочке bash нет никакого механизма поглощения исключений, такого как конструкции try/catch. Некоторые ошибки bash могут быть молча проигнорированы, но могут иметь последствия в дальнейшем. Перехват и обработка ошибок в bash

Проверка статуса завершения команды

Всегда рекомендуется проверять статус завершения команды, так как ненулевой статус выхода обычно указывает на ошибку

if ! command; then
    echo "command returned an error"
fi

Другой (более компактный) способ инициировать обработку ошибок на основе статуса выхода — использовать OR:

<command_1> || <command_2>

С помощью оператора OR, <command_2> выполняется тогда и только тогда, когда <command_1> возвращает ненулевой статус выхода.

В качестве второй команды, можно использовать свою Bash функцию обработки ошибок

error_exit()
{
    echo "Error: $1"
    exit 1
}

bad-command || error_exit "Some error"

В Bash имеется встроенная переменная $?, которая сообщает вам статус выхода последней выполненной команды.

Когда вызывается функция bash, $? считывает статус выхода последней команды, вызванной внутри функции. Поскольку некоторые ненулевые коды выхода имеют специальные значения, вы можете обрабатывать их выборочно.

status=$?

case "$status" in
"1") echo "General error";;
"2") echo "Misuse of shell builtins";;
"126") echo "Command invoked cannot execute";;
"128") echo "Invalid argument";;
esac

Выход из сценария при ошибке в Bash

Когда возникает ошибка в сценарии bash, по умолчанию он выводит сообщение об ошибке в stderr, но продолжает выполнение в остальной части сценария. Даже если ввести неправильную команду, это не приведет к завершению работы сценария. Вы просто увидите ошибку «command not found».

Такое поведение оболочки по умолчанию может быть нежелательным для некоторых bash сценариев. Например, если скрипт содержит критический блок кода, в котором не допускаются ошибки, вы хотите, чтобы ваш скрипт немедленно завершал работу при возникновении любой ошибки внутри этого блока . Чтобы активировать это поведение «выход при ошибке» в bash, вы можете использовать команду set следующим образом.

set -e
# некоторый критический блок кода, где ошибка недопустима
set +e

Вызванная с опцией -e, команда set заставляет оболочку bash немедленно завершить работу, если любая последующая команда завершается с ненулевым статусом (вызванным состоянием ошибки). Опция +e возвращает оболочку в режим по умолчанию. set -e эквивалентна set -o errexit. Аналогично, set +e является сокращением команды set +o errexit.

set -e
true | false | true
echo "Это будет напечатано" # "false" внутри конвейера не обнаружено

Если необходимо, чтобы при любом сбое в работе конвейеров также завершался сценарий bash, необходимо добавить опцию -o pipefail.

set -o pipefail -e
true | false | true # "false" внутри конвейера определен правильно
echo "Это не будет напечатано"

Для «защиты» критический блока в сценарии от любого типов ошибок команд или ошибок конвейера, необходимо использовать следующую комбинацию команд set.

set -o pipefail -e
# некоторый критический блок кода, в котором не допускается ошибка или ошибка конвейера
set +o pipefail +e

Понравилась статья? Поделить с друзьями:
  • Bas esp w168 ошибка
  • Barton th 562 boot ошибка
  • Bartender ошибка 6670
  • Bartender ошибка 3704
  • Bartender ошибка 3408