parsing parameters in shell script - shell

I have a myscript.sh which starts like this:
#!/usr/bin/env bash
set -e
usage(){
echo "Show Usage ... Blah blah"
exit 1
}
if [ $# = 0 ]; then
usage;
fi
while true; do
case "$1" in
-l | --build-lib ) BUILD_LIB=true;
--other-option ) OTHER_OPTION=$2; shift; shift;;
-h | --help ) usage; shift;;
* ) break ;;
esac
done
# I do my thing here ....
echo "Do my thing"
I am not sure if this is the best way to parse the parameters but so far I have a problem. I am not correctly breaking/failing when the user passes wrong or unknown parameters. How can I address this correctly?
for example I want to avoid calls like:
$ ./myscript.sh unknownParameter

You need to exit when an incorrect option is given, not just break out of the loop. Easiest way is to call your usage function.
while [ $# -gt 0 ]; do
case "$1" in
-l | --build-lib ) BUILD_LIB=tru ;;
--xcode-dev-path ) XCODE_DEV_PATH=${2%/}; shift ;;
-h | --help ) usage;;
* ) usage ;;
esac
shift
done

Related

Bash subcommands with arguments

I have a script in bash as such:
#!/usr/bin/env bash
set -e
if [[ "$#" == 0 ]]; then
printhelp
exit 1
fi
# process options
while [[ "$1" != "" ]]; do
case "$1" in
-n | --name)
shift
_NAME="$1"
;;
-i | --id)
shift
_ID="$1"
;;
-h | --help)
printhelp
exit 1
;;
*)
printhelp
exit 1
;;
esac
shift
done
This works fine, but I want to add some "actions" that will take the above params. Eg. usage will be:
./run.sh create --name foo --id 1234
./run.sh delete --id 1234
I am not able to figure out the right syntax, and I am unable to phrase this requirement into appropriate words to be able to search.
For sub-command you can handle it this way:
function main(){
if (( ${#} == 0 )); then
main_help 0;
fi
case ${1} in
help | version | encrypt | decrypt )
$1 "${#:2}";
;;
* )
echo "unknown command: $1";
main_help 1;
exit 1;
;;
esac
}
main "$#";
Then wrap each sub-command is a function. And inside each function you will have isolated options and parsing it separately.
For example:
function decrypt(){
if [[ ${#} == 0 ]]; then
decrypt_help;
fi
local __filename='';
local __salt='';
local __anchor=false;
local error_message='';
while [ ${#} -gt 0 ]; do
error_message="Error: a value is needed for '$1'";
case $1 in
-f | --file )
__filename=${2:?$error_message}
shift 2;
;;
-s | --salt )
__salt=${2:?$error_message}
shift 2;
;;
-a | --anchor )
__anchor=${2:?$error_message}
shift 2;
;;
* )
echo "unknown option $1";
break;
;;
esac
done
echo filename: ${__filename:-empty};
echo salt: ${__salt:-empty};
echo anchor: $__anchor;
exit 0;
}
Here is a full version bash-CLI-template I have used in my projects
demo ;)
Sounds like you want something like:
create() {
# actiony stuff here
}
ACTION=$1 ; shift
# put all your argument parsing here
$ACTION # call
However, since different actions probably have different arguments, I'd probably do it differently...
create() {
# argument parsing for create
# then do your create stuff
}
ACTION=$1 ; shift
$ACTION "$#"
This will pass all your arguments to your subfunction, which can then parse its own arguments.

Bash script with named parameter containing equal sign

I am trying to pass some values to my bash script using named parameters similar to the following:
./script.sh --username='myusername' --password='superS3cret!' --domainou="OU=Groups with Space,OU=subou,DC=mydomain,DC=local"
I have the following code:
#!/bin/bash
while [ "$1" != "" ]; do
PARAM=`echo $1 | awk -vFPAT='([^=]*)|("[^"]+")' -vOFS="=" '{print $1}'`
VALUE=`echo $1 | awk -vFPAT='([^=]*)|("[^"]+")' -vOFS="=" '{print $2}'`
case $PARAM in
-u | --username)
username=$VALUE
;;
-p | --password)
password=$VALUE
;;
-ou | --domainou)
domainou=$VALUE
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
exit 1
;;
esac
shift
done
echo $username
echo "$password"
echo "$domainou"
What I get when I run my script is:
myusername
superS3cret!
OU
Now the first two lines are correct but obviously I don't want OU...
I want:
OU=Groups with Space,OU=subou,DC=mydomain,DC=local
Awk seems to be matching the = inside the quote. As best as I can tell the way to solve that is using
-vFPAT='([^=]*)|("[^"]+")' -vOFS="="
But clearly that's not working so I am just wondering if any awk gurus can help me understand what's wrong with my awk statement.
Thanks
Brad
You can do it like this:
#!/bin/bash
while [ $# -gt 0 ]; do
case "$1" in
-u=* | --username=*)
username="${1#*=}"
;;
-p=* | --password=*)
password="${1#*=}"
;;
-ou=* | --domainou=*)
domainou="${1#*=}"
;;
*)
printf "Error: unknown option: $1\n"
exit 1
esac
shift
done
printf "username: $username\n"
printf "password: $password\n"
printf "domainou: $domainou\n"
For parsing command line options that include both long and short optoins, consider using GNU getopt, which has support for long options. While it is possible to build-your-own parser replacement, using the getopt provides for more robust parsing:
Abbreviation of options (e.g., accepting --user for --username).
Checking for required/optional values
Error handling
See also: Using getopts to process long and short command line options
set $(getopt --long 'username:,password:,ou:,domain:' -o 'u:p:' -- "$0" "$#")
while [ "$#" -gt 0 ] ; do
OP=$1
shift
case "$OP" in
--) PROG=$1 ; shift ; break ;;
-u | --username) username=$1 ; shift ;;
-p | --password) password=$1 ; shift ;;
--ou | --domain) domainou=$1 ; shift ;;
esac
done
# Positional arguments are set ...
Below is what ultimately worked best for me.
#dash-o definitely got me pointed in the right direction but the script you provided was printing out extraneous info:
set: usage: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
I believe the offending line was this:
set --long 'username:,password:,ou:,domain:' -o 'u:p:' -- "$0" "$#"
Here's the code that accomplished what I needed. I can't take credit for this. I stole it from here Using getopts to process long and short command line options but I never would have found that if not for dash-o so a big thank you!
#!/bin/bash
die() { echo "$*" >&2; exit 2; } # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }
while getopts ab:c:-: OPT; do
# support long options: https://stackoverflow.com/a/28466267/519360
if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG
OPT="${OPTARG%%=*}" # extract long option name
OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty)
OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=`
fi
case "$OPT" in
u | username ) needs_arg; username="$OPTARG" ;;
p | password ) needs_arg; password="$OPTARG" ;;
o | domainou ) needs_arg; domainou="$OPTARG" ;;
??* ) die "Illegal option --$OPT" ;; # bad long option
\? ) exit 2 ;; # bad short option (error reported via getopts)
esac
done
shift $((OPTIND-1)) # remove parsed options and args from $# list
echo "$username"
echo "$password"
echo "$domainou"

How to get multiple argumen for getopt option?

I need help to write getopt function to handle multiple argument for one option like below example, appreciate your kind support.
Example:
./getopt.sh -s abcd -s efgh -s ijkl -s bdnc -e test
This is i got so far
#!/bin/bash
OPTS=`getopt -o s:e:h -n '$0' -- "$#"`
eval set -- "$OPTS"
while true; do
case "$1" in
-s ) SOURCE=$1 ;shift ;;
-h ) echo "$0 -s source -e enviroment"; shift ;;
-e ) ENV=$1; shift; shift ;;
* ) break ;;
esac
done
if [ $ENV='TEST' ];then
echo "execute on test with $SOURCE"
elif [ $ENV='DEV' ];then
echo "execute on dev with $SOURCE"
else
exit
fi
but here I want to execute -s multiple time.
You can use the same option multiple times and add all values to an array.
like:
while getopts "o:" i; do
case $i in
o) arr+=("$OPTARG");;
#...
esac
done

Why do I have error with multi long options using getopt in bash?

I just wrote a script in bash, which work expect for multi long option:
#!/bin/bash
OPTS=`getopt -q -o fdhl: -l free,df,help,log: -- "$*"`
#Check if error with getopt
if [ $? != 0 ]
then
echo -e "error: parameter could not be found\n\nUsage:\n supervision [options]\n\n Try 'supervision --help'\n or 'supervision -h'\n for additional help text." ;
exit 1
fi
eval set -- "$OPTS"
while true ; do
case "$1" in
-f|--free)
free -h ;
shift;;
-d|--df)
df -h ; #Run df system command
shift;;
-l|--log)
case "$2" in
"") echo "miss file" ;
shift 2;; #No file passed as parameter
*)
df -h >> "$2" ;
shift 2;;
esac ;;
-h|--help) #Display help
shift;;
--) #End of parsed parameters list
shift ; break ;;
*)
break ;;
esac
done
I don't get why i'm supposed to, when I use more than 1 long option, for example:
sh myscript --free --df
And when I use --log:
sh myscript --log logfile
Both case exit on the if [ $? != 0 ], seems like the element which follow the 1st long option doesn't get parsed.
Ok, I figured out and it's all due to the using of "$*" instead of "$#" in the getopt call. I don't exactly why, i guessed both do the same thing, but it turns out to be the one which causes the problem.

Search for one command line parameter before the rest

I'm reading my command line parameters using getopt, and I'm reading a configuration file using .:
test.sh:
#!/bin/bash
set -- `getopt C:a:b:c: "$#"`
C="default.cfg"
. $C
while [ $# -gt 0 ]; do
case "$1" in
-a) cfg1="$2"; shift;;
-b) cfg2="$2"; shift;;
-c) cfg3="$2"; shift;;
-C) C="$2"; #you'll see what this is for later
shift;;
--) shift;
break;;
-*) echo "invalid option";
exit 1;;
*) break;;
esac
shift
done
echo "cfg1 = $cfg1"
echo "cfg2 = $cfg2"
echo "cfg3 = $cfg3"
exit 0
default.cfg::
cfg1=hello
cfg2=there
cfg3=friend
This all works as expected:
$ ./test.sh
cfg1 = hello
cfg2 = there
cfg3 = friend
$ ./test.sh -b optional
cfg1 = hello
cfg2 = optional
cfg3 = friend
This issue is I want configurations to be prioritized in the following manner:
options given on the command line
options defined in the config file defined by the -C option
options defined in the default config file
So if I have this:
test.cfg:
cfg1=custom_file_1
cfg2=custom_file_2
I want to get this:
$ ./test.sh -b command_line -C test.cfg
cfg1 = custom_file_1
cfg2 = command_line
cfg3 = friend
I just can't figure out how to load the default config file, then search the options for -C, then load the custom config file, overwriting the default, then search the command line parameters AGAIN and overwrite the configs again. I'm pretty new to shell scripting, so forgive me if I'm missing something obvious.
You can preprocess the arguments and pull out the value you're looking for:
#!/bin/bash
args=$(getopt C:a:b:c: "$#")
eval set -- $args
conf="default.cfg"
source "$conf"
# pre-process the arguments and see if we can find -C
found=0
for opt in "$#"; do
if [[ $found -eq 1 ]] && [[ -f "$opt" ]]; then
source "$opt"
break
fi
if [[ "$opt" == "-C" ]]; then
found=1
fi
done
while [ $# -gt 0 ]; do
case "$1" in
-a) cfg1="$2"; shift;;
-b) cfg2="$2"; shift;;
-c) cfg3="$2"; shift;;
-C) shift;; #don't do anything with this
--) shift;
break;;
-*) echo "invalid option";
exit 1;;
*) break;;
esac
shift
done
echo "cfg1 = $cfg1"
echo "cfg2 = $cfg2"
echo "cfg3 = $cfg3"
exit 0
To overwrite variables, try to replace :
-C) C="$2";
with :
-C) . "$2";
And invoke it with :
./test.sh -C test.cfg -a command_line1 -b command_line2
Update :
For options in any order, you can try this :
C="default.cfg"
. $C
while getopts C:a:b:c: OPTION
do
case $OPTION in
a) cfg1_override=$OPTARG;;
b) cfg2_override=$OPTARG;;
c) cfg3_override=$OPTARG ;;
C) . $OPTARG;;
-) break;;
-*) echo "invalid option";
exit 1;;
*) break;;
esac
done
shift $(($OPTIND - 1))
cfg1="${cfg1_override-${cfg1}}"
cfg2="${cfg2_override-${cfg2}}"
cfg3="${cfg3_override-${cfg3}}"
echo "cfg1 = $cfg1"
echo "cfg2 = $cfg2"
echo "cfg3 = $cfg3"
exit 0
Based on Is it possible to specify the order getopts conditions are executed?
First source default.cfg.
Than scan your options for a -C option. Handle this one when found.
Finally use getopts and skip -C when you find it during getopts.

Resources