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

while getopts "hd:R:" arg; do
  case $arg in
    h)
      echo "usgae" 
      ;;
    d)
      dir=$OPTARG
      ;;
    R)
      if [[ $OPTARG =~ ^[0-9]+$ ]];then
        level=$OPTARG
      else
        level=1
      fi
      ;;
    \?)
      echo "WRONG" >&2
      ;;
  esac
done
  • level относится к параметру -R, dir относится к параметрам -d

  • когда я ввожу ./count.sh -R 1 -d test/, он работает правильно

  • когда я ввожу ./count.sh -d test/ -R 1, он работает правильно

  • но я хочу, чтобы это работало, когда я ввожу ./count.sh -d test/ -R или ./count.sh -R -d test/

Это означает, что я хочу, чтобы -R имел значение по умолчанию, а последовательность команд могла быть более гибкой.


person iverson    schedule 17.07.2012    source источник
comment
Так каков фактический ответ здесь? Как сделать параметр необязательным?   -  person user1394    schedule 06.11.2018


Ответы (12)


getopts на самом деле не поддерживает это; но нетрудно написать собственную замену.

while true; do
    case $1 in
      -R) level=1
            shift
            case $1 in
              *[!0-9]* | "") ;;
              *) level=$1; shift ;;
            esac ;;
        # ... Other options ...
        -*) echo "$0: Unrecognized option $1" >&2
            exit 2;;
        *) break ;;
    esac
done
person tripleee    schedule 17.07.2012
comment
Пожалуйста, смотрите ответ Андреаса Спиндлера ниже, он поддерживается - person Rohit; 14.09.2016
comment
@Rohit Обратите внимание, что ответ Andreas Spindler неверен в большинстве случаев, как описано в комментариях под его ответом. - person Otiel; 23.04.2018

Неправильный. На самом деле getopts поддерживает необязательные аргументы! Со страницы руководства bash:

If  a  required  argument is not found, and getopts is not silent, 
a question mark (?) is placed in name, OPTARG is unset, and a diagnostic
message is printed.  If getopts is silent, then a colon (:) is placed in name 
and OPTARG is set to the option character found.

Когда на справочной странице написано «без звука», это означает автоматическое сообщение об ошибках. Чтобы включить его, первым символом optstring должно быть двоеточие:

while getopts ":hd:R:" arg; do
    # ...rest of iverson's loop should work as posted 
done

Поскольку getopt в Bash не распознает -- в конце списка параметров, он может не работать, когда -R является последним параметром, за которым следует какой-либо аргумент пути.

P.S.: Обычно getopt.c использует два двоеточия (::) для указания необязательного аргумента. Однако версия, используемая Bash, этого не делает.

person Andreas Spindler    schedule 10.02.2013
comment
Не осмысленно. Если -R является последним аргументом, он не обрабатывается - person gerardw; 12.09.2013
comment
Нет, вы не правы. ./count.sh -R -d test/ не работает, потому что -d принимается в качестве аргумента -R (что вовсе не обязательно). - person calandoa; 25.05.2016
comment
Этот ответ в лучшем случае вводит в заблуждение. Как указал @calandoa, любой параметр с необязательным аргументом работает, только если он указан последним. В противном случае он будет использовать следующую опцию в качестве аргумента. например в этом использовании ./count.sh -R -d test/ '-R' принимает '-d' в качестве аргумента, а '-d' не распознается как опция. Я только повторяю то, что уже было сказано (от @calandoa), потому что этот неправильный ответ имеет 20 чистых голосов. - person user2141130; 14.03.2017
comment
Возможно, предыдущая версия bash (или sh) работает правильно, но bash 4.3.30(1) Debian Jessie дает сбой, как описано в предыдущем комментарии. - person user2141130; 14.03.2017
comment
Этот ответ является странным случаем с точки зрения получения голосов, поскольку он не отвечает на довольно конкретный заданный вопрос, но он отвечает на (я предполагаю) гораздо более распространенный вопрос: каков синтаксис, чтобы заставить getopts игнорировать аргументы он не распознает, не сообщая об ошибке? Это вопрос, на который я пришел в поисках ответа, и это был ответ, который я искал. - person Chuck Wilbur; 26.07.2018

