BASH: функция getopts — используем опции в скриптах

Автор: | 11/26/2013
 

terminalИмеется две схожие программы — getopt и getopts.

Основные различия — getopts является встроенной в bash командой, тогда как getopt — вызываемая внешняя (/usr/bin/getopt):

У getopt есть несколько недостатков, основная — getopts внесена в стандарт POSIX для sh, тогда как getopt может быть вообще не установлена в системе. Кроме того, это сравнительная сложность, которая в свою очередь вызывает проблемы со стабильностью работы и количеством вероятных ошибок.

С другой стороны — getopt имеет встроенный механизм обработки --longoption, вместо коротких опций типа -a. Однако — к использованию рекомендуется именно getopts, которую мы и рассмотрим далее.

Перейдём к сути вопроса, и немного опишем применение.

Всем приходилось сталкиваться с программами, которые могут выполнять различные действия в зависимости от указанных им опций. Например:

$ dig localhost

Вернёт результат выполнения прямого запроса к DNS:

;; ANSWER SECTION:
localhost.              600     IN      A       127.0.0.1

Тогда как будучи запущенной с опцией -x — выполнит обратный запрос, преобразуя IP-адрес в домен:

$ dig -x 127.0.0.1

;; ANSWER SECTION:
1.0.0.127.in-addr.arpa. 3600    IN      PTR     localhost.

Так опциями кардинально меняется выполнение одной и тот же утилиты.

Что бы использовать подобное поведение в собственных скриптах, и при этом не загромождать код ненужными проверками — достаточно выполнить в коде команду getopts.

Приведём самый простой пример:

$ cat getopts.sh
#!/bin/bash

while getopts "abc" opt
do
case $opt in
a) echo "Found option $opt";;
b) echo "Found option $opt";;
c) echo "Found option $opt";;
esac
done

Тут мы запускаем цикл, который выполняет команду getopts. В свою очередь getopts получает список допустимых опций — «abc» (обратите внимание — допустимые опции указываются без знака тире), и при нахождении одной из них — передаёт её в переменную $opt. Далее уже case выполняет действия, связанные с переданной опцией.

При попытке выполнить скрипт с недопустимой опцией — getopts выведет сообщение об ошибке:

$ ./getopts.sh -h
./getopts.sh: illegal option -- h

Можно отключить такое поведение (verbose mode), добавив символ двоеточия в начале списка допустимых опций:

...
while getopts ":abc" opt
do
...

Теперь getopts будет выполняться в silent mode:

$ ./getopts.sh -h
$

Усложним пример, и добавим проверку на:

1) обязательное наличие хотя бы одной опции при запуске скрипта;
2) вывод собственного сообщения об ошибке (можно заменить на выполнение какого-то действия), если получена неверная (не найденная в списке) опция:

$ cat getopts.sh
#!/bin/bash

if [ -z $* ]
then
echo "No options found!"
exit 1
fi

while getopts "abc" opt
do
case $opt in
a) echo "Found option $opt";;
b) echo "Found option $opt";;
c) echo "Found option $opt";;
*) echo "No reasonable options found!";;
esac
done

Оператор if и утилита test [] в начале проверяют есть ли какое-то содержимое в позиционных аргументах, переданных скрипту и, если ничего не найдено — скрипт выдаст сообщение об ошибке и завершит работу.

Последняя опция в case проверит все, не попавшие под предыдущие определения ключи — и выполнит соответствующее действие.

Для опций можно передавать аргументы. Что бы указать, что для опции аргумент обязателен — установите двоеточие : после её имени в списке опций:

while getopts "a:bc" opt

Теперь, опция -a будет требовать обязательного указания аргумента.

Тут надо упомянуть о специальных переменных, используемых getopts:

$OPTIND — хранит «внутренний индекс», по которому getopts определяет очередность выполнения опций;
$OPTARG — содержит аргумент, передаваемый опции;
$OPTERR — содержит код ошибки, обычно 1.

Изменим наш скрипт, для использования аргумента опцией -a:

$ cat getopts.sh
#!/bin/bash

