Mac OS: Bash GetOpts Sometimes Ignored - bash

I wrote a small bash script in my bash_profile. I want to use getopts to parse options.
deployMenu() {
noInstallDependencies='false'
build='true'
echo "Args: $#"
while getopts 'db' flag; do
echo "flag ${flag}"
case "${flag}" in
d) noInstallDependencies='true' ;;
b) build='false' ;;
#*) echo "Unexpected option ${flag}" ;;
#\?) echo "Unexpected option ${flag}" ;;
esac
done
echo "noInstallDependencies $noInstallDependencies"
echo "build $build"
If I run the command multiple times, the argument is ignored. I have to run diff. flags in order to get it recognized.
User:project User$ deployMenu -b
Args: -b
noInstallDependencies false
build true
User:project User$ deployMenu -b
Args: -b
noInstallDependencies false
build true
User:project User$ deployMenu --b -b
Args: --b -b
flag b
noInstallDependencies false
build false
User:project User$ deployMenu --b -b
Args: --b -b
noInstallDependencies false
build true
As you can see the flag is only recognized after altering the params from -b to --<something> -b. I first thought the first param is ignored but running twice --b -b also fails. Is there any cache or anything to reset first? To get it working by first using -b and then switching to --b -b is reproducible.

Since you are calling a shell function repeatedly in the same shell instance, the value of $OPTIND isn't being reset between calls to deployMenu. This affects which option getopts sees as "next" each time it is called. Try your same experiment with deployMenu ...; echo $OPTIND. The solution is probably just to explicitly set OPTIND=1 if you plan on calling deployMenu multiple times.
deployMenu() {
noInstallDependencies='false'
build='true'
echo "Args: $#"
OPTIND=1
while getopts 'db' flag; do
...
}

Related

How to handle errors in getopt

Based on tutorials I found here and here getopt should provide me with information about errors using some combination of characters :?*.
But when I used this code:
#!/bin/bash
eval set -- "$(getopt -o hspna: --long help,server,project,name-prefix,action: -- "$#")"
while [ : ]; do
case "$1" in
-s | --server)
echo "Setting server"
shift
;;
-p | --project)
echo "Setting project"
shift
;;
-n | --name-prefix)
echo "Setting name prefix"
shift
;;
-a | --action)
echo "Setting action"
shift
;;
--)
shift
break
;;
-h | --help)
echo "Providing help 1"
exit
;;
:)
echo "Providing help 2"
exit
;;
?)
echo "Providing help 3"
exit
;;
*)
echo "Providing help 4"
exit
;;
esac
done
echo $#
echo "Configured"
exit
Then following command that was supposed to show an error gave me the following output:
$ ./debug.sh -a -s -b -- foo bar baz
getopt: invalid option -- 'b'
Setting action
Setting server
foo bar baz
Configured
I was expecting that:
Providing help 2 will appear due to -a missing a value
Providing help 3 will appear due to -b not being a valid parameter
Providing help 4 will appear due to overall errors
Configured should never appear since the previous 3 points have an exit
But none of the above was true.
Also when testing further even more things did not work as expected.
# Expecting error due to missing value for `-a` but instead everything worked fine
$ ./debug.sh -a -s
Setting action
Setting server
Configured
# This time I expected everything to work fine, since I provided `X` as value of `-a`, but error was shown.
$ ./debug.sh -aX
Setting action
Providing help 3
What am I doing wrong?
What am I doing wrong?
Util-linux getopt prints and handles errors.
if ! args="$(getopt \
-n your_command \
-o hspna: \
--long help,server,project,name-prefix,action: \
-- "$#"\
)"; then
exit 1
fi
eval "set -- $args"
...
$ ./util -a
your_command: option requires an argument -- 'a'
I was expecting that:
I do not understand why. There is no such documentation in getopt. No, getopt will not output ? nor :. You can handle your (as the author of the program) errors, like you forgot to handle the option in case that you have given to getopt - you handle that with *).
The ? is a glob that matches any character. Because you forgot a shift after esac before done, X remains in $1, which is one character and is matched by ?). You meant '?'). This should go into *) case, and you should print yourself an error message.
Example, subjective in my style that I use (many people do not like set -eu):
set -euo pipefail
args=$(getopt -o ab -- "$#")
eval "set -- $args"
aflag=0
while (($#)); do
case "$1" in
-a) afloag=1; ;;
--) shift; break;
*) echo "Och no, I forgot about -b, or some other error!" >&2; exit 1; ;;
easc
shift
done

Why do shortened versions of long options work with getopt?

