Можно ли сделать аргументы опций bash-скрипта необязательными?

Вопрос или проблема

Я бы хотел, чтобы работали оба этих ввода. То есть, сам параметр -n является необязательным – я уже знаю, как это сделать – но затем он может иметь необязательный параметр сверху. Если параметр не указан, будет применено значение по умолчанию.

command -n 100
command -n

Я могу заставить работать только первый тип ввода или последний, но не оба.

HAS_NICE_THINGS=0
NICE_THINGS=50       # значение по умолчанию.

while getopts n: option; do
#while getopts n option; do    # NICE_THINGS всегда будет иметь это значение по умолчанию.
#while getopts nn: option; do    # то же самое.
    case "${option}" in
    n)
        HAS_NICE_THINGS=1
        if [[ ! -z "${OPTARG}" ]] && (( "${OPTARG}" > 0 )) && (( "${OPTARG}" <= 100 )); then
            NICE_THINGS=${OPTARG}
        fi;;
    esac
done

# сообщение об ошибке:
# параметр требует аргумент -- n

Я пока не совсем уверен, нужно ли мне логическое значение для моего скрипта, но на всякий случай я его фиксирую (HAS_NICE_THINGS).

Конечная цель, которую я имел в виду, заключалась в установке качества JPG при сохранении изображения. Хотя я могу представить, что эта конструкция будет полезна и в других местах.

Я использую Ubuntu 18.04.5 и GNU bash, версия 4.4.20(1)-release (x86_64-pc-linux-gnu).

Не разумно использовать getopts от Bash/POSIX, но вы могли бы сделать это с помощью "усовершенствованного" getopt (без s) из util-linux или Busybox. (И только с ними, в частности многие "традиционные" реализации getopt сломаны насчет пробелов.)

В мануале говорится о getopts:

optstring содержит символы опций, которые нужно распознать; если символ следует за двоеточием, ожидается, что у опции будет аргумент, который должен быть отделен от нее пробелом.

В нем не упоминаются необязательные аргументы опций.

Конечно, вы могли бы иметь другую необязательную опцию, чтобы задать значение не по умолчанию. Например, пусть -n не принимает аргумент и просто активирует приятные вещи, а -N <arg> принимает аргумент, включает приятные вещи и устанавливает значение.

Например, что-то вроде этого:

#!/bin/bash
HAS_NICE_THINGS=0
NICE_THINGS_VALUE=50
        
while getopts nN: option; do
    case "${option}" in
    n)
        HAS_NICE_THINGS=1
        shift;;
    N)
        HAS_NICE_THINGS=1
        NICE_THINGS_VALUE=$OPTARG
        shift; shift;;
    esac
done

if [ "$HAS_NICE_THINGS" = 1 ]; then
    echo "приятные вещи включены со значением $NICE_THINGS_VALUE"
fi

это даст

$ bash nice-getotps.sh -n
приятные вещи включены со значением 50
$ bash nice-getopts.sh -N42
приятные вещи включены со значением 42

Утилита getopt из util-linux принимает необязательные аргументы опций с помощью двойного двоеточия. Это немного неудобно использовать, и вам нужно будет поработать с eval, но сделав это правильно, кажется, что все работает.

Страница мануала:

-o shortopts [...] Каждый короткий символ опции в shortopts может быть последован одним двоеточием, чтобы указать, что у него есть обязательный аргумент, и двумя двоеточиями, чтобы указать, что у него есть необязательный аргумент.

С помощью скрипта, который просто печатает сырые значения, чтобы мы могли проверить, работает ли это правильно (getopt-optional.sh):

#!/bin/bash
getopt -T
if [ "$?" -ne 4 ]; then
    echo "неправильная версия 'getopt', выход..." >&2
    exit 1
fi 
params="$(getopt -o an:: -- "$@")"
eval set -- "$params"
while [ "$#" -gt 0 ]; do
    case "$1" in
    -n)
        echo "опция -n с аргументом '$2'"
        shift 2;;
    -a)
        echo "опция -a"
        shift;;
    --) 
        shift
        break;;
     *) 
        echo "что-то другое: '$1'"
        shift;;
    esac
done
echo "оставшиеся аргументы ($#):"
printf "<%s> " "$@"
echo

мы получим