Этот обходной путь определяет «R» без аргумента (без «:»), проверяет любой аргумент после «-R» (управление последним параметром в командной строке) и проверяет, начинается ли существующий аргумент с тире.

# No : after R
while getopts "hd:R" arg; do
  case $arg in
  (...)
  R)
    # Check next positional parameter
    eval nextopt=\${$OPTIND}
    # existing or starting with dash?
    if [[ -n $nextopt && $nextopt != -* ]] ; then
      OPTIND=$((OPTIND + 1))
      level=$nextopt
    else
      level=1
    fi
    ;;
  (...)
  esac
done
person calandoa    schedule 01.08.2016
comment
На самом деле это единственный из ответов, который здесь работает! Пожалуйста, проголосуйте за это. - person nandilugio; 31.07.2019
comment
Вдохновленный этим ответом (единственным, который действительно работает!), я сделал простую функцию, которую можно легко использовать несколько раз. См. мой ответ здесь - person nandilugio; 31.07.2019
comment
eval nextopt=\${$OPTIND} — творческое решение, но в Bash уже есть специальный синтаксис для косвенного раскрытия: nextopt=${!OPTIND}. - person Christoph; 04.07.2021

Я согласен с Tripleee, getopts не поддерживает необязательную обработку аргументов.

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

Пример:

COMMAND_LINE_OPTIONS_HELP='
Command line options:
    -I          Process all the files in the default dir: '`pwd`'/input/
    -i  DIR     Process all the files in the user specified input dir
    -h          Print this help menu

Examples:
    Process all files in the default input dir
        '`basename $0`' -I

    Process all files in the user specified input dir
        '`basename $0`' -i ~/my/input/dir

'

VALID_COMMAND_LINE_OPTIONS="i:Ih"
INPUT_DIR=

while getopts $VALID_COMMAND_LINE_OPTIONS options; do
    #echo "option is " $options
    case $options in
        h)
            echo "$COMMAND_LINE_OPTIONS_HELP"
            exit $E_OPTERROR;
        ;;
        I)
            INPUT_DIR=`pwd`/input
            echo ""
            echo "***************************"
            echo "Use DEFAULT input dir : $INPUT_DIR"
            echo "***************************"
        ;;
        i)
            INPUT_DIR=$OPTARG
            echo ""
            echo "***************************"
            echo "Use USER SPECIFIED input dir : $INPUT_DIR"
            echo "***************************"
        ;;
        \?)
            echo "Usage: `basename $0` -h for help";
            echo "$COMMAND_LINE_OPTIONS_HELP"
            exit $E_OPTERROR;
        ;;
    esac
done
person Akos Cz    schedule 25.07.2012

Это на самом деле довольно легко. Просто отпустите двоеточие после R и используйте OPTIND

while getopts "hRd:" opt; do
   case $opt in
      h) echo -e $USAGE && exit
      ;;
      d) DIR="$OPTARG"
      ;;
      R)       
        if [[ ${@:$OPTIND} =~ ^[0-9]+$ ]];then
          LEVEL=${@:$OPTIND}
          OPTIND=$((OPTIND+1))
        else
          LEVEL=1
        fi
      ;;
      \?) echo "Invalid option -$OPTARG" >&2
      ;;
   esac
done
echo $LEVEL $DIR

count.sh -d тест

контрольная работа

count.sh -d тест -R

1 тест

count.sh -R -d тест

1 тест

count.sh -d тест -R 2

2 тест

count.sh -R 2 -d тест

2 тест

person Aaron Sua    schedule 01.08.2017
comment
Последний тест (count.sh -R 2 -d test) дает мне результат 1, а не 2 test (bash 5.0.3). Все остальные работают. Это связано с тем, что ${@:$OPTIND} вычисляет все остальные аргументы, а не только следующий. - person nandilugio; 31.07.2019
comment
Исправление с помощью ответа @calandoa позволяет пройти все тесты. - person nandilugio; 31.07.2019

Вдохновленный ответом @calandoa (единственный, который действительно работает!), я сделал простую функцию, которая может сделать это легко использовать несколько раз.

