Bash output error code

What is your favorite method to handle errors in Bash? The best example of handling errors I have found on the web was written by William Shotts, Jr at http://www.linuxcommand.org. He suggests u...

Using trap is not always an option. For example, if you’re writing some kind of re-usable function that needs error handling and that can be called from any script (after sourcing the file with helper functions), that function cannot assume anything about exit time of the outer script, which makes using traps very difficult. Another disadvantage of using traps is bad composability, as you risk overwriting previous trap that might be set earlier up in the caller chain.

There is a little trick that can be used to do proper error handling without traps. As you may already know from other answers, set -e doesn’t work inside commands if you use || operator after them, even if you run them in a subshell; e.g., this wouldn’t work:

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

But || operator is needed to prevent returning from the outer function before cleanup. The trick is to run the inner command in background, and then immediately wait for it. The wait builtin will return the exit code of the inner command, and now you’re using || after wait, not the inner function, so set -e works properly inside the latter:

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

Here is the generic function that builds upon this idea. It should work in all POSIX-compatible shells if you remove local keywords, i.e. replace all local x=y with just 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
}

Example of usage:

#!/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 "$@"

Running the example:

$ ./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

The only thing that you need to be aware of when using this method is that all modifications of Shell variables done from the command you pass to run will not propagate to the calling function, because the command runs in a subshell.

Scripting is one of the key tools for a sysadmin to manage a set of day-to-day activities such as running backups, adding users/groups, installing/updating packages, etc. While writing a script, error handling is one of the crucial things to manage.

This article shows some basic/intermediate techniques of dealing with error handling in Bash scripting. I discuss how to obtain the error codes, get verbose output while executing the script, deal with the debug function, and standard error redirection. Using these techniques, sysadmins can make their daily work easy.

[ Readers also liked: Bash command line exit codes demystified ]

Exit status

In Bash scripting, $? prints the exit status. If it returns zero, it means there is no error. If it is non-zero, then you can conclude the earlier task has some issue.

A basic example is as follows:

$ cat myscript.sh
           #!/bin/bash
           mkdir learning
           echo $?

If you run the above script once, it will print 0 because the directory does not exist, therefore the script will create it. Naturally, you will get a non-zero value if you run the script a second time, as seen below:

$ sh myscript.sh
mkdir: cannot create directory 'learning': File exists
1

Best practices

It is always recommended to enable the debug mode by adding the -e option to your shell script as below:

$ cat test3.sh
!/bin/bash
set -x
echo "hello World"
mkdiir testing
 ./test3.sh
+ echo 'hello World'
hello World
+ mkdiir testing
./test3.sh: line 4: mkdiir: command not found

You can write a debug function as below, which helps to call it anytime, using the example below:

$ cat debug.sh
#!/bin/bash
_DEBUG="on"
function DEBUG()
{
 [ "$_DEBUG" == "on" ] && $@
}
DEBUG echo 'Testing Debudding'
DEBUG set -x
a=2
b=3
c=$(( $a + $b ))
DEBUG set +x
echo "$a + $b = $c"

Which prints:

$ ./debug.sh
Testing Debudding
+ a=2
+ b=3
+ c=5
+ DEBUG set +x
+ '[' on == on ']'
+ set +x
2 + 3 = 5

Standard error redirection

You can redirect all the system errors to a custom file using standard errors, which can be denoted by the number 2. Execute it in normal Bash commands, as demonstrated below:

$ mkdir users 2> errors.txt
$ cat errors.txt
mkdir: cannot create directory ‘users’: File exists

Most of the time, it is difficult to find the exact line number in scripts. To print the line number with the error, use the PS4 option (supported with Bash 4.1 or later). Example below:

$ cat test3.sh
#!/bin/bash
PS4='LINENO:'

set -x
echo "hello World"
mkdiir testing

You can easily see the line number while reading the errors:

$ /test3.sh
5: echo 'hello World'
hello World
6: mkdiir testing
./test3.sh: line 6: mkdiir: command not found

[ Get this free ebook: Managing your Kubernetes clusters for dummies. ]

Wrap up

Managing errors is a key skill for administrators when writing scripts. These tips should help make your life easier when troubleshooting Bash scripts or even general commands.

