How to handle errors in getopt - bash

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

Related

Update parameter with default value in a bash script [duplicate]

I'm trying to make a getopt command such that when I pass the "-ab" parameter to a script,
that script will treat -ab as a single parameter.
#!/bin/sh
args=`getopt "ab":fc:d $*`
set -- $args
for i in $args
do
case "$i" in
-ab) shift;echo "You typed ab $1.";shift;;
-c) shift;echo "You typed a c $1";shift;;
esac
done
However, this does not seem to work. Can anyone offer any assistance?
getopt doesn't support what you are looking for. You can either use single-letter (-a) or long options (--long). Something like -ab is treated the same way as -a b: as option a with argument b. Note that long options are prefixed by two dashes.
i was struggling with this for long - then i got into reading about getopt and getopts
single char options and long options .
I had similar requirement where i needed to have number of multichar input arguments.
so , i came up with this - it worked in my case - hope this helps you
function show_help {
echo "usage: $BASH_SOURCE --input1 <input1> --input2 <input2> --input3 <input3>"
echo " --input1 - is input 1 ."
echo " --input2 - is input 2 ."
echo " --input3 - is input 3 ."
}
# Read command line options
ARGUMENT_LIST=(
"input1"
"input2"
"input3"
)
# read arguments
opts=$(getopt \
--longoptions "$(printf "%s:," "${ARGUMENT_LIST[#]}")" \
--name "$(basename "$0")" \
--options "" \
-- "$#"
)
echo $opts
eval set --$opts
while true; do
case "$1" in
h)
show_help
exit 0
;;
--input1)
shift
empId=$1
;;
--input2)
shift
fromDate=$1
;;
--input3)
shift
toDate=$1
;;
--)
shift
break
;;
esac
shift
done
Note - I have added help function as per my requirement, you can remove it if not needed
That's not the unix way, though some do it e.g. java -cp classpath.
Hack: instead of -ab arg, have -b arg and a dummy option -a.
That way, -ab arg does what you want. (-b arg will too; hopefully that's not a bug, but a shortcut feature...).
The only change is your line:
-ab) shift;echo "You typed ab $1.";shift;;
becomes
-b) shift;echo "You typed ab $1.";shift;;
GNU getopt have --alternative option
-a, --alternative
Allow long options to start with a single '-'.
Example:
#!/usr/bin/env bash
SOPT='a:b'
LOPT='ab:'
OPTS=$(getopt -q -a \
--options ${SOPT} \
--longoptions ${LOPT} \
--name "$(basename "$0")" \
-- "$#"
)
if [[ $? > 0 ]]; then
exit 2
fi
A=
B=false
AB=
eval set -- $OPTS
while [[ $# > 0 ]]; do
case ${1} in
-a) A=$2 && shift ;;
-b) B=true ;;
--ab) AB=$2 && shift ;;
--) ;;
*) ;;
esac
shift
done
printf "Params:\n A=%s\n B=%s\n AB=%s\n" "${A}" "${B}" "${AB}"
$ ./test.sh -a aaa -b -ab=test
Params:
A=aaa
B=true
AB=test
getopt supports long format. You can search SO for such examples.
See here, for example

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"

BASH getopt command returns its own parameters instead of command line parameters

