Безопасные bash-скрипты или set -euxo pipefail

В 2020 году когда есть такие языки как python, ruby и go, использовать bash приходится редко, но иногда без него не обойтись.

Bash не похож на высокоуровневые языки программирования, он не предоставляет привычных гарантий. К примеру, если в python обратиться к неинициализированной переменной, то скрипт тут же завершиться, не выполнив ни одной инструкции. В bash это не так, любая переменная, к которой вы обратились, но не инициализировали, будет заменена на пустую строку. Только представьте сколько можно наворотить дел, если подобная переменная была в параметрах у команды rm -rf.

К счастью, можно изменить поведение оболочки используя встроенные функции, в частности set. С ее помощью, можно значительно повысить безопасность.

set -e

Указав параметр -e скрипт немедленно завершит работу, если любая команда выйдет с ошибкой. По-умолчанию, игнорируются любые неудачи и сценарий продолжет выполнятся. Если предполагается, что команда может завершиться с ошибкой, но это не критично, можно использовать пайплайн || true.

Без -e:

#!/bin/bash

./non-existing-command
echo "RUNNING"

# output
# ------
# line 3: non-existing-command: command not found
# RUNNING

С использованием -e:

#!/bin/bash
set -e

./non-existing-command || true
./non-existing-command
echo "RUNNING"

# output
# ------
# line 4: non-existing-command: command not found
# line 5: non-existing-command: command not found

set -o pipefail

Но -e не идеален. Bash возвращает только код ошибки последней команды в пайпе (конвейере). И параметр -e проверяет только его. Если нужно убедиться, что все команды в пайпах завершились успешно, нужно использовать -o pipefail.

Без -o pipefail:

#!/bin/bash
set -e

./non-existing-command | echo "PIPE"
echo "RUNNING"

# output
# ------
# PIPE
# line 4: non-existing-command: command not found
# RUNNING

С использованием -o pipefail:

#!/bin/bash
set -eo pipefail

./non-existing-command | echo "PIPE"
echo "RUNNING"

# output
# ------
# PIPE
# line 4: non-existing-command: command not found

set -u

Наверно самый полезный параметр - -u. Благодаря ему оболочка проверяет инициализацию переменных в скрипте. Если переменной не будет, скрипт немедленно завершиться. Данный параметр достаточно умен, чтобы нормально работать с переменной по-умолчанию ${MY_VAR:-$DEFAULT} и условными операторами (if, while, и др).

Без -u:

#!/bin/bash

echo "${MY_VAR}"
echo "RUNNING"

# output
# ------
# 
# RUNNING

С использованием -u:

#!/bin/bash
set -u

echo "${MY_VAR}"
echo "RUNNING"

# output
# ------
# line 4: MY_VAR: unbound variable

set -x

Параметр -x очень полезен при отладке. С помощью него bash печатает в стандартный вывод все команды перед их исполнением. Стоит учитывать, что все переменные будут уже доставлены, и с этим нужно быть аккуратнее, к примеру если используете пароли.

Без -x:

#!/bin/bash

MY_VAR="a"
echo "${MY_VAR}"
echo "RUNNING"

# output
# ------
# a
# RUNNING

С использованием -x:

#!/bin/bash
set -x

echo "${MY_VAR}"
echo "RUNNING"

# output
# ------
# + MY_VAR=a
# + echo a
# a
# + echo RUNNING
# RUNNING

Вывод

Не стоит забывать, что все эти параметры можно объединять и комбинировать между собой! Думаю, при работе с bash будет хорошим тоном начинать каждый сценарий с set -euxo pipefail.