Exit codes indicates a failure condition when ending a program and they fall between 0 and 255. The shell and its builtins may use especially the values above 125 to indicate specific failure modes, so list of codes can vary between shells and operating systems (e.g. Bash uses the value 128+N as the exit status). See: Bash — 3.7.5 Exit Status or man bash.

In general a zero exit status indicates that a command succeeded, a non-zero exit status indicates failure.

To check which error code is returned by the command, you can print $? for the last exit code or ${PIPESTATUS[@]} which gives a list of exit status values from pipeline (in Bash) after a shell script exits.

There is no full list of all exit codes which can be found, however there has been an attempt to systematize exit status numbers in kernel source, but this is main intended for C/C++ programmers and similar standard for scripting might be appropriate.

Some list of sysexits on both Linux and BSD/OS X with preferable exit codes for programs (64-78) can be found in /usr/include/sysexits.h (or: man sysexits on BSD):

0   /* successful termination */
64  /* base value for error messages */
64  /* command line usage error */
65  /* data format error */
66  /* cannot open input */
67  /* addressee unknown */
68  /* host name unknown */
69  /* service unavailable */
70  /* internal software error */
71  /* system error (e.g., can't fork) */
72  /* critical OS file missing */
73  /* can't create (user) output file */
74  /* input/output error */
75  /* temp failure; user is invited to retry */
76  /* remote error in protocol */
77  /* permission denied */
78  /* configuration error */
/* maximum listed value */

The above list allocates previously unused exit codes from 64-78. The range of unallotted exit codes will be further restricted in the future.

However above values are mainly used in sendmail and used by pretty much nobody else, so they aren’t anything remotely close to a standard (as pointed by @Gilles).

In shell the exit status are as follow (based on Bash):

  • 1125 — Command did not complete successfully. Check the command’s man page for the meaning of the status, few examples below:

  • 1 — Catchall for general errors

    Miscellaneous errors, such as «divide by zero» and other impermissible operations.

    Example:

    $ let "var1 = 1/0"; echo $?
    -bash: let: var1 = 1/0: division by 0 (error token is "0")
    1
    
  • 2 — Misuse of shell builtins (according to Bash documentation)

    Missing keyword or command, or permission problem (and diff return code on a failed binary file comparison).

    Example:

     empty_function() {}
    
  • 6 — No such device or address

    Example:

    $ curl foo; echo $?
    curl: (6) Could not resolve host: foo
    6
    
  • 124 — command times out

  • 125 — if a command itself failssee: coreutils
  • 126 — if command is found but cannot be invoked (e.g. is not executable)

    Permission problem or command is not an executable.

    Example:

    $ /dev/null
    $ /etc/hosts; echo $?
    -bash: /etc/hosts: Permission denied
    126
    
  • 127 — if a command cannot be found, the child process created to execute it returns that status

    Possible problem with $PATH or a typo.

    Example:

    $ foo; echo $?
    -bash: foo: command not found
    127
    
  • 128 — Invalid argument to exit

    exit takes only integer args in the range 0 — 255.

    Example:

    $ exit 3.14159
    -bash: exit: 3.14159: numeric argument required
    
  • 128254 — fatal error signal «n» — command died due to receiving a signal. The signal code is added to 128 (128 + SIGNAL) to get the status (Linux: man 7 signal, BSD: man signal), few examples below:

  • 130 — command terminated due to Ctrl-C being pressed, 130-128=2 (SIGINT)

    Example:

    $ cat
    ^C
    $ echo $?
    130
    
  • 137 — if command is sent the KILL(9) signal (128+9), the exit status of command otherwise

    kill -9 $PPID of script.

  • 141SIGPIPE — write on a pipe with no reader

    Example:

    $ hexdump -n100000 /dev/urandom | tee &>/dev/null >(cat > file1.txt) >(cat > file2.txt) >(cat > file3.txt) >(cat > file4.txt) >(cat > file5.txt)
    $ find . -name '*.txt' -print0 | xargs -r0 cat | tee &>/dev/null >(head /dev/stdin > head.out) >(tail /dev/stdin > tail.out)
    xargs: cat: terminated by signal 13
    $ echo ${PIPESTATUS[@]}
    0 125 141
    
  • 143 — command terminated by signal code 15 (128+15=143)

    Example:

    $ sleep 5 && killall sleep &
    [1] 19891
    $ sleep 100; echo $?
    Terminated: 15
    143
    
  • 255* — exit status out of range.

    exit takes only integer args in the range 0 — 255.

    Example:

    $ sh -c 'exit 3.14159'; echo $?
    sh: line 0: exit: 3.14159: numeric argument required
    255
    