$ bash getopt-optional.sh -n -a
опция -n с аргументом ''
опция -a
оставшиеся аргументы (0):
<> 
$ bash getopt-optional.sh -n'blah blah' -a
 -n 'blah blah' -a --
опция -n с аргументом 'blah blah'
опция -a
оставшиеся аргументы (0):
<> 

Отсутствие аргумента для -n отображается как пустой аргумент.

Не думайте, что вы могли бы передать явный пустой аргумент, поскольку аргумент опции должен быть внутри того же аргумента командной строки, что и сама опция, и -n то же самое, что и -n"" после удаления кавычек. Это делает необязательные аргументы опций неудобными в использовании, потому что вам нужно использовать -nx, поскольку -n x будет воспринято как опция -n (без аргумента опции), за которой следует обычный аргумент командной строки x. Это отличается от того, что произошло бы, если бы -n требовал обязательный аргумент опции.

Больше об getopt в ответе на Stackoverflow на Как разобрать аргументы командной строки в Bash?


Учтите еще раз, что поддержка необязательных аргументов опций ограничена
конкретными реализациями getopt, которые чаще всего встречаются в системах Linux. Другие реализации getopt даже не поддерживают пробелы в аргументах, так как полагаются на разбиение слов args=$(getopt...); set -- $args.

"Усовершенствованные" версии getopt имеют опцию -T, чтобы протестировать поддержку этих усовершенствований. Существуют некоторые обсуждения ограничений и особенностей работы с getopt здесь:
getopt, getopts или ручной разбор - что использовать, когда я хочу поддерживать как короткие, так и длинные опции?

Кроме того, getopt не является стандартным инструментом, как getopts (не уверен, связано ли это именно с этими ограничениями и отличиями).

Допустим, мы хотим предоставить опцию -a, которая принимает необязательный аргумент опции. Использование этой опции должно установить переменную оболочки a_opt в оболочке, но установить ее в значение $a_default, если аргумент опции не указан, в противном случае она устанавливается в значение $OPTARG, как обычно.

Чтобы определить, когда -a используется без аргумента, у нас есть два случая:

  1. Аргумент опции, который getopts считает, что пользователь использует, на самом деле является одной из других опций, или
  2. Опция -a используется в самом конце командной строки (getopts обычно выдает ошибку о пропущенной опции в этом случае).

Первый случай обрабатывается путем проверки $OPTARG, чтобы определить, выглядит ли он как одна из других опций. Если да, то используйте значение по умолчанию, иначе используйте $OPTARG в качестве значения. Когда используется значение по умолчанию, нам нужно перезапустить разбор опций с точки после -a, что означает сдвиг нескольких аргументов (OPTIND-1 из них), сброс OPTIND на 1, а затем добавление $OPTARG в начало списка позиционных параметров.

Это все означает, что мы не можем использовать -a с аргументом, который выглядит как одна из опций.

Второй случай обрабатывается тем, что первый символ строки опций, данной getopts, становится :. Это имеет два эффекта:

  1. getopts больше не вызывает ошибок самостоятельно.
  2. Когда опция, требующая аргумент опции, не получает аргумент опции, имя этой опции помещается в $OPTARG и знак : помещается в переменную опции, используемую с getopts.

Это означает, что мы можем добавить случай : в наше обычное выражение case для разбора опций, которое проверяет $OPTARG на соответствие a, чтобы увидеть, использовал ли пользователь -a в самом конце командной строки.

Код:

#!/usr/local/bin/bash

a_default="Значение по умолчанию"
d_opt=false

while getopts :a:b:c:d opt; do
        case $opt in
                a)
                        case $OPTARG in
                                -[a-d-]*)
                                        shift "$(( OPTIND - 1 ))"
                                        OPTIND=1
                                        set -- "$OPTARG" "$@"
                                        a_opt=$a_default
                                        ;;
                                *)
                                        a_opt=$OPTARG
                        esac
                        ;;

                b)      b_opt=$OPTARG ;;
                c)      c_opt=$OPTARG ;;
                d)      d_opt=true ;;

                :)
                        if [ "$OPTARG" = a ]; then
                                a_opt=$a_default
                        else
                                printf '%s: опция требует аргумент -- %s\n' \
                                        "$0" "$OPTARG" >&2
                                exit 1
                        fi
                        ;;

                *)      printf '%s: ошибка разбора опций\n' "$0" >&2
                        exit 1
        esac