In the following script:
#!/usr/bin/env bash
func_usage ()
{
cat <<EOF \
USAGE: ${0} \
EOF
}
## Defining_Version
version=1.0
## Defining_Input
options=$(getopt -o "t:" -l "h,help,v,version,taxonomy:" -a -- "$#")
eval set -- "$options"
while true;do
case $1 in
-h|--h|-help|--help)
func_usage
exit 0
;;
-v|--v|-version|--version)
echo $version
;;
-t|--t|-taxonomy|--taxonomy)
echo "Option t = $2 ";
Taxonomy_ID=$2
echo $Taxonomy_ID
shift
;;
--)
shift
break;;
esac
shift
done
## Defining Taxonomy Default Value (in case is not provided)
TaxonomyID=${Taxonomy_ID:=9606};
echo $TaxonomyID
exit 0
The commands:
./script.sh -v
./script.sh --v
./script.sh -version
./script.sh --version
Work as expected. But what I do not understand is why the commands:
./script.sh -ver
./script.sh --ver
work at all. An equivalent unexpected behavior is also observed for the commands:
./script.sh -tax 22
./script.sh --tax 22
I would be grateful to get an explanation and/or a way to correct this unexpected behavior.
Note that getopt is an external utility unrelated to Bash.
what I do not understand is why the commands: .. work at all.
Because getopt was designed to support it, there is no other explanation. From man getopt:
[...] Long options may be abbreviated, as long as the abbreviation is not ambiguous.
Unambiguous abbreviations of long options are converted to long options.
Based on the comments I have received, specially from #CharlesDuffy, I have modified my code to what I believe is a more robust and compatible version. Importantly, the code below addresses the pitfalls of the original code
#!/usr/bin/env bash
func_usage ()
{
cat <<EOF
USAGE: ${0}
EOF
## Defining_Version
version=1.0
## Defining_Input
while true;do
case $1 in
-h|--h|-help|--help|-\?|--\?)
func_usage
exit 0
;;
-v|--v|-version|--version)
echo $version
;;
-t|--t|-taxonomy|--taxonomy)
echo "Option t = $2 ";
Taxonomy_ID=$2
echo $Taxonomy_ID
shift
;;
--)
shift
break;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
;;
*)
break
esac
shift
done
TaxonomyID=${Taxonomy_ID:=9606};
echo $TaxonomyID
exit 0
The code above behaves as expected in that the commands:
./script -tax 22
Gives the warning:
WARN: Unknown option (ignored): -tax
9606
As expected

Handling unused getopts argument

I have a script that starts with getopts and looks as follows:
USAGE() { echo -e "Usage: bash $0 [-w <in-dir>] [-o <out-dir>] [-c <template1>] [-t <template2>] \n" 1>&2; exit 1; }
if (($# == 0))
then
USAGE
fi
while getopts ":w:o:c:t:h" opt
do
case $opt in
w ) BIGWIGS=$OPTARG
;;
o ) OUTDIR=$OPTARG
;;
c ) CONTAINER=$OPTARG
;;
t ) TRACK=$OPTARG
;;
h ) USAGE
;;
\? ) echo "Invalid option: -$OPTARG exiting" >&2
exit
;;
: ) echo "Option -$OPTARG requires an argument" >&2
exit
;;
esac
done
more commands etc
echo $OUTDIR
echo $CONTAINER
I am fairly new to getopts. I was doing some testing on this script and at some stage, I didn't need/want to use the -c argument [-c ]. In other words, I was trying to test another specific part of the script not involving the $CONTAINER variable at all. Therefore, I simply added # in front of all commands with the $CONTAINER and did some testing which was fine.
When testing the script without using $CONTAINER, I typed:
bash script.bash -w mydir -o myoutdir -t mywantedtemplate
However, I was wondering, given my getopts command I didn't get a warning. In other words, why did I not get a warning asking for -c argument. Is this possible? Does the warning only occur if I type:
bash script.bash -w mydir -o myoutdir -t mywantedtemplate -c
UPDATE
After doing some testing, I think that is it:
If you don't explicitly write "-c", getopts won't "ask" you for it and give you an error (unless your script is doing something with it - i.e. if you haven't put # in front of each command using this argument)
You only get an error if you put "-c "
Is this correct?
getopts does not warn when some options are not used (i.e. they are optional). Usually that's a good thing because some options (e.g. -h) are not used with other options. There is no way to specify mandatory options directly with the Bash builtin getopts. If you want mandatory options then you will need to write code to check that they have been used. See bash getopts with multiple and mandatory options. Also (as you have found), you won't get an error if you fail to write code to handle options specified in the optstring (first) argument to getopts.
You could get a kind of automatic warning for mandatory arguments by using the nounset setting in your Bash code (with set -o nounset or set -u). That would cause warnings to be issued for code like echo $CONTAINER if the -c option is not specified so $CONTAINER is not set. However, using the nounset option would mean that all of your code needs to be written more carefully. See How can I make bash treat undefined variables as errors?, including the comments and "Linked" answers, for more information.
You might use this script:
printf captures the stdout and put into the stderr and then back to the stdout also captures the exit code, so we can handle if the code is greater than 0.
some_command() {
echo 'this is the stdout'
echo 'this is the stderr' >&2
exit 1
}
run_command() {
{
IFS=$'\n' read -r -d '' stderr;
IFS=$'\n' read -r -d '' stdout;
(IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(some_command)" "${?}" 1>&2) 2>&1)
}
echo 'Run command:'
if ! run_command; then
## Show the values
typeset -p stdout stderr
else
typeset -p stdout stderr
fi
Just replace some_command with getopts ":w:o:c:t:h"

Pass options received in bash script to a called script, command or builtin

I have a bash script myscript.sh.
I mean to call another script, command or builtin from within it, e.g., diff.
I mean to pass options to myscript.sh, some of which would be passed to diff when calling it.
The way I implemented this is by setting up an option string optstring via getopt, and then using
eval "diff ${optstring} ${file} ${TRG_DIR}/${filebase2}"
So far, it worked, but I do not know if this is prone to issues when passing arguments with wildcards, etc. At any rate, ...
Is there a better way to do it?
The way I set up optstring is
set -o errexit -o noclobber -o nounset -o pipefail
params="$(getopt -o qy --long brief,side-by-side,suppress-common-lines --name "$0" -- "$#")"
if [ $? != 0 ] ; then echo "Failed parsing options." >&2 ; exit 1 ; fi
echo params=$params
echo params=$#
eval set -- "$params"
optstring=""
# These variables are likely not needed
brief=false
sbs=false
scl=false
#while false ; do
while true ; do
case "$1" in
-q|--brief)
optstring=${optstring}" -q"
brief=true
echo "brief"
shift
;;
-y|--side-by-side)
optstring=${optstring}" -y"
sbs=true
echo "side-by-side"
shift
;;
--suppress-common-lines)
optstring=${optstring}" --suppress-common-lines"
scl=true
echo "suppress-common-lines"
shift
;;
--)
shift
break
;;
*)
echo "Not implemented: $1" >&2
exit 1
;;
esac
done
echo optstring=${optstring}
Use an array. Arrays can handle multi-word arguments with whitespace. Initialize a blank array with:
options=()
To append an option, do:
options+=(--suppress-common-lines)
Then finally you can get rid of the eval when you call diff and just call it normally. Make sure to quote all of the variable expansions in case they have whitespace:
diff "${options[#]}" "$file" "$TRG_DIR/$filebase2"

