| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required for the examples |
| Scope | Positional parameters ($0–$9, ${10}), $#, "$@" vs "$*", looping, shift, validation, default values, using -- with commands such as cp to protect path operands, and a short note on function parameters. Does not cover getopts, long options, or advanced CLI parsing. |
| Related guides | Shell scripting course hub |
Bash script arguments are values passed after the script name on the command line. Inside the script, Bash exposes them as positional parameters such as $1, $2, $#, and "$@".
Quoting matters when an argument contains spaces or wildcard characters because it determines whether the value stays one argument. Several examples use filenames with spaces because they make quoting mistakes easy to see.
Quick Reference
| Parameter | Meaning |
|---|---|
$0 |
Script name or invocation path |
$1, $2 |
First and second argument |
${10} |
Tenth argument (braces required past $9) |
$# |
Number of positional parameters |
"$@" |
All arguments, each kept separate |
"$*" |
All arguments joined into one word when quoted (normally space-separated) |
shift |
Remove the first argument and renumber the rest |
What Are Bash Script Arguments and Positional Parameters?
When you run a Bash script as ./backup-file.sh report.txt archive/, Bash stores each word after the script name as a positional parameter. The script name itself is $0; the first argument is $1, the second is $2, and so on.
Before your script starts, the calling shell parses and expands the command line into arguments. Quoting affects those argument boundaries: "my report.txt" arrives as one argument, while my report.txt normally arrives as two.
Bash can also replace the current positional parameters with set -- arguments, but normal scripts usually receive them from their command line.
$0, $1…$9 and ${10}
| Parameter | Meaning |
|---|---|
$0 |
Script name or path used to invoke the script |
$1, $2, … |
First, second, … positional argument |
${10} |
Tenth argument — braces required past $9 |
Past $9, Bash needs braces. $10 is read as $1 followed by a literal 0, not the tenth argument.
Save this as show-tenth.sh:
#!/bin/bash
printf 'ninth=%s tenth=%s eleventh=%s\n' "$9" "${10}" "${11}"Make it executable and pass eleven words:
chmod +x show-tenth.sh && ./show-tenth.sh one two three four five six seven eight nine ten elevenninth=nine tenth=ten eleventh=eleven${10} expands to ten, the tenth argument. The braces are required because $10 would otherwise mean $1 followed by 0.
Pass a filename that contains a space as the second argument:
bash -c 'printf "count=%s first=%s second=%s\n" "$#" "$1" "$2"' demo.sh alpha "b c"count=2 first=alpha second=b cWithout quotes around b c, the shell would pass three parameters instead of two.
Count Arguments with $#
$# is the number of positional parameters, not counting $0.
Check how many arguments arrived:
bash -c 'printf "argument count=%s\n" "$#"' demo.sh alpha "b c"argument count=2Use $# at the top of a script when you expect a fixed number of operands, such as exactly two paths for a copy helper.
Difference Between "$@" and "$*"
Both expand to the positional parameters, but quoting changes how many words the result contains.
| Form | Behavior |
|---|---|
"$@" |
Expands each argument as a separate quoted word |
"$*" |
Joins all arguments into one word, separated by the first character of IFS (normally a space) |
Avoid unquoted $@ and $* when preserving arguments matters. Without double quotes, the expanded values can undergo further word splitting and pathname expansion.
With the default IFS, "$*" joins the arguments with spaces. If IFS is changed, the separator can change as well.
Loop with "$@" so each argument stays separate:
bash -c 'for a in "$@"; do printf "<%s>\n" "$a"; done' demo one "two words" three<one>
<two words>
<three>The same loop with "$*" collapses everything into one iteration:
bash -c 'for a in "$*"; do printf "<%s>\n" "$a"; done' demo one "two words" three<one two words three>Default to "$@" when you forward arguments to another command or loop over user input. Use "$*" only when you deliberately want one combined string.
Loop Over Arguments Safely
A for loop over "$@" is the straightforward way to handle an unknown number of arguments:
for path in "$@"; do
printf 'processing <%s>\n' "$path"
doneEach path keeps its original word boundaries, including spaces inside quoted filenames.
Use while with shift when you need to consume arguments one at a time from the front of the list. See Bash while loop for loop syntax and examples.
Process Arguments with shift
shift removes $1 from the list and promotes $2 to the new $1. Repeat it inside while [[ $# -gt 0 ]] to walk the list from left to right.
Watch $# decrease as each argument is consumed:
bash -c 'while [[ $# -gt 0 ]]; do printf "arg=%s remaining=%s\n" "$1" "$#"; shift; done' demo first second thirdarg=first remaining=3
arg=second remaining=2
arg=third remaining=1shift defaults to one position, but you can provide a number when several arguments have already been handled:
bash -c 'printf "before: \$1=%s \$2=%s \$3=%s\n" "$1" "$2" "$3"; shift 2; printf "after: \$1=%s \$2=%s \$#=%s\n" "$1" "$2" "$#"' demo first second third fourthbefore: $1=first $2=second $3=third
after: $1=third $2=fourth $#=2shift 2 removes the old $1 and $2; the previous $3 becomes the new $1.
Guard the loop with [[ $# -gt 0 ]] so you never call shift on an empty list. After the last shift, $# is 0 and the loop exits.
Validate Required Arguments
When a script requires a fixed number of arguments, check $# before using $1, $2, and other positional parameters.
Save this as check-two.sh:
#!/bin/bash
if [[ $# -ne 2 ]]; then
printf 'usage: %s SOURCE DESTINATION\n' "$0" >&2
exit 1
fi
printf 'ok: %s -> %s\n' "$1" "$2"Make it executable and invoke it with no arguments:
chmod +x check-two.sh && ./check-two.shusage: ./check-two.sh SOURCE DESTINATIONThe script exits with status 1 when the count is wrong. A valid invocation prints ok and exits 0:
./check-two.sh alpha betaok: alpha -> betaNon-zero exit status tells callers the invocation was invalid.
Set Default Values for Optional Arguments
Use parameter expansion when an argument is optional:
dest=${1:-/tmp}
printf 'destination=%s\n' "$dest"If the caller passes no operand, dest falls back to /tmp. If they pass a first operand, that value replaces the default. ${1:-/tmp} also uses /tmp when the first argument is present but empty (""). If you want the default only when the parameter is unset, use ${1-/tmp} instead.
Run it with no argument:
bash -c 'dest=${1:-/tmp}; printf "destination=%s\n" "$dest"' set-destdestination=/tmpPass a destination and the default is replaced:
bash -c 'dest=${1:-/tmp}; printf "destination=%s\n" "$dest"' set-dest /var/backupsdestination=/var/backupsThe ${var:-default} form is covered in more depth in Bash variables.
Forward Arguments to Another Command
To pass every script argument to an external program, use "$@". Each original argument stays separate, so a filename containing spaces remains one argument.
Save this wrapper as list-files.sh:
#!/bin/bash
printf 'Arguments received:\n'
printf '<%s>\n' "$@"
ls -- "$@"Create two test files, make the wrapper executable, and pass both paths quoted:
printf 'x\n' > 'my report.txt' && printf 'y\n' > 'another file.txt' && chmod +x list-files.sh && ./list-files.sh "my report.txt" "another file.txt"Arguments received:
<my report.txt>
<another file.txt>
another file.txt
my report.txtThe printf output confirms that each value remains a separate argument; ls receives the same argument list through "$@".
When the target command supports --, place it before path operands so filenames beginning with - are not interpreted as options. A copy helper might call:
cp -- "$source_path" "$dest_dir/"The -- tells cp that everything after it is a path, not an option.
Positional Parameters Inside Functions
Inside a function, $1 and $# refer to that function call, not the script's original arguments. $0 is different: calling a function does not replace $0 with the function name.
Forward the script's list with func "$@" when the function should see the same parameters the script received.
Function scope, local, and return status are covered in Bash function. This article stays focused on script-level positional parameters.
Practical Bash Script with Arguments
The script below expects exactly two arguments: a source file and a destination directory. It validates the argument count, checks that both paths exist, quotes the paths so filenames with spaces remain intact, and uses -- so a path beginning with - is not treated as an option.
Save as backup-file.sh:
#!/bin/bash
usage() {
printf 'usage: %s SOURCE DESTINATION\n' "$0" >&2
}
if [[ $# -ne 2 ]]; then
usage
exit 1
fi
source_path=$1
dest_dir=$2
if [[ ! -e "$source_path" ]]; then
printf 'error: source not found: %s\n' "$source_path" >&2
exit 1
fi
if [[ ! -d "$dest_dir" ]]; then
printf 'error: destination is not a directory: %s\n' "$dest_dir" >&2
exit 1
fi
cp -- "$source_path" "$dest_dir/"
printf 'copied %s to %s/\n' "$source_path" "$dest_dir"Prepare the example files and make the script executable:
printf 'content\n' > 'my report.txt' && mkdir -p 'backup dir' && chmod +x backup-file.shPass both spaced paths as separate arguments:
./backup-file.sh "my report.txt" "backup dir"copied my report.txt to backup dir/Confirm the file landed in the destination directory:
ls -1 "backup dir/"my report.txtCommon Argument Bugs
| Symptom | Likely cause | Fix |
|---|---|---|
| Filename split into two arguments | Unquoted spaces on the command line or unquoted $* in the script |
Quote paths: "$@", "$1", "$2" |
| Wrong value for argument 10 | Using $10 instead of ${10} |
Use ${10}, ${11}, … past $9 |
| Child command sees one merged string | "$*" passed to a command that expects separate args |
Forward with "$@" |
| Further splitting after expansion | Unquoted $@ or $* |
Use "$@" or "$*" in double quotes |
shift runs past the end of the list |
Missing [[ $# -gt 0 ]] guard |
Wrap shift in while [[ $# -gt 0 ]] |
| Path treated as an option | Operand starts with - |
If the command supports it, insert -- before paths: cp -- "$src" "$dest" |
Scripts with many flags and optional values belong in Bash getopts. Positional parameters and "$@" stay the right model for simple tools and wrappers.
Summary
Bash turns words after the script name into positional parameters. $0 is the script, $1 and $2 are the first operands, and $# counts them. Quote filenames on the command line and inside the script so spaces do not create extra parameters.
"$@" preserves each argument when you loop or forward to commands such as ls or cp. "$*" joins parameters into one word when quoted, using the first character of IFS (normally a space). Past $9, write ${10} with braces.
Validate $# early for scripts that require a fixed number of operands. Use shift or shift n with a guard to walk arguments from the front, "$@" when forwarding the full list, -- before paths when the target command supports it, and non-zero exit status with a clear usage message when the invocation is wrong.
References
- GNU Bash manual — Shell Parameters — positional parameters and special variables
- GNU Bash manual — Special Parameters —
$#,$@,$*, and$0 - GNU Bash manual — shift —
shiftbuiltin
Frequently Asked Questions
How do I access argument 10?
Use ${10} with braces. Bare $10 is parsed as $1 followed by the digit 0, not the tenth parameter.
Why should I use "$@"?
"$@" expands to every positional parameter as a separate quoted word. That preserves spaces inside filenames when you loop or pass arguments to another command. "$*" joins parameters into one word when quoted, using the first character of IFS.
What does shift do?
shift removes $1 and moves each remaining parameter down by one index. You can pass a count, such as shift 2, to discard several arguments at once. Use it inside while [[ $# -gt 0 ]] to process arguments from left to right.
When should I use getopts instead?
Use positional parameters and "$@" for simple scripts with a fixed order of operands or a handful of paths. Move to getopts or a dedicated parser when you need multiple flags, optional values, and help text for a full CLI.

