LaurVas

Trap в Bash — обработка сигналов и ошибок

bash

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

trap ДЕЙСТВИЕ СИГНАЛ...

Trap работает довольно просто — при возникновении сигнала будет выполнено указанное действие. Если действие простое (цепочка команд, умещающаяся на одной строке), его можно указать прямо в аргументе trap. Если не очень простое, то надо объявить функцию и поместить вызов этой функции в trap.

Можно обрабатывать стандартные сигналы из signal.h. Их полный список выводится по trap -l. Также доступны специфические для Bash: DEBUG, RETURN, ERR, EXIT.

На практике trap оказывается не такой уж простой штукой. Иногда он не срабатывает, хотя вроде бы должен.

Простой наглядный пример

Ко мне понимание пришло одновременно с этим демонстрационным скриптом

trap_signals_demo.sh
#!/bin/bash

trap 'echo trap SIGINT' SIGINT
trap 'echo trap SIGTERM' SIGTERM
trap 'echo trap SIGHUP' SIGHUP
trap 'echo trap SIGQUIT' SIGQUIT
trap 'echo trap EXIT' EXIT
trap 'echo trap ERR' ERR

echo 'start'
sleep 1m
echo 'end'

Скрипт выводит “start”, спит одну минуту, выводит “end”. Если во время сна поступает один из сигналов SIGINT, SIGTERM, SIGHUP, SIGQUIT, EXIT, ERR, то просто выводится соответствующее сообщение.

Тестируем:

$ ./trap_signal_demo.sh
start
end
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGINT %1
trap SIGINT
trap ERR
end
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGHUP %1
Обрыв терминальной линии
trap SIGHUP
trap ERR
end
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGQUIT %1
Выход (core dumped)
trap SIGQUIT
trap ERR
end
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGTERM %1
Завершено
trap SIGTERM
trap ERR
end
trap EXIT

Вывод реального терминала выглядит несколько иначе, я скрыл несущественные детали. Поясню что здесь происходило. Я запускал скрипт в фоне (& после команды), затем командой kill посылал сигнал только что запущенному процессу. Чтобы послать SIGINT, не обязательно связываться с kill. Можно во время работы скрипта нажать Ctrl+C.

Если убрать из скрипта обработку прерывающих сигналов, то мы увидим другую картину. SIGINT, SIGHUP, SIGTERM не создают сигнал ERR, а сразу ведут на выход. SIGQUIT создаёт ERR, но на выход не ведёт. Никакой закономерности тут нет, просто у каждого сигнала своя специфика.

$ ./trap_signal_demo.sh &
start
$ kill -SIGINT %1
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGHUP %1
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGQUIT %1
Выход (core dumped)
trap ERR
end
trap EXIT

$ ./trap_signal_demo.sh &
start
$ kill -SIGTERM %1
trap EXIT

Для игнорирования сигналов используется пустой trap. Этот демонстрационный скрипт можно прервать только смертоносным сигналом SIGKILL.

#!/bin/bash
trap '' SIGINT SIGTERM SIGHUP SIGQUIT
sleep 1m

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

trap - СИГНАЛ...

Вызов trap без аргументов напечатает все установленные обработчики сигналов. Это полезно при отладке.

Переменные внутри trap

Можно по-разному запихивать переменные внутрь trap. Здесь я использую ls, чтобы продемонстрировать обработку пробелов и false для имитации возникновения ошибки.

variables_in_trap_demo1.sh
#!/bin/bash
F="one two"
trap 'ls $F' ERR
F="three four"
false
variables_in_trap_demo2.sh
#!/bin/bash
F="one two"
trap "ls $F" ERR
F="three four"
false
variables_in_trap_demo3.sh
#!/bin/bash
F="one two"
trap "ls \"$F\"" ERR
F="three four"
false
variables_in_trap_demo4.sh
#!/bin/bash
F="one two"
trap "ls \"\$F\"" ERR
F="three four"
false
variables_in_trap_demo5.sh
#!/bin/bash
F="one two"
trap 'ls "$F"' ERR
F="three four"
false
$ touch 'one two' 'three four'

$ ./variables_in_trap_demo1.sh
ls: невозможно получить доступ к three: Нет такого файла или каталога
ls: невозможно получить доступ к four: Нет такого файла или каталога
$ ./variables_in_trap_demo2.sh
ls: невозможно получить доступ к one: Нет такого файла или каталога
ls: невозможно получить доступ к two: Нет такого файла или каталога
$ ./variables_in_trap_demo3.sh
one two
$ ./variables_in_trap_demo4.sh
three four
$ ./variables_in_trap_demo5.sh
three four