Why does eval exit subshell mid-&& with set -e?

Why does bash do what I'd expect here with a compound command in a subshell:
$ bash -x -c 'set -e; (false && true; echo hi); echo here'
+ set -e
+ false
+ echo hi
hi
+ echo here
here
But NOT do what I'd expect here:
$ bash -x -c 'set -e; (eval "false && true"; echo hi); echo here'
+ set -e
+ eval 'false && true'
++ false
Basically, the difference is between 'eval'-uating a compound command and just executing a compound command. When the shell executes a compound command, non-terminal commands in the compound command that fail do not cause the entire compound command to fail, they simply terminate the command. But when eval runs the compound command and any non-terminal sub-command terminates the command with an error, eval terminates the command with an error.
I guess I need to format my eval statement like this:
eval "false && true" || :
so that the eval command doesn't exit my subshell with an error, because this works as I'd expect it to:
$ bash -x -c 'set -e; (eval "false && true" || :; echo hi); echo here'
+ set -e
+ false
+ echo hi
hi
+ echo here
here
The problem I have with this is that I've written a function:
function execute() {
local command="$1"
local remote="$2"
if [ ! -z "$remote" ]; then
$SSH $remote "$command" || :
else
eval "$command" || :
fi
}
I'm using set -e in my script. The same problem occurs with ssh in this function - if the last command in the ssh script is a compound command that terminates early, the entire command terminates with an error. I want commands like this to behave as if they were executing locally - early terminating compound commands should not cause ssh or eval to return 1, failing the entire command. If I tack || : on the end of my eval statement or my ssh statement, then all such commands will succeed, even if they shouldn't because the last command in the eval'd or ssh'd command failed.
Any ideas would be much appreciated.
I should also mention that set -e is terribly error-prone; see http://mywiki.wooledge.org/BashFAQ/105 for a bunch of examples. So the best solution might be to dispense with it, and write your own logic to detect errors and abort.
That out of the way . . .
The problem here is that eval "false && true" is a single command, and evaluates to false (nonzero), so set -e aborts after that command runs.
If you were instead to run eval "false && true; true", you would not see this behavior, because then eval evaluates to true (zero). (Note that, although eval does implement the set -e behavior, it obeys the rule that false && true is non-aborting.)
This is not actually specific to eval, by the way. A subshell would give the same result, for the same reason:
$ bash -x -c 'set -e; (false && true); echo here'
+ set -e
+ false
The simplest fix for your problem is probably just to run an extra true if the end is reached:
$SSH $remote "set -e; $command; true"
eval "$command; true"
eval counts as its own command with its own exit code.
Since eval "false && true" returns an exit code of 1, it triggers set -e.

Resources