0

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

9
  • 1
    Why aren't you using getopts in the while loop that processes arguments? Commented Dec 23, 2022 at 16:36
  • @Barmar didn't I use while loop for that ? Commented Dec 23, 2022 at 16:37
  • No, I mean something like while getopts ...; do Commented Dec 23, 2022 at 16:38
  • I saw your new edit from getopt to getopts. The reason I use getopt as opposed from getopts is because I want to have a long options that is easier to implement. How do you use getopt to process arguments in while loop. I dont get it as I think it was done. Commented Dec 23, 2022 at 16:39
  • 3
    See ComplexOptionParsing on the Wooledge wiki (home of the #bash IRC channel) for a discussion of things they consider bad ideas, and BashFAQ #35 for discussion of the approaches they consider good ideas. Note that getopt is in the former category. Commented Dec 23, 2022 at 17:07

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

5 Comments

I'm testing yours but doesn't seems like my geopt version cameout the same output like urs. On the 3rd test ./tst -e i got this output declare -- x declare -- e="''" .
I'm trying to undertand that if the enchanced getopt would have issue on what you said here.
Sorry, editing error. Make more sense now?
The one that you mention don't work ./tst -x -e foo is actually working for me. Same thing like ./tst -e foo -x (I got the correct output). and it is working because I changed xe:: to xe: because I don't know why did you put extra : there. My first question is why do you have the double colon at the end like this xe:: and not this xe:
getopt vs getopts. Notice how they are used completely differently in the respective versions of the scripts. getopt "allows" an optional argument if you use two colons, which is really a bad design. getopts does not.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.