Если вам непонятно почему так происходит, попробуйте запустить эти примеры с включенной опцией xtrace. Для этого добавьте в начале скрипта set -x или set -o xtrace. Или укажите в sha-bang’е bash -x. Или запускайте скрипты командой bash -x СКРИПТ.

Trap и функции

Наследует ли функция обработчики сигналов? Если да, то в какой момент: при вызове функции или при её объявлении?

trap_and_functions_demo1.sh
#!/bin/bash

f1() {
  echo 'f1 start'
  echo 'trap inside f1:'
  trap
  echo 'f1 exit'
}

trap 'echo trap SIGINT' SIGINT
trap 'echo trap SIGTERM' SIGTERM
trap 'echo trap SIGHUP' SIGHUP
trap 'echo trap SIGQUIT' SIGQUIT
trap 'echo trap EXIT' EXIT
trap 'echo trap ERR' ERR
trap 'echo trap RETURN' RETURN

f2() {
  echo 'f2 start'
  echo 'trap inside f2:'
  trap
  echo 'f2 exit'
}

echo 'trap in main code:'
trap
echo 'call f1'
f1
echo 'call f2'
f2
echo 'end of script'
$ ./trap_and_functions_demo1.sh
trap in main code:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
trap -- 'echo trap ERR' ERR
trap -- 'echo trap RETURN' RETURN
call f1
f1 start
trap inside f1:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
f1 exit
call f2
f2 start
trap inside f2:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
f2 exit
end of script
trap EXIT

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

В этом выводе заметно странное поведение обработчиков ERR и RETURN: они не наследуются! Чтобы получить обработку этих сигналов внутри функции, надо включить опцию errtrace или объявить их явно в теле функции.

Всё становится ещё запутаннее, если объявить функцию, которая выполняется в подоболочке (subshell). Этот пример отличается от предыдущего заменой фигурных скобочек на круглые:

trap_and_functions_demo2.sh
#!/bin/bash

f() (
  echo 'f start'
  echo 'trap inside f:'
  trap
  echo 'f exit'
)

trap 'echo trap SIGINT' SIGINT
trap 'echo trap SIGTERM' SIGTERM
trap 'echo trap SIGHUP' SIGHUP
trap 'echo trap SIGQUIT' SIGQUIT
trap 'echo trap EXIT' EXIT
trap 'echo trap ERR' ERR
trap 'echo trap RETURN' RETURN

echo 'trap in main code:'
trap
echo 'call f'
f1
echo 'end of script'
$ ./trap_and_functions_demo2.sh
trap in main code:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
trap -- 'echo trap ERR' ERR
trap -- 'echo trap RETURN' RETURN
call f
f start
trap inside f:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
f exit
end of script
trap EXIT

Видим такое же поведение: наследуются все обработчики кроме ERR и RETURN. Однако если объявить какой-либо trap внутри функции, то наследование пропадает:

trap_and_functions_demo3.sh
#!/bin/bash

f() (
  echo 'f start'
  trap 'echo f trap ERR' ERR
  echo 'trap inside f:'
  trap
  echo 'f exit'
)

trap 'echo trap SIGINT' SIGINT
trap 'echo trap SIGTERM' SIGTERM
trap 'echo trap SIGHUP' SIGHUP
trap 'echo trap SIGQUIT' SIGQUIT
trap 'echo trap EXIT' EXIT
trap 'echo trap ERR' ERR
trap 'echo trap RETURN' RETURN

echo 'trap in main code:'
trap
echo 'call f'
f
echo 'end of script'
$ ./trap_and_functions_demo1.sh
trap in main code:
trap -- 'echo trap EXIT' EXIT
trap -- 'echo trap SIGHUP' SIGHUP
trap -- 'echo trap SIGINT' SIGINT
trap -- 'echo trap SIGQUIT' SIGQUIT
trap -- 'echo trap SIGTERM' SIGTERM
trap -- 'echo trap ERR' ERR
trap -- 'echo trap RETURN' RETURN
call f
f start
trap inside f:
trap -- 'echo f trap ERR' ERR
f exit
end of script
trap EXIT

А как bash ведёт себя в обратной ситуации? Попадают ли обработчики сигналов из функций наружу? Да, если функция была объявлена без подоболочки.

