2626

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?

6
  • 2
    For zsh-users there's a great builtin called zparseopts which can do: zparseopts -D -E -M -- d=debug -debug=d And have both -d and --debug in the $debug array echo $+debug[1] will return 0 or 1 if one of those are used. Ref: zsh.org/mla/users/2011/msg00350.html Commented Aug 2, 2016 at 2:13
  • 4
    Really good tutorial: linuxcommand.org/lc3_wss0120.php. I especially like the "Command Line Options" example. Commented Feb 10, 2020 at 18:45
  • I created a script which does it for you, it's called - github.com/unfor19/bargs Commented Aug 7, 2020 at 14:29
  • 2
    See also Giving a bash script the option to accepts flags, like a command? for an elaborate, ad hoc, long and short option parser. It does not attempt to handle option arguments attached to short options, nor long options with = separating option name from option value (in both cases, it simply assumes that the option value is in the next argument). It also doesn't handle short option clustering — the question didn't need it. Commented Oct 9, 2020 at 15:21
  • 1
    This great tutorial by Baeldung shows 4 ways to process command-line arguments in bash, including: 1) positional parameters $1, $2, etc., 2) flags with getopts and ${OPTARG}, 3) looping over all parameters ($@), and 4) looping over all parameters using $#, $1, and the shift operator. Commented Dec 26, 2020 at 21:05

43 Answers 43

3682

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:

  1. It's more portable, and will work in other shells like dash.
  2. 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.

Sign up to request clarification or add additional context in comments.

37 Comments

Is this really true? According to Wikipedia there's a newer GNU enhanced version of getopt which includes all the functionality of getopts and then some. man getopt on Ubuntu 13.04 outputs getopt - parse command options (enhanced) as the name, so I presume this enhanced version is standard now.
That something is a certain way on your system is a very weak premise to base asumptions of "being standard" on.
@Livven, that getopt is not a GNU utility, it's part of util-linux.
If you use -gt 0, remove your shift after the esac, augment all the shift by 1 and add this case: *) break;; you can handle non optionnal arguments. Ex: pastebin.com/6DJ57HTc
The getopts "h?vf:" should be getopts "hvf:" without question mark. Arguments which are not recognized are stored as ? in $opt. Quote from man builtins: “The colon and question mark characters may not be used as option characters.”
|
857

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 (this rules non-enhanced getopt out)
    • it can handle options at the end: script.sh -o outFile file1 file2 -v (this rules getopts out)
    • allows =-style long options: script.sh --outfile=fileOut --infile fileIn (allowing both at the same time makes it really lengthy when self parsing)
    • allows combined short options, e.g. -vfd (together with the one before this practically rules out self parsing)
    • allows touching option-arguments, e.g. -oOutfile or -vfdoOutfile (you still want to program it yourself?)
  • Is so old already3 that it comes preinstalled on any GNU system (i.e. Linux mostly); see footnote1
  • 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. 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)

17 Comments

Thanks for this. Just confirmed from the feature table at en.wikipedia.org/wiki/Getopts, if you need support for long options, and you're not on Solaris, getopt is the way to go.
I believe that the only caveat with getopt is that it cannot be used conveniently in wrapper scripts where one might have few options specific to the wrapper script, and then pass the non-wrapper-script options to the wrapped executable, intact. Let's say I have a grep wrapper called mygrep and I have an option --foo specific to mygrep, then I cannot do mygrep --foo -A 2, and have the -A 2 passed automatically to grep; I need to do mygrep --foo -- -A 2. Here is my implementation on top of your solution.
Note this doesn't work on Mac at least up to the current 10.14.3. The getopt that ships is BSD getopt from 1999...
@jjj footnote 1 covers OS X. – For OS X out-of-the-box solution check other questions and answers. Or to be honest: for real programming don’t use bash. ;-)
@BenjaminW. The top-voted answer covers two self parsing solutions and getopts. The former don’t do combined short options, the latter doesn’t parse options after non-option arguments.
|
421

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 