getopts_get_optional_argument() {
  eval next_token=\${$OPTIND}
  if [[ -n $next_token && $next_token != -* ]]; then
    OPTIND=$((OPTIND + 1))
    OPTARG=$next_token
  else
    OPTARG=""
  fi
}

Пример использования:

while getopts "hdR" option; do
  case $option in
  d)
    getopts_get_optional_argument $@
    dir=${OPTARG}
    ;;
  R)
    getopts_get_optional_argument $@
    level=${OPTARG:-1}
    ;;
  h)
    show_usage && exit 0
    ;;
  \?)
    show_usage && exit 1
    ;;
  esac
done

Это дает нам практичный способ получить эту недостающую функцию в getopts :)

ПРИМЕЧАНИЕ, тем не менее, параметры командной строки с необязательными аргументами кажутся явно не рекомендуется

Рекомендация 7: Option-аргументы не должны быть необязательными.

но у меня нет интуитивного способа реализовать мой случай без этого: у меня есть 2 режима, которые активируются либо с помощью одного флага, либо с другим, и оба имеют аргумент с четким значением по умолчанию. Введение третьего флага только для устранения неоднозначности выглядит плохим стилем CLI.

Я проверил это со многими комбинациями, включая все ответы @aaron-sua, и работает хорошо.

person nandilugio    schedule 31.07.2019

Следующий код решает эту проблему, проверяя начальный тире и, если он найден, уменьшает значение OPTIND, чтобы указать обратно на пропущенный параметр для обработки. Обычно это работает нормально, за исключением того, что вы не знаете порядок, в котором пользователь будет размещать параметры в командной строке - если ваш необязательный параметр аргумента является последним и не предоставляет аргумент, getopts захочет вывести ошибку.

Чтобы решить проблему отсутствия последнего аргумента, к массиву "$@" просто добавляется пустая строка "$@ ", так что getopts будет удовлетворен тем, что он проглотил еще один аргумент option. Чтобы исправить этот новый пустой аргумент, устанавливается переменная, которая содержит общее количество всех параметров, подлежащих обработке — когда обрабатывается последний параметр, вызывается вспомогательная функция, называемая trim, которая удаляет пустую строку перед используемым значением.

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

#!/usr/bin/env bash 
declare  -r CHECK_FLOAT="%f"  
declare  -r CHECK_INTEGER="%i"  

 ## <arg 1> Number - Number to check
 ## <arg 2> String - Number type to check
 ## <arg 3> String - Error message
function check_number() {
  local NUMBER="${1}"
  local NUMBER_TYPE="${2}"
  local ERROR_MESG="${3}"
  local FILTERED_NUMBER=$(sed 's/[^.e0-9+\^]//g' <<< "${NUMBER}")
  local -i PASS=1
  local -i FAIL=0
    if [[ -z "${NUMBER}" ]]; then 
        echo "Empty number argument passed to check_number()." 1>&2
        echo "${ERROR_MESG}" 1>&2
        echo "${FAIL}"          
  elif [[ -z "${NUMBER_TYPE}" ]]; then 
        echo "Empty number type argument passed to check_number()." 1>&2
        echo "${ERROR_MESG}" 1>&2
        echo "${FAIL}"          
  elif [[ ! "${#NUMBER}" -eq "${#FILTERED_NUMBER}" ]]; then 
        echo "Non numeric characters found in number argument passed to check_number()." 1>&2
        echo "${ERROR_MESG}" 1>&2
        echo "${FAIL}"          
  else  
   case "${NUMBER_TYPE}" in
     "${CHECK_FLOAT}")
         if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then
            echo "${PASS}"
         else
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"
         fi
         ;;
     "${CHECK_INTEGER}")
         if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then
            echo "${PASS}"
         else
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"
         fi
         ;;
                      *)
         echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
         echo "${FAIL}"
         ;;
    esac
 fi 
}

 ## Note: Number can be any printf acceptable format and includes leading quotes and quotations, 
 ##       and anything else that corresponds to the POSIX specification. 
 ##       E.g. "'1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054
 ## <arg 1> Number - Number to print
 ## <arg 2> String - Number type to print
