Validate bash script arguments - bash

I am trying to do something like this to make a script to perform backups if they have failed. I am taking in the environment as argument to the script.
The one thing i am unsure on how to do is that i want to verify $1 to only include some predefined values. The predefined values should be something like tst, prd, qa, rpt. Anyone?
#!/bin/bash
ENVIRONMENT=$1
BACKUPDATE=$(date +"%d_%m_%Y")
BACKUPFILE="$ENVIRONMENT".backup."$BACKUPDATE".tar.gz
if [ $1 == "" ]
then
echo "No environment specified"
exit
elif [ -f "$BACKUPFILE" ]; then
echo "The file '$BACKUPFILE' exists."
else
echo "The file '$BACKUPFILE' in not found."
exec touch "$BACKUPFILE"
fi

You can use case:
case "$1" in
tst) echo "Backing up Test style" ;;
prd)
echo "Production backup"
/etc/init.d/myservice stop
tar czf ...
/etc/init.d/myservice start
;;
qa) echo "Quality skipped" ;;
rpt)
echo "Different type of backup"
echo "This could be another processing"
...
;;
*)
echo "Unknown backup type"
exit 2
;;
esac
Note the double ;; to end each case, and the convenient use of pattern matching.
Edit: following your comment and #CharlesDuffy suggestion, if you want to have all valid options in an array and test your value against any of them (hence having the same piece of code for all valid values), you can use an associative array:
declare -A valids=(["tst"]=1 ["prd"]=1 ["qa"]=1 ["rpt"]=1)
if [[ -z ${valids[$1]} ]] ; then
echo "Invalid parameter value"
# Any other processing here ...
exit 1
fi
# Here your parameter is valid, proceed with processing ...
This works by having a value (here 1 but it could be anything else in that case) assigned to every valid parameter. So any invalid parameter will be null and the -z test will trigger.
Credits go to him.

Depending on how many different values you have, what about a case statement? It even allows for globbing.
case $1 in
(John) printf "Likes Yoko\n";;
(Paul) printf "Likes to write songs\n";;
(George) printf "Harrison\n";;
(Ringo) printf "Da drumma\n";;
(*) printf "Management, perhaps?\n";;
esac
On another note, if you can you should avoid unportable bashisms like the [[ test operator (and use [ if you can, e.g. if [ "$1" = "John" ]; then ...; fi.)

Related

bash script case statement needs to detect specific arguments

I have to write this script where it will display each entry that is entered in on its own line and separate each line with "*****". I've already got that down for the most part but now I need it to detect when the word "TestError" and/or "now" is entered in as an argument. The way it is setup right now it will detect those words correctly if they are the first argument on the line, I'm just not sure how to set it up where it will detect the word regardless of which argument it is on the line. Also I need help with the *? case where I need it to say "Do not know what to do with " for every other argument that is not "TestError" or "now", at the moment it will do it for the first argument but not the rest.
Would it work the way it is right now? Or would I have to use only the *? and * cases and just put an if/then/else/fi statement in the *? case in order to find the "TestError" "now" and any other argument.
# template.sh
function usage
{
echo "usage: $0 arguments ..."
if [ $# -eq 1 ]
then echo "ERROR: $1"
fi
}
# Script starts after this line.
case $1 in
TestError)
usage $*
;;
now)
time=$(date +%X)
echo "It is now $time"
;;
*?)
echo "My Name"
date
echo
usage
printf "%s\n*****\n" "Do not know what to do with " "$#"
;;
*)
usage
;;
esac
You'll need to loop over the arguments, executing the case statement for each one.
for arg in "$#"; do
case $arg in
TestError)
usage $*
;;
now)
time=$(date +%X)
echo "It is now $time"
;;
*?)
echo "My Name"
date
echo
usage
printf "%s\n*****\n" "Do not know what to do with " "$#"
;;
*)
usage
;;
esac
done
* and *? will match the same strings. Did you mean to match the ? literally (*\?)?

Best way to parse arguments in bash script

