ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Надёжный Bash: массивы, getopts, trap, блокировки и wait

Автор: · Обновлено

Надёжный скрипт сохраняет границы аргументов, проверяет ожидаемые отказы и освобождает свои временные ресурсы. Здесь используем Bash с поддержкой массивов, ассоциативных массивов и mapfile; это не POSIX sh. Для проверки версии выполните bash --version.

Массивы вместо строки с разделителями

files=('one.txt' 'two words.txt')
files+=('three.txt')
printf 'first=%s\n' "${files[0]}"
printf '<%s>\n' "${files[@]}"
for file in "${files[@]}"; do
  printf 'path=%s\n' "$file"
done

declare -A status=([api]=ready [worker]=waiting)
printf '%s\n' "${status[api]}"
mapfile -t lines < <(printf '%s\n' alpha 'beta gamma')
printf 'second=%s\n' "${lines[1]}"

Раскрытие "${files[@]}" создаёт отдельный аргумент для каждого элемента; "${files[*]}" объединяет их. Индексы начинаются с нуля и могут иметь пропуски. Ассоциативные массивы хранят значения по строковым ключам; порядок перебора не следует считать сортировкой. mapfile -t снимает конечный перевод строки и загружает весь ввод в память, поэтому для огромного файла нужен потоковый while. Process substitution <(...) предоставляет путь к потоку, но ошибка producer не становится автоматически статусом mapfile.

Для параметров используйте ${name:-default} — значение по умолчанию при отсутствующей или пустой переменной, ${name:=default} — также присвоение. ${name:?message} останавливает неинтерактивную Bash при нарушении обязательного значения. ${text:2:3} извлекает подстроку; ${text#prefix} и ${text%suffix} удаляют совпадающий край по glob-шаблону. Это не регулярные выражения.

Getopts и проверка пользовательского ввода

#!/usr/bin/env bash
label=report
while getopts ':n:h' option; do
  case "$option" in
    n) label=$OPTARG ;;
    h) printf 'usage: %s [-n LABEL] FILE...\n' "$0"; exit 0 ;;
    :) printf 'option requires a value: -%s\n' "$OPTARG" >&2; exit 2 ;;
    \?) printf 'unknown option: -%s\n' "$OPTARG" >&2; exit 2 ;;
  esac
done
shift "$((OPTIND - 1))"
printf 'label=%s\n' "$label"
printf 'file=<%s>\n' "$@"

Getopts разбирает короткие опции и сохраняет значения без ручного разрезания строки. После shift остаются позиционные аргументы. Форма -n 'daily report' передаёт один label. Для повторного разбора внутри той же оболочки нужно учитывать OPTIND. Начальное двоеточие включает управляемую диагностику; missing value и unknown option остаются разными ошибками.

Временные ресурсы и trap

Полный пример запускает две независимые функции, ожидает обе и выдаёт результат в определённом порядке:

#!/usr/bin/env bash
work=$(mktemp -d) || exit 1
cleanup() { rm -r -- "$work"; }
trap cleanup EXIT

make_result() {
  local name=$1
  printf '%s\n' "$name"
}
make_result alpha >"$work/first" &
first_pid=$!
make_result 'beta gamma' >"$work/second" &
second_pid=$!
failed=0
if ! wait "$first_pid"; then failed=1; fi
if ! wait "$second_pid"; then failed=1; fi
if (( failed != 0 )); then
  printf 'worker failed\n' >&2
  exit 1
fi
cat "$work/first" "$work/second"

Ожидаются строки alpha и beta gamma, затем временный каталог удаляется. Присваивание внутри фоновой функции не меняет parent, поэтому передаём данные файлами и отдельно проверяем статусы. Порядок wait не сериализует запуск: обе задачи уже стартовали. Для длительных дочерних процессов нужен отдельный обработчик TERM/INT, который останавливает принадлежащих скрипту детей и ждёт их до cleanup. SIGKILL не исполняет trap; пример коротких задач не обещает такую гарантию.

Блокировка параллельных запусков

Flock удерживает advisory lock, пока открыт связанный дескриптор. Все экземпляры должны использовать один стабильный lock-файл; уникальный mktemp-файл для каждого запуска не обеспечит взаимного исключения. Проверим механику в собственном каталоге:

lock_lab=$(mktemp -d)
(
  flock -n 9 || exit 1
  if flock -n "$lock_lab/job.lock" true; then
    printf 'unexpected second lock\n'
  else
    printf 'second holder blocked\n'
  fi
) 9>"$lock_lab/job.lock"
rm -r -- "$lock_lab"

Вложенная попытка должна попасть в else. Удаление lock-файла во время работы нескольких экземпляров опасно: следующий процесс может создать новый inode и получить другую блокировку. Для настоящего задания выбирайте постоянный путь с нужным владельцем; проверьте поддержку блокировок на NFS/CIFS и не переносите предположения локального ext4 автоматически.

Here-document, диагностика и статусы

Кавычки в <<'CONFIG' оставляют $name буквальным текстом, отсутствие кавычек разрешает подстановки. Такой блок удобен для шаблона, но данные с недоверенными значениями лучше сериализовать подходящим инструментом. Скрипт, который получает JSON, не должен собирать его конкатенацией строк.

set -o pipefail сообщает об отказе внутри pipeline, а PIPESTATUS позволяет сразу сохранить все статусы. set -e имеет исключения в условных контекстах; функция, вызванная в if, может выполняться при иных правилах errexit. Проверяйте необходимые операции явно, особенно запись результата и запуск детей. set -u делает отсутствующие переменные ошибкой, поэтому используйте проверенные defaults и параметры.

bash -n script.sh проверяет синтаксис без исполнения. shellcheck script.sh ищет известные опасные конструкции, но не доказывает корректность бизнес-логики. bash -x показывает раскрытые команды и может раскрыть пароли: применяйте его на учебных значениях и отключайте трассировку перед секретами.

Практика с ожидаемым отказом

Сохраните пример getopts, выполните -n 'night report' 'two words.txt', затем -n без значения: второй запуск обязан вернуть 2. В параллельном примере замените одну функцию на return 7: итог должен стать неуспешным, даже если другая задача завершилась нормально. Проверьте исчезновение временного каталога после обычного выхода. Эти пробы проверяют контракт; успешная проверка bash -n сама по себе этого не делает.

Источники