Search code examples
bashargumentsgetopt

Getopt not parsing well bash


I wrote a script in Bash serving as a template for several monitors. I choose getopt in order to be able to use long options on CLI. However, I have some problems implementing it correctly.

The whole script is a lot longer, but this is the relevant part:

#!/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 "${@}"

Now, when I call the script the right way, everything goes smooth:

$ ./test1.sh -i 10

When I forget the argument, it also does what I want:

$ ./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]

But when I simply forget the argument and add another one, it fails:

$ ./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]

This happens because I check if the interval is an integer, but for other purposes, this is a dangerous thing How can I change the case so it will not read the options as arguments? So far getopt is not serving me very well, because I also ran into another bug/feature as well: the 'optional argument' (::) is not working as I expected as it only works when used with an '=' between option and argument.

Versions:

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

Solution

  • getopt doesn't know about the semantics of your options. For all it knows, --colors is a valid argument for the -i option. You have to check for these kinds of errors yourself, unfortunately, if you want to handle them.

     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
            ;;
    
        ...