Bash scripting guide · part 26 of 32
Script arguments
Positional parameters $1 to ${10}, $# and $0, why "$@" is almost always right and "$*" rarely, shift, getopts, and ending options with --.
The words after a script's name on the command line become its positional parameters:
$1, $2 and so on. $# holds how many there are, and $0 is the name the script was
called by. Functions get their own set while they run, which is why the same rules apply to
function arguments (see Functions).
The basics
Read the first arguments and count them
#!/bin/bash
echo "script: $0"
echo "count: $#"
echo "first: $1"
echo "second: $2"
echo "tenth: ${10}"
bash args.sh alpha "beta gamma" 3 4 5 6 7 8 9 ten
script: args.sh count: 10 first: alpha second: beta gamma tenth: ten
"beta gamma" was quoted on the command line, so it arrived as one argument, $2. Past
nine you need braces: $10 would be $1 followed by a literal 0, while ${10} is the
tenth argument. $0 is the path as typed, here args.sh.
"$@" versus "$*"
Both expand to all the arguments, and the difference only shows when they are quoted:
"$@"expands to each argument as a separate word, exactly as received. Spaces inside an argument are preserved, and no argument is split or merged."$*"joins all arguments into one word, separated by the first character ofIFS(a space by default).- Unquoted,
$@and$*are the same and both are subject to word splitting and globbing, which destroys arguments containing spaces.
The three ways of passing arguments on
#!/bin/bash
printf ' [%s]\n' "$@"
#!/bin/bash
echo '"$@":'; bash show.sh "$@"
echo '"$*":'; bash show.sh "$*"
echo 'bare $@:'; bash show.sh $@
bash wrapper.sh "file with spaces.txt" notes.txt
"$@": [file with spaces.txt] [notes.txt] "$*": [file with spaces.txt notes.txt] bare $@: [file] [with] [spaces.txt] [notes.txt]
show.sh prints each argument it receives in brackets. Only "$@" passed the two
arguments through intact. "$*" merged them into one, and unquoted $@ split the first
one into three words. When a script forwards its arguments to another command, "$@" is
the right choice in practically every case.
Where "$*" is useful
set -- web01 web02 db01
echo "hosts: $*"
IFS=,
echo "csv: $*"
hosts: web01 web02 db01 csv: web01,web02,db01
set -- replaces the current shell's positional parameters, a convenient way to try
these out. "$*" is the tool for joining arguments into one string, for a message or a
log line. With IFS set to a comma, the join uses commas.
Requiring and defaulting arguments
Parameter expansion handles the common checks in one line. ${1:-default} substitutes a
default when $1 is missing or empty; ${1:?message} aborts the script with the message.
For more than one argument, check $# instead.
Fail early when an argument is missing
#!/bin/bash
target=${1:?usage: deploy.sh TARGET [ENV]}
env=${2:-staging}
echo "deploying $target to $env"
bash deploy.sh web01
bash deploy.sh web01 production
bash deploy.sh
deploying web01 to staging deploying web01 to production deploy.sh: line 2: 1: usage: deploy.sh TARGET [ENV]
The first two calls used the default staging and an explicit value. The third had no
target, so ${1:?...} printed the script name, the line number, the parameter's name (1) and the
message to standard error, and exited with status 1.
Looping over arguments and shift
for arg; do (with no in list) loops over "$@". shift drops $1 and moves every
other argument down by one; shift 2 drops two. It is the tool for consuming arguments one at
a time, typically in a while loop.
Consume arguments with shift
#!/bin/bash
group=$1
shift
echo "group: $group ($# users)"
for user; do
echo " adding $user"
done
bash mkuser.sh admins alice bob carol
group: admins (3 users) adding alice adding bob adding carol
After shift, the group name is gone from the list: $# dropped from 4 to 3, and the
for loop saw only the users.
Options with getopts
getopts is the shell's built-in option parser. It handles single-letter options, clustering
(-vo file), options with arguments (attached -ofile or separate -o file), and the --
end marker. You give it an option string: each letter is an option, a letter followed by :
takes an argument, and a leading : switches on silent error handling, in which your case
handles bad options itself.
Each call puts the next option letter in your variable and its argument in OPTARG, and
advances OPTIND. After the loop, shift $((OPTIND - 1)) removes the options so that $@
holds only the remaining operands.
A complete getopts loop
#!/bin/bash
verbose=0 out=-
while getopts ":vo:h" opt; do
case $opt in
v) verbose=1 ;;
o) out=$OPTARG ;;
h) echo "usage: report.sh [-v] [-o FILE] INPUT..."; exit 0 ;;
:) echo "option -$OPTARG needs an argument" >&2; exit 2 ;;
?) echo "unknown option -$OPTARG" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose out=$out inputs: $*"
bash report.sh -v -o summary.txt access.log logs/app.log
bash report.sh -vo summary.txt access.log
bash report.sh -ofile.txt access.log
verbose=1 out=summary.txt inputs: access.log logs/app.log verbose=1 out=summary.txt inputs: access.log verbose=0 out=file.txt inputs: access.log
All three spellings parse the same way: separate options, clustered -vo with the
argument in the next word, and the argument attached to the letter. After the shift,
only the input files remain in $@.
Bad options and the -- marker
#!/bin/bash
verbose=0 out=-
while getopts ":vo:" opt; do
case $opt in
v) verbose=1 ;;
o) out=$OPTARG ;;
:) echo "option -$OPTARG needs an argument" >&2; exit 2 ;;
?) echo "unknown option -$OPTARG" >&2; exit 2 ;;
esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose out=$out inputs: $*"
bash report.sh -x; echo "status $?"
bash report.sh -o; echo "status $?"
bash report.sh -v -- -o.log
bash report.sh access.log -v
unknown option -x status 2 option -o needs an argument status 2 verbose=1 out=- inputs: -o.log verbose=0 out=- inputs: access.log -v
In silent mode an unknown option arrives as ? with the letter in OPTARG, and a missing
argument as :. Everything after -- is an operand, so a file literally named -o.log
gets through. The last call shows that getopts stops at the first non-option: -v
after access.log is treated as an operand, unlike GNU tools, which accept options
anywhere.
getopts does not do long options (--verbose). For those, scripts usually parse by hand
with while and case, which also makes the -- handling explicit:
Parse long options by hand
#!/bin/bash
dry_run=0 dest=
while (($#)); do
case $1 in
-n|--dry-run) dry_run=1 ;;
-d|--dest) dest=${2:?--dest needs a value}; shift ;;
--dest=*) dest=${1#*=} ;;
--) shift; break ;;
-*) echo "unknown option: $1" >&2; exit 2 ;;
*) break ;;
esac
shift
done
echo "dry_run=$dry_run dest=$dest files: $*"
bash sync.sh --dry-run --dest /backup a.txt b.txt
bash sync.sh --dest=/srv -- --weird-name.txt
bash sync.sh --force
dry_run=1 dest=/backup files: a.txt b.txt dry_run=0 dest=/srv files: --weird-name.txt unknown option: --force
Each pass through the loop looks at $1 and shifts it away; an option that takes a value
shifts once more. --dest=* splits the attached form with ${1#*=}. The loop stops at
-- or at the first operand, and an unknown option exits with status 2, the conventional
code for usage errors.
The -- convention
Most commands treat -- as "end of options": every word after it is an operand even if it
starts with a dash. Pass -- whenever you hand a variable to a command and cannot be sure it
does not start with -, as in rm -- "$file" or grep -- "$pattern" file. Scripts that
use getopts get this behavior for free.
Related
getoptsParse short command-line options such as -v and -o FILE in a script, one option per call, inside a while loop.shiftDrop the first N positional parameters ($1, $2, ...) and renumber the rest down.setTurn shell options such as -e, -u, -x and pipefail on or off, and replace the positional parameters.- FunctionsDefining and calling bash functions, arguments, local variables, return status versus output, namerefs, and sourcing a function library.
- Parameter expansionThe ${...} operators for defaults, lengths, substrings, trimming prefixes and suffixes, search and replace, and case conversion, without calling sed or cut.
- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.
- Word splittingWhen bash splits expansion results into separate words, how IFS controls it, and why
for line in $(cat file)does not loop over lines. - Bash idioms for real scriptsThe small patterns that make scripts robust - die(), logging to stderr, ${1:?usage}, command -v checks, mktemp with trap cleanup, retries with backoff, locking and safe config parsing.