Использование getopts для обработки длинных и коротких командных опций
Проблема с обработкой длинных и коротких командных опций в shell-скрипте
Я хотел бы реализовать в моем shell-скрипте поддержку как длинных, так и коротких форм командных опций. Я знаю, что для этого можно использовать getopts
, как в Perl, но мне не удалось достичь такого же результата в shell.
Есть ли идеи о том, как это можно сделать, чтобы я мог использовать опции следующим образом:
./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/
В этих примерах обе команды должны означать одно и то же для моего скрипта, но с использованием getopts
я не смог реализовать подобный функционал. Спасибо за помощь!
5 ответ(ов)
getopt
и getopts
— это совершенно разные инструменты, и многие люди немного недопонимают, что они делают. getopts
— это встроенная команда в bash
для обработки опций командной строки в цикле и присвоения каждой найденной опции и ее значения встроенным переменным, чтобы вы могли дальше с ними работать. В отличие от этого, getopt
— это внешняя утилита, и она на самом деле не обрабатывает ваши опции так, как это делает, например, getopts
в shell, модуль Getopt
в Perl или модули optparse
/argparse
в Python. Все, что делает getopt
, — это канонизирует переданные опции, то есть преобразует их в более стандартный вид, чтобы облегчить обработку в скрипте. Например, применение getopt
может преобразовать следующее:
myscript -ab infile.txt -ooutfile.txt
вот так:
myscript -a -b -o outfile.txt infile.txt
Обработку вы должны выполнять самостоятельно. В принципе, вы можете вообще не использовать getopt
, если установите различные ограничения на способ указания опций:
- указывать только одну опцию на аргумент;
- все опции должны идти перед любыми позиционными параметрами (т.е. не опциональными аргументами);
- для опций с значениями (например,
-o
выше) значение должно идти как отдельный аргумент (после пробела).
Почему стоит использовать getopt
вместо getopts
? Основная причина — только GNU getopt
поддерживает длинные именованные опции командной строки.1 (GNU getopt
является стандартным в Linux. Mac OS X и FreeBSD поставляются с базовой и не очень полезной версией getopt
, но версию GNU можно установить; смотрите ниже.)
Например, вот пример использования GNU getopt
из моего скрипта с названием javawrap
:
# ПРИМЕЧАНИЕ: Это требует GNU getopt. На Mac OS X и FreeBSD вам нужно установить его
# отдельно; смотрите ниже.
TEMP=$(getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \
-n 'javawrap' -- "$@")
if [ $? != 0 ] ; then echo "Завершение..." >&2 ; exit 1 ; fi
# Обратите внимание на кавычки вокруг '$TEMP': они необходимы!
eval set -- "$TEMP"
VERBOSE=false
DEBUG=false
MEMORY=
DEBUGFILE=
JAVA_MISC_OPT=
while true; do
case "$1" in
-v | --verbose ) VERBOSE=true; shift ;;
-d | --debug ) DEBUG=true; shift ;;
-m | --memory ) MEMORY="$2"; shift 2 ;;
--debugfile ) DEBUGFILE="$2"; shift 2 ;;
--minheap )
JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;;
--maxheap )
JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;;
-- ) shift; break ;;
* ) break ;;
esac
done
Это позволяет вам задавать опции вроде --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt"
или подобные. Эффект от вызова getopt
заключается в канонизации опций в --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt"
, чтобы вам было проще их обрабатывать. Важно упомянуть о кавычках вокруг "$1"
и "$2"
, так как они обеспечивают правильную обработку аргументов с пробелами в них.
Если вы удалите первые 9 строк (все до строки с eval set
), код все равно будет работать! Однако, ваш код станет более требовательным к тому, какие опции он принимает: В частности, вам придется указывать все опции в "канонической" форме, описанной выше. При использовании getopt
, однако, вы можете группировать однобуквенные опции, использовать более короткие недвусмысленные формы длинных опций, применять как стиль --file foo.txt
, так и стиль --file=foo.txt
, использовать как -m 4096
, так и -m4096
, смешивать опции и не-опции в любом порядке и так далее. Кроме того, getopt
выдает сообщение об ошибке, если обнаружены нераспознанные или двусмысленные опции.
ПРИМЕЧАНИЕ: На самом деле существует две совершенно разные версии getopt
, базовая getopt
и GNU getopt
, с разными функциями и разными правилами вызова.2 Базовая getopt
довольно сильно ограничена: она не обрабатывает длинные опции, и не может справляться с вложенными пробелами внутри аргументов или пустыми аргументами, тогда как getopts
делает это корректно. Приведенный выше код не сработает с базовой getopt
. GNU getopt
по умолчанию установлен в Linux, но на Mac OS X и FreeBSD его нужно устанавливать отдельно. На Mac OS X установите MacPorts (http://www.macports.org), а затем выполните sudo port install getopt
для установки GNU getopt
(обычно в /opt/local/bin
), и убедитесь, что /opt/local/bin
находится в вашем пути перед /usr/bin
. На FreeBSD установите misc/getopt
.
Краткое руководство по изменению примерного кода для вашей программы: Из первых нескольких строк все — это "распечатка", которая должна остаться той же, кроме строки, вызывающей getopt
. Вы должны изменить название программы после -n
, указать короткие опции после -o
, и длинные опции после --long
. Поставьте двоеточие после опций, которые принимают значение.
Наконец, если вы видите код с просто set
вместо eval set
, это написано для BSD getopt
. Вам стоит изменить его на стиль eval set
, который работает нормально с обеими версиями getopt
, в то время как простой set
не работает правильно с GNU getopt
.
1На самом деле, getopts
в ksh93
поддерживает длинные именованные опции, но этот шелл используется реже, чем bash
. В zsh
используйте zparseopts
, чтобы получить эту функциональность.
2Технически, "GNU getopt
" — это неверный термин; эта версия на самом деле была написана для Linux, а не для проекта GNU. Тем не менее, она следует всем стандартам GNU, и термин "GNU getopt
" используется довольно часто (например, в FreeBSD).
Встроенная команда getopts
, насколько я знаю, по-прежнему ограничена только однобуквенными опциями.
Существовала (или все еще существует) внешняя программа getopt
, которая реорганизовывала набор опций так, чтобы их было легче анализировать. Вы могли бы адаптировать этот подход для работы с длинными опциями. Пример использования:
aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
case "$1" in
(-a) aflag=yes;;
(-b) bflag=yes;;
(-f) flist="$flist $2"; shift;;
(--) shift; break;;
(-*) echo "$0: ошибка - неопознанная опция $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
# Обработка оставшихся неопционных аргументов
...
Вы могли бы использовать аналогичную схему с командой getoptlong
.
Обратите внимание, что основная проблема с внешней программой getopt
заключается в трудности обработки аргументов с пробелами внутри них и в точном сохранении этих пробелов. Именно поэтому встроенная команда getopts
считается более надежной, хотя и ограничена тем, что обрабатывает только однобуквенные опции.
Вот пример использования getopt
с длинными опциями:
aflag=no
bflag=no
cargument=none
# Опции могут быть с одним двоеточием, чтобы указать, что у них есть обязательный аргумент
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$@")
then
# что-то пошло не так, getopt выведет сообщение об ошибке
exit 1
fi
set -- $options
while [ $# -gt 0 ]
do
case $1 in
-a|--along) aflag="yes" ;;
-b|--blong) bflag="yes" ;;
# для опций с обязательными аргументами требуется дополнительный shift
-c|--clong) cargument="$2" ; shift;;
(--) shift; break;;
(-*) echo "$0: ошибка - нераспознанная опция $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
Этот скрипт демонстрирует, как можно использовать getopt
для обработки коротких и длинных опций в bash. Важно помнить, что для опций с обязательными аргументами необходимо использовать дополнительный shift
, чтобы правильно передать значения.
Использование getopts
с короткими и длинными опциями и аргументами
Работает со всеми комбинациями, например:
- foobar -f --bar
- foobar --foo -b
- foobar -bf --bar --foobar
- foobar -fbFBAshorty --bar -FB --arguments=longhorn
- foobar -fA "text shorty" -B --arguments="text longhorn"
- bash foobar -F --barfoo
- sh foobar -B --foobar - ...
- bash ./foobar -F --bar
Некоторые определения для этого примера
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty
Как может выглядеть функция использования
function _usage()
{
###### U S A G E : Помощь и ОШИБКИ ######
cat <<EOF
foobar $Options
$*
Использование: foobar <[опции]>
Опции:
-b --bar Установить bar в yes ($foo)
-f --foo Установить foo в yes ($bart)
-h --help Показать это сообщение
-A --arguments=... Установить arguments в yes ($arguments) И получить ARGUMENT ($ARG)
-B --barfoo Установить barfoo в yes ($barfoo)
-F --foobar Установить foobar в yes ($foobar)
EOF
}
[ $# = 0 ] && _usage " >>>>>>>> не заданы опции "
getopts
с короткими/длинными флагами, а также длинными аргументами
while getopts ':bfh-A:BF' OPTION ; do
case "$OPTION" in
b ) sbar=yes ;;
f ) sfoo=yes ;;
h ) _usage ;;
A ) sarguments=yes;sARG="$OPTARG" ;;
B ) sbarfoo=yes ;;
F ) sfoobar=yes ;;
- ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
eval OPTION="\$$optind"
OPTARG=$(echo $OPTION | cut -d'=' -f2)
OPTION=$(echo $OPTION | cut -d'=' -f1)
case $OPTION in
--foo ) lfoo=yes ;;
--bar ) lbar=yes ;;
--foobar ) lfoobar=yes ;;
--barfoo ) lbarfoo=yes ;;
--help ) _usage ;;
--arguments ) larguments=yes;lARG="$OPTARG" ;;
* ) _usage " Длинная: >>>>>>>> недопустимые опции (длинные) " ;;
esac
OPTIND=1
shift
;;
? ) _usage "Короткая: >>>>>>>> недопустимые опции (короткие) " ;;
esac
done
Вывод
##################################################################
echo "----------------------------------------------------------"
echo "РЕЗУЛЬТАТ короткий-foo : $sfoo длинный-foo : $lfoo"
echo "РЕЗУЛЬТАТ короткий-bar : $sbar длинный-bar : $lbar"
echo "РЕЗУЛЬТАТ короткий-foobar : $sfoobar длинный-foobar : $lfoobar"
echo "РЕЗУЛЬТАТ короткий-barfoo : $sbarfoo длинный-barfoo : $lbarfoo"
echo "РЕЗУЛЬТАТ короткий-arguments: $sarguments с аргументом = \"$sARG\" длинный-arguments: $larguments и $lARG"
Объединение вышеизложенного в целостный скрипт
#!/bin/bash
# foobar: getopts с короткими и длинными опциями и аргументами
function _cleanup ()
{
unset -f _usage _cleanup ; return 0
}
## Очистка вложенных функций при выходе
trap _cleanup INT EXIT RETURN
###### некоторые определения для этого примера ######
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty
function _usage()
{
###### U S A G E : Помощь и ОШИБКИ ######
cat <<EOF
foobar $Options
$*
Использование: foobar <[опции]>
Опции:
-b --bar Установить bar в yes ($foo)
-f --foo Установить foo в yes ($bart)
-h --help Показать это сообщение
-A --arguments=... Установить arguments в yes ($arguments) И получить ARGUMENT ($ARG)
-B --barfoo Установить barfoo в yes ($barfoo)
-F --foobar Установить foobar в yes ($foobar)
EOF
}
[ $# = 0 ] && _usage " >>>>>>>> не заданы опции "
##################################################################
####### "getopts" с: короткими опциями И длинными опциями #######
####### И короткими/длинными аргументами #######
while getopts ':bfh-A:BF' OPTION ; do
case "$OPTION" in
b ) sbar=yes ;;
f ) sfoo=yes ;;
h ) _usage ;;
A ) sarguments=yes;sARG="$OPTARG" ;;
B ) sbarfoo=yes ;;
F ) sfoobar=yes ;;
- ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
eval OPTION="\$$optind"
OPTARG=$(echo $OPTION | cut -d'=' -f2)
OPTION=$(echo $OPTION | cut -d'=' -f1)
case $OPTION in
--foo ) lfoo=yes ;;
--bar ) lbar=yes ;;
--foobar ) lfoobar=yes ;;
--barfoo ) lbarfoo=yes ;;
--help ) _usage ;;
--arguments ) larguments=yes;lARG="$OPTARG" ;;
* ) _usage " Долгий: >>>>>>>> недопустимые опции (длинные) " ;;
esac
OPTIND=1
shift
;;
? ) _usage "Короткая: >>>>>>>> недопустимые опции (короткие) " ;;
esac
done
Другой способ...
# Перевод длинных опций в короткие
for arg
do
delim=""
case "$arg" in
--help) args="${args}-h ";;
--verbose) args="${args}-v ";;
--config) args="${args}-c ";;
# Передать всё остальное без изменений
*) [[ "${arg:0:1}" == "-" ]] || delim="\""
args="${args}${delim}${arg}${delim} ";;
esac
done
# Сбрасываем переведённые аргументы
eval set -- $args
# Теперь можно обрабатывать с помощью getopts
while getopts ":hvc:" opt; do
case $opt in
h) usage ;; # Показать справку
v) VERBOSE=true ;; # Включить режим подробного вывода
c) source $OPTARG ;; # Загрузить файл конфигурации
\?) usage ;; # Неверная опция
:)
echo "Опция -$OPTARG требует аргумент"
usage
;;
esac
done
Этот код переводит длинные опции командной строки (такие как --help
, --verbose
, и --config
) в короткие (например, -h
, -v
, -c
). В цикле for
обработчик каждого аргумента определяет, является ли он длинной опцией или чем-то другим, и формирует строку аргументов. Затем с помощью eval set -- $args
аргументы можно сбросить для дальнейшей обработки с использованием getopts
. В конце, в зависимости от переданных опций, выполняются соответствующие действия (например, выводится справка или загружается конфигурационный файл).
Как разобрать аргументы командной строки в Bash?
"Передача всех аргументов в Bash-скрипте"
Проверьте количество аргументов, переданных Bash-скрипту
Ошибка "Слишком длинный список аргументов" для команд rm, cp, mv
Пример использования getopts в bash