Функция с getopts работает только первый раз: локализуем OPTIND

Проверяющую функцию запускают дважды: сначала для staging, затем для production. Первый вызов принимает -n и печатает нужное число повторов. Второй ведёт себя так, будто параметров вообще не передали. В отдельном исполняемом скрипте этот же код работал: каждый запуск получал новую оболочку и новое состояние getopts.

Внутри одной оболочки getopts хранит позицию разбора в OPTIND. После окончания первого списка позиция остаётся за последним обработанным параметром. При втором вызове функция получает другой $@, но getopts не обязан автоматически начинать сначала. Для Bash-функции удобно локализовать OPTIND и сразу установить его в 1.

Сделаем каждый вызов независимым

Пример обрабатывает единственный параметр -n и затем печатает оставшиеся позиционные значения. Он не выполняет внешние операции, поэтому два вызова можно безопасно сравнить.

#!/usr/bin/env bash
inspect_batch() {
    local OPTIND=1 OPTARG opt repeats=1
    while getopts ':n:' opt; do
        case "$opt" in
            n) repeats=$OPTARG ;;
            :) printf 'Нет значения -%s\n' "$OPTARG" >&2; return 2 ;;
            \?) printf 'Неизвестный параметр -%s\n' "$OPTARG" >&2; return 2 ;;
        esac
    done
    shift "$((OPTIND - 1))"
    printf 'repeats=%s; arguments=%s\n' "$repeats" "$#"
}
inspect_batch -n 2 staging
inspect_batch -n 5 production

Первая строка результата содержит repeats=2, вторая repeats=5. В обоих случаях остаётся один позиционный аргумент. Если удалить локальную инициализацию OPTIND, второй вызов сможет начать разбор с позиции, оставленной первым. Это и есть проверяемая поломка, а не особенность параметра -n.

local действует внутри функции Bash. После return вызывающий код получает своё прежнее значение OPTIND. Это особенно полезно, если верхний скрипт тоже разбирает параметры: вспомогательная функция не должна сдвигать курсор его парсера. Локализованы также OPTARG и opt, чтобы служебные значения не протекали наружу.

Разбор и валидация — разные этапы

getopts устанавливает, что у -n есть аргумент. Он не доказывает, что передано положительное целое число. Значение five попадёт в repeats так же, как 5. Если число управляет циклом повторов, после разбора проверьте допустимый диапазон и только затем запускайте работу. Разместите эту проверку до первого побочного эффекта.

В начале optstring стоит двоеточие. Оно включает режим, где функция сама формирует сообщения об отсутствующем значении и неизвестной опции. Для отсутствующего значения opt получает :, а OPTARG содержит имя параметра. Для неизвестного параметра используется ?. Такой интерфейс удобно сохранять одинаковым при повторных вызовах.

После цикла shift удаляет уже разобранные параметры. Не выполняйте его до сохранения нужных значений или вне функции, если вызывающему коду нужен исходный список. В примере каждая функция имеет собственные позиционные параметры, поэтому изменение $@ остаётся локальным вызову.

Почему перезапуск процесса скрывал ошибку

При каждом старте shell-скрипта OPTIND инициализируется заново. Поэтому тест «запустить файл два раза» не проверяет повторное использование функции. Нужен именно один процесс, два последовательных вызова и разные значения параметров. Добавьте третий вызов без -n: он должен вернуть значение по умолчанию, а не повторить предыдущие 5.

Этот способ использует Bash local и не является готовым рецептом для любой POSIX-оболочки. Если библиотека обязана работать в sh, выберите явное сохранение и восстановление состояния либо отдельный процесс разбора. Важно закрепить контракт среды в shebang и способе подключения библиотеки.

Откройте программу Linux и Bash для DevOps-инженеров. Практика безопасных скриптов помогает перейти от команды для одного запуска к повторно используемым функциям, где аргументы, состояние и сообщения об ошибках остаются под контролем вызывающего кода.

Ещё по теме

Продолжить на практике

  • Linux и Bash для DevOps-инженеров ↗

    Linux для работы с серверами: процессы, права, systemd, сеть, диски и логи. Автоматизируйте повседневные задачи на Bash и разберите типичные сбои сервиса — от занятого порта до переполненного диска.