function print_number() { 
  local NUMBER="${1}" 
  local NUMBER_TYPE="${2}" 
  case "${NUMBER_TYPE}" in 
      "${CHECK_FLOAT}") 
           printf "${CHECK_FLOAT}" "${NUMBER}" || echo "Error printing Float in print_number()." 1>&2
        ;;                 
    "${CHECK_INTEGER}") 
           printf "${CHECK_INTEGER}" "${NUMBER}" || echo "Error printing Integer in print_number()." 1>&2
        ;;                 
                     *) 
        echo "Invalid number type format: ${NUMBER_TYPE} to print_number()." 1>&2
        ;;                 
   esac
} 

 ## <arg 1> String - String to trim single ending whitespace from
function trim_string() { 
 local STRING="${1}" 
 echo -En $(sed 's/ $//' <<< "${STRING}") || echo "Error in trim_string() expected a sensible string, found: ${STRING}" 1>&2
} 

 ## This a hack for getopts because getopts does not support optional
 ## arguments very intuitively. E.g. Regardless of whether the values
 ## begin with a dash, getopts presumes that anything following an
 ## option that takes an option argument is the option argument. To fix  
 ## this the index variable OPTIND is decremented so it points back to  
 ## the otherwise skipped value in the array option argument. This works
 ## except for when the missing argument is on the end of the list,
 ## in this case getopts will not have anything to gobble as an
 ## argument to the option and will want to error out. To avoid this an
 ## empty string is appended to the argument array, yet in so doing
 ## care must be taken to manage this added empty string appropriately.
 ## As a result any option that doesn't exit at the time its processed
 ## needs to be made to accept an argument, otherwise you will never
 ## know if the option will be the last option sent thus having an empty
 ## string attached and causing it to land in the default handler.
function process_options() {
local OPTIND OPTERR=0 OPTARG OPTION h d r s M R S D
local ERROR_MSG=""  
local OPTION_VAL=""
local EXIT_VALUE=0
local -i NUM_OPTIONS
let NUM_OPTIONS=${#@}+1
while getopts “:h?d:DM:R:S:s:r:” OPTION "$@";
 do
     case "$OPTION" in
         h)
             help | more
             exit 0
             ;;
         r)
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG="Invalid input: Integer or floating point number required."
             if [[ -z "${OPTION_VAL}" ]]; then
               ## can set global flags here 
               :;
             elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
               let OPTIND=${OPTIND}-1
               ## can set global flags here 
             elif [ "${OPTION_VAL}" = "0" ]; then
               ## can set global flags here 
               :;               
             elif (($(check_number "${OPTION_VAL}" "${CHECK_FLOAT}" "${ERROR_MSG}"))); then
               :; ## do something really useful here..               
             else
               echo "${ERROR_MSG}" 1>&2 && exit -1
             fi
             ;;
         d)
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             [[  ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]] && let OPTIND=${OPTIND}-1            
             DEBUGMODE=1
             set -xuo pipefail
             ;;
         s)
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             if [[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]]; then ## if you want a variable value that begins with a dash, escape it
               let OPTIND=${OPTIND}-1
             else
              GLOBAL_SCRIPT_VAR="${OPTION_VAL}"
                :; ## do more important things
             fi
             ;;
         M)  
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG=$(echo "Error - Invalid input: ${OPTION_VAL}, Integer required"\
                              "retry with an appropriate option argument.")
             if [[ -z "${OPTION_VAL}" ]]; then
               echo "${ERROR_MSG}" 1>&2 && exit -1
             elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
               let OPTIND=${OPTIND}-1
               echo "${ERROR_MSG}" 1>&2 && exit -1
             elif (($(check_number "${OPTION_VAL}" "${CHECK_INTEGER}" "${ERROR_MSG}"))); then
             :; ## do something useful here
             else
               echo "${ERROR_MSG}" 1>&2 && exit -1
             fi
             ;;                      
         R)  
             OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG=$(echo "Error - Invalid option argument: ${OPTION_VAL},"\
                              "the value supplied to -R is expected to be a "\
                              "qualified path to a random character device.")            
             if [[ -z "${OPTION_VAL}" ]]; then
               echo "${ERROR_MSG}" 1>&2 && exit -1
             elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
               let OPTIND=${OPTIND}-1
               echo "${ERROR_MSG}" 1>&2 && exit -1
             elif [[ -c "${OPTION_VAL}" ]]; then
               :; ## Instead of erroring do something useful here..  
             else
               echo "${ERROR_MSG}" 1>&2 && exit -1
             fi
             ;;                      
         S)  
             STATEMENT=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
             ERROR_MSG="Error - Default text string to set cannot be empty."
             if [[ -z "${STATEMENT}" ]]; then
               ## Instead of erroring you could set a flag or do something else with your code here..  
             elif [[ "${STATEMENT}" =~ ^-. ]]; then ## if you want a statement that begins with a dash, escape it
               let OPTIND=${OPTIND}-1
               echo "${ERROR_MSG}" 1>&2 && exit -1
               echo "${ERROR_MSG}" 1>&2 && exit -1
             else
                :; ## do something even more useful here you can modify the above as well 
             fi
             ;;                      
         D)  
             ## Do something useful as long as it is an exit, it is okay to not worry about the option arguments 
             exit 0
             ;;          
         *)
             EXIT_VALUE=-1
             ;&                  
         ?)
             usage
             exit ${EXIT_VALUE}
             ;;
     esac