So I've been reading around about getopts, getopt, etc. but I haven't found an exact solution to my problem.
The basic idea of the usage of my script is:
./program [-u] [-s] [-d] <TEXT>
Except TEXT is not required if -d is passed. Note that TEXT is usually a paragraph of text.
My main problem is that once getopts finishing parsing the flags, I have no way of knowing the position of the TEXT parameter. I could just assume that TEXT is the last argument, however, if a user messes up and does something like:
./program -u "sentence 1" "sentence 2"
then the program will not realize that the usage is incorrect.
The closest I've come is using getopt and IFS by doing
ARGS=$(getopt usd: $*)
IFS=' ' read -a array <<< "$ARGS"
The only problem is that TEXT might be a long paragraph of text and this method splits every word of text because of the spaces.
I'm thinking my best bet is to use a regular expression to ensure the usage is correctly formed and then extract the arguments with getopts, but it would be nice if there was a simpler solution
It's quite simple with getopts:
#!/bin/bash
u_set=0
s_set=0
d_set=0
while getopts usd OPT; do
case "$OPT" in
u) u_set=1;;
s) s_set=1;;
d) d_set=1;;
*) # getopts produces error
exit 1;;
esac
done
if ((!d_set && OPTIND>$#)); then
echo You must provide text or use -d >>/dev/stderr
exit 1
fi
# The easiest way to get rid of the processed options:
shift $((OPTIND-1))
# This will run all of the remaining arguments together with spaces between them:
TEXT="$*"
This is what I typically do:
local badflag=""
local aflag=""
local bflag=""
local cflag=""
local dflag=""
while [[ "$1" == -* ]]; do
case $1 in
-a)
aflag="-a"
;;
-b)
bflag="-b"
;;
-c)
cflag="-c"
;;
-d)
dflag="-d"
;;
*)
badflag=$1
;;
esac
shift
done
if [ "$badflag" != "" ]; do
echo "ERROR CONDITION"
fi
if [ "$1" == "" ] && [ "$dflag" == "" ]; do
echo "ERROR CONDITION"
fi
local remaining_text=$#

Bash case statement

I'm trying to learn case as I was to write a fully functional script.
I'm starting off with the below
#!/bin/sh
case $# in
-h|--help)
echo "You have selected Help"
;;
-B|-b)
echo "You have selected B"
;;
-C|-c)
echo "You have selected C"
;;
*)
echo "Valid Choices are A,B,C"
exit 1
;;
esac
I want to use two of these options:
./getopts.sh -h -c
But i get this result
Valid Choices are A,B,C
Please can you help out and let me know what I'm doing wrong?
I want to build a script that will do something if you enter one option but do multiple things if you enter multiple.
Also how would i parse $1 to this script as surley which ever option i enter first (-h) will be $1 ??
Thanks!
Try this
#!/bin/sh
usage() {
echo `basename $0`: ERROR: $* 1>&2
echo usage: `basename $0` '[-a] [-b] [-c]
[file ...]' 1>&2
exit 1
}
while :
do
case "$1" in
-a|-A) echo you picked A;;
-b|-B) echo you picked B;;
-c|-C) echo you picked C;;
-*) usage "bad argument $1";;
*) break;;
esac
shift
done
Using getopt or getopts is the better solution. But to answer your immediate question, $# is all of your arguments, so -h -c, which doesn't match any of the single-argument patterns in your case statement. You would still need to iterate over your arguments like so
for arg in "$#"; do
case $arg in
....
esac
done
to parse the positional arguments like ... $1 , just use $1 in the case stmt and then at the end ... use shift to pust the 2nd arg to $1 and likewise .
also i would put the case stmt in a while loop or better a fxn so that i can run it twice for the two options or the number of options ..........
$# will let you know how many options/arguments were there .

bash: partial match up to a complete word for case