According to the above table, exit codes 1 — 2, 126 — 165, and 255 have special meanings, and should therefore be avoided for user-specified exit parameters.

Please note that out of range exit values can result in unexpected exit codes (e.g. exit 3809 gives an exit code of 225, 3809 % 256 = 225).

See:

  • Appendix E. Exit Codes With Special Meanings at Advanced Bash-Scripting Guide
  • Writing Better Shell Scripts – Part 2 at Innovationsts

Bash-скрипты: начало
Bash-скрипты, часть 2: циклы
Bash-скрипты, часть 3: параметры и ключи командной строки
Bash-скрипты, часть 4: ввод и вывод
Bash-скрипты, часть 5: сигналы, фоновые задачи, управление сценариями
Bash-скрипты, часть 6: функции и разработка библиотек
Bash-скрипты, часть 7: sed и обработка текстов
Bash-скрипты, часть 8: язык обработки данных awk
Bash-скрипты, часть 9: регулярные выражения
Bash-скрипты, часть 10: практические примеры
Bash-скрипты, часть 11: expect и автоматизация интерактивных утилит

В прошлый раз, в третьей части этой серии материалов по bash-скриптам, мы говорили о параметрах командной строки и ключах. Наша сегодняшняя тема — ввод, вывод, и всё, что с этим связано.

image

Вы уже знакомы с двумя методами работы с тем, что выводят сценарии командной строки:

  • Отображение выводимых данных на экране.
  • Перенаправление вывода в файл.

Иногда что-то надо показать на экране, а что-то — записать в файл, поэтому нужно разобраться с тем, как в Linux обрабатывается ввод и вывод, а значит — научиться отправлять результаты работы сценариев туда, куда нужно. Начнём с разговора о стандартных дескрипторах файлов.

Стандартные дескрипторы файлов

Всё в Linux — это файлы, в том числе — ввод и вывод. Операционная система идентифицирует файлы с использованием дескрипторов.

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

  • 0, STDIN — стандартный поток ввода.
  • 1, STDOUT — стандартный поток вывода.
  • 2, STDERR — стандартный поток ошибок.

Эти три специальных дескриптора обрабатывают ввод и вывод данных в сценарии.
Вам нужно как следует разобраться в стандартных потоках. Их можно сравнить с фундаментом, на котором строится взаимодействие скриптов с внешним миром. Рассмотрим подробности о них.

STDIN

STDIN — это стандартный поток ввода оболочки. Для терминала стандартный ввод — это клавиатура. Когда в сценариях используют символ перенаправления ввода — <, Linux заменяет дескриптор файла стандартного ввода на тот, который указан в команде. Система читает файл и обрабатывает данные так, будто они введены с клавиатуры.

Многие команды bash принимают ввод из STDIN, если в командной строке не указан файл, из которого надо брать данные. Например, это справедливо для команды cat.

Когда вы вводите команду cat в командной строке, не задавая параметров, она принимает ввод из STDIN. После того, как вы вводите очередную строку, cat просто выводит её на экран.

STDOUT

STDOUT — стандартный поток вывода оболочки. По умолчанию это — экран. Большинство bash-команд выводят данные в STDOUT, что приводит к их появлению в консоли. Данные можно перенаправить в файл, присоединяя их к его содержимому, для этого служит команда >>.

Итак, у нас есть некий файл с данными, к которому мы можем добавить другие данные с помощью этой команды:

pwd >> myfile

То, что выведет pwd, будет добавлено к файлу myfile, при этом уже имеющиеся в нём данные никуда не денутся.

Перенаправление вывода команды в файл

