Search code examples
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.