Best way to parse command line args in Bash? - bash

After several days of research, I still can't figure out the best method for parsing cmdline args in a .sh script. According to my references the getopts cmd is the way to go since it "extracts and checks switches without disturbing the positional parameter variables.Unexpected switches, or switches that are missing arguments, are recognized and reportedas errors."
Positional params(Ex. 2 - $#, $#, etc) apparently don't work well when spaces are involved but can recognize regular and long parameters(-p and --longparam). I noticed that both methods fail when passing parameters with nested quotes ("this is an Ex. of ""quotes""."). Which one of these three code samples best illustrates the way to deal with cmdline args? The getopt function is not recommended by gurus, so I'm trying to avoid it!
Example 1:
#!/bin/bash
for i in "$#"
do
case $i in
-p=*|--prefix=*)
PREFIX=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
-s=*|--searchpath=*)
SEARCHPATH=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
-l=*|--lib=*)
DIR=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
--default)
DEFAULT=YES
;;
*)
# unknown option
;;
esac
done
exit 0
Example 2:
#!/bin/bash
echo ‘number of arguments’
echo "\$#: $#"
echo ”
echo ‘using $num’
echo "\$0: $0"
if [ $# -ge 1 ];then echo "\$1: $1"; fi
if [ $# -ge 2 ];then echo "\$2: $2"; fi
if [ $# -ge 3 ];then echo "\$3: $3"; fi
if [ $# -ge 4 ];then echo "\$4: $4"; fi
if [ $# -ge 5 ];then echo "\$5: $5"; fi
echo ”
echo ‘using $#’
let i=1
for x in $#; do
echo "$i: $x"
let i=$i+1
done
echo ”
echo ‘using $*’
let i=1
for x in $*; do
echo "$i: $x"
let i=$i+1
done
echo ”
let i=1
echo ‘using shift’
while [ $# -gt 0 ]
do
echo "$i: $1"
let i=$i+1
shift
done
[/bash]
output:
bash> commandLineArguments.bash
number of arguments
$#: 0
using $num
$0: ./commandLineArguments.bash
using $#
using $*
using shift
#bash> commandLineArguments.bash "abc def" g h i j*
Example 3:
#!/bin/bash
while getopts ":a:" opt; do
case $opt in
a)
echo "-a was triggered, Parameter: $OPTARG" >&2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
exit 0

I find the use of getopt to be the easiest. It provides correct handling of arguments which is tricky otherwise. For example, getopt will know how to handle arguments to a long option specified on the command line as --arg=option or --arg option.
What is useful in parsing any input passed to a shell script is the use of the "$#" variables. See the bash man page for how this differs from $#. It ensures that you can process arguments that include spaces.
Here's an example of how I might write s script to parse some simple command line arguments:
#!/bin/bash
args=$(getopt -l "searchpath:" -o "s:h" -- "$#")
eval set -- "$args"
while [ $# -ge 1 ]; do
case "$1" in
--)
# No more options left.
shift
break
;;
-s|--searchpath)
searchpath="$2"
shift
;;
-h)
echo "Display some help"
exit 0
;;
esac
shift
done
echo "searchpath: $searchpath"
echo "remaining args: $*"
And used like this to show that spaces and quotes are preserved:
user#machine:~/bin$ ./getopt_test --searchpath "File with spaces and \"quotes\"."
searchpath: File with spaces and "quotes".
remaining args: other args
Some basic information about the use of getopt can be found here

If you want to avoid using getopt you can use this nice quick approach:
Defining help with all options as ## comments (customise as you wish).
Define for each option a function with same name.
Copy the last five lines of this script to your script (the magic).
Example script: log.sh
#!/bin/sh
## $PROG 1.0 - Print logs [2017-10-01]
## Compatible with bash and dash/POSIX
##
## Usage: $PROG [OPTION...] [COMMAND]...
## Options:
## -i, --log-info Set log level to info (default)
## -q, --log-quiet Set log level to quiet
## -l, --log MESSAGE Log a message
## Commands:
## -h, --help Displays this help and exists
## -v, --version Displays output version and exists
## Examples:
## $PROG -i myscrip-simple.sh > myscript-full.sh
## $PROG -r myscrip-full.sh > myscript-simple.sh
PROG=${0##*/}
LOG=info
die() { echo $# >&2; exit 2; }
log_info() {
LOG=info
}
log_quiet() {
LOG=quiet
}
log() {
[ $LOG = info ] && echo "$1"; return 1 ## number of args used
}
help() {
grep "^##" "$0" | sed -e "s/^...//" -e "s/\$PROG/$PROG/g"; exit 0
}
version() {
help | head -1
}
[ $# = 0 ] && help
while [ $# -gt 0 ]; do
CMD=$(grep -m 1 -Po "^## *$1, --\K[^= ]*|^##.* --\K${1#--}(?:[= ])" log.sh | sed -e "s/-/_/g")
if [ -z "$CMD" ]; then echo "ERROR: Command '$1' not supported"; exit 1; fi
shift; eval "$CMD" $# || shift $? 2> /dev/null
done
Testing
Running this command:
./log.sh --log yep --log-quiet -l nop -i -l yes
Produces this output:
yep
yes
By the way: It's compatible with posix!

Related

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"

bash - passing optional arguments to script - parametername + string_value

i use a script that accepts parameter. parameters are optional and may occur in any order.
#!/bin/bash
# script name: test.sh
for var in "$#"
do
if [ ! -z "$var" ] && ([ $var = "--example" ] || [ $var = "-e" ]); then
echo "example"
elif [ ! -z "$var" ] && ([ $var = "--project" ] || [ $var = "-p" ]); then
echo "project with string xxxxxxx"
fi
done
in this simple example, you could call it like follows (some examples):
# this will echo example
./test.sh --example
# this will echo project with string xxxxxxx
./test.sh --project
# this will echo both example and project with string xxxxxxx
./test.sh --example --project
NOW, what i want to achieve is that i can do something like this (warning, this is pseuco code):
#!/bin/bash
# script name: test.sh
for var in "$#"
do
if [ ! -z "$var" ] && ([ $var = "--example" ] || [ $var = "-e" ]); then
echo "example"
elif [ ! -z "$var" ] && ([ $var = "--project" ] || [ $var = "-p" ]); then
echo "project with string $VAR_VALUE"
fi
done
# this will echo example
./test.sh --example
# this will echo project with string myproject1
./test.sh --project="myproject1"
# this will echo both example and project with string myproject2
./test.sh --example --project="myproject2"
can someone help me rewrite it so this will work somehow?
Use getopt. It handles short and long options, allows for both --long value and --long=value, decomposes -abc into -a -b -c, understands -- to end option parsing, and more.
#!/bin/bash
args=$(getopt -o ep: -l example,project: -n "$0" -- "$#") || exit
eval set -- "$args"
while [[ $1 != '--' ]]; do
case "$1" in
-e|--example) echo "example"; shift 1;;
-p|--project) echo "project = $2"; shift 2;;
# shouldn't happen unless we're missing a case
*) echo "unhandled option: $1" >&2; exit 1;;
esac
done
shift # skip '--'
echo "remaining non-option arguments: $#"
There are two possible path toward parsing argument list
Build custom option parser
use getopt, using 'long options'
The first approach is relatively simple (at this time). Using case instead of if to handle variants:
last_arg=
for arg in "$#"
do
if [ "$last_arg" = "-p" ] ; then
VAR_VALUE=$arg ;
last_arg=
echo "project with string $VAR_VALUE"
continue
fi
case "$arg" in
-e | --example)
echo "example" ;;
-p)
last_arg=$arg ;;
--project=*)
VAR_VALUE=${arg#*=}
echo "project with string $VAR_VALUE" ;;
*) ERROR-MESSAGE ;;
esac
done
exit
The BETTER approach is to leverage existing code. In particular getopt, which can handle long options:
#! /bin/bash
if T=$(getopt -o ep: --long 'example,project:' -n ${0#*/} -- "$#") ; then
eval set -- "$T"
else
exit $?
fi
while [ "$#" -gt 0 ] ; do
case "$1" in
-e | --example)
echo "example"
;;
-p | --project)
shift
VAR_VALUE=$1
echo "project with string $VAR_VALUE"
;;
--)
break
;;
*) echo "ERROR:$1" ;;
esac
shift
done

How to detect if an argument was passed to the script?

I am a novice at shell scripting. I have written a script that takes zero or more options and an optional path parameter. I want to use the current directory if a path parameter is not set.
This is the argument parsing section of the script:
OPTIONS=$(getopt -o dhlv -l drop-databases,help,learner-portal,verifier-portal -- "$#")
if [ $? -ne 0 ]; then
echo "getopt error"
exit 1
fi
eval set -- $OPTIONS
while true; do
case "$1" in
-d|--drop-databases) RESETDB=1
;;
-h|--help) echo "$usage"
exit
;;
-l|--learner-portal) LERPOR=1
;;
-v|--verifier-portal) VERPOR=1
;;
--) shift
break;;
*) echo -e "\e[31munknown option: $1\e[0m"
echo "$usage"
exit 1
;;
esac
shift
done
# Set directory of module
if [[ -n $BASH_ARGV ]]
then
MOD_DIR=$(readlink -f $BASH_ARGV)
fi
if [[ -n $MOD_DIR ]]
then
cd $MOD_DIR
fi
The script works as intended when called without and arguments, or when called with both options and a path.
However, when I run the script and only specify options, I get an error from readlink like so
$ rebuild_module -dv
readlink: invalid option -- 'd'
Try 'readlink --help' for more information.
Obviously, it's parsing the options wrong, but I'm not sure how to detect that I haven't passed a path, and therefore avoid calling readlink. How can I go about correcting this behaviour?
You can do [ $# -ne 0 ] instead of [[ -n $BASH_ARGV ]]. The former is affected by shift/set, but the latter isn't:
$ cat test.sh
echo "$#"
echo "${BASH_ARGV[#]}"
echo "$#"
eval set -- foo bar
shift
echo "$#"
echo "${BASH_ARGV[#]}"
echo "$#"
$ bash test.sh x y z
3
z y x
x y z
1
z y x
bar

How to create a bash script with optional parameters for a flag

I'm trying to create a script which will have a flag with optional options. With getopts it's possible to specify a mandatory argument (using a colon) after the flag, but I want to keep it optional.
It will be something like this:
./install.sh -a 3
or
./install.sh -a3
where 'a' is the flag and '3' is the optional parameter that follows a.
Thanks in advance.
The getopt external program allows options to have a single optional argument by adding a double-colon to the option name.
# Based on a longer example in getopt-parse.bash, included with
# getopt
TEMP=$(getopt -o a:: -- "$#")
eval set -- "$TEMP"
while true ; do
case "$1" in
-a)
case "$2" in
"") echo "Option a, no argument"; shift 2 ;;
*) echo "Option a, argument $2"; shift 2;;
esac ;;
--) shift; break ;;
*) echo "Internal error!"; exit 1 ;;
esac
done
The following is without getopt and it takes an optional argument with the -a flag:
for WORD; do
case $WORD in
-a?) echo "single arg Option"
SEP=${WORD:2:1}
echo $SEP
shift ;;
-a) echo "split arg Option"
if [[ ${2:0:1} != "-" && ${2:0:1} != ""]] ; then
SEP=$2
shift 2
echo "arg present"
echo $SEP
else
echo "optional arg omitted"
fi ;;
-a*) echo "arg Option"
SEP=${WORD:2}
echo $SEP
shift ;;
-*) echo "Unrecognized Short Option"
echo "Unrecognized argument"
;;
esac
done
Other options/flags also can be added easily.
Use the getopt feature. On most systems, man getopt will yield documentation for it, and even examples of using it in a script. From the man page on my system:
The following code fragment shows how one might process the arguments
for a command that can take the options -a and -b, and the option -o,
which requires an argument.
args=`getopt abo: $*`
# you should not use `getopt abo: "$#"` since that would parse
# the arguments differently from what the set command below does.
if [ $? != 0 ]
then
echo 'Usage: ...'
exit 2
fi
set -- $args
# You cannot use the set command with a backquoted getopt directly,
# since the exit code from getopt would be shadowed by those of set,
# which is zero by definition.
for i
do
case "$i"
in
-a|-b)
echo flag $i set; sflags="${i#-}$sflags";
shift;;
-o)
echo oarg is "'"$2"'"; oarg="$2"; shift;
shift;;
--)
shift; break;;
esac
done
echo single-char flags: "'"$sflags"'"
echo oarg is "'"$oarg"'"
This code will accept any of the following as equivalent:
cmd -aoarg file file
cmd -a -o arg file file
cmd -oarg -a file file
cmd -a -oarg -- file file
In bash there is some implicit variable:
$#: contains number of arguments for a called script/function
$0: contains names of script/function
$1: contains first argument
$2: contains second argument
...
$n: contains n-th argument
For example:
#!/bin/ksh
if [ $# -ne 2 ]
then
echo "Wrong number of argument - expected 2 : $#"
else
echo "Argument list:"
echo "\t$0"
echo "\t$1"
echo "\t$2"
fi
My solution:
#!/bin/bash
count=0
skip=0
flag="no flag"
list=($#) #put args in array
for arg in $# ; do #iterate over array
count=$(($count+1)) #update counter
if [ $skip -eq 1 ]; then #check if we have to skip this args
skip=0
continue
fi
opt=${arg:0:2} #get only first 2 chars as option
if [ $opt == "-a" ]; then #check if option equals "-a"
if [ $opt == $arg ] ; then #check if this is only the option or has a flag
if [ ${list[$count]:0:1} != "-" ]; then #check if next arg is an option
skip=1 #skip next arg
flag=${list[$count]} #use next arg as flag
fi
else
flag=${arg:2} #use chars after "-a" as flag
fi
fi
done
echo $flag

Converting Bash command line options to variable name

I am trying to write a bash script that takes in an option.
Lets call these options A and B.
In the script A and B may or may not be defined as variables.
I want to be able to check if the variable is defined or not.
I have tried the following but it doesn't work.
if [ ! -n $1 ]; then
echo "Error"
fi
Thanks
The "correct" way to test whether a variable is set is to use the + expansion option. You'll see this a lot in configure scripts:
if test -s "${foo+set}"
where ${foo+set} expands to "set" if it is set or "" if it's not. This allows for the variable to be set but empty, if you need it. ${foo:+set} additionally requires $foo to not be empty.
(That $(eval echo $a) thing has problems: it's slow, and it's vulnerable to code injection (!).)
Oh, and if you just want to throw an error if something required isn't set, you can just refer to the variable as ${foo:?} (leave off the : if set but empty is permissible), or for a custom error message ${foo:?Please specify a foo.}.
You did not define how these options should be passed in, but I think:
if [ -z "$1" ]; then
echo "Error"
exit 1
fi
is what you are looking for.
However, if some of these options are, err, optional, then you might want something like:
#!/bin/bash
USAGE="$0: [-a] [--alpha] [-b type] [--beta file] [-g|--gamma] args..."
ARGS=`POSIXLY_CORRECT=1 getopt -n "$0" -s bash -o ab:g -l alpha,beta:,gamma -- "$#"`
if [ $? -ne 0 ]
then
echo "$USAGE" >&2
exit 1
fi
eval set -- "$ARGS"
unset ARGS
while true
do
case "$1" in
-a) echo "Option a"; shift;;
--alpha) echo "Option alpha"; shift;;
-b) echo "Option b, arg '$2'"; shift 2;;
--beta) echo "Option beta, arg '$2'"; shift 2;;
-g|--gamma) echo "Option g or gamma"; shift;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
echo Remaining args
for arg in "$#"
do
echo '--> '"\`$arg'"
done
exit 0
Don't do it that way, try this:
if [[ -z $1 ]]; then
echo "Error"
fi
The error in your version is actually the lack of quoting.
Should be:
if [ ! -n "$1" ]; then
echo "Error"
fi
But you don't need the negation, use -z instead.
If you work on Bash, then use double brackets [[ ]] too.
from the man bash page:
-z string
True if the length of string is zero.
-n string
True if the length of string is non-zero.
Also, if you use bash v4 or greater (bash --version) there's -v
-v varname
True if the shell variable varname is set (has been assigned a value).
The trick is "$1", i.e.
root#root:~# cat auto.sh
Usage () {
echo "error"
}
if [ ! -n $1 ];then
Usage
exit 1
fi
root#root:~# bash auto.sh
root#root:~# cat auto2.sh
Usage () {
echo "error"
}
if [ ! -n "$1" ];then
Usage
exit 1
fi
root#root:~# bash auto2.sh
error

Resources