Коды ошибок bash

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

Инструменты автоматизации и мониторинга удобны тем, что разработчик может взять готовые скрипты, при необходимости адаптировать и использовать в своём проекте. Можно заметить, что в некоторых скриптах используются коды завершения (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. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

���������� C. ���� ����������, �������
���������������� �����

������� C-1. «�����������������» ����
����������

��� ���������� ����� ������ ����������
1 ������������� ������ let «var1 = 1/0» ��������� ������, ����� ��� «������� �� ����»
��.
2 �������� ������������ � Bash — ��������
������������� ���������� ������
����������� �������� �����, ������ ���
���������� ������������ ������ 1
126 ���������� ������� �� ����� ����
���������
��������� ��-�� ������� � ������� �������
��� ����� ������ �� ���������� ������������� ����
127 «������� �� �������» �������� ������� ���� � ����������
��������� $PATH, ���� � �������� ����������
����� �������
128 �������� �������� ������� exit exit 3.14159 ������� exit ����� ��������� ������
������������� ��������, � ��������� 0 — 255
128+n ��������� ������ �� ������� «n» kill -9 $PPID �������� $? ������ 137
(128 + 9)
130 ���������� �� Control-C Control-C — ��� ����� �� ������� 2, (130 =
128 + 2, ��. ����)
255* ��� ���������� ��� �����������
���������
exit -1 exit ����� ��������� ������
������������� ��������, � ��������� 0 — 255

�������� ���� �������, ���� ���������� 1 — 2, 126 — 165 � 255
[1] ����� ���������������� ��������,
������� ��� ������� �������� ������������ ���� ����� ��� �����
����. ���������� �������� � ����� �������� exit 127, ����� �������� �
�������������� ��� ������ ������ � �������� (������������� �� ��
�������� ������ «������� �� �������»? ��� ���
��������������� ������������� ��� ����������?). � �����������
�������, ������������ ��������� exit 1, � �������� ������� �� ������.
��� ��� ��� ���������� 1 ������������� �����
«�����» ������, �� � ������ ������ ������ �������� �
����� ���� ���������������, ���� � �� ��������������� —
����.

�� ��� ��������������� ������� ����������������� ����
���������� (��. /usr/include/sysexits.h), �� ���
�������������� ������������� ��� �������������, ������� �� ������
C � C++. ����� ��������� ���������� ���������� ���� ����������,
������������ �������������, ���������� 64 — 113 (�, ���� �����
���������� — 0, ��� ����������� ��������� ����������), �
������������ �� ���������� C/C++. ��� ������� �� ����� ������
����� �������.

��� ��������, ����������� � ������� ���������, ��������� �
������������ � ���� ����������, �� ����������� �������, �����
���������� ���������� ��������������, �������� � ������ 9-2.

Note

��������� � ���������� $?, �� ���������
������, ����� ���������� ������ ��������, ���� ���������,
� ������������ � ��������, ����������� ����, �� ������
��� Bash ��� sh. ��� ����������� csh ���
tcsh �������� ����� �
��������� ������� ����������.

The exit status is a numeric value that is returned by a program to the calling program or shell. In C programs, this is represented by the return value of the main() function or the value you give to exit(3). The only part of the number that matters are the least significant 8 bits, which means there are only values from 0 to 255.

In the shell, every operation generates an exit status (return status), even if no program is called. An example for such an operation is a redirection.

The parameter to the

builtin commands serve the purpose of giving the exit status to the calling component.

This — and only this — makes it possible to determinate the success or failure of an operation. For scripting, always set exit codes.

The code is a number between 0 and 255, where the part from 126 to 255 is reserved to be used by the Bash shell directly or for special purposes, like reporting a termination by a signal:

Code Description
0 success
1-255 failure (in general)
126 the requested command (file) can’t be executed (but was found)
127 command (file) not found
128 according to ABS it’s used to report an invalid argument to the exit builtin, but I wasn’t able to verify that in the source code of Bash (see code 255)
128 + N the shell was terminated by the signal N (also used like this by various other programs)
255 wrong argument to the exit builtin (see code 128)

The lower codes 0 to 125 are not reserved and may be used for whatever the program likes to report. A value of 0 means successful termination, a value not 0 means unsuccessful termination. This behavior (== 0, != 0) is also what Bash reacts on in some code flow control statements like if or while.

Tables of shell behavior involving non-portable side-effects or common bugs with exit statuses. Note heirloom doesn’t support pipeline negation (! pipeline).

test bash
4.2.45
bash
(POSIX)
zsh 5.0.2
(emulate ksh)
ksh93
93v- 2013-03-18
mksh
R44 2013/02/24
posh
0.11
dash
0.5.7.3
busybox
1.2.1
heirloom
050706
:; : `false` `echo $? >&2`
1 1 1 1 0 0 0 0 1
false; eval; echo $?
0 0 0 0 0 1 0 1 0
x=`false` eval echo $?
1 1 1 1 0 0 0 0 1
eval echo $? <&0`false`
1 1 1 1 0 0 0 0 1
while :; do ! break; done; echo $?
1 1 1 1 0 0 1 1
discussion

false; : | echo $?
1 1 1 0 1 1 1 1 0
(exit 2); for x in "`exit 3`"; do echo $?; done
3 3 3 3 2 2 0 0 3

Measuring side-effects during the function call, during return, and transparency of the return builtin.

test bash bash
(POSIX)
zsh
(emulate ksh)
ksh93 mksh posh dash busybox heirloom
f() { echo $?; }; :; f `false`
1 1 1 1 0 0 0 0 1
f() { return; }; false; f; echo $?
1 1 1 0 1 1 1 1 1
f() { return $?; }; false; f; echo $?
1 1 1 1 1 1 1 1 1
f() { ! return; }; f; echo $?
0 0 1 0 0 0 1 1
f() { ! return; }; false; f; echo $?
1 1 0 0 1 1 0 0
f() { return; }; x=`false` f; echo $?
1 1 1 1 0 0 0 0 0
f() { return; }; f <&0`false`; echo $?
1 1 1 1 0 0 0 0 1
f() { x=`false` return; }; f; echo $?
1 1 1 0 0 0 0 0 1
f() { return <&0`false`; }; f; echo $?
1 1 1 0 0 0 0 0 1
f() { x=`false` return <&0`false`; }; f; echo $?
1 1 1 1 0 0 0 0 1

Statuses measured within the command and after, with matching and non-matching patterns.

test bash bash
(POSIX)
zsh
(emulate ksh)
ksh93 mksh posh dash busybox heirloom
(exit 2); case x in x) echo $?;; esac
2 2 0 2 2 2 0 0 2
(exit 2); case `exit 3`x in x) echo $?;; esac
3 3 0 3 2 2 0 0 3
(exit 2); case x in `exit 4`x) echo $?;; esac
4 4 4 4 2 2 0 0 4
(exit 2); case `exit 3`x in `exit 4`x) echo $?;; esac
4 4 4 4 2 2 0 0 4
(exit 2); case x in x);; esac; echo $?
0 0 0 0 0 0 0 0 2
(exit 2); case x in "");; esac; echo $?
0 0 0 0 0 0 0 0 2
(exit 2); case `exit 3`x in x);; esac; echo $?
0 0 0 3 0 0 0 0 3
(exit 2); case `exit 3`x in "");; esac; echo $?
0 0 0 3 0 0 0 0 3
(exit 2); case x in `exit 4`x);; esac; echo $?
0 0 0 4 0 0 0 0 4
(exit 2); case x in `exit 4`);; esac; echo $?
0 0 4 4 0 0 0 0 4
(exit 2); case `exit 3`x in `exit 4`);; esac; echo $?
0 0 4 4 0 0 0 0 4
(exit 2); case `exit 3`x in `exit 4`x);; esac; echo $?
0 0 0 4 0 0 0 0 4

