2627

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?

Benjamin W.
54k19 gold badges135 silver badges136 bronze badges
asked Oct 10, 2008 at 16:57
6
  • 3
    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
  • 5
    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
  • 3
    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
  • 2
    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

1
2
3685

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.

answered Jan 7, 2013 at 20:01
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."
|
858

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)

answered Apr 20, 2015 at 17:47

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
answered Nov 20, 2015 at 12:28

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.

Rob Bednark
28.8k28 gold badges90 silver badges131 bronze badges
answered Nov 13, 2012 at 10:31

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
Inanc Gumus
28.3k10 gold badges89 silver badges104 bronze badges
answered Jul 15, 2015 at 23:43

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

Rob Bednark
28.8k28 gold badges90 silver badges131 bronze badges
answered Oct 10, 2008 at 17:03

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
Rob Bednark
28.8k28 gold badges90 silver badges131 bronze badges
answered Jul 1, 2014 at 1:20

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.

answered Jun 27, 2020 at 23:07

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.

answered Sep 7, 2016 at 18:25

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
Inanc Gumus
28.3k10 gold badges89 silver badges104 bronze badges
answered Sep 8, 2016 at 18:59

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[@]}"
answered Mar 5, 2019 at 17:12

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
Inanc Gumus
28.3k10 gold badges89 silver badges104 bronze badges
answered Jun 9, 2014 at 13:46

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
answered Mar 1, 2012 at 15:15

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
}
answered Aug 14, 2020 at 13:36

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
Hive
2131 gold badge5 silver badges17 bronze badges
answered Feb 12, 2015 at 21:50

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
answered Jul 1, 2016 at 20:56

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

answered Sep 16, 2018 at 17:45

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!
answered Oct 27, 2021 at 10:26

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
...
}
answered Jul 19, 2013 at 7:50

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
answered Jun 24, 2015 at 10:54

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=${@}"
answered Dec 24, 2019 at 2:09

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.

answered Aug 29, 2016 at 3:44

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
answered Mar 15, 2017 at 13:24

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ドル
answered Apr 27, 2015 at 2:42

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

answered Oct 19, 2015 at 13:59

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

answered Feb 20, 2017 at 21:30

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
answered Oct 10, 2017 at 22:49

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.

answered Dec 1, 2019 at 11:06

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.

answered Apr 10, 2020 at 12:12

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!"
answered Jul 9, 2013 at 16:51

Comments

1
2

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.