Привет, Хабр!
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
ShellCheck — линтер, который мгновенно ловит «ух ты, двойные кавычки забыл» и
[ $var == foo ]
без кавычек.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 её придётся установить.
Заключение
Берите
getopts
по дефолту: для 90 % утилит хватит коротких флажков и 5-10 строк кода.Не бойтесь «--long» — смешанный парсинг (pre-loop +
getopts
) решает и не ломает переносимость.Хотите UX на уровне
kubectl
— идите вgetopt_long
или Argbash, принимая риски доступности этих тулов у целевой аудитории.Бейте по рукам себя (и коллег) за отсутствие
set -euo pipefail
и незакрытые кавычки — это дешевле, чем ночной инцидент.Тестируйте: ShellCheck + Bats закрывают 80 % возможных факапов ещё до code-review.
Всех желающих приглашаем на открытый урок «Память в Linux: Cache, swap, dirty pages», который пройдёт 16 июня в 20:00, в преддверии курса «Administrator Linux. Professional», стартующего 26 июня.
Для проверки своего уровня рекомендуем пройти бесплатное тестирование.