Перейти к содержанию

Bash — параметры, проверки и договор сценария

Оглавление серии

В предыдущих главах мы отдельно читали журнал, изучали процессы и связывали состояние службы с доступностью порта. Теперь часть повторяющейся работы можно описать сценарием. Начнём с уже знакомой задачи: подсчитать статусы в учебном TSV. Здесь полезно не просто сохранить команду в файл, а заранее договориться, какой вход она принимает, что печатает и каким способом сообщает о проблеме. Этот договор позже позволит встроить отчёт в сборщик наблюдений.

Работаем в профиле Ubuntu 24.04 и Bash 5.x. Полный снимок находится в stages/13 учебного архива. В нём есть fixtures/access.tsv и новый scripts/summary.sh. Таблица по-прежнему содержит двенадцать синтетических записей; приложение и база для этого этапа не нужны. При подготовке сценарий не запускался: приводимые числа следуют из входного файла, а читатель отдельно получает собственный результат.

Сначала определим вход и выход

Сценарий принимает путь к таблице и необязательное максимально допустимое количество ответов 5xx. Если второй параметр пропущен, используется значение три. Мы сравниваем количество, а не процент: в маленьком учебном наборе так легче увидеть границу условия. Четыре ошибки при максимуме три дают отдельный код завершения, хотя сама таблица успешно прочитана. Повреждённая строка означает другую проблему и получает другой код.

Код Значение для вызывающей стороны
0 Формат корректен, число 5xx не превышает максимум
1 Вход недоступен либо произошла ошибка чтения
2 Неверное количество или значение параметров
3 Нарушен договор TSV
4 Отчёт построен, но число 5xx превышает максимум

Такое различие важно для сопровождения. Если автоматизация видит только слово «ошибка», она не понимает, нужно ли исследовать приложение, проверить путь или исправить формат данных. Код четыре не разрешает перезапускать DeskQueue: он лишь сообщает о результате подсчёта. Решение о следующем действии остаётся отдельной частью рабочего процесса. Тем самым сценарий отвечает на один вопрос и не принимает скрытых решений об инфраструктуре.

Параметры остаются данными

$# содержит число переданных аргументов, $1 — первый аргумент, а ${2:-3} подставляет значение по умолчанию для второго. Сначала проверяем количество параметров и только затем обращаемся к обязательному первому. Это особенно заметно при set -u: обращение к отсутствующей переменной считается ошибкой оболочки. Такая настройка помогает заметить опечатку, но не заменяет внятное сообщение об использовании программы.

Путь всегда передаём в двойных кавычках. Тогда имя вроде fixtures/access copy.tsv остаётся одним аргументом. Без кавычек Bash может разделить значение по пробелам и раскрыть символы шаблона. Это меняет уже саму команду, ещё до чтения файла. Кавычки не проверяют содержимое TSV и не делают путь существующим: за эти свойства отвечают отдельные условия -f и -r.

Максимум принимаем только как строку цифр длиной не более шести символов. Поэтому отрицательное число, произвольный текст или выражение оболочки отклоняются до подсчёта. Не используем eval, не загружаем вход через source и не пытаемся исполнить значения столбцов. Сценарий интерпретирует ограниченный формат данных, а не произвольную программу. Детали раскрытия параметров и кавычек описаны в справке Bash для выбранной Ubuntu.

Полный файл сценария

Bash здесь отвечает за параметры, доступность входа и передачу управления. Сам подсчёт оставляем awk, поскольку работа с полями является его прямой задачей. Не требуется вручную разбивать каждую строку средствами оболочки и сохранять весь журнал в переменной. Потоковая обработка сохраняет простую структуру: одна запись поступила, проверена, добавлена к счётчикам.

#!/usr/bin/env bash
set -u
set -o pipefail

