Your First Scripts · about 45 min

Arguments: $1, $#, "$@" and shift

Why this matters

A script that always does the same thing to the same file is of limited use. Arguments let the caller say what to work on: ./backup.sh /home/ada, ./resize.sh 800 *.jpg, ./deploy.sh staging. This lesson shows how arguments arrive in a script, the special parameters that give you access to them, and the one form, "$@", that passes them along without damage.

Where arguments come from

When you type ./greet.sh Ada "Grace Hopper", bash expands and splits the line as usual, then calls execve with an argument vector, a list of strings:

 argv[0] = "./greet.sh"     (via the shebang, bash receives: /bin/bash ./greet.sh Ada "Grace Hopper")
 argv[1] = "Ada"
 argv[2] = "Grace Hopper"

The bash that runs the script stores these as positional parameters:

ParameterMeaning
$0the script's name, as it was called (./greet.sh)
$1, $2, ... $9the arguments
${10}, ${11}, ...arguments beyond nine (braces required)
$#how many arguments there are (not counting $0)
"$@"all arguments, each as a separate word
"$*"all arguments joined into a single word

Note that the quoting was done by the caller's shell: the script just receives Grace Hopper as one string in $2. Whether it stays one string inside the script depends on how the script quotes $2.

#!/bin/bash
# args.sh: show positional parameters
echo "script: $0"
echo "count:  $#"
echo "first:  $1"
echo "second: $2"
$ ./args.sh Ada "Grace Hopper"
script: ./args.sh
count:  2
first:  Ada
second: Grace Hopper
$ bash args.sh one
script: args.sh
count:  1
first:  one
second:

An argument that was not given is simply unset, and expands to nothing.

${10} needs braces because $10 means $1 followed by the character 0. Scripts with that many positional arguments are rare; that is usually a sign they should use options or shift.

$0 and naming yourself

$0 is useful in messages, so the message names the script correctly however it was invoked. basename strips the directory part:

echo "usage: $(basename "$0") FILE" >&2

All the arguments: "$@" versus "$*"

To hand all arguments to another command, there is exactly one right answer: "$@", with the double quotes. It is special: although it is inside one pair of quotes, it expands to one word per argument, each kept intact, including empty ones.

#!/bin/bash
# compare.sh
printf '[%s]\n' "$@"; echo ---
printf '[%s]\n' "$*"; echo ---
printf '[%s]\n' $@
$ ./compare.sh "my file.txt" notes.txt ""
[my file.txt]
[notes.txt]
[]
---
[my file.txt notes.txt ]
---
[my]
[file.txt]
[notes.txt]

So a wrapper script, one that adds something and then calls another program with the caller's arguments, looks like this:

#!/bin/bash
# mygrep: grep with color and line numbers always on
grep --color=auto -n "$@"

shift: moving along the list

shift throws away $1 and moves every other parameter down by one: $2 becomes $1, $3 becomes $2, and $# decreases by one. shift N drops N at once.

#!/bin/bash
# tag.sh: first argument is a tag, the rest are files
tag=$1
shift
echo "tagging $# files with '$tag'"
printf '  %s\n' "$@"
$ ./tag.sh urgent a.txt "b c.txt"
tagging 2 files with 'urgent'
  a.txt
  b c.txt

This "take what you need from the front, then process the rest" shape is how most scripts handle a fixed leading argument followed by a list. With loops (module 10) it also becomes the classic way to process options one at a time.

shift when there are no parameters left does nothing and returns a failure status (1); it does not crash the script.

Setting positional parameters: set --

You can replace the positional parameters of the current shell with set --:

$ set -- alpha "beta gamma" delta
$ echo $#
3
$ echo "$2"
beta gamma

That is mostly useful for experimenting in an interactive shell, as here, and for a few tricks in scripts. set -- with nothing after it clears them all.

Optional arguments with defaults

Combine positional parameters with the parameter expansion operators from the last module:

