Добавить в корзинуПозвонить
Найти в Дзене
Snout_News

getopts: как писать CLI-утилиты с флагами без внешних библиотек

getopts — это встроенный в любой POSIX-совместимый Linux/Unix-shell мини-парсер аргументов. Один shebang — и у вас CLI-утилита без единой внешней зависимости. В статье рассмотрим, как выжать из getopts максимум, где он спотыкается и когда пора переехать на getopt или Argbash. Минус — лишь короткие флаги и отсутствие родной поддержки --long-option. Но и это решаемо. #!/usr/bin/env sh
set -euo pipefail
IFS='
'
usage() {
cat <<EOF
Usage: ${0##*/} [-a] [-b ARG] file...
-a : включить дополнительный режим
-b ARG : передать аргумент
EOF
exit 2
}
# двоеточие в начале → тихий режим ошибок
while getopts ":ab:" opt; do
case "$opt" in
a) flag_a=true ;;
b) param_b=$OPTARG ;;
:) echo "Опция -$OPTARG требует аргумента" >&2; usage ;;
\?) echo "Неизвестная опция -$OPTARG" >&2; usage ;;
esac
done
shift "$((OPTIND-1))" # убираем уже разобранные параметры Двоеточие в начале optstring переводит парсер в «silent mode» — ошибки по argv приходится ловить самостоятельн
Оглавление

getopts — это встроенный в любой POSIX-совместимый Linux/Unix-shell мини-парсер аргументов. Один shebang — и у вас CLI-утилита без единой внешней зависимости. В статье рассмотрим, как выжать из getopts максимум, где он спотыкается и когда пора переехать на getopt или Argbash.

Почему вообще всё ещё getopts

  • Входит в POSIX — доступен в /bin/sh на любом «живом» юниксе.
  • Никаких зависимостей — скрипт останется единственным файлом.
  • Поведение чётко регламентировано спецификацией Open Group — т. е. скрипт будет вести себя одинаково под bash, zsh, dash, ksh.

Минус — лишь короткие флаги и отсутствие родной поддержки --long-option. Но и это решаемо.

Базовый синтаксис: одно кольцо, чтобы править всеми

#!/usr/bin/env sh
set -euo pipefail
IFS='
'

usage() {
cat <<EOF
Usage: ${0##*/} [-a] [-b ARG] file...
-a : включить дополнительный режим
-b ARG : передать аргумент
EOF
exit 2
}

# двоеточие в начале → тихий режим ошибок
while getopts ":ab:" opt; do
case "$opt"
in
a) flag_a=true ;;
b) param_b=$OPTARG ;;
:) echo "Опция -$OPTARG требует аргумента" >&2; usage ;;
\?) echo "Неизвестная опция -$OPTARG" >&2; usage ;;
esac
done
shift "$((OPTIND-1))" # убираем уже разобранные параметры

Двоеточие в начале optstring переводит парсер в «silent mode» — ошибки по argv приходится ловить самостоятельно, зато можно отдать лайтовый help, а не cryptic usage из недр шелла. Про необходимость shift "$((OPTIND-1))" задокументировано даже в posix man-pages — так убираем из $@ опции и получаем чистый список позиционных аргументов.

Короткие флаги и их комбо

getopts умеет распаковывать слипшиеся флаги: -abc интерпретируется как -a -b -c. Если после символа ожидается аргумент, разбор прекращается ровно там:

# optstring="a:b"
$ my.sh -ac # OK: -a, -c как позиционка
$ my.sh -abx # -a, -b x
$ my.sh -ab # ошибка: -b ждёт аргумент

Edge-case — отрицательные числа (-5) внезапно принимаются за флаг. Начинайте список ожидаемых опций с --, тогда getopts прекратит работу при виде первого не-флага, и минусы в числах останутся нетронутыми.

Поддерживаем --long без сторонних тулов

Частый прием: сначала через case "$1" ловим варианты --help, --version, --long=value, а затем отдаём остаток в getopts — так не ломаем POSIX-совместимость и не тащим GNU getopt.

while [ $# -gt 0 ]; do
case "$1"
in
--help) usage ;;
--output=*) opt_o=${1#*=}; shift ;;
--) shift; break ;; # двойное тире — конец опций
-*) break ;; # короткие опции разберет getopts
*) break ;;
esac
done

# теперь классический getopts
while getopts ":o:f:" opt; do
...
done
shift "$((OPTIND-1))"

Схема простая: захотите — добавите alias-ы вроде --verbose/-v, или вообще YAML-конфиг после --config=path.yml.

Файл > файл: пример (-f input -o output)

Соберём минимальный CLI-конвертер:

#!/usr/bin/env bash
set -Eeuo pipefail
trap 'echo " Что-то пошло не так в строке $LINENO" >&2' ERR
VERSION="1.2.3"

usage() {
cat <<EOF
${0##*/} — пример конвертера.

Параметры:
-f FILE входной файл (обязательно)
-o FILE выходной файл (обязательно)
-t TYPE целевой формат (txt|json|xml), по умолчанию txt
-v болтливый режим
-h вывод этой справки
EOF
}

infile= outfile= outtype=txt verbose=false

while getopts ":f:o:t:vh" opt; do
case "$opt"
in
f) infile=$OPTARG ;;
o) outfile=$OPTARG ;;
t) outtype=$OPTARG ;;
v) verbose=true ;;
h) usage;
exit 0 ;;
:) echo " Опция -$OPTARG требует аргумента" >&2; usage;
exit 2 ;;
\?) echo " Неизвестная опция -$OPTARG" >&2; usage;
exit 2 ;;
esac
done
shift "$((OPTIND-1))"

