Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 15 · Writing and Running Bash Scripts

Positional Parameters: $1, $@, $# and shift

A script that takes no input does one thing. A script that takes arguments is a tool. The mechanism is half a dozen special variables, and one of them has a quoting rule that is the difference between a wrapper that works and one that corrupts every path containing a space.

Intermediate18 min readLinuxPart IFundamentalsScripting

Part I · Module 15 · Chapter 15.4 · Positional Parameters

Chapter 15.3 listed arguments as one of the four things a script receives. This is them.

1. The Variables

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./args.sh alpha "two words" gamma
  $0 = [./args.sh]
  $# = 3
  $1 = [alpha]  $2 = [two words]  $3 = [gamma]
VariableHolds
$0How the script was invoked
$1, $2, $3, …The arguments, in order
$#How many arguments there are
"$@"All of them, as separate words
"$*"All of them, joined into one

Note that "two words" arrived as a single argument. The quotes were the caller's, consumed by their shell at Chapter 9.1's quote-removal stage — but the word stayed whole, and $2 holds all nine characters.

$0 is not counted in $#. Three arguments means $# is 3 and the script's own name is separate.

2. The Three Forms, and Only One Is Right

This is the chapter's critical content. A script that passes its arguments to another command has three ways to spell it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./at.sh alpha "two words" gamma
  passing "$@":
    received 3 arguments
    [alpha]
    [two words]
    [gamma]
  passing  $@ :
    received 4 arguments
    [alpha]
    [two]
    [words]
    [gamma]
  passing "$*":
    received 1 arguments
    [alpha two words gamma]

Three, four and one. Same input, three different results.

Three ways to pass a script's arguments on, and only one of them is correct. Quoted at-sign gives one word per original argument, whatever any of them contained. Unquoted at-sign splits every argument again at its spaces. Quoted star joins them all into a single argument.Quoted at-signone word per original argument— the only correct formUnquoted at-signevery argument split again atits spacesQuoted starall of them joined into asingle argument12
Figure 1 — three ways to pass a script's arguments on, and only one of them is correct. The quoted at-sign gives exactly one word per original argument, whatever any of them contained. The unquoted at-sign splits every argument again at its spaces, so a path with a space in it becomes two. The quoted star joins them all into a single argument, which is occasionally what you want for a message and never what you want for passing arguments on.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# a wrapper that adds one flag and passes everything else through
exec ./real_tool --verbose "$@"

That is the whole shape of a wrapper script, and $@ without its quotes would break it on the first path containing a space.

When "$*" is right

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo "running with: $*"                 # a message for a person
log "invoked as $0 $*"

"$*" joins the arguments with the first character of IFS — a space by default. It is for describing the arguments, never for passing them. A log line is the main use.

With no arguments at all

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./args.sh
  $# = 0
  "$@" one per line:
    []

One empty line, from printf '[%s]\n' "$@" with nothing to print — because printf runs its format once even when given no arguments. "$@" itself expanded to nothing, which is correct: zero arguments produce zero words, not one empty one.

That distinction matters when passing through: cmd "$@" with no arguments runs cmd with none, which is what you want. cmd "$*" would run it with one empty argument.

3. Counting and Checking

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if [ "$#" -lt 1 ]; then
  echo "usage: $0 <test-name> [seed]" >&2
  exit 2
fi

$# is how a script checks it was called properly, and a usage message with a documented exit code is the minimum interface (Chapter 15.6).

The shorter form, from Chapter 8.2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
test_name="${1:?usage: $0 <test-name> [seed]}"
seed="${2:-0}"

:? for required, :- for optional with a default. Two lines, no if, and the error names what is missing. It is the form to reach for when the script takes one or two arguments.

4. shift

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./sh.sh a b c d
  before: $#=4 $1=[a]
  after 1 shift: $#=3 $1=[b]
  after shift 2: $#=1 $1=[d]
  shift past the end FAILS (status 1)

shift discards $1 and moves everything down, so $2 becomes $1 and $# decreases. shift N does it N times.