if (( $# < 1 || $# > 2 )); then
  printf 'Usage: bash summary.sh INPUT.tsv [MAX_5XX]\n' >&2
  exit 2
fi
input=$1
maximum=${2:-3}
if [[ ! $maximum =~ ^[0-9]+$ || ${#maximum} -gt 6 ]]; then
  printf 'MAX_5XX must be a nonnegative integer of at most six digits\n' >&2
  exit 2
fi
if [[ ! -f "$input" || ! -r "$input" ]]; then
  printf 'Input must be a readable regular file: %s\n' "$input" >&2
  exit 1
fi

if awk -F '\t' -v maximum="$maximum" '
  NR == 1 {
    if (NF != 6 || $1 != "time" || $2 != "status" ||
        $3 != "method" || $4 != "path" ||
        $5 != "duration_ms" || $6 != "request_id") {
      print "Invalid TSV header" > "/dev/stderr"
      invalid = 1
      exit 3
    }
    header = 1
    next
  }
  {
    if (NF != 6 || $2 !~ /^[1-5][0-9][0-9]$/ ||
        $5 !~ /^[0-9]+$/ || $1 == "" || $3 == "" ||
        $4 == "" || $6 == "") {
      printf "Invalid TSV record at line %d\n", NR > "/dev/stderr"
      invalid = 1
      exit 3
    }
    counts[$2]++
    total++
    if ($2 + 0 >= 500) failures++
  }
  END {
    if (invalid) exit 3
    if (!header) {
      print "Missing TSV header" > "/dev/stderr"
      exit 3
    }
    printf "total\t%d\n", total
    for (status = 100; status <= 599; status++) {
      if (counts[status]) printf "status_%d\t%d\n", status, counts[status]
    }
    printf "server_errors\t%d\n", failures
    printf "server_error_percent\t%.2f\n", total ? 100 * failures / total : 0
    if (failures > maximum) exit 4
  }
' "$input"; then
  exit 0
else
  result=$?
  case "$result" in
    3|4) exit "$result" ;;
    *) exit 1 ;;
  esac
fi

Первая строка указывает желаемый интерпретатор при прямом запуске файла. В упражнениях явно пишем bash, поэтому не требуется менять права файла ради первого знакомства. Сценарий использует конструкцию [[ ... ]] и арифметическую проверку (( ... )), относящиеся к Bash; запуск через произвольный sh меняет договор. Подход «у всех оболочек примерно одинаковый синтаксис» приводит к ошибкам, которые трудно связать с исходным заданием.

set -o pipefail делает ненулевой статус этапа конвейера заметным через статус всего конвейера. В текущей версии подсчёт выполняется одной командой, поэтому эта настройка пока не меняет его результат; она задаёт осознанное правило на случай дальнейшего добавления конвейера. set -e не включён. Мы хотим явно обработать ожидаемые ненулевые результаты, а не полагаться на сложные исключения автоматического завершения оболочки.

Проверка структуры предшествует сводке

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

Эта проверка намеренно ограничена. Она не доказывает, что дата существует в календаре, метод известен серверу, идентификаторы уникальны или запрос действительно был выполнен. Пустая таблица с корректным заголовком допускается и даёт нулевые счётчики. Если продукту понадобится иной договор, например обязательная хотя бы одна запись, его следует сформулировать явно и изменить проверку. Добавление случайных условий без объяснения делает интерфейс сценария непредсказуемым.

Блок END исполняется и после досрочного выхода из обработки. Поэтому флаг invalid не позволяет напечатать частичную сводку как успешный отчёт. Отдельный флаг header отличает полностью пустой файл от файла с заголовком. Для процентов предусмотрено деление только при ненулевом количестве записей. Статусы выводятся в порядке чисел, а не в неопределённом порядке обхода массива awk. Порядок выполнения END и действие exit описаны в справке mawk, одной из реализаций awk для этого профиля.

Посмотрим на ожидаемое поведение

Из корня ops-lab передайте пути к файлам снимка. В первой команде максимум совпадает с тремя учебными ответами 500. Во второй он меньше этого количества. Оба вызова должны построить одинаковую сводку; различаться будет заключительный статус процесса.

bash stages/13/scripts/summary.sh stages/13/fixtures/access.tsv 3
if bash stages/13/scripts/summary.sh stages/13/fixtures/access.tsv 2; then
  printf 'The selected threshold is satisfied\n'
else
  result=$?
  printf 'Summary finished with status %s\n' "$result"
fi

Для подготовленного входа ожидаются total со значением двенадцать, группы 200 — четыре, 201 — три, 404 — два, 500 — три, server_errors — три и server_error_percent — 25.00. Второй вызов должен закончиться кодом четыре. Сохраняем $? сразу: следующая команда заменит статус своим собственным результатом. Это небольшой приём, который становится критичным в сценарии из многих шагов.

Для самостоятельной проверки создайте копию TSV, измените в ней заголовок и сопоставьте сообщения. Исходный fixture оставьте доступным для следующих уроков. Ожидаемый результат повреждённой копии — сообщение о договоре данных без итоговой сводки. Итог главы — один полный сценарий с устойчивым интерфейсом. Теперь его можно вызывать из другого сценария, сохранять оба потока отдельно и различать сбой наблюдения и обнаруженный симптом.