Пока всё хорошо, но что если попытаться выполнить что-то вроде показанного ниже, обратившись к несуществующему файлу xfile, задумывая всё это для того, чтобы в файл myfile попало сообщение об ошибке.

ls –l xfile > myfile

После выполнения этой команды мы увидим сообщения об ошибках на экране.

Попытка обращения к несуществующему файлу

При попытке обращения к несуществующему файлу генерируется ошибка, но оболочка не перенаправила сообщения об ошибках в файл, выведя их на экран. Но мы-то хотели, чтобы сообщения об ошибках попали в файл. Что делать? Ответ прост — воспользоваться третьим стандартным дескриптором.

STDERR

STDERR представляет собой стандартный поток ошибок оболочки. По умолчанию этот дескриптор указывает на то же самое, на что указывает STDOUT, именно поэтому при возникновении ошибки мы видим сообщение на экране.

Итак, предположим, что надо перенаправить сообщения об ошибках, скажем, в лог-файл, или куда-нибудь ещё, вместо того, чтобы выводить их на экран.

▍Перенаправление потока ошибок

Как вы уже знаете, дескриптор файла STDERR — 2. Мы можем перенаправить ошибки, разместив этот дескриптор перед командой перенаправления:

ls -l xfile 2>myfile
cat ./myfile

Сообщение об ошибке теперь попадёт в файл myfile.

Перенаправление сообщения об ошибке в файл

▍Перенаправление потоков ошибок и вывода

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

ls –l myfile xfile anotherfile 2> errorcontent 1> correctcontent

Перенаправление ошибок и стандартного вывода

Оболочка перенаправит то, что команда ls обычно отправляет в STDOUT, в файл correctcontent благодаря конструкции 1>. Сообщения об ошибках, которые попали бы в STDERR, оказываются в файле errorcontent из-за команды перенаправления 2>.

Если надо, и STDERR, и STDOUT можно перенаправить в один и тот же файл, воспользовавшись командой &>:

Перенаправление STDERR и STDOUT в один и тот же файл

После выполнения команды то, что предназначено для STDERR и STDOUT, оказывается в файле content.

Перенаправление вывода в скриптах

Существует два метода перенаправления вывода в сценариях командной строки:

  • Временное перенаправление, или перенаправление вывода одной строки.
  • Постоянное перенаправление, или перенаправление всего вывода в скрипте либо в какой-то его части.

▍Временное перенаправление вывода

В скрипте можно перенаправить вывод отдельной строки в STDERR. Для того, чтобы это сделать, достаточно использовать команду перенаправления, указав дескриптор STDERR, при этом перед номером дескриптора надо поставить символ амперсанда (&):

#!/bin/bash
echo "This is an error" >&2
echo "This is normal output"

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

Временное перенаправление

Запустим скрипт так, чтобы вывод STDERR попадал в файл.

./myscript 2> myfile

Как видно, теперь обычный вывод делается в консоль, а сообщения об ошибках попадают в файл.

Сообщения об ошибках записываются в файл

▍Постоянное перенаправление вывода

Если в скрипте нужно перенаправлять много выводимых на экран данных, добавлять соответствующую команду к каждому вызову echo неудобно. Вместо этого можно задать перенаправление вывода в определённый дескриптор на время выполнения скрипта, воспользовавшись командой exec:

#!/bin/bash
exec 1>outfile
echo "This is a test of redirecting all output"
echo "from a shell script to another file."
echo "without having to redirect every line"

Запустим скрипт.

Перенаправление всего вывода в файл

Если просмотреть файл, указанный в команде перенаправления вывода, окажется, что всё, что выводилось командами echo, попало в этот файл.

Команду exec можно использовать не только в начале скрипта, но и в других местах:

#!/bin/bash
exec 2>myerror
echo "This is the start of the script"
echo "now redirecting all output to another location"
exec 1>myfile
echo "This should go to the myfile file"
echo "and this should go to the myerror file" >&2

Вот что получится после запуска скрипта и просмотра файлов, в которые мы перенаправляли вывод.

Перенаправление вывода в разные файлы