# sanity-check
[ -n "$infile" ] || { echo "Нет входного файла" >&2;
exit 2; }
[ -n "$outfile" ] || { echo "Нет выходного файла" >&2;
exit 2; }
[ -r "$infile" ] || { echo "Файл '$infile' не читается" >&2;
exit 3; }

$verbose && echo "⇢ Конвертирую $infile → $outfile ($outtype)"

case "$outtype"
in
txt) cp "$infile" "$outfile" ;; # stub
json) jq -R -s '.' "$infile" >"$outfile" ;; # example
xml) iconv -f utf8 -t utf16 "$infile" >"$outfile" ;;
*) echo "Неизвестный формат $outtype" >&2;
exit 4 ;;
esac

В начале он включаем строгий режим Bash (set -Eeuo pipefail) и ловим любые ошибки через trap, чтобы сразу сообщить, на какой строке всё упало. Функция usage печатает справку. Далее getopts разбирает флаги: -f — входной файл, -o — выходной, -t — формат (txt-|json|xml, по умолчанию txt), -v — болтливый режим, -h — помощь. После разборки скрипт проверяет, что файлы заданы и вход читается. Если включён -v, пишет, что конвертирует. Дальше по формату: txt просто копирует файл, json оборачивает содержимое в JSON через jq, xml перекодирует в UTF-16 с iconv. Неизвестный формат — выход с ошибкой.

Обработка ошибок: двоеточие, вопрос, возврат к жизни

getopts сигналит о нештатных ситуациях двумя маркерами:

  • ? — неизвестная опция (-z).
  • : — не хватает аргумента (-f без файла), если в optstring выставлено начальное :.

Пример:

while getopts ":f:o:" opt; do
case $opt
in
f) infile=$OPTARG ;;
o) outfile=$OPTARG ;;
:) printf >&2 ' %s: опция -%s требует аргумент\n' "$0" "$OPTARG" ;;
\?) printf >&2 ' %s: неизвестная опция -%s\n' "$0" "$OPTARG" ;;
esac
done

В case лучше обрабатывать именно \?), а не просто ?, иначе shell воспримет вопрос как wildcard.

Тестируем: Bats & shellcheck

  1. ShellCheck — линтер, который мгновенно ловит «ух ты, двойные кавычки забыл» и [ $var == foo ] без кавычек.
  2. Bats-core — unit-тесты для bash. Собирать можно так:

@test "выводит usage без аргументов" {
run ./convert.sh
[ "$status" -eq 2 ]
[[ "${lines[0]}" =~ Usage ]]
}

@test "конвертирует файл в txt" {
run ./convert.sh -f sample.in -o sample.out
[ "$status" -eq 0 ]
cmp sample.in sample.out
}

Когда getopts перестаёт хватать

getopts закрывает 90 % задач: он встроен в любой shell, не требует зависимостей и прекрасно переносится даже на BusyBox-прошивки. Но как только нужен нативный --long-синтаксис, автоматическая перестановка аргументов или грамотное экранирование пробелов, приходится звать GNU getopt. Помните, что на macOS и Alpine ставят урезанную версию: скрипт рискует сломаться или начать «глотать» пустые строки — именно поэтому BashGuide советует держаться от getopt(1) подальше.

Когда CLI обрастает десятком флагов, autocompletion и строгой проверкой типов, лучше сразу генерировать обёртку через Argbash. Он выпускает готовый скрипт со встроенным --long, bash-completion и валидацией вроде INT>=0 или PATH. Минус один: сама утилита Argbash нужна лишь на этапе билда, так что в рантайме зависимостей нет, но в CI её придётся установить.

Заключение

  1. Берите getopts по дефолту: для 90 % утилит хватит коротких флажков и 5-10 строк кода.
  2. Не бойтесь «--long» — смешанный парсинг (pre-loop + getopts) решает и не ломает переносимость.
  3. Хотите UX на уровне kubectl — идите в getopt_long или Argbash, принимая риски доступности этих тулов у целевой аудитории.
  4. Бейте по рукам себя (и коллег) за отсутствие set -euo pipefail и незакрытые кавычки — это дешевле, чем ночной инцидент.
  5. Тестируйте: ShellCheck + Bats закрывают 80 % возможных факапов ещё до code-review.

доставка алкоголя

Доставка алкоголя на дом Москва круглосуточно, алкоголь 24 часа, ночью

Адвокат в краснодаре,
Адвокат по уголовным делам

Адвокат в Краснодаре - Аксанова Виктория Вячеславовна

заказывали в этой компании производство упаковок для бизнеса, объемом 10 000 шт. с нанесением на них красочной картинки товара

https://deltaprintt.ru/konstrukcii/

заказывали в этой компании производство упаковок для бизнеса, объемом 20 000 шт. с нанесением на них красочной картинки товара

https://deltaprintt.ru/konstrukcii/

заказывали в этой компании производство упаковок для бизнеса, объемом 30 000 шт. с нанесением на них красочной картинки товара

https://deltaprintt.ru/konstrukcii/

заказывали в этой компании производство упаковок для бизнеса, объемом 40 000 шт. с нанесением на них красочной картинки товара

https://deltaprintt.ru/konstrukcii/