bashgetopt

Make getopt optionally accept an argument without showing error


I'm trying to make the getopt to optionally accept an argument. For example, based on the code below:


#!/bin/bash
short_opts="e:"

options=$(getopt  -o "${short_opts}" -- "$@")
retval=$?

if [[ "${retval}" != 0 ]]; then
  echo "Invalid option"
  exit 1
fi

eval set -- "${options}"

enable_value=false
while true; do
  option="$1"
  case "${option}" in
  -e)
    enable_value=$2
    shift 2
    echo "enable is: ${enable_value}"
    ;;
  --)
    shift
    break
    ;;
  -*)
    echo "invalid"
    exit 1
    ;;
  *)
    break
    ;;
  esac
done

In the above code, I can run an argument -e like this

./script.sh -e true

Then it will print out this output

enable is: true

Now what I want is, to use the same option -e without any argument

./script.sh -e

and my expected output should be this:

enable is: true

but when I use -e without argument, it will complain that it needs an argument. I understand what is going on because I should put the symbol e: so that it can accept the argument.

So, what I have done is I try to add another e: argument for the short options like below:

short_opts="e,e:"

and obviously it did not work

In my code above in order to be able to pass -e alone it is easy to change the above code to be like this (only a small change):

#!/bin/bash
short_opts="e"

options=$(getopt  -o "${short_opts}" -- "$@")
retval=$?

if [[ "${retval}" != 0 ]]; then
  echo "Invalid option"
  exit 1
fi

eval set -- "${options}"

enable_value=false
while true; do
  option="$1"
  case "${option}" in
  -e)
    enable_value=false
    shift
    echo "enable is: ${enable_value}"
    ;;
  --)
    shift
    break
    ;;
  -*)
    echo "invalid"
    exit 1
    ;;
  *)
    break
    ;;
  esac
done

But, based on my 2 codes above, is there a way to make getopt accept both argument and empty argument (by just passing an option -e alone)?

In brief I want to be able to pass the following 2 syntax:

  1. -e <boolean> (value is based from the value passed)

  2. -e (will make the value of variable enable_value became true)

I also have read this and it does not related to what I asked:

how to make an argument optional in getopt bash


Solution

  • Please c.f. Unable to read bash shell script arguments

    If you have even one more than just that one -e option, then this is a near-untenable situation, and your users will hate you.

    I usually try to set required defaults silently in my code with lines like

    : ${e:=false}
    

    That way, if not set, it gets a sane default. If inherited, exported, set on the CLI (etc) then it uses whatever value is present.

    With the following code -

    $: cat tst
    #! /bin/bash
    
    declare x
    while getopts "xe" o
    do case "$o" in
       x) x=1; echo "X is set";;
       e) if [[ -n "${e:-}" ]]
          then echo >&2 "e inherited value '$e', cannot set"; exit 1
          else e=true; echo "E set to $e"
          fi ;;
       [?]) echo "oops"; exit ;;
       esac
    done
    : ${e:=false}
    
    declare -p x e
    

    Consider the following cases -

    $: ./tst                         # NO ARGUMENTS
    declare -- x
    declare -- e="false"
    
    $: ./tst -x                      # one arg, not -e
    X is set
    declare -- x="1"
    declare -- e="false"
    
    $: ./tst -e                      # one arg, -e
    E set to true
    declare -- x
    declare -- e="true"
    
    $: ./tst -ex                     # both args, empty
    E set to true
    X is set
    declare -- x="1"
    declare -- e="true"
    
    $: ./tst -f                      # invalid argument
    ./tst: illegal option -- f
    oops
    
    $: e=foo ./tst                   # no args, e inherited/exported/pre-set
    declare -- x
    declare -x e="foo"
    
    $: e=foo ./tst -x                # one non -e arg, e inherited/exported/pre-set
    X is set
    declare -- x="1"
    declare -x e="foo"
    

    With an export -

    $: export e=foo
    
    $: ./tst -x                      # letting the export stand
    X is set
    declare -- x="1"
    declare -x e="foo"
    
    $: e= ./tst -x                   # override/unset
    X is set
    declare -- x="1"
    declare -x e="false"
    

    expicit overrides -

    $: e=bar ./tst                   # uses bar
    declare -- x
    declare -x e="bar"
    
    $: e=bar ./tst -x                # same
    X is set
    declare -- x="1"
    declare -x e="bar"
    
    $: e= ./tst -x -e                # override/unset, then set internal true
    X is set
    E set to true
    declare -- x="1"
    declare -x e="true"
    

    still doesn't allow both, either way.

    $: ./tst -x -e                   # trying to set, didn't override export
    X is set
    e inherited value 'foo', cannot set
    
    $: e=bar ./tst -x -e             # override, but set, can't use -e
    X is set
    e inherited value 'bar', cannot set
    

    (End of export assumptions...)

    These all work well enough, but when you start trying to use an optional argument -

    $: ./tst -e foo                  # e: *requires*, e w/o : *ignores*
    E set to true
    declare -- x
    declare -- e="true"
    
    $: e=foo ./tst -e bar            # e: *requires*, e w/o : *ignores*
    e inherited value 'foo', cannot set
    
    $: e=foo ./tst -ex               # e: *requires*, e w/o : *ignores*
    e inherited value 'foo', cannot set
    

    and of course,

    $: ./tst -x -efoo
    X is set
    E set to true
    ./tst: illegal option -- f
    oops
    

    Like most programs, you can stack args, but this blows up as soon as it doesn't recognize one as a boolean option.

    Changing

    while getopts "xe" o
    

    to

    while getopts "xe:" o # just adding the colon 
    

    requires we also change

      else e=true; echo "E set to $e"
    

    to

      else e=$OPTARG; echo "E set to $e"
    

    This gives -

    $: ./tst                         # same
    declare -- x
    declare -- e="false"
    
    $: ./tst -x                      # same
    X is set
    declare -- x="1"
    declare -- e="false"
    
    $: ./tst -e foo                  # works like a champ...
    E set to foo
    declare -- x
    declare -- e="foo"
    

    and (almost surprisingly), these work...

    $: ./tst -efoo
    E set to foo
    declare -- x
    declare -- e="foo"
    
    $: ./tst -x -efoo                # I hate this
    X is set
    E set to foo
    declare -- x="1"
    declare -- e="foo"
    
    $: ./tst -efoo -x
    E set to foo
    X is set
    declare -- x="1"
    declare -- e="foo"
    

    but

    $:  ./tst -e
    ./tst: option requires an argument -- e
    oops
    

    While the -e can be omitted, if you do use it, the argument isn't "optional" at all.

    AND -

    $: ./tst -e -x                   # this one really tangles users.
    E set to -x
    declare -- x
    declare -- e="-x"
    

    Don't use getopt

    If that's why you are using getopt instead of getopts, I recommend finding another way.
    You can make getopt work - sort of... but don't.

    Looking at it -
    c.f. https://ss64.com/osx/getopt.html

    $: cat tst
    #! /bin/bash
    short_opts="xe::"
    options=$(getopt -o "${short_opts}" -- "$@")
    if (($?))
    then echo "Invalid option"
         exit 1
    fi
    set -- ${options} # no eval and no quotes - which will eventually cause problems
    declare x
    while [[ -n "$1" ]]
    do case "$1" in
       -x) x=1; echo "X is set";;
       -e) if [[ -n "${e:-}" ]]
           then echo >&2 "e inherited value '$e', cannot set"; exit 1
           fi
           if [[ -n "$2" ]]
           then e="$2"; shift
           else e=true
           fi ;;
       --) shift; break;;
       -*) echo "oops"; exit ;;
       esac
       shift
    done
    : ${e:=false}
    
    declare -p x e
    

    This feels like a lot of hackery to me. In use:

    $: ./tst                         # ok
    declare -- x
    declare -- e="false"
    
    $: ./tst -x                      # ok
    X is set
    declare -- x="1"
    declare -- e="false"
    
    $: ./tst -efoo                   # ugh... works, but who does this?
    declare -- x
    declare -- e="foo"
    
    $: ./tst -xefoo                  # works... but very confusing
    X is set
    declare -- x="1"
    declare -- e="foo"
    
    $: ./tst -x -efoo                # works, still ugly
    X is set
    declare -- x="1"
    declare -- e="foo"
    
    $: ./tst -efoo -x                # works, but one habitual space breaks
    X is set
    declare -- x="1"
    declare -- e="foo"
    

    but these don't work because the space after the -e is not allowed.

    $: ./tst -x -e foo               # e is '', foo is silently IGNORED...
    X is set
    declare -- x="1"
    declare -- e="''"
    
    $: ./tst -e foo -x               # SAME
    X is set
    declare -- x="1"
    declare -- e="''"
    

    With no arg -

    $ ./tst -e                       # oops, still empty e
    declare -- x
    declare -- e="''"
    
    $: ./tst -e -x                   # effectively same again
    X is set
    declare -- x="1"
    declare -- e="''"
    

    So basically, still not optional...
    And if you expect stacking, this one is bad enough...

    $: ./tst -xe
    X is set
    declare -- x="1"
    declare -- e="''"
    

    but THIS guy...

    $: ./tst -ex           # doesn't set x - assigns the x to e
    declare -- x
    declare -- e="'x'"
    

    final observations

    If you just want optional...

    $: cat tst
    #! /bin/bash
    declare x e
    : ${e:=false}          # set a default
    declare -p x e
    
    $: ./tst             
    declare -- x
    declare -- e="false"
    
    $: x=foo ./tst           
    declare -x x="foo"
    declare -- e="false"
    
    $: x=2 e=true ./tst
    declare -x x="2"
    declare -x e="true"
    

    No parsing. Plenty of options for testing.

    Good luck.