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:
-e <boolean> (value is based from the value passed)
-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:
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"
$: 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
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"
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'"
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.