Обработка длинных опций с помощью getopts

Обработка длинных опций с помощью getopts

Я анализирую параметры, getoptsно хотел бы также обрабатывать длинные параметры.

print-args ()
{
 title="$1" ; shift
 printf "\n%s\n" "${title}: \$@:"
 for arg in "$@"; do
   (( i = i + 1 ))
   printf "%s |%s|\n" "${i}." "$arg"
 done
}

getopts_test ()
{
 aggr=()
 for arg in "$@"; do
   case $arg in
    ("--colour"|"--color")     aggr+=( "-c" ) ;;
    ("--colour="*|"--color="*) aggr+=( "-c" "${arg#*=}" ) ;;
    (*)  aggr+=( "$arg" ) ;;
   esac
 done

 print-args "print" "$@"

 eval set -- "${aggr[@]}"
 print-args "eval" "$@"

 set -- "${aggr[@]}"
 print-args "set" "$@"

 local OPTIND OPTARG
 local shortopts="C:"
 while getopts "$shortopts" arg; do
   case $arg in
    ("c") context="$OPTARG" ;;
    (*) break ;;
   esac
 done
 shift $(( OPTIND - 1 ))
}

set -- "${aggr[@]}"Но мне интересно , правильно ли используется .

Или более уместен следующий вариант (использование eval)?

eval set -- "${aggr[@]}"

Я выполнил тест, показанный ниже. С eval строка "Gunga Din" разделяется, тогда как с set -- "${aggr[@]}", она корректно анализируется как одна строка.

getopts_test -f -g 130 --colour="170 20" "Gunga Din"

print: $@:
1. |-f|
2. |-g|
3. |130|
4. |--colour=170 20|
5. |Gunga Din|

eval: $@:
1. |-f|
2. |-g|
3. |130|
4. |-c|
5. |170|
6. |20|
7. |Gunga|
8. |Din|

set: $@:
1. |-f|
2. |-g|
3. |130|
4. |-c|
5. |170 20|
6. |Gunga Din|

Затем я запустил другую функцию, которая использует не-GNU getopt.

getopt_test ()
{
 shortopts="Vuhv::H::w::e::n::l::C:"
 shortopts="${shortopts}bgcrmo"
 longopts="version,usage,help,verbosity::"
 longopts="${longopts},heading::,warning::,error::"
 longopts="${longopts},blu,grn,cyn,red,mgn,org"
 
 opts=$( getopt -o "$shortopts" -l "$longopts" -n "${0##*/}" -- "$@" )

 print-args "\$@:" "$@"
 print-args "opts:" "$opts"

 set -- "$opts"
 print-args "set -- \"$opts\"" "$@"

 eval set -- "$opts"
 print-args "eval set -- \"$opts\"" "$@"

}

Это привело к следующему

getopt_test --warning=3 "foo'bar" "Gunga Din"

$@:
1. |--warning=3|
2. |foo'bar|
3. |Gunga Din|

opts:
1. | --warning '3' -- 'foo'\''bar' 'Gunga Din'|

set -- "$opts"
1. | --warning '3' -- 'foo'\''bar' 'Gunga Din'|

eval set -- "$opts"
1. |--warning|
2. |3|
3. |--|
4. |foo'bar|
5. |Gunga Din|

Как показано, результатом getopt является одна запись с переупорядоченными позиционными аргументами. Это показывает необходимость использования eval set -- "$opts"для разделения позиционных аргументов в optsстроке на пять записей для разбора и обработки опций.

решение1

Идея заключается в предварительной обработке аргументов и изменении каждого --contextна -Cкоторый getoptsзатем можно обработать? Я полагаю, что это сработает, но учтите, что длинные параметры в стиле GNU также могут принимать аргументы в формате --context=foobar, а ваша конструкция здесь не поддерживает это. Пользователь должен знать, что этот конкретный инструмент здесь требует --context foobarдва отдельных аргумента. Или вам нужно будет сделать предварительную обработку более сложной.

Вы также можете проверить все аргументы, которые начинаются с --, так как в противном случае, например, опечатка --cotnextбудет отправлена getopts​​как есть, и вы получите жалобы на неизвестные параметры. (Или, что еще хуже, будут включены неправильные параметры.)

set -- "${aggr[@]}"Но мне интересно , правильно ли используется .

