Read

Module 9 · Shell scripting basics

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

Make scripts that take input from the command line: positional parameters, counting them, looping over all of them safely and consuming options with shift.

What you will learn

  • Read `$0`, `$1`…`$9`, `${10}`, `$#` and tell `$@` from `$*`.
  • Validate the argument count and print a usage message to stderr.
  • Use `shift` to walk through arguments and handle simple options.

Everything you type after a script's name arrives inside it as positional parameters: $1 is the first word, $2 the second, and so on up to $9; from ten on you need braces, ${10}. $0 is the name the script was invoked with, useful in error messages. $# is how many arguments there are, and $@ is all of them. These are the same variables you met inside functions in the previous lesson, now at script level.

~% cat lab/l88/args.sh
#!/bin/sh
echo "script: $0"
echo "count:  $#"
echo "first:  $1"
echo "all:    $@"
~% lab/l88/args.sh red "light blue" green
script: lab/l88/args.sh
count:  3
first:  red
all:    red light blue green

Checking what you were given

A script that needs a file name should refuse to run without one, and say how to use it. The convention is a usage line on standard error and a non-zero exit, so that the caller, a human or another script, knows something went wrong. Use $# for the count and -z "$1" when an empty argument is also unacceptable.

#!/bin/sh
if [ $# -lt 2 ]; then
    echo "usage: $0 SOURCE DEST" >&2
    exit 1
fi
src="$1"
dst="$2"
cp "$src" "$dst" && echo "copied $src to $dst"

$@ versus $*

Both expand to all the arguments, and unquoted they behave identically, which is to say badly: words get split again. The difference appears in double quotes. "$*" is one string with the arguments joined by spaces. "$@" is each argument as its own word, spaces inside preserved. When you pass arguments on to another command or loop over them, "$@" is almost always what you mean; "$*" is for building a single message.

~% cat lab/l88/each.sh
#!/bin/sh
for a in "$@"; do echo "arg: $a"; done
echo "---"
for a in $*; do echo "word: $a"; done
~% lab/l88/each.sh "light blue" green
arg: light blue
arg: green
---
word: light
word: blue
word: green
ExpressionMeaning
$0Script name as invoked
$1 … $9, ${10}Individual arguments
$#Number of arguments
"$@"All arguments, one word each
"$*"All arguments joined into one word
shift [N]Drop the first N (default 1) arguments

shift

shift throws away $1 and renumbers the rest: the old $2 becomes $1, $# drops by one. It is how you process arguments one at a time, or peel off options before the real arguments. shift 2 drops two at once. A loop while [ $# -gt 0 ]; do … shift; done visits every argument and also lets you react to flags like -v. For anything more elaborate the getopts builtin exists, but the shift loop covers most small tools.

#!/bin/sh
verbose=0
if [ "$1" = "-v" ]; then
    verbose=1
    shift
fi
echo "$# files to process: $*"
[ "$verbose" -eq 1 ] && echo "(verbose mode)"

Commands in this lesson

CommandWhat it does
$1 $2 … ${10}Positional arguments.
$#Number of arguments.
"$@"All arguments, safely separated.
"$*"All arguments as one string.
shiftDiscard $1, renumber the rest.
echo "usage: $0 FILE" >&2; exit 1Standard usage error.

Quiz

  1. `./s.sh a b c` — what is `$#`?

    • `a`
    • `3`
    • `./s.sh`
  2. Which loop keeps an argument like `"my file"` as one item?

    • `for a in $*`
    • `for a in $@`
    • `for a in "$@"`
  3. After `shift`, what happened to the old `$2`?

    • It is now `$1`
    • It is gone
    • It is now `$3`
  4. Where should a usage message go, and with what exit code?

    • stdout, exit 0
    • stderr (`>&2`), non-zero exit
    • A file in /tmp

Practice

  1. Write an executable script `/root/lab/l88/args.sh` that prints only the number of arguments it received: `./args.sh a b c` prints `3`.

  2. Write an executable script `/root/lab/l88/each.sh` that prints `arg: X` on its own line for every argument X, keeping arguments with spaces whole: `./each.sh "a b" c` prints `arg: a b` then `arg: c`.

  3. Write an executable script `/root/lab/l88/drop.sh` that discards its first argument with `shift` and prints the remaining ones on one line: `./drop.sh x y z` prints `y z`.

Open this lesson in the app to do the tasks in a real Linux machine and have them checked.