done

shift "$(( OPTIND - 1 ))"

printf '%s = "%s"\n' \
        'a_opt' "${a_opt-(Не установлено)}" \
        'b_opt' "${b_opt-(Не установлено)}" \
        'c_opt' "${c_opt-(Не установлено)}" \
        'd_opt' "${d_opt-(Не установлено)}"

if [ "$#" -gt 0 ]; then
        printf 'Дополнительный аргумент: "%s"\n' "$@"
fi

Тестирование:

a_opt получает значение по умолчанию, если -a используется без аргумента-опции:

$ ./script -a
a_opt = "Значение по умолчанию"
b_opt = "(Не установлено)"
c_opt = "(Не установлено)"
d_opt = "false"

a_opt не получает значение, если -a не используется:

$ ./script -b bval
a_opt = "(Не установлено)"
b_opt = "bval"
c_opt = "(Не установлено)"
d_opt = "false"

Мы можем задать a_opt значение, как обычно, предоставив аргумент опции с -a:

$ ./script -da aval -c cval
a_opt = "aval"
b_opt = "(Не установлено)"
c_opt = "cval"
d_opt = "true"

Мы можем все еще использовать -- для окончания разбора опций:

$ ./script -d -a -- aval -c cval hello world
a_opt = "Значение по умолчанию"
b_opt = "(Не установлено)"
c_opt = "(Не установлено)"
d_opt = "true"
Дополнительный аргумент: "aval"
Дополнительный аргумент: "-c"
Дополнительный аргумент: "cval"
Дополнительный аргумент: "hello"
Дополнительный аргумент: "world"

Ответ или решение

Для того чтобы сделать аргументы опции в bash-скрипте необязательными, особенно если вы хотите, чтобы опция могла принимать только значение по умолчанию, можно использовать разные способы. Один из способов — использовать утилиту getopt, которая поддерживает необязательные аргументы опций, в отличие от встроенной команды getopts. Рассмотрим реализацию этого метода.

Пример реализации с использованием getopt

  1. Проверка поддерживаемой версии getopt:
    Убедитесь, что ваша версия getopt поддерживает необязательные аргументы, выполнив следующую команду:

    getopt -T

    Если команда возвращает код ошибки, это значит, что ваша версия getopt не поддерживает нужные функции.

  2. Основной скрипт:
    Создадим bash-скрипт, который принимает опцию -n, у которой есть необязательный аргумент. Если аргумент не указан, будет использоваться значение по умолчанию.

#!/bin/bash

# Значение по умолчанию
NICE_THINGS_VALUE=50

# Обработка аргументов
params="$(getopt -o n:: -- "$@")"

# Проверка на ошибки
if [ $? -ne 0 ]; then
    echo "Ошибка в аргументах. Используйте '-n [значение]'."
    exit 1
fi

# Установка параметров
eval set -- "$params"

while true; do
    case "$1" in
        -n)
            if [ -z "$2" ] || [[ "$2" == -* ]]; then
                # Если аргумент опции -n отсутствует или является другой опцией
                NICE_THINGS_VALUE=50
            else
                NICE_THINGS_VALUE=$2
                shift
            fi
            shift
            ;;
        --) 
            shift
            break
            ;;
        *) 
            echo "Неизвестный параметр: $1" 
            exit 1
            ;;
    esac
done

echo "Значение nice things: $NICE_THINGS_VALUE"

# Остальной код вашего скрипта

Как использовать скрипт

  1. Если вызвать скрипт с аргументом:

    bash script.sh -n 100

    Вывод будет:

    Значение nice things: 100
  2. Если вызвать скрипт без аргумента для опции -n:

    bash script.sh -n

    Вывод будет:

    Значение nice things: 50

Таким образом, с помощью команды getopt, вы можете обрабатывать опции с необязательными аргументами, позволяя вашему скрипту работать гибко с пользовательским вводом.

Заключение

Использование getopt позволяет создать более сложные сценарии обработки командной строки, чем стандартные возможности getopts. Если вы ожидаете, что ваши скрипты будут использоваться с различными параметрами, рассмотрите возможность использования более продвинутых инструментов, таких как getopt, для улучшения взаимодействия с пользователем.

Оцените материал
Добавить комментарий

Капча загружается...