if [ $# -lt 1 ]
then
echo "No options found!"
exit 1
fi

while getopts "a:b" opt
do
case $opt in
a) echo "Found option $opt"
echo "Found  argument for option $opt - $OPTARG"
;;
b) echo "Found option $opt";;
*) echo "No reasonable options found!";;
esac
done

Во-первых — мы изменили проверку на наличие опций, теперь проверяется «количество опций больше чем 1».

Далее — мы добавили обязательное условие при вызове опции -a — наличие аргумента (a:). И — добавили вывод на консоль самого аргумента, при указании опции -a.

Посмотрим, как это будет работать:

$ ./getopts.sh
No options found!
$ ./getopts.sh -a
./getopts.sh: option requires an argument -- a
No reasonable options found!
$ ./getopts.sh -a arg
Found option a
Found  argument for ption a - arg

Тоже можно сделать для второй опции — -b:

case $opt in
a) echo "Found option $opt"
echo "Found  argument for ption $opt - $OPTARG"
;;
b) echo "Found option $opt"
echo "Found  argument for ption $opt - $OPTARG"
;;
*) echo "No reasonable options found!";;
esac
$ ./getopts.sh -a arg1 -b arg2
Found option a
Found  argument for ption a - arg1
Found option b
Found  argument for ption b - arg2

Заметьте, что getopts сам заменяет значение $OPTARG в зависимости от обрабатываемой опции — тут и используется переменная $OPTIND.

Однако, тут имеется подводный камень — в случае, если «забыть» указать аргумент для первой опции — вторая опция будет воспринята как аргумент для первой:

$ ./getopts.sh -a -b
Found option a
Found  argument for ption a - -b

Для решения этой проблемы единственный вариант, который пришёл  в голову — это добавить функцию, которая будет проверять корректность переданных аргументов:

checkargs () {
if [[ $OPTARG =~ ^-[a/b]$ ]]
then
echo "Unknow argument $OPTARG for option $opt!"
exit 1
fi
}

Как видно, функция проверяет наличие любой из указанных опций (регулярное выражение ^-[a/b]$ — принять -a или -b ), и в случае нахождения строки — выдаст соответствующее сообщение.

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

case $opt in
a) checkargs
echo "Found option $opt"
echo "Found  argument for ption $opt - $OPTARG"
;;
b) checkargs
echo "Found option $opt"
echo "Found  argument for ption $opt - $OPTARG"
;;
*) echo "No reasonable options found!";;
esac

И посмотрим, что получилось:

$ ./getopts.sh -a -b
Unknow argument -b for option a!
$ ./getopts.sh -b -a
Unknow argument -a for option b!
$ ./getopts.sh -a arg1 -b arg2
Found option a
Found  argument for ption a - arg1
Found option b
Found  argument for ption b - arg2

Кстати, одним из достоинств getopts является то, что вам не надо заботиться о позициях передаваемых аргументов:

$ ./getopts.sh -b arg2 -a arg1
Found option b
Found  argument for ption b - arg2
Found option a
Found  argument for ption a - arg1

Так же, не надо беспокоиться о наличии тире перед каждой опцией (тут мы уберём проверку наличия аргументов):

$ ./getopts.sh -ba
Found option b
Found option a
$ ./getopts.sh -b -a
Found option b
Found option a
$ ./getopts.sh -ab
Found option a
Found option b

И, напоследок, пример из рабочего скрипта — та часть, которая демонстрирует работу getopts:

[...]
while getopts "x:hlcp" opt; do
 case $opt in
  h)
   usage && exit 1
   ;;
  l)
   last=1
   current=""
   ;;
  c)
   current=1
   last=""
   ;;
  p)
   prints=1
   exports=""
   ;;
  x)
   exports=1
   prints=""
   ;;
  ?)
   usage && exit 1
   ;;
 esac
done

[...]

while [[  $prints ]]
do
if [[ $last ]]
then
 echo -e "nLast available version in repository is: "$LAST"n"
fi

if [[ $current ]]
then
 echo -e "nCurrent available version in repository is: "$CURR"n"
fi
break
done
[...]

Ссылки по теме

http://wiki.bash-hackers.org
http://programmingexamples.net
http://mywiki.wooledge.org