When you execute a command in Linux, it generates a numeric return code. This happens whether you’re running the command directly from the shell, from a script, or even from an Ansible playbook. You can use those return codes to handle the result of that command properly.

What the return codes mean

When running commands at a shell prompt, the special variable $? contains a number that indicates the result of the last command executed.

[ Download now: A sysadmin’s guide to Bash scripting. ]

A zero (0) means everything went fine. Anything else means there is a problem.

A value of 1 generically indicates that some error has happened.

$ who
admin2   :1           2022-03-15 10:14 (:1)

$ echo $?
0

$ who | grep thisstringdoesnotexist

$ echo $?
1

In the example above, I executed the who command, which showed that I am admin2.

Immediately after that, I executed echo $?, and it returned zero because the previous command was executed successfully.

Then I executed the same who command (which I know is working fine) and piped that to grep with the non-existing argument. This time the $? variable contains a 1.

Why? It’s because the last command executed was grep, which returns 1 when it cannot find the argument in its input.

[ Free eBook: Manage your Linux environment for success ]

Here is another example:

$ls myfile.cfg
ls: cannot access 'myfile.cfg': No such file or directory

$echo $?
2

Here I tried to list a file that doesn’t exist. Then when I entered echo $? I got 2, which is how the ls command indicates that the argument is not a file or directory name.

