Kalib Zen
Kalib Zen

Reputation: 925

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

Upvotes: 0

Views: 258

Answers (1)

Paul Hodges
Paul Hodges

Reputation: 15246

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.

Upvotes: 2

Related Questions