Getopt плохо разбирает bash

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

Весь сценарий намного длиннее, но это важная часть:

#!/bin/bash
#
# FUNCTION
#   main
# DESCRIPTION
#   Main function. Everything will be called from here
# ARGS
#   Nothing
#
# RETURN CODE
#   Nothing
#
# Main function. Everything will be called from here
main() {
  # Parse the options and arguments
  parse_options "${@}"

  # Check if the interval is set to a valid number
  check_interval
}

#
# FUNCTION
#   check_interval
# DESCRIPTION
#   Checks if a number is valid
# ARGS
#   1: number to be checked
# RETURN CODE
#   0: valid
#   1: invalid
#
check_interval() {
  # We don't have to worry if interval is set at all, because getopt is already doing this
  if ( ! check_number_pos ${arginterval} ); then
    echo "Error: invalid interval: ${arginterval}"
    show_usage
    exit 2
  fi
}

#
# FUNCTION
#   show_usage
# DESCRIPTION
#   This is the Usage section. We a showing the Usage according to docopt standards
# ARGS
#   Nothing
# RETURN CODE
#   Nothing
#
show_usage() {
  echo "Usage:"                                                                           >&2
  echo "  ${THIS_SCRIPT_NAME} -i|--interval=<interval in s> [-r | --random [--randomwait=<wait in s>]] [-v|--verbose] [-d|--debug] [--colors]"  >&2
  echo "  ${THIS_SCRIPT_NAME} [-h|--help]"                                                >&2
}

#
# FUNCTION
#   check_number_pos
# DESCRIPTION
#   Checks if a number is valid and positive
# ARGS
#   1: number to be checked
#
# RETURN CODE
#   0: valid and positive
#   1: invalid or negative
#
check_number_pos() {
  local returnval
  if [[ "${1}" =~ ^[0-9]+$ ]]; then
    returnval=0
  else
    returnval=1
  fi
  return ${returnval}
}

#
# FUNCTION
#   parse_options
# DESCRIPTION
#   Parse options from command line
# ARGS
#   @: Arguments and options as given at CLI
# RETURN CODE
#   Nothing
#
parse_options() {
  # Use getopt(1) to parse options according to POSIX. If it fails, an error is shown, and we're showing the Usage and exit
  # Add new options here and also in the case-statement below.
  # Short options must be added in the 'options'-section
  # Long options must be added in the 'longoptions'-section
  # All short options must have a long equivalent
  # The --name is set so the error-output will not show 'getopt'-errors but neat <application name>-errors
  # Options and longoptions have the following format:
  # <letter>       Option without argument
  # <letter>:      Option with mandarory argument
  # <letter>::     Option with optional argument <- this is broken for short options and long options without '='. Don't use it!
  local -r GETOPT=$(getopt --name ${0} --options hrvdi: --longoptions help,random,verbose,debug,colors,randomwait:,interval: -- "${@}")

  if [ ${?} != 0 ]; then
    echo "Error: Error while getting arguments"
    show_usage
    exit 127;
  fi

  # No options or arguments given. Show Usage.
  if [[ "${GETOPT}" == " --" ]]; then
    show_usage
    exit 127;
  fi

  # Evaluate GETOPT. We need this to have the quotes in the output of getopt(1) interpreted.
  eval set -- "${GETOPT}"

  # Walk through all the options. Don't put too much code in here, just point to a function or set a variable.
  # Please note, all new options need to be added here but also in the GETOPT line above.
  # Note: shift removes the first value from the string, so the option itself will be removed from the GETOPT-string, and the argument is available in $1
  # After using an argument, please shift again, so the next option will be the first value in GETOPT
  while true;
  do
    case "${1}" in
      -i|--interval)
        shift
        arginterval=${1}
        shift
        ;;
      -r|--random)
        shift
        flagrandom=1
        ;;
      --randomwait)
        shift
        flagrandom=1
        argrandom=${1}
        shift
        ;;
      -v|-d|--verbose|--debug)
        flagdebug=1
        shift
        ;;
      --colors)
        flagcolors=1
        shift
        ;;
      -h|--help)
        #show_help
        exit 0
        ;;
      --)
        shift
        break
        ;;
      -*)
    echo "Error: unrecognized option ${1}"
        show_usage
        exit 127
        ;;
      *)
        show_usage
        exit 127
        ;;
    esac
  done
}

#Call main function after all
main "${@}"

Теперь, когда я вызываю скрипт правильно, все идет гладко:

$ ./test1.sh -i 10

Когда я забываю аргумент, он также делает то, что я хочу:

$ ./test1.sh -i
./test1.sh: option requires an argument -- 'i'
Usage:
   -i|--interval=<interval in s> [-r | --random [--randomwait=<wait in s>]] [-v|--verbose] [-d|--debug] [--colors]
   [-h|--help]

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

$ ./test1.sh -i --colors
Error: invalid interval: --colors
Usage:
   -i|--interval=<interval in s> [-r | --random [--randomwait=<wait in s>]] [-v|--verbose] [-d|--debug] [--colors]
   [-h|--help]

Это происходит потому, что я проверяю, является ли интервал целым числом, но для других целей это опасная вещь. Как я могу изменить регистр, чтобы он не читал параметры как аргументы? Пока что getopt не очень хорошо мне помогает, потому что я также столкнулся с другой ошибкой/функцией: «необязательный аргумент» (::) не работает, как я ожидал, поскольку он работает только при использовании с опцией «=» между и аргумент.

Версии:

$ getopt -V
getopt (enhanced) 1.1.4
$ bash --version
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

person Gertjan Bijl    schedule 11.07.2016    source источник


Ответы (1)


getopt не знает о семантике ваших опций. Насколько он знает, --colors является допустимым аргументом для опции -i. К сожалению, если вы хотите с ними справиться, вам придется самостоятельно проверять такие ошибки.

 while true;
  do
    case "${1}" in
      -i|--interval)
        shift
        arginterval=${1}
        if [[ $arginterval = -* ]]; then
            printf 'You appear to have forgotten the interval argument before the %s option\n' "$arginterval" >&2
            exit 1
        fi
        shift
        ;;

    ...
person chepner    schedule 11.07.2016
comment
Конечно; аналогично, getup требует, чтобы необязательный аргумент-опция был прикреплен непосредственно к опции - без разделителя в краткой форме (например, -i10), с = в качестве разделителя в длинная форма (например, --interval=10) - потому что это единственный однозначный способ сделать это. - person mklement0; 11.07.2016
comment
Хорошо, может быть, я пытаюсь сделать мысли слишком надежными, но я ожидал, что getopt поймет, что --colors - это «опция», потому что я сказал ему. Я добавлю в скрипт еще несколько проверок, чтобы предотвратить ошибочный ввод. - person Gertjan Bijl; 12.07.2016
comment
--colors может быть вариантом, но нет способа сказать getopt, что он также не может быть допустимым аргументом для -i. - person chepner; 12.07.2016