23

Everything I see uses getopt or the slightly-fancier getopts which only supports one-character options (e.g., -h but not --help). I want to do fancy long options.

Ian Bicking
  • 9,762
  • 6
  • 33
  • 32
  • 1
    possible duplicate of [Using getopts in bash shell script to get long and short command line options](http://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options) – Jonathan Leffler Feb 14 '12 at 03:55
  • 2
    @JonathanLeffler Really not a duplicate as `getops` does not always have to be the answer – Potherca Mar 06 '15 at 13:20
  • 1
    @Potherca: And indeed, `getopts` is *not* the answer, as explained both in this question and in the answers to that question. – ruakh Jul 28 '16 at 21:42

3 Answers3

30

I've done something like this:

_setArgs(){
  while [ "${1:-}" != "" ]; do
    case "$1" in
      "-c" | "--configFile")
        shift
        configFile=$1
        ;;
      "-f" | "--forceUpdate")
        forceUpdate=true
        ;;
      "-r" | "--forceRetry")
        forceRetry=true
        ;;
    esac
    shift
  done
}

As you can see, this supports both the single-character and the longer options nicely. It allows for values to be associated with each argument, as in the case of --configFile. It's also quite extensible, with no artificial limitations as to what options can be configured, etc.

As included above, the "${1:-}" prevents an "unbound variable" error when running in bash "strict" mode (set -euo pipefail).

ziesemer
  • 27,712
  • 8
  • 86
  • 94
  • 1
    This would work, but wouldn't obey some conventions: folding together single-letter arguments, and things like `-configFile=filename` (the `=`). – Ian Bicking Feb 14 '12 at 04:17
  • 1
    @IanBicking - Very true. However, for my case, I decided to allow for this over the frustration with getopt/getops echoed in the many (though still valuable) answers provided in the marked duplicate at http://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options. – ziesemer Feb 14 '12 at 04:19
  • 1
    Ah... that question was not among the candidates for duplicates when I wrote my question. Thanks! – Ian Bicking Feb 14 '12 at 06:15
  • 1
    Thanks! Call the function with: _setArgs $* – Masa Mar 31 '14 at 08:38
  • 4
    @Masa Shouldn't that be `_setArgs "$@"` ? – Potherca Mar 06 '15 at 13:20
13

Assuming that you "want to do fancy long options" regardless of the tool, just go with getopt (getopts seems to be mainly used when portability is crucial). Here's an example of about the maximum complexity that you'll get:

params="$(getopt -o e:hv -l exclude:,help,verbose --name "$(basename "$0")" -- "$@")"

if [ $? -ne 0 ]
then
    usage
fi

eval set -- "$params"
unset params

while true
do
    case $1 in
        -e|--exclude)
            excludes+=("$2")
            shift 2
            ;;
        -h|--help)
            usage
            ;;
        -v|--verbose)
            verbose='--verbose'
            shift
            ;;
        --)
            shift
            break
            ;;
        *)
            usage
            ;;
    esac
done

With this code, you can specify -e/--exclude more than once, and ${excludes[@]} will contain all of the given excludes. After processing (-- is always present) anything remaining is stored in $@.

l0b0
  • 55,365
  • 30
  • 138
  • 223
  • `basename`does not take the path as an option, but as an argument. (I could not edit the answer, because the system requires at least 6 character edits.) – jarno Jul 28 '16 at 17:16
  • Thanks @jarno, don't know how that snuck in. – l0b0 Jul 28 '16 at 21:03
  • 1
    For another thing, I think you don't need to use `${2-}`, but `$2` is enough, since `getopt` will exit with a non-zero code, if there was no argument for `-e`, right? – jarno Jul 29 '16 at 13:46
  • Additionally, to the options that require arguments (like `--exclude`, above) you might wanna add a line to check if the user did not forget the mandatory value while in the middle of the command line. I got into a situation where a typo produces the name of the next option as a parameter of the previous one which had that value missing in the command line typed as `foo.sh --exclude --verbose` for example. -e|--exclude) [[ "$2" =~ ^- ]] && echo "Missing argument for '$1' && exit 1 excludes+=("$2") shift 2 ;; – Mauro Ribeiro Feb 23 '23 at 10:52
1

I have created a bash function that is the easiest to use and needs no customization. Just use the function and pass all long options with or without arguments and the function will set them as variables with the corresponding option arguments as values in your script.

function get_longOpt {
  ## Pass all the script's long options to this function.
  ## It will parse all long options with its arguments,
  ## will convert the option name to a variable and
  ## convert its option value to the variable's value.
  ## If the option does not have an argument, the
  ## resulting variable's value will be set to true.
  ## Works properly when providing long options, only.
  ## Arguments to options may not start with two dashes.
  ##
  ####  Usage
  ##
  ## get_longOpt $@
  ##
  ##  May expand to:
  ##
  ## get_longOpt --myOption optimopti --longNumber 1000 --enableMe --hexNumber 0x16
  ##
  ### Results in the bash interpretation of:
  ## myOption=optimopti
  ## longNumber=1000
  ## enableMe=true
  ## hexNumber=0x16
  ##
  local -a opt_list=( $@ )
  local -A opt_map
  local -i index=0
  local next_item
  for item in ${opt_list[@]}; do
    # Convert arg list to map.
    let index++
    next_item="${opt_list[$index]}"
    if   [[ "${item}" == --* ]] \
      && [[ "${next_item}" != --* ]] \
      && [[ ! -z "${next_item}" ]]
    then
      item="$(printf '%s' "${item##*-}")"
      opt_map[${item}]="${next_item}"
    elif [[ "${item}" == --* ]] \
      && { [[ "${next_item}" == --* ]] \
      || [[ -z "${next_item}" ]]; }
    then
      item="$(printf '%s' "${item##*-}")"
      opt_map[${item}]=true
    fi
  done
  for item in ${!opt_map[@]}; do
    # Convert map keys to shell vars.
    value="${opt_map[$item]}"
    [[ ! -z "${value}" ]] && \
    printf -v "$item" '%s' "$value"
  done
}

The up to date original source code is available here:

https://github.com/theAkito/akito-libbash/blob/master/bishy.bash

Akito
  • 115
  • 2
  • 7