I would like to be able to parse an input to a bash shell script that looks like the following.
myscript.sh --casename obstacle1 --output en --variables v P pResidualTT
The best I have so far fails because the last argument has multiple values. The first arguments should only ever have 1 value, but the third could have anything greater than 1. Is there a way to specify that everything after the third argument up to the next set of "--" should be grabbed? I'm going to assume that a user is not constrained to give the arguments in the order that I have shown.
casename=notset
variables=notset
output_format=notset
while [[ $# -gt 1 ]]
do
key="$1"
case $key in
--casename)
casename=$2
shift
;;
--output)
output_format=$2
shift
;;
--variables)
variables="$2"
shift
;;
*)
echo configure option \'$1\' not understood!
echo use ./configure --help to see correct usage!
exit -1
break
;;
esac
shift
done
echo $casename
echo $output_format
echo $variables
One conventional practice (if you're going to do this) is to shift multiple arguments off. That is:
variables=( )
case $key in
--variables)
while (( "$#" >= 2 )) && ! [[ $2 = --* ]]; do
variables+=( "$2" )
shift
done
;;
esac
That said, it's more common to build your calling convention so a caller would pass one -V or --variable argument per following variable -- that is, something like:
myscript --casename obstacle1 --output en -V=v -V=p -V=pResidualTT
...in which case you only need:
case $key in
-V=*|--variable=*) variables+=( "${1#*=}" );;
-V|--variable) variables+=( "$2" ); shift;;
esac
Related
After a lot of read, SO or others, I'm really wondering about the best / cleanest way to have a bash script with parameters, optionals with default values.
Here is my script for now:
#!/bin/bash
helpFunction()
{
echo ""
echo "Usage: $0 --reload --mode=[single|cluster]"
echo -e "\t--reload Reload the database : fixtures & schema"
echo -e "\t--mode Mode of build : single or cluster"
exit 1
}
while [[ "$#" -gt 0 ]]; do
case $1 in
-h|--help) helpFunction; shift ;;
-r|--reload) reload=true; shift ;;
-m|--mode) mode="single"; shift ;;
# ... (same format for other required arguments)
*) echo "Unknown parameter passed: $1" ;;
esac
shift
done
./bin/sh/tools/build.sh -e local -m $mode -p local
For now, the $mode variable seems to not be set if I don't set it, how can I have a default value for this variable ? (default is single)
What I want is the user to call the script like (reload is true, mode is cluster):
bin/script.sh -r --mode=cluster
Or by default (reload is false, mode is single):
bin/script.sh
Is this the good way to wait for parameters ? I read other ways, but no real explanations.
Thanks.
There is no universal best/cleanest method; it eventually comes down to what works best for you; some ideas:
set mode to a default value before the while/case loop
after the while/case loop test mode and if unset/undefined then set to a
default value (should probably play it safe and unset mode before the while/case loop though in this case you might as well see idea #1)
pass to build.sh wrapped in double quotes (ie, build.sh -e local -m "$mode" -p local) and have build.sh test for the -m argument being unset/undefined and set to a default value
a variation on #2 and #3 is to use parameter substitution when
passing mode to build.sh, eg: build.sh -e local -m "${mode:-default_value}" -p local
if your script sources a config/ini file you could assign mode a
default value in said config/ini file and then make sure said config/ini file is sourced before the while/case loop
If you set the mode beforehand, that will be its default. If you want parameter with assignment, you need to grab $2 and shift twice.
mode="single"
while [[ "$#" -gt 0 ]]; do
case $1 in
-h|--help) helpFunction; shift ;;
-r|--reload) reload=true; shift ;;
-m|--mode) mode="$2"; shift; shift ;;
# ... (same format for other required arguments)
*) echo "Unknown parameter passed: $1" ;;
esac
shift
done
I think it's also good to restore the positional arguments. Since you may want to use them, but that depends on your script.
mode="single"
reload="false"
POSITIONAL=()
while [[ "$#" -gt 0 ]]; do
case $1 in
-h | --help)
helpFunction
shift
;;
-r | --reload)
reload=true
shift
;;
-m | --mode)
mode="$2"
shift
shift
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
set -- "${POSITIONAL[#]}"
COMMAND="$1"
Then you could invoke a function that is called like the $COMMAND and pass the rest of the params with $#, and shift again and set the subcommand to $1. You can do this infinitely to get a chain of (sub)commands.
You can still check if a valid command was provided before you try to call the $COMMAND.
COMMAND_LIST="add issue revoke"
case $COMMAND_LIST in
*"$COMMAND"*)
"$COMMAND" "$#"
;;
*)
helpFunction
echo "ERROR: Unknown command: $COMMAND"
exit 1
;;
esac
But that is of course only if your script supports commands/subcommands. Otherwise, it makes sense to error right away like you did, if there is an unknown parameter.
I am inexperienced with bash shell scripting, and have run into a problem with bash optarg
Here's a small script to reproduce the problem:
#!/bin/sh
while getopts ":a:b:" opt; do
case ${opt} in
a ) echo "a=$OPTARG"
;;
b ) echo "b=$OPTARG"
;;
\? ) echo "Invalid option: $OPTARG" 1>&2
;;
: ) echo "Invalid option: $OPTARG requires an argument" 1>&2
esac
done
When I try this:
./args.sh -a av -b bv
I get the expected result:
a=av
b=bv
But when I omit the argument for -a:
/args.sh -a -b bv
I get this unfortunate result:
a=-b
When I would expect an error to show that the value of -a is missing.
It seems to have taken the -b argument as the value for -a.
Have I done something wrong & how can I achieve the expected behaviour?
The only positive advice is how do you treat But when I omit the argument for '-a', you cannot just skip to the next subsequent option. By convention getopts a: means you are expecting to an provide an arg value for the flag defined.
So even for the omitting case, you need to define an empty string which means the value for the arg is not defined i.e.
-a '' -b bv
Or if you don't expect the -a to get any arg values, better change the option string to not receive any as :ab:.
Any other ways of working around by checking if the OPTARG for -a is does not contain - or other hacks are not advised as it does not comply with the getopts() work flow.
getopts doesn't support such detection. So there's no way to do that with getopts.
You can probably write a loop around the arguments instead. something like:
#!/bin/sh
check_option()
{
case $1 in
-*)
return 1
;;
esac
return 0
}
for opt in $#; do
case ${opt} in
-a) shift
if check_option $1; then
echo "arg for -a: $1"
shift
else
echo "Invalid option -a"
fi
;;
-b) shift
if check_option $1; then
echo "arg for -b: $1"
shift
else
echo "Invalid option -b"
fi
;;
esac
done
Wrapping another command's parameters
I have a command tool1 that parses arguments this way:
#!/usr/bin/env bash
# ...
while [[ $# -ge 1 ]]
do
key="$1"
case $key in
-o|--option)
OPT="$2"
shift
;;
-u|--user)
USR="$2"
shift
;;
-*)
echo -e "Unrecognized option: \"$key\"" && exit 1
;;
*)
OTHERS+=("$1")
;;
esac
shift
done
# ...
I have tool2 that calls tool1. Thus tool2 will have to pass parameters to tool1. It may also need to process the same parameters (--user)
tool2 looks like:
#!/usr/bin/env bash
# ...
while [[ $# -ge 1 ]]
do
key="$1"
case $key in
-O|--option2)
opt2="$2"
shift
;;
-u|--user)
USR="$2"
OTHERS+=("-u $2")
shift
;;
-*)
echo -e "Unrecognized option: \"$key\"" && exit 1
;;
*)
OTHERS+=("$1")
;;
esac
shift
done
## Call tool1 with other parameters to pass
bash tool1.sh ${OTHERS[#]}
# ...
To sum up
--option2 is an option used only by tool2.
--user is common to both tools, and may be used by tool2 too, before calling tool1.sh. Because of this, in this example --user has to be explicitly passed to tool1 thanks to the array OTHERS.
I'd like to know about possible and/or alternative ways of dealing with such parameter redundancies. A methodology that would help me wrapping another tool's expected parameters/options, without having to copy/paste the lines regarding the parsing of such redundant parameters/options.
tool2's approach is fine. However, you aren't setting OTHERS correctly.
-u|--user)
USR="$2"
OTHERS+=("-u" "$2")
shift
-u and its argument need to remain separate array elements, just as they were separate arguments to tool2. You also need to quote the expansion of OTHERS, to preserve arguments containing word-splitting characters or globs:
bash tool1.sh "${OTHERS[#]}"
Finally, all-uppercase variable names are reserved for use by the shell itself; don't define such names yourself. Just use others instead of OTHERS.
So I have a couple of getopts in my bash script. Here's an example of a working one.
FOUND=
SEARCH=
COUNT=0
while getopts "ips:flenkc" OPTION
do
case $OPTION in
i)
FOUND=1
let "COUNT++"
;;
p)
FOUND=2
let "COUNT++"
;;
s)
FOUND=3
SEARCH=$OPTARG
let "COUNT++"
;;
esac
done
Later on a case statement that checks to see if count=1 (meaning, only one of the following, i, p, and s, are used in the call) Not important except that it determines the main action being done.
Now the getopts thing in question. This was working before, and now it's not. The goal is to make it so that if someone wants to input data, they can do so with the following bash command.
./programname -i -f Mary -l Sue -e smary#email.com -n 555-555-5555
Where, when -i is used, we must have -f, -l, -e, and -n (for first name, last name, e-mail, and number).
The code I was using: Warning, code is full of syntax errors. If you're learning bash, I highly recommend you do not use anything you see here in my post.
if [ $FOUND == "1" ]
then
echo "You have chosen to insert things."
FIRST=
LAST=
EMAIL=
NUMBER=
while getopts "if:l:e:n:" OPTION
do
case $OPTION in
f)
FIRST=$OPTARG
;;
l)
LAST=$OPTARG
;;
e)
EMAIL=$OPTARG
;;
n)
NUMBER=$OPTARG
;;
esac
done
if [[ -z $FIRST ]] || [[ -z $LAST ]] || [[ -z $EMAIL ]] || [[ -z $NUMBER ]]
echo "Error!!! Some input is missing!!!"
usage // display usage
exit 1
fi
echo -e $FIRST"\t"$LAST"\t"$EMAIL"\t"$NUMBER >> contacts
fi
Before this program would work, but now, not even a single thing is making it to input for FIRST, LAST, EMAIL, and NUMBER (in my attempts to change the code to see if it was making it to certain steps).
What am I doing wrong with the getopts? It was working fine before, but now.... it's not working at all!
One thing worth noting up front: if your script has already called getopts once, another getopts call will start AFTER all options and therefore effectively do nothing; reset OPTIND to 1 before each subsequent getopts calls to have them reprocess all options.
Your code has both syntax errors and is worth cleaning up in general:
The if [[ -z ... statement was missing a then.
The // after usage would have caused a syntax error - POSIX-like shells use # as the comment char.
Since this is bash script, stick with using [[ ... ]] consistently (no need for [ ... ]) and/or use (( ... )) for arithmetic operations.
Specifically, avoid [ ... == ... ], because it mixes POSIX syntax - [ ... ] - with Bash-specific syntax - == ( POSIX only supports =).
If you do use [ ... ], be sure to double-quote variable references, to be safe.
No need for multiple [[ ... ]] expressions to OR them together - do it in a single [[ ... || ... || ... ]].
It's best to avoid all-uppercase shell-variable names so as to avoid conflicts with environment variables and special shell variables.
Output error messages to stderr, using >&2.
Enclose the entire argument to echo -e in double-quotes to protect variable values from possibly unwanted expansions.
Mere syntax errors can usually be caught using shellcheck.net.
Putting it all together, we get:
#!/usr/bin/env bash
# ... code that sets $found
# If you've already processed args. with getopts above,
# you must reset OPTIND to process them again.
OPTIND=1
if (( found == 1 )) # found is numeric, use arithmetic expression to compare
then
echo "You have chosen to insert things."
first= last= email= number= # don't use all-uppercase var. names
while getopts "if:l:e:n:" option
do
case $option in
f)
first=$OPTARG
;;
l)
last=$OPTARG
;;
e)
email=$OPTARG
;;
n)
number=$OPTARG
;;
esac
done
if [[ -z $first || -z $last || -z $email || -z $number ]]; then
echo "Error!!! Some input is missing!!!" >&2
usage # display usage
exit 1
fi
echo -e "$first\t$last\t$email\t$number" >> contacts
fi
I am looking for a reusable code snippet that does command line argument validation for bash.
Ideally something akin to the functionality offered by Apache Commons CLI:
Commons CLI supports different types of options:
POSIX like options (ie. tar -zxvf foo.tar.gz)
GNU like long options (ie. du --human-readable --max-depth=1)
Short options with value attached (ie. gcc -O2 foo.c)
long options with single hyphen (ie. ant -projecthelp)
...
and it generates a "usage" message for the program automatically, like this:
usage: ls
-A,--almost-all do not list implied . and ..
-a,--all do not hide entries starting with .
-B,--ignore-backups do not list implied entried ending with ~
-b,--escape print octal escapes for nongraphic characters
--block-size <SIZE> use SIZE-byte blocks
-c with -lt: sort by, and show, ctime (time of last
modification of file status information) with
-l:show ctime and sort by name otherwise: sort
by ctime
-C list entries by columns
I would include this code snippet at the beginning of my Bash scripts and reuse it across scripts.
There must be something like this. I don't believe we are all writing code to this effect or similar:
#!/bin/bash
NUMBER_OF_REQUIRED_COMMAND_LINE_ARGUMENTS=3
number_of_supplied_command_line_arguments=$#
function show_command_usage() {
echo usage:
(...)
}
if (( number_of_supplied_command_line_arguments < NUMBER_OF_REQUIRED_COMMAND_LINE_ARGUMENTS )); then
show_command_usage
exit
fi
...
This is the solution I use (found it on the net somewhere, probably here itself, don't remember for sure). Please note that the GNU getopt (/usr/bin/getopt) does support single dash long options (ant -projecthelp style) using the option -a, however I haven't used it so it is not shown in the example.
This code parses for 3 options: --host value or -h value, --port value or -p value and --table value or -t value. In case the required parameter isn't set, a test for it is
# Get and parse options using /usr/bin/getopt
OPTIONS=$(getopt -o h:p:t: --long host:,port:,table: -n "$0" -- "$#")
# Note the quotes around `$OPTIONS': they are essential for handling spaces in
# option values!
eval set -- "$OPTIONS"
while true ; do
case "$1" in
-h|--host) HOST=$2 ; shift 2 ;;
-t|--table)TABLE=$2 ; shift 2 ;;
-p|--port)
case "$2" in
"") PORT=1313; shift 2 ;;
*) PORT=$2; shift 2 ;;
esac;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
if [[ -z "$HOST" ]] || [[-z "$TABLE" ]] || [[ -z "$PORT" ]] ; then
usage()
exit
if
An alternative implementation using the getopts shell builtin(this only supports small options):
while getopts ":h:p:t:" option; do
case "$option" in
h) HOST=$OPTARG ;;
p) PORT=$OPTARG ;;
t) TABLE=$OPTARG ;;
*) usage(); exit 1 ;;
esac
done
if [[ -z "$HOST" ]] || [[-z "$TABLE" ]] || [[ -z "$PORT" ]] ; then
usage()
exit
if
shift $((OPTIND - 1))
Further reading for GNU getopt and getopts bash builtin