17 Comments

This is what I am doing. Have to while [[ "$#" > 1 ]] if I want to support ending the line with a boolean flag ./script.sh --debug dev --uglify fast --verbose. Example: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
Wow! Simple and clean! This is how I'm using this: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
This is much nicer to paste into each script rather than dealing with source or having people wonder where your functionality actually starts.
Warning: this tolerates duplicated arguments, the latest argument prevails. e.g. ./script.sh -d dev -d prod would result in deploy == 'prod'. I used it anyway :P :) :+1:
Great answer, tnx! I shortened it a bit - while (( "$#" )); do instead of while [[ "$#" -gt 0 ]]; do
|
169

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.

4 Comments

Neat! Though this won't work for space-separated arguments à la mount -t tempfs .... One can probably fix this via something like while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;; etc
This can’t handle -vfd style combined short options.
If you want to generically evaluate --option and -option without repeating OPTION=$i every time, use -*=*) as match pattern and eval ${i##*-}.
This is amazing. Straightforward enough for me to use and adopt.
124
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

4 Comments

Sorry for the delay. In my script, the handle_argument function receives all the non-option arguments. You can replace that line with whatever you'd like, maybe *) die "unrecognized argument: $1" or collect the args into a variable *) args+="$1"; shift 1;;.
Amazing! I've tested a couple of answers, but this is the only one that worked for all cases, including many positional parameters (both before and after flags)
nice succinct code, but using -n and no other arg causes infinite loop due to error on shift 2, issuing shift twice instead of shift 2. Suggested the edit.
--name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;; required param not working. I'm working on this. i will keep update.
114

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 "--".

3 Comments

Using $* is broken usage of getopt. (It hoses arguments with spaces.) See my answer for proper usage.
Why would you want to make it more complicated?
@Matt J, the first part of the script (for i) would be able to handle arguments with spaces in them if you use "$i" instead of $i. The getopts does not seem to be able to handle arguments with spaces. What would be the advantage of using getopt over the for i loop?
44

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 

2 Comments

This code can’t handle options with arguments like this: -c1. And the use of = to separate short options from their arguments is unusual...
I ran into two problems with this useful chunk of code: 1) the "shift" in the case of "-c=foo" ends up eating the next parameter; and 2) 'c' should not be included in the "[cfr]" pattern for combinable short options.
40

ASAP: Another Shell Argument Parser

Edit note: now with pure POSIX shell code and gluten free!

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 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...] 

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 an output-restricted queue 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 orthogonal 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: 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.

6 Comments

Thank you, @Liso! I need to update this answer. I've analyzed the regular expressions' decision tree and found some minor errors (nothing serious though).
@leogama your $EOL is not a binary sequence. It is just a printable string with length 12 (containing four backslashes). Did you mean to use printf instead of echo? I don't know if this impacts POSIX compliance though.
@PedroA Great! From GNU echo info page: "POSIX does not require support for any options, and says that the behavior of ‘echo’ is implementation-defined if any STRING contains a backslash or if the first argument is ‘-n’. Portable programs can use the ‘printf’ command if they need to omit trailing newlines or output control characters or backslashes." At least printf is in POSIX...
Running this using #!/bin/bash shebang resulting in infinite loop.
|
34

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.

4 Comments

This is one of the best answers.
Note that for macOs, this script would require gnu-getopt to work properly. brew install gnu-getopt
This will bring it from getopt v1.10 to getopt 2.38+. opensource.apple.com/source/shell_cmds/shell_cmds-216.60.1/…
Very nice. But failes with files/folder/argument list.
34
# 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

2 Comments

I have a question here. Why did you use shift; OUTPUTFILE="$1" instead of OUTPUTFILE="$2"? Maybe it has an easy answer but I am a newbie in bash
I believe you could do either and it really just comes down to personal preference. In this case I just wanted to keep $1 as the "active" argument everywhere
26

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[@]}" 

1 Comment

This looks great - but wondering if END_OF_OPT=1 is actually necessary on this line: --*) ARGV+=("$arg"); END_OF_OPT=1 ;;. If left in there, it fails to parse --username=fred if it's included after --quiet (or any other long-style boolean option). For example, script.sh --quiet --username=fred fails with Unrecognized argument: --username=fred (though script.sh --quiet --username fred works fine). I took out that END_OF_OPT=1 in my script and now it works, but not sure if maybe that breaks some other scenario I'm not aware of.
21

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 

Comments

18

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 

3 Comments

I read all and this one is my preferred one. I don't like to use -a=1 as argc style. I prefer to put first the main option -options and later the special ones with single spacing -o option. Im looking for the simplest-vs-better way to read argvs.
It's working really well but if you pass an argument to a non a: option all the following options would be taken as arguments. You can check this line ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFile with your own script. -d option is not set as d:
This is great, only downsides are that all options need to have an argument and that you cannot use options like --option, only -o
15

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 } 

Comments

12

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 

Comments

12

I give you The Function parse_params that will parse params from the command line.

  1. It is a pure Bash solution, no additional utilities.
  2. Does not pollute global scope.
  3. Effortlessly returns you simple to use variables, that you could build further logic on.
  4. 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:

  1. Does not support space delimited params (-d 1)
  2. Param names will lose dashes so --any-param and -anyparam are equivalent
  3. 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 

2 Comments

To use the demo to parse params that come into your bash script you just do show_use "$@"
Basically I found out that github.com/renatosilva/easyoptions does the same in the same way but is a bit more massive than this function.
9

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

Comments

8

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! 

1 Comment

Could you please explain why you think this version is better than others? With no explanation it’s hard to understand why we should use this version rather than another.
7

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 ... } 

Comments

7

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 

2 Comments

what's the meaning for "+x" on ${key+x} ?
It is a test to see if 'key' is present or not. Further down I unset key and this breaks the inner while loop.
7

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=${@}" 

2 Comments

It's something. I much prefer this over the classic getopts garbage, but the way it parses is not "natural". ./program-name.sh NOTES.md -f produced foo=, bar=, baz=, files=NOTES.md -f and it thought -f was part of the file name. It did not set foo=1. It doesn't do what I want it to do.
@Kalec that would be because -f comes after a non-option argument NOTES.md and by design, option processing stops after first non-option argument like many unix commands, since you could have different subcommands with different sub options. -f in this case is the stored in $2 with NOTES.md in $1. You could repeat option parsing if you so wish, until $# -eq 0
5

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.

Comments

4

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 

1 Comment

Like this one. Maybe just add -e param to echo with new line.
3

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 

Comments

3

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

1 Comment

This is Solaris-specific and doesn't cover bash which the OP specifically requested
3

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 :)

2 Comments

I get this on Mac OS X: ``` lib/bashopts.sh: line 138: declare: -A: invalid option declare: usage: declare [-afFirtx] [-p] [name[=value] ...] Error in lib/bashopts.sh:138. 'declare -x -A bashopts_optprop_name' exited with status 2 Call tree: 1: lib/controller.sh:4 source(...) Exiting with status 1 ```
You need Bash version 4 to use this. On Mac, the default version is 3. You can use home brew to install bash 4.
3

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 

1 Comment

This takes the same approach as Noah's answer, but has less safety checks / safeguards. This allows us to write arbitrary arguments into the script's environment and I'm pretty sure your use of eval here may allow command injection.
3

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.

Comments

3

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.

Comments

2

Use module "arguments" from bash-modules

Example:

#!/bin/bash . import.sh log arguments NAME="world" parse_arguments "-n|--name)NAME;S" -- "$@" || { error "Cannot parse command line." exit 1 } info "Hello, $NAME!" 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.