Bash script arguments and parameters: `$1`, `$#`, `shift`, and parsing flags

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
bash -c 'echo "arg count=$#"; printf "arg: <%s>\n" "$@"; echo "first=$1 second=$2"' \
  bash alpha "b c"

Output:

output
arg count=2
arg: <alpha>
arg: <b c>
first=alpha second=b c

Quoting "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:

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

After 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

For case syntax, patterns, and a service-style $1 parser, see Bash case statement. Here, a hand-rolled loop 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 == -* ]]).

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

text
name=alice n=3
name=bob n=2

Missing value after --name:

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

bash
if [[ $# -ne 2 ]]; then
  printf 'usage: %s INPUT OUTPUT\n' "$0" >&2
  exit 1
fi

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

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 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)