Bash Script Arguments: $1, $#, $@, $* and shift

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
Short on time?Beta Get the gist of this 10-minute read in seconds, then ask follow-up questions

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:

text
#!/bin/bash
printf 'ninth=%s tenth=%s eleventh=%s\n' "$9" "${10}" "${11}"

Make it executable and pass eleven words:

bash
chmod +x show-tenth.sh && ./show-tenth.sh one two three four five six seven eight nine ten eleven
output
ninth=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
bash -c 'printf "count=%s first=%s second=%s\n" "$#" "$1" "$2"' demo.sh alpha "b c"
output
count=2 first=alpha second=b c

Without 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
bash -c 'printf "argument count=%s\n" "$#"' demo.sh alpha "b c"
output
argument count=2

Use $# 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
bash -c 'for a in "$@"; do printf "<%s>\n" "$a"; done' demo one "two words" three
output
<one>
<two words>
<three>

The same loop with "$*" collapses everything into one iteration:

bash
bash -c 'for a in "$*"; do printf "<%s>\n" "$a"; done' demo one "two words" three
output
<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:

bash
for path in "$@"; do
  printf 'processing <%s>\n' "$path"
done

Each 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
bash -c 'while [[ $# -gt 0 ]]; do printf "arg=%s remaining=%s\n" "$1" "$#"; shift; done' demo first second third
output
arg=first remaining=3
arg=second remaining=2
arg=third remaining=1

shift defaults to one position, but you can provide a number when several arguments have already been handled:

bash
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 fourth
output
before: $1=first $2=second $3=third
after:  $1=third $2=fourth $#=2

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

text
#!/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:

bash
chmod +x check-two.sh && ./check-two.sh
output
usage: ./check-two.sh SOURCE DESTINATION

The script exits with status 1 when the count is wrong. A valid invocation prints ok and exits 0:

bash
./check-two.sh alpha beta
output
ok: alpha -> beta

Non-zero exit status tells callers the invocation was invalid.


Set Default Values for Optional Arguments

Use parameter expansion when an argument is optional:

bash
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
bash -c 'dest=${1:-/tmp}; printf "destination=%s\n" "$dest"' set-dest
output
destination=/tmp

Pass a destination and the default is replaced:

bash
bash -c 'dest=${1:-/tmp}; printf "destination=%s\n" "$dest"' set-dest /var/backups
output
destination=/var/backups

The ${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:

text
#!/bin/bash
printf 'Arguments received:\n'
printf '<%s>\n' "$@"
ls -- "$@"

Create two test files, make the wrapper executable, and pass both paths quoted:

bash
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"
output
Arguments received:
<my report.txt>
<another file.txt>
another file.txt
my report.txt

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

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

text
#!/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:

bash
printf 'content\n' > 'my report.txt' && mkdir -p 'backup dir' && chmod +x backup-file.sh

Pass both spaced paths as separate arguments:

bash
./backup-file.sh "my report.txt" "backup dir"
output
copied my report.txt to backup dir/

Confirm the file landed in the destination directory:

bash
ls -1 "backup dir/"
output
my report.txt

Common 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)