I wrote a bash script that takes a command as the first positional parameter and uses a case construct as a dispatch similar to the following:
do_command() {
# responds to invocation `$0 command ...`
}
do_copy() {
# respond to invocation: `$0 copy...`
}
do_imperative() {
# respond to invocation: `$0 imperative ...`
}
cmd=$1
shift
case $cmd in
command)
do_command $*
;;
copy)
do_copy $*
;;
imperative)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
This script decides what function to call based on $1 and then passes the remaining arguments to that function. I would like to add the ability dispatch on distinct partial matches, but I want to do it in an elegant way (elegant defined as a way that is both easy to read and is not so verbose as to be an eyesore or a distraction).
The obvious functioning (but not elegant) solution might be something like this:
case $cmd in
command|comman|comma|comm|com)
do_command $*
;;
copy|cop)
do_copy $*
;;
imperative|imperativ|imperati|imperat|impera|imper|impe|imp|im|i)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
As you can see, explicitly enumerating all distinct permutations of each command name can get really messy.
For a moment, I thought it might be alright to use a wildcard match like this:
case $cmd in
com*)
do_command $*
;;
cop*)
do_copy $*
;;
i*)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
This is less of an eyesore. However, this could result in undesirable behavior such as where do_command is called when $1 is given as "comblah" or something else that shouldn't be recognized as a valid argument.
My question is: What is the most elegant (as defined above) way to correctly dispatch such a command where the user can provide any distinct truncated form of the expected commands?
I came up with the following solution, which should work with any
bourne-compatible shell:
disambiguate() {
option="$1"
shift
found=""
all=""
comma=""
for candidate in "$#"; do
case "$candidate" in
"$option"*)
found="$candidate"
all="$all$comma$candidate"
comma=", "
esac
done
if [ -z "$found" ] ; then
echo "Unknown option $option: should be one of $#" >&2
return 1;
fi
if [ "$all" = "$found" ] ; then
echo "$found"
else
echo "Ambigious option $option: may be $all" >&2
return 1
fi
}
foo=$(disambiguate "$1" lorem ipsum dolor dollar)
if [ -z "$foo" ] ; then exit 1; fi
echo "$foo"
Yes, the source code of disambiguate is not pretty, but I hope you
won't have to look at this code most of the time.
Pattern Matching for Command Dispatch in Bash
It seems that a few of you like the idea of using a resolver to find the full command match prior to the dispatching logic. That's might just be the best way to go for large command sets or sets that have long words. I put together the following hacked up mess--it makes 2 passes using built-in parameter expansion substring removal. I seems to work well enough and it keeps the dispatch logic clean of the distraction of resolving partial commands. My bash version is 4.1.5.
#!/bin/bash
resolve_cmd() {
local given=$1
shift
local list=($*)
local inv=(${list[*]##${given}*})
local OIFS=$IFS; IFS='|'; local pat="${inv[*]}"; IFS=$OIFS
shopt -s extglob
echo "${list[*]##+($pat)}"
shopt -u extglob
}
valid_cmds="start stop status command copy imperative empathy emperor"
m=($(resolve_cmd $1 $valid_cmds))
if [ ${#m[*]} -gt 1 ]; then
echo "$1 is ambiguous, possible matches: ${m[*]}" >&2
exit 1
elif [ ${#m[*]} -lt 1 ]; then
echo "$1 is not a recognized command." >&2
exit 1
fi
echo "Matched command: $m"
Update 1:
match is called using (partial) command as first positional parameter, followed by strings to test against. On multiple matches, each partial match will be hinted with uppercase.
# #pos 1 string
# #pos 2+ strings to compare against
# #ret true on one match, false on none|disambiguate match
match() {
local w input="${1,,}" disa=();
local len=${#input}; # needed for uppercase hints
shift;
for w in $*; do
[[ "$input" == "$w" ]] && return 0;
[[ "$w" == "$input"* ]] && disa+=($w);
done
if ! (( ${#disa[*]} == 1 )); then
printf "Matches: "
for w in ${disa[*]}; do
printf "$( echo "${w:0:$len}" | tr '[:lower:]' '[:upper:]')${w:${len}} ";
done
echo "";
return 1;
fi
return 0;
}
Example of usage. match can be tweaked to print/return the whole non disambiguate command, otherwise do_something_with would require some logic to resolve partial commands. (something like my first answer)
cmds="start stop status command copy imperative empathy emperor"
while true; do
read -p"> " cmd
test -z "$cmd" && exit 1;
match $cmd $cmds && do_something_with "$cmd";
done
First answer: Case approach; would require some logic before use, to solve disambiguate partial matches.
#!/bin/bash
# script.sh
# set extended globbing, in most shells it's not set by default
shopt -s extglob;
do_imperative() {
echo $*;
}
case $1 in
i?(m?(p?(e?(r?(a?(t?(i?(v?(e))))))))))
shift;
do_imperative $*;
;;
*)
echo "Error: no match on $1";
exit 1;
;;
esac
exit 0;
i, im, imp up till imperative will match. The shift will set second positional parameter as first, meaning; if the script is called as:
./script.sh imp say hello
will resolve into
do_imperative say hello
If you wish to further resolve short hand commands, use the same approach within the functions as well.
Here's a simple (possibly even elegant) solution which will only work with bash, because it relies on the bash-specific compgen command.
This version assumes that the action functions are always called do_X where X is the command name. Before calling this function, you need to set $commands to a space-separated list of legal commands; the assumption is that legal commands will be simple words, since function names cannot include special characters.
doit () {
# Do nothing if there is no command
if (( ! $# )); then return 0; fi;
local cmd=$1
shift
local -a options=($(compgen -W "$commands" "$cmd"));
case ${#options[#]} in
0)
printf "Unrecognized command '%b'\n" "$cmd" >> /dev/stderr;
return 1
;;
1)
# Assume that the action functions all have a consistent name
"do_$options" "$#"
;;
*)
printf "Ambigous command '%b'. Possible completions:" "$cmd";
printf " %s" "${options[#]}";
printf "\n";
return 1
;;
esac
}
do_command () { printf "command %s\n" "$#"; }
do_copy () { printf "copy %s\n" "$#"; }
do_imperative () { printf "imperative %s\n" "$#"; }
commands="command copy imperative"
Trial run:
$ doit cop a "b c"
copy a
copy b c
$ doit comfoo a "b c"
Unrecognized command 'comfoo'
$ doit co a "b c"
Ambigous command 'co'. Possible completions: command copy
$ doit i a "b c"
If you were confident that there were no stray do_X variables available, you could use compgen to make the list of commands as well:
command=$(compgen -Afunction do_ | cut -c4-)
Alternatively, you could use the same system to make a resolver, and then handle the returned option with a normal case statement:
# resolve cmd possible-commands
resolve () {
# Fail silently if there is no command
if [[ -z $1 ]]; then return 1; fi;
local cmd=$1
shift
local commands="$*"
local -a options=($(compgen -W "$commands" "$cmd"));
case ${#options[#]} in
0)
printf "Unrecognized command '%b'\n" "$cmd" >> /dev/stderr;
return 1
;;
1)
echo $options
return 0
;;
*)
printf "Ambigous command '%b'. Possible completions:" "$cmd";
printf " %s" "${options[#]}";
printf "\n";
return 1
;;
esac
}
$ resolve com command copy imperative && echo OK
command
OK
$ resolve co command copy imperative && echo OK
Ambigous command 'co'. Possible completions: command copy
$ resolve copx command copy imperative && echo OK
Unrecognized command 'copx'
The intent would be to write something like:
cmd=$(resolve "$1" "$commands") || exit 1
case "$cmd" in
command)
# ...

Correct way to check for a command line flag in bash

In the middle of a script, I want to check if a given flag was passed on the command line. The following does what I want but seems ugly:
if echo $* | grep -e "--flag" -q
then
echo ">>>> Running with flag"
else
echo ">>>> Running without flag"
fi
Is there a better way?
Note: I explicitly don't want to list all the flags in a switch/getopt. (In this case any such things would become half or more of the full script. Also the bodies of the if just set a set of vars)
An alternative to what you're doing:
if [[ $* == *--flag* ]]
See also BashFAQ/035.
Note: This will also match --flags-off since it's a simple substring check.
I typically see this done with a case statement. Here's an excerpt from the git-repack script:
while test $# != 0
do
case "$1" in
-n) no_update_info=t ;;
-a) all_into_one=t ;;
-A) all_into_one=t
unpack_unreachable=--unpack-unreachable ;;
-d) remove_redundant=t ;;
-q) GIT_QUIET=t ;;
-f) no_reuse=--no-reuse-object ;;
-l) local=--local ;;
--max-pack-size|--window|--window-memory|--depth)
extra="$extra $1=$2"; shift ;;
--) shift; break;;
*) usage ;;
esac
shift
done
Note that this allows you to check for both short and long flags. Other options are built up using the extra variable in this case.
you can take the straight-forward approach, and iterate over the arguments to test each of them for equality with a given parameter (e.g. -t, --therizinosaurus).
put it into a function:
has_param() {
local term="$1"
shift
for arg; do
if [[ $arg == "$term" ]]; then
return 0
fi
done
return 1
}
… and use it as a predicate in test expressions:
if has_param '-t' "$#"; then
echo "yay!"
fi
if ! has_param '-t' "$1" "$2" "$wat"; then
echo "nay..."
fi
if you want to reject empty arguments, add an exit point at the top of the loop body:
for arg; do
if [[ -z "$arg" ]]; then
return 2
fi
# ...
this is very readable, and will not give you false positives, like pattern matching or regex matching will.
it will also allow placing flags at arbitrary positions, for example, you can put -h at the end of the command line (not going into whether it's good or bad).
but, the more i thought about it, the more something bothered me.
with a function, you can take any implementation (e.g. getopts), and reuse it. encapsulation rulez!
but even with commands, this strength can become a flaw. if you'll be using it again and again, you'll be parsing all the arguments each time.
my tendency is to favor reuse, but i have to be aware of the implications. the opposed approach would be to parse these arguments once at the script top, as you dreaded, and avoid the repeated parsing.
you can still encapsulate that switch case, which can be as big as you decide (you don't have to list all the options).
You can use the getopt keyword in bash.
From http://aplawrence.com/Unix/getopts.html:
getopt
This is a standalone executable that has been around a long time.
Older versions lack the ability to handle quoted arguments (foo a "this
won't work" c) and the versions that can, do so clumsily. If you are
running a recent Linux version, your "getopt" can do that; SCO OSR5,
Mac OS X 10.2.6 and FreeBSD 4.4 has an older version that does not.
The simple use of "getopt" is shown in this mini-script:
#!/bin/bash
echo "Before getopt"
for i
do
echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
echo "-->$i"
done
I've made small changes to the answer of Eliran Malka:
This function can evaluate different parameter synonyms, like "-q" and "--quick". Also, it does not use return 0/1 but an echo to return a non-null value when the parameter is found:
function has_param() {
local terms="$1"
shift
for term in $terms; do
for arg; do
if [[ $arg == "$term" ]]; then
echo "yes"
fi
done
done
}
# Same usage:
# Assign result to a variable.
FLAG_QUICK=$(has_param "-q --quick" "$#") # "yes" or ""
# Test in a condition using the nonzero-length-test to detect "yes" response.
if [[ -n $(has_param "-h --help" "$#") ]]; then;
echo "Need help?"
fi
# Check, is a flag is NOT set by using the zero-length test.
if [[ -z $(has_param "-f --flag" "$#") ]]; then
echo "FLAG NOT SET"
fi
The modification of Dennis Williamson's answer with additional example for a argument in the short form.
if [[ \ $*\ == *\ --flag\ * ]] || [[ \ $*\ == *\ -f\ * ]]
It solves the problem of false positive matching --flags-off and even --another--flag (more popular such case for an one-dashed arguments: --one-more-flag for *-f*).
\ (backslash + space) means space for expressions inside [[ ]]. Putting spaces around $* allows to be sure that the arguments contacts neither line's start nor line's end, they contacts only spaces. And now the target flag surrounded by spaces can be searched in the line with arguments.
if [ "$1" == "-n" ]; then
echo "Flag set";
fi
Here is a variation on the most voted answer that won't pick up false positives
if [[ " $* " == *" -r "* ]]; then
Not an alternative, but an improvement, though.
if echo $* | grep -e "\b--flag\b" -q
Looking for word boundaries will make sure to really get the option --flag and neither --flagstaff nor --not-really--flag

Resources