Customize the return code

You can also use the exit command from a shell script to customize the return code to the caller script.

The following script illustrates this:

#!/bin/bash

if [ ! -f myfile.cfg ];
then
  echo The file does not exist and we display an error
  exit 64
fi

echo The file exists and we will do something
echo "(Not really doing anything, but this is where we could do it)"

exit 0
$./myscrypt.sh 
The file does not exist and we display an error

$echo $?
64

$touch myfile.cfg

$./myscrypt.sh 
The file exists and we will do something
(Not really doing anything, but this is where we could do it)

$echo $?
0

In this script, I explicitly provide the exit code for the failed and for the successful cases. Some observations:

  1. If I do not explicitly use exit 0, the return code from myscript.sh will be the return code from the last command executed inside it. This could be what I want, but here I wanted to state the return code pretty clearly inside the script.
  2. I used 64 as an arbitrary return code for the error condition. The Advanced Bash-Scripting Guide offers some guidelines about exit code values.

Test the return code with a shell script

If you need to test the return code of a command you invoked on your shell script, you just need to test the $? variable immediately after the command executes.

#!/bin/bash

# A snipet from a shell script ...
# Next we will invoke a command or another shell script

./myscript.sh

RETURN=$?

if [ $RETURN -eq 0 ];
then
  echo "The script myscript.sh was executed successfuly"
  exit 0
else
  echo "The script myscript.sh was NOT executed successfuly and returned the code $RETURN"
  exit $RETURN
fi 

In this example, after invoking my command or script, I saved the exit code from $? on a variable for further utilization. (Again, $? returns the status of the last command, so you need to be careful if you run more commands—even a simple echo.).

Test the return code with an Ansible playbook

It is recommended to avoid running shell commands from an Ansible playbook if there is an Ansible module that performs the same action. This is especially because with a module you have better odds of being idempotent.

[ Ready to start automating? Check out this Ansible quick start series. ]

To illustrate how to handle return codes from a shell command, check this simple playbook:

---
- name: Executes a shell script
  hosts: localhost
  gather_facts: no
  tasks:
  - name: Execute the shell script
    shell: ./myscript.sh
    ignore_errors: true
    register: result

  - name: Shows the result of executing the script
    debug:
      msg: 
      - "Return code...: {{ result.rc }}"
      - "{{ result.stdout_lines }}"

This is the Ansible playbook executed when a script returns an error:

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match
'all'

PLAY [Executes a shell script] *****************************************************************************************

TASK [Execute the shell script] ****************************************************************************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": "./myscript.sh", "delta": "0:00:00.003315", "end": "2022-06-13 15:35:06.123759", "msg": "non-zero return code", "rc": 64, "start": "2022-06-13 15:35:06.120444", "stderr": "", "stderr_lines": [], "stdout": "The file does not exist and we display an error", "stdout_lines": ["The file does not exist and we display an error"]}
...ignoring

TASK [Shows the result of executing the script] ************************************************************************
ok: [localhost] => {
    "msg": [
        "Return code...: 64",
        [
            "The file does not exist and we display an error"
        ]
    ]
}

PLAY RECAP *************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   

I defined the task to ignore the error, so in the next task, I can simply display the return code and message from the script. I could also have handled the return code in my Ansible playbook by using the result.rc variable with some combination of the assert module or adding the when condition to another task.

Wrapping up

Now you have some knowledge about the return codes from commands and scripts invoked by your shell scripts and Ansible playbooks. I hope this helps you handle your scripts more easily.

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Коды ошибок ariston avsl 129
  • Коды ошибок ariston avsl 109
  • Коды ошибок arctic cat m1100
  • Коды ошибок arcadia
  • Коды ошибок alpha гбо

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии