Reputation: 69322
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
?
Upvotes: 2609
Views: 2136428
Reputation: 34735
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:
getopt
from util-linux or formerly GNU glibc.1getopt_long()
the C function of GNU glibc.getopt
can’t do this)script.sh -o outFile file1 file2 -v
(getopts
doesn’t do this)=
-style long options: script.sh --outfile=fileOut --infile fileIn
(allowing both is lengthy if self parsing)-vfd
(real work if self parsing)-oOutfile
or -vfdoOutfile
getopt --test
→ return value 4.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.
set -o errexit -o pipefail -o noclobber -o nounset
# ignore errexit with `&& true`
getopt --test > /dev/null && true
if [[ $? -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
# -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
# -if getopt fails, it complains itself to stderr
PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") || exit 2
# 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
, brew install util-linux
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)
Upvotes: 837
Reputation: 1059
Edit note: now with pure POSIX shell code and gluten free!
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] [--name=TEXT] [(-o|--output) FILE] [ARGS...]"
# 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
# defined options - EDIT HERE!
--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
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
# example of script using command-line options
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] [--name=TEXT] [(-o|--output) FILE] [ARG...]
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 an output-restricted queue to loop over and process arguments. Here's the result:
-o [ARG]
, -abo [ARG]
, --long-option [ARG]
and --long-option=[ARG]
styles of options are accepted;$@
after the loop;--
to force remaining arguments to be treated as positional;getopt(s)
or external utilities;This code was tested and verified to work with a reasonably recent version of: bash
, dash
, mksh
, ksh93
, yash
, zsh
and BusyBox's ash
(all called with their standard executable paths, not as /bin/sh
).
If you find a bug or that it doesn't work with a particular POSIX compatible shell, please leave a comment.
PS: I know... An argument with the binary value of 0x01030307
could break the logic. However, if anyone is passing around binary arguments in a command-line, this issue should be their last concern.
Upvotes: 39
Reputation: 329
The goal is to parse a command line AND create a useful usage message
code:
for arg ; do
case "$arg" in
--edit) # edit file
cd "$(dirname $0)" && vim "$0"
;;
--noN) # do NOT create 'NHI1/../tags'
let noN=1
;;
--noS) # do NOT create 'HOME/src/*-latest/tags'
let noS=1
;;
--help) # write this help message
;&
*) echo "usage: $(basename $0) options..." 1>&2
awk '/--?\w+\)/' "$0" 1>&2
exit
;;
esac
done
this create the usage message:
> build_tags.bash -x
usage: build_tags.bash options...
--edit) # edit file
--noN) # do NOT create 'NHI1/../tags'
--noS) # do NOT create 'HOME/src/*-latest/tags'
--help) # write this help message
the clue is that the definition* of the case
target is also the documentation of the case
target.
Upvotes: 1
Reputation: 70439
--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
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
demo-space-separated.sh -e conf -s /etc /etc/hosts
--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
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
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.
getopt(1) limitations (older, relatively-recent getopt
versions):
More recent getopt
versions don't have these limitations. For more information, see these docs.
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
verbose=1, output_file='/etc/hosts', Leftovers: foo bar
demo-getopts.sh -vf /etc/hosts foo bar
The advantages of getopts
are:
dash
.-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.
Upvotes: 3648
Reputation: 7297
Most of the time args needs to set/change some variables in script to change it's behavior. But we can do this(set/change some variables) directly without parsing args. Take a look at this script:
#!/bin/bash
a=${a:-1} # Set $a from env var $a, set default value 1 if $a is not set
b=${b:-2} # Set $b from env var $b, set default value 2 if $b is not set
echo $a $b
Lets run it:
$ ./test
1 2
Now lets change $a
:
$ a=3 ./test
3 2
and $b
:
$ a=3 b=7 ./test
3 7
And some logic could be implemented like this:
#!/bin/bash
a=${a:-1}
b=${b:-2}
[[ $c ]] && b=
echo $a $b
check:
$ a=3 b=7 c=1 ./test
3
But unfortunately you can't pass arrays as env vars, this won't work:
#!/bin/bash
a=${a:-1}
b=${b:-2}
[[ $c ]] && b=
echo $a $b
for i in "${data[@]}"; { echo $i; }
try:
$ a=3 b=7 data=(a b c) ./test
3 7
(a b c)
Data must flow in like this:
#!/bin/bash
a=${a:-1}
b=${b:-2}
data=("$@")
[[ $c ]] && b=
echo $a $b
for i in "${data[@]}"; { echo $i; }
test:
$ a=3 b=7 ./test a b c
3 7
a
b
c
And a lot more...
Upvotes: 0
Reputation: 1362
The Problem
Bash
and many other similar shells like zsh
, tcsh
etc. suffer in this area. Even the top answers in this post do not offer an easy way to document the help of the options and the script itself.
Python's argparse does a very good job here.
The Solution
If you can afford to have an additional file dedicated to the command line arguments (which is worthwhile if you have many options and related help etc.) and if you can afford to have Python in the loop (It is installed by default on most Linux systems these days anyway), we can leverage Python's argparse in any shell scripts you might have and get the best argparser across all languages work for you.
Try this Python package https://argparse-enh.readthedocs.io/en/latest/argparse_shell.html
Disclaimer: I am the author of that Python package.
Upvotes: -1
Reputation: 62846
I have browsed through all of the answers to this question. And although some contain wealth of information, I was specifically looking for the answers that allow us to describe the supported command line arguments declaratively and get the help text generated automatically from the spec.
And I found 5 such answers (sorry, if I missed yours):
I am also guilty of writing my own command line parser, which seems to be different from all the rest, because I use JSON to describe the supported command line arguments and thus my implementation depends on jq.
Check it out here - https://github.com/MarkKharitonov/bash-parse-command-line-args. The repo contains an example script and the README shows off various invocation scenarios.
Upvotes: 2
Reputation: 1021
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:
Upvotes: 56
Reputation: 6177
I'm using a combination of optget
and optgets
to parse short and long options with or without arguments and even non-options (those without -
or --
):
# catch wrong options and move non-options to the end of the string
args=$(getopt -l "$opt_long" "$opt_short" "$@" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1
mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) /\1=/g")" )
set -- "${args[@]}"
# parse short and long options
while getopts "$opt_short-:" opt; do
...
done
# remove all parsed options from $@
shift $((OPTIND-1)
By that I'm able to access all options with a variable like $opt_verbose
and the non-options are accessible through the default variables $1
, $2
, etc.:
echo "help:$opt_help"
echo "file:$opt_file"
echo "verbose:$opt_verbose"
echo "long_only:$opt_long_only"
echo "short_only:$opt_s"
echo "path:$1"
echo "mail:$2"
One of the main features is, that I'm able to pass all options and non-options in a complete random order:
# $opt_file $1 $2 $opt_... $opt_... $opt_...
# /demo.sh --file=file.txt /dir info@example.com -V -h --long_only=yes -s
help:1
file:file.txt
verbose:1
long_only:yes
short_only:1
path:/dir
mail:info@example.com
More details: https://stackoverflow.com/a/74275254/318765
Upvotes: 1
Reputation: 12837
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!
Upvotes: 8
Reputation: 7230
Expanding on @bruno-bronosky's answer, I added a "preprocessor" to handle some common formatting:
--longopt=val
into --longopt val
-xyz
into -x -y -z
--
to indicate the end of flags#!/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[@]}"
Upvotes: 25
Reputation: 7337
This also might be useful to know: you can set a value and if someone provides input, override the default with that value.
myscript.sh -f ./serverlist.txt
or just ./myscript.sh
(and it takes defaults)
#!/bin/bash
# --- set the value, if there is inputs, override the defaults.
HOME_FOLDER="${HOME}/owned_id_checker"
SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"
while [[ $# > 1 ]]
do
key="$1"
shift
case $key in
-i|--inputlist)
SERVER_FILE_LIST="$1"
shift
;;
esac
done
echo "SERVER LIST = ${SERVER_FILE_LIST}"
Upvotes: 1
Reputation: 8816
I ended up implementing the dash
(or /bin/sh
) version of the accepted answer, basically, without array usage:
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) verbose=1; shift;;
-o|--output) if [[ $# -gt 1 && "$2" != -* ]]; then
file=$2; shift 2
else
echo "-o requires file-path" 1>&2; exit 1
fi ;;
--)
while [[ $# -gt 0 ]]; do BACKUP="$BACKUP;$1"; shift; done
break;;
*)
BACKUP="$BACKUP;$1"
shift
;;
esac
done
# Restore unused arguments.
while [ -n "$BACKUP" ] ; do
[ ! -z "${BACKUP%%;*}" ] && set -- "$@" "${BACKUP%%;*}"
[ "$BACKUP" = "${BACKUP/;/}" ] && break
BACKUP="${BACKUP#*;}"
done
Upvotes: 2
Reputation: 893
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
Upvotes: 17
Reputation: 959
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
}
Upvotes: 15
Reputation:
I use it to iterate over key => value from the end. A first optional argument is caught after the loop.
Usage is ./script.sh optional-first-arg -key value -key2 value2
#!/bin/sh
a=$(($#-1))
b=$(($#))
while [ $a -gt 0 ]; do
eval 'key="$'$a'"; value="$'$b'"'
echo "$key => $value"
b=$(($b-2))
a=$(($a-2))
done
unset a b key value
[ $(($#%2)) -ne 0 ] && echo "first_arg = $1"
Sure you can do it from the left to the right with a few changes.
This snippet code shows the key => value pairs and the first argument if it exists.
#!/bin/sh
a=$((1+$#%2))
b=$((1+$a))
[ $(($#%2)) -ne 0 ] && echo "first_arg = $1"
while [ $a -lt $# ]; do
eval 'key="$'$a'"; value="$'$b'"'
echo "$key => $value"
b=$(($b+2))
a=$(($a+2))
done
unset a b key value
Tested with 100,000 arguments, fast.
You can also iterate key => value and first optional arg from the left to the right without eval :
#!/bin/sh
a=$(($#%2))
b=0
[ $a -eq 1 ] && echo "first_arg = $1"
for value; do
if [ $b -gt $a -a $(($b%2)) -ne $a ]; then
echo "$key => $value"
fi
key="$value"
b=$((1+$b))
done
unset a b key value
Upvotes: 1
Reputation: 27947
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
Upvotes: 417
Reputation: 4938
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
Upvotes: 21
Reputation: 939
# 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
Pure bash. No need to learn/use getopt or getopts
CONS:
Can't combine args
Upvotes: 34
Reputation: 6012
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:
-n arg
and --name=arg
Upvotes: 122
Reputation: 6502
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.
Upvotes: 166
Reputation: 565
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
Upvotes: 44
Reputation: 45223
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 "--".
Upvotes: 114
Reputation: 6779
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:
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.
Upvotes: 3
Reputation: 1117
There are several ways to parse cmdline args (e.g. GNU getopt (not portable) vs BSD (MacOS) getopt vs getopts) - all problematic. This solution
=
separator between option and argument-vxf
--color
vs --color=always
),--
to signal end of options, andExamples: 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=${@}"
Upvotes: 7
Reputation: 19122
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.
Upvotes: 34
Reputation: 3306
I wrote down a script that can assist with parsing command-line arguments easily - https://github.com/unfor19/bargs
$ bash example.sh -n Willy --gender male -a 99
Name: Willy
Age: 99
Gender: male
Location: chocolate-factory
$ bash example.sh -n Meir --gender male
[ERROR] Required argument: age
Usage: bash example.sh -n Willy --gender male -a 99
--person_name | -n [Willy] What is your name?
--age | -a [Required]
--gender | -g [Required]
--location | -l [chocolate-factory] insert your location
$ bash example.sh -h
Usage: bash example.sh -n Willy --gender male -a 99
--person_name | -n [Willy] What is your name?
--age | -a [Required]
--gender | -g [Required]
--location | -l [chocolate-factory] insert your location
Upvotes: 2
Reputation: 22369
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.
Upvotes: 3
Reputation: 401
I give you The Function parse_params
that will parse params from the command line.
--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:
-d 1
)--any-param
and -anyparam
are equivalenteval $(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
Upvotes: 12
Reputation: 48
Simple and easy to modify, parameters can be in any order. this can be modified to take parameters in any form (-a, --a, a, etc).
for arg in "$@"
do
key=$(echo $arg | cut -f1 -d=)`
value=$(echo $arg | cut -f2 -d=)`
case "$key" in
name|-name) read_name=$value;;
id|-id) read_id=$value;;
*) echo "I dont know what to do with this"
ease
done
Upvotes: 1