I am creating a BASH script and working with the BASH getopt command to parse command line arguments. Instead of returning the arguments provided on the command line, getopt returns the arguments that were supplied to the getopt command. I am not sure what could be going on, as the code that I have was working perfect, and seemingly out of nowhere, it has stoped working correctly (and no, I haven't updated anything or changed any code or environment settings). I can't use getopts (with the extra 's') because it is, for some unknown reason, not installed on the machine that will be running this script.
Even though the script is supplied with zero command line arguments, the getopt command is for some reason returning all of the arguments that I have supplied, minus the -o flag, instead of the expected -- value indicating the end of the options. The code that I have is as follows:
SHORT_OPTS=":hvso:l:d:n:p:t:"
LONG_OPTS="help,version,submit-job,output:,library:,job-dir:"
LONG_OPTS="${LONG_OPTS},num-nodes:,num-procs:,max-time:"
OPTS=$(getopt -o "${SHORT_OPTS}" -l "${LONG_OPTS}" -a -n "${PROG_NAME}" -- "${#}")
# Check for invalid command line options and arguments
if [[ ${?} -ne ${SUCCESS} ]] ; then
echo -e "${PROG_NAME}: error: Invalid option or argument\n" >&2
usage ; exit ${FAILURE}
else
echo "BEFORE $#"
eval set -- ${OPTS}
echo "AFTER $#"
fi
# Process command line options and their arguments
while true ; do
case "${1}" in
-h | --help)
# Display script usage information and exit
usage ; exit ${SUCCESS} ;;
-v | --version)
# Display script version information and exit
echo "${PROG_NAME} v${PROG_VERSION}" ; exit ${SUCCESS} ;;
-s | --submit-job)
# Enable automatic submission of the Moab job
JOB_AUTO_SUBMIT="${PREF_YES}" ; shift 1 ;;
-o | --output)
# Set the base name for output file names
TARGET="${2}" ; shift 2 ;;
-l | --library)
# Set the library to use for NWChem atomic configurations
NW_LIB="${2}" ; shift 2 ;;
-d | --job-dir)
# Ensure the specified directory for the Moab job exists
if [[ -e "${2}" ]] ; then
JOB_WORK_DIR=$(resolvePath "${2}") ; shift 2
else
echo -e "${PROG_NAME}: error: -d ${2}: No such directory\n"
usage ; exit ${FAILURE}
fi ;;
-n | --num-nodes)
# Ensure the number of compute nodes is greater than zero
if positiveInt "${2}" ; then
JOB_NODES="${2}" ; shift 2
else
echo -n "${PROG_NAME}: error: -n ${1}: Number of "
echo -e "job nodes must be a positive integer\n"
usage ; exit ${FAILURE}
fi ;;
-p | --num-procs)
# Ensure the number of processors per node is greater than zero
if positiveInt "${2}" ; then
JOB_PROCS="${2}" ; shift 2
else
echo -n "${PROG_NAME}: error: -p ${2}: Number of "
echo -e "processors per node must be a positive integer\n"
usage ; exit ${FAILURE}
fi ;;
-t | --max-time)
# Ensure the maximum job runtime is in the correct format
if [[ "${2}" == [0-9][0-9]:[0-9][0-9]:[0-9][0-9] ]] ; then
JOB_MAX_TIME="${2}" ; shift 2
else
echo -n "${PROG_NAME}: error: -t ${2}: Invalid time "
echo -e "format, please use hh:mm:ss format\n"
usage ; exit ${FAILURE}
fi ;;
--)
# No more options to process
shift ; break ;;
esac
done
# Check to see if POTCAR and CONTCAR locations were specified
if [[ ${#} -eq 2 ]] ; then
# Regular expressions for identifying POTCAR and CONTCAR files
PCAR_REGEX="[Pp][Oo][Tt][Cc][Aa][Rr]"
CCAR_REGEX="[Cc][Oo][Nn][Tt][Cc][Aa][Rr]"
# Attempt to identify POTCAR and CONTCAR argument ordering
if [[ ${1} =~ ${PCAR_REGEX} && ${2} =~ ${CCAR_REGEX} ]] ; then
POTCAR="${1}" ; CONTCAR="${2}" ; shift 2
else
POTCAR="${2}" ; CONTCAR="${1}" ; shift 2
fi
# Accept exactly two or zero command line arguments
elif [[ ${#} -ne 0 ]] ; then
echo "${PROG_NAME}: error: ${#}: Invalid argument count, expected [2|0]"
echo "$#"
exit ${FAILURE}
fi
Given this code, and running the application, I get the following output:
BEFORE
AFTER -- :hvso:l:d:n:p:t: -l help,version,submit-job,output:,library:,job-dir:,num-nodes:,num-procs:,max-time: -a -n vasp2nwchem --
vasp2nwchem: error: 7: Invalid argument count, expected [2|0]
:hvso:l:d:n:p:t: -l help,version,submit-job,output:,library:,job-dir:,num-nodes:,num-procs:,max-time: -a -n vasp2nwchem --
So, the code enters the while loop portion of the code, jumps to the last case, and shifts off the first --, leaving me with all of the arguments that I supplied to getopt, minus the -o flag.
Any light that anyone could shed on this conundrum would be immensely appreciated, because it is seriously about to send me over the edge, especially since this code was functional no less than thrity minutes ago, and has now stopped working entirely!!!
I don't see anything wrong. I have GNU getopt installed as /usr/gnu/bin/getopt (and BSD getopt in /usr/bin), so this script (chk.getopt.sh) is almost equivalent to the start of yours, though I do set the PROG_NAME variable. This is more or less the SSCCE (Short, Self-Contained, Complete Example) for your rather substantial script.
#!/bin/bash
PROG_NAME=$(basename $0 .sh)
SHORT_OPTS=":hvso:l:d:n:p:t:"
LONG_OPTS="help,version,submit-job,output:,library:,job-dir:"
LONG_OPTS="${LONG_OPTS},num-nodes:,num-procs:,max-time:"
OPTS=$(/usr/gnu/bin/getopt -o "${SHORT_OPTS}" -l "${LONG_OPTS}" -a -n "${PROG_NAME}" -- "$#")
# Check for invalid command line options and arguments
if [[ ${?} -ne ${SUCCESS} ]] ; then
echo -e "${PROG_NAME}: error: Invalid option or argument\n" >&2
usage ; exit ${FAILURE}
else
echo "BEFORE $#"
eval set -- ${OPTS}
echo "AFTER $#"
fi
When I run it, this is the output:
$ bash -x chk.getopt.sh -ooutput -nnumber -pperhaps -ppotato -- -o oliphaunt
++ basename chk.getopt.sh .sh
+ PROG_NAME=chk.getopt
+ SHORT_OPTS=:hvso:l:d:n:p:t:
+ LONG_OPTS=help,version,submit-job,output:,library:,job-dir:
+ LONG_OPTS=help,version,submit-job,output:,library:,job-dir:,num-nodes:,num-procs:,max-time:
++ /usr/gnu/bin/getopt -o :hvso:l:d:n:p:t: -l help,version,submit-job,output:,library:,job-dir:,num-nodes:,num-procs:,max-time: -a -n chk.getopt -- -ooutput -nnumber -pperhaps -ppotato -- -o oliphaunt
+ OPTS=' -o '\''output'\'' -n '\''number'\'' -p '\''perhaps'\'' -p '\''potato'\'' -- '\''-o'\'' '\''oliphaunt'\'''
+ [[ 0 -ne '' ]]
+ echo 'BEFORE -ooutput' -nnumber -pperhaps -ppotato -- -o oliphaunt
BEFORE -ooutput -nnumber -pperhaps -ppotato -- -o oliphaunt
+ eval set -- -o ''\''output'\''' -n ''\''number'\''' -p ''\''perhaps'\''' -p ''\''potato'\''' -- ''\''-o'\''' ''\''oliphaunt'\'''
++ set -- -o output -n number -p perhaps -p potato -- -o oliphaunt
+ echo 'AFTER -o' output -n number -p perhaps -p potato -- -o oliphaunt
AFTER -o output -n number -p perhaps -p potato -- -o oliphaunt
$
This all looks correct; the options before the double dash have been split from their arguments, and the ones after the double dash are OK.
So, it is not obvious that there's a problem with your code. Even with an empty string as the program name, it worked OK for me.
May be you should show the output of this much of the script on your machine?

Resources