done
}

process_options "$@ " ## extra space, so getopts can find arguments  
person Community    schedule 13.03.2015

Пытаться:

while getopts "hd:R:" arg; do
  case $arg in
    h)
      echo "usage" 
    ;;
    d)
      dir=$OPTARG
    ;;
    R)
      if [[ $OPTARG =~ ^[0-9]+$ ]];then
        level=$OPTARG
      elif [[ $OPTARG =~ ^-. ]];then
        level=1
        let OPTIND=$OPTIND-1
      else
        level=1
      fi          
    ;;
    \?)
      echo "WRONG" >&2
    ;;
  esac
done

Я думаю, что приведенный выше код будет работать для ваших целей при использовании getopts. Я добавил следующие три строки в ваш код, когда getopts встречает -R:

      elif [[ $OPTARG =~ ^-. ]];then
        level=1
        let OPTIND=$OPTIND-1

Если встречается -R и первый аргумент выглядит как другой параметр getopts, level устанавливается на значение по умолчанию 1, а затем переменная $OPTIND уменьшается на единицу. В следующий раз, когда getopts попытается получить аргумент, он возьмет правильный аргумент, а не пропустит его.


Вот аналогичный пример, основанный на коде из комментария Яна Шампера к этому руководству:

#!/bin/bash
while getopts :abc: opt; do
  case $opt in
    a)
      echo "option a"
    ;;
    b)
      echo "option b"
    ;;
    c)
      echo "option c"

      if [[ $OPTARG = -* ]]; then
        ((OPTIND--))
        continue
      fi

      echo "(c) argument $OPTARG"
    ;;
    \?)
      echo "WTF!"
      exit 1
    ;;
  esac
done

Когда вы обнаружите, что OPTARG von -c начинается с дефиса, сбросьте OPTIND и повторно запустите getopts (продолжите цикл while). О, конечно, это не идеально и требует большей надежности. Это просто пример.

person Menzies    schedule 03.12.2013
comment
Судя по игре с getopts, похоже, что здесь работает let OPTIND=$OPTIND-1 не знаю, что происходит с отслеживанием уровней. Может быть, это для другого использования из вашего кода? - person ; 12.03.2015
comment
Пример, приведенный на wiki.bash-hackers.org, не работает, если последним параметром является необязательный OPTARG. $ prog.bash -a -b -c даже не узнает об опции -c, когда $ prog.bash -a -c -b узнает. Как это может работать, если это последний аргумент? - person johnnyB; 07.09.2016

Вы всегда можете решить различать опцию строчными или прописными буквами.

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

Вот код:

