How do I parse command line arguments in Bash?
Bash Space-Separated (e.g., --option argument
)
cat >/tmp/demo-space-separated.sh <<'EOF'#!/bin/bashPOSITIONAL=()while [[ $# -gt 0 ]]; do key="$1" case $key in -e|--extension) EXTENSION="$2" shift # past argument shift # past value ;; -s|--searchpath) SEARCHPATH="$2" shift # past argument shift # past value ;; -l|--lib) LIBPATH="$2" shift # past argument shift # past value ;; --default) DEFAULT=YES shift # past argument ;; *) # unknown option POSITIONAL+=("$1") # save it in an array for later shift # past argument ;; esacdoneset -- "${POSITIONAL[@]}" # restore positional parametersecho "FILE EXTENSION = ${EXTENSION}"echo "SEARCH PATH = ${SEARCHPATH}"echo "LIBRARY PATH = ${LIBPATH}"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"fiEOFchmod +x /tmp/demo-space-separated.sh/tmp/demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts
Output from copy-pasting the block above
FILE EXTENSION = confSEARCH PATH = /etcLIBRARY PATH = /usr/libDEFAULT =Number files in SEARCH PATH with EXTENSION: 14Last line of file specified as non-opt/last argument:#93.184.216.34 example.com
Usage
demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts
Bash Equals-Separated (e.g., --option=argument
)
cat >/tmp/demo-equals-separated.sh <<'EOF'#!/bin/bashfor i in "$@"; do case $i in -e=*|--extension=*) EXTENSION="${i#*=}" shift # past argument=value ;; -s=*|--searchpath=*) SEARCHPATH="${i#*=}" shift # past argument=value ;; -l=*|--lib=*) LIBPATH="${i#*=}" shift # past argument=value ;; --default) DEFAULT=YES shift # past argument with no value ;; *) # unknown option ;; esacdoneecho "FILE EXTENSION = ${EXTENSION}"echo "SEARCH PATH = ${SEARCHPATH}"echo "LIBRARY PATH = ${LIBPATH}"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 $1fiEOFchmod +x /tmp/demo-equals-separated.sh/tmp/demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts
Output from copy-pasting the block above
FILE EXTENSION = confSEARCH PATH = /etcLIBRARY PATH = /usr/libDEFAULT =Number files in SEARCH PATH with EXTENSION: 14Last line of file specified as non-opt/last argument:#93.184.216.34 example.com
Usage
demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /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 variableOPTIND=1 # Reset in case getopts has been used previously in the shell.# Initialize our own variables:output_file=""verbose=0while getopts "h?vf:" opt; do case "$opt" in h|\?) show_help exit 0 ;; v) verbose=1 ;; f) output_file=$OPTARG ;; esacdoneshift $((OPTIND-1))[ "${1:-}" = "--" ] && shiftecho "verbose=$verbose, output_file='$output_file', Leftovers: $@"EOFchmod +x /tmp/demo-getopts.sh/tmp/demo-getopts.sh -vf /etc/hosts foo bar
Output from copy-pasting the block above
verbose=1, output_file='/etc/hosts', Leftovers: foo bar
Usage
demo-getopts.sh -vf /etc/hosts foo bar
The advantages of getopts
are:
- It's more portable, and will work in other shells like
dash
. - It can handle multiple single options like
-vf filename
in the typical Unix way, automatically.
The disadvantage of getopts
is that it can only handle short options (-h
, not --help
) without additional code.
There is a getopts tutorial which explains what all of the syntax and variables mean. In bash, there is also help getopts
, which might be informative.
No answer showcases enhanced getopt. And the top-voted answer is misleading: It either ignores -vfd
style short options (requested by the OP) or options after positional arguments (also requested by the OP); and it ignores parsing-errors. Instead:
- Use enhanced
getopt
from util-linux or formerly GNU glibc.1 - It works with
getopt_long()
the C function of GNU glibc. - no other solution on this page can do all this:
- handles spaces, quoting characters and even binary in arguments2 (non-enhanced
getopt
can’t do this) - it can handle options at the end:
script.sh -o outFile file1 file2 -v
(getopts
doesn’t do this) - allows
=
-style long options:script.sh --outfile=fileOut --infile fileIn
(allowing both is lengthy if self parsing) - allows combined short options, e.g.
-vfd
(real work if self parsing) - allows touching option-arguments, e.g.
-oOutfile
or-vfdoOutfile
- handles spaces, quoting characters and even binary in arguments2 (non-enhanced
- Is so old already3 that no GNU system is missing this (e.g. any Linux has it).
- You can test for its existence with:
getopt --test
→ return value 4. - Other
getopt
or shell-builtingetopts
are of limited use.
The following calls
myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFilemyscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFilemyscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFilemyscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfdmyscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile
all return
verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile
with the following myscript
#!/bin/bash# More safety, by turning some bugs into errors.# Without `errexit` you don’t need ! and can replace# PIPESTATUS with a simple $?, but I don’t do that.set -o errexit -o pipefail -o noclobber -o nounset# -allow a command to fail with !’s side effect on errexit# -use return value from ${PIPESTATUS[0]}, because ! hosed $?! getopt --test > /dev/null if [[ ${PIPESTATUS[0]} -ne 4 ]]; then echo 'I’m sorry, `getopt --test` failed in this environment.' exit 1fiOPTIONS=dfo:vLONGOPTS=debug,force,output:,verbose# -regarding ! and PIPESTATUS see above# -temporarily store output to be able to check for errors# -activate quoting/enhanced mode (e.g. by writing out “--options”)# -pass arguments only via -- "$@" to separate them correctly! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")if [[ ${PIPESTATUS[0]} -ne 0 ]]; then # e.g. return value is 1 # then getopt has complained about wrong arguments to stdout exit 2fi# 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 ;; esacdone# handle non-option argumentsif [[ $# -ne 1 ]]; then echo "$0: A single input file is required." exit 4fiecho "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"
1 enhanced getopt is available on most “bash-systems”, including Cygwin; on OS X try brew install gnu-getopt or sudo port install getopt
2 the POSIX exec()
conventions have no reliable way to pass binary NULL in command line arguments; those bytes prematurely end the argument
3 first version released in 1997 or before (I only tracked it back to 1997)
deploy.sh
#!/bin/bashwhile [[ "$#" -gt 0 ]]; do case $1 in -t|--target) target="$2"; shift ;; -u|--uglify) uglify=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shiftdoneecho "Where to deploy: $target"echo "Should uglify : $uglify"
Usage:
./deploy.sh -t dev -u# OR:./deploy.sh --target dev --uglify