5

Использование getopts для обработки длинных и коротких командных опций

12

Проблема с обработкой длинных и коротких командных опций в shell-скрипте

Я хотел бы реализовать в моем shell-скрипте поддержку как длинных, так и коротких форм командных опций. Я знаю, что для этого можно использовать getopts, как в Perl, но мне не удалось достичь такого же результата в shell.

Есть ли идеи о том, как это можно сделать, чтобы я мог использовать опции следующим образом:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

В этих примерах обе команды должны означать одно и то же для моего скрипта, но с использованием getopts я не смог реализовать подобный функционал. Спасибо за помощь!

5 ответ(ов)

4

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).

1

Встроенная команда 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 считается более надежной, хотя и ограничена тем, что обрабатывает только однобуквенные опции.

0

Вот пример использования 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, чтобы правильно передать значения.

0

Использование 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
0

Другой способ...

# Перевод длинных опций в короткие
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. В конце, в зависимости от переданных опций, выполняются соответствующие действия (например, выводится справка или загружается конфигурационный файл).

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь