Using getopts to process long and short command line options - bash
I wish to have long and short forms of command line options invoked using my shell script.
I know that getopts can be used, but like in Perl, I have not been able to do the same with shell.
Any ideas on how this can be done, so that I can use options like:
./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/
In the above, both the commands mean the same thing to my shell, but using getopts, I have not been able to implement these?
getopt and getopts are different beasts, and people seem to have a bit of misunderstanding of what they do. getopts is a built-in command to bash to process command-line options in a loop and assign each found option and value in turn to built-in variables, so you can further process them. getopt, however, is an external utility program, and it doesn't actually process your options for you the way that e.g. bash getopts, the Perl Getopt module or the Python optparse/argparse modules do. All that getopt does is canonicalize the options that are passed in — i.e. convert them to a more standard form, so that it's easier for a shell script to process them. For example, an application of getopt might convert the following:
myscript -ab infile.txt -ooutfile.txt
into this:
myscript -a -b -o outfile.txt infile.txt
You have to do the actual processing yourself. You don't have to use getopt at all if you make various restrictions on the way you can specify options:
only put one option per argument;
all options go before any positional parameters (i.e. non-option arguments);
for options with values (e.g. -o above), the value has to go as a separate argument (after a space).
Why use getopt instead of getopts? The basic reason is that only GNU getopt gives you support for long-named command-line options.1 (GNU getopt is the default on Linux. Mac OS X and FreeBSD come with a basic and not-very-useful getopt, but the GNU version can be installed; see below.)
For example, here's an example of using GNU getopt, from a script of mine called javawrap:
# NOTE: This requires GNU getopt. On Mac OS X and FreeBSD, you have to install this
# separately; see below.
TEMP=$(getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \
-n 'javawrap' -- "$#")
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
# Note the quotes around '$TEMP': they are essential!
eval set -- "$TEMP"
VERBOSE=false
DEBUG=false
MEMORY=
DEBUGFILE=
JAVA_MISC_OPT=
while true; do
case "$1" in
-v | --verbose ) VERBOSE=true; shift ;;
-d | --debug ) DEBUG=true; shift ;;
-m | --memory ) MEMORY="$2"; shift 2 ;;
--debugfile ) DEBUGFILE="$2"; shift 2 ;;
--minheap )
JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;;
--maxheap )
JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;;
-- ) shift; break ;;
* ) break ;;
esac
done
This lets you specify options like --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt" or similar. The effect of the call to getopt is to canonicalize the options to --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt" so that you can more easily process them. The quoting around "$1" and "$2" is important as it ensures that arguments with spaces in them get handled properly.
If you delete the first 9 lines (everything up through the eval set line), the code will still work! However, your code will be much pickier in what sorts of options it accepts: In particular, you'll have to specify all options in the "canonical" form described above. With the use of getopt, however, you can group single-letter options, use shorter non-ambiguous forms of long-options, use either the --file foo.txt or --file=foo.txt style, use either the -m 4096 or -m4096 style, mix options and non-options in any order, etc. getopt also outputs an error message if unrecognized or ambiguous options are found.
NOTE: There are actually two totally different versions of getopt, basic getopt and GNU getopt, with different features and different calling conventions.2 Basic getopt is quite broken: Not only does it not handle long options, it also can't even handle embedded spaces inside of arguments or empty arguments, whereas getopts does do this right. The above code will not work in basic getopt. GNU getopt is installed by default on Linux, but on Mac OS X and FreeBSD it needs to be installed separately. On Mac OS X, install MacPorts (http://www.macports.org) and then do sudo port install getopt to install GNU getopt (usually into /opt/local/bin), and make sure that /opt/local/bin is in your shell path ahead of /usr/bin. On FreeBSD, install misc/getopt.
A quick guide to modifying the example code for your own program: Of the first few lines, all is "boilerplate" that should stay the same, except the line that calls getopt. You should change the program name after -n, specify short options after -o, and long options after --long. Put a colon after options that take a value.
Finally, if you see code that has just set instead of eval set, it was written for BSD getopt. You should change it to use the eval set style, which works fine with both versions of getopt, while the plain set doesn't work right with GNU getopt.
1Actually, getopts in ksh93 supports long-named options, but this shell isn't used as often as bash. In zsh, use zparseopts to get this functionality.
2Technically, "GNU getopt" is a misnomer; this version was actually written for Linux rather than the GNU project. However, it follows all the GNU conventions, and the term "GNU getopt" is commonly used (e.g. on FreeBSD).
There are three implementations that may be considered:
Bash builtin getopts. This does not support long option names with the double-dash prefix. It only supports single-character options.
BSD UNIX implementation of standalone getopt command (which is what MacOS uses). This does not support long options either.
GNU implementation of standalone getopt. GNU getopt(3) (used by the command-line getopt(1) on Linux) supports parsing long options.
Some other answers show a solution for using the bash builtin getopts to mimic long options. That solution actually makes a short option whose character is "-". So you get "--" as the flag. Then anything following that becomes OPTARG, and you test the OPTARG with a nested case.
This is clever, but it comes with caveats:
getopts can't enforce the opt spec. It can't return errors if the user supplies an invalid option. You have to do your own error-checking as you parse OPTARG.
OPTARG is used for the long option name, which complicates usage when your long option itself has an argument. You end up having to code that yourself as an additional case.
So while it is possible to write more code to work around the lack of support for long options, this is a lot more work and partially defeats the purpose of using a getopt parser to simplify your code.
The Bash builtin getopts function can be used to parse long options by putting a dash character followed by a colon into the optspec:
#!/usr/bin/env bash
optspec=":hv-:"
while getopts "$optspec" optchar; do
case "${optchar}" in
-)
case "${OPTARG}" in
loglevel)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
;;
loglevel=*)
val=${OPTARG#*=}
opt=${OPTARG%=$val}
echo "Parsing option: '--${opt}', value: '${val}'" >&2
;;
*)
if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
echo "Unknown option --${OPTARG}" >&2
fi
;;
esac;;
h)
echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
exit 2
;;
v)
echo "Parsing option: '-${optchar}'" >&2
;;
*)
if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
echo "Non-option argument: '-${OPTARG}'" >&2
fi
;;
esac
done
After copying to executable file name=getopts_test.sh in the current working directory, one can produce output like
$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'
Obviously getopts neither performs OPTERR checking nor option-argument parsing for the long options. The script fragment above shows how this may be done manually. The basic principle also works in the Debian Almquist shell ("dash"). Note the special case:
getopts -- "-:" ## without the option terminator "-- " bash complains about "-:"
getopts "-:" ## this works in the Debian Almquist shell ("dash")
Note that, as GreyCat from over at http://mywiki.wooledge.org/BashFAQ points out, this trick exploits a non-standard behaviour of the shell which permits the option-argument (i.e. the filename in "-f filename") to be concatenated to the option (as in "-ffilename"). The POSIX standard says there must be a space between them, which in the case of "-- longoption" would terminate the option-parsing and turn all longoptions into non-option arguments.
The built-in getopts command is still, AFAIK, limited to single-character options only.
There is (or used to be) an external program getopt that would reorganize a set of options such that it was easier to parse. You could adapt that design to handle long options too. Example usage:
aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$#")
while [ $# -gt 0 ]
do
case "$1" in
(-a) aflag=yes;;
(-b) bflag=yes;;
(-f) flist="$flist $2"; shift;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
# Process remaining non-option arguments
...
You could use a similar scheme with a getoptlong command.
Note that the fundamental weakness with the external getopt program is the difficulty of handling arguments with spaces in them, and in preserving those spaces accurately. This is why the built-in getopts is superior, albeit limited by the fact it only handles single-letter options.
Long options can be parsed by the standard getopts builtin as “arguments” to the - “option”
This is portable and native POSIX shell – no external programs or bashisms are needed.
This guide implements long options as arguments to the - option, so --alpha is seen by getopts as - with argument alpha and --bravo=foo is seen as - with argument bravo=foo. The true argument is harvested via shell parameter expansion, updating $OPT and $OPTARG.
In this example, -b and -c (and their long forms, --bravo and --charlie) have mandatory arguments. Arguments to long options come after equals signs, e.g. --bravo=foo (space delimiters for long options would be hard to implement, see below).
Because this uses the getopts builtin, this solution supports usage like cmd --bravo=foo -ac FILE (which has combined options -a and -c and interleaves long options with standard options) while most other answers here either struggle or fail to do that.
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
a | alpha ) alpha=true ;;
b | bravo ) needs_arg; bravo="$OPTARG" ;;
c | charlie ) needs_arg; charlie="$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
When the option is a dash (-), it is a long option. getopts will have parsed the actual long option into $OPTARG, e.g. --bravo=foo originally sets OPT='-' and OPTARG='bravo=foo'. The if stanza sets $OPT to the contents of $OPTARG before the first equals sign (bravo in our example) and then removes that from the beginning of $OPTARG (yielding =foo in this step, or an empty string if there is no =). Finally, we strip the argument's leading =. At this point, $OPT is either a short option (one character) or a long option (2+ characters).
The case then matches either short or long options (the pipe, |, indicates that "or" operation. A long-only option like delta ) delta=true ;; doesn't need a pipe). For short options, getopts automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg function, which fatally exits when $OPTARG is empty. The ??* condition will match any remaining long option (? matches a single character and * matches zero or more, so ??* matches 2+ characters), allowing us to issue the "Illegal option" error before exiting.
As is normal for GNU-style long options, providing -- will stop parsing, so -a -- --bravo=4 will set $alpha to true but $bravo will remain untouched and $1 will be --bravo=4. I can't say I recommend naming files with leading dashes, but this is how you denote they aren't options.
Minor bug: If somebody gives an invalid single-character long option (and it's not also a short option), this will exit with an error but without a message (this implementation assumes it's a short option). You could track that with an extra variable in the conditional preceding the case and then test for it in the final case condition, but I consider that too much of a corner case to bother.
Uppercase variable names: Generally, the advice is to reserve all-uppercase variables for system use. I'm keeping $OPT as all-uppercase to keep it in line with $OPTARG, but this does break that convention. I think it fits because this is something the system should have done, and it should be safe; I haven't heard of any standards that use this variable name.
To complain about unexpected arguments to long options: Mimic needs_arg with a flipped test to complain about an argument when one isn't expected:
no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }
To accept long options with space-delimited arguments: You can pull in the next argument with eval "ARG_B=\"\$$OPTIND\"" (or with bash's indirect expansion, ARG_B="${!OPTIND}") and then increment $OPTIND as noted in an older version of this answer, but it's unreliable; getopts can prematurely terminate on the assumption that the argument was beyond its scope and some implementations don't take well to manual manipulations of $OPTIND.
Here's an example that actually uses getopt with long options:
aflag=no
bflag=no
cargument=none
# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$#")
then
# something went wrong, getopt will put out an error message for us
exit 1
fi
set -- $options
while [ $# -gt 0 ]
do
case $1 in
-a|--along) aflag="yes" ;;
-b|--blong) bflag="yes" ;;
# for options with required arguments, an additional shift is required
-c|--clong) cargument="$2" ; shift;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
Using getopts with short/long options and arguments
Works with all combinations, e.g:
foobar -f --bar
foobar --foo -b
foobar -bf --bar --foobar
foobar -fbFBAshorty --bar -FB --arguments=longhorn
foobar -fA "text shorty" -B --arguments="text longhorn"
bash foobar -F --barfoo
sh foobar -B --foobar - ...
bash ./foobar -F --bar
Some declarations for this example
Options=$#
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty
How the Usage function would look
function _usage()
{
###### U S A G E : Help and ERROR ######
cat <<EOF
foobar $Options
$*
Usage: foobar <[options]>
Options:
-b --bar Set bar to yes ($foo)
-f --foo Set foo to yes ($bart)
-h --help Show this message
-A --arguments=... Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
-B --barfoo Set barfoo to yes ($barfoo)
-F --foobar Set foobar to yes ($foobar)
EOF
}
[ $# = 0 ] && _usage " >>>>>>>> no options given "
getops with long/short flags as well as long arguments
while getopts ':bfh-A:BF' OPTION ; do
case "$OPTION" in
b ) sbar=yes ;;
f ) sfoo=yes ;;
h ) _usage ;;
A ) sarguments=yes;sARG="$OPTARG" ;;
B ) sbarfoo=yes ;;
F ) sfoobar=yes ;;
- ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
eval OPTION="\$$optind"
OPTARG=$(echo $OPTION | cut -d'=' -f2)
OPTION=$(echo $OPTION | cut -d'=' -f1)
case $OPTION in
--foo ) lfoo=yes ;;
--bar ) lbar=yes ;;
--foobar ) lfoobar=yes ;;
--barfoo ) lbarfoo=yes ;;
--help ) _usage ;;
--arguments ) larguments=yes;lARG="$OPTARG" ;;
* ) _usage " Long: >>>>>>>> invalid options (long) " ;;
esac
OPTIND=1
shift
;;
? ) _usage "Short: >>>>>>>> invalid options (short) " ;;
esac
done
Output
##################################################################
echo "----------------------------------------------------------"
echo "RESULT short-foo : $sfoo long-foo : $lfoo"
echo "RESULT short-bar : $sbar long-bar : $lbar"
echo "RESULT short-foobar : $sfoobar long-foobar : $lfoobar"
echo "RESULT short-barfoo : $sbarfoo long-barfoo : $lbarfoo"
echo "RESULT short-arguments: $sarguments with Argument = \"$sARG\" long-arguments: $larguments and $lARG"
Combining the above into a cohesive script
#!/bin/bash
# foobar: getopts with short and long options AND arguments
function _cleanup ()
{
unset -f _usage _cleanup ; return 0
}
## Clear out nested functions on exit
trap _cleanup INT EXIT RETURN
###### some declarations for this example ######
Options=$#
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty
function _usage()
{
###### U S A G E : Help and ERROR ######
cat <<EOF
foobar $Options
$*
Usage: foobar <[options]>
Options:
-b --bar Set bar to yes ($foo)
-f --foo Set foo to yes ($bart)
-h --help Show this message
-A --arguments=... Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
-B --barfoo Set barfoo to yes ($barfoo)
-F --foobar Set foobar to yes ($foobar)
EOF
}
[ $# = 0 ] && _usage " >>>>>>>> no options given "
##################################################################
####### "getopts" with: short options AND long options #######
####### AND short/long arguments #######
while getopts ':bfh-A:BF' OPTION ; do
case "$OPTION" in
b ) sbar=yes ;;
f ) sfoo=yes ;;
h ) _usage ;;
A ) sarguments=yes;sARG="$OPTARG" ;;
B ) sbarfoo=yes ;;
F ) sfoobar=yes ;;
- ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
eval OPTION="\$$optind"
OPTARG=$(echo $OPTION | cut -d'=' -f2)
OPTION=$(echo $OPTION | cut -d'=' -f1)
case $OPTION in
--foo ) lfoo=yes ;;
--bar ) lbar=yes ;;
--foobar ) lfoobar=yes ;;
--barfoo ) lbarfoo=yes ;;
--help ) _usage ;;
--arguments ) larguments=yes;lARG="$OPTARG" ;;
* ) _usage " Long: >>>>>>>> invalid options (long) " ;;
esac
OPTIND=1
shift
;;
? ) _usage "Short: >>>>>>>> invalid options (short) " ;;
esac
done
Take a look at shFlags which is a portable shell library (meaning: sh, bash, dash, ksh, zsh on Linux, Solaris, etc.).
It makes adding new flags as simple as adding one line to your script, and it provides an auto generated usage function.
Here is a simple Hello, world! using shFlag:
#!/bin/sh
# source shflags from current directory
. ./shflags
# define a 'name' command-line string flag
DEFINE_string 'name' 'world' 'name to say hello to' 'n'
# parse the command-line
FLAGS "$#" || exit 1
eval set -- "${FLAGS_ARGV}"
# say hello
echo "Hello, ${FLAGS_name}!"
For OSes that have the enhanced getopt that supports long options (e.g. Linux), you can do:
$ ./hello_world.sh --name Kate
Hello, Kate!
For the rest, you must use the short option:
$ ./hello_world.sh -n Kate
Hello, Kate!
Adding a new flag is as simple as adding a new DEFINE_ call.
Another way...
# translate long options to short
for arg
do
delim=""
case "$arg" in
--help) args="${args}-h ";;
--verbose) args="${args}-v ";;
--config) args="${args}-c ";;
# pass through anything else
*) [[ "${arg:0:1}" == "-" ]] || delim="\""
args="${args}${delim}${arg}${delim} ";;
esac
done
# reset the translated args
eval set -- $args
# now we can process with getopt
while getopts ":hvc:" opt; do
case $opt in
h) usage ;;
v) VERBOSE=true ;;
c) source $OPTARG ;;
\?) usage ;;
:)
echo "option -$OPTARG requires an argument"
usage
;;
esac
done
I kind of solved this way:
# A string with command options
options=$#
# An array with all the arguments
arguments=($options)
# Loop index
index=0
for argument in $options
do
# Incrementing index
index=`expr $index + 1`
# The conditions
case $argument in
-a) echo "key $argument value ${arguments[index]}" ;;
-abc) echo "key $argument value ${arguments[index]}" ;;
esac
done
exit;
Am I being dumb or something? getopt and getopts are so confusing.
In case you don't want the getopt dependency, you can do this:
while test $# -gt 0
do
case $1 in
# Normal option processing
-h | --help)
# usage and help
;;
-v | --version)
# version info
;;
# ...
# Special cases
--)
break
;;
--*)
# error unknown (long) option $1
;;
-?)
# error unknown (short) option $1
;;
# FUN STUFF HERE:
# Split apart combined short options
-*)
split=$1
shift
set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$#"
continue
;;
# Done with options
*)
break
;;
esac
# for testing purposes:
echo "$1"
shift
done
Of course, then you can't use long style options with one dash. And if you want to add shortened versions (e.g. --verbos instead of --verbose), then you need to add those manually.
But if you are looking to get getopts functionality along with long options, this is a simple way to do it.
I also put this snippet in a gist.
The built-in getopts can't do this. There is an external getopt(1) program that can do this, but you only get it on Linux from the util-linux package. It comes with an example script getopt-parse.bash.
There is also a getopts_long written as a shell function.
#!/bin/bash
while getopts "abc:d:" flag
do
case $flag in
a) echo "[getopts:$OPTIND]==> -$flag";;
b) echo "[getopts:$OPTIND]==> -$flag";;
c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
esac
done
shift $((OPTIND-1))
echo "[otheropts]==> $#"
exit
.
#!/bin/bash
until [ -z "$1" ]; do
case $1 in
"--dlong")
shift
if [ "${1:1:0}" != "-" ]
then
echo "==> dlong $1"
shift
fi;;
*) echo "==> other $1"; shift;;
esac
done
exit
The accepted answer does a very nice job of pointing out all the shortcomings of bash built-in getopts. The answer ends with:
So while it is possible to write more code to work around the lack of support for long options, this is a lot more work and partially defeats the purpose of using a getopt parser to simplify your code.
And even though I agree in principle with that statement, I feel that the number of times we all implemented this feature in various scripts justifies putting a bit of effort into creating a "standardised", well tested solution.
As such, I've "upgraded" bash built in getopts by implementing getopts_long in pure bash, with no external dependencies. The usage of the function is 100% compatible with the built-in getopts.
By including getopts_long (which is hosted on GitHub) in a script, the answer to the original question can be implemented as simply as:
source "${PATH_TO}/getopts_long.bash"
while getopts_long ':c: copyfile:' OPTKEY; do
case ${OPTKEY} in
'c'|'copyfile')
echo 'file supplied -- ${OPTARG}'
;;
'?')
echo "INVALID OPTION -- ${OPTARG}" >&2
exit 1
;;
':')
echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
exit 1
;;
*)
echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
exit 1
;;
esac
done
shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
In ksh93, getopts does support long names...
while getopts "f(file):s(server):" flag
do
echo "$flag" $OPTIND $OPTARG
done
Or so the tutorials I have found have said. Try it and see.
Inventing yet another version of the wheel...
This function is a (hopefully) POSIX-compatible plain bourne shell replacement for GNU getopt. It supports short/long options which can accept mandatory/optional/no arguments, and the way in which options are specified is almost identical to GNU getopt, so conversion is trivial.
Of course this is still a sizeable chunk of code to drop into a script, but it's about half the lines of the well-known getopt_long shell function, and might be preferable in cases where you just want to replace existing GNU getopt uses.
This is pretty new code, so YMMV (and definitely please let me know if this isn't actually POSIX-compatible for any reason -- portability was the intention from the outset, but I don't have a useful POSIX test environment).
Code and example usage follows:
#!/bin/sh
# posix_getopt shell function
# Author: Phil S.
# Version: 1.0
# Created: 2016-07-05
# URL: http://stackoverflow.com/a/37087374/324105
# POSIX-compatible argument quoting and parameter save/restore
# http://www.etalabs.net/sh_tricks.html
# Usage:
# parameters=$(save "$#") # save the original parameters.
# eval "set -- ${parameters}" # restore the saved parameters.
save () {
local param
for param; do
printf %s\\n "$param" \
| sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
done
printf %s\\n " "
}
# Exit with status $1 after displaying error message $2.
exiterr () {
printf %s\\n "$2" >&2
exit $1
}
# POSIX-compatible command line option parsing.
# This function supports long options and optional arguments, and is
# a (largely-compatible) drop-in replacement for GNU getopt.
#
# Instead of:
# opts=$(getopt -o "$shortopts" -l "$longopts" -- "$#")
# eval set -- ${opts}
#
# We instead use:
# opts=$(posix_getopt "$shortopts" "$longopts" "$#")
# eval "set -- ${opts}"
posix_getopt () { # args: "$shortopts" "$longopts" "$#"
local shortopts longopts \
arg argtype getopt nonopt opt optchar optword suffix
shortopts="$1"
longopts="$2"
shift 2
getopt=
nonopt=
while [ $# -gt 0 ]; do
opt=
arg=
argtype=
case "$1" in
# '--' means don't parse the remaining options
( -- ) {
getopt="${getopt}$(save "$#")"
shift $#
break
};;
# process short option
( -[!-]* ) { # -x[foo]
suffix=${1#-?} # foo
opt=${1%$suffix} # -x
optchar=${opt#-} # x
case "${shortopts}" in
( *${optchar}::* ) { # optional argument
argtype=optional
arg="${suffix}"
shift
};;
( *${optchar}:* ) { # required argument
argtype=required
if [ -n "${suffix}" ]; then
arg="${suffix}"
shift
else
case "$2" in
( -* ) exiterr 1 "$1 requires an argument";;
( ?* ) arg="$2"; shift 2;;
( * ) exiterr 1 "$1 requires an argument";;
esac
fi
};;
( *${optchar}* ) { # no argument
argtype=none
arg=
shift
# Handle multiple no-argument parameters combined as
# -xyz instead of -x -y -z. If we have just shifted
# parameter -xyz, we now replace it with -yz (which
# will be processed in the next iteration).
if [ -n "${suffix}" ]; then
eval "set -- $(save "-${suffix}")$(save "$#")"
fi
};;
( * ) exiterr 1 "Unknown option $1";;
esac
};;
# process long option
( --?* ) { # --xarg[=foo]
suffix=${1#*=} # foo (unless there was no =)
if [ "${suffix}" = "$1" ]; then
suffix=
fi
opt=${1%=$suffix} # --xarg
optword=${opt#--} # xarg
case ",${longopts}," in
( *,${optword}::,* ) { # optional argument
argtype=optional
arg="${suffix}"
shift
};;
( *,${optword}:,* ) { # required argument
argtype=required
if [ -n "${suffix}" ]; then
arg="${suffix}"
shift
else
case "$2" in
( -* ) exiterr 1 \
"--${optword} requires an argument";;
( ?* ) arg="$2"; shift 2;;
( * ) exiterr 1 \
"--${optword} requires an argument";;
esac
fi
};;
( *,${optword},* ) { # no argument
if [ -n "${suffix}" ]; then
exiterr 1 "--${optword} does not take an argument"
fi
argtype=none
arg=
shift
};;
( * ) exiterr 1 "Unknown option $1";;
esac
};;
# any other parameters starting with -
( -* ) exiterr 1 "Unknown option $1";;
# remember non-option parameters
( * ) nonopt="${nonopt}$(save "$1")"; shift;;
esac
if [ -n "${opt}" ]; then
getopt="${getopt}$(save "$opt")"
case "${argtype}" in
( optional|required ) {
getopt="${getopt}$(save "$arg")"
};;
esac
fi
done
# Generate function output, suitable for:
# eval "set -- $(posix_getopt ...)"
printf %s "${getopt}"
if [ -n "${nonopt}" ]; then
printf %s "$(save "--")${nonopt}"
fi
}
Example usage:
# Process command line options
shortopts="hvd:c::s::L:D"
longopts="help,version,directory:,client::,server::,load:,delete"
#opts=$(getopt -o "$shortopts" -l "$longopts" -n "$(basename $0)" -- "$#")
opts=$(posix_getopt "$shortopts" "$longopts" "$#")
if [ $? -eq 0 ]; then
#eval set -- ${opts}
eval "set -- ${opts}"
while [ $# -gt 0 ]; do
case "$1" in
( -- ) shift; break;;
( -h|--help ) help=1; shift; break;;
( -v|--version ) version_help=1; shift; break;;
( -d|--directory ) dir=$2; shift 2;;
( -c|--client ) useclient=1; client=$2; shift 2;;
( -s|--server ) startserver=1; server_name=$2; shift 2;;
( -L|--load ) load=$2; shift 2;;
( -D|--delete ) delete=1; shift;;
esac
done
else
shorthelp=1 # getopt returned (and reported) an error.
fi
I only write shell scripts now and then and fall out of practice, so any feedback is appreciated.
Using the strategy proposed by #Arvid Requate, we noticed some user errors. A user who forgets to include a value will accidentally have the next option's name treated as a value:
./getopts_test.sh --loglevel= --toc=TRUE
will cause the value of "loglevel" to be seen as "--toc=TRUE". This can
be avoided.
I adapted some ideas about checking user error for CLI from http://mwiki.wooledge.org/BashFAQ/035 discussion of manual parsing. I inserted error checking into handling both "-" and "--" arguments.
Then I started fiddling around with the syntax, so any errors in here are strictly my fault, not the original authors.
My approach helps users who prefer to enter long with or without the equal sign. That is, it should have same response to "--loglevel 9" as "--loglevel=9". In the --/space method, it is not possible to know for sure if the user forgets an argument, so some guessing is needed.
if the user has the long/equal sign format (--opt=), then a space after = triggers an error because an argument was not supplied.
if user has long/space arguments (--opt ), this script causes a fail if no argument follows (end of command) or if argument begins with dash)
In case you are starting out on this, there is an interesting difference between "--opt=value" and "--opt value" formats. With the equal sign, the command line argument is seen as "opt=value" and the work to handle that is string parsing, to separate at the "=". In contrast, with "--opt value", the name of the argument is "opt" and we have the challenge of getting the next value supplied in the command line. That's where #Arvid Requate used ${!OPTIND}, the indirect reference. I still don't understand that, well, at all, and comments in BashFAQ seem to warn against that style (http://mywiki.wooledge.org/BashFAQ/006). BTW, I don't think previous poster's comments about importance of OPTIND=$(( $OPTIND + 1 )) are correct. I mean to say, I see no harm from omitting it.
In newest version of this script, flag -v means VERBOSE printout.
Save it in a file called "cli-5.sh", make executable, and any of these will work, or fail in the desired way
./cli-5.sh -v --loglevel=44 --toc TRUE
./cli-5.sh -v --loglevel=44 --toc=TRUE
./cli-5.sh --loglevel 7
./cli-5.sh --loglevel=8
./cli-5.sh -l9
./cli-5.sh --toc FALSE --loglevel=77
./cli-5.sh --toc=FALSE --loglevel=77
./cli-5.sh -l99 -t yyy
./cli-5.sh -l 99 -t yyy
Here is example output of the error-checking on user intpu
$ ./cli-5.sh --toc --loglevel=77
ERROR: toc value must not have dash at beginning
$ ./cli-5.sh --toc= --loglevel=77
ERROR: value for toc undefined
You should consider turning on -v, because it prints out internals of OPTIND and OPTARG
#/usr/bin/env bash
## Paul Johnson
## 20171016
##
## Combines ideas from
## https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
## by #Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035
# What I don't understand yet:
# In #Arvid REquate's answer, we have
# val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
# this works, but I don't understand it!
die() {
printf '%s\n' "$1" >&2
exit 1
}
printparse(){
if [ ${VERBOSE} -gt 0 ]; then
printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
fi
}
showme(){
if [ ${VERBOSE} -gt 0 ]; then
printf 'VERBOSE: %s\n' "$1" >&2;
fi
}
VERBOSE=0
loglevel=0
toc="TRUE"
optspec=":vhl:t:-:"
while getopts "$optspec" OPTCHAR; do
showme "OPTARG: ${OPTARG[*]}"
showme "OPTIND: ${OPTIND[*]}"
case "${OPTCHAR}" in
-)
case "${OPTARG}" in
loglevel) #argument has no equal sign
opt=${OPTARG}
val="${!OPTIND}"
## check value. If negative, assume user forgot value
showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
if [[ "$val" == -* ]]; then
die "ERROR: $opt value must not have dash at beginning"
fi
## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
printparse "--${OPTARG}" " " "${val}"
loglevel="${val}"
shift
;;
loglevel=*) #argument has equal sign
opt=${OPTARG%=*}
val=${OPTARG#*=}
if [ "${OPTARG#*=}" ]; then
printparse "--${opt}" "=" "${val}"
loglevel="${val}"
## shift CAUTION don't shift this, fails othewise
else
die "ERROR: $opt value must be supplied"
fi
;;
toc) #argument has no equal sign
opt=${OPTARG}
val="${!OPTIND}"
## check value. If negative, assume user forgot value
showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
if [[ "$val" == -* ]]; then
die "ERROR: $opt value must not have dash at beginning"
fi
## OPTIND=$(( $OPTIND + 1 )) #??
printparse "--${opt}" " " "${val}"
toc="${val}"
shift
;;
toc=*) #argument has equal sign
opt=${OPTARG%=*}
val=${OPTARG#*=}
if [ "${OPTARG#*=}" ]; then
toc=${val}
printparse "--$opt" " -> " "$toc"
##shift ## NO! dont shift this
else
die "ERROR: value for $opt undefined"
fi
;;
help)
echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
exit 2
;;
*)
if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
echo "Unknown option --${OPTARG}" >&2
fi
;;
esac;;
h|-\?|--help)
## must rewrite this for all of the arguments
echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
exit 2
;;
l)
loglevel=${OPTARG}
printparse "-l" " " "${loglevel}"
;;
t)
toc=${OPTARG}
;;
v)
VERBOSE=1
;;
*)
if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
echo "Non-option argument: '-${OPTARG}'" >&2
fi
;;
esac
done
echo "
After Parsing values
"
echo "loglevel $loglevel"
echo "toc $toc"
I don't have enough rep yet to comment or vote his solution up, but sme's answer worked extremely well for me. The only issue I ran into was that the arguments end up wrapped in single-quotes (so I have an strip them out).
I also added some example usages and HELP text. I'll included my slightly extended version here:
#!/bin/bash
# getopt example
# from: https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
HELP_TEXT=\
" USAGE:\n
Accepts - and -- flags, can specify options that require a value, and can be in any order. A double-hyphen (--) will stop processing options.\n\n
Accepts the following forms:\n\n
getopt-example.sh -a -b -c value-for-c some-arg\n
getopt-example.sh -c value-for-c -a -b some-arg\n
getopt-example.sh -abc some-arg\n
getopt-example.sh --along --blong --clong value-for-c -a -b -c some-arg\n
getopt-example.sh some-arg --clong value-for-c\n
getopt-example.sh
"
aflag=false
bflag=false
cargument=""
# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc:h\? -l along,blong,help,clong: -- "$#")
then
# something went wrong, getopt will put out an error message for us
exit 1
fi
set -- $options
while [ $# -gt 0 ]
do
case $1 in
-a|--along) aflag=true ;;
-b|--blong) bflag=true ;;
# for options with required arguments, an additional shift is required
-c|--clong) cargument="$2" ; shift;;
-h|--help|-\?) echo -e $HELP_TEXT; exit;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
# to remove the single quotes around arguments, pipe the output into:
# | sed -e "s/^'\\|'$//g" (just leading/trailing) or | sed -e "s/'//g" (all)
echo aflag=${aflag}
echo bflag=${bflag}
echo cargument=${cargument}
while [ $# -gt 0 ]
do
echo arg=$1
shift
if [[ $aflag == true ]]; then
echo a is true
fi
done
Here you can find a few different approaches for complex option parsing in bash:
http://mywiki.wooledge.org/ComplexOptionParsing
I did create the following one, and I think it's a good one, because it's minimal code
and both long and short options work. A long option can also have multiple arguments with this approach.
#!/bin/bash
# Uses bash extensions. Not portable as written.
declare -A longoptspec
longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default
optspec=":h-:"
while getopts "$optspec" opt; do
while true; do
case "${opt}" in
-) #OPTARG is name-of-long-option or name-of-long-option=value
if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible
then
opt=${OPTARG/=*/}
OPTARG=${OPTARG#*=}
((OPTIND--))
else #with this --key value1 value2 format multiple arguments are possible
opt="$OPTARG"
OPTARG=(${#:OPTIND:$((longoptspec[$opt]))})
fi
((OPTIND+=longoptspec[$opt]))
continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options
;;
loglevel)
loglevel=$OPTARG
;;
h|help)
echo "usage: $0 [--loglevel[=]<value>]" >&2
exit 2
;;
esac
break; done
done
# End of file
I have been working on that subject for quite a long time... and made my own library which you will need to source in your main script.
See libopt4shell and cd2mpc for an example.
Hope it helps !
An improved solution:
# translate long options to short
# Note: This enable long options but disable "--?*" in $OPTARG, or disable long options after "--" in option fields.
for ((i=1;$#;i++)) ; do
case "$1" in
--)
# [ ${args[$((i-1))]} == ... ] || EndOpt=1 ;;& # DIRTY: we still can handle some execptions...
EndOpt=1 ;;&
--version) ((EndOpt)) && args[$i]="$1" || args[$i]="-V";;
# default case : short option use the first char of the long option:
--?*) ((EndOpt)) && args[$i]="$1" || args[$i]="-${1:2:1}";;
# pass through anything else:
*) args[$i]="$1" ;;
esac
shift
done
# reset the translated args
set -- "${args[#]}"
function usage {
echo "Usage: $0 [options] files" >&2
exit $1
}
# now we can process with getopt
while getopts ":hvVc:" opt; do
case $opt in
h) usage ;;
v) VERBOSE=true ;;
V) echo $Version ; exit ;;
c) source $OPTARG ;;
\?) echo "unrecognized option: -$opt" ; usage -1 ;;
:)
echo "option -$OPTARG requires an argument"
usage -1
;;
esac
done
shift $((OPTIND-1))
[[ "$1" == "--" ]] && shift
Maybe it's simpler to use ksh, just for the getopts part, if need long command line options, as it can be easier done there.
# Working Getopts Long => KSH
#! /bin/ksh
# Getopts Long
USAGE="s(showconfig)"
USAGE+="c:(createdb)"
USAGE+="l:(createlistener)"
USAGE+="g:(generatescripts)"
USAGE+="r:(removedb)"
USAGE+="x:(removelistener)"
USAGE+="t:(createtemplate)"
USAGE+="h(help)"
while getopts "$USAGE" optchar ; do
case $optchar in
s) echo "Displaying Configuration" ;;
c) echo "Creating Database $OPTARG" ;;
l) echo "Creating Listener LISTENER_$OPTARG" ;;
g) echo "Generating Scripts for Database $OPTARG" ;;
r) echo "Removing Database $OPTARG" ;;
x) echo "Removing Listener LISTENER_$OPTARG" ;;
t) echo "Creating Database Template" ;;
h) echo "Help" ;;
esac
done
I wanted something without external dependencies, with strict bash support (-u), and I needed it to work on even the older bash versions. This handles various types of params:
short bools (-h)
short options (-i "image.jpg")
long bools (--help)
equals options (--file="filename.ext")
space options (--file "filename.ext")
concatinated bools (-hvm)
Just insert the following at the top of your script:
# Check if a list of params contains a specific param
# usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ...
# the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable)
_param_variant() {
for param in $1 ; do
local variants=${param//\|/ }
for variant in $variants ; do
if [[ "$variant" = "$2" ]] ; then
# Update the key to match the long version
local arr=(${param//\|/ })
let last=${#arr[#]}-1
key="${arr[$last]}"
return 0
fi
done
done
return 1
}
# Get input parameters in short or long notation, with no dependencies beyond bash
# usage:
# # First, set your defaults
# param_help=false
# param_path="."
# param_file=false
# param_image=false
# param_image_lossy=true
# # Define allowed parameters
# allowed_params="h|?|help p|path f|file i|image image-lossy"
# # Get parameters from the arguments provided
# _get_params $*
#
# Parameters will be converted into safe variable names like:
# param_help,
# param_path,
# param_file,
# param_image,
# param_image_lossy
#
# Parameters without a value like "-h" or "--help" will be treated as
# boolean, and will be set as param_help=true
#
# Parameters can accept values in the various typical ways:
# -i "path/goes/here"
# --image "path/goes/here"
# --image="path/goes/here"
# --image=path/goes/here
# These would all result in effectively the same thing:
# param_image="path/goes/here"
#
# Concatinated short parameters (boolean) are also supported
# -vhm is the same as -v -h -m
_get_params(){
local param_pair
local key
local value
local shift_count
while : ; do
# Ensure we have a valid param. Allows this to work even in -u mode.
if [[ $# == 0 || -z $1 ]] ; then
break
fi
# Split the argument if it contains "="
param_pair=(${1//=/ })
# Remove preceeding dashes
key="${param_pair[0]#--}"
# Check for concatinated boolean short parameters.
local nodash="${key#-}"
local breakout=false
if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then
# Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h"
local short_param_count=${#nodash}
let new_arg_count=$#+$short_param_count-1
local new_args=""
# $str_pos is the current position in the short param string $nodash
for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do
# The first character becomes the current key
if [ $str_pos -eq 0 ] ; then
key="${nodash:$str_pos:1}"
breakout=true
fi
# $arg_pos is the current position in the constructed arguments list
let arg_pos=$str_pos+1
if [ $arg_pos -gt $short_param_count ] ; then
# handle other arguments
let orignal_arg_number=$arg_pos-$short_param_count+1
local new_arg="${!orignal_arg_number}"
else
# break out our one argument into new ones
local new_arg="-${nodash:$str_pos:1}"
fi
new_args="$new_args \"$new_arg\""
done
# remove the preceding space and set the new arguments
eval set -- "${new_args# }"
fi
if ! $breakout ; then
key="$nodash"
fi
# By default we expect to shift one argument at a time
shift_count=1
if [ "${#param_pair[#]}" -gt "1" ] ; then
# This is a param with equals notation
value="${param_pair[1]}"
else
# This is either a boolean param and there is no value,
# or the value is the next command line argument
# Assume the value is a boolean true, unless the next argument is found to be a value.
value=true
if [[ $# -gt 1 && -n "$2" ]]; then
local nodash="${2#-}"
if [ "$nodash" = "$2" ]; then
# The next argument has NO preceding dash so it is a value
value="$2"
shift_count=2
fi
fi
fi
# Check that the param being passed is one of the allowed params
if _param_variant "$allowed_params" "$key" ; then
# --key-name will now become param_key_name
eval param_${key//-/_}="$value"
else
printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2
fi
shift $shift_count
done
}
And use it like so:
# Assign defaults for parameters
param_help=false
param_path=$(pwd)
param_file=false
param_image=true
param_image_lossy=true
param_image_lossy_quality=85
# Define the params we will allow
allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality"
# Get the params from arguments provided
_get_params $*
if simply this is how you want to call the script
myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ"
then you can follow this simplest way to achieve it with the help of getopt and --longoptions
try this , hope this is useful
# 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
--input1)
shift
empId=$1
;;
--input2)
shift
fromDate=$1
;;
--input3)
shift
toDate=$1
;;
--)
shift
break
;;
esac
shift
done
It took some time, but I wanted it all:
short options
long options
with or without arguments
non-option arguments (those without "-" or "--")
order shouldn't matter (script.sh /file -V or script.sh -V /file)
catch wrong usage
use it as a module in different scripts without the need to change multiple lines of code
Finally I came up with the following solution, which uses getopt to catch errors and move non-options to the end of the list and after that getopts, which parses the short and long options.
All options are automatically parsed with their long option name as variable name (look at the example):
# create string of short options
opt_short=$(printf "%s" "${!options[#]}")
# create string of long options
opt_long="$(printf ",%s" "${options[#]}")"
# catch wrong options and move non-options to the end of the string
args=$(getopt -l "$opt_long" "$opt_short" "$#" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1
# create new array of options
mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) '/\1='/g")" )
# overwrite $# (options)
set -- "${args[#]}"
# parse options ([h]=help sets the variable "$opt_help" and [V]="" sets the variable "$opt_V")
while getopts "$opt_short-:" opt; do
echo "$opt:$OPTARG"
# long option
if [[ "$opt" == "-" ]]; then
# extract long option name
opt="${OPTARG%%=*}"
# extract long option argument (may be empty)
OPTARG="${OPTARG#"$opt"}"
# remove "=" from long option argument
OPTARG="${OPTARG#=}"
# set variable name
opt=opt_$opt
# short option without argument uses long option name as variable name
elif [[ "${options[$opt]+x}" ]] && [[ "${options[$opt]}" ]]; then
opt=opt_${options[$opt]}
# short option with argument uses long option name as variable name
elif [[ "${options[$opt:]+x}" ]] && [[ "${options[$opt:]}" ]]; then
opt=opt_${options[$opt:]}
# short option without long option name uses short option name as variable name
else
opt=opt_$opt
fi
# remove double colon
opt="${opt%:}"
# options without arguments are set to 1
[[ ! $OPTARG ]] && OPTARG=1
# replace hyphen against underscore
opt="${opt//-/_}"
# set variable variables (replaces hyphen against underscore)
printf -v "$opt" '%s' "$OPTARG"
done
Now, I only need to define the needed option names and source the script:
# import options module
declare -A options=( [h]=help [f:]=file: [V]=verbose [0]=long_only: [s]="" )
source "/usr/local/bin/inc/options.sh";
# display help text
if [[ $opt_help ]]; then
echo "help text"
exit
fi
# output
echo "opt_help:$opt_help"
echo "opt_file:$opt_file"
echo "opt_verbose:$opt_verbose"
echo "opt_long_only:$opt_long_only"
echo "opt_short_only:$opt_s"
echo "opt_path:$1"
echo "opt_mail:$2"
And while calling the script, it's possible to pass all options and non-options in a complete random order:
# $opt_file $1 $2 $opt_V $opt_long_only $opt_s
# /demo.sh --file=file.txt /dir info#example.com -V --long_only=yes -s
opt_help:1
opt_file:file.txt
opt_verbose:1
opt_long_only:yes
opt_short_only:1
opt_path=/dir
opt_mail:info#example.com
Notes
In the options array add : after an option name to enable arguments.
If no long option name is given, the variable name will be $opt_X, where X is the short option name.
If you want to use a long option name without defining a short option name, then set the array index to a number as in the above example done with [0]=long_only. Of course every array index must be unique.
Used techniques
Capture stderr without using a temporary file
Convert string to array
Use : to parse arguments with getopt
Parse long option names with getopts
getopts "could be used" for parsing long options as long as you don't expect them to have arguments...
Here's how to:
$ cat > longopt
while getopts 'e:-:' OPT; do
case $OPT in
e) echo echo: $OPTARG;;
-) #long option
case $OPTARG in
long-option) echo long option;;
*) echo long option: $OPTARG;;
esac;;
esac
done
$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test
If you try to use OPTIND for getting a parameter for the long option, getopts will treat it as the first no optional positional parameter and will stop parsing any other parameters.
In such a case you'll be better off handling it manually with a simple case statement.
This will "always" work:
$ cat >longopt2
while (($#)); do
OPT=$1
shift
case $OPT in
--*) case ${OPT:2} in
long1) echo long1 option;;
complex) echo comples with argument $1; shift;;
esac;;
-*) case ${OPT:1} in
a) echo short option a;;
b) echo short option b with parameter $1; shift;;
esac;;
esac
done
$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test
Albeit is not as flexible as getopts and you have to do much of the error checking code yourself within the case instances...
But it is an option.
In order to stay cross-platform compatible, and avoid the reliance on external executables, I ported some code from another language.
I find it very easy to use, here is an example:
ArgParser::addArg "[h]elp" false "This list"
ArgParser::addArg "[q]uiet" false "Supress output"
ArgParser::addArg "[s]leep" 1 "Seconds to sleep"
ArgParser::addArg "v" 1 "Verbose mode"
ArgParser::parse "$#"
ArgParser::isset help && ArgParser::showArgs
ArgParser::isset "quiet" \
&& echo "Quiet!" \
|| echo "Noisy!"
local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
&& echo "Sleep for $__sleep seconds" \
|| echo "No value passed for sleep"
# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"
The required BASH is a little longer than it could be, but I wanted to avoid reliance on BASH 4's associative arrays. You can also download this directly from http://nt4.com/bash/argparser.inc.sh
#!/usr/bin/env bash
# Updates to this script may be found at
# http://nt4.com/bash/argparser.inc.sh
# Example of runtime usage:
# mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com
# Example of use in script (see bottom)
# Just include this file in yours, or use
# source argparser.inc.sh
unset EXPLODED
declare -a EXPLODED
function explode
{
local c=$#
(( c < 2 )) &&
{
echo function "$0" is missing parameters
return 1
}
local delimiter="$1"
local string="$2"
local limit=${3-99}
local tmp_delim=$'\x07'
local delin=${string//$delimiter/$tmp_delim}
local oldifs="$IFS"
IFS="$tmp_delim"
EXPLODED=($delin)
IFS="$oldifs"
}
# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
# Usage: local "$1" && upvar $1 "value(s)"
upvar() {
if unset -v "$1"; then # Unset & validate varname
if (( $# == 2 )); then
eval $1=\"\$2\" # Return single value
else
eval $1=\(\"\${#:2}\"\) # Return array
fi
fi
}
function decho
{
:
}
function ArgParser::check
{
__args=${#__argparser__arglist[#]}
for (( i=0; i<__args; i++ ))
do
matched=0
explode "|" "${__argparser__arglist[$i]}"
if [ "${#1}" -eq 1 ]
then
if [ "${1}" == "${EXPLODED[0]}" ]
then
decho "Matched $1 with ${EXPLODED[0]}"
matched=1
break
fi
else
if [ "${1}" == "${EXPLODED[1]}" ]
then
decho "Matched $1 with ${EXPLODED[1]}"
matched=1
break
fi
fi
done
(( matched == 0 )) && return 2
# decho "Key $key has default argument of ${EXPLODED[3]}"
if [ "${EXPLODED[3]}" == "false" ]
then
return 0
else
return 1
fi
}
function ArgParser::set
{
key=$3
value="${1:-true}"
declare -g __argpassed__$key="$value"
}
function ArgParser::parse
{
unset __argparser__argv
__argparser__argv=()
# echo parsing: "$#"
while [ -n "$1" ]
do
# echo "Processing $1"
if [ "${1:0:2}" == '--' ]
then
key=${1:2}
value=$2
elif [ "${1:0:1}" == '-' ]
then
key=${1:1} # Strip off leading -
value=$2
else
decho "Not argument or option: '$1'" >& 2
__argparser__argv+=( "$1" )
shift
continue
fi
# parameter=${tmp%%=*} # Extract name.
# value=${tmp##*=} # Extract value.
decho "Key: '$key', value: '$value'"
# eval $parameter=$value
ArgParser::check $key
el=$?
# echo "Check returned $el for $key"
[ $el -eq 2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" )
[ $el -eq 0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments" >&2 && ArgParser::set true "${EXPLODED[#]}"
[ $el -eq 1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'" >&2 && ArgParser::set "$2" "${EXPLODED[#]}" && shift
shift
done
}
function ArgParser::isset
{
declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0
return 1
}
function ArgParser::getArg
{
# This one would be a bit silly, since we can only return non-integer arguments ineffeciently
varname="__argpassed__$1"
echo "${!varname}"
}
##
# usage: tryAndGetArg <argname> into <varname>
# returns: 0 on success, 1 on failure
function ArgParser::tryAndGetArg
{
local __varname="__argpassed__$1"
local __value="${!__varname}"
test -z "$__value" && return 1
local "$3" && upvar $3 "$__value"
return 0
}
function ArgParser::__construct
{
unset __argparser__arglist
# declare -a __argparser__arglist
}
##
# #brief add command line argument
# #param 1 short and/or long, eg: [s]hort
# #param 2 default value
# #param 3 description
##
function ArgParser::addArg
{
# check for short arg within long arg
if [[ "$1" =~ \[(.)\] ]]
then
short=${BASH_REMATCH[1]}
long=${1/\[$short\]/$short}
else
long=$1
fi
if [ "${#long}" -eq 1 ]
then
short=$long
long=''
fi
decho short: "$short"
decho long: "$long"
__argparser__arglist+=("$short|$long|$1|$2|$3")
}
##
# #brief show available command line arguments
##
function ArgParser::showArgs
{
# declare -p | grep argparser
printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )"
printf "Defaults for the options are specified in brackets.\n\n";
__args=${#__argparser__arglist[#]}
for (( i=0; i<__args; i++ ))
do
local shortname=
local fullname=
local default=
local description=
local comma=
explode "|" "${__argparser__arglist[$i]}"
shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide:
fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html
test -n "$shortname" \
&& test -n "$fullname" \
&& comma=","
default="${EXPLODED[3]}"
case $default in
false )
default=
;;
"" )
default=
;;
* )
default="[$default]"
esac
description="${EXPLODED[4]}"
printf " %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default"
done
}
function ArgParser::test
{
# Arguments with a default of 'false' do not take paramaters (note: default
# values are not applied in this release)
ArgParser::addArg "[h]elp" false "This list"
ArgParser::addArg "[q]uiet" false "Supress output"
ArgParser::addArg "[s]leep" 1 "Seconds to sleep"
ArgParser::addArg "v" 1 "Verbose mode"
ArgParser::parse "$#"
ArgParser::isset help && ArgParser::showArgs
ArgParser::isset "quiet" \
&& echo "Quiet!" \
|| echo "Noisy!"
local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
&& echo "Sleep for $__sleep seconds" \
|| echo "No value passed for sleep"
# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"
echo "Remaining command line: ${__argparser__argv[#]}"
}
if [ "$( basename "$0" )" == "argparser.inc.sh" ]
then
ArgParser::test "$#"
fi
If all your long options have unique, and matching, first characters as the short options, so for example
./slamm --chaos 23 --plenty test -quiet
Is the same as
./slamm -c 23 -p test -q
You can use this before getopts to rewrite $args:
# change long options to short options
for arg; do
[[ "${arg:0:1}" == "-" ]] && delim="" || delim="\""
if [ "${arg:0:2}" == "--" ];
then args="${args} -${arg:2:1}"
else args="${args} ${delim}${arg}${delim}"
fi
done
# reset the incoming args
eval set -- $args
# proceed as usual
while getopts ":b:la:h" OPTION; do
.....
Thanks for mtvee for the inspiration ;-)
Builtin getopts only parse short options (except in ksh93),
but you can still add few lines of scripting to make getopts handles long options.
Here is a part of code found in http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts
#== set short options ==#
SCRIPT_OPTS=':fbF:B:-:h'
#== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
[foo]=f
[bar]=b
[foobar]=F
[barfoo]=B
[help]=h
[man]=h
)
#== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
#== translate long options to short ==#
if [[ "x$OPTION" == "x-" ]]; then
LONG_OPTION=$OPTARG
LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
LONG_OPTIND=-1
[[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
[[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
OPTION=${ARRAY_OPTS[$LONG_OPTION]}
[[ "x$OPTION" = "x" ]] && OPTION="?" OPTARG="-$LONG_OPTION"
if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then
OPTION=":" OPTARG="-$LONG_OPTION"
else
OPTARG="$LONG_OPTARG";
if [[ $LONG_OPTIND -ne -1 ]]; then
[[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
shift $OPTIND
OPTIND=1
fi
fi
fi
fi
#== options follow by another option instead of argument ==#
if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then
OPTARG="$OPTION" OPTION=":"
fi
#== manage options ==#
case "$OPTION" in
f ) foo=1 bar=0 ;;
b ) foo=0 bar=1 ;;
B ) barfoo=${OPTARG} ;;
F ) foobar=1 && foobar_name=${OPTARG} ;;
h ) usagefull && exit 0 ;;
: ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;;
? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;;
esac
done
shift $((${OPTIND} - 1))
Here is a test:
# Short options test
$ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello world
files=file1 file2
# Long and short options test
$ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello
files=file1 file2
Otherwise in recent Korn Shell ksh93, getopts can naturally parse long options and even display a man page alike. (See http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options)
Th built-in OS X (BSD) getopt does not support long options, but the GNU version does: brew install gnu-getopt. Then, something similar to: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt.
Related
How to use getopt for parsing long in bash
I have a use case in which I have to execute functionality of different methods in shell script. User will call my shell script as I have to read the value of options and on basis of it have to call methods. Please find the sample code below. ./testfile.sh --option1 value1 --option2 value2 #!/bin/bash # # Example of how to parse short/long options with 'getopt' # OPTS=`getopt --long option1,option2 -n 'parse-options' -- "$#"` if [ $? != 0 ] ; then echo "Failed parsing options." >&2 ; exit 1 ; fi echo "$OPTS" eval set -- "$OPTS" opt="" id="" while true; do echo "Inside while $1 $2" case "$1" in --option1) echo $1; shift ;; --option2 ) echo $2; shift ;; -- ) shift; break ;; * ) break ;; esac done echo "Value for opt $opt" echo "Value for id $id"
We can achieve parsing of long option calling using getopt. My USE CASE :- When we only have long option provided then how can we use getopt, because getopt can parse both the short i.e something like '-o, -k. -p etc' and also the long option like "--opt, --pass, etc". When we use getopt, below is the default syntax for it 'getopt -o 'o,p,k' --long opt,pass -n 'parse-options' -- "$#"' Above line will parse both the long and short option. Solution to my use case is we have pass single quotes for short option, like below, below line will parse only long option i.e opt and pass. 'getopt -o '' --long opt,pass -n 'parse-options' -- "$#"'
Bash: handling mass arguments
I'd like to be able to handle multiple arguments to a given flag no matter what the order of flags is. Do you guys think this is acceptable? Any improvements? So: $ ./script -c opt1 opt2 opt3 -b foo opt1 opt2 opt3 foo Code: echo_args () { echo "$#" } while (( $# > 0 )); do case "$1" in -b) echo $2 ;; -c|--create) c_args=() # start looping from this flag for arg in ${#:2}; do [ "${arg:0:1}" == "-" ] && break c_args+=("$arg") done echo_args "${c_args[#]}" ;; *) echo "huh?" ;; esac shift 1 done
The getopts utility shall retrieve options and option-arguments from a list of parameters. $ cat script.sh cflag= bflag= while getopts c:b: name do case $name in b) bflag=1 bval="$OPTARG";; c) cflag=1 cval="$OPTARG";; ?) printf "Usage: %s: [-c value] [-b value] args\n" $0 exit 2;; esac done if [ ! -z "$bflag" ]; then printf 'Option -b "%s" specified\n' "$bval" fi if [ ! -z "$cflag" ]; then printf 'Option -c "%s" specified\n' "$cval" fi shift $(($OPTIND - 1)) printf "Remaining arguments are: %s\n" "$*" Note the Guideline 8: When multiple option-arguments are specified to follow a single option, they should be presented as a single argument, using commas within that argument or <blank>s within that argument to separate them. $ ./script.sh -c "opt1 opt2 opt3" -b foo Option -b "foo" specified Option -c "opt1 opt2 opt3" specified Remaining arguments are: The standard links are listed below: getopts - parse utility options Section 12.2 Utility Syntax Guidelines
I noticed in the comments that you don't want to use any of these. What you could do is set all of the arguments as a string, then sort them using a loop, pulling out the ones you want to set as switched and sorting them using if statements. It is a little brutish, but it can be done. #!/bin/bash #set all of the arguments as a variable ARGUMENTS=$# # Look at each argument and determine what to do with it. for i in $ARGUMENTS; do # If the previous loop was -b then grab the value of this argument if [[ "$bgrab" == "1" ]]; then #adds the value of -b to the b string bval="$bval $i" bgrab="0" else # If this argument is -b, prepare to grab the next argument and assign it if [[ "$i" == "-b" ]]; then bgrab="1" else #Collect the remaining arguments into one list per your example RemainingArgs="$RemainingArgs $i" fi fi done echo "Arguments: $RemainingArgs" echo "B Value: $bval" I use something similar in a lot of my scripts because there are a significant amount of arguments that can be fed into some of them, and the script needs to look at each one to figure out what to do. They can be out of order or not exist at all and the code still has to work.
osx Terminal / Shell Script function modifier [duplicate]
I wish to have long and short forms of command line options invoked using my shell script. I know that getopts can be used, but like in Perl, I have not been able to do the same with shell. Any ideas on how this can be done, so that I can use options like: ./shell.sh --copyfile abc.pl /tmp/ ./shell.sh -c abc.pl /tmp/ In the above, both the commands mean the same thing to my shell, but using getopts, I have not been able to implement these?
getopt and getopts are different beasts, and people seem to have a bit of misunderstanding of what they do. getopts is a built-in command to bash to process command-line options in a loop and assign each found option and value in turn to built-in variables, so you can further process them. getopt, however, is an external utility program, and it doesn't actually process your options for you the way that e.g. bash getopts, the Perl Getopt module or the Python optparse/argparse modules do. All that getopt does is canonicalize the options that are passed in — i.e. convert them to a more standard form, so that it's easier for a shell script to process them. For example, an application of getopt might convert the following: myscript -ab infile.txt -ooutfile.txt into this: myscript -a -b -o outfile.txt infile.txt You have to do the actual processing yourself. You don't have to use getopt at all if you make various restrictions on the way you can specify options: only put one option per argument; all options go before any positional parameters (i.e. non-option arguments); for options with values (e.g. -o above), the value has to go as a separate argument (after a space). Why use getopt instead of getopts? The basic reason is that only GNU getopt gives you support for long-named command-line options.1 (GNU getopt is the default on Linux. Mac OS X and FreeBSD come with a basic and not-very-useful getopt, but the GNU version can be installed; see below.) For example, here's an example of using GNU getopt, from a script of mine called javawrap: # NOTE: This requires GNU getopt. On Mac OS X and FreeBSD, you have to install this # separately; see below. TEMP=$(getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \ -n 'javawrap' -- "$#") if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi # Note the quotes around '$TEMP': they are essential! eval set -- "$TEMP" VERBOSE=false DEBUG=false MEMORY= DEBUGFILE= JAVA_MISC_OPT= while true; do case "$1" in -v | --verbose ) VERBOSE=true; shift ;; -d | --debug ) DEBUG=true; shift ;; -m | --memory ) MEMORY="$2"; shift 2 ;; --debugfile ) DEBUGFILE="$2"; shift 2 ;; --minheap ) JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;; --maxheap ) JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;; -- ) shift; break ;; * ) break ;; esac done This lets you specify options like --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt" or similar. The effect of the call to getopt is to canonicalize the options to --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt" so that you can more easily process them. The quoting around "$1" and "$2" is important as it ensures that arguments with spaces in them get handled properly. If you delete the first 9 lines (everything up through the eval set line), the code will still work! However, your code will be much pickier in what sorts of options it accepts: In particular, you'll have to specify all options in the "canonical" form described above. With the use of getopt, however, you can group single-letter options, use shorter non-ambiguous forms of long-options, use either the --file foo.txt or --file=foo.txt style, use either the -m 4096 or -m4096 style, mix options and non-options in any order, etc. getopt also outputs an error message if unrecognized or ambiguous options are found. NOTE: There are actually two totally different versions of getopt, basic getopt and GNU getopt, with different features and different calling conventions.2 Basic getopt is quite broken: Not only does it not handle long options, it also can't even handle embedded spaces inside of arguments or empty arguments, whereas getopts does do this right. The above code will not work in basic getopt. GNU getopt is installed by default on Linux, but on Mac OS X and FreeBSD it needs to be installed separately. On Mac OS X, install MacPorts (http://www.macports.org) and then do sudo port install getopt to install GNU getopt (usually into /opt/local/bin), and make sure that /opt/local/bin is in your shell path ahead of /usr/bin. On FreeBSD, install misc/getopt. A quick guide to modifying the example code for your own program: Of the first few lines, all is "boilerplate" that should stay the same, except the line that calls getopt. You should change the program name after -n, specify short options after -o, and long options after --long. Put a colon after options that take a value. Finally, if you see code that has just set instead of eval set, it was written for BSD getopt. You should change it to use the eval set style, which works fine with both versions of getopt, while the plain set doesn't work right with GNU getopt. 1Actually, getopts in ksh93 supports long-named options, but this shell isn't used as often as bash. In zsh, use zparseopts to get this functionality. 2Technically, "GNU getopt" is a misnomer; this version was actually written for Linux rather than the GNU project. However, it follows all the GNU conventions, and the term "GNU getopt" is commonly used (e.g. on FreeBSD).
There are three implementations that may be considered: Bash builtin getopts. This does not support long option names with the double-dash prefix. It only supports single-character options. BSD UNIX implementation of standalone getopt command (which is what MacOS uses). This does not support long options either. GNU implementation of standalone getopt. GNU getopt(3) (used by the command-line getopt(1) on Linux) supports parsing long options. Some other answers show a solution for using the bash builtin getopts to mimic long options. That solution actually makes a short option whose character is "-". So you get "--" as the flag. Then anything following that becomes OPTARG, and you test the OPTARG with a nested case. This is clever, but it comes with caveats: getopts can't enforce the opt spec. It can't return errors if the user supplies an invalid option. You have to do your own error-checking as you parse OPTARG. OPTARG is used for the long option name, which complicates usage when your long option itself has an argument. You end up having to code that yourself as an additional case. So while it is possible to write more code to work around the lack of support for long options, this is a lot more work and partially defeats the purpose of using a getopt parser to simplify your code.
The Bash builtin getopts function can be used to parse long options by putting a dash character followed by a colon into the optspec: #!/usr/bin/env bash optspec=":hv-:" while getopts "$optspec" optchar; do case "${optchar}" in -) case "${OPTARG}" in loglevel) val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2; ;; loglevel=*) val=${OPTARG#*=} opt=${OPTARG%=$val} echo "Parsing option: '--${opt}', value: '${val}'" >&2 ;; *) if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then echo "Unknown option --${OPTARG}" >&2 fi ;; esac;; h) echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2 exit 2 ;; v) echo "Parsing option: '-${optchar}'" >&2 ;; *) if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then echo "Non-option argument: '-${OPTARG}'" >&2 fi ;; esac done After copying to executable file name=getopts_test.sh in the current working directory, one can produce output like $ ./getopts_test.sh $ ./getopts_test.sh -f Non-option argument: '-f' $ ./getopts_test.sh -h usage: code/getopts_test.sh [-v] [--loglevel[=]<value>] $ ./getopts_test.sh --help $ ./getopts_test.sh -v Parsing option: '-v' $ ./getopts_test.sh --very-bad $ ./getopts_test.sh --loglevel Parsing option: '--loglevel', value: '' $ ./getopts_test.sh --loglevel 11 Parsing option: '--loglevel', value: '11' $ ./getopts_test.sh --loglevel=11 Parsing option: '--loglevel', value: '11' Obviously getopts neither performs OPTERR checking nor option-argument parsing for the long options. The script fragment above shows how this may be done manually. The basic principle also works in the Debian Almquist shell ("dash"). Note the special case: getopts -- "-:" ## without the option terminator "-- " bash complains about "-:" getopts "-:" ## this works in the Debian Almquist shell ("dash") Note that, as GreyCat from over at http://mywiki.wooledge.org/BashFAQ points out, this trick exploits a non-standard behaviour of the shell which permits the option-argument (i.e. the filename in "-f filename") to be concatenated to the option (as in "-ffilename"). The POSIX standard says there must be a space between them, which in the case of "-- longoption" would terminate the option-parsing and turn all longoptions into non-option arguments.
The built-in getopts command is still, AFAIK, limited to single-character options only. There is (or used to be) an external program getopt that would reorganize a set of options such that it was easier to parse. You could adapt that design to handle long options too. Example usage: aflag=no bflag=no flist="" set -- $(getopt abf: "$#") while [ $# -gt 0 ] do case "$1" in (-a) aflag=yes;; (-b) bflag=yes;; (-f) flist="$flist $2"; shift;; (--) shift; break;; (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;; (*) break;; esac shift done # Process remaining non-option arguments ... You could use a similar scheme with a getoptlong command. Note that the fundamental weakness with the external getopt program is the difficulty of handling arguments with spaces in them, and in preserving those spaces accurately. This is why the built-in getopts is superior, albeit limited by the fact it only handles single-letter options.
Long options can be parsed by the standard getopts builtin as “arguments” to the - “option” This is portable and native POSIX shell – no external programs or bashisms are needed. This guide implements long options as arguments to the - option, so --alpha is seen by getopts as - with argument alpha and --bravo=foo is seen as - with argument bravo=foo. The true argument is harvested via shell parameter expansion, updating $OPT and $OPTARG. In this example, -b and -c (and their long forms, --bravo and --charlie) have mandatory arguments. Arguments to long options come after equals signs, e.g. --bravo=foo (space delimiters for long options would be hard to implement, see below). Because this uses the getopts builtin, this solution supports usage like cmd --bravo=foo -ac FILE (which has combined options -a and -c and interleaves long options with standard options) while most other answers here either struggle or fail to do that. 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 a | alpha ) alpha=true ;; b | bravo ) needs_arg; bravo="$OPTARG" ;; c | charlie ) needs_arg; charlie="$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 When the option is a dash (-), it is a long option. getopts will have parsed the actual long option into $OPTARG, e.g. --bravo=foo originally sets OPT='-' and OPTARG='bravo=foo'. The if stanza sets $OPT to the contents of $OPTARG before the first equals sign (bravo in our example) and then removes that from the beginning of $OPTARG (yielding =foo in this step, or an empty string if there is no =). Finally, we strip the argument's leading =. At this point, $OPT is either a short option (one character) or a long option (2+ characters). The case then matches either short or long options (the pipe, |, indicates that "or" operation. A long-only option like delta ) delta=true ;; doesn't need a pipe). For short options, getopts automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg function, which fatally exits when $OPTARG is empty. The ??* condition will match any remaining long option (? matches a single character and * matches zero or more, so ??* matches 2+ characters), allowing us to issue the "Illegal option" error before exiting. As is normal for GNU-style long options, providing -- will stop parsing, so -a -- --bravo=4 will set $alpha to true but $bravo will remain untouched and $1 will be --bravo=4. I can't say I recommend naming files with leading dashes, but this is how you denote they aren't options. Minor bug: If somebody gives an invalid single-character long option (and it's not also a short option), this will exit with an error but without a message (this implementation assumes it's a short option). You could track that with an extra variable in the conditional preceding the case and then test for it in the final case condition, but I consider that too much of a corner case to bother. Uppercase variable names: Generally, the advice is to reserve all-uppercase variables for system use. I'm keeping $OPT as all-uppercase to keep it in line with $OPTARG, but this does break that convention. I think it fits because this is something the system should have done, and it should be safe; I haven't heard of any standards that use this variable name. To complain about unexpected arguments to long options: Mimic needs_arg with a flipped test to complain about an argument when one isn't expected: no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; } To accept long options with space-delimited arguments: You can pull in the next argument with eval "ARG_B=\"\$$OPTIND\"" (or with bash's indirect expansion, ARG_B="${!OPTIND}") and then increment $OPTIND as noted in an older version of this answer, but it's unreliable; getopts can prematurely terminate on the assumption that the argument was beyond its scope and some implementations don't take well to manual manipulations of $OPTIND.
Here's an example that actually uses getopt with long options: aflag=no bflag=no cargument=none # options may be followed by one colon to indicate they have a required argument if ! options=$(getopt -o abc: -l along,blong,clong: -- "$#") then # something went wrong, getopt will put out an error message for us exit 1 fi set -- $options while [ $# -gt 0 ] do case $1 in -a|--along) aflag="yes" ;; -b|--blong) bflag="yes" ;; # for options with required arguments, an additional shift is required -c|--clong) cargument="$2" ; shift;; (--) shift; break;; (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;; (*) break;; esac shift done
Using getopts with short/long options and arguments Works with all combinations, e.g: foobar -f --bar foobar --foo -b foobar -bf --bar --foobar foobar -fbFBAshorty --bar -FB --arguments=longhorn foobar -fA "text shorty" -B --arguments="text longhorn" bash foobar -F --barfoo sh foobar -B --foobar - ... bash ./foobar -F --bar Some declarations for this example Options=$# Optnum=$# sfoo='no ' sbar='no ' sfoobar='no ' sbarfoo='no ' sarguments='no ' sARG=empty lfoo='no ' lbar='no ' lfoobar='no ' lbarfoo='no ' larguments='no ' lARG=empty How the Usage function would look function _usage() { ###### U S A G E : Help and ERROR ###### cat <<EOF foobar $Options $* Usage: foobar <[options]> Options: -b --bar Set bar to yes ($foo) -f --foo Set foo to yes ($bart) -h --help Show this message -A --arguments=... Set arguments to yes ($arguments) AND get ARGUMENT ($ARG) -B --barfoo Set barfoo to yes ($barfoo) -F --foobar Set foobar to yes ($foobar) EOF } [ $# = 0 ] && _usage " >>>>>>>> no options given " getops with long/short flags as well as long arguments while getopts ':bfh-A:BF' OPTION ; do case "$OPTION" in b ) sbar=yes ;; f ) sfoo=yes ;; h ) _usage ;; A ) sarguments=yes;sARG="$OPTARG" ;; B ) sbarfoo=yes ;; F ) sfoobar=yes ;; - ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND eval OPTION="\$$optind" OPTARG=$(echo $OPTION | cut -d'=' -f2) OPTION=$(echo $OPTION | cut -d'=' -f1) case $OPTION in --foo ) lfoo=yes ;; --bar ) lbar=yes ;; --foobar ) lfoobar=yes ;; --barfoo ) lbarfoo=yes ;; --help ) _usage ;; --arguments ) larguments=yes;lARG="$OPTARG" ;; * ) _usage " Long: >>>>>>>> invalid options (long) " ;; esac OPTIND=1 shift ;; ? ) _usage "Short: >>>>>>>> invalid options (short) " ;; esac done Output ################################################################## echo "----------------------------------------------------------" echo "RESULT short-foo : $sfoo long-foo : $lfoo" echo "RESULT short-bar : $sbar long-bar : $lbar" echo "RESULT short-foobar : $sfoobar long-foobar : $lfoobar" echo "RESULT short-barfoo : $sbarfoo long-barfoo : $lbarfoo" echo "RESULT short-arguments: $sarguments with Argument = \"$sARG\" long-arguments: $larguments and $lARG" Combining the above into a cohesive script #!/bin/bash # foobar: getopts with short and long options AND arguments function _cleanup () { unset -f _usage _cleanup ; return 0 } ## Clear out nested functions on exit trap _cleanup INT EXIT RETURN ###### some declarations for this example ###### Options=$# Optnum=$# sfoo='no ' sbar='no ' sfoobar='no ' sbarfoo='no ' sarguments='no ' sARG=empty lfoo='no ' lbar='no ' lfoobar='no ' lbarfoo='no ' larguments='no ' lARG=empty function _usage() { ###### U S A G E : Help and ERROR ###### cat <<EOF foobar $Options $* Usage: foobar <[options]> Options: -b --bar Set bar to yes ($foo) -f --foo Set foo to yes ($bart) -h --help Show this message -A --arguments=... Set arguments to yes ($arguments) AND get ARGUMENT ($ARG) -B --barfoo Set barfoo to yes ($barfoo) -F --foobar Set foobar to yes ($foobar) EOF } [ $# = 0 ] && _usage " >>>>>>>> no options given " ################################################################## ####### "getopts" with: short options AND long options ####### ####### AND short/long arguments ####### while getopts ':bfh-A:BF' OPTION ; do case "$OPTION" in b ) sbar=yes ;; f ) sfoo=yes ;; h ) _usage ;; A ) sarguments=yes;sARG="$OPTARG" ;; B ) sbarfoo=yes ;; F ) sfoobar=yes ;; - ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND eval OPTION="\$$optind" OPTARG=$(echo $OPTION | cut -d'=' -f2) OPTION=$(echo $OPTION | cut -d'=' -f1) case $OPTION in --foo ) lfoo=yes ;; --bar ) lbar=yes ;; --foobar ) lfoobar=yes ;; --barfoo ) lbarfoo=yes ;; --help ) _usage ;; --arguments ) larguments=yes;lARG="$OPTARG" ;; * ) _usage " Long: >>>>>>>> invalid options (long) " ;; esac OPTIND=1 shift ;; ? ) _usage "Short: >>>>>>>> invalid options (short) " ;; esac done
Take a look at shFlags which is a portable shell library (meaning: sh, bash, dash, ksh, zsh on Linux, Solaris, etc.). It makes adding new flags as simple as adding one line to your script, and it provides an auto generated usage function. Here is a simple Hello, world! using shFlag: #!/bin/sh # source shflags from current directory . ./shflags # define a 'name' command-line string flag DEFINE_string 'name' 'world' 'name to say hello to' 'n' # parse the command-line FLAGS "$#" || exit 1 eval set -- "${FLAGS_ARGV}" # say hello echo "Hello, ${FLAGS_name}!" For OSes that have the enhanced getopt that supports long options (e.g. Linux), you can do: $ ./hello_world.sh --name Kate Hello, Kate! For the rest, you must use the short option: $ ./hello_world.sh -n Kate Hello, Kate! Adding a new flag is as simple as adding a new DEFINE_ call.
Another way... # translate long options to short for arg do delim="" case "$arg" in --help) args="${args}-h ";; --verbose) args="${args}-v ";; --config) args="${args}-c ";; # pass through anything else *) [[ "${arg:0:1}" == "-" ]] || delim="\"" args="${args}${delim}${arg}${delim} ";; esac done # reset the translated args eval set -- $args # now we can process with getopt while getopts ":hvc:" opt; do case $opt in h) usage ;; v) VERBOSE=true ;; c) source $OPTARG ;; \?) usage ;; :) echo "option -$OPTARG requires an argument" usage ;; esac done
I kind of solved this way: # A string with command options options=$# # An array with all the arguments arguments=($options) # Loop index index=0 for argument in $options do # Incrementing index index=`expr $index + 1` # The conditions case $argument in -a) echo "key $argument value ${arguments[index]}" ;; -abc) echo "key $argument value ${arguments[index]}" ;; esac done exit; Am I being dumb or something? getopt and getopts are so confusing.
In case you don't want the getopt dependency, you can do this: while test $# -gt 0 do case $1 in # Normal option processing -h | --help) # usage and help ;; -v | --version) # version info ;; # ... # Special cases --) break ;; --*) # error unknown (long) option $1 ;; -?) # error unknown (short) option $1 ;; # FUN STUFF HERE: # Split apart combined short options -*) split=$1 shift set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$#" continue ;; # Done with options *) break ;; esac # for testing purposes: echo "$1" shift done Of course, then you can't use long style options with one dash. And if you want to add shortened versions (e.g. --verbos instead of --verbose), then you need to add those manually. But if you are looking to get getopts functionality along with long options, this is a simple way to do it. I also put this snippet in a gist.
The built-in getopts can't do this. There is an external getopt(1) program that can do this, but you only get it on Linux from the util-linux package. It comes with an example script getopt-parse.bash. There is also a getopts_long written as a shell function.
#!/bin/bash while getopts "abc:d:" flag do case $flag in a) echo "[getopts:$OPTIND]==> -$flag";; b) echo "[getopts:$OPTIND]==> -$flag";; c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";; d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";; esac done shift $((OPTIND-1)) echo "[otheropts]==> $#" exit . #!/bin/bash until [ -z "$1" ]; do case $1 in "--dlong") shift if [ "${1:1:0}" != "-" ] then echo "==> dlong $1" shift fi;; *) echo "==> other $1"; shift;; esac done exit
The accepted answer does a very nice job of pointing out all the shortcomings of bash built-in getopts. The answer ends with: So while it is possible to write more code to work around the lack of support for long options, this is a lot more work and partially defeats the purpose of using a getopt parser to simplify your code. And even though I agree in principle with that statement, I feel that the number of times we all implemented this feature in various scripts justifies putting a bit of effort into creating a "standardised", well tested solution. As such, I've "upgraded" bash built in getopts by implementing getopts_long in pure bash, with no external dependencies. The usage of the function is 100% compatible with the built-in getopts. By including getopts_long (which is hosted on GitHub) in a script, the answer to the original question can be implemented as simply as: source "${PATH_TO}/getopts_long.bash" while getopts_long ':c: copyfile:' OPTKEY; do case ${OPTKEY} in 'c'|'copyfile') echo 'file supplied -- ${OPTARG}' ;; '?') echo "INVALID OPTION -- ${OPTARG}" >&2 exit 1 ;; ':') echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2 exit 1 ;; *) echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2 exit 1 ;; esac done shift $(( OPTIND - 1 )) [[ "${1}" == "--" ]] && shift
In ksh93, getopts does support long names... while getopts "f(file):s(server):" flag do echo "$flag" $OPTIND $OPTARG done Or so the tutorials I have found have said. Try it and see.
Inventing yet another version of the wheel... This function is a (hopefully) POSIX-compatible plain bourne shell replacement for GNU getopt. It supports short/long options which can accept mandatory/optional/no arguments, and the way in which options are specified is almost identical to GNU getopt, so conversion is trivial. Of course this is still a sizeable chunk of code to drop into a script, but it's about half the lines of the well-known getopt_long shell function, and might be preferable in cases where you just want to replace existing GNU getopt uses. This is pretty new code, so YMMV (and definitely please let me know if this isn't actually POSIX-compatible for any reason -- portability was the intention from the outset, but I don't have a useful POSIX test environment). Code and example usage follows: #!/bin/sh # posix_getopt shell function # Author: Phil S. # Version: 1.0 # Created: 2016-07-05 # URL: http://stackoverflow.com/a/37087374/324105 # POSIX-compatible argument quoting and parameter save/restore # http://www.etalabs.net/sh_tricks.html # Usage: # parameters=$(save "$#") # save the original parameters. # eval "set -- ${parameters}" # restore the saved parameters. save () { local param for param; do printf %s\\n "$param" \ | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" done printf %s\\n " " } # Exit with status $1 after displaying error message $2. exiterr () { printf %s\\n "$2" >&2 exit $1 } # POSIX-compatible command line option parsing. # This function supports long options and optional arguments, and is # a (largely-compatible) drop-in replacement for GNU getopt. # # Instead of: # opts=$(getopt -o "$shortopts" -l "$longopts" -- "$#") # eval set -- ${opts} # # We instead use: # opts=$(posix_getopt "$shortopts" "$longopts" "$#") # eval "set -- ${opts}" posix_getopt () { # args: "$shortopts" "$longopts" "$#" local shortopts longopts \ arg argtype getopt nonopt opt optchar optword suffix shortopts="$1" longopts="$2" shift 2 getopt= nonopt= while [ $# -gt 0 ]; do opt= arg= argtype= case "$1" in # '--' means don't parse the remaining options ( -- ) { getopt="${getopt}$(save "$#")" shift $# break };; # process short option ( -[!-]* ) { # -x[foo] suffix=${1#-?} # foo opt=${1%$suffix} # -x optchar=${opt#-} # x case "${shortopts}" in ( *${optchar}::* ) { # optional argument argtype=optional arg="${suffix}" shift };; ( *${optchar}:* ) { # required argument argtype=required if [ -n "${suffix}" ]; then arg="${suffix}" shift else case "$2" in ( -* ) exiterr 1 "$1 requires an argument";; ( ?* ) arg="$2"; shift 2;; ( * ) exiterr 1 "$1 requires an argument";; esac fi };; ( *${optchar}* ) { # no argument argtype=none arg= shift # Handle multiple no-argument parameters combined as # -xyz instead of -x -y -z. If we have just shifted # parameter -xyz, we now replace it with -yz (which # will be processed in the next iteration). if [ -n "${suffix}" ]; then eval "set -- $(save "-${suffix}")$(save "$#")" fi };; ( * ) exiterr 1 "Unknown option $1";; esac };; # process long option ( --?* ) { # --xarg[=foo] suffix=${1#*=} # foo (unless there was no =) if [ "${suffix}" = "$1" ]; then suffix= fi opt=${1%=$suffix} # --xarg optword=${opt#--} # xarg case ",${longopts}," in ( *,${optword}::,* ) { # optional argument argtype=optional arg="${suffix}" shift };; ( *,${optword}:,* ) { # required argument argtype=required if [ -n "${suffix}" ]; then arg="${suffix}" shift else case "$2" in ( -* ) exiterr 1 \ "--${optword} requires an argument";; ( ?* ) arg="$2"; shift 2;; ( * ) exiterr 1 \ "--${optword} requires an argument";; esac fi };; ( *,${optword},* ) { # no argument if [ -n "${suffix}" ]; then exiterr 1 "--${optword} does not take an argument" fi argtype=none arg= shift };; ( * ) exiterr 1 "Unknown option $1";; esac };; # any other parameters starting with - ( -* ) exiterr 1 "Unknown option $1";; # remember non-option parameters ( * ) nonopt="${nonopt}$(save "$1")"; shift;; esac if [ -n "${opt}" ]; then getopt="${getopt}$(save "$opt")" case "${argtype}" in ( optional|required ) { getopt="${getopt}$(save "$arg")" };; esac fi done # Generate function output, suitable for: # eval "set -- $(posix_getopt ...)" printf %s "${getopt}" if [ -n "${nonopt}" ]; then printf %s "$(save "--")${nonopt}" fi } Example usage: # Process command line options shortopts="hvd:c::s::L:D" longopts="help,version,directory:,client::,server::,load:,delete" #opts=$(getopt -o "$shortopts" -l "$longopts" -n "$(basename $0)" -- "$#") opts=$(posix_getopt "$shortopts" "$longopts" "$#") if [ $? -eq 0 ]; then #eval set -- ${opts} eval "set -- ${opts}" while [ $# -gt 0 ]; do case "$1" in ( -- ) shift; break;; ( -h|--help ) help=1; shift; break;; ( -v|--version ) version_help=1; shift; break;; ( -d|--directory ) dir=$2; shift 2;; ( -c|--client ) useclient=1; client=$2; shift 2;; ( -s|--server ) startserver=1; server_name=$2; shift 2;; ( -L|--load ) load=$2; shift 2;; ( -D|--delete ) delete=1; shift;; esac done else shorthelp=1 # getopt returned (and reported) an error. fi
I only write shell scripts now and then and fall out of practice, so any feedback is appreciated. Using the strategy proposed by #Arvid Requate, we noticed some user errors. A user who forgets to include a value will accidentally have the next option's name treated as a value: ./getopts_test.sh --loglevel= --toc=TRUE will cause the value of "loglevel" to be seen as "--toc=TRUE". This can be avoided. I adapted some ideas about checking user error for CLI from http://mwiki.wooledge.org/BashFAQ/035 discussion of manual parsing. I inserted error checking into handling both "-" and "--" arguments. Then I started fiddling around with the syntax, so any errors in here are strictly my fault, not the original authors. My approach helps users who prefer to enter long with or without the equal sign. That is, it should have same response to "--loglevel 9" as "--loglevel=9". In the --/space method, it is not possible to know for sure if the user forgets an argument, so some guessing is needed. if the user has the long/equal sign format (--opt=), then a space after = triggers an error because an argument was not supplied. if user has long/space arguments (--opt ), this script causes a fail if no argument follows (end of command) or if argument begins with dash) In case you are starting out on this, there is an interesting difference between "--opt=value" and "--opt value" formats. With the equal sign, the command line argument is seen as "opt=value" and the work to handle that is string parsing, to separate at the "=". In contrast, with "--opt value", the name of the argument is "opt" and we have the challenge of getting the next value supplied in the command line. That's where #Arvid Requate used ${!OPTIND}, the indirect reference. I still don't understand that, well, at all, and comments in BashFAQ seem to warn against that style (http://mywiki.wooledge.org/BashFAQ/006). BTW, I don't think previous poster's comments about importance of OPTIND=$(( $OPTIND + 1 )) are correct. I mean to say, I see no harm from omitting it. In newest version of this script, flag -v means VERBOSE printout. Save it in a file called "cli-5.sh", make executable, and any of these will work, or fail in the desired way ./cli-5.sh -v --loglevel=44 --toc TRUE ./cli-5.sh -v --loglevel=44 --toc=TRUE ./cli-5.sh --loglevel 7 ./cli-5.sh --loglevel=8 ./cli-5.sh -l9 ./cli-5.sh --toc FALSE --loglevel=77 ./cli-5.sh --toc=FALSE --loglevel=77 ./cli-5.sh -l99 -t yyy ./cli-5.sh -l 99 -t yyy Here is example output of the error-checking on user intpu $ ./cli-5.sh --toc --loglevel=77 ERROR: toc value must not have dash at beginning $ ./cli-5.sh --toc= --loglevel=77 ERROR: value for toc undefined You should consider turning on -v, because it prints out internals of OPTIND and OPTARG #/usr/bin/env bash ## Paul Johnson ## 20171016 ## ## Combines ideas from ## https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options ## by #Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035 # What I don't understand yet: # In #Arvid REquate's answer, we have # val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) # this works, but I don't understand it! die() { printf '%s\n' "$1" >&2 exit 1 } printparse(){ if [ ${VERBOSE} -gt 0 ]; then printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2; fi } showme(){ if [ ${VERBOSE} -gt 0 ]; then printf 'VERBOSE: %s\n' "$1" >&2; fi } VERBOSE=0 loglevel=0 toc="TRUE" optspec=":vhl:t:-:" while getopts "$optspec" OPTCHAR; do showme "OPTARG: ${OPTARG[*]}" showme "OPTIND: ${OPTIND[*]}" case "${OPTCHAR}" in -) case "${OPTARG}" in loglevel) #argument has no equal sign opt=${OPTARG} val="${!OPTIND}" ## check value. If negative, assume user forgot value showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\"" if [[ "$val" == -* ]]; then die "ERROR: $opt value must not have dash at beginning" fi ## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect? printparse "--${OPTARG}" " " "${val}" loglevel="${val}" shift ;; loglevel=*) #argument has equal sign opt=${OPTARG%=*} val=${OPTARG#*=} if [ "${OPTARG#*=}" ]; then printparse "--${opt}" "=" "${val}" loglevel="${val}" ## shift CAUTION don't shift this, fails othewise else die "ERROR: $opt value must be supplied" fi ;; toc) #argument has no equal sign opt=${OPTARG} val="${!OPTIND}" ## check value. If negative, assume user forgot value showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\"" if [[ "$val" == -* ]]; then die "ERROR: $opt value must not have dash at beginning" fi ## OPTIND=$(( $OPTIND + 1 )) #?? printparse "--${opt}" " " "${val}" toc="${val}" shift ;; toc=*) #argument has equal sign opt=${OPTARG%=*} val=${OPTARG#*=} if [ "${OPTARG#*=}" ]; then toc=${val} printparse "--$opt" " -> " "$toc" ##shift ## NO! dont shift this else die "ERROR: value for $opt undefined" fi ;; help) echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2 exit 2 ;; *) if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then echo "Unknown option --${OPTARG}" >&2 fi ;; esac;; h|-\?|--help) ## must rewrite this for all of the arguments echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2 exit 2 ;; l) loglevel=${OPTARG} printparse "-l" " " "${loglevel}" ;; t) toc=${OPTARG} ;; v) VERBOSE=1 ;; *) if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then echo "Non-option argument: '-${OPTARG}'" >&2 fi ;; esac done echo " After Parsing values " echo "loglevel $loglevel" echo "toc $toc"
I don't have enough rep yet to comment or vote his solution up, but sme's answer worked extremely well for me. The only issue I ran into was that the arguments end up wrapped in single-quotes (so I have an strip them out). I also added some example usages and HELP text. I'll included my slightly extended version here: #!/bin/bash # getopt example # from: https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options HELP_TEXT=\ " USAGE:\n Accepts - and -- flags, can specify options that require a value, and can be in any order. A double-hyphen (--) will stop processing options.\n\n Accepts the following forms:\n\n getopt-example.sh -a -b -c value-for-c some-arg\n getopt-example.sh -c value-for-c -a -b some-arg\n getopt-example.sh -abc some-arg\n getopt-example.sh --along --blong --clong value-for-c -a -b -c some-arg\n getopt-example.sh some-arg --clong value-for-c\n getopt-example.sh " aflag=false bflag=false cargument="" # options may be followed by one colon to indicate they have a required argument if ! options=$(getopt -o abc:h\? -l along,blong,help,clong: -- "$#") then # something went wrong, getopt will put out an error message for us exit 1 fi set -- $options while [ $# -gt 0 ] do case $1 in -a|--along) aflag=true ;; -b|--blong) bflag=true ;; # for options with required arguments, an additional shift is required -c|--clong) cargument="$2" ; shift;; -h|--help|-\?) echo -e $HELP_TEXT; exit;; (--) shift; break;; (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;; (*) break;; esac shift done # to remove the single quotes around arguments, pipe the output into: # | sed -e "s/^'\\|'$//g" (just leading/trailing) or | sed -e "s/'//g" (all) echo aflag=${aflag} echo bflag=${bflag} echo cargument=${cargument} while [ $# -gt 0 ] do echo arg=$1 shift if [[ $aflag == true ]]; then echo a is true fi done
Here you can find a few different approaches for complex option parsing in bash: http://mywiki.wooledge.org/ComplexOptionParsing I did create the following one, and I think it's a good one, because it's minimal code and both long and short options work. A long option can also have multiple arguments with this approach. #!/bin/bash # Uses bash extensions. Not portable as written. declare -A longoptspec longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default optspec=":h-:" while getopts "$optspec" opt; do while true; do case "${opt}" in -) #OPTARG is name-of-long-option or name-of-long-option=value if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible then opt=${OPTARG/=*/} OPTARG=${OPTARG#*=} ((OPTIND--)) else #with this --key value1 value2 format multiple arguments are possible opt="$OPTARG" OPTARG=(${#:OPTIND:$((longoptspec[$opt]))}) fi ((OPTIND+=longoptspec[$opt])) continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options ;; loglevel) loglevel=$OPTARG ;; h|help) echo "usage: $0 [--loglevel[=]<value>]" >&2 exit 2 ;; esac break; done done # End of file
I have been working on that subject for quite a long time... and made my own library which you will need to source in your main script. See libopt4shell and cd2mpc for an example. Hope it helps !
An improved solution: # translate long options to short # Note: This enable long options but disable "--?*" in $OPTARG, or disable long options after "--" in option fields. for ((i=1;$#;i++)) ; do case "$1" in --) # [ ${args[$((i-1))]} == ... ] || EndOpt=1 ;;& # DIRTY: we still can handle some execptions... EndOpt=1 ;;& --version) ((EndOpt)) && args[$i]="$1" || args[$i]="-V";; # default case : short option use the first char of the long option: --?*) ((EndOpt)) && args[$i]="$1" || args[$i]="-${1:2:1}";; # pass through anything else: *) args[$i]="$1" ;; esac shift done # reset the translated args set -- "${args[#]}" function usage { echo "Usage: $0 [options] files" >&2 exit $1 } # now we can process with getopt while getopts ":hvVc:" opt; do case $opt in h) usage ;; v) VERBOSE=true ;; V) echo $Version ; exit ;; c) source $OPTARG ;; \?) echo "unrecognized option: -$opt" ; usage -1 ;; :) echo "option -$OPTARG requires an argument" usage -1 ;; esac done shift $((OPTIND-1)) [[ "$1" == "--" ]] && shift
Maybe it's simpler to use ksh, just for the getopts part, if need long command line options, as it can be easier done there. # Working Getopts Long => KSH #! /bin/ksh # Getopts Long USAGE="s(showconfig)" USAGE+="c:(createdb)" USAGE+="l:(createlistener)" USAGE+="g:(generatescripts)" USAGE+="r:(removedb)" USAGE+="x:(removelistener)" USAGE+="t:(createtemplate)" USAGE+="h(help)" while getopts "$USAGE" optchar ; do case $optchar in s) echo "Displaying Configuration" ;; c) echo "Creating Database $OPTARG" ;; l) echo "Creating Listener LISTENER_$OPTARG" ;; g) echo "Generating Scripts for Database $OPTARG" ;; r) echo "Removing Database $OPTARG" ;; x) echo "Removing Listener LISTENER_$OPTARG" ;; t) echo "Creating Database Template" ;; h) echo "Help" ;; esac done
I wanted something without external dependencies, with strict bash support (-u), and I needed it to work on even the older bash versions. This handles various types of params: short bools (-h) short options (-i "image.jpg") long bools (--help) equals options (--file="filename.ext") space options (--file "filename.ext") concatinated bools (-hvm) Just insert the following at the top of your script: # Check if a list of params contains a specific param # usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ... # the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable) _param_variant() { for param in $1 ; do local variants=${param//\|/ } for variant in $variants ; do if [[ "$variant" = "$2" ]] ; then # Update the key to match the long version local arr=(${param//\|/ }) let last=${#arr[#]}-1 key="${arr[$last]}" return 0 fi done done return 1 } # Get input parameters in short or long notation, with no dependencies beyond bash # usage: # # First, set your defaults # param_help=false # param_path="." # param_file=false # param_image=false # param_image_lossy=true # # Define allowed parameters # allowed_params="h|?|help p|path f|file i|image image-lossy" # # Get parameters from the arguments provided # _get_params $* # # Parameters will be converted into safe variable names like: # param_help, # param_path, # param_file, # param_image, # param_image_lossy # # Parameters without a value like "-h" or "--help" will be treated as # boolean, and will be set as param_help=true # # Parameters can accept values in the various typical ways: # -i "path/goes/here" # --image "path/goes/here" # --image="path/goes/here" # --image=path/goes/here # These would all result in effectively the same thing: # param_image="path/goes/here" # # Concatinated short parameters (boolean) are also supported # -vhm is the same as -v -h -m _get_params(){ local param_pair local key local value local shift_count while : ; do # Ensure we have a valid param. Allows this to work even in -u mode. if [[ $# == 0 || -z $1 ]] ; then break fi # Split the argument if it contains "=" param_pair=(${1//=/ }) # Remove preceeding dashes key="${param_pair[0]#--}" # Check for concatinated boolean short parameters. local nodash="${key#-}" local breakout=false if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then # Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h" local short_param_count=${#nodash} let new_arg_count=$#+$short_param_count-1 local new_args="" # $str_pos is the current position in the short param string $nodash for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do # The first character becomes the current key if [ $str_pos -eq 0 ] ; then key="${nodash:$str_pos:1}" breakout=true fi # $arg_pos is the current position in the constructed arguments list let arg_pos=$str_pos+1 if [ $arg_pos -gt $short_param_count ] ; then # handle other arguments let orignal_arg_number=$arg_pos-$short_param_count+1 local new_arg="${!orignal_arg_number}" else # break out our one argument into new ones local new_arg="-${nodash:$str_pos:1}" fi new_args="$new_args \"$new_arg\"" done # remove the preceding space and set the new arguments eval set -- "${new_args# }" fi if ! $breakout ; then key="$nodash" fi # By default we expect to shift one argument at a time shift_count=1 if [ "${#param_pair[#]}" -gt "1" ] ; then # This is a param with equals notation value="${param_pair[1]}" else # This is either a boolean param and there is no value, # or the value is the next command line argument # Assume the value is a boolean true, unless the next argument is found to be a value. value=true if [[ $# -gt 1 && -n "$2" ]]; then local nodash="${2#-}" if [ "$nodash" = "$2" ]; then # The next argument has NO preceding dash so it is a value value="$2" shift_count=2 fi fi fi # Check that the param being passed is one of the allowed params if _param_variant "$allowed_params" "$key" ; then # --key-name will now become param_key_name eval param_${key//-/_}="$value" else printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2 fi shift $shift_count done } And use it like so: # Assign defaults for parameters param_help=false param_path=$(pwd) param_file=false param_image=true param_image_lossy=true param_image_lossy_quality=85 # Define the params we will allow allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality" # Get the params from arguments provided _get_params $*
if simply this is how you want to call the script myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ" then you can follow this simplest way to achieve it with the help of getopt and --longoptions try this , hope this is useful # 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 --input1) shift empId=$1 ;; --input2) shift fromDate=$1 ;; --input3) shift toDate=$1 ;; --) shift break ;; esac shift done
It took some time, but I wanted it all: short options long options with or without arguments non-option arguments (those without "-" or "--") order shouldn't matter (script.sh /file -V or script.sh -V /file) catch wrong usage use it as a module in different scripts without the need to change multiple lines of code Finally I came up with the following solution, which uses getopt to catch errors and move non-options to the end of the list and after that getopts, which parses the short and long options. All options are automatically parsed with their long option name as variable name (look at the example): # create string of short options opt_short=$(printf "%s" "${!options[#]}") # create string of long options opt_long="$(printf ",%s" "${options[#]}")" # catch wrong options and move non-options to the end of the string args=$(getopt -l "$opt_long" "$opt_short" "$#" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1 # create new array of options mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) '/\1='/g")" ) # overwrite $# (options) set -- "${args[#]}" # parse options ([h]=help sets the variable "$opt_help" and [V]="" sets the variable "$opt_V") while getopts "$opt_short-:" opt; do echo "$opt:$OPTARG" # long option if [[ "$opt" == "-" ]]; then # extract long option name opt="${OPTARG%%=*}" # extract long option argument (may be empty) OPTARG="${OPTARG#"$opt"}" # remove "=" from long option argument OPTARG="${OPTARG#=}" # set variable name opt=opt_$opt # short option without argument uses long option name as variable name elif [[ "${options[$opt]+x}" ]] && [[ "${options[$opt]}" ]]; then opt=opt_${options[$opt]} # short option with argument uses long option name as variable name elif [[ "${options[$opt:]+x}" ]] && [[ "${options[$opt:]}" ]]; then opt=opt_${options[$opt:]} # short option without long option name uses short option name as variable name else opt=opt_$opt fi # remove double colon opt="${opt%:}" # options without arguments are set to 1 [[ ! $OPTARG ]] && OPTARG=1 # replace hyphen against underscore opt="${opt//-/_}" # set variable variables (replaces hyphen against underscore) printf -v "$opt" '%s' "$OPTARG" done Now, I only need to define the needed option names and source the script: # import options module declare -A options=( [h]=help [f:]=file: [V]=verbose [0]=long_only: [s]="" ) source "/usr/local/bin/inc/options.sh"; # display help text if [[ $opt_help ]]; then echo "help text" exit fi # output echo "opt_help:$opt_help" echo "opt_file:$opt_file" echo "opt_verbose:$opt_verbose" echo "opt_long_only:$opt_long_only" echo "opt_short_only:$opt_s" echo "opt_path:$1" echo "opt_mail:$2" And while calling the script, it's possible to pass all options and non-options in a complete random order: # $opt_file $1 $2 $opt_V $opt_long_only $opt_s # /demo.sh --file=file.txt /dir info#example.com -V --long_only=yes -s opt_help:1 opt_file:file.txt opt_verbose:1 opt_long_only:yes opt_short_only:1 opt_path=/dir opt_mail:info#example.com Notes In the options array add : after an option name to enable arguments. If no long option name is given, the variable name will be $opt_X, where X is the short option name. If you want to use a long option name without defining a short option name, then set the array index to a number as in the above example done with [0]=long_only. Of course every array index must be unique. Used techniques Capture stderr without using a temporary file Convert string to array Use : to parse arguments with getopt Parse long option names with getopts
getopts "could be used" for parsing long options as long as you don't expect them to have arguments... Here's how to: $ cat > longopt while getopts 'e:-:' OPT; do case $OPT in e) echo echo: $OPTARG;; -) #long option case $OPTARG in long-option) echo long option;; *) echo long option: $OPTARG;; esac;; esac done $ bash longopt -e asd --long-option --long1 --long2 -e test echo: asd long option long option: long1 long option: long2 echo: test If you try to use OPTIND for getting a parameter for the long option, getopts will treat it as the first no optional positional parameter and will stop parsing any other parameters. In such a case you'll be better off handling it manually with a simple case statement. This will "always" work: $ cat >longopt2 while (($#)); do OPT=$1 shift case $OPT in --*) case ${OPT:2} in long1) echo long1 option;; complex) echo comples with argument $1; shift;; esac;; -*) case ${OPT:1} in a) echo short option a;; b) echo short option b with parameter $1; shift;; esac;; esac done $ bash longopt2 --complex abc -a --long -b test comples with argument abc short option a short option b with parameter test Albeit is not as flexible as getopts and you have to do much of the error checking code yourself within the case instances... But it is an option.
In order to stay cross-platform compatible, and avoid the reliance on external executables, I ported some code from another language. I find it very easy to use, here is an example: ArgParser::addArg "[h]elp" false "This list" ArgParser::addArg "[q]uiet" false "Supress output" ArgParser::addArg "[s]leep" 1 "Seconds to sleep" ArgParser::addArg "v" 1 "Verbose mode" ArgParser::parse "$#" ArgParser::isset help && ArgParser::showArgs ArgParser::isset "quiet" \ && echo "Quiet!" \ || echo "Noisy!" local __sleep ArgParser::tryAndGetArg sleep into __sleep \ && echo "Sleep for $__sleep seconds" \ || echo "No value passed for sleep" # This way is often more convienient, but is a little slower echo "Sleep set to: $( ArgParser::getArg sleep )" The required BASH is a little longer than it could be, but I wanted to avoid reliance on BASH 4's associative arrays. You can also download this directly from http://nt4.com/bash/argparser.inc.sh #!/usr/bin/env bash # Updates to this script may be found at # http://nt4.com/bash/argparser.inc.sh # Example of runtime usage: # mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com # Example of use in script (see bottom) # Just include this file in yours, or use # source argparser.inc.sh unset EXPLODED declare -a EXPLODED function explode { local c=$# (( c < 2 )) && { echo function "$0" is missing parameters return 1 } local delimiter="$1" local string="$2" local limit=${3-99} local tmp_delim=$'\x07' local delin=${string//$delimiter/$tmp_delim} local oldifs="$IFS" IFS="$tmp_delim" EXPLODED=($delin) IFS="$oldifs" } # See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference # Usage: local "$1" && upvar $1 "value(s)" upvar() { if unset -v "$1"; then # Unset & validate varname if (( $# == 2 )); then eval $1=\"\$2\" # Return single value else eval $1=\(\"\${#:2}\"\) # Return array fi fi } function decho { : } function ArgParser::check { __args=${#__argparser__arglist[#]} for (( i=0; i<__args; i++ )) do matched=0 explode "|" "${__argparser__arglist[$i]}" if [ "${#1}" -eq 1 ] then if [ "${1}" == "${EXPLODED[0]}" ] then decho "Matched $1 with ${EXPLODED[0]}" matched=1 break fi else if [ "${1}" == "${EXPLODED[1]}" ] then decho "Matched $1 with ${EXPLODED[1]}" matched=1 break fi fi done (( matched == 0 )) && return 2 # decho "Key $key has default argument of ${EXPLODED[3]}" if [ "${EXPLODED[3]}" == "false" ] then return 0 else return 1 fi } function ArgParser::set { key=$3 value="${1:-true}" declare -g __argpassed__$key="$value" } function ArgParser::parse { unset __argparser__argv __argparser__argv=() # echo parsing: "$#" while [ -n "$1" ] do # echo "Processing $1" if [ "${1:0:2}" == '--' ] then key=${1:2} value=$2 elif [ "${1:0:1}" == '-' ] then key=${1:1} # Strip off leading - value=$2 else decho "Not argument or option: '$1'" >& 2 __argparser__argv+=( "$1" ) shift continue fi # parameter=${tmp%%=*} # Extract name. # value=${tmp##*=} # Extract value. decho "Key: '$key', value: '$value'" # eval $parameter=$value ArgParser::check $key el=$? # echo "Check returned $el for $key" [ $el -eq 2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" ) [ $el -eq 0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments" >&2 && ArgParser::set true "${EXPLODED[#]}" [ $el -eq 1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'" >&2 && ArgParser::set "$2" "${EXPLODED[#]}" && shift shift done } function ArgParser::isset { declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0 return 1 } function ArgParser::getArg { # This one would be a bit silly, since we can only return non-integer arguments ineffeciently varname="__argpassed__$1" echo "${!varname}" } ## # usage: tryAndGetArg <argname> into <varname> # returns: 0 on success, 1 on failure function ArgParser::tryAndGetArg { local __varname="__argpassed__$1" local __value="${!__varname}" test -z "$__value" && return 1 local "$3" && upvar $3 "$__value" return 0 } function ArgParser::__construct { unset __argparser__arglist # declare -a __argparser__arglist } ## # #brief add command line argument # #param 1 short and/or long, eg: [s]hort # #param 2 default value # #param 3 description ## function ArgParser::addArg { # check for short arg within long arg if [[ "$1" =~ \[(.)\] ]] then short=${BASH_REMATCH[1]} long=${1/\[$short\]/$short} else long=$1 fi if [ "${#long}" -eq 1 ] then short=$long long='' fi decho short: "$short" decho long: "$long" __argparser__arglist+=("$short|$long|$1|$2|$3") } ## # #brief show available command line arguments ## function ArgParser::showArgs { # declare -p | grep argparser printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )" printf "Defaults for the options are specified in brackets.\n\n"; __args=${#__argparser__arglist[#]} for (( i=0; i<__args; i++ )) do local shortname= local fullname= local default= local description= local comma= explode "|" "${__argparser__arglist[$i]}" shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide: fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html test -n "$shortname" \ && test -n "$fullname" \ && comma="," default="${EXPLODED[3]}" case $default in false ) default= ;; "" ) default= ;; * ) default="[$default]" esac description="${EXPLODED[4]}" printf " %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default" done } function ArgParser::test { # Arguments with a default of 'false' do not take paramaters (note: default # values are not applied in this release) ArgParser::addArg "[h]elp" false "This list" ArgParser::addArg "[q]uiet" false "Supress output" ArgParser::addArg "[s]leep" 1 "Seconds to sleep" ArgParser::addArg "v" 1 "Verbose mode" ArgParser::parse "$#" ArgParser::isset help && ArgParser::showArgs ArgParser::isset "quiet" \ && echo "Quiet!" \ || echo "Noisy!" local __sleep ArgParser::tryAndGetArg sleep into __sleep \ && echo "Sleep for $__sleep seconds" \ || echo "No value passed for sleep" # This way is often more convienient, but is a little slower echo "Sleep set to: $( ArgParser::getArg sleep )" echo "Remaining command line: ${__argparser__argv[#]}" } if [ "$( basename "$0" )" == "argparser.inc.sh" ] then ArgParser::test "$#" fi
If all your long options have unique, and matching, first characters as the short options, so for example ./slamm --chaos 23 --plenty test -quiet Is the same as ./slamm -c 23 -p test -q You can use this before getopts to rewrite $args: # change long options to short options for arg; do [[ "${arg:0:1}" == "-" ]] && delim="" || delim="\"" if [ "${arg:0:2}" == "--" ]; then args="${args} -${arg:2:1}" else args="${args} ${delim}${arg}${delim}" fi done # reset the incoming args eval set -- $args # proceed as usual while getopts ":b:la:h" OPTION; do ..... Thanks for mtvee for the inspiration ;-)
Builtin getopts only parse short options (except in ksh93), but you can still add few lines of scripting to make getopts handles long options. Here is a part of code found in http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts #== set short options ==# SCRIPT_OPTS=':fbF:B:-:h' #== set long options associated with short one ==# typeset -A ARRAY_OPTS ARRAY_OPTS=( [foo]=f [bar]=b [foobar]=F [barfoo]=B [help]=h [man]=h ) #== parse options ==# while getopts ${SCRIPT_OPTS} OPTION ; do #== translate long options to short ==# if [[ "x$OPTION" == "x-" ]]; then LONG_OPTION=$OPTARG LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2) LONG_OPTIND=-1 [[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1) [[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND" OPTION=${ARRAY_OPTS[$LONG_OPTION]} [[ "x$OPTION" = "x" ]] && OPTION="?" OPTARG="-$LONG_OPTION" if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then OPTION=":" OPTARG="-$LONG_OPTION" else OPTARG="$LONG_OPTARG"; if [[ $LONG_OPTIND -ne -1 ]]; then [[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 )) shift $OPTIND OPTIND=1 fi fi fi fi #== options follow by another option instead of argument ==# if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then OPTARG="$OPTION" OPTION=":" fi #== manage options ==# case "$OPTION" in f ) foo=1 bar=0 ;; b ) foo=0 bar=1 ;; B ) barfoo=${OPTARG} ;; F ) foobar=1 && foobar_name=${OPTARG} ;; h ) usagefull && exit 0 ;; : ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;; ? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;; esac done shift $((${OPTIND} - 1)) Here is a test: # Short options test $ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2 foo=0 bar=1 barfoo=6 foobar=1 foobar_name=Hello world files=file1 file2 # Long and short options test $ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2 foo=0 bar=1 barfoo=6 foobar=1 foobar_name=Hello files=file1 file2 Otherwise in recent Korn Shell ksh93, getopts can naturally parse long options and even display a man page alike. (See http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options)
Th built-in OS X (BSD) getopt does not support long options, but the GNU version does: brew install gnu-getopt. Then, something similar to: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt.
Command line argument validation library for Bash
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
How to get arguments with flags in Bash
I know that I can easily get positioned parameters like this in bash: $0 or $1 I want to be able to use flag options like this to specify for what each parameter is used: mysql -u user -h host What is the best way to get -u param value and -h param value by flag instead of by position?
This example uses Bash's built-in getopts command and is from the Google Shell Style Guide: a_flag='' b_flag='' files='' verbose='false' print_usage() { printf "Usage: ..." } while getopts 'abf:v' flag; do case "${flag}" in a) a_flag='true' ;; b) b_flag='true' ;; f) files="${OPTARG}" ;; v) verbose='true' ;; *) print_usage exit 1 ;; esac done Note: If a character is followed by a colon (e.g. f:), that option is expected to have an argument. Example usage: ./script -v -a -b -f filename Using getopts has several advantages over the accepted answer: the while condition is a lot more readable and shows what the accepted options are cleaner code; no counting the number of parameters and shifting you can join options (e.g. -a -b -c → -abc) However, a big disadvantage is that it doesn't support long options, only single-character options.
This is the idiom I usually use: while test $# -gt 0; do case "$1" in -h|--help) echo "$package - attempt to capture frames" echo " " echo "$package [options] application [arguments]" echo " " echo "options:" echo "-h, --help show brief help" echo "-a, --action=ACTION specify an action to use" echo "-o, --output-dir=DIR specify a directory to store output in" exit 0 ;; -a) shift if test $# -gt 0; then export PROCESS=$1 else echo "no process specified" exit 1 fi shift ;; --action*) export PROCESS=`echo $1 | sed -e 's/^[^=]*=//g'` shift ;; -o) shift if test $# -gt 0; then export OUTPUT=$1 else echo "no output dir specified" exit 1 fi shift ;; --output-dir*) export OUTPUT=`echo $1 | sed -e 's/^[^=]*=//g'` shift ;; *) break ;; esac done Key points are: $# is the number of arguments while loop looks at all the arguments supplied, matching on their values inside a case statement shift takes the first one away. You can shift multiple times inside of a case statement to take multiple values.
getopt is your friend.. a simple example: function f () { TEMP=`getopt --long -o "u:h:" "$#"` eval set -- "$TEMP" while true ; do case "$1" in -u ) user=$2 shift 2 ;; -h ) host=$2 shift 2 ;; *) break ;; esac done; echo "user = $user, host = $host" } f -u myself -h some_host There should be various examples in your /usr/bin directory.
I propose a simple TLDR:; example for the un-initiated. Create a bash script called greeter.sh #!/bin/bash while getopts "n:" arg; do case $arg in n) Name=$OPTARG;; esac done echo "Hello $Name!" You can then pass an optional parameter -n when executing the script. Execute the script as such: $ bash greeter.sh -n 'Bob' Output $ Hello Bob! Notes If you'd like to use multiple parameters: extend while getops "n:" arg: do with more paramaters such as while getops "n:o:p:" arg: do extend the case switch with extra variable assignments. Such as o) Option=$OPTARG and p) Parameter=$OPTARG To make the script executable: chmod u+x greeter.sh
I think this would serve as a simpler example of what you want to achieve. There is no need to use external tools. Bash built in tools can do the job for you. function DOSOMETHING { while test $# -gt 0; do case "$1" in -first) shift first_argument=$1 shift ;; -last) shift last_argument=$1 shift ;; *) echo "$1 is not a recognized flag!" return 1; ;; esac done echo "First argument : $first_argument"; echo "Last argument : $last_argument"; } This will allow you to use flags so no matter which order you are passing the parameters you will get the proper behavior. Example : DOSOMETHING -last "Adios" -first "Hola" Output : First argument : Hola Last argument : Adios You can add this function to your profile or put it inside of a script. Thanks! Edit : Save this as a a file and then execute it as yourfile.sh -last "Adios" -first "Hola" #!/bin/bash while test $# -gt 0; do case "$1" in -first) shift first_argument=$1 shift ;; -last) shift last_argument=$1 shift ;; *) echo "$1 is not a recognized flag!" return 1; ;; esac done echo "First argument : $first_argument"; echo "Last argument : $last_argument";
Another alternative would be to use something like the below example which would allow you to use long --image or short -i tags and also allow compiled -i="example.jpg" or separate -i example.jpg methods of passing in arguments. # declaring a couple of associative arrays declare -A arguments=(); declare -A variables=(); # declaring an index integer declare -i index=1; # any variables you want to use here # on the left left side is argument label or key (entered at the command line along with it's value) # on the right side is the variable name the value of these arguments should be mapped to. # (the examples above show how these are being passed into this script) variables["-gu"]="git_user"; variables["--git-user"]="git_user"; variables["-gb"]="git_branch"; variables["--git-branch"]="git_branch"; variables["-dbr"]="db_fqdn"; variables["--db-redirect"]="db_fqdn"; variables["-e"]="environment"; variables["--environment"]="environment"; # $# here represents all arguments passed in for i in "$#" do arguments[$index]=$i; prev_index="$(expr $index - 1)"; # this if block does something akin to "where $i contains =" # "%=*" here strips out everything from the = to the end of the argument leaving only the label if [[ $i == *"="* ]] then argument_label=${i%=*} else argument_label=${arguments[$prev_index]} fi # this if block only evaluates to true if the argument label exists in the variables array if [[ -n ${variables[$argument_label]} ]] then # dynamically creating variables names using declare # "#$argument_label=" here strips out the label leaving only the value if [[ $i == *"="* ]] then declare ${variables[$argument_label]}=${i#$argument_label=} else declare ${variables[$argument_label]}=${arguments[$index]} fi fi index=index+1; done; # then you could simply use the variables like so: echo "$git_user";
I like Robert McMahan's answer the best here as it seems the easiest to make into sharable include files for any of your scripts to use. But it seems to have a flaw with the line if [[ -n ${variables[$argument_label]} ]] throwing the message, "variables: bad array subscript". I don't have the rep to comment, and I doubt this is the proper 'fix,' but wrapping that if in if [[ -n $argument_label ]] ; then cleans it up. Here's the code I ended up with, if you know a better way please add a comment to Robert's answer. Include File "flags-declares.sh" # declaring a couple of associative arrays declare -A arguments=(); declare -A variables=(); # declaring an index integer declare -i index=1; Include File "flags-arguments.sh" # $# here represents all arguments passed in for i in "$#" do arguments[$index]=$i; prev_index="$(expr $index - 1)"; # this if block does something akin to "where $i contains =" # "%=*" here strips out everything from the = to the end of the argument leaving only the label if [[ $i == *"="* ]] then argument_label=${i%=*} else argument_label=${arguments[$prev_index]} fi if [[ -n $argument_label ]] ; then # this if block only evaluates to true if the argument label exists in the variables array if [[ -n ${variables[$argument_label]} ]] ; then # dynamically creating variables names using declare # "#$argument_label=" here strips out the label leaving only the value if [[ $i == *"="* ]] then declare ${variables[$argument_label]}=${i#$argument_label=} else declare ${variables[$argument_label]}=${arguments[$index]} fi fi fi index=index+1; done; Your "script.sh" . bin/includes/flags-declares.sh # any variables you want to use here # on the left left side is argument label or key (entered at the command line along with it's value) # on the right side is the variable name the value of these arguments should be mapped to. # (the examples above show how these are being passed into this script) variables["-gu"]="git_user"; variables["--git-user"]="git_user"; variables["-gb"]="git_branch"; variables["--git-branch"]="git_branch"; variables["-dbr"]="db_fqdn"; variables["--db-redirect"]="db_fqdn"; variables["-e"]="environment"; variables["--environment"]="environment"; . bin/includes/flags-arguments.sh # then you could simply use the variables like so: echo "$git_user"; echo "$git_branch"; echo "$db_fqdn"; echo "$environment";
#!/bin/bash if getopts "n:" arg; then echo "Welcome $OPTARG" fi Save it as sample.sh and try running sh sample.sh -n John in your terminal.
If you're familiar with Python argparse, and don't mind calling python to parse bash arguments, there is a piece of code I found really helpful and super easy to use called argparse-bash https://github.com/nhoffman/argparse-bash Example take from their example.sh script: #!/bin/bash source $(dirname $0)/argparse.bash || exit 1 argparse "$#" <<EOF || exit 1 parser.add_argument('infile') parser.add_argument('outfile') parser.add_argument('-a', '--the-answer', default=42, type=int, help='Pick a number [default %(default)s]') parser.add_argument('-d', '--do-the-thing', action='store_true', default=False, help='store a boolean [default %(default)s]') parser.add_argument('-m', '--multiple', nargs='+', help='multiple values allowed') EOF echo required infile: "$INFILE" echo required outfile: "$OUTFILE" echo the answer: "$THE_ANSWER" echo -n do the thing? if [[ $DO_THE_THING ]]; then echo " yes, do it" else echo " no, do not do it" fi echo -n "arg with multiple values: " for a in "${MULTIPLE[#]}"; do echo -n "[$a] " done echo
I had trouble using getopts with multiple flags, so I wrote this code. It uses a modal variable to detect flags, and to use those flags to assign arguments to variables. Note that, if a flag shouldn't have an argument, something other than setting CURRENTFLAG can be done. for MYFIELD in "$#"; do CHECKFIRST=`echo $MYFIELD | cut -c1` if [ "$CHECKFIRST" == "-" ]; then mode="flag" else mode="arg" fi if [ "$mode" == "flag" ]; then case $MYFIELD in -a) CURRENTFLAG="VARIABLE_A" ;; -b) CURRENTFLAG="VARIABLE_B" ;; -c) CURRENTFLAG="VARIABLE_C" ;; esac elif [ "$mode" == "arg" ]; then case $CURRENTFLAG in VARIABLE_A) VARIABLE_A="$MYFIELD" ;; VARIABLE_B) VARIABLE_B="$MYFIELD" ;; VARIABLE_C) VARIABLE_C="$MYFIELD" ;; esac fi done
So here it is my solution. I wanted to be able to handle boolean flags without hyphen, with one hyphen, and with two hyphen as well as parameter/value assignment with one and two hyphens. # Handle multiple types of arguments and prints some variables # # Boolean flags # 1) No hyphen # create Assigns `true` to the variable `CREATE`. # Default is `CREATE_DEFAULT`. # delete Assigns true to the variable `DELETE`. # Default is `DELETE_DEFAULT`. # 2) One hyphen # a Assigns `true` to a. Default is `false`. # b Assigns `true` to b. Default is `false`. # 3) Two hyphens # cats Assigns `true` to `cats`. By default is not set. # dogs Assigns `true` to `cats`. By default is not set. # # Parameter - Value # 1) One hyphen # c Assign any value you want # d Assign any value you want # # 2) Two hyphens # ... Anything really, whatever two-hyphen argument is given that is not # defined as flag, will be defined with the next argument after it. # # Example: # ./parser_example.sh delete -a -c VA_1 --cats --dir /path/to/dir parser() { # Define arguments with one hyphen that are boolean flags HYPHEN_FLAGS="a b" # Define arguments with two hyphens that are boolean flags DHYPHEN_FLAGS="cats dogs" # Iterate over all the arguments while [ $# -gt 0 ]; do # Handle the arguments with no hyphen if [[ $1 != "-"* ]]; then echo "Argument with no hyphen!" echo $1 # Assign true to argument $1 declare $1=true # Shift arguments by one to the left shift # Handle the arguments with one hyphen elif [[ $1 == "-"[A-Za-z0-9]* ]]; then # Handle the flags if [[ $HYPHEN_FLAGS == *"${1/-/}"* ]]; then echo "Argument with one hyphen flag!" echo $1 # Remove the hyphen from $1 local param="${1/-/}" # Assign true to $param declare $param=true # Shift by one shift # Handle the parameter-value cases else echo "Argument with one hyphen value!" echo $1 $2 # Remove the hyphen from $1 local param="${1/-/}" # Assign argument $2 to $param declare $param="$2" # Shift by two shift 2 fi # Handle the arguments with two hyphens elif [[ $1 == "--"[A-Za-z0-9]* ]]; then # NOTE: For double hyphen I am using `declare -g $param`. # This is the case because I am assuming that's going to be # the final name of the variable echo "Argument with two hypens!" # Handle the flags if [[ $DHYPHEN_FLAGS == *"${1/--/}"* ]]; then echo $1 true # Remove the hyphens from $1 local param="${1/--/}" # Assign argument $2 to $param declare -g $param=true # Shift by two shift # Handle the parameter-value cases else echo $1 $2 # Remove the hyphens from $1 local param="${1/--/}" # Assign argument $2 to $param declare -g $param="$2" # Shift by two shift 2 fi fi done # Default value for arguments with no hypheb CREATE=${create:-'CREATE_DEFAULT'} DELETE=${delete:-'DELETE_DEFAULT'} # Default value for arguments with one hypen flag VAR1=${a:-false} VAR2=${b:-false} # Default value for arguments with value # NOTE1: This is just for illustration in one line. We can well create # another function to handle this. Here I am handling the cases where # we have a full named argument and a contraction of it. # For example `--arg1` can be also set with `-c`. # NOTE2: What we are doing here is to check if $arg is defined. If not, # check if $c was defined. If not, assign the default value "VD_" VAR3=$(if [[ $arg1 ]]; then echo $arg1; else echo ${c:-"VD_1"}; fi) VAR4=$(if [[ $arg2 ]]; then echo $arg2; else echo ${d:-"VD_2"}; fi) } # Pass all the arguments given to the script to the parser function parser "$#" echo $CREATE $DELETE $VAR1 $VAR2 $VAR3 $VAR4 $cats $dir Some references The main procedure was found here. More about passing all the arguments to a function here. More info regarding default values here. More info about declare do $ bash -c "help declare". More info about shift do $ bash -c "help shift".