Positional parameters and shift
◈ 10 cardsRead a script's arguments through the read-only variables, consume them with shift, choose correctly between the two all-arguments forms, and rebind the parameters with set --.
Arguments arrive as numbered variables
A script gets its command-line arguments through a set of variables the shell fills in before the first line runs. They are read-only: you can read them, and shift and set can rebind them, but you cannot assign to one directly.
Here is a script called args:
echo "name: $0"
echo "count: $#"
echo "all: $@"
echo "first three: $1 $2 $3"
Run it with five arguments — sh args alpha beta gamma delta epsilon — and it prints:
name: args
count: 5
all: alpha beta gamma delta epsilon
first three: alpha beta gamma
$0 is the name the script was invoked by, which is not always the filename on disk. $# counts the arguments and does not include 1 through $9 are the arguments themselves, in order.
Nine is a real limit on a traditional Bourne shell: a single digit is all the syntax allows, and ${10} is a later addition that the shell this course assumes may not have. That limitation is the entire reason shift exists.
shift consumes, and it does not give anything back
shift moves every positional parameter down one place. The old $2 becomes $1, the old $3 becomes $2, $# drops by one, and the old first argument is gone. There is no stack, no history, no way to look at it again.
Starting from the five arguments above, one shift leaves four: $# is 4, and $1 3 prints beta gamma delta. A further shift 3 shifts three places at once and leaves one: $# is 1, $1 is epsilon, and $2 and $3 are the null string.
One more rule, and it is the one people guess wrong. shift N where N is greater than $# does nothing at all and returns a non-zero status. It does not empty the list and it does not shift as far as it can.
The idiomatic use is a loop that processes an argument and then discards it:
while [ $# -gt 0 ]
do
echo "handling $1"
shift
done
Two ways to say "all the arguments", and only one of them is right
$@ and $* both expand to all the positional parameters. Unquoted they behave identically, and identically badly: the result goes through word splitting and globbing, so an argument containing a space becomes two arguments and an argument containing * becomes a list of filenames.
Quoted, they differ, and the difference is the whole point:
"$@"expands to one word per argument, each preserved exactly. An argument containing spaces stays a single argument."$*"expands to one single word: every argument joined together, separated by the first character ofIFS, which is normally a space.
Watch it happen. Run a script with two arguments, where the first is the two-word string two words and the second is second:
$ for f in "$@"; do echo "[$f]"; done
[two words]
[second]
$ for f in "$*"; do echo "[$f]"; done
[two words second]
The first loop runs twice and keeps the space inside argument one. The second loop runs once, over a single fused string, and the boundary between the two arguments is lost forever.
The rule to carry into every script you write: use "$@", with the quotes. Reach for "$*" only when you deliberately want one string — and even then it is usually clearer to build that string on purpose than to let the shell fuse it behind your back.
set -- and the last two read-only variables
set with arguments rebinds the positional parameters to those arguments, which is how you take apart the fields of a command's output without writing a loop:
set -- $(ls -l report.txt)
echo "mode: $1"
echo "size: $5"
The -- is not decoration. A long listing begins with a permission string such as -rw-r--r--, which starts with a hyphen, so without -- the set command would try to read it as a bundle of options and fail. -- means "no more options; everything after this is an operand". The same convention rescues you elsewhere: it is how you rm a file whose name begins with a hyphen.
Two more read-only variables complete the collection. $$ holds the process ID of the current shell, which is the conventional way to build a temporary filename that no other run of the script will collide with. $! holds the process ID of the most recent command you put in the background, and it is what you hand to wait or kill afterwards.