src=${1:?usage: backup.sh SOURCE [DEST]}
dest=${2:-/var/backups}

Here the first argument is required: if it is missing, the script stops with the usage text as its error. The second is optional with a default. It is a compact, honest way to document and enforce how a script is called; the next lessons improve on the message.

Common mistakes

Exercises

The code exercises are run with arguments, including ones that contain spaces, a lone *, and empty strings, so quote carefully. Test in the lesson shell with your own arguments: bash script.sh one "two three" "".

Recap

Going further: in man bash, search /Special Parameters and read about $@ and $*. Write a three-line wrapper script around ls that always adds -l and passes everything else through with "$@", then call it with a file name containing a space.

Exercises

Exercise 1 · quiz

"$@" or "$*"?

A script is run as ./s.sh "my file.txt" notes.txt. Inside it, which line passes exactly the two original file names, my file.txt and notes.txt, on to ls?

Answer
  • ls "$@"

"$@" expands to each positional parameter as a separate, intact word: exactly the arguments the script received. "$*" joins them all into one word (my file.txt notes.txt), and the unquoted forms are word-split, so my file.txt breaks into my and file.txt. Whenever you pass a script's arguments along, write "$@" with the quotes.

Exercise 2 · write code

Hello, arguments

Write a script that prints two lines using its arguments:

Hello, FIRST!
You gave N arguments.

where FIRST is the first argument and N is the number of arguments. For example bash script.sh Ada Grace Linus prints:

Hello, Ada!
You gave 3 arguments.

With no arguments at all, FIRST is replaced by nobody (use a parameter expansion default) and N is 0.

File: script.sh

Starting point

echo "Hello, $1!"

Example with arguments Ada Grace Linus

expected output

Hello, Ada!
You gave 3 arguments.

Example with arguments Ada Lovelace

expected output

Hello, Ada Lovelace!
You gave 1 arguments.
Hint 1

$1 is the first argument, $# the count.

Hint 2

${1:-nobody} uses nobody when $1 is unset or empty.

Reference solution
echo "Hello, ${1:-nobody}!"
echo "You gave $# arguments."

Exercise 3 · write code

Show me the arguments

Write a script that helps debug quoting: it prints each argument on its own line, wrapped in angle brackets, and then a line count: N. For bash script.sh a "b c" "" the output is:

<a>
<b c>
<>
count: 3

Spaces inside arguments and empty arguments must be preserved exactly. You can assume at least one argument is given. Use one printf for the bracketed lines.

File: script.sh

Starting point

printf '<%s>\n' $*
echo "count: $#"

Example with arguments a b c

expected output

<a>
<b c>
<>
count: 3

Example with arguments x *

expected output

< x >
<*>
count: 2
Hint 1

$* unquoted splits arguments apart and drops empty ones.

Hint 2

printf '<%s>\n' "$@" repeats the format once per argument, keeping each one intact.

Reference solution
printf '<%s>\n' "$@"
echo "count: $#"

Exercise 4 · write code

First and the rest

Write a script that treats its first argument as a label and the rest as items. It prints:

label: LABEL
items: N
ITEM1
ITEM2
...

For bash script.sh fruit apple "blood orange" kiwi:

label: fruit
items: 3
apple
blood orange
kiwi

Use shift to drop the label, then work with what remains. You can assume there is at least one argument (the label); with no items, items: 0 is the last line (a trailing empty line is ignored by the checker).

File: script.sh

Starting point

label=$1
echo "label: $label"

Example with arguments fruit apple blood orange kiwi

expected output

label: fruit
items: 3
apple
blood orange
kiwi

Example with arguments empty

expected output

label: empty
items: 0
Hint 1

After shift, $1 is the old $2, and $# is one smaller.

Hint 2

Then "$@" holds only the items: printf '%s\n' "$@". (With no items, printf still prints its format once, an empty line; trailing blank lines are not compared here.)

Reference solution
label=$1
shift
echo "label: $label"
echo "items: $#"
printf '%s\n' "$@"