Bash script arguments are the words after the script name on the command line. The
shell turns them into positional parameters—$1, $2, …—and a count $#. shift
drops $1 and renumbers the rest, which is how many small flag parsers keep reading
the “current” option from $1 without tracking every index by hand.
Tested all the commands and code from this article on Ubuntu 25.04, kernel 6.14.0-37-generic, Bash 5.2.37.
For "$@", functions, and exec other "$@", read
bash script multiple arguments.
For real getopts CLIs, see bash getopts.
Bash script arguments and parameters ($0–$9, $#)
$0— how the script was invoked (name or path).$1,$2, … — positional bash arguments in order.$#— how many bash script parameters you received (not counting$0).
Past $9, use braces: ${10} so Bash does not treat $1 plus a literal
0.
bash -c 'echo "arg count=$#"; printf "arg: <%s>\n" "$@"; echo "first=$1 second=$2"' \
bash alpha "b c"Output:
arg count=2
arg: <alpha>
arg: <b c>
first=alpha second=b cQuoting "b c" keeps it a single shell script argument; without quotes it
would become two parameters.
Bash arguments as "$@" (all args at once)
"$@" expands to every parameter, each safely quoted—what you want when you pass
bash shell script arguments through to another command or wrap a tool.
$* joins differently when quoted; for “give me all bash arguments exactly as
the user typed them,” default to "$@".
shift and walking the list with while
A common pattern for bash scripting arguments you did not declare in advance:
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose) verbose=1; shift;;
--) shift; break;; # optional: stop parsing, rest is operands
*) echo "unknown flag: $1" >&2; exit 1;;
esac
doneAfter each shift, the next word becomes $1, so the case always looks
at the “current” token. Pair this with bash while loop
and bash if else when the branches get heavy.
Example: while / case parser with missing-value checks
Hand-rolled loops should fail fast when --flag is not followed by a value. A
cheap guard is: after shift, require $# > 0 and reject values that look
like another flag ([[ $1 == -* ]]).
#!/bin/bash
usage() {
printf 'usage: %s [--name WORD] [--n NUM] [-h]\n' "$0" >&2
}
name=
n=1
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--name)
shift
if [[ $# -eq 0 || $1 == -* ]]; then
printf 'error: --name needs a value\n' >&2
usage
exit 1
fi
name=$1
shift
;;
--n)
shift
if [[ $# -eq 0 || $1 == -* ]]; then
printf 'error: --n needs a value\n' >&2
usage
exit 1
fi
n=$1
shift
;;
*)
printf 'unknown: %s\n' "$1" >&2
usage
exit 1
;;
esac
done
printf 'name=%s n=%s\n' "${name:-}" "$n"Successful runs (order of flags can change):
name=alice n=3
name=bob n=2Missing value after --name:
error: --name needs a value
usage: ./your-script [--name WORD] [--n NUM] [-h]The first line is stderr; exit status is 1.
Bash script with arguments: when to gate on $#
When the script expects a fixed layout—script.sh input.txt output.dir—test
$# up front instead of parsing forever:
if [[ $# -ne 2 ]]; then
printf 'usage: %s INPUT OUTPUT\n' "$0" >&2
exit 1
fiThat pattern is clearer than overloading flags for simple tools.
Passing bash arguments into a function
Inside a function, $1 / $# refer to that function call. Forward the
script’s parameters with func "$@" when the function should see the same list the
script received. See bash function for scope and
local.
Summary
Bash script arguments map to $1, $2, …, with $# counting them and $0 naming
the script. Use "$@" when you need every argument preserved under quoting, and use
shift inside while / case when you walk a list of flags. Always validate values
after flags so --opt with nothing following does not consume the next flag. For
anything beyond a few options, prefer
bash getopts and reuse the forwarding patterns from
bash script multiple arguments.
Further reading:

