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:
| Parameter | Meaning |
|---|---|
$0 | the script's name, as it was called (./greet.sh) |
$1, $2, ... $9 | the 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]
"$@"reproduced the three arguments exactly."$*"joined them with spaces (more precisely, with the first character ofIFS) into one string. That is occasionally what you want, for a message:echo "you said: $*".- Unquoted
$@(or$*) undergoes word splitting and globbing, somy file.txtbroke apart and the empty argument vanished: all the problems from the word splitting lesson.
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
- Unquoted
$1.cat $1breaks on file names with spaces. Write"$1". $@or$*without quotes. Arguments get split and globbed. Write"$@".$10. That is$1followed by0. Write${10}.- Counting
$0in$#.$#counts only the real arguments. - Using arguments after
shiftwith old numbers. After oneshift, the old$2is$1. - Assuming
$0is a full path. It is whatever the caller typed:./s.sh,s.sh,/work/s.sh, orbashforbash -c.
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
- Arguments arrive as positional parameters
$1,$2, ...,${10};$0is the script name;$#is the count. "$@"is every argument as its own intact word: use it to pass arguments on."$*"joins them into one string. Unquoted forms split and glob.shiftdrops$1and renumbers the rest;set --replaces them.${1:-default}and${1:?message}give optional and required arguments.
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.