unix/linux shell (command) script [duplicate] - linux
Say, I have a script that gets called with this line:
./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
or this one:
./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile
What's the accepted way of parsing this such that in each case (or some combination of the two) $v, $f, and $d will all be set to true and $outFile will be equal to /fizz/someOtherFile?
Bash Space-Separated (e.g., --option argument)
cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-e|--extension)
EXTENSION="$2"
shift # past argument
shift # past value
;;
-s|--searchpath)
SEARCHPATH="$2"
shift # past argument
shift # past value
;;
--default)
DEFAULT=YES
shift # past argument
;;
-*|--*)
echo "Unknown option $1"
exit 1
;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done
set -- "${POSITIONAL_ARGS[#]}" # restore positional parameters
echo "FILE EXTENSION = ${EXTENSION}"
echo "SEARCH PATH = ${SEARCHPATH}"
echo "DEFAULT = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
echo "Last line of file specified as non-opt/last argument:"
tail -1 "$1"
fi
EOF
chmod +x /tmp/demo-space-separated.sh
/tmp/demo-space-separated.sh -e conf -s /etc /etc/hosts
Output from copy-pasting the block above
FILE EXTENSION = conf
SEARCH PATH = /etc
DEFAULT =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34 example.com
Usage
demo-space-separated.sh -e conf -s /etc /etc/hosts
Bash Equals-Separated (e.g., --option=argument)
cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash
for i in "$#"; do
case $i in
-e=*|--extension=*)
EXTENSION="${i#*=}"
shift # past argument=value
;;
-s=*|--searchpath=*)
SEARCHPATH="${i#*=}"
shift # past argument=value
;;
--default)
DEFAULT=YES
shift # past argument with no value
;;
-*|--*)
echo "Unknown option $i"
exit 1
;;
*)
;;
esac
done
echo "FILE EXTENSION = ${EXTENSION}"
echo "SEARCH PATH = ${SEARCHPATH}"
echo "DEFAULT = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
echo "Last line of file specified as non-opt/last argument:"
tail -1 $1
fi
EOF
chmod +x /tmp/demo-equals-separated.sh
/tmp/demo-equals-separated.sh -e=conf -s=/etc /etc/hosts
Output from copy-pasting the block above
FILE EXTENSION = conf
SEARCH PATH = /etc
DEFAULT =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34 example.com
Usage
demo-equals-separated.sh -e=conf -s=/etc /etc/hosts
To better understand ${i#*=} search for "Substring Removal" in this guide. It is functionally equivalent to `sed 's/[^=]*=//' <<< "$i"` which calls a needless subprocess or `echo "$i" | sed 's/[^=]*=//'` which calls two needless subprocesses.
Using bash with getopt[s]
getopt(1) limitations (older, relatively-recent getopt versions):
can't handle arguments that are empty strings
can't handle arguments with embedded whitespace
More recent getopt versions don't have these limitations. For more information, see these docs.
POSIX getopts
Additionally, the POSIX shell and others offer getopts which doen't have these limitations. I've included a simplistic getopts example.
cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh
# A POSIX variable
OPTIND=1 # Reset in case getopts has been used previously in the shell.
# Initialize our own variables:
output_file=""
verbose=0
while getopts "h?vf:" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
v) verbose=1
;;
f) output_file=$OPTARG
;;
esac
done
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
echo "verbose=$verbose, output_file='$output_file', Leftovers: $#"
EOF
chmod +x /tmp/demo-getopts.sh
/tmp/demo-getopts.sh -vf /etc/hosts foo bar
Output from copy-pasting the block above
verbose=1, output_file='/etc/hosts', Leftovers: foo bar
Usage
demo-getopts.sh -vf /etc/hosts foo bar
The advantages of getopts are:
It's more portable, and will work in other shells like dash.
It can handle multiple single options like -vf filename in the typical Unix way, automatically.
The disadvantage of getopts is that it can only handle short options (-h, not --help) without additional code.
There is a getopts tutorial which explains what all of the syntax and variables mean. In bash, there is also help getopts, which might be informative.
No answer showcases enhanced getopt. And the top-voted answer is misleading: It either ignores -vfd style short options (requested by the OP) or options after positional arguments (also requested by the OP); and it ignores parsing-errors. Instead:
Use enhanced getopt from util-linux or formerly GNU glibc.1
It works with getopt_long() the C function of GNU glibc.
no other solution on this page can do all this:
handles spaces, quoting characters and even binary in arguments2 (non-enhanced getopt can’t do this)
it can handle options at the end: script.sh -o outFile file1 file2 -v (getopts doesn’t do this)
allows =-style long options: script.sh --outfile=fileOut --infile fileIn (allowing both is lengthy if self parsing)
allows combined short options, e.g. -vfd (real work if self parsing)
allows touching option-arguments, e.g. -oOutfile or -vfdoOutfile
Is so old already3 that no GNU system is missing this (e.g. any Linux has it).
You can test for its existence with: getopt --test → return value 4.
Other getopt or shell-builtin getopts are of limited use.
The following calls
myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile
all return
verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile
with the following myscript
#!/bin/bash
# More safety, by turning some bugs into errors.
# Without `errexit` you don’t need ! and can replace
# ${PIPESTATUS[0]} with a simple $?, but I prefer safety.
set -o errexit -o pipefail -o noclobber -o nounset
# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
echo 'I’m sorry, `getopt --test` failed in this environment.'
exit 1
fi
# option --output/-o requires 1 argument
LONGOPTS=debug,force,output:,verbose
OPTIONS=dfo:v
# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via -- "$#" to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$#")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
# e.g. return value is 1
# then getopt has complained about wrong arguments to stdout
exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"
d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
case "$1" in
-d|--debug)
d=y
shift
;;
-f|--force)
f=y
shift
;;
-v|--verbose)
v=y
shift
;;
-o|--output)
outFile="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Programming error"
exit 3
;;
esac
done
# handle non-option arguments
if [[ $# -ne 1 ]]; then
echo "$0: A single input file is required."
exit 4
fi
echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"
1 enhanced getopt is available on most “bash-systems”, including Cygwin; on OS X try brew install gnu-getopt or sudo port install getopt
2 the POSIX exec() conventions have no reliable way to pass binary NULL in command line arguments; those bytes prematurely end the argument
3 first version released in 1997 or before (I only tracked it back to 1997)
deploy.sh
#!/bin/bash
while [[ "$#" -gt 0 ]]; do
case $1 in
-t|--target) target="$2"; shift ;;
-u|--uglify) uglify=1 ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
shift
done
echo "Where to deploy: $target"
echo "Should uglify : $uglify"
Usage:
./deploy.sh -t dev -u
# OR:
./deploy.sh --target dev --uglify
From digitalpeer.com with minor modifications:
Usage myscript.sh -p=my_prefix -s=dirname -l=libname
#!/bin/bash
for i in "$#"
do
case $i in
-p=*|--prefix=*)
PREFIX="${i#*=}"
;;
-s=*|--searchpath=*)
SEARCHPATH="${i#*=}"
;;
-l=*|--lib=*)
DIR="${i#*=}"
;;
--default)
DEFAULT=YES
;;
*)
# unknown option
;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}
To better understand ${i#*=} search for "Substring Removal" in this guide. It is functionally equivalent to `sed 's/[^=]*=//' <<< "$i"` which calls a needless subprocess or `echo "$i" | sed 's/[^=]*=//'` which calls two needless subprocesses.
while [ "$#" -gt 0 ]; do
case "$1" in
-n) name="$2"; shift 2;;
-p) pidfile="$2"; shift 2;;
-l) logfile="$2"; shift 2;;
--name=*) name="${1#*=}"; shift 1;;
--pidfile=*) pidfile="${1#*=}"; shift 1;;
--logfile=*) logfile="${1#*=}"; shift 1;;
--name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;
-*) echo "unknown option: $1" >&2; exit 1;;
*) handle_argument "$1"; shift 1;;
esac
done
This solution:
handles -n arg and --name=arg
allows arguments at the end
shows sane errors if anything is misspelled
compatible, doesn't use bashisms
readable, doesn't require maintaining state in a loop
getopt()/getopts() is a good option. Copied from here:
The simple use of "getopt" is shown in this mini-script:
#!/bin/bash
echo "Before getopt"
for i
do
echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
echo "-->$i"
done
What we have said is that any of -a,
-b, -c or -d will be allowed, but that -c is followed by an argument (the "c:" says that).
If we call this "g" and try it out:
bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--
We start with two arguments, and
"getopt" breaks apart the options and
puts each in its own argument. It also
added "--".
I have found the matter to write portable parsing in scripts so frustrating that I have written Argbash - a FOSS code generator that can generate the arguments-parsing code for your script plus it has some nice features:
https://argbash.io
I used the earlier answers as a starting point to tidy up my old adhoc param parsing. I then refactored out the following template code. It handles both long and short params, using = or space separated arguments, as well as multiple short params grouped together. Finally it re-inserts any non-param arguments back into the $1,$2.. variables.
#!/usr/bin/env bash
# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $# ; exit $? ; fi
echo "Before"
for i ; do echo - $i ; done
# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.
while [ -n "$1" ]; do
# Copy so we can modify it (can't modify $1)
OPT="$1"
# Detect argument termination
if [ x"$OPT" = x"--" ]; then
shift
for OPT ; do
REMAINS="$REMAINS \"$OPT\""
done
break
fi
# Parse current opt
while [ x"$OPT" != x"-" ] ; do
case "$OPT" in
# Handle --flag=value opts like this
-c=* | --config=* )
CONFIGFILE="${OPT#*=}"
shift
;;
# and --flag value opts like this
-c* | --config )
CONFIGFILE="$2"
shift
;;
-f* | --force )
FORCE=true
;;
-r* | --retry )
RETRY=true
;;
# Anything unknown is recorded for later
* )
REMAINS="$REMAINS \"$OPT\""
break
;;
esac
# Check for multiple short options
# NOTICE: be sure to update this pattern to match valid options
NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
if [ x"$OPT" != x"$NEXTOPT" ] ; then
OPT="-$NEXTOPT" # multiple short opts, keep going
else
break # long form, exit inner loop
fi
done
# Done with that param. move to next
shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS
echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done
# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
key="$1"
case "$key" in
# This is a flag type option. Will catch either -f or --foo
-f|--foo)
FOO=1
;;
# Also a flag type option. Will catch either -b or --bar
-b|--bar)
BAR=1
;;
# This is an arg value type option. Will catch -o value or --output-file value
-o|--output-file)
shift # past the key and to the value
OUTPUTFILE="$1"
;;
# This is an arg=value type option. Will catch -o=value or --output-file=value
-o=*|--output-file=*)
# No need to shift here since the value is part of the same string
OUTPUTFILE="${key#*=}"
;;
*)
# Do whatever you want with extra options
echo "Unknown option '$key'"
;;
esac
# Shift after checking all the cases to get the next option
shift
done
This allows you to have both space separated options/values, as well as equal defined values.
So you could run your script using:
./myscript --foo -b -o /fizz/file.txt
as well as:
./myscript -f --bar -o=/fizz/file.txt
and both should have the same end result.
PROS:
Allows for both -arg=value and -arg value
Works with any arg name that you can use in bash
Meaning -a or -arg or --arg or -a-r-g or whatever
Pure bash. No need to learn/use getopt or getopts
CONS:
Can't combine args
Meaning no -abc. You must do -a -b -c
This example shows how to use getopt and eval and HEREDOC and shift to handle short and long parameters with and without a required value that follows. Also the switch/case statement is concise and easy to follow.
#!/usr/bin/env bash
# usage function
function usage()
{
cat << HEREDOC
Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]
optional arguments:
-h, --help show this help message and exit
-n, --num NUM pass in a number
-t, --time TIME_STR pass in a time string
-v, --verbose increase the verbosity of the bash script
--dry-run do a dry run, dont change any files
HEREDOC
}
# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=
# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$#")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"
while true; do
# uncomment the next line to see how shift is working
# echo "\$1:\"$1\" \$2:\"$2\""
case "$1" in
-h | --help ) usage; exit; ;;
-n | --num ) num_str="$2"; shift 2 ;;
-t | --time ) time_str="$2"; shift 2 ;;
--dry-run ) dryrun=1; shift ;;
-v | --verbose ) verbose=$((verbose + 1)); shift ;;
-- ) shift; break ;;
* ) break ;;
esac
done
if (( $verbose > 0 )); then
# print out all the parameters we read in
cat <<EOM
num=$num_str
time=$time_str
verbose=$verbose
dryrun=$dryrun
EOM
fi
# The rest of your script below
The most significant lines of the script above are these:
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$#")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"
while true; do
case "$1" in
-h | --help ) usage; exit; ;;
-n | --num ) num_str="$2"; shift 2 ;;
-t | --time ) time_str="$2"; shift 2 ;;
--dry-run ) dryrun=1; shift ;;
-v | --verbose ) verbose=$((verbose + 1)); shift ;;
-- ) shift; break ;;
* ) break ;;
esac
done
Short, to the point, readable, and handles just about everything (IMHO).
Hope that helps someone.
Expanding on #bruno-bronosky's answer, I added a "preprocessor" to handle some common formatting:
Expands --longopt=val into --longopt val
Expands -xyz into -x -y -z
Supports -- to indicate the end of flags
Shows an error for unexpected options
Compact and easy-to-read options switch
#!/bin/bash
# Report usage
usage() {
echo "Usage:"
echo "$(basename "$0") [options] [--] [file1, ...]"
}
invalid() {
echo "ERROR: Unrecognized argument: $1" >&2
usage
exit 1
}
# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
arg="$1"; shift
case "${END_OF_OPT}${arg}" in
--) ARGV+=("$arg"); END_OF_OPT=1 ;;
--*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
--*) ARGV+=("$arg") ;;
-*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
*) ARGV+=("$arg") ;;
esac
done
# Apply pre-processed options
set -- "${ARGV[#]}"
# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "${END_OF_OPT}${1}" in
-h|--help) usage; exit 0 ;;
-p|--password) shift; PASSWORD="$1" ;;
-u|--username) shift; USERNAME="$1" ;;
-n|--name) shift; names+=("$1") ;;
-q|--quiet) QUIET=1 ;;
-C|--copy) COPY=1 ;;
-N|--notify) NOTIFY=1 ;;
--stdin) READ_STDIN=1 ;;
--) END_OF_OPT=1 ;;
-*) invalid "$1" ;;
*) POSITIONAL+=("$1") ;;
esac
shift
done
# Restore positional parameters
set -- "${POSITIONAL[#]}"
ASAP: Another Shell Argument Parser
Edit note: version 2.0, now with pure POSIX shell code and zero witchery!
TL;DR
This parser uses only POSIX compliant shell code to process options in these formats: -o [ARG], -abo [ARG], --opt [ARG] or --opt=[ARG], where ARG is an optional argument. It can handle intermixed options and arguments, and also "--" to force any argument after it to be treated as positional.
Here is a minimal version that works as long as the command is correct, i.e. it doesn't perform almost any checks. You can paste it at the top of your sh script —it won't work as a function— and substitute your option definitions.
#!/bin/sh -e
USAGE="Usage: ${CMD=${0##*/}} [(-v|--verbose)] [--name=TEXT] [(-o|--output) FILE] [ARGS...]"
exit2 () { printf >&2 "%s: %s: '%s'\n%s\n" "$CMD" "$1" "$2" "$USAGE"; exit 2; }
check () { { [ "$1" != "$EOL" ] && [ "$1" != '--' ]; } || exit2 "missing argument" "$2"; } # avoid infinite loop
# parse command-line options
set -- "$#" "${EOL:=$(printf '\1\3\3\7')}" # end-of-list marker
while [ "$1" != "$EOL" ]; do
opt="$1"; shift
case "$opt" in
#EDIT HERE: defined options
--name ) check "$1" "$opt"; opt_name="$1"; shift;;
-o | --output ) check "$1" "$opt"; opt_output="$1"; shift;;
-v | --verbose ) opt_verbose='true';;
-h | --help ) printf "%s\n" "$USAGE"; exit 0;;
# process special cases
--) while [ "$1" != "$EOL" ]; do set -- "$#" "$1"; shift; done;; # parse remaining as positional
--[!=]*=*) set -- "${opt%%=*}" "${opt#*=}" "$#";; # "--opt=arg" -> "--opt" "arg"
-[A-Za-z0-9] | -*[!A-Za-z0-9]*) exit2 "invalid option" "$opt";; # anything invalid like '-*'
-?*) other="${opt#-?}"; set -- "${opt%$other}" "-${other}" "$#";; # "-abc" -> "-a" "-bc"
*) set -- "$#" "$opt";; # positional, rotate to the end
esac
done; shift # $EOL
unset CMD EOL USAGE opt other
unset -f check exit2
printf "name = '%s'\noutput = '%s'\nverbose = '%s'\n\$# = (%s)\n" "$opt_name" "$opt_output" "$opt_verbose" "$*"
Sample outputs
$ ./asap-example.sh -vo path/to/camelot 'spam?' --name=Arthur 'spam!' -- +42 -17
name = 'Arthur'
output = 'path/to/camelot'
verbose = 'true'
$# = (spam? spam! +42 -17)
$ ./asap-example.sh -name Lancelot eggs bacon
asap-example.sh: invalid option: '-n'
Usage: asap-example.sh [(-v|--verbose)] [--name=TEXT] [(-o|--output) FILE] [ARG...]
Description
I was inspired by the relatively simple answer by #bronson and tempted to try to improve it (without adding too much complexity).
This parser implementation uses pattern matching, parameter expansion and the shell's own positional parameters as a rotating stack to loop over and process arguments. Here's the result:
Any of the -o [ARG], -abo [ARG], --long-option [ARG] and --long-option=[ARG] styles of options are accepted;
Arguments may occur in any order, only positional ones are left in $# after the loop;
Use -- to force remaining arguments to be treated as positional;
Portable, compact, quite readable, with independent features;
Doesn't depend on getopt(s) or external utilities;
Detects invalid options and missing arguments.
Portability
This code was tested and verified to work with a reasonably recent version of: BusyBox's ash, bash, dash, mksh, ksh93, yash and zsh (called with their standard executable path, not as /bin/sh).
PS: I know... An argument with the binary value 0x01030307 could break the logic. But, if anyone passes such an argument in a command-line, they deserve it.
If you are making scripts that are interchangeable with other utilities, below flexibility may be useful.
Either:
command -x=myfilename.ext --another_switch
Or:
command -x myfilename.ext --another_switch
Here is the code:
STD_IN=0
prefix=""
key=""
value=""
for keyValue in "$#"
do
case "${prefix}${keyValue}" in
-i=*|--input_filename=*) key="-i"; value="${keyValue#*=}";;
-ss=*|--seek_from=*) key="-ss"; value="${keyValue#*=}";;
-t=*|--play_seconds=*) key="-t"; value="${keyValue#*=}";;
-|--stdin) key="-"; value=1;;
*) value=$keyValue;;
esac
case $key in
-i) MOVIE=$(resolveMovie "${value}"); prefix=""; key="";;
-ss) SEEK_FROM="${value}"; prefix=""; key="";;
-t) PLAY_SECONDS="${value}"; prefix=""; key="";;
-) STD_IN=${value}; prefix=""; key="";;
*) prefix="${keyValue}=";;
esac
done
I think this one is simple enough to use:
#!/bin/bash
#
readopt='getopts $opts opt;rc=$?;[ "$rc$opt" = "0?" ]&&exit 1;[ $rc = 0 ]||{ shift $[OPTIND-1];false; }'
opts=vfdo:
# Enumerating options
while eval "$readopt"
do
echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done
# Enumerating arguments
for arg
do
echo ARG:$arg
done
Invocation example:
./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v
OPT:d
OPT:o OPTARG:/fizz/someOtherFile
OPT:f
ARG:./foo/bar/someFile
I give you The Function parse_params that will parse params from the command line.
It is a pure Bash solution, no additional utilities.
Does not pollute global scope.
Effortlessly returns you simple to use variables, that you could build further logic on.
Amount of dashes before params does not matter (--all equals -all equals all=all)
The script below is a copy-paste working demonstration. See show_use function to understand how to use parse_params.
Limitations:
Does not support space delimited params (-d 1)
Param names will lose dashes so --any-param and -anyparam are equivalent
eval $(parse_params "$#") must be used inside bash function (it will not work in the global scope)
#!/bin/bash
# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# #author Oleksii Chekulaiev
# #version v1.4.1 (Jul-27-2018)
parse_params ()
{
local existing_named
local ARGV=() # un-named params
local ARGN=() # named params
local ARGO=() # options (--params)
echo "local ARGV=(); local ARGN=(); local ARGO=();"
while [[ "$1" != "" ]]; do
# Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
_escaped=${1/\*/\'\"*\"\'}
_escaped=${_escaped//\'/\\\'}
_escaped=${_escaped//\"/\\\"}
# If equals delimited named parameter
nonspace="[^[:space:]]"
if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
# Add to named parameters array
echo "ARGN+=('$_escaped');"
# key is part before first =
local _key=$(echo "$1" | cut -d = -f 1)
# Just add as non-named when key is empty or contains space
if [[ "$_key" == "" || "$_key" =~ " " ]]; then
echo "ARGV+=('$_escaped');"
shift
continue
fi
# val is everything after key and = (protect from param==value error)
local _val="${1/$_key=}"
# remove dashes from key name
_key=${_key//\-}
# skip when key is empty
# search for existing parameter name
if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
# if name already exists then it's a multi-value named parameter
# re-declare it as an array if needed
if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
echo "$_key=(\"\$$_key\");"
fi
# append new value
echo "$_key+=('$_val');"
else
# single-value named parameter
echo "local $_key='$_val';"
existing_named=" $_key"
fi
# If standalone named parameter
elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
# remove dashes
local _key=${1//\-}
# Just add as non-named when key is empty or contains space
if [[ "$_key" == "" || "$_key" =~ " " ]]; then
echo "ARGV+=('$_escaped');"
shift
continue
fi
# Add to options array
echo "ARGO+=('$_escaped');"
echo "local $_key=\"$_key\";"
# non-named parameter
else
# Escape asterisk to prevent bash asterisk expansion
_escaped=${1/\*/\'\"*\"\'}
echo "ARGV+=('$_escaped');"
fi
shift
done
}
#--------------------------- DEMO OF THE USAGE -------------------------------
show_use ()
{
eval $(parse_params "$#")
# --
echo "${ARGV[0]}" # print first unnamed param
echo "${ARGV[1]}" # print second unnamed param
echo "${ARGN[0]}" # print first named param
echo "${ARG0[0]}" # print first option param (--force)
echo "$anyparam" # print --anyparam value
echo "$k" # print k=5 value
echo "${multivalue[0]}" # print first value of multi-value
echo "${multivalue[1]}" # print second value of multi-value
[[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}
show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
getopts works great if #1 you have it installed and #2 you intend to run it on the same platform. OSX and Linux (for example) behave differently in this respect.
Here is a (non getopts) solution that supports equals, non-equals, and boolean flags. For example you could run your script in this way:
./script --arg1=value1 --arg2 value2 --shouldClean
# parse the arguments.
COUNTER=0
ARGS=("$#")
while [ $COUNTER -lt $# ]
do
arg=${ARGS[$COUNTER]}
let COUNTER=COUNTER+1
nextArg=${ARGS[$COUNTER]}
if [[ $skipNext -eq 1 ]]; then
echo "Skipping"
skipNext=0
continue
fi
argKey=""
argVal=""
if [[ "$arg" =~ ^\- ]]; then
# if the format is: -key=value
if [[ "$arg" =~ \= ]]; then
argVal=$(echo "$arg" | cut -d'=' -f2)
argKey=$(echo "$arg" | cut -d'=' -f1)
skipNext=0
# if the format is: -key value
elif [[ ! "$nextArg" =~ ^\- ]]; then
argKey="$arg"
argVal="$nextArg"
skipNext=1
# if the format is: -key (a boolean flag)
elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
argKey="$arg"
argVal=""
skipNext=0
fi
# if the format has not flag, just a value.
else
argKey=""
argVal="$arg"
skipNext=0
fi
case "$argKey" in
--source-scmurl)
SOURCE_URL="$argVal"
;;
--dest-scmurl)
DEST_URL="$argVal"
;;
--version-num)
VERSION_NUM="$argVal"
;;
-c|--clean)
CLEAN_BEFORE_START="1"
;;
-h|--help|-help|--h)
showUsage
exit
;;
esac
done
Yet another option parser (generator)
An elegant option parser for shell scripts (full support for all POSIX shells)
https://github.com/ko1nksm/getoptions (Update: v3.3.0 released on 2021-05-02)
getoptions is a new option parser (generator) written in POSIX-compliant shell script and released in august 2020. It is for those who want to support the POSIX / GNU style option syntax in your shell scripts.
The supported syntaxes are -a, +a, -abc, -vvv, -p VALUE, -pVALUE, --flag, --no-flag, --with-flag, --without-flag, --param VALUE, --param=VALUE, --option[=VALUE], --no-option --.
It supports subcommands, validation, abbreviated options, and automatic help generation. And works with all POSIX shells (dash 0.5.4+, bash 2.03+, ksh88+, mksh R28+, zsh 3.1.9+, yash 2.29+, busybox ash 1.1.3+, etc).
#!/bin/sh
VERSION="0.1"
parser_definition() {
setup REST help:usage -- "Usage: example.sh [options]... [arguments]..." ''
msg -- 'Options:'
flag FLAG -f --flag -- "takes no arguments"
param PARAM -p --param -- "takes one argument"
option OPTION -o --option on:"default" -- "takes one optional argument"
disp :usage -h --help
disp VERSION --version
}
eval "$(getoptions parser_definition) exit 1"
echo "FLAG: $FLAG, PARAM: $PARAM, OPTION: $OPTION"
printf '%s\n' "$#" # rest arguments
It's parses the following arguments:
example.sh -f --flag -p VALUE --param VALUE -o --option -oVALUE --option=VALUE 1 2 3
And automatic help generation.
$ example.sh --help
Usage: example.sh [options]... [arguments]...
Options:
-f, --flag takes no arguments
-p, --param PARAM takes one argument
-o, --option[=OPTION] takes one optional argument
-h, --help
--version
It is also an option parser generator, generates the following simple option parsing code. If you use the generated code, you won't need getoptions. Achieve true portability and zero dependency.
FLAG=''
PARAM=''
OPTION=''
REST=''
getoptions_parse() {
OPTIND=$(($#+1))
while OPTARG= && [ $# -gt 0 ]; do
case $1 in
--?*=*) OPTARG=$1; shift
eval 'set -- "${OPTARG%%\=*}" "${OPTARG#*\=}"' ${1+'"$#"'}
;;
--no-*|--without-*) unset OPTARG ;;
-[po]?*) OPTARG=$1; shift
eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$#"'}
;;
-[fh]?*) OPTARG=$1; shift
eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$#"'}
OPTARG= ;;
esac
case $1 in
'-f'|'--flag')
[ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break
eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
FLAG="$OPTARG"
;;
'-p'|'--param')
[ $# -le 1 ] && set "required" "$1" && break
OPTARG=$2
PARAM="$OPTARG"
shift ;;
'-o'|'--option')
set -- "$1" "$#"
[ ${OPTARG+x} ] && {
case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac
[ "${OPTARG:-}" ] && { shift; OPTARG=$2; } || OPTARG='default'
} || OPTARG=''
OPTION="$OPTARG"
shift ;;
'-h'|'--help')
usage
exit 0 ;;
'--version')
echo "${VERSION}"
exit 0 ;;
--)
shift
while [ $# -gt 0 ]; do
REST="${REST} \"\${$(($OPTIND-$#))}\""
shift
done
break ;;
[-]?*) set "unknown" "$1"; break ;;
*)
REST="${REST} \"\${$(($OPTIND-$#))}\""
esac
shift
done
[ $# -eq 0 ] && { OPTIND=1; unset OPTARG; return 0; }
case $1 in
unknown) set "Unrecognized option: $2" "$#" ;;
noarg) set "Does not allow an argument: $2" "$#" ;;
required) set "Requires an argument: $2" "$#" ;;
pattern:*) set "Does not match the pattern (${1#*:}): $2" "$#" ;;
notcmd) set "Not a command: $2" "$#" ;;
*) set "Validation error ($1): $2" "$#"
esac
echo "$1" >&2
exit 1
}
usage() {
cat<<'GETOPTIONSHERE'
Usage: example.sh [options]... [arguments]...
Options:
-f, --flag takes no arguments
-p, --param PARAM takes one argument
-o, --option[=OPTION] takes one optional argument
-h, --help
--version
GETOPTIONSHERE
}
I wanna submit my project : https://github.com/flyingangel/argparser
source argparser.sh
parse_args "$#"
Simple as that. The environment will be populated with variables with the same name as the arguments
This is how I do in a function to avoid breaking getopts run at the same time somewhere higher in stack:
function waitForWeb () {
local OPTIND=1 OPTARG OPTION
local host=localhost port=8080 proto=http
while getopts "h:p:r:" OPTION; do
case "$OPTION" in
h)
host="$OPTARG"
;;
p)
port="$OPTARG"
;;
r)
proto="$OPTARG"
;;
esac
done
...
}
I'd like to offer my version of option parsing, that allows for the following:
-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello
Also allows for this (could be unwanted):
-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder
You have to decide before use if = is to be used on an option or not. This is to keep the code clean(ish).
while [[ $# > 0 ]]
do
key="$1"
while [[ ${key+x} ]]
do
case $key in
-s*|--stage)
STAGE="$2"
shift # option has parameter
;;
-w*|--workfolder)
workfolder="$2"
shift # option has parameter
;;
-e=*)
EXAMPLE="${key#*=}"
break # option has been fully handled
;;
*)
# unknown option
echo Unknown option: $key #1>&2
exit 10 # either this: my preferred way to handle unknown options
break # or this: do this to signal the option has been handled (if exit isn't used)
;;
esac
# prepare for next option in this key, if any
[[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
done
shift # option(s) fully processed, proceed to next input argument
done
There are several ways to parse cmdline args (e.g. GNU getopt (not portable) vs BSD (MacOS) getopt vs getopts) - all problematic. This solution
is portable!
has zero dependencies, only relies on bash built-ins
allows for both short and long options
handles whitespace or simultaneously the use of = separator between option and argument
supports concatenated short option style -vxf
handles option with optional arguments (E.g. --color vs --color=always),
correctly detects and reports unknown options
supports -- to signal end of options, and
doesn't require code bloat compared with alternatives for the same feature set. I.e. succinct, and therefore easier to maintain
Examples: Any of
# flag
-f
--foo
# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"
# option with optional argument
--baz
--baz="Optional Hello"
#!/usr/bin/env bash
usage() {
cat - >&2 <<EOF
NAME
program-name.sh - Brief description
SYNOPSIS
program-name.sh [-h|--help]
program-name.sh [-f|--foo]
[-b|--bar <arg>]
[--baz[=<arg>]]
[--]
FILE ...
REQUIRED ARGUMENTS
FILE ...
input files
OPTIONS
-h, --help
Prints this and exits
-f, --foo
A flag option
-b, --bar <arg>
Option requiring an argument <arg>
--baz[=<arg>]
Option that has an optional argument <arg>. If <arg>
is not specified, defaults to 'DEFAULT'
--
Specify end of options; useful if the first non option
argument starts with a hyphen
EOF
}
fatal() {
for i; do
echo -e "${i}" >&2
done
exit 1
}
# For long option processing
next_arg() {
if [[ $OPTARG == *=* ]]; then
# for cases like '--opt=arg'
OPTARG="${OPTARG#*=}"
else
# for cases like '--opt arg'
OPTARG="${args[$OPTIND]}"
OPTIND=$((OPTIND + 1))
fi
}
# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$#") # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
case "$optchar" in
h) usage; exit 0 ;;
f) foo=1 ;;
b) bar="$OPTARG" ;;
-) # long option processing
case "$OPTARG" in
help)
usage; exit 0 ;;
foo)
foo=1 ;;
bar|bar=*) next_arg
bar="$OPTARG" ;;
baz)
baz=DEFAULT ;;
baz=*) next_arg
baz="$OPTARG" ;;
-) break ;;
*) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
esac
;;
*) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
esac
done
shift $((OPTIND-1))
if [ "$#" -eq 0 ]; then
fatal "Expected at least one required argument FILE" \
"See '${0} --help' for usage"
fi
echo "foo=$foo, bar=$bar, baz=$baz, files=${#}"
Solution that preserves unhandled arguments. Demos Included.
Here is my solution. It is VERY flexible and unlike others, shouldn't require external packages and handles leftover arguments cleanly.
Usage is: ./myscript -flag flagvariable -otherflag flagvar2
All you have to do is edit the validflags line. It prepends a hyphen and searches all arguments. It then defines the next argument as the flag name e.g.
./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2
The main code (short version, verbose with examples further down, also a version with erroring out):
#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $#
do
match=0
argval=$1
for flag in $validflags
do
sflag="-"$flag
if [ "$argval" == "$sflag" ]
then
declare $flag=$2
match=1
fi
done
if [ "$match" == "1" ]
then
shift 2
else
leftovers=$(echo $leftovers $argval)
shift
fi
count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
The verbose version with built in echo demos:
#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$#"
validflags="rate time number"
count=1
for arg in $#
do
match=0
argval=$1
# argval=$(echo $# | cut -d ' ' -f$count)
for flag in $validflags
do
sflag="-"$flag
if [ "$argval" == "$sflag" ]
then
declare $flag=$2
match=1
fi
done
if [ "$match" == "1" ]
then
shift 2
else
leftovers=$(echo $leftovers $argval)
shift
fi
count=$(($count+1))
done
#Cleanup then restore the leftovers
echo "pre final clear args:
$#"
shift $#
echo "post final clear args:
$#"
set -- $leftovers
echo "all post set args:
$#"
echo arg1: $1 arg2: $2
echo leftovers: $leftovers
echo rate $rate time $time number $number
Final one, this one errors out if an invalid -argument is passed through.
#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $#
do
argval=$1
match=0
if [ "${argval:0:1}" == "-" ]
then
for flag in $validflags
do
sflag="-"$flag
if [ "$argval" == "$sflag" ]
then
declare $flag=$2
match=1
fi
done
if [ "$match" == "0" ]
then
echo "Bad argument: $argval"
exit 1
fi
shift 2
else
leftovers=$(echo $leftovers $argval)
shift
fi
count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers
Pros: What it does, it handles very well. It preserves unused arguments which a lot of the other solutions here don't. It also allows for variables to be called without being defined by hand in the script. It also allows prepopulation of variables if no corresponding argument is given. (See verbose example).
Cons: Can't parse a single complex arg string e.g. -xcvf would process as a single argument. You could somewhat easily write additional code into mine that adds this functionality though.
Based on other answers here, this my version:
#!/bin/bash
set -e
function parse() {
for arg in "$#"; do # transform long options to short ones
shift
case "$arg" in
"--name") set -- "$#" "-n" ;;
"--verbose") set -- "$#" "-v" ;;
*) set -- "$#" "$arg"
esac
done
while getopts "n:v" optname # left to ":" are flags that expect a value, right to the ":" are flags that expect nothing
do
case "$optname" in
"n") name=${OPTARG} ;;
"v") verbose=true ;;
esac
done
shift "$((OPTIND-1))" # shift out all the already processed options
}
parse "$#"
echo "hello $name"
if [ ! -z $verbose ]; then echo 'nice to meet you!'; fi
Usage:
$ ./parse.sh
hello
$ ./parse.sh -n YOUR_NAME
hello YOUR_NAME
$ ./parse.sh -n YOUR_NAME -v
hello YOUR_NAME
nice to meet you!
$ ./parse.sh -v -n YOUR_NAME
hello YOUR_NAME
nice to meet you!
$ ./parse.sh -v
hello
nice to meet you!
Here is my approach - using regexp.
no getopts
it handles block of short parameters -qwerty
it handles short parameters -q -w -e
it handles long options --qwerty
you can pass attribute to short or long option (if you are using block of short options, attribute is attached to the last option)
you can use spaces or = to provide attributes, but attribute matches until encountering hyphen+space "delimiter", so in --q=qwe ty qwe ty is one attribute
it handles mix of all above so -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ribute is valid
script:
#!/usr/bin/env sh
help_menu() {
echo "Usage:
${0##*/} [-h][-l FILENAME][-d]
Options:
-h, --help
display this help and exit
-l, --logfile=FILENAME
filename
-d, --debug
enable debug
"
}
parse_options() {
case $opt in
h|help)
help_menu
exit
;;
l|logfile)
logfile=${attr}
;;
d|debug)
debug=true
;;
*)
echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
exit 1
esac
}
options=$#
until [ "$options" = "" ]; do
if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
opt=${BASH_REMATCH[3]}
attr=${BASH_REMATCH[7]}
options=${BASH_REMATCH[9]}
elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
pile=${BASH_REMATCH[4]}
while (( ${#pile} > 1 )); do
opt=${pile:0:1}
attr=""
pile=${pile/${pile:0:1}/}
parse_options
done
opt=$pile
attr=${BASH_REMATCH[7]}
options=${BASH_REMATCH[9]}
else # leftovers that don't match
opt=${BASH_REMATCH[10]}
options=""
fi
parse_options
fi
done
Mixing positional and flag-based arguments
--param=arg (equals delimited)
Freely mixing flags between positional arguments:
./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d
can be accomplished with a fairly concise approach:
# process flags
pointer=1
while [[ $pointer -le $# ]]; do
param=${!pointer}
if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
else
case $param in
# paramter-flags with arguments
-e=*|--environment=*) environment="${param#*=}";;
--another=*) another="${param#*=}";;
# binary flags
-q|--quiet) quiet=true;;
-d) debug=true;;
esac
# splice out pointer frame from positional list
[[ $pointer -gt 1 ]] \
&& set -- ${#:1:((pointer - 1))} ${#:((pointer + 1)):$#} \
|| set -- ${#:((pointer + 1)):$#};
fi
done
# positional remain
node_name=$1
ip_address=$2
--param arg (space delimited)
It's usualy clearer to not mix --flag=value and --flag value styles.
./script.sh dumbo 127.0.0.1 --environment production -q -d
This is a little dicey to read, but is still valid
./script.sh dumbo --environment production 127.0.0.1 --quiet -d
Source
# process flags
pointer=1
while [[ $pointer -le $# ]]; do
if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
else
param=${!pointer}
((pointer_plus = pointer + 1))
slice_len=1
case $param in
# paramter-flags with arguments
-e|--environment) environment=${!pointer_plus}; ((slice_len++));;
--another) another=${!pointer_plus}; ((slice_len++));;
# binary flags
-q|--quiet) quiet=true;;
-d) debug=true;;
esac
# splice out pointer frame from positional list
[[ $pointer -gt 1 ]] \
&& set -- ${#:1:((pointer - 1))} ${#:((pointer + $slice_len)):$#} \
|| set -- ${#:((pointer + $slice_len)):$#};
fi
done
# positional remain
node_name=$1
ip_address=$2
Note that getopt(1) was a short living mistake from AT&T.
getopt was created in 1984 but already buried in 1986 because it was not really usable.
A proof for the fact that getopt is very outdated is that the getopt(1) man page still mentions "$*" instead of "$#", that was added to the Bourne Shell in 1986 together with the getopts(1) shell builtin in order to deal with arguments with spaces inside.
BTW: if you are interested in parsing long options in shell scripts, it may be of interest to know that the getopt(3) implementation from libc (Solaris) and ksh93 both added a uniform long option implementation that supports long options as aliases for short options. This causes ksh93 and the Bourne Shell to implement a uniform interface for long options via getopts.
An example for long options taken from the Bourne Shell man page:
getopts "f:(file)(input-file)o:(output-file)" OPTX "$#"
shows how long option aliases may be used in both Bourne Shell and ksh93.
See the man page of a recent Bourne Shell:
http://schillix.sourceforge.net/man/man1/bosh.1.html
and the man page for getopt(3) from OpenSolaris:
http://schillix.sourceforge.net/man/man3c/getopt.3c.html
and last, the getopt(1) man page to verify the outdated $*:
http://schillix.sourceforge.net/man/man1/getopt.1.html
I have write a bash helper to write a nice bash tool
project home: https://gitlab.mbedsys.org/mbedsys/bashopts
example:
#!/bin/bash -ei
# load the library
. bashopts.sh
# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR
# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"
# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"
# Parse arguments
bashopts_parse_args "$#"
# Process argument
bashopts_process_args
will give help:
NAME:
./example.sh - This is myapp tool description displayed on help message
USAGE:
[options and commands] [-- [extra args]]
OPTIONS:
-h,--help Display this help
-n,--non-interactive true Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
-f,--first "John" First name - [$first_name] (type:string, default:"")
-l,--last "Smith" Last name - [$last_name] (type:string, default:"")
--display-name "John Smith" Display name - [$display_name] (type:string, default:"$first_name $last_name")
--number 0 Age - [$age] (type:number, default:0)
--email Email adress - [$email_list] (type:string, default:"")
enjoy :)
Assume we create a shell script named test_args.sh as follow
#!/bin/sh
until [ $# -eq 0 ]
do
name=${1:1}; shift;
if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi
done
echo "year=$year month=$month day=$day flag=$flag"
After we run the following command:
sh test_args.sh -year 2017 -flag -month 12 -day 22
The output would be:
year=2017 month=12 day=22 flag=true
Here is a getopts that achieves the parsing with minimal code and allows you to define what you wish to extract in one case using eval with substring.
Basically eval "local key='val'"
function myrsync() {
local backup=("${#}") args=(); while [[ $# -gt 0 ]]; do k="$1";
case "$k" in
---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include)
eval "local ${k:3}='${2}'"; shift; shift # Past two arguments
;;
*) # Unknown option
args+=("$1"); shift; # Past argument only
;;
esac
done; set -- "${backup[#]}" # Restore $#
echo "${sourceurl}"
}
Declares the variables as locals instead of globals as most answers here.
Called as:
myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ...
The ${k:3} is basically a substring to remove the first --- from the key.
I wanted to share what I made for parsing options.
Some of my needs were not fulfilled by the answers here so I had to come up with this: https://github.com/MihirLuthra/bash_option_parser
This supports:
Suboption parsing
Alias names for options
Optional args
Variable args
Printing usage and errors
Let's say we have a command named fruit with usage as follows:
fruit <fruit-name> ...
[-e|—-eat|—-chew]
[-c|--cut <how> <why>]
<command> [<args>]
-e takes no args
-c takes two args i.e. how to cut and why to cut
fruit itself takes at least one argument.
<command> is for suboptions like apple, orange etc. (similar to git which has suboptions commit, push etc. )
So to parse it:
parse_options \
'fruit' '1 ...' \
'-e' , '--eat' , '--chew' '0' \
'-c' , '--cut' '1 1' \
'apple' 'S' \
'orange' 'S' \
';' \
"$#"
Now if there was any usage error, it can be printed using option_parser_error_msg as follows:
retval=$?
if [ $retval -ne 0 ]; then
# this will manage error messages if
# insufficient or extra args are supplied
option_parser_error_msg "$retval"
# This will print the usage
print_usage 'fruit'
exit 1
fi
To check now if some options was passed,
if [ -n "${OPTIONS[-c]}" ]
then
echo "-c was passed"
# args can be accessed in a 2D-array-like format
echo "Arg1 to -c = ${ARGS[-c,0]}"
echo "Arg2 to -c = ${ARGS[-c,1]}"
fi
Suboption parsing can also be done by passing $shift_count to parse_options_detailed which makes it start parsing after shifting args to reach args of suboption. It is demonstrated in this example.
A detailed description is provided in the readme and examples
in the repository.
Related
how to specify OPRARG only for a specfic option , when using getopts [duplicate]
I want to call myscript file in this way: $ ./myscript -s 45 -p any_string or $ ./myscript -h #should display help $ ./myscript #should display help My requirements are: getopt here to get the input arguments check that -s exists, if not return an error check that the value after the -s is 45 or 90 check that the -p exists and there is an input string after if the user enters ./myscript -h or just ./myscript then display help I tried so far this code: #!/bin/bash while getopts "h:s:" arg; do case $arg in h) echo "usage" ;; s) strength=$OPTARG echo $strength ;; esac done But with that code I get errors. How to do it with Bash and getopt?
#!/bin/bash usage() { echo "Usage: $0 [-s <45|90>] [-p <string>]" 1>&2; exit 1; } while getopts ":s:p:" o; do case "${o}" in s) s=${OPTARG} ((s == 45 || s == 90)) || usage ;; p) p=${OPTARG} ;; *) usage ;; esac done shift $((OPTIND-1)) if [ -z "${s}" ] || [ -z "${p}" ]; then usage fi echo "s = ${s}" echo "p = ${p}" Example runs: $ ./myscript.sh Usage: ./myscript.sh [-s <45|90>] [-p <string>] $ ./myscript.sh -h Usage: ./myscript.sh [-s <45|90>] [-p <string>] $ ./myscript.sh -s "" -p "" Usage: ./myscript.sh [-s <45|90>] [-p <string>] $ ./myscript.sh -s 10 -p foo Usage: ./myscript.sh [-s <45|90>] [-p <string>] $ ./myscript.sh -s 45 -p foo s = 45 p = foo $ ./myscript.sh -s 90 -p bar s = 90 p = bar
The problem with the original code is that: h: expects parameter where it shouldn't, so change it into just h (without colon) to expect -p any_string, you need to add p: to the argument list Basically : after the option means it requires the argument. The basic syntax of getopts is (see: man bash): getopts OPTSTRING VARNAME [ARGS...] where: OPTSTRING is string with list of expected arguments, h - check for option -h without parameters; gives error on unsupported options; h: - check for option -h with parameter; gives errors on unsupported options; abc - check for options -a, -b, -c; gives errors on unsupported options; :abc - check for options -a, -b, -c; silences errors on unsupported options; Notes: In other words, colon in front of options allows you handle the errors in your code. Variable will contain ? in the case of unsupported option, : in the case of missing value. OPTARG - is set to current argument value, OPTERR - indicates if Bash should display error messages. So the code can be: #!/usr/bin/env bash usage() { echo "$0 usage:" && grep " .)\ #" $0; exit 0; } [ $# -eq 0 ] && usage while getopts ":hs:p:" arg; do case $arg in p) # Specify p value. echo "p is ${OPTARG}" ;; s) # Specify strength, either 45 or 90. strength=${OPTARG} [ $strength -eq 45 -o $strength -eq 90 ] \ && echo "Strength is $strength." \ || echo "Strength needs to be either 45 or 90, $strength found instead." ;; h | *) # Display help. usage exit 0 ;; esac done Example usage: $ ./foo.sh ./foo.sh usage: p) # Specify p value. s) # Specify strength, either 45 or 90. h | *) # Display help. $ ./foo.sh -s 123 -p any_string Strength needs to be either 45 or 90, 123 found instead. p is any_string $ ./foo.sh -s 90 -p any_string Strength is 90. p is any_string See: Small getopts tutorial at Bash Hackers Wiki
Use getopt Why getopt? To parse elaborated command-line arguments to avoid confusion and clarify the options we are parsing so that reader of the commands can understand what's happening. What is getopt? getopt is used to break up (parse) options in command lines for easy parsing by shell procedures, and to check for legal options. It uses the GNU getopt(3) routines to do this. getopt can have following types of options. No-value options key-value pair options Note: In this document, during explaining syntax: Anything inside [ ] is optional parameter in the syntax/examples. <value> is a place holder, which mean it should be substituted with an actual value. HOW TO USE getopt? Syntax: First Form getopt optstring parameters Examples: # This is correct getopt "hv:t::" -v 123 -t123 getopt "hv:t::" -v123 -t123 # -v and 123 doesn't have whitespace # -h takes no value. getopt "hv:t::" -h -v123 # This is wrong. after -t can't have whitespace. # Only optional params cannot have whitespace between key and value getopt "hv:t::" -v 123 -t 123 # Multiple arguments that takes value. getopt "h:v:t::g::" -h abc -v 123 -t21 # Multiple arguments without value # All of these are correct getopt "hvt" -htv getopt "hvt" -h -t -v getopt "hvt" -tv -h Here h,v,t are the options and -h -v -t is how options should be given in command-line. 'h' is a no-value option. 'v:' implies that option -v has value and is a mandatory option. ':' means has a value. 't::' implies that option -t has value but is optional. '::' means optional. In optional param, value cannot have whitespace separation with the option. So, in "-t123" example, -t is option 123 is value. Syntax: Second Form getopt [getopt_options] [--] optstring parameters Here after getopt is split into five parts The command itself i.e. getopt The getopt_options, it describes how to parse the arguments. single dash long options, double dash options. --, separates out the getopt_options from the options you want to parse and the allowed short options The short options, is taken immediately after -- is found. Just like the Form first syntax. The parameters, these are the options that you have passed into the program. The options you want to parse and get the actual values set on them. Examples getopt -l "name:,version::,verbose" -- "n:v::V" --name=Karthik -version=5.2 -verbose Syntax: Third Form getopt [getopt_options] -o|--options optstring [getopt_options] [--] [parameters] Here after getopt is split into five parts The command itself i.e. getopt The getopt_options, it describes how to parse the arguments. single dash long options, double dash options. The short options i.e. -o or --options. Just like the Form first syntax but with option "-o" and before the "--" (double dash). --, separates out the getopt_options from the options you want to parse and the allowed short options The parameters, these are the options that you have passed into the program. The options you want to parse and get the actual values set on them. Examples getopt -l "name:,version::,verbose" -a -o "n:v::V" -- -name=Karthik -version=5.2 -verbose GETOPT_OPTIONS getopt_options changes the way command-line params are parsed. Below are some of the getopt_options Option: -l or --longoptions Means getopt command should allow multi-character options to be recognised. Multiple options are separated by comma. For example, --name=Karthik is a long option sent in command line. In getopt, usage of long options are like getopt -l "name:,version" -- "" --name=Karthik Since name: is specified, the option should contain a value Option: -a or --alternative Means getopt command should allow long option to have a single dash '-' rather than double dash '--'. Example, instead of --name=Karthik you could use just -name=Karthik getopt -a -l "name:,version" -- "" -name=Karthik A complete script example with the code: #!/bin/bash # filename: commandLine.sh # author: #theBuzzyCoder showHelp() { # `cat << EOF` This means that cat should stop reading when EOF is detected cat << EOF Usage: ./installer -v <espo-version> [-hrV] Install Pre-requisites for EspoCRM with docker in Development mode -h, -help, --help Display help -v, -espo-version, --espo-version Set and Download specific version of EspoCRM -r, -rebuild, --rebuild Rebuild php vendor directory using composer and compiled css using grunt -V, -verbose, --verbose Run script in verbose mode. Will print out each step of execution. EOF # EOF is found above and hence cat command stops reading. This is equivalent to echo but much neater when printing out. } export version=0 export verbose=0 export rebuilt=0 # $# is all command line parameters passed to the script. # -o is for short options like -v # -l is for long options with double dash like --version # the comma separates different long options # -a is for long options with single dash like -version options=$(getopt -l "help,version:,verbose,rebuild,dryrun" -o "hv:Vrd" -a -- "$#") # set --: # If no arguments follow this option, then the positional parameters are unset. Otherwise, the positional parameters # are set to the arguments, even if some of them begin with a ‘-’. eval set -- "$options" while true do case "$1" in -h|--help) showHelp exit 0 ;; -v|--version) shift export version="$1" ;; -V|--verbose) export verbose=1 set -xv # Set xtrace and verbose mode. ;; -r|--rebuild) export rebuild=1 ;; --) shift break;; esac shift done Running this script file: # With short options grouped together and long option # With double dash '--version' bash commandLine.sh --version=1.0 -rV # With short options grouped together and long option # With single dash '-version' bash commandLine.sh -version=1.0 -rV # OR with short option that takes value, value separated by whitespace # by key bash commandLine.sh -v 1.0 -rV # OR with short option that takes value, value without whitespace # separation from key. bash commandLine.sh -v1.0 -rV # OR Separating individual short options bash commandLine.sh -v1.0 -r -V
The example packaged with getopt (my distro put it in /usr/share/getopt/getopt-parse.bash) looks like it covers all of your cases: #!/bin/bash # A small example program for using the new getopt(1) program. # This program will only work with bash(1) # An similar program using the tcsh(1) script language can be found # as parse.tcsh # Example input and output (from the bash prompt): # ./parse.bash -a par1 'another arg' --c-long 'wow!*\?' -cmore -b " very long " # Option a # Option c, no argument # Option c, argument 'more' # Option b, argument ' very long ' # Remaining arguments: # --> 'par1' # --> 'another arg' # --> 'wow!*\?' # Note that we use `"$#"' to let each command-line parameter expand to a # separate word. The quotes around '$#' are essential! # We need TEMP as the `eval set --' would nuke the return value of getopt. TEMP=$(getopt -o ab:c:: --long a-long,b-long:,c-long:: \ -n 'example.bash' -- "$#") if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi # Note the quotes around '$TEMP': they are essential! eval set -- "$TEMP" while true ; do case "$1" in -a|--a-long) echo "Option a" ; shift ;; -b|--b-long) echo "Option b, argument '$2'" ; shift 2 ;; -c|--c-long) # c has an optional argument. As we are in quoted mode, # an empty parameter will be generated if its optional # argument is not found. case "$2" in "") echo "Option c, no argument"; shift 2 ;; *) echo "Option c, argument '$2'" ; shift 2 ;; esac ;; --) shift ; break ;; *) echo "Internal error!" ; exit 1 ;; esac done echo "Remaining arguments:" for arg do echo '--> '"'$arg'" ; done
I know that this is already answered, but for the record and for anyone with the same requeriments as me I decided to post this related answer. The code is flooded with comments to explain the code. Updated answer: Save the file as getopt.sh: #!/bin/bash function get_variable_name_for_option { local OPT_DESC=${1} local OPTION=${2} local VAR=$(echo ${OPT_DESC} | sed -e "s/.*\[\?-${OPTION} \([A-Z_]\+\).*/\1/g" -e "s/.*\[\?-\(${OPTION}\).*/\1FLAG/g") if [[ "${VAR}" == "${1}" ]]; then echo "" else echo ${VAR} fi } function parse_options { local OPT_DESC=${1} local INPUT=$(get_input_for_getopts "${OPT_DESC}") shift while getopts ${INPUT} OPTION ${#}; do [ ${OPTION} == "?" ] && usage VARNAME=$(get_variable_name_for_option "${OPT_DESC}" "${OPTION}") [ "${VARNAME}" != "" ] && eval "${VARNAME}=${OPTARG:-true}" # && printf "\t%s\n" "* Declaring ${VARNAME}=${!VARNAME} -- OPTIONS='$OPTION'" done check_for_required "${OPT_DESC}" } function check_for_required { local OPT_DESC=${1} local REQUIRED=$(get_required "${OPT_DESC}" | sed -e "s/\://g") while test -n "${REQUIRED}"; do OPTION=${REQUIRED:0:1} VARNAME=$(get_variable_name_for_option "${OPT_DESC}" "${OPTION}") [ -z "${!VARNAME}" ] && printf "ERROR: %s\n" "Option -${OPTION} must been set." && usage REQUIRED=${REQUIRED:1} done } function get_input_for_getopts { local OPT_DESC=${1} echo ${OPT_DESC} | sed -e "s/\([a-zA-Z]\) [A-Z_]\+/\1:/g" -e "s/[][ -]//g" } function get_optional { local OPT_DESC=${1} echo ${OPT_DESC} | sed -e "s/[^[]*\(\[[^]]*\]\)[^[]*/\1/g" -e "s/\([a-zA-Z]\) [A-Z_]\+/\1:/g" -e "s/[][ -]//g" } function get_required { local OPT_DESC=${1} echo ${OPT_DESC} | sed -e "s/\([a-zA-Z]\) [A-Z_]\+/\1:/g" -e "s/\[[^[]*\]//g" -e "s/[][ -]//g" } function usage { printf "Usage:\n\t%s\n" "${0} ${OPT_DESC}" exit 10 } Then you can use it like this: #!/bin/bash # # [ and ] defines optional arguments # # location to getopts.sh file source ./getopt.sh USAGE="-u USER -d DATABASE -p PASS -s SID [ -a START_DATE_TIME ]" parse_options "${USAGE}" ${#} echo ${USER} echo ${START_DATE_TIME} Old answer: I recently needed to use a generic approach. I came across with this solution: #!/bin/bash # Option Description: # ------------------- # # Option description is based on getopts bash builtin. The description adds a variable name feature to be used # on future checks for required or optional values. # The option description adds "=>VARIABLE_NAME" string. Variable name should be UPPERCASE. Valid characters # are [A-Z_]*. # # A option description example: # OPT_DESC="a:=>A_VARIABLE|b:=>B_VARIABLE|c=>C_VARIABLE" # # -a option will require a value (the colon means that) and should be saved in variable A_VARIABLE. # "|" is used to separate options description. # -b option rule applies the same as -a. # -c option doesn't require a value (the colon absense means that) and its existence should be set in C_VARIABLE # # ~$ echo get_options ${OPT_DESC} # a:b:c # ~$ # # Required options REQUIRED_DESC="a:=>REQ_A_VAR_VALUE|B:=>REQ_B_VAR_VALUE|c=>REQ_C_VAR_FLAG" # Optional options (duh) OPTIONAL_DESC="P:=>OPT_P_VAR_VALUE|r=>OPT_R_VAR_FLAG" function usage { IFS="|" printf "%s" ${0} for i in ${REQUIRED_DESC}; do VARNAME=$(echo $i | sed -e "s/.*=>//g") printf " %s" "-${i:0:1} $VARNAME" done for i in ${OPTIONAL_DESC}; do VARNAME=$(echo $i | sed -e "s/.*=>//g") printf " %s" "[-${i:0:1} $VARNAME]" done printf "\n" unset IFS exit } # Auxiliary function that returns options characters to be passed # into 'getopts' from a option description. # Arguments: # $1: The options description (SEE TOP) # # Example: # OPT_DESC="h:=>H_VAR|f:=>F_VAR|P=>P_VAR|W=>W_VAR" # OPTIONS=$(get_options ${OPT_DESC}) # echo "${OPTIONS}" # # Output: # "h:f:PW" function get_options { echo ${1} | sed -e "s/\([a-zA-Z]\:\?\)=>[A-Z_]*|\?/\1/g" } # Auxiliary function that returns all variable names separated by '|' # Arguments: # $1: The options description (SEE TOP) # # Example: # OPT_DESC="h:=>H_VAR|f:=>F_VAR|P=>P_VAR|W=>W_VAR" # VARNAMES=$(get_values ${OPT_DESC}) # echo "${VARNAMES}" # # Output: # "H_VAR|F_VAR|P_VAR|W_VAR" function get_variables { echo ${1} | sed -e "s/[a-zA-Z]\:\?=>\([^|]*\)/\1/g" } # Auxiliary function that returns the variable name based on the # option passed by. # Arguments: # $1: The options description (SEE TOP) # $2: The option which the variable name wants to be retrieved # # Example: # OPT_DESC="h:=>H_VAR|f:=>F_VAR|P=>P_VAR|W=>W_VAR" # H_VAR=$(get_variable_name ${OPT_DESC} "h") # echo "${H_VAR}" # # Output: # "H_VAR" function get_variable_name { VAR=$(echo ${1} | sed -e "s/.*${2}\:\?=>\([^|]*\).*/\1/g") if [[ ${VAR} == ${1} ]]; then echo "" else echo ${VAR} fi } # Gets the required options from the required description REQUIRED=$(get_options ${REQUIRED_DESC}) # Gets the optional options (duh) from the optional description OPTIONAL=$(get_options ${OPTIONAL_DESC}) # or... $(get_options "${OPTIONAL_DESC}|${REQUIRED_DESC}") # The colon at starts instructs getopts to remain silent while getopts ":${REQUIRED}${OPTIONAL}" OPTION do [[ ${OPTION} == ":" ]] && usage VAR=$(get_variable_name "${REQUIRED_DESC}|${OPTIONAL_DESC}" ${OPTION}) [[ -n ${VAR} ]] && eval "$VAR=${OPTARG}" done shift $(($OPTIND - 1)) # Checks for required options. Report an error and exits if # required options are missing. # Using function version ... VARS=$(get_variables ${REQUIRED_DESC}) IFS="|" for VARNAME in $VARS; do [[ -v ${VARNAME} ]] || usage done unset IFS # ... or using IFS Version (no function) OLDIFS=${IFS} IFS="|" for i in ${REQUIRED_DESC}; do VARNAME=$(echo $i | sed -e "s/.*=>//g") [[ -v ${VARNAME} ]] || usage printf "%s %s %s\n" "-${i:0:1}" "${!VARNAME:=present}" "${VARNAME}" done IFS=${OLDIFS} I didn't test this roughly, so I could have some bugs in there.
POSIX 7 example It is also worth checking the example from the standard: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html aflag= bflag= while getopts ab: name do case $name in a) aflag=1;; b) bflag=1 bval="$OPTARG";; ?) printf "Usage: %s: [-a] [-b value] args\n" $0 exit 2;; esac done if [ ! -z "$aflag" ]; then printf "Option -a specified\n" fi if [ ! -z "$bflag" ]; then printf 'Option -b "%s" specified\n' "$bval" fi shift $(($OPTIND - 1)) printf "Remaining arguments are: %s\n" "$*" And then we can try it out: $ sh a.sh Remaining arguments are: $ sh a.sh -a Option -a specified Remaining arguments are: $ sh a.sh -b No arg for -b option Usage: a.sh: [-a] [-b value] args $ sh a.sh -b myval Option -b "myval" specified Remaining arguments are: $ sh a.sh -a -b myval Option -a specified Option -b "myval" specified Remaining arguments are: $ sh a.sh remain Remaining arguments are: remain $ sh a.sh -- -a remain Remaining arguments are: -a remain Tested in Ubuntu 17.10, sh is dash 0.5.8.
getopts and getopt are very limited. While getopt is suggested not to be used at all, it does offer long options. Whereas getopts does only allow single character options such as -a -b. There are a few more disadvantages when using either one. So I've written a small script that replaces getopts and getopt. It's a start, it could probably be improved a lot. Update 08-04-2020: I've added support for hyphens e.g. --package-name. Usage: "./script.sh package install --package "name with space" --build --archive" # Example: # parseArguments "${#}" # echo "${ARG_0}" -> package # echo "${ARG_1}" -> install # echo "${ARG_PACKAGE}" -> "name with space" # echo "${ARG_BUILD}" -> 1 (true) # echo "${ARG_ARCHIVE}" -> 1 (true) function parseArguments() { PREVIOUS_ITEM='' COUNT=0 for CURRENT_ITEM in "${#}" do if [[ ${CURRENT_ITEM} == "--"* ]]; then printf -v "ARG_$(formatArgument "${CURRENT_ITEM}")" "%s" "1" # could set this to empty string and check with [ -z "${ARG_ITEM-x}" ] if it's set, but empty. else if [[ $PREVIOUS_ITEM == "--"* ]]; then printf -v "ARG_$(formatArgument "${PREVIOUS_ITEM}")" "%s" "${CURRENT_ITEM}" else printf -v "ARG_${COUNT}" "%s" "${CURRENT_ITEM}" fi fi PREVIOUS_ITEM="${CURRENT_ITEM}" (( COUNT++ )) done } # Format argument. function formatArgument() { ARGUMENT="${1^^}" # Capitalize. ARGUMENT="${ARGUMENT/--/}" # Remove "--". ARGUMENT="${ARGUMENT//-/_}" # Replace "-" with "_". echo "${ARGUMENT}" }
Turning the huge one-liner in Mark G.'s comment (under Adrian Frühwirth's answer) into a more readable answer - this shows how to avoid using getopts in order to get optional arguments: usage() { printf "Usage: %s <req> [<-s|--sopt> <45|90>] [<-p|--popt> <string>]\n" "$0"; return 1; }; main() { req="${1:?$(usage)}"; shift; s=""; p=""; while [ "$#" -ge 1 ]; do case "$1" in -s|--sopt) shift; s="${1:?$(usage)}"; [ "$s" -eq 45 ] || [ "$s" -eq 90 ] || { usage; return 1; } ;; -p|--popt) shift; p="${1:?$(usage)}" ;; *) usage; return 1 ;; esac; shift; done; printf "req = %s\ns = %s\np = %s\n" "$req" "$s" "$p"; }; main "$#" As noted in n.caillou's comment: it will fail if there's no space between the options and their argument. However, to make it more POSIX compliant (from Mark G.'s other comment): case "$1" in -s*) s=${1#-s}; if [ -z "$s" ]; shift; s=$1; fi
bash script options parser fails
I have a bashscript.sh that I $ chmod +x bashscript.sh and move it to $ mv bashscript.sh ~/.local/bin/ in order for it to be executable like a system command. I'd like to be able to invoke it with bashscript [<-w|-working|--working>[=|:]] <y|yes|n|no> And return usage/help/error (call it whatever we want) if the call isn't respected. To do so, I wrote this parsing part: usage(){ echo "you're wrong." exit 1 } [[ $# -lt 1 ]] && usage options=$(getopt -o y,n,h,w: -l yes,no,help,working: -- "$#") set -- $options while true; do case "$1" in -h|--help|*) usage shift;; y|yes) #do something shift;; n|no) #.. shift;; -w|-working|--working) shift;; --) #this is just syntax shift break;; esac done But when I test it doesn't work as intended*, would you know why/have a sample that handles my option possibilites? *edit : I always trigger the usage display edit 2 : removed the spaces around the "=" of options as #costaparas3 pointed out, thank you, still stuck to usage() though
Here are the issues I found: Exit if there are no arguments Set the options with options=$(... (no spaces) -h|--help|*) matches everything so you have an infinite loop. You don't need to match on * as getopt will reuturn non-zero if it finds an invalid argument, and the match on -- is what usually terminates the loop. getopt returns non-zero for invalid arguments so exit 1 then Use -n|--no to specify short and long options --working requires an argument but you only shift 1. -working is not valid (with getopt). Use either -w or --working. Here is corrected version: #!/bin/bash usage() { echo "you're wrong." exit $1 } [ $# -lt 1 ] && usage options=$(getopt -o y,n,h,w: -l yes,no,help,working: -- "$#") [ $? -ne 0 ] && usage 1 # default values yes= working= set -- $options while : do case "$1" in -h|--help) usage ;; -y|--yes) yes=1 shift ;; -n|--no) yes=0 # or no=1 shift ;; -w|--working) working=$2 shift 2 ;; --) break ;; esac done
Explain ${!OPTIND} in getopts. Dangers? Alternatives?
I've been learning command line argument parsing. There are long threads about this already, I don't mean to provoke one here: Using getopts in bash shell script to get long and short command line options How do I parse command line arguments in Bash? Using getopts, if you want to parse an argument/value pair like "--opt value", one way is to let getopts treat this as an argument named "-" and the value becomes "-opt". Then we parse that and take the user value by the symbol ${!OPTIND}. I need to know more about it. In the first thread I cited above, ${!OPTIND} was used, somebody said "what's that?" and answer was "its an indirect substitution". After reading notes on indirect references, especially https://unix.stackexchange.com/questions/41292/variable-substitution-with-an-exclamation-mark-in-bash and http://mywiki.wooledge.org/BashFAQ/006, I generally understand indirection, but I do not understand ${!OPTIND} as an example of it. The value of $OPTIND is an integer, index of next command line argument. It is not a value in another array. In the BashFAQ/006 link above, there are warnings about indirection and general advice not to use it. Maybe it is no big deal, but I'd like to avoid danger where possible. Could we avoid indirection? Seems like I ought to be able to just use ${OPTIND} as an integer to take a value from $#, $#[$OPTIND]}. In case you want example, here is a script I called "cli-6.sh" and it will receive long form arguments WITHOUT equal signs. Run like this: $ ./cli-6.sh -v --fred good --barney bad --wilma happy Leave off -v for less verbosity. $ ./cli-6.sh --fred good --barney bad --wilma happy After Parsing values, ordinary getopts VERBOSE 0 Arrays of opts and values optary: fred barney wilma valary: good bad happy Hopefully, this runs for you too :) I did not use an associative array to hold the values because I had hope this would work in other shells, eventually. #/usr/bin/env bash 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 ## index for imported values in optary and valary arrays idx=0 ## Need v first so VERBOSE is set early optspec=":vh-:" while getopts "$optspec" OPTCHAR; do case "${OPTCHAR}" in -) showme "OPTARG: ${OPTARG[*]}" showme "OPTIND: ${OPTIND[*]}" showme "OPTCHAR: ${OPTCHAR}" showme "There is no equal sign in ${OPTARG}" opt=${OPTARG} val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) printparse "--${OPTARG}" " " "\"${val}\"" if [[ "$val" == -* ]]; then die "ERROR: $opt value must be supplied" fi optary[${idx}]=${opt} valary[${idx}]=${val} idx=$(($idx + 1)) ;; h) echo "usage: $0 [-v] [--anyOptYouQant[=]<valueIsRequired>] [--another[=]<value>]" >&2 exit 2 ;; v) ## if -v flag is present, it means TRUE VERBOSE=1 ;; *) if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then die "Undefined argument: '-${OPTARG}'" fi ;; esac done echo "After Parsing values, ordinary getopts" echo "VERBOSE $VERBOSE" echo 'Arrays of opts and values' echo "optary: ${optary[*]}" echo "valary: ${valary[*]}"
Not sure if this helps but here is bash only version of an CLI options parser that does not use getopts but accepts short and long arguments. It also handles groups of short form arguments. This should be useful on systems that do not support a recent version of getopts. #!/bin/bash # # Copyright (c) 2017 by Joe Linoff # MIT Open Source License. # # This script shows how to implement an argument parser with # 4 options. Two of the options are simply flags, one of # of them has a single argument and the other has 2 arguments. # # It is meant to show bash can support reasonably complex # argument parsing idioms that will make shell scripts # more user friendly without using getopts. It is useful # for cases where getopts is not available. # # The options demonstrated are: # # 1. -h or --help # 2. -v or --verbose # 3. -f ARG or --file ARG or --file=ARG # 4. -c ARG1 ARG2 or --compare ARG1 ARG2 # # The options parsing allows the following specifications. # # 1. -h # 2. --help # 3. -v # 4. --verbose # 5. -vv # 6. -f ARG1 # 7. --file ARG1 # 8. --file=ARG1 # 9. -c ARG1 ARG2 # 10. --compare ARG1 ARG2 # # This example does not show how to implement best match which would # mean accepting an option like "--com" (because it is the best unique # match to --compare). That could be added but i am not convinced # that it is worth the overhead. # # The options parser is global in this example because it is setting # global (script wide) variables. # ======================================================================== # Functions # ======================================================================== # Simple error function that prints the line number of the caller and # highlights the message in red. function _err() { echo -e "\033[31;1mERROR:\033[0;31m:${BASH_LINENO[0]} $*\033[0m" exit 1 } # ======================================================================== # Main # ======================================================================== CARGS=() FILE='' HELP=0 VERBOSE=0 # The OPT_CACHE is to cache short form options. OPT_CACHE=() while (( $# )) || (( ${#OPT_CACHE[#]} )) ; do if (( ${#OPT_CACHE[#]} > 0 )) ; then OPT="${OPT_CACHE[0]}" if (( ${#OPT_CACHE[#]} > 1 )) ; then OPT_CACHE=(${OPT_CACHE[#]:1}) else OPT_CACHE=() fi else OPT="$1" shift fi case "$OPT" in # Handle the case of multiple short arguments in a single # string: # -abc ==> -a -b -c -[!-][a-zA-Z0-9\-_]*) for (( i=1; i<${#OPT}; i++ )) ; do # Note that the leading dash is added here. CHAR=${OPT:$i:1} OPT_CACHE+=("-$CHAR") done ;; -h|--help) (( HELP++ )) ;; -v|--verbose) # Increase the verbosity. # Can accept: -v -v OR -vv. (( VERBOSE++ )) ;; -f|--file|--file=*) # Can be specified multiple times but we only accept the # last one. # Can accept: --file foo and --file=foo if [ -z "${OPT##*=*}" ] ; then FILE="${OPT#*=}" else FILE="$1" shift fi [[ -z "$FILE" ]] && _err "Missing argument for '$OPT'." ;; -c|--compare) # Can be specified multiple times but we only accept the # last one. # Can accept: # --compare ARG1 ARG2 # Cannot accept: # --compare=* # The reason for not accepting the '=' sign is to reduce # complexity because of the ambiguity of separators. If # you decide that you will always use a comma as the # separator, that is fine until one of the arguments # contains a comma. CARG1="$1" CARG2="$2" shift 2 [[ -z "$CARG1" ]] && _err "Missing both arguments for '$OPT'." [[ -z "$CARG2" ]] && _err "Missing second argument for '$OPT'." CARGS=() CARGS+=("$CARG1") CARGS+=("$CARG2") ;; -*) _err "Unrecognized option '$OPT'." ;; *) _err "Unrecognized argument '$OPT'." ;; esac done echo "COMPARE : ${CARGS[#]}" echo "FILE : ${FILE}" echo "HELP : ${HELP}" echo "VERBOSE : ${VERBOSE}" The code is also available from https://gist.github.com/jlinoff/1876972c0b37259c82367d51c8313171.
Bash - How to vary positional parameters in a case statement depending on command line arguments? [duplicate]
Say, I have a script that gets called with this line: ./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile or this one: ./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile What's the accepted way of parsing this such that in each case (or some combination of the two) $v, $f, and $d will all be set to true and $outFile will be equal to /fizz/someOtherFile?
Bash Space-Separated (e.g., --option argument) cat >/tmp/demo-space-separated.sh <<'EOF' #!/bin/bash POSITIONAL_ARGS=() while [[ $# -gt 0 ]]; do case $1 in -e|--extension) EXTENSION="$2" shift # past argument shift # past value ;; -s|--searchpath) SEARCHPATH="$2" shift # past argument shift # past value ;; --default) DEFAULT=YES shift # past argument ;; -*|--*) echo "Unknown option $1" exit 1 ;; *) POSITIONAL_ARGS+=("$1") # save positional arg shift # past argument ;; esac done set -- "${POSITIONAL_ARGS[#]}" # restore positional parameters echo "FILE EXTENSION = ${EXTENSION}" echo "SEARCH PATH = ${SEARCHPATH}" echo "DEFAULT = ${DEFAULT}" echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l) if [[ -n $1 ]]; then echo "Last line of file specified as non-opt/last argument:" tail -1 "$1" fi EOF chmod +x /tmp/demo-space-separated.sh /tmp/demo-space-separated.sh -e conf -s /etc /etc/hosts Output from copy-pasting the block above FILE EXTENSION = conf SEARCH PATH = /etc DEFAULT = Number files in SEARCH PATH with EXTENSION: 14 Last line of file specified as non-opt/last argument: #93.184.216.34 example.com Usage demo-space-separated.sh -e conf -s /etc /etc/hosts Bash Equals-Separated (e.g., --option=argument) cat >/tmp/demo-equals-separated.sh <<'EOF' #!/bin/bash for i in "$#"; do case $i in -e=*|--extension=*) EXTENSION="${i#*=}" shift # past argument=value ;; -s=*|--searchpath=*) SEARCHPATH="${i#*=}" shift # past argument=value ;; --default) DEFAULT=YES shift # past argument with no value ;; -*|--*) echo "Unknown option $i" exit 1 ;; *) ;; esac done echo "FILE EXTENSION = ${EXTENSION}" echo "SEARCH PATH = ${SEARCHPATH}" echo "DEFAULT = ${DEFAULT}" echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l) if [[ -n $1 ]]; then echo "Last line of file specified as non-opt/last argument:" tail -1 $1 fi EOF chmod +x /tmp/demo-equals-separated.sh /tmp/demo-equals-separated.sh -e=conf -s=/etc /etc/hosts Output from copy-pasting the block above FILE EXTENSION = conf SEARCH PATH = /etc DEFAULT = Number files in SEARCH PATH with EXTENSION: 14 Last line of file specified as non-opt/last argument: #93.184.216.34 example.com Usage demo-equals-separated.sh -e=conf -s=/etc /etc/hosts To better understand ${i#*=} search for "Substring Removal" in this guide. It is functionally equivalent to `sed 's/[^=]*=//' <<< "$i"` which calls a needless subprocess or `echo "$i" | sed 's/[^=]*=//'` which calls two needless subprocesses. Using bash with getopt[s] getopt(1) limitations (older, relatively-recent getopt versions): can't handle arguments that are empty strings can't handle arguments with embedded whitespace More recent getopt versions don't have these limitations. For more information, see these docs. POSIX getopts Additionally, the POSIX shell and others offer getopts which doen't have these limitations. I've included a simplistic getopts example. cat >/tmp/demo-getopts.sh <<'EOF' #!/bin/sh # A POSIX variable OPTIND=1 # Reset in case getopts has been used previously in the shell. # Initialize our own variables: output_file="" verbose=0 while getopts "h?vf:" opt; do case "$opt" in h|\?) show_help exit 0 ;; v) verbose=1 ;; f) output_file=$OPTARG ;; esac done shift $((OPTIND-1)) [ "${1:-}" = "--" ] && shift echo "verbose=$verbose, output_file='$output_file', Leftovers: $#" EOF chmod +x /tmp/demo-getopts.sh /tmp/demo-getopts.sh -vf /etc/hosts foo bar Output from copy-pasting the block above verbose=1, output_file='/etc/hosts', Leftovers: foo bar Usage demo-getopts.sh -vf /etc/hosts foo bar The advantages of getopts are: It's more portable, and will work in other shells like dash. It can handle multiple single options like -vf filename in the typical Unix way, automatically. The disadvantage of getopts is that it can only handle short options (-h, not --help) without additional code. There is a getopts tutorial which explains what all of the syntax and variables mean. In bash, there is also help getopts, which might be informative.
No answer showcases enhanced getopt. And the top-voted answer is misleading: It either ignores -vfd style short options (requested by the OP) or options after positional arguments (also requested by the OP); and it ignores parsing-errors. Instead: Use enhanced getopt from util-linux or formerly GNU glibc.1 It works with getopt_long() the C function of GNU glibc. no other solution on this page can do all this: handles spaces, quoting characters and even binary in arguments2 (non-enhanced getopt can’t do this) it can handle options at the end: script.sh -o outFile file1 file2 -v (getopts doesn’t do this) allows =-style long options: script.sh --outfile=fileOut --infile fileIn (allowing both is lengthy if self parsing) allows combined short options, e.g. -vfd (real work if self parsing) allows touching option-arguments, e.g. -oOutfile or -vfdoOutfile Is so old already3 that no GNU system is missing this (e.g. any Linux has it). You can test for its existence with: getopt --test → return value 4. Other getopt or shell-builtin getopts are of limited use. The following calls myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile all return verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile with the following myscript #!/bin/bash # More safety, by turning some bugs into errors. # Without `errexit` you don’t need ! and can replace # ${PIPESTATUS[0]} with a simple $?, but I prefer safety. set -o errexit -o pipefail -o noclobber -o nounset # -allow a command to fail with !’s side effect on errexit # -use return value from ${PIPESTATUS[0]}, because ! hosed $? ! getopt --test > /dev/null if [[ ${PIPESTATUS[0]} -ne 4 ]]; then echo 'I’m sorry, `getopt --test` failed in this environment.' exit 1 fi # option --output/-o requires 1 argument LONGOPTS=debug,force,output:,verbose OPTIONS=dfo:v # -regarding ! and PIPESTATUS see above # -temporarily store output to be able to check for errors # -activate quoting/enhanced mode (e.g. by writing out “--options”) # -pass arguments only via -- "$#" to separate them correctly ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$#") if [[ ${PIPESTATUS[0]} -ne 0 ]]; then # e.g. return value is 1 # then getopt has complained about wrong arguments to stdout exit 2 fi # read getopt’s output this way to handle the quoting right: eval set -- "$PARSED" d=n f=n v=n outFile=- # now enjoy the options in order and nicely split until we see -- while true; do case "$1" in -d|--debug) d=y shift ;; -f|--force) f=y shift ;; -v|--verbose) v=y shift ;; -o|--output) outFile="$2" shift 2 ;; --) shift break ;; *) echo "Programming error" exit 3 ;; esac done # handle non-option arguments if [[ $# -ne 1 ]]; then echo "$0: A single input file is required." exit 4 fi echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile" 1 enhanced getopt is available on most “bash-systems”, including Cygwin; on OS X try brew install gnu-getopt or sudo port install getopt 2 the POSIX exec() conventions have no reliable way to pass binary NULL in command line arguments; those bytes prematurely end the argument 3 first version released in 1997 or before (I only tracked it back to 1997)
deploy.sh #!/bin/bash while [[ "$#" -gt 0 ]]; do case $1 in -t|--target) target="$2"; shift ;; -u|--uglify) uglify=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done echo "Where to deploy: $target" echo "Should uglify : $uglify" Usage: ./deploy.sh -t dev -u # OR: ./deploy.sh --target dev --uglify
From digitalpeer.com with minor modifications: Usage myscript.sh -p=my_prefix -s=dirname -l=libname #!/bin/bash for i in "$#" do case $i in -p=*|--prefix=*) PREFIX="${i#*=}" ;; -s=*|--searchpath=*) SEARCHPATH="${i#*=}" ;; -l=*|--lib=*) DIR="${i#*=}" ;; --default) DEFAULT=YES ;; *) # unknown option ;; esac done echo PREFIX = ${PREFIX} echo SEARCH PATH = ${SEARCHPATH} echo DIRS = ${DIR} echo DEFAULT = ${DEFAULT} To better understand ${i#*=} search for "Substring Removal" in this guide. It is functionally equivalent to `sed 's/[^=]*=//' <<< "$i"` which calls a needless subprocess or `echo "$i" | sed 's/[^=]*=//'` which calls two needless subprocesses.
while [ "$#" -gt 0 ]; do case "$1" in -n) name="$2"; shift 2;; -p) pidfile="$2"; shift 2;; -l) logfile="$2"; shift 2;; --name=*) name="${1#*=}"; shift 1;; --pidfile=*) pidfile="${1#*=}"; shift 1;; --logfile=*) logfile="${1#*=}"; shift 1;; --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;; -*) echo "unknown option: $1" >&2; exit 1;; *) handle_argument "$1"; shift 1;; esac done This solution: handles -n arg and --name=arg allows arguments at the end shows sane errors if anything is misspelled compatible, doesn't use bashisms readable, doesn't require maintaining state in a loop
getopt()/getopts() is a good option. Copied from here: The simple use of "getopt" is shown in this mini-script: #!/bin/bash echo "Before getopt" for i do echo $i done args=`getopt abc:d $*` set -- $args echo "After getopt" for i do echo "-->$i" done What we have said is that any of -a, -b, -c or -d will be allowed, but that -c is followed by an argument (the "c:" says that). If we call this "g" and try it out: bash-2.05a$ ./g -abc foo Before getopt -abc foo After getopt -->-a -->-b -->-c -->foo -->-- We start with two arguments, and "getopt" breaks apart the options and puts each in its own argument. It also added "--".
I have found the matter to write portable parsing in scripts so frustrating that I have written Argbash - a FOSS code generator that can generate the arguments-parsing code for your script plus it has some nice features: https://argbash.io
I used the earlier answers as a starting point to tidy up my old adhoc param parsing. I then refactored out the following template code. It handles both long and short params, using = or space separated arguments, as well as multiple short params grouped together. Finally it re-inserts any non-param arguments back into the $1,$2.. variables. #!/usr/bin/env bash # NOTICE: Uncomment if your script depends on bashisms. #if [ -z "$BASH_VERSION" ]; then bash $0 $# ; exit $? ; fi echo "Before" for i ; do echo - $i ; done # Code template for parsing command line parameters using only portable shell # code, while handling both long and short params, handling '-f file' and # '-f=file' style param data and also capturing non-parameters to be inserted # back into the shell positional parameters. while [ -n "$1" ]; do # Copy so we can modify it (can't modify $1) OPT="$1" # Detect argument termination if [ x"$OPT" = x"--" ]; then shift for OPT ; do REMAINS="$REMAINS \"$OPT\"" done break fi # Parse current opt while [ x"$OPT" != x"-" ] ; do case "$OPT" in # Handle --flag=value opts like this -c=* | --config=* ) CONFIGFILE="${OPT#*=}" shift ;; # and --flag value opts like this -c* | --config ) CONFIGFILE="$2" shift ;; -f* | --force ) FORCE=true ;; -r* | --retry ) RETRY=true ;; # Anything unknown is recorded for later * ) REMAINS="$REMAINS \"$OPT\"" break ;; esac # Check for multiple short options # NOTICE: be sure to update this pattern to match valid options NEXTOPT="${OPT#-[cfr]}" # try removing single short opt if [ x"$OPT" != x"$NEXTOPT" ] ; then OPT="-$NEXTOPT" # multiple short opts, keep going else break # long form, exit inner loop fi done # Done with that param. move to next shift done # Set the non-parameters back into the positional parameters ($1 $2 ..) eval set -- $REMAINS echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'" for i ; do echo - $i ; done
# As long as there is at least one more argument, keep looping while [[ $# -gt 0 ]]; do key="$1" case "$key" in # This is a flag type option. Will catch either -f or --foo -f|--foo) FOO=1 ;; # Also a flag type option. Will catch either -b or --bar -b|--bar) BAR=1 ;; # This is an arg value type option. Will catch -o value or --output-file value -o|--output-file) shift # past the key and to the value OUTPUTFILE="$1" ;; # This is an arg=value type option. Will catch -o=value or --output-file=value -o=*|--output-file=*) # No need to shift here since the value is part of the same string OUTPUTFILE="${key#*=}" ;; *) # Do whatever you want with extra options echo "Unknown option '$key'" ;; esac # Shift after checking all the cases to get the next option shift done This allows you to have both space separated options/values, as well as equal defined values. So you could run your script using: ./myscript --foo -b -o /fizz/file.txt as well as: ./myscript -f --bar -o=/fizz/file.txt and both should have the same end result. PROS: Allows for both -arg=value and -arg value Works with any arg name that you can use in bash Meaning -a or -arg or --arg or -a-r-g or whatever Pure bash. No need to learn/use getopt or getopts CONS: Can't combine args Meaning no -abc. You must do -a -b -c
This example shows how to use getopt and eval and HEREDOC and shift to handle short and long parameters with and without a required value that follows. Also the switch/case statement is concise and easy to follow. #!/usr/bin/env bash # usage function function usage() { cat << HEREDOC Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run] optional arguments: -h, --help show this help message and exit -n, --num NUM pass in a number -t, --time TIME_STR pass in a time string -v, --verbose increase the verbosity of the bash script --dry-run do a dry run, dont change any files HEREDOC } # initialize variables progname=$(basename $0) verbose=0 dryrun=0 num_str= time_str= # use getopt and store the output into $OPTS # note the use of -o for the short options, --long for the long name options # and a : for any option that takes a parameter OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$#") if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi eval set -- "$OPTS" while true; do # uncomment the next line to see how shift is working # echo "\$1:\"$1\" \$2:\"$2\"" case "$1" in -h | --help ) usage; exit; ;; -n | --num ) num_str="$2"; shift 2 ;; -t | --time ) time_str="$2"; shift 2 ;; --dry-run ) dryrun=1; shift ;; -v | --verbose ) verbose=$((verbose + 1)); shift ;; -- ) shift; break ;; * ) break ;; esac done if (( $verbose > 0 )); then # print out all the parameters we read in cat <<EOM num=$num_str time=$time_str verbose=$verbose dryrun=$dryrun EOM fi # The rest of your script below The most significant lines of the script above are these: OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$#") if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi eval set -- "$OPTS" while true; do case "$1" in -h | --help ) usage; exit; ;; -n | --num ) num_str="$2"; shift 2 ;; -t | --time ) time_str="$2"; shift 2 ;; --dry-run ) dryrun=1; shift ;; -v | --verbose ) verbose=$((verbose + 1)); shift ;; -- ) shift; break ;; * ) break ;; esac done Short, to the point, readable, and handles just about everything (IMHO). Hope that helps someone.
Expanding on #bruno-bronosky's answer, I added a "preprocessor" to handle some common formatting: Expands --longopt=val into --longopt val Expands -xyz into -x -y -z Supports -- to indicate the end of flags Shows an error for unexpected options Compact and easy-to-read options switch #!/bin/bash # Report usage usage() { echo "Usage:" echo "$(basename "$0") [options] [--] [file1, ...]" } invalid() { echo "ERROR: Unrecognized argument: $1" >&2 usage exit 1 } # Pre-process options to: # - expand -xyz into -x -y -z # - expand --longopt=arg into --longopt arg ARGV=() END_OF_OPT= while [[ $# -gt 0 ]]; do arg="$1"; shift case "${END_OF_OPT}${arg}" in --) ARGV+=("$arg"); END_OF_OPT=1 ;; --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;; --*) ARGV+=("$arg") ;; -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;; *) ARGV+=("$arg") ;; esac done # Apply pre-processed options set -- "${ARGV[#]}" # Parse options END_OF_OPT= POSITIONAL=() while [[ $# -gt 0 ]]; do case "${END_OF_OPT}${1}" in -h|--help) usage; exit 0 ;; -p|--password) shift; PASSWORD="$1" ;; -u|--username) shift; USERNAME="$1" ;; -n|--name) shift; names+=("$1") ;; -q|--quiet) QUIET=1 ;; -C|--copy) COPY=1 ;; -N|--notify) NOTIFY=1 ;; --stdin) READ_STDIN=1 ;; --) END_OF_OPT=1 ;; -*) invalid "$1" ;; *) POSITIONAL+=("$1") ;; esac shift done # Restore positional parameters set -- "${POSITIONAL[#]}"
ASAP: Another Shell Argument Parser Edit note: version 2.0, now with pure POSIX shell code and zero witchery! TL;DR This parser uses only POSIX compliant shell code to process options in these formats: -o [ARG], -abo [ARG], --opt [ARG] or --opt=[ARG], where ARG is an optional argument. It can handle intermixed options and arguments, and also "--" to force any argument after it to be treated as positional. Here is a minimal version that works as long as the command is correct, i.e. it doesn't perform almost any checks. You can paste it at the top of your sh script —it won't work as a function— and substitute your option definitions. #!/bin/sh -e USAGE="Usage: ${CMD=${0##*/}} [(-v|--verbose)] [--name=TEXT] [(-o|--output) FILE] [ARGS...]" exit2 () { printf >&2 "%s: %s: '%s'\n%s\n" "$CMD" "$1" "$2" "$USAGE"; exit 2; } check () { { [ "$1" != "$EOL" ] && [ "$1" != '--' ]; } || exit2 "missing argument" "$2"; } # avoid infinite loop # parse command-line options set -- "$#" "${EOL:=$(printf '\1\3\3\7')}" # end-of-list marker while [ "$1" != "$EOL" ]; do opt="$1"; shift case "$opt" in #EDIT HERE: defined options --name ) check "$1" "$opt"; opt_name="$1"; shift;; -o | --output ) check "$1" "$opt"; opt_output="$1"; shift;; -v | --verbose ) opt_verbose='true';; -h | --help ) printf "%s\n" "$USAGE"; exit 0;; # process special cases --) while [ "$1" != "$EOL" ]; do set -- "$#" "$1"; shift; done;; # parse remaining as positional --[!=]*=*) set -- "${opt%%=*}" "${opt#*=}" "$#";; # "--opt=arg" -> "--opt" "arg" -[A-Za-z0-9] | -*[!A-Za-z0-9]*) exit2 "invalid option" "$opt";; # anything invalid like '-*' -?*) other="${opt#-?}"; set -- "${opt%$other}" "-${other}" "$#";; # "-abc" -> "-a" "-bc" *) set -- "$#" "$opt";; # positional, rotate to the end esac done; shift # $EOL unset CMD EOL USAGE opt other unset -f check exit2 printf "name = '%s'\noutput = '%s'\nverbose = '%s'\n\$# = (%s)\n" "$opt_name" "$opt_output" "$opt_verbose" "$*" Sample outputs $ ./asap-example.sh -vo path/to/camelot 'spam?' --name=Arthur 'spam!' -- +42 -17 name = 'Arthur' output = 'path/to/camelot' verbose = 'true' $# = (spam? spam! +42 -17) $ ./asap-example.sh -name Lancelot eggs bacon asap-example.sh: invalid option: '-n' Usage: asap-example.sh [(-v|--verbose)] [--name=TEXT] [(-o|--output) FILE] [ARG...] Description I was inspired by the relatively simple answer by #bronson and tempted to try to improve it (without adding too much complexity). This parser implementation uses pattern matching, parameter expansion and the shell's own positional parameters as a rotating stack to loop over and process arguments. Here's the result: Any of the -o [ARG], -abo [ARG], --long-option [ARG] and --long-option=[ARG] styles of options are accepted; Arguments may occur in any order, only positional ones are left in $# after the loop; Use -- to force remaining arguments to be treated as positional; Portable, compact, quite readable, with independent features; Doesn't depend on getopt(s) or external utilities; Detects invalid options and missing arguments. Portability This code was tested and verified to work with a reasonably recent version of: BusyBox's ash, bash, dash, mksh, ksh93, yash and zsh (called with their standard executable path, not as /bin/sh). PS: I know... An argument with the binary value 0x01030307 could break the logic. But, if anyone passes such an argument in a command-line, they deserve it.
If you are making scripts that are interchangeable with other utilities, below flexibility may be useful. Either: command -x=myfilename.ext --another_switch Or: command -x myfilename.ext --another_switch Here is the code: STD_IN=0 prefix="" key="" value="" for keyValue in "$#" do case "${prefix}${keyValue}" in -i=*|--input_filename=*) key="-i"; value="${keyValue#*=}";; -ss=*|--seek_from=*) key="-ss"; value="${keyValue#*=}";; -t=*|--play_seconds=*) key="-t"; value="${keyValue#*=}";; -|--stdin) key="-"; value=1;; *) value=$keyValue;; esac case $key in -i) MOVIE=$(resolveMovie "${value}"); prefix=""; key="";; -ss) SEEK_FROM="${value}"; prefix=""; key="";; -t) PLAY_SECONDS="${value}"; prefix=""; key="";; -) STD_IN=${value}; prefix=""; key="";; *) prefix="${keyValue}=";; esac done
I think this one is simple enough to use: #!/bin/bash # readopt='getopts $opts opt;rc=$?;[ "$rc$opt" = "0?" ]&&exit 1;[ $rc = 0 ]||{ shift $[OPTIND-1];false; }' opts=vfdo: # Enumerating options while eval "$readopt" do echo OPT:$opt ${OPTARG+OPTARG:$OPTARG} done # Enumerating arguments for arg do echo ARG:$arg done Invocation example: ./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile OPT:v OPT:d OPT:o OPTARG:/fizz/someOtherFile OPT:f ARG:./foo/bar/someFile
I give you The Function parse_params that will parse params from the command line. It is a pure Bash solution, no additional utilities. Does not pollute global scope. Effortlessly returns you simple to use variables, that you could build further logic on. Amount of dashes before params does not matter (--all equals -all equals all=all) The script below is a copy-paste working demonstration. See show_use function to understand how to use parse_params. Limitations: Does not support space delimited params (-d 1) Param names will lose dashes so --any-param and -anyparam are equivalent eval $(parse_params "$#") must be used inside bash function (it will not work in the global scope) #!/bin/bash # Universal Bash parameter parsing # Parse equal sign separated params into named local variables # Standalone named parameter value will equal its param name (--force creates variable $force=="force") # Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array) # Puts un-named params as-is into ${ARGV[*]} array # Additionally puts all named params as-is into ${ARGN[*]} array # Additionally puts all standalone "option" params as-is into ${ARGO[*]} array # #author Oleksii Chekulaiev # #version v1.4.1 (Jul-27-2018) parse_params () { local existing_named local ARGV=() # un-named params local ARGN=() # named params local ARGO=() # options (--params) echo "local ARGV=(); local ARGN=(); local ARGO=();" while [[ "$1" != "" ]]; do # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage _escaped=${1/\*/\'\"*\"\'} _escaped=${_escaped//\'/\\\'} _escaped=${_escaped//\"/\\\"} # If equals delimited named parameter nonspace="[^[:space:]]" if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then # Add to named parameters array echo "ARGN+=('$_escaped');" # key is part before first = local _key=$(echo "$1" | cut -d = -f 1) # Just add as non-named when key is empty or contains space if [[ "$_key" == "" || "$_key" =~ " " ]]; then echo "ARGV+=('$_escaped');" shift continue fi # val is everything after key and = (protect from param==value error) local _val="${1/$_key=}" # remove dashes from key name _key=${_key//\-} # skip when key is empty # search for existing parameter name if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then # if name already exists then it's a multi-value named parameter # re-declare it as an array if needed if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then echo "$_key=(\"\$$_key\");" fi # append new value echo "$_key+=('$_val');" else # single-value named parameter echo "local $_key='$_val';" existing_named=" $_key" fi # If standalone named parameter elif [[ "$1" =~ ^\-${nonspace}+ ]]; then # remove dashes local _key=${1//\-} # Just add as non-named when key is empty or contains space if [[ "$_key" == "" || "$_key" =~ " " ]]; then echo "ARGV+=('$_escaped');" shift continue fi # Add to options array echo "ARGO+=('$_escaped');" echo "local $_key=\"$_key\";" # non-named parameter else # Escape asterisk to prevent bash asterisk expansion _escaped=${1/\*/\'\"*\"\'} echo "ARGV+=('$_escaped');" fi shift done } #--------------------------- DEMO OF THE USAGE ------------------------------- show_use () { eval $(parse_params "$#") # -- echo "${ARGV[0]}" # print first unnamed param echo "${ARGV[1]}" # print second unnamed param echo "${ARGN[0]}" # print first named param echo "${ARG0[0]}" # print first option param (--force) echo "$anyparam" # print --anyparam value echo "$k" # print k=5 value echo "${multivalue[0]}" # print first value of multi-value echo "${multivalue[1]}" # print second value of multi-value [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you" } show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
getopts works great if #1 you have it installed and #2 you intend to run it on the same platform. OSX and Linux (for example) behave differently in this respect. Here is a (non getopts) solution that supports equals, non-equals, and boolean flags. For example you could run your script in this way: ./script --arg1=value1 --arg2 value2 --shouldClean # parse the arguments. COUNTER=0 ARGS=("$#") while [ $COUNTER -lt $# ] do arg=${ARGS[$COUNTER]} let COUNTER=COUNTER+1 nextArg=${ARGS[$COUNTER]} if [[ $skipNext -eq 1 ]]; then echo "Skipping" skipNext=0 continue fi argKey="" argVal="" if [[ "$arg" =~ ^\- ]]; then # if the format is: -key=value if [[ "$arg" =~ \= ]]; then argVal=$(echo "$arg" | cut -d'=' -f2) argKey=$(echo "$arg" | cut -d'=' -f1) skipNext=0 # if the format is: -key value elif [[ ! "$nextArg" =~ ^\- ]]; then argKey="$arg" argVal="$nextArg" skipNext=1 # if the format is: -key (a boolean flag) elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then argKey="$arg" argVal="" skipNext=0 fi # if the format has not flag, just a value. else argKey="" argVal="$arg" skipNext=0 fi case "$argKey" in --source-scmurl) SOURCE_URL="$argVal" ;; --dest-scmurl) DEST_URL="$argVal" ;; --version-num) VERSION_NUM="$argVal" ;; -c|--clean) CLEAN_BEFORE_START="1" ;; -h|--help|-help|--h) showUsage exit ;; esac done
Yet another option parser (generator) An elegant option parser for shell scripts (full support for all POSIX shells) https://github.com/ko1nksm/getoptions (Update: v3.3.0 released on 2021-05-02) getoptions is a new option parser (generator) written in POSIX-compliant shell script and released in august 2020. It is for those who want to support the POSIX / GNU style option syntax in your shell scripts. The supported syntaxes are -a, +a, -abc, -vvv, -p VALUE, -pVALUE, --flag, --no-flag, --with-flag, --without-flag, --param VALUE, --param=VALUE, --option[=VALUE], --no-option --. It supports subcommands, validation, abbreviated options, and automatic help generation. And works with all POSIX shells (dash 0.5.4+, bash 2.03+, ksh88+, mksh R28+, zsh 3.1.9+, yash 2.29+, busybox ash 1.1.3+, etc). #!/bin/sh VERSION="0.1" parser_definition() { setup REST help:usage -- "Usage: example.sh [options]... [arguments]..." '' msg -- 'Options:' flag FLAG -f --flag -- "takes no arguments" param PARAM -p --param -- "takes one argument" option OPTION -o --option on:"default" -- "takes one optional argument" disp :usage -h --help disp VERSION --version } eval "$(getoptions parser_definition) exit 1" echo "FLAG: $FLAG, PARAM: $PARAM, OPTION: $OPTION" printf '%s\n' "$#" # rest arguments It's parses the following arguments: example.sh -f --flag -p VALUE --param VALUE -o --option -oVALUE --option=VALUE 1 2 3 And automatic help generation. $ example.sh --help Usage: example.sh [options]... [arguments]... Options: -f, --flag takes no arguments -p, --param PARAM takes one argument -o, --option[=OPTION] takes one optional argument -h, --help --version It is also an option parser generator, generates the following simple option parsing code. If you use the generated code, you won't need getoptions. Achieve true portability and zero dependency. FLAG='' PARAM='' OPTION='' REST='' getoptions_parse() { OPTIND=$(($#+1)) while OPTARG= && [ $# -gt 0 ]; do case $1 in --?*=*) OPTARG=$1; shift eval 'set -- "${OPTARG%%\=*}" "${OPTARG#*\=}"' ${1+'"$#"'} ;; --no-*|--without-*) unset OPTARG ;; -[po]?*) OPTARG=$1; shift eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$#"'} ;; -[fh]?*) OPTARG=$1; shift eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$#"'} OPTARG= ;; esac case $1 in '-f'|'--flag') [ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG='' FLAG="$OPTARG" ;; '-p'|'--param') [ $# -le 1 ] && set "required" "$1" && break OPTARG=$2 PARAM="$OPTARG" shift ;; '-o'|'--option') set -- "$1" "$#" [ ${OPTARG+x} ] && { case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac [ "${OPTARG:-}" ] && { shift; OPTARG=$2; } || OPTARG='default' } || OPTARG='' OPTION="$OPTARG" shift ;; '-h'|'--help') usage exit 0 ;; '--version') echo "${VERSION}" exit 0 ;; --) shift while [ $# -gt 0 ]; do REST="${REST} \"\${$(($OPTIND-$#))}\"" shift done break ;; [-]?*) set "unknown" "$1"; break ;; *) REST="${REST} \"\${$(($OPTIND-$#))}\"" esac shift done [ $# -eq 0 ] && { OPTIND=1; unset OPTARG; return 0; } case $1 in unknown) set "Unrecognized option: $2" "$#" ;; noarg) set "Does not allow an argument: $2" "$#" ;; required) set "Requires an argument: $2" "$#" ;; pattern:*) set "Does not match the pattern (${1#*:}): $2" "$#" ;; notcmd) set "Not a command: $2" "$#" ;; *) set "Validation error ($1): $2" "$#" esac echo "$1" >&2 exit 1 } usage() { cat<<'GETOPTIONSHERE' Usage: example.sh [options]... [arguments]... Options: -f, --flag takes no arguments -p, --param PARAM takes one argument -o, --option[=OPTION] takes one optional argument -h, --help --version GETOPTIONSHERE }
I wanna submit my project : https://github.com/flyingangel/argparser source argparser.sh parse_args "$#" Simple as that. The environment will be populated with variables with the same name as the arguments
This is how I do in a function to avoid breaking getopts run at the same time somewhere higher in stack: function waitForWeb () { local OPTIND=1 OPTARG OPTION local host=localhost port=8080 proto=http while getopts "h:p:r:" OPTION; do case "$OPTION" in h) host="$OPTARG" ;; p) port="$OPTARG" ;; r) proto="$OPTARG" ;; esac done ... }
I'd like to offer my version of option parsing, that allows for the following: -s p1 --stage p1 -w somefolder --workfolder somefolder -sw p1 somefolder -e=hello Also allows for this (could be unwanted): -s--workfolder p1 somefolder -se=hello p1 -swe=hello p1 somefolder You have to decide before use if = is to be used on an option or not. This is to keep the code clean(ish). while [[ $# > 0 ]] do key="$1" while [[ ${key+x} ]] do case $key in -s*|--stage) STAGE="$2" shift # option has parameter ;; -w*|--workfolder) workfolder="$2" shift # option has parameter ;; -e=*) EXAMPLE="${key#*=}" break # option has been fully handled ;; *) # unknown option echo Unknown option: $key #1>&2 exit 10 # either this: my preferred way to handle unknown options break # or this: do this to signal the option has been handled (if exit isn't used) ;; esac # prepare for next option in this key, if any [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}" done shift # option(s) fully processed, proceed to next input argument done
There are several ways to parse cmdline args (e.g. GNU getopt (not portable) vs BSD (MacOS) getopt vs getopts) - all problematic. This solution is portable! has zero dependencies, only relies on bash built-ins allows for both short and long options handles whitespace or simultaneously the use of = separator between option and argument supports concatenated short option style -vxf handles option with optional arguments (E.g. --color vs --color=always), correctly detects and reports unknown options supports -- to signal end of options, and doesn't require code bloat compared with alternatives for the same feature set. I.e. succinct, and therefore easier to maintain Examples: Any of # flag -f --foo # option with required argument -b"Hello World" -b "Hello World" --bar "Hello World" --bar="Hello World" # option with optional argument --baz --baz="Optional Hello" #!/usr/bin/env bash usage() { cat - >&2 <<EOF NAME program-name.sh - Brief description SYNOPSIS program-name.sh [-h|--help] program-name.sh [-f|--foo] [-b|--bar <arg>] [--baz[=<arg>]] [--] FILE ... REQUIRED ARGUMENTS FILE ... input files OPTIONS -h, --help Prints this and exits -f, --foo A flag option -b, --bar <arg> Option requiring an argument <arg> --baz[=<arg>] Option that has an optional argument <arg>. If <arg> is not specified, defaults to 'DEFAULT' -- Specify end of options; useful if the first non option argument starts with a hyphen EOF } fatal() { for i; do echo -e "${i}" >&2 done exit 1 } # For long option processing next_arg() { if [[ $OPTARG == *=* ]]; then # for cases like '--opt=arg' OPTARG="${OPTARG#*=}" else # for cases like '--opt arg' OPTARG="${args[$OPTIND]}" OPTIND=$((OPTIND + 1)) fi } # ':' means preceding option character expects one argument, except # first ':' which make getopts run in silent mode. We handle errors with # wildcard case catch. Long options are considered as the '-' character optspec=":hfb:-:" args=("" "$#") # dummy first element so $1 and $args[1] are aligned while getopts "$optspec" optchar; do case "$optchar" in h) usage; exit 0 ;; f) foo=1 ;; b) bar="$OPTARG" ;; -) # long option processing case "$OPTARG" in help) usage; exit 0 ;; foo) foo=1 ;; bar|bar=*) next_arg bar="$OPTARG" ;; baz) baz=DEFAULT ;; baz=*) next_arg baz="$OPTARG" ;; -) break ;; *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;; esac ;; *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;; esac done shift $((OPTIND-1)) if [ "$#" -eq 0 ]; then fatal "Expected at least one required argument FILE" \ "See '${0} --help' for usage" fi echo "foo=$foo, bar=$bar, baz=$baz, files=${#}"
Solution that preserves unhandled arguments. Demos Included. Here is my solution. It is VERY flexible and unlike others, shouldn't require external packages and handles leftover arguments cleanly. Usage is: ./myscript -flag flagvariable -otherflag flagvar2 All you have to do is edit the validflags line. It prepends a hyphen and searches all arguments. It then defines the next argument as the flag name e.g. ./myscript -flag flagvariable -otherflag flagvar2 echo $flag $otherflag flagvariable flagvar2 The main code (short version, verbose with examples further down, also a version with erroring out): #!/usr/bin/env bash #shebang.io validflags="rate time number" count=1 for arg in $# do match=0 argval=$1 for flag in $validflags do sflag="-"$flag if [ "$argval" == "$sflag" ] then declare $flag=$2 match=1 fi done if [ "$match" == "1" ] then shift 2 else leftovers=$(echo $leftovers $argval) shift fi count=$(($count+1)) done #Cleanup then restore the leftovers shift $# set -- $leftovers The verbose version with built in echo demos: #!/usr/bin/env bash #shebang.io rate=30 time=30 number=30 echo "all args $#" validflags="rate time number" count=1 for arg in $# do match=0 argval=$1 # argval=$(echo $# | cut -d ' ' -f$count) for flag in $validflags do sflag="-"$flag if [ "$argval" == "$sflag" ] then declare $flag=$2 match=1 fi done if [ "$match" == "1" ] then shift 2 else leftovers=$(echo $leftovers $argval) shift fi count=$(($count+1)) done #Cleanup then restore the leftovers echo "pre final clear args: $#" shift $# echo "post final clear args: $#" set -- $leftovers echo "all post set args: $#" echo arg1: $1 arg2: $2 echo leftovers: $leftovers echo rate $rate time $time number $number Final one, this one errors out if an invalid -argument is passed through. #!/usr/bin/env bash #shebang.io rate=30 time=30 number=30 validflags="rate time number" count=1 for arg in $# do argval=$1 match=0 if [ "${argval:0:1}" == "-" ] then for flag in $validflags do sflag="-"$flag if [ "$argval" == "$sflag" ] then declare $flag=$2 match=1 fi done if [ "$match" == "0" ] then echo "Bad argument: $argval" exit 1 fi shift 2 else leftovers=$(echo $leftovers $argval) shift fi count=$(($count+1)) done #Cleanup then restore the leftovers shift $# set -- $leftovers echo rate $rate time $time number $number echo leftovers: $leftovers Pros: What it does, it handles very well. It preserves unused arguments which a lot of the other solutions here don't. It also allows for variables to be called without being defined by hand in the script. It also allows prepopulation of variables if no corresponding argument is given. (See verbose example). Cons: Can't parse a single complex arg string e.g. -xcvf would process as a single argument. You could somewhat easily write additional code into mine that adds this functionality though.
Based on other answers here, this my version: #!/bin/bash set -e function parse() { for arg in "$#"; do # transform long options to short ones shift case "$arg" in "--name") set -- "$#" "-n" ;; "--verbose") set -- "$#" "-v" ;; *) set -- "$#" "$arg" esac done while getopts "n:v" optname # left to ":" are flags that expect a value, right to the ":" are flags that expect nothing do case "$optname" in "n") name=${OPTARG} ;; "v") verbose=true ;; esac done shift "$((OPTIND-1))" # shift out all the already processed options } parse "$#" echo "hello $name" if [ ! -z $verbose ]; then echo 'nice to meet you!'; fi Usage: $ ./parse.sh hello $ ./parse.sh -n YOUR_NAME hello YOUR_NAME $ ./parse.sh -n YOUR_NAME -v hello YOUR_NAME nice to meet you! $ ./parse.sh -v -n YOUR_NAME hello YOUR_NAME nice to meet you! $ ./parse.sh -v hello nice to meet you!
Here is my approach - using regexp. no getopts it handles block of short parameters -qwerty it handles short parameters -q -w -e it handles long options --qwerty you can pass attribute to short or long option (if you are using block of short options, attribute is attached to the last option) you can use spaces or = to provide attributes, but attribute matches until encountering hyphen+space "delimiter", so in --q=qwe ty qwe ty is one attribute it handles mix of all above so -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ribute is valid script: #!/usr/bin/env sh help_menu() { echo "Usage: ${0##*/} [-h][-l FILENAME][-d] Options: -h, --help display this help and exit -l, --logfile=FILENAME filename -d, --debug enable debug " } parse_options() { case $opt in h|help) help_menu exit ;; l|logfile) logfile=${attr} ;; d|debug) debug=true ;; *) echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2 exit 1 esac } options=$# until [ "$options" = "" ]; do if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute] opt=${BASH_REMATCH[3]} attr=${BASH_REMATCH[7]} options=${BASH_REMATCH[9]} elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute] pile=${BASH_REMATCH[4]} while (( ${#pile} > 1 )); do opt=${pile:0:1} attr="" pile=${pile/${pile:0:1}/} parse_options done opt=$pile attr=${BASH_REMATCH[7]} options=${BASH_REMATCH[9]} else # leftovers that don't match opt=${BASH_REMATCH[10]} options="" fi parse_options fi done
Mixing positional and flag-based arguments --param=arg (equals delimited) Freely mixing flags between positional arguments: ./script.sh dumbo 127.0.0.1 --environment=production -q -d ./script.sh dumbo --environment=production 127.0.0.1 --quiet -d can be accomplished with a fairly concise approach: # process flags pointer=1 while [[ $pointer -le $# ]]; do param=${!pointer} if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer else case $param in # paramter-flags with arguments -e=*|--environment=*) environment="${param#*=}";; --another=*) another="${param#*=}";; # binary flags -q|--quiet) quiet=true;; -d) debug=true;; esac # splice out pointer frame from positional list [[ $pointer -gt 1 ]] \ && set -- ${#:1:((pointer - 1))} ${#:((pointer + 1)):$#} \ || set -- ${#:((pointer + 1)):$#}; fi done # positional remain node_name=$1 ip_address=$2 --param arg (space delimited) It's usualy clearer to not mix --flag=value and --flag value styles. ./script.sh dumbo 127.0.0.1 --environment production -q -d This is a little dicey to read, but is still valid ./script.sh dumbo --environment production 127.0.0.1 --quiet -d Source # process flags pointer=1 while [[ $pointer -le $# ]]; do if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer else param=${!pointer} ((pointer_plus = pointer + 1)) slice_len=1 case $param in # paramter-flags with arguments -e|--environment) environment=${!pointer_plus}; ((slice_len++));; --another) another=${!pointer_plus}; ((slice_len++));; # binary flags -q|--quiet) quiet=true;; -d) debug=true;; esac # splice out pointer frame from positional list [[ $pointer -gt 1 ]] \ && set -- ${#:1:((pointer - 1))} ${#:((pointer + $slice_len)):$#} \ || set -- ${#:((pointer + $slice_len)):$#}; fi done # positional remain node_name=$1 ip_address=$2
Note that getopt(1) was a short living mistake from AT&T. getopt was created in 1984 but already buried in 1986 because it was not really usable. A proof for the fact that getopt is very outdated is that the getopt(1) man page still mentions "$*" instead of "$#", that was added to the Bourne Shell in 1986 together with the getopts(1) shell builtin in order to deal with arguments with spaces inside. BTW: if you are interested in parsing long options in shell scripts, it may be of interest to know that the getopt(3) implementation from libc (Solaris) and ksh93 both added a uniform long option implementation that supports long options as aliases for short options. This causes ksh93 and the Bourne Shell to implement a uniform interface for long options via getopts. An example for long options taken from the Bourne Shell man page: getopts "f:(file)(input-file)o:(output-file)" OPTX "$#" shows how long option aliases may be used in both Bourne Shell and ksh93. See the man page of a recent Bourne Shell: http://schillix.sourceforge.net/man/man1/bosh.1.html and the man page for getopt(3) from OpenSolaris: http://schillix.sourceforge.net/man/man3c/getopt.3c.html and last, the getopt(1) man page to verify the outdated $*: http://schillix.sourceforge.net/man/man1/getopt.1.html
I have write a bash helper to write a nice bash tool project home: https://gitlab.mbedsys.org/mbedsys/bashopts example: #!/bin/bash -ei # load the library . bashopts.sh # Enable backtrace dusplay on error trap 'bashopts_exit_handle' ERR # Initialize the library bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc" # Declare the options bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name" bashopts_declare -n age -l number -d "Age" -t number bashopts_declare -n email_list -t string -m add -l email -d "Email adress" # Parse arguments bashopts_parse_args "$#" # Process argument bashopts_process_args will give help: NAME: ./example.sh - This is myapp tool description displayed on help message USAGE: [options and commands] [-- [extra args]] OPTIONS: -h,--help Display this help -n,--non-interactive true Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false) -f,--first "John" First name - [$first_name] (type:string, default:"") -l,--last "Smith" Last name - [$last_name] (type:string, default:"") --display-name "John Smith" Display name - [$display_name] (type:string, default:"$first_name $last_name") --number 0 Age - [$age] (type:number, default:0) --email Email adress - [$email_list] (type:string, default:"") enjoy :)
Assume we create a shell script named test_args.sh as follow #!/bin/sh until [ $# -eq 0 ] do name=${1:1}; shift; if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi done echo "year=$year month=$month day=$day flag=$flag" After we run the following command: sh test_args.sh -year 2017 -flag -month 12 -day 22 The output would be: year=2017 month=12 day=22 flag=true
Here is a getopts that achieves the parsing with minimal code and allows you to define what you wish to extract in one case using eval with substring. Basically eval "local key='val'" function myrsync() { local backup=("${#}") args=(); while [[ $# -gt 0 ]]; do k="$1"; case "$k" in ---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include) eval "local ${k:3}='${2}'"; shift; shift # Past two arguments ;; *) # Unknown option args+=("$1"); shift; # Past argument only ;; esac done; set -- "${backup[#]}" # Restore $# echo "${sourceurl}" } Declares the variables as locals instead of globals as most answers here. Called as: myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ... The ${k:3} is basically a substring to remove the first --- from the key.
I wanted to share what I made for parsing options. Some of my needs were not fulfilled by the answers here so I had to come up with this: https://github.com/MihirLuthra/bash_option_parser This supports: Suboption parsing Alias names for options Optional args Variable args Printing usage and errors Let's say we have a command named fruit with usage as follows: fruit <fruit-name> ... [-e|—-eat|—-chew] [-c|--cut <how> <why>] <command> [<args>] -e takes no args -c takes two args i.e. how to cut and why to cut fruit itself takes at least one argument. <command> is for suboptions like apple, orange etc. (similar to git which has suboptions commit, push etc. ) So to parse it: parse_options \ 'fruit' '1 ...' \ '-e' , '--eat' , '--chew' '0' \ '-c' , '--cut' '1 1' \ 'apple' 'S' \ 'orange' 'S' \ ';' \ "$#" Now if there was any usage error, it can be printed using option_parser_error_msg as follows: retval=$? if [ $retval -ne 0 ]; then # this will manage error messages if # insufficient or extra args are supplied option_parser_error_msg "$retval" # This will print the usage print_usage 'fruit' exit 1 fi To check now if some options was passed, if [ -n "${OPTIONS[-c]}" ] then echo "-c was passed" # args can be accessed in a 2D-array-like format echo "Arg1 to -c = ${ARGS[-c,0]}" echo "Arg2 to -c = ${ARGS[-c,1]}" fi Suboption parsing can also be done by passing $shift_count to parse_options_detailed which makes it start parsing after shifting args to reach args of suboption. It is demonstrated in this example. A detailed description is provided in the readme and examples in the repository.
not allow multiple option in getopts of bash script
Having bash script as follows #! /bin/bash usage() { echo -e "need help!" } while getopts ":a:b:h" OPTION do case $OPTION in a) printf "a option with value %s\n" $OPTARG ;; b) printf "b option with value %s\n" $OPTARG ;; h) usage ;; ?) echo -e "No option selected" ;; esac done exit 0 Above script run fine with different option but i wanted to extend it to not allow to pass multiple option at same time like as following argument $ ./test.bash -a 1 -b 2 a option with value 1 b option with value 2 should be not valid means some way it give me error like wrong syntax i achieved it by as follows but it seems to long it is as follow #! /bin/bash usage() { echo -e "need help!" } let "a_count=0" let "b_count=0" MY_ARG="" while getopts ":a:b:h" OPTION do case $OPTION in a) let a_count=1 MY_ARG=$OPTARG ;; b) let b_count=1 MY_ARG=$OPTARG ;; h) usage ;; ?) echo -e "No option selected" ;; esac done [[ $a_count -eq 1 ]] && [[ $b_count -eq 1 ]] && echo "wrong command sytax" && exit 0 [[ $a_count -eq 1 ]] && printf "a option with value %s\n" $MY_ARG [[ $b_count -eq 1 ]] && printf "b option with value %s\n" $MY_ARG exit 0 run like $ ./test.bash -a 1 -b 2 wrong command sytax But i want to finish validation in while..loop of getopts. Also this validation not works for following command ./test.bash -a -b a option with value -b any one have batter idea how to use getopts for this type validation?
you've almost got it. The : after the a and after the b say that they take an argument, so your example with -a -b is actually valid, saying "There is option a with value -b". If you really just want "-a or -b and then an argument", you probably don't need getopts at all, but should do: [ "$1" == "-a" ] && printf "a option with value %s\n" $2 [ "$1" == "-b" ] && printf "b option with value %s\n" $2
any one have batter idea how to use getopts for this type validation? well, actually, you're explicitly telling geptopts that -a and -b are not boolean parameters, but parameters that take an extra argument. The argument parser cannot tell whether the argument following -a is a parameter or its own argument, and thus they consider [-a ] [-b ] as syntax. The best way, would actually be to have a different boolean parameter that matches the use case when you do not want an argument for -a and -b. Though, it won't help you with your issue trying to have parameters with argument or boolean, but for the argument syntax checking you can try docopt which has a nicer way to create command line interface. You focus on doing the --help documentation, it parses it to build your parameter/argument parser. e.g.: eval "$(docopts -V - -h - : "$#" <<EOF Usage: myscript [(-a <foo> | -b <bar> | -abool | -bbool)] -a <foo> The A option. -b <bar> The B option. -abool The A bool -bbool The B bool --help Show help options. --version Print program version. ---- myscript 0.0.0 Copyright (C)20.. Your Name License... )" if $a ; then echo "a option! with $a" fi if $b ; then echo "b option! with $b" fi if $abool ; then echo "abool option!" fi if $bbool ; then echo "bbool option!" fi
This is not quite perfect because it will always process the first switch, but it does place an exit inside the while loop which is in keeping with your design requirement. It may give you an idea how to finish it. #!/bin/bash usage() { echo -e "need help!" } while getopts "a:b:h" OPTION do case $OPTION in a) aflag=1 aval=$OPTARG if [ ! -z "$bflag" ] then printf "ERROR: cant use both -a and -b\n" exit 1 fi ;; b) bflag=1 bval=$OPTARG if [ ! -z "$aflag" ] then printf "ERROR: cant use both -a and -b\n" exit 1 fi ;; h) usage ;; ?) printf "ERROR" ; exit 1 ;; esac done if [ ! -z "$aflag" ] then printf "a option with value %s $aval\n" elif [ ! -z "$bflag" ] then printf "b option with value %s $bval\n" fi exit 0
If you still want to use the getopts I would use bash built-in variable for the arguments count $# to detect wrong number of arguments passed: #! /bin/bash usage() { echo -e "need help!" } # Check if number of arguments is greater than 2 as "-a1" (one arg) and "-a 2" are correct. # You might want to check for other wrong inputs. if [ $# > 2 ] then echo "Some warning t o the user or" usage exit 1 fi while getopts ":a:b:h" OPTION do case $OPTION in a) printf "a option with value %s\n" $OPTARG ;; b) printf "b option with value %s\n" $OPTARG ;; h) usage ;; ?) echo -e "No option selected" ;; esac done exit 0