#!/usr/bin/env bash
while getopts ":hd:R" arg; do
  case $arg in
    d) # Set directory, e.g. -d /foo
      dir=$OPTARG
      ;;
    R) # Optional level value, e.g. -R 123
      OI=$OPTIND # Backup old value.
      ((OPTIND--)) # Decrease argument index, to parse -R again.
      while getopts ":R:" r; do
        case $r in
          R)
            # Check if value is in numeric format.
            if [[ $OPTARG =~ ^[0-9]+$ ]]; then
              level=$OPTARG
            else
              level=1
            fi
          ;;
          :)
            # Missing -R value.
            level=1
          ;;
        esac
      done
      [ -z "$level" ] && level=1 # If value not found, set to 1.
      OPTIND=$OI # Restore old value.
      ;;
    \? | h | *) # Display help.
      echo "$0 usage:" && grep " .)\ #" $0
      exit 0
      ;;
  esac
done
echo Dir: $dir
echo Level: $level

Вот несколько тестов для сценариев, которые работают:

$ ./getopts.sh -h
./getopts.sh usage:
    d) # Set directory, e.g. -d /foo
    R) # Optional level value, e.g. -R 123
    \? | h | *) # Display help.
$ ./getopts.sh -d /foo
Dir: /foo
Level:
$ ./getopts.sh -d /foo -R
Dir: /foo
Level: 1
$ ./getopts.sh -d /foo -R 123
Dir: /foo
Level: 123
$ ./getopts.sh -d /foo -R wtf
Dir: /foo
Level: 1
$ ./getopts.sh -R -d /foo
Dir: /foo
Level: 1

Сценарии, которые не работают (поэтому код нуждается в дополнительных настройках):

$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123

Дополнительную информацию об использовании getopts можно найти в man bash.

См. также: Небольшой учебник по getopts на Bash Hackers Wiki.

person kenorb    schedule 30.12.2015

Я просто столкнулся с этим сам и почувствовал, что ни одно из существующих решений не было действительно чистым. Немного поработав над этим и попробовав разные вещи, я обнаружил, что использование режима getopts SILENT с :) ..., по-видимому, помогло, наряду с синхронизацией OPTIND.


usage: test.sh [-abst] [-r [DEPTH]] filename
*NOTE: -r (recursive) with no depth given means full recursion

#!/usr/bin/env bash

depth='-d 1'

while getopts ':abr:st' opt; do
    case "${opt}" in
        a) echo a;;
        b) echo b;;
        r) if [[ "${OPTARG}" =~ ^[0-9]+$ ]]; then
               depth="-d ${OPTARG}"
           else
               depth=
               (( OPTIND-- ))
           fi
           ;;
        s) echo s;;
        t) echo t;;
        :) [[ "${OPTARG}" = 'r' ]] && depth=;;
        *) echo >&2 "Invalid option: ${opt}"; exit 1;;
    esac
done
shift $(( OPTIND - 1 ))

filename="$1"
...
person Terra    schedule 14.01.2019

Я думаю, что есть два пути.

Во-первых, это ответ каландоа: использование OPTIND и отсутствие бесшумного режима.

Во-вторых, использование OPTIND и бесшумного режима.

while getopts ":Rr:" name; do
    case ${name} in
        R)
            eval nextArg=\${$OPTIND}
            # check option followed by nothing or other option.
            if [[ -z ${nextArg} || $nextArg =~ ^-.* ]]; then
                level=1
            elif [[ $nextArg =~ ^[0-9]+$ ]]; then
                level=$nextArg
                OPTIND=$((OPTIND + 1))
            else
                level=1
            fi
            ;;
        r)
            # check option followed by other option.
            if [[ $OPTARG =~ ^-.* ]]; then
                OPTIND=$((OPTIND - 1))
                level2=2
            elif [[ $OPTARG =~ ^[0-9]+$ ]]; then
                level2="$OPTARG"
            else
                level2=2
            fi
            ;;
        :)
            # check no argument
            case $OPTARG in
                r)
                    level2=2
                    ;;
            esac
    esac
done

echo "Level 1 : $level"
echo "Level 2 : $level2"
person Heedo Lee    schedule 26.11.2020

Все решения, представленные до сих пор, помещают код в case ... in ... esac, но, на мой взгляд, было бы гораздо более естественным иметь модифицированную команду getopts, поэтому я написал эту функцию:

ИЗМЕНИТЬ:

Теперь вы можете указать тип необязательного аргумента (см. информацию об использовании).

Кроме того, вместо того, чтобы проверять, выглядит ли $nextArg как параметр(ы) arg, функция теперь проверяет, содержит ли $nextArg букву из $optstring. Таким образом, буква параметра, не содержащаяся в $optstring, может использоваться как необязательный аргумент, как и в случае обязательных аргументов getopts.

Последние изменения:

  • Фиксированная проверка, если $nextArg является параметром arg:
    Проверяется, начинается ли $nextArg с дефиса.
    Без этой проверки необязательные аргументы, содержащие букву из $optstring, не распознаются как таковые.
  • Добавлен спецификатор типа регулярного выражения (см. информацию об использовании).

Использование:

Вызов: getopts-plus optstring name "$@"

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

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

Если вам повезло и ожидается, что необязательный аргумент будет целым числом, а необязательный аргумент — строкой или наоборот, вы можете указать тип, добавив :::i для целого числа. или :::s для строки, чтобы решить эту проблему.

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

#!/bin/bash

# Invocation: getopts-plus optstring name "$@"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a regexp for the optional arg by appending
# ::/.../ to the option letter.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
    local optstring=$1
    local -n name=$2

    shift 2

    local optionalArgSuffixRE='::(:[si]|/.*/)?'
    local optionalArgTypeCaptureRE=':::([si])|::(/.*/)'

    # If we pass 'opt' for 'name' (as I always do when using getopts) and there is
    # also a local variable 'opt', the "outer" 'opt' will always be empty.
    # I don't understand why a local variable interferes with caller's variable with
    # same name in this case; however, we can easily circumvent this.
    local opt_

    # Extract options with optional arg

    local -A isOptWithOptionalArg

    while read opt_; do
        # Using an associative array as set
        isOptWithOptionalArg[$opt_]=1
    done <<<$(grep -Eo "[a-zA-Z]$optionalArgSuffixRE" <<<$optstring | sed -E "s#$optionalArgSuffixRE##")

    # Extract all option letters (used to weed out possible optional args that are option args)
    local optLetters=$(sed -E "s#:|$optionalArgSuffixRE##g" <<<$optstring | grep -o '[A-Za-z]')

    # Save original optstring, then remove our suffix(es)
    local optstringOrg=$optstring
    optstring=$(sed -E "s#$optionalArgSuffixRE##g" <<<$optstring)

    getopts $optstring name "$@" || return # Return value is getopts' exit value.

    # If current option is an option with optional arg and if an arg has been provided,
    # check if that arg is not an option and if it isn't, check if that arg matches(*)
    # the specified type, if any, and if it does or no type has been specified,
    # assign it to OPTARG and inc OPTIND.
    #
    # (*) We detect an int because it's easy, but we assume a string if it's not an int
    # because detecting a string would be complicated.
    # So it sounds strange to call it a match if we know that the optional arg is specified
    # to be a string, but merely that the provided arg is not an int, but in this context,
    # "not an int" is equivalent to "string". At least I think so, but I might be wrong.

    if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
        local nextArg=${!OPTIND} foundOpt=0

        # Test if $nextArg is an option arg
        if [[ $nextArg == -* ]]; then
            # Check if $nextArg contains a letter from $optLetters.
            # This way, an option not contained in $optstring can be
            # used as optional arg, as with getopts' mandatory args.

            local i

            # Start at char 1 to skip the leading dash
            for ((i = 1; i < ${#nextArg}; i++)); do
                while read opt_; do
                    [[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
                done <<<$optLetters
            done

            ((foundOpt)) && return
        fi

        # Extract type of optional arg if specified
        # N.B.: We use -n option and p modifier to get an output
        # only if the substitution has actually been performed.
        local optArgType=$(sed -En "s#.*$name($optionalArgTypeCaptureRE).*#\2\3#p" <<<$optstringOrg)

        local nextArgIsOptArg=0

        case $optArgType in
            /*/) # Check if $nextArg matches regexp
                local optArgRE=$(sed -E "s#/(.*)/#\1#" <<<$optArgType)

                [[ $nextArg =~ $optArgRE ]] && nextArgIsOptArg=1
                ;;
            [si]) # Check if $nextArg is an int
                # Use i attribute to check if $nextArg is an integer.
                # N.B.: - Assigning something that is not an int
                #         to a variable that has the i attribute set
                #         has the same effect as assigning 0.
                #       - Trying to assign a string that contains a space,
                #         a \t or a \n (no matter if literally or decoded)
                #         to a variable that has the i attribute set fails.
                local -i nextArgIsInt

                # First, test if $nextArg contains a space, a \t or a \n (literally or decoded).
                if [[ $nextArg =~ .*[\ \t\n$'\t'$'\n'].* ]]; then # => Not a number
                    nextArgIsInt=0
                else
                    nextArgIsInt=$nextArg
                fi

                # Test if specified type and arg type match (see (*) above).
                # N.B.: We need command groups since && and || between commands have same precedence.
                { [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
                ;;
            '') # No type or regexp specified => Assume $nextArg is optional arg.
                nextArgIsOptArg=1
                ;;
        esac

        if ((nextArgIsOptArg)); then
            OPTARG=$nextArg && ((OPTIND++))
        fi
    fi
}

Та же функция без комментариев, если они вам не нужны:

#!/bin/bash

# Invocation: getopts-plus optstring name "$@"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a regexp for the optional arg by appending
# ::/.../ to the option letter.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
    local optstring=$1
    local -n name=$2

    shift 2

    local optionalArgSuffixRE='::(:[si]|/.*/)?'
    local optionalArgTypeCaptureRE=':::([si])|::(/.*/)'

    local opt_

    local -A isOptWithOptionalArg

    while read opt_; do
        isOptWithOptionalArg[$opt_]=1
    done <<<$(grep -Eo "[a-zA-Z]$optionalArgSuffixRE" <<<$optstring | sed -E "s#$optionalArgSuffixRE##")

    local optLetters=$(sed -E "s#:|$optionalArgSuffixRE##g" <<<$optstring | grep -o '[A-Za-z]')

    local optstringOrg=$optstring
    optstring=$(sed -E "s#$optionalArgSuffixRE##g" <<<$optstring)

    getopts $optstring name "$@" || return

    if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
        local nextArg=${!OPTIND} foundOpt=0

        if [[ $nextArg == -* ]]; then
            local i

            for ((i = 1; i < ${#nextArg}; i++)); do
                while read opt_; do
                    [[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
                done <<<$optLetters
            done

            ((foundOpt)) && return
        fi

        local optArgType=$(sed -En "s#.*$name($optionalArgTypeCaptureRE).*#\2\3#p" <<<$optstringOrg)

        local nextArgIsOptArg=0

        case $optArgType in
            /*/)
                local optArgRE=$(sed -E "s#/(.*)/#\1#" <<<$optArgType)

                [[ $nextArg =~ $optArgRE ]] && nextArgIsOptArg=1
                ;;
            [si])
                local -i nextArgIsInt

                if [[ $nextArg =~ .*[\ \t\n$'\t'$'\n'].* ]]; then
                    nextArgIsInt=0
                else
                    nextArgIsInt=$nextArg
                fi

                { [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
                ;;
            '')
                nextArgIsOptArg=1
                ;;
        esac

        if ((nextArgIsOptArg)); then
            OPTARG=$nextArg && ((OPTIND++))
        fi
    fi
}

Некоторые тесты:

Необязательный тип аргумента -g, указанный как целое число, целое число не передается, но за которым следует неопциональная строка arg.

$ . ./getopts-plus.sh
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == '' <-- Empty because "hello you" is not an int

Как и выше, но с int arg.

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g 7 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == '7' <-- The passed int

Добавлен необязательный параметр -h с регулярным выражением ^(a|b|ab|ba)$, аргумент не передается.

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == ''

-------------------------

opt == 'h'

OPTARG == '' <-- Empty because "hello you" does not match the regexp

Как и выше, но с аргументом, соответствующим регулярному выражению.

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh ab "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == ''

-------------------------

opt == 'h'

OPTARG == 'ab' <-- The arg that matches the regexp
person Christoph    schedule 06.07.2021