It is how a script consumes a leading argument and passes the rest on:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# usage: wrapper.sh <mode> [args to pass through...]
mode="${1:?usage: $0 <mode> [args...]}"
shift

case "$mode" in
  fast) exec ./real_tool --quick "$@" ;;
  full) exec ./real_tool --thorough "$@" ;;
  *)    echo "unknown mode: $mode" >&2; exit 2 ;;
esac

After shift, "$@" is everything except the mode. That pattern — take one, pass the rest — is most of what shift is for. (case is Module 16.)

Shifting past the end fails with status 1 rather than erroring, which is occasionally useful as a loop condition and is mostly a thing to know so it does not surprise you.

5. Looping Over Arguments

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for arg in "$@"; do
  echo "processing $arg"
done

for arg in "$@" is the safe loop, and the quotes are the same rule as everywhere else. for arg in $@ splits each argument again.

A useful shorthand: for arg; do with no in clause iterates over "$@" implicitly, and is impossible to get wrong because there is nothing to quote:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for arg; do
  echo "processing $arg"
done

(for is Chapter 17.1; this one form is worth having now.)

The shift-based alternative, for when you need to consume arguments as you go:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
while [ "$#" -gt 0 ]; do
  echo "handling $1"
  shift
done

6. Replacing the Arguments

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ set -- x "y z"
$ printf '[%s]\n' "$@"
[x]
[y z]
$ echo "count=$#"
count=2

