Подтвердить что ты не робот

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

4b9b3361

Ответ 1

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

Ответ 2

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

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.

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

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

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

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

Ответ 3

Я согласен с 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

Ответ 4

На самом деле это довольно просто. Просто оставьте трейлинг-двоеточие после 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 test

Тест

count.sh -d test -R

1 тест

count.sh -R -d test

1 тест

count.sh -d test -R 2

2 тест

count.sh -R 2 -d test

2 тест

Ответ 5

Этот обход определяет "R" без аргумента (no ':'), проверяет любой аргумент после "-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

Ответ 6

Try:

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, уровень устанавливается на значение по умолчанию 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 - это что-то, начинающееся с дефиса, тогда reset OPTIND и повторно запустите getopts (продолжите цикл while). О, конечно, это не идеально и требует некоторой уверенности. Это просто пример.

Ответ 7

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

Чтобы устранить проблему отсутствия последнего аргумента, массив "$ @" просто имеет пустую строку "$ @", добавленную так, что getopts будет удовлетворен тем, что она собрала еще один аргумент параметра. Чтобы исправить этот новый пустой аргумент, задана переменная, содержащая общий счет всех обрабатываемых параметров - при обработке последней опции создается вспомогательная функция под названием 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 "[email protected]";
 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 "[email protected] " ## extra space, so getopts can find arguments  

Ответ 8

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

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

Вот код:

#!/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.

См. также: Малый учебник по учебникам в Bash Hackers Wiki

Ответ 9

Я сам столкнулся с этим и почувствовал, что ни одно из существующих решений не было действительно чистым. Немного поработав над ним и попробовав разные вещи, я обнаружил, что использование режима 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"
...

Ответ 10

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

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 [email protected]
    dir=${OPTARG}
    ;;
  R)
    getopts_get_optional_argument [email protected]
    level=${OPTARG:-1}
    ;;
  h)
    show_usage && exit 0
    ;;
  \?)
    show_usage && exit 1
    ;;
  esac
done

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

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

Guideline 7: Option-arguments should not be optional.

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

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