Сначала команда exec задаёт перенаправление вывода из STDERR в файл myerror. Затем вывод нескольких команд echo отправляется в STDOUT и выводится на экран. После этого команда exec задаёт отправку того, что попадает в STDOUT, в файл myfile, и, наконец, мы пользуемся командой перенаправления в STDERR в команде echo, что приводит к записи соответствующей строки в файл myerror.

Освоив это, вы сможете перенаправлять вывод туда, куда нужно. Теперь поговорим о перенаправлении ввода.

Перенаправление ввода в скриптах

Для перенаправления ввода можно воспользоваться той же методикой, которую мы применяли для перенаправления вывода. Например, команда exec позволяет сделать источником данных для STDIN какой-нибудь файл:

exec 0< myfile

Эта команда указывает оболочке на то, что источником вводимых данных должен стать файл myfile, а не обычный STDIN. Посмотрим на перенаправление ввода в действии:

#!/bin/bash
exec 0< testfile
count=1
while read line
do
echo "Line #$count: $line"
count=$(( $count + 1 ))
done

Вот что появится на экране после запуска скрипта.

Перенаправление ввода

В одном из предыдущих материалов вы узнали о том, как использовать команду read для чтения данных, вводимых пользователем с клавиатуры. Если перенаправить ввод, сделав источником данных файл, то команда read, при попытке прочитать данные из STDIN, будет читать их из файла, а не с клавиатуры.

Некоторые администраторы Linux используют этот подход для чтения и последующей обработки лог-файлов.

Создание собственного перенаправления вывода

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

Назначить дескриптор для вывода данных можно, используя команду exec:

#!/bin/bash
exec 3>myfile
echo "This should display on the screen"
echo "and this should be stored in the file" >&3
echo "And this should be back on the screen"

После запуска скрипта часть вывода попадёт на экран, часть — в файл с дескриптором 3.

Перенаправление вывода, используя собственный дескриптор

Создание дескрипторов файлов для ввода данных

Перенаправить ввод в скрипте можно точно так же, как и вывод. Сохраните STDIN в другом дескрипторе, прежде чем перенаправлять ввод данных.

После окончания чтения файла можно восстановить STDIN и пользоваться им как обычно:

#!/bin/bash
exec 6<&0
exec 0< myfile
count=1
while read line
do
echo "Line #$count: $line"
count=$(( $count + 1 ))
done
exec 0<&6
read -p "Are you done now? " answer
case $answer in
y) echo "Goodbye";;
n) echo "Sorry, this is the end.";;
esac

Испытаем сценарий.

Перенаправление ввода

В этом примере дескриптор файла 6 использовался для хранения ссылки на STDIN. Затем было сделано перенаправление ввода, источником данных для STDIN стал файл. После этого входные данные для команды read поступали из перенаправленного STDIN, то есть из файла.

После чтения файла мы возвращаем STDIN в исходное состояние, перенаправляя его в дескриптор 6. Теперь, для того, чтобы проверить, что всё работает правильно, скрипт задаёт пользователю вопрос, ожидает ввода с клавиатуры и обрабатывает то, что введено.

Закрытие дескрипторов файлов

Оболочка автоматически закрывает дескрипторы файлов после завершения работы скрипта. Однако, в некоторых случаях нужно закрывать дескрипторы вручную, до того, как скрипт закончит работу. Для того, чтобы закрыть дескриптор, его нужно перенаправить в &-. Выглядит это так:

#!/bin/bash
exec 3> myfile
echo "This is a test line of data" >&3
exec 3>&-
echo "This won't work" >&3

После исполнения скрипта мы получим сообщение об ошибке.

Попытка обращения к закрытому дескриптору файла

Всё дело в том, что мы попытались обратиться к несуществующему дескриптору.

Будьте внимательны, закрывая дескрипторы файлов в сценариях. Если вы отправляли данные в файл, потом закрыли дескриптор, потом — открыли снова, оболочка заменит существующий файл новым. То есть всё то, что было записано в этот файл ранее, будет утеряно.

Получение сведений об открытых дескрипторах