set -- replaces the positional parameters. The -- is required so that an argument starting with a hyphen is not read as an option (Chapter 9.6's -- rule).

Two real uses:

Setting defaults when none were given:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[ "$#" -eq 0 ] && set -- "$(cat default_tests.txt)"

Splitting a string into arguments, deliberately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
IFS=, read -r a b c <<< "$csv_line"     # usually better — Chapter 8.6
set -- $unquoted_flag_string            # deliberate splitting — Chapter 9.2

That second one is Chapter 9.2's legitimate unquoted expansion, and like all of them it deserves a comment saying it is intentional.

7. Where Flags Belong

Everything above is positional — the meaning comes from the order. That works for one or two arguments and stops working beyond that.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./run_sim.sh alu 7 fast /tmp/out yes

Nobody can read that, and adding a sixth means changing every caller.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./run_sim.sh --test alu --seed 7 --mode fast --out /tmp/out --verbose

Named options are self-describing, order-independent and extensible. Bash has getopts for parsing them, and it is Chapter 21.2's subject.

8. A Wrapper Script

The shape that uses everything in this chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
#
# sim — run one simulation, with sensible defaults.
#
# usage: sim <test-name> [seed]
#        sim --help
#
# exit: 0 passed · 1 failed · 2 bad usage · 3 simulator not found

set -euo pipefail

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

usage() {
  sed -n '3,8p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}

# --help before anything else, so it works even when nothing is configured.
if [ "$#" -ge 1 ] && [ "$1" = "--help" ]; then
  usage
  exit 0
fi

test_name="${1:?usage: $0 <test-name> [seed]}"
seed="${2:-0}"

command -v vcs >/dev/null 2>&1 || { echo "vcs not on PATH" >&2; exit 3; }

echo "test: $test_name  seed: $seed"
exec "$script_dir/run_one.sh" --name "$test_name" --seed "$seed"

Five things worth naming:

  • --help is checked before the required argument, so asking for help does not produce a usage error.
  • [ "$#" -ge 1 ] && guards the $1 test, so it is safe with no arguments even under set -u.
  • usage reads the comment block out of the script itself, so the documentation cannot drift from the help text.
  • ${1:?} and ${2:-0} give the required-and-optional pair in two lines.
  • exec replaces the script's process with the real tool, so there is no extra shell waiting around and the tool's exit status becomes the script's directly.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/pp && cd /tmp/pp

1 — See the basic variables.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > args.sh <<'EOF'
#!/bin/bash
echo "  \$0=[$0]  \$#=$#"
echo "  \$1=[$1] \$2=[$2] \$3=[${3-unset}]"
EOF
chmod +x args.sh
./args.sh alpha "two words" gamma
./args.sh

2 — Prove the three forms differ.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > at.sh <<'EOF'
#!/bin/bash
f() { echo "    $# arguments:"; printf '      [%s]\n' "$@"; }
echo '  "$@":'; f "$@"
echo '   $@ :'; f $@
echo '  "$*":'; f "$*"
EOF
chmod +x at.sh
./at.sh alpha "two words" gamma

Three, four and one.

3 — Meet the tenth argument.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/bash\necho "  \\$10=[$10]  \\${10}=[${10}]"\n' > ten.sh
chmod +x ten.sh
./ten.sh a b c d e f g h i j

4 — Use shift.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > sh.sh <<'EOF'
#!/bin/bash
echo "  start: \$#=$# \$1=[$1]"
shift;   echo "  shift:  \$#=$# \$1=[$1]"
shift 2; echo "  shift2: \$#=$# \$1=[${1-none}]"
shift 99 2>/dev/null || echo "  shift past the end: status $?"
EOF
chmod +x sh.sh
./sh.sh a b c d

5 — Write a pass-through wrapper.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > wrap.sh <<'EOF'
#!/bin/bash
mode="${1:?usage: $0 <mode> [args...]}"
shift
echo "  mode=$mode, passing $# arguments on:"
printf '    [%s]\n' "$@"
EOF
chmod +x wrap.sh
./wrap.sh fast alpha "two words"

6 — Required and optional in two lines.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > req.sh <<'EOF'
#!/bin/bash
name="${1:?usage: $0 <name> [seed]}"
seed="${2:-0}"
echo "  name=$name seed=$seed"
EOF
chmod +x req.sh
./req.sh alu
./req.sh alu 7
./req.sh; echo "  status $?"

7 — Loop two ways.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > loop.sh <<'EOF'
#!/bin/bash
for a in "$@"; do echo "  explicit: [$a]"; done
for a;       do echo "  implicit: [$a]"; done
EOF
chmod +x loop.sh
./loop.sh one "two words"

8 — Replace the arguments.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bash -c 'set -- x "y z"; echo "count=$#"; printf "[%s]\n" "$@"'

9 — Clean up. cd /tmp && rm -rf /tmp/pp

Check Your Understanding

1. $# is 3. Does that include the script's name?

2. Give the argument count each of "$@", $@ and "$*" produces from alpha "two words" gamma, and say which to use.

3. $10 gives a0. What happened?

4. After shift, what is in $1 and what is $#?

5. When is "$*" the right choice?

6. Why does a wrapper's last line often use exec?

Tools have their own argument conventions on top of this one — Chapter 27.4 covers plusargs, where a misspelled name is accepted without complaint and the default runs instead.

9. Remember

  • $0 is the invocation, $1…$9 are the arguments, $# is how many — and $0 is not counted.
  • Past nine, use braces. $10 is $1 followed by a 0, and it fails silently.
  • "$@" is special-cased to expand to one word per argument, whatever they contain.
  • It is the only correct way to pass a set of arguments on. $@ splits them; "$*" joins them.
  • "$*" is for describing arguments in a message, never for passing them.
  • With no arguments, "$@" expands to nothing — zero words, not one empty one.
  • ${1:?usage: …} and ${2:-default} give required-and-optional in two lines, with no if.
  • shift discards $1 and moves the rest down; shifting past the end returns 1.
  • Take one, shift, pass "$@" on is the wrapper pattern.
  • for arg; do iterates over "$@" implicitly and cannot be misquoted.
  • set -- replaces the arguments, and the -- matters.
  • One or two arguments can be positional; three or more want named options and Chapter 21.2's getopts.
  • exec at the end of a wrapper saves a process and forwards the status and signals for free.
  • A function gets its own set of these, and the caller's come back on return — Chapter 18.2 measures which ones, and the one that is not replaced.

A script can now be told what to do. The next chapter is about somebody else being able to read how.

Where this fits

Part of the Linux & Bash curriculum.