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

Tech reviewed: Deepak Prasad
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:

text
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

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 == -* ]]).

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 over a decade 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, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security