Для того, чтобы получить список всех открытых в Linux дескрипторов, можно воспользоваться командой lsof. Во многих дистрибутивах, вроде Fedora, утилита lsof находится в /usr/sbin. Эта команда весьма полезна, так как она выводит сведения о каждом дескрипторе, открытом в системе. Сюда входит и то, что открыли процессы, выполняемые в фоне, и то, что открыто пользователями, вошедшими в систему.

У этой команды есть множество ключей, рассмотрим самые важные.

  • -p Позволяет указать ID процесса.
  • -d Позволяет указать номер дескриптора, о котором надо получить сведения.

Для того, чтобы узнать PID текущего процесса, можно использовать специальную переменную окружения $$, в которую оболочка записывает текущий PID.

Ключ -a используется для выполнения операции логического И над результатами, возвращёнными благодаря использованию двух других ключей:

lsof -a -p $$ -d 0,1,2

Вывод сведений об открытых дескрипторах

Тип файлов, связанных с STDIN, STDOUT и STDERR — CHR (character mode, символьный режим). Так как все они указывают на терминал, имя файла соответствует имени устройства, назначенного терминалу. Все три стандартных файла доступны и для чтения, и для записи.

Посмотрим на вызов команды lsof из скрипта, в котором открыты, в дополнение к стандартным, другие дескрипторы:

#!/bin/bash
exec 3> myfile1
exec 6> myfile2
exec 7< myfile3
lsof -a -p $$ -d 0,1,2,3,6,7

Вот что получится, если этот скрипт запустить.

Просмотр дескрипторов файлов, открытых скриптом

Скрипт открыл два дескриптора для вывода (3 и 6) и один — для ввода (7). Тут же показаны и пути к файлам, использованных для настройки дескрипторов.

Подавление вывода

Иногда надо сделать так, чтобы команды в скрипте, который, например, может исполняться как фоновый процесс, ничего не выводили на экран. Для этого можно перенаправить вывод в /dev/null. Это — что-то вроде «чёрной дыры».

Вот, например, как подавить вывод сообщений об ошибках:

ls -al badfile anotherfile 2> /dev/null

Тот же подход используется, если, например, надо очистить файл, не удаляя его:

cat /dev/null > myfile

Итоги

Сегодня вы узнали о том, как в сценариях командной строки работают ввод и вывод. Теперь вы умеете обращаться с дескрипторами файлов, создавать, просматривать и закрывать их, знаете о перенаправлении потоков ввода, вывода и ошибок. Всё это очень важно в деле разработки bash-скриптов.

В следующий раз поговорим о сигналах Linux, о том, как обрабатывать их в сценариях, о запуске заданий по расписанию и о фоновых задачах.

Уважаемые читатели! В этом материале даны основы работы с потоками ввода, вывода и ошибок. Уверены, среди вас есть профессионалы, которые могут рассказать обо всём этом то, что приходит лишь с опытом. Если так — передаём слово вам.

Bash, or the Bourne-Again Shell, is a powerful command-line interface (CLI) that is commonly used in Linux and Unix systems. When working with Bash, it is important to understand how to handle errors that may occur during the execution of commands. In this article, we will discuss various ways to understand and ignore errors in Bash. Bash scripting is a powerful tool for automating and simplifying various tasks in Linux and Unix systems. However, errors can occur during the execution of commands and can cause scripts to fail. In this article, we will explore the various ways to understand and handle errors in Bash. We will look at ways to check the exit status code and error messages of commands, as well as techniques for ignoring errors when necessary. By understanding and properly handling errors, you can ensure that your Bash scripts run smoothly and achieve the desired outcome.

Step-by-step approach for understanding and ignoring errors in Bash:

Step 1: Understand how errors are generated in Bash.

  • When a command is executed, it returns an exit status code.
  • A successful command will have an exit status of 0, while a failed command will have a non-zero exit status.
  • Error messages are generated when a command returns a non-zero exit status code.

Step 2: Check the exit status code of a command.

  • To check the exit status code of a command, you can use the $? variable, which holds the exit status of the last executed command.
  • For example, after executing the command ls non_existent_directory, you can check the exit status code by running echo $? The output
  • will be non-zero (e.g., 2) indicating that the command failed.

