How to make getopts parameter optional
I wrote the following (probably simple) bash script as a budgeting tool to more accurately calculate (and moderate) when I last bought a pack of cigarettes.
In addition to -h for printing the output, it takes one other option: -b with (what is intended to be) an optional parameter to set the offset in hours. So -b 5 logs a new pack of cigs bought 5 hours ago, and subsequent runs of cigs.sh indicate how many weeks, days or hours it's been since then. If the elapsed time since the last time I bought a pack is less than $threshold, the output is printed in red; if the elapsed time is more than $threshold, the output is green, indicating it's now "okay" for me to buy a new pack.
The problem is while I've managed to get all this working fine except for one thing: it's currently not possible to specify -b without an offset (i.e. log the purchase as being made right now instead of some hours into the past). As far as I've been able to tell this seems to be a limitation of getopts: without the colon in b:h, $OPTARG fails to populate causing the logic in the b) case option to fail; but with the colon, it prevents -b from being used without a parameter, throwing the following error:
/usr/local/bin/scripts/cigs.sh: option requires an argument -- b
Unknown parameter passed: -b
Is it possible to get around this for my script?
Here is my script in full, and reproduced below is the getopts portion of it:
while getopts "b:h" OPTION; do case $OPTION in h) echo "$usage" exit ;; b) offset=$OPTARG if [[ -n $offset && ! $offset =~ ^[0-9]+$ ]]; then echo "HOURS parameter passed to -b must be an integer." exit fi echo -e "Bought a new $item...\nResetting timer to 0 days and $offset hours." offset=$((offset*60*60)) last_bought="$(date -u +%s)" new_lb=$((last_bought-offset)) echo $new_lb > $lb_file exit ;; *) echo "Unknown parameter passed: $1" exit 1 ;; esac
done 3 1 Answer
Very interesting issue here. Based on what I understand about how getopts works, this use of a single colon (:) means that the option value must be set:
getopts "b:h"And if you wanted it to be optional, you would need to set two colons (::) and also have nothing follow that like this:
getopts "hb::"But that hb:: doesn’t work.
I played around with your script and this is the only solution I could get to work. It changes your getopts "b:h" to be getopts "bh" so both option parameters are optional. But the magic is in adding more code to the b) case so the whole while chunk now looks like this:
while getopts "bh" OPTION; do case $OPTION in h) echo "$usage" exit ;; b) eval nextopt=\${$OPTIND} if [[ -n $nextopt && $nextopt != -* ]] ; then OPTIND=$((OPTIND + 1)) offset=$nextopt else offset=0 fi if [[ -n $offset && ! $offset =~ ^[0-9]+$ ]]; then echo "HOURS parameter passed to -b must be an integer." exit fi echo -e "Bought a new $item...\nResetting timer to 0 days and $offset hours." offset=$((offset*60*60)) last_bought="$(date -u +%s)" new_lb=$((last_bought-offset)) echo $new_lb > $lb_file exit ;; *) echo "Unknown parameter passed: $1" exit 1 ;; esac
doneThe magic comes from this chunk over here that is based on this other Stack Overflow answer to a question about the same issue:
# Check next positional parameter
eval nextopt=\${$OPTIND}
# existing or starting with dash?
if [[ -n $nextopt && $nextopt != -* ]] ; then
OPTIND=$((OPTIND + 1))
offset=$nextoptClearly not as clean as what would be expect if the getopts "hb::" worked, but it doesn’t so this kludge works.
If anyone else more experienced with Bash can explain why — or come up with a cleaner solution — they should post it. For now, this works so it works!
1