Или более уместен следующий вариант (с использованием eval)?

set -- "${aggr[@]}"расширяет элементы массива до отдельных слов, а затем назначает эти слова позиционным параметрам. Каждый элемент массива станет ровно одним позиционным параметром, без изменений.

eval set -- "${aggr[@]}"будет расширять все элементы массива, затем объединять их вместе пробелами, добавлять set --и оценивать результат как команду оболочки. То есть, если у вас есть элементы массива abc def, $(date >&2), ghi'jkl, команда будет

set -- abc def $(date >&2) ghi'jkl 

что в итоге приведет к получению abcи defкак двух отдельных параметров, и выведет дату на stderr, за исключением того, что одинарная кавычка вызовет синтаксическую ошибку.

Использование evalбудет уместным, если у вас есть что-то, предназначенное для создания выходных данных, которые указаны для ввода в оболочку.


Если вы используете Linux (и вас не волнует переносимость), вы можете сделать то, что roaima предложил в комментариях, и использовать версию util-linux getopt(без s). Она также поддерживает длинные параметры, есть ответы, показывающие, как использовать ее вgetopt, getopts или ручной парсинг — что использовать, если я хочу поддерживать как короткие, так и длинные параметры?И вэтот ТАК ответа такжемой ответ здесь.

Кстати, с этим getoptвыбыиспользуйте eval, так как в качестве команды она ограничена созданием только одной строки в качестве вывода, а не списка, как массив, поэтому она использует кавычки оболочки, чтобы обойти эту проблему.

решение2

Вы можете разобрать --fooдлинные опции в стиле -style с помощью getoptsвстроенной функции, добавив -в качестве короткой опции аргумент в optstring и извлекая фактическую длинную опцию из $OPTARG. Простой пример:

while getopts :sc:-: o; do
    case $o in
    :) echo >&2 "option -$OPTARG needs an argument"; continue;;
    '?') echo >&2 "unknown option -$OPTARG"; continue;;
    -) o=${OPTARG%%=*}; OPTARG=${OPTARG#"$o"}; OPTARG=${OPTARG#=};;
    esac
    echo "OPT $o=$OPTARG"
done
shift "$((OPTIND - 1))"
echo "ARGS $*"

который затем можно использовать как script -c fooили script --context=foo.

Если вы также хотите, чтобы длинные опции проверялись так же, как и короткие, и также принимали сокращенные формы, вам нужно что-то более сложное. Нет особой мудрости в том, чтобы переусложнять плохой скрипт оболочки, как этот, но если вам нужен пример, вот он:

short_opts=sc:
long_opts=silent/ch/context:/check/co   # those who take an arg END with :

# override via command line for testing purposes
# if [ "$#" -ge 2 ]; then
#   short_opts=$1; long_opts=$2; shift 2
# fi

while getopts ":$short_opts-:" o; do
    case $o in
    :) echo >&2 "option -$OPTARG needs an argument" ;continue;;
    '?') echo >&2 "bad option -$OPTARG" ;continue;;
    -)  o=${OPTARG%%=*}; OPTARG=${OPTARG#"$o"}; lo=/$long_opts/
        case $lo in
        *"/$o"[!/:]*"/$o"[!/:]*) echo >&2 "ambiguous option --$o"; continue;;
        *"/$o"[:/]*) ;;
        *) o=$o${lo#*"/$o"}; o=${o%%[/:]*} ;;
        esac
        case $lo in
        *"/$o/"*) OPTARG= ;;
        *"/$o:/"*)
            case $OPTARG in
            '='*)   OPTARG=${OPTARG#=};;
            *)  eval "OPTARG=\$$OPTIND"
                if [ "$OPTIND" -le "$#" ] && [ "$OPTARG" != -- ]; then
                    OPTIND=$((OPTIND + 1))
                else
                    echo >&2 "option --$o needs an argument"; continue
                fi;;
            esac;;
        *) echo >&2 "unknown option --$o"; continue;;
        esac
    esac
    echo "OPT $o=$OPTARG"
done
shift "$((OPTIND - 1))"
echo "ARGS $*"

затем

$ ./script --context=33
OPT context=33
$ ./script --con=33
OPT context=33
$ ./script --co
OPT co=
$ ./script --context
option --context needs an argument

Связанный контент