trap_and_functions_demo4.sh
#!/bin/bash

f1() {
  echo 'f1 start'
  trap 'echo f1 trap SIGINT' SIGINT
  trap 'echo f1 trap SIGTERM' SIGTERM
  trap 'echo f1 trap SIGHUP' SIGHUP
  trap 'echo f1 trap SIGQUIT' SIGQUIT
  trap 'echo f1 trap EXIT' EXIT
  trap 'echo f1 trap ERR' ERR
  trap 'echo f1 trap RETURN' RETURN
  echo 'trap inside f1:'
  trap
  echo 'f1 exit'
}

f2() (
  echo 'f2 start'
  trap 'echo f2 trap SIGINT' SIGINT
  trap 'echo f2 trap SIGTERM' SIGTERM
  trap 'echo f2 trap SIGHUP' SIGHUP
  trap 'echo f2 trap SIGQUIT' SIGQUIT
  trap 'echo f2 trap EXIT' EXIT
  trap 'echo f2 trap ERR' ERR
  trap 'echo f2 trap RETURN' RETURN
  echo 'trap inside f2:'
  trap
  echo 'f2 exit'
)

echo 'trap in main code:'
trap
echo 'call f1'
f1
echo 'trap in main code:'
trap
echo 'call f2'
f2
echo 'trap in main code:'
trap
echo end of script
$ ./trap_and_functions_demo4.sh
trap in main code:
call f1
f1 start
trap inside f1:
trap -- 'echo f1 trap EXIT' EXIT
trap -- 'echo f1 trap SIGHUP' SIGHUP
trap -- 'echo f1 trap SIGINT' SIGINT
trap -- 'echo f1 trap SIGQUIT' SIGQUIT
trap -- 'echo f1 trap SIGTERM' SIGTERM
trap -- 'echo f1 trap ERR' ERR
trap -- 'echo f1 trap RETURN' RETURN
f1 exit
f1 trap RETURN
trap in main code:
trap -- 'echo f1 trap EXIT' EXIT
trap -- 'echo f1 trap SIGHUP' SIGHUP
trap -- 'echo f1 trap SIGINT' SIGINT
trap -- 'echo f1 trap SIGQUIT' SIGQUIT
trap -- 'echo f1 trap SIGTERM' SIGTERM
trap -- 'echo f1 trap ERR' ERR
trap -- 'echo f1 trap RETURN' RETURN
call f2
f2 start
trap inside f2:
trap -- 'echo f2 trap EXIT' EXIT
trap -- 'echo f2 trap SIGHUP' SIGHUP
trap -- 'echo f2 trap SIGINT' SIGINT
trap -- 'echo f2 trap SIGQUIT' SIGQUIT
trap -- 'echo f2 trap SIGTERM' SIGTERM
trap -- 'echo f2 trap ERR' ERR
trap -- 'echo f2 trap RETURN' RETURN
f2 exit
f2 trap EXIT
trap in main code:
trap -- 'echo f1 trap EXIT' EXIT
trap -- 'echo f1 trap SIGHUP' SIGHUP
trap -- 'echo f1 trap SIGINT' SIGINT
trap -- 'echo f1 trap SIGQUIT' SIGQUIT
trap -- 'echo f1 trap SIGTERM' SIGTERM
trap -- 'echo f1 trap ERR' ERR
trap -- 'echo f1 trap RETURN' RETURN
end of script
f1 trap EXIT

Надеюсь эти примеры внесли ясность, а не запутали вас ещё больше.

Практическое применение trap

Trap можно применять для удаления временных файлов и всевозможной “подчистки за собой”. Для этого подходят два сигнала: ERR и EXIT.

Простой однострочный вариант:

#!/bin/bash
trap 'rm /tmp/tempfile' EXIT
# Do things...

С использованием функции:

#!/bin/bash

cleanup() {
    return_value=$?
    rm -rf "$tmpfile"
    exit $return_value
}

tmpfile="$(mktemp)"
trap "cleanup" EXIT
# Do things...

Удаление файла произойдёт при завершении скрипта любым из стандартных способов:

  • при нормальном завершении,
  • при возникновении ошибки при включённой опцией errexit,
  • при получении прерывающего сигнала, который может быть обработан.

Файл не удалится, если:

  • скрипт был убит сигналом SIGKILL,
  • пришёл OOM-killer и убил ваш процесс,
  • у компьютера внезапно отобрали питание.