Поиск по сайту:

Как использовать set и pipefail в сценариях Bash в Linux


Команды Linux set и pipefail определяют, что произойдет, если в сценарии Bash произойдет сбой. Есть о чем подумать, чем следует остановиться или продолжаться.

Сценарии Bash и условия ошибок

Сценарии оболочки Bash великолепны. Они быстро пишутся и не требуют компиляции. Любое повторяющееся или многоэтапное действие, которое вам нужно выполнить, можно завернуть в удобный сценарий. А поскольку скрипты могут вызывать любые стандартные утилиты Linux, вы не ограничены возможностями самого языка оболочки.

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

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

Команды set и pipefile позволяют решить, что произойдет при возникновении подобных ошибок. Они также позволяют обнаруживать ошибки, даже если они происходят в середине цепочки конвейеров.

Вот как их использовать.

Демонстрация проблемы

Вот тривиальный скрипт Bash. Он выводит на терминал две строки текста. Вы можете запустить этот скрипт, если скопируете текст в редактор и сохраните его как «script-1.sh».

#!/bin/bash

echo This will happen first 
echo This will happen second

Чтобы сделать его исполняемым, вам нужно использовать chmod:

chmod +x script-1.sh

Вам нужно будет запустить эту команду для каждого скрипта, если вы хотите запустить их на своем компьютере. Запустим скрипт:

./script-1.sh

Две строки текста отправляются в окно терминала, как и ожидалось.

Немного изменим скрипт. Мы попросим ls указать сведения о несуществующем файле. Это не удастся. Мы сохранили это как «script-2.sh» и сделали его исполняемым.

#!/bin/bash

echo This will happen first
ls imaginary-filename
echo This will happen second

Когда мы запускаем этот скрипт, мы видим сообщение об ошибке от ls.

./script-2.sh

Хотя команда ls не удалась, сценарий продолжал выполняться. И хотя при выполнении скрипта произошла ошибка, код возврата из скрипта в оболочку равен нулю, что говорит об успехе. Мы можем проверить это, используя echo и переменную $?, которая содержит последний код возврата, отправленный в оболочку.

echo $?

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

Опция set -e

Параметр set -e (выход) вызывает завершение сценария, если какой-либо из вызываемых им процессов генерирует ненулевой код возврата. Все, что не равно нулю, считается ошибкой.

Добавив параметр set -e в начало скрипта, мы можем изменить его поведение. Это «script-3.sh».

#!/bin/bash 
set -e

echo This will happen first
ls imaginary-filename
echo This will happen second

Если мы запустим этот скрипт, мы увидим эффект set -e.

./script-3.sh
echo $?

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

Устранение неполадок в трубах

Трубопровод усложняет задачу. Код возврата, который выходит из переданной по конвейеру последовательности команд, является кодом возврата последней команды в цепочке. Если происходит сбой с командой в середине цепочки, мы возвращаемся к исходной точке. Этот код возврата будет потерян, и сценарий продолжит обработку.

Мы можем увидеть эффект от передачи команд с разными кодами возврата с помощью встроенных команд оболочки true и false. Эти две команды не более чем генерируют код возврата, равный нулю или единице соответственно.

true
echo $?
false
echo $?

Если мы передаем false в true — где false представляет неудачный процесс — мы получаем код возврата true нуль.

false | true
echo $?

В Bash есть переменная массива с именем PIPESTATUS, которая фиксирует все коды возврата каждой программы в цепочке конвейеров.

false | true | false | true
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]} ${PIPESTATUS[3]}"

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

Здесь вступают в действие set -o (параметры) и pipefail. Это «script-4.sh». Это попытается передать содержимое несуществующего файла в wc.

#!/bin/bash 
set -e

echo This will happen first
cat script-99.sh | wc -l
echo This will happen second

Это терпит неудачу, как мы и ожидали.

./script-4.sh
echo $?

Первый ноль — это вывод wc, говорящий нам, что он не прочитал ни одной строки для отсутствующего файла. Второй ноль — это код возврата из второй команды echo.

Мы добавим -o pipefail, сохраним его как «script-5.sh» и сделаем исполняемым.

#!/bin/bash 
set -eo pipefail

echo This will happen first
cat script-99.sh | wc -l
echo This will happen second

Давайте запустим это и проверим код возврата.

./script-5.sh
echo $?

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

Перехват неинициализированных переменных

Неинициализированные переменные может быть трудно обнаружить в реальном скрипте. Если мы попытаемся echo вывести значение неинициализированной переменной, echo просто напечатает пустую строку. Это не вызывает сообщение об ошибке. Остальная часть скрипта будет продолжать выполняться.

Это скрипт-6.sh.

#!/bin/bash 
set -eo pipefail

echo "$notset" 
echo "Another echo command"

Мы запустим его и понаблюдаем за его поведением.

./script-6.sh
echo $?

Сценарий обходит неинициализированную переменную и продолжает выполняться. Код возврата равен нулю. Попытка найти подобную ошибку в очень длинном и сложном сценарии может быть очень сложной.

Мы можем перехватить этот тип ошибки, используя параметр set -u (unset). Мы добавим это в нашу растущую коллекцию установленных параметров в верхней части скрипта, сохраним его как «script-7.sh» и сделаем его исполняемым.

#!/bin/bash 

set -eou pipefail

echo "$notset" 

echo "Another echo command"

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

./script-7.sh
echo $?

Обнаружена неинициализированная переменная, сценарий останавливается, а код возврата устанавливается равным единице.

Параметр -u (unset) достаточно умен, чтобы не срабатывать в ситуациях, когда вы можете законно взаимодействовать с неинициализированной переменной.

В «script-8.sh» скрипт проверяет, инициализирована ли переменная New_Var или нет. Вы не хотите, чтобы сценарий останавливался на достигнутом, в реальном сценарии вы будете выполнять дальнейшую обработку и разбираться с ситуацией самостоятельно.

Обратите внимание, что мы добавили параметр -u в качестве второго параметра в инструкции set. Параметр -o pipefail должен быть последним.

#!/bin/bash 

set -euo pipefail

if [ -z "${New_Var:-}" ]; then 

echo "New_Var has no value assigned to it." 

fi

В «script-9.sh» проверяется неинициализированная переменная, и если она неинициализирована, вместо нее предоставляется значение по умолчанию.

#!/bin/bash
set -euo pipefail

default_value=484
Value=${New_Var:-$default_value}
echo "New_Var=$Value"

Сценарии разрешено выполнять до их завершения.

./script-8.sh
./script-9.sh

Запечатанный с х

Еще одна удобная опция — это опция set -x (выполнить и распечатать). Когда вы пишете сценарии, это может быть спасением. он печатает команды и их параметры по мере их выполнения.

Это дает вам быструю «грубую и готовую» форму трассировки выполнения. Выделение логических ошибок и обнаружение ошибок становится намного проще.

Мы добавим параметр set -x в «script-8.sh», сохраним его как «script-10.sh» и сделаем исполняемым.

#!/bin/bash
set -euxo pipefail

if [ -z "${New_Var:-}" ]; then
  echo "New_Var has no value assigned to it."
fi

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

./script-10.sh

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