Step 3: Check the error message of a command.

  • To check the error message of a command, you can redirect the standard error output (stderr) to a file or to the standard output (stdout) using the 2> operator.
  • For example, you can redirect the stderr of the command ls non_existent_directory to a file by running ls non_existent_directory 2> error.log. Then you can view the error message by running cat error.log.

Step 4: Use the set -e command.

  • The set -e command causes the script to exit immediately if any command exits with a non-zero status. This can be useful for detecting and handling errors early on in a script.
  • For example, if you run set -e followed by ls non_existent_directory, the script will exit immediately with an error message. 

Step 5: Ignore errors when necessary.

  • To ignore errors, you can use the command || true construct. This construct allows you to execute a command, and if it returns a non-zero exit status, the command following the || operator (in this case, true) will be executed instead.
  • For example, you can run rm non_existent_file || true to remove a file that does not exist without exiting with an error.
  • Another way to ignore errors is to use the command 2> /dev/null construct, which redirects the standard error output (stderr) of a command to the null device, effectively ignoring any error messages.
  • Additionally, you can use the command 2>&1 >/dev/null construct to ignore both standard error and standard output.
  • You can also use the command || : construct which allows you to execute a command and if it returns a non-zero exit status, the command following the || operator (in this case, 🙂 will be executed instead. The: command is a no-op command that does nothing, effectively ignoring the error.

Practical Explanation for Understanding Errors

First, let’s examine how errors are generated in Bash. When a command is executed, it returns an exit status code. This code indicates whether the command was successful (exit status 0) or not (non-zero exit status). For example, the following command attempts to list the files in a directory that does not exist:

$ ls non_existent_directory
ls: cannot access 'non_existent_directory': No such file or directory

As you can see, the command generated an error message and returned a non-zero exit status code. To check the exit status code of a command, you can use the $? variable, which holds the exit status of the last executed command.

$ echo $?
2

In addition to the exit status code, you can also check the standard error output (stderr) of a command to understand errors. This can be done by redirecting the stderr to a file or to the standard output (stdout) using the 2> operator.

For example, the following script will redirect the stderr of a command to a file:

$ ls non_existent_directory 2> error.log
$ cat error.log
ls: cannot access 'non_existent_directory': No such file or directory

You can also redirect the stderr to the stdout using the 2>&1 operator, which allows you to see the error message along with the standard output of the command.

$ ls non_existent_directory 2>&1
ls: cannot access 'non_existent_directory': No such file or directory

Another useful tool for understanding errors is the set -e command, which causes the script to exit immediately if any command exits with a non-zero status. This can be useful for detecting and handling errors early on in a script.

$ set -e
$ ls non_existent_directory
# as soon as you hit enter this will exit shell and will close the terminal.

After this command script will exit from the shell if the exit code is nonzero.

Practical Explanation for Ignoring Errors

While it is important to handle errors in Bash scripts, there may be certain situations where you want to ignore errors and continue running the script. In this section, we will discuss different methods for ignoring errors in Bash and provide examples of how to implement them.

Heredoc

Heredoc is a feature in Bash that allows you to specify a string or command without having to escape special characters. This can be useful when you want to ignore errors that may occur while executing a command. The following example demonstrates how to use Heredoc to ignore errors.

#!/bin/bash

# Example of ignoring errors using Heredoc

# The `command` will fail but it will not stop execution
cat <<EOF | while read line; do
  echo $line
done
command that will fail
EOF

# Rest of the script

In this example, the command that is inside the Heredoc will fail, but the script will not stop execution. This is because the output of the command is piped to the while loop, which reads the output and ignores the error.

Pipefail

The pipe fails option in Bash can be used to change the behavior of pipelines so that the exit status of the pipeline is the value of the last (rightmost) command to exit with a non-zero status or zero if all commands exit successfully. This can be useful when you want to ignore errors that may occur while executing multiple commands in a pipeline. The following example demonstrates how to use the pipe fail option to ignore errors.

#!/bin/bash

# Example of ignoring errors using pipefail

# The `command1` will fail but it will not stop execution
set -o pipefail
command1 | command2

# Rest of the script

In this example, command1 will fail, but command2 will continue to execute, and the script will not stop execution.

Undefined Variables

By default, Bash will stop the execution of a script if an undefined variable is used. However, you can use the -u option to ignore this behavior and continue running the script even if an undefined variable is used. The following example demonstrates how to ignore undefined variables.

#!/bin/bash

# Example of ignoring undefined variables

set +u

echo $undefined_variable

# Rest of the script

In this example, the script will not stop execution when an undefined variable is used.

Compiling and Interpreting

When compiling or interpreting a script, errors may occur. However, these errors can be ignored by using the -f option when running the script. The following example demonstrates how to ignore errors when compiling or interpreting a script.

#!/bin/bash

# Example of ignoring errors when compiling or interpreting

bash -f script.sh

# Rest of the script

In this example, the script will continue to run even if there are errors during the compilation or interpretation process.

Traps

A trap is a way to execute a command or a set of commands when a specific signal is received by the script. This can be useful when you want to ignore errors and run a cleanup command instead. The following example demonstrates how to use a trap to ignore errors.

#!/bin/bash

# Example of ignoring errors using a trap

# Set a trap to run the cleanup function when an error occurs
trap cleanup ERR

# Function to run when an error occurs
cleanup() {
  echo "Cleaning up before exiting..."
}

# Command that will cause an error
command_that_will_fail

# Rest of the script

In this example, when the command_that_will_fail causes an error, the script will execute the cleanup function instead of stopping execution. This allows you to perform any necessary cleanup before exiting the script.

Examples of Bash for Error Handling:

Example 1: Error Handling Using a Conditional Condition

One way to handle errors in Bash is to use a conditional statement. The following example demonstrates how to check for a specific error and handle it accordingly.

#!/bin/bash

# Example of error handling using a conditional condition

file=example.txt

if [ ! -f $file ]; then
  echo "Error: $file does not exist"
  exit 1
fi

# Rest of the script

In this example, we check if the file “example.txt” exists using the -f option of the [ command. If the file does not exist, the script will print an error message and exit with a status code of 1. This allows the script to continue running if the file exists and exit if it does not.

Example 2: Error Handling Using the Exit Status Code

Another way to handle errors in Bash is to check the exit status code of a command. Every command in Bash returns an exit status code when it completes, with a code of 0 indicating success and any other code indicating an error. The following example demonstrates how to check the exit status code of a command and handle it accordingly.

#!/bin/bash

# Example of error handling using the exit status code

command1

if [ $? -ne 0 ]; then
  echo "Error: command1 failed"
  exit 1
fi

# Rest of the script

In this example, the script runs the command “command1” and then checks the exit status code using the special variable $?. If the exit status code is not 0, the script will print an error message and exit with a status code of 1.

Example 3: Stop the Execution on the First Error

When running a script, it can be useful to stop the execution on the first error that occurs. This can be achieved by using the set -e command, which tells Bash to exit the script if any command exits with a non-zero status code.

#!/bin/bash

# Stop execution on the first error

set -e

command1
command2
command3

# Rest of the script

In this example, if any of the commands “command1”, “command2” or “command3” fail, the script will exit immediately.

Example 4: Stop the Execution for Uninitialized Variable

Another way to stop execution on error is if an uninitialized variable is used during script execution. This can be achieved by using the set -u command, which tells Bash to exit the script if any uninitialized variable is used.

#!/bin/bash

# Stop execution for uninitialized variable

set -u

echo $uninitialized_variable

# Rest of the script

In this example, if the uninitialized_variable is not defined, the script will exit immediately.

Conclusion

In conclusion, understanding and ignoring errors in Bash is an important aspect of working with the command-line interface. By checking the exit status code of a command, its associated error message, and redirecting the stderr to a file or the stdout, you can understand what went wrong. And by using the command || true, command 2> /dev/null, command 2>&1 >/dev/null, and command || : constructs, you can ignore errors when necessary. It’s always a good practice to test these constructs in a testing environment before using them in production.

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

Понравилась статья? Поделить с друзьями:
  • Bad installation no jre found in configuration file как исправить
  • Bad image ошибка 0xc000012f windows 10 как исправить
  • Bad image ошибка 0xc0000020 windows 10
  • Bad guys error sans
  • Bad gateway error code 502 эпик геймс