7.3 Process Script Inputs ($1, $2, and Beyond)
Key Takeaways
- Positional parameters $1, $2, $3… hold command-line arguments; $0 is the script name as invoked.
- $# is the argument count; "$@" is all arguments as separate words; "$*" joins arguments into one word (IFS first character)—prefer "$@" for safe iteration.
- Validate inputs with [ $# -eq N ], -z tests, and usage messages on stderr before changing the system.
- shift discards $1 and renumbers remaining parameters—useful for option parsing in simple scripts.
- Exam scripts must behave correctly for missing, extra, or wrong-type arguments: check, message, non-zero exit.
7.3 Process Script Inputs ($1, $2, and Beyond)
Quick Answer: Arguments appear as
$1,$2, …;$0is the script name;$#is how many arguments were passed; iterate safely withfor arg in "$@"; do ...; done. Check counts and values withif/[ ]before acting. Prefer"$@"over unquoted$*.
Why inputs matter on the exam
A script that only hard-codes /tmp/foo is easy to write and often wrong for the task. Graders pass arguments or expect the script to read parameters the task describes:
./mkuser.sh jdoe./backup.sh /data /mnt/backup./check.sh file1 file2 file3
You must process those inputs, not ignore them.
Positional parameters map
| Parameter | Meaning |
|---|---|
$0 | Script name as invoked (./tool.sh or full path) |
$1 | First argument |
$2 | Second argument |
$3 … $9 | Third through ninth |
${10}, ${11}, … | Tenth and beyond—use braces |
$# | Number of arguments (not counting $0) |
$* | All arguments (joining rules below) |
$@ | All arguments (joining rules below) |
$$ | PID of the shell (not an input; useful in temp names) |
$? | Exit status of last command (not an input) |
#!/bin/bash
echo "script=$0"
echo "count=$#"
echo "first=$1"
echo "second=$2"
Run:
chmod +x demo.sh
./demo.sh alpha beta
# script=./demo.sh
# count=2
# first=alpha
# second=beta
Braces for clarity and double digits
echo "${1}"
echo "${10}" # not $10 which is $1 followed by 0
"$@" vs "$*" vs unquoted forms
This distinction is a classic gotcha.
| Form | Behavior (typical) |
|---|---|
"$@" | Each argument is a separate word, even with spaces |
"$*" | All arguments joined into one word using the first IFS character (usually space) |
$@ / $* unquoted | Subject to word-splitting and globbing—unsafe |
#!/bin/bash
# Safe: preserves "My File.txt" as one argument
for f in "$@"; do
echo "arg=[$f]"
done
# Dangerous if an argument has spaces:
for f in $*; do
echo "split badly: $f"
done
Exam rule of thumb: loop with for x in "$@"; do and expand individuals as "$1", "$2".
Validating argument count
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 source_dir dest_dir" >&2
exit 2
fi
SRC="$1"
DST="$2"
Common checks:
| Test | Meaning |
|---|---|
[ $# -eq 0 ] | No arguments |
[ $# -ne 1 ] | Not exactly one |
[ $# -lt 2 ] | Fewer than two |
[ $# -gt 5 ] | Too many |
Exit code 2 is a conventional “usage error”; 1 is general failure. Be consistent within a script.
Validating argument meaning
Count alone is not enough:
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 existing-directory" >&2
exit 2
fi
if [ ! -d "$1" ]; then
echo "Not a directory: $1" >&2
exit 1
fi
if [ ! -w "$1" ]; then
echo "Directory not writable: $1" >&2
exit 1
fi
echo "OK to use $1"
Other frequent validations:
# Non-empty string
[ -z "$1" ] && { echo "empty" >&2; exit 2; }
# User exists
id "$1" &>/dev/null || { echo "no such user" >&2; exit 1; }
# Absolute path required by task
case "$1" in
/*) ;;
*) echo "need absolute path" >&2; exit 2 ;;
esac
case is optional sugar; nested if is fine on EX200.
Assigning to named variables
Immediately copy positionals into readable names after validation:
USER_NAME="$1"
COMMENT="$2"
HOME_BASE="${3:-/home}" # default if $3 unset/null
${VAR:-default} supplies a default—handy when an argument is optional.
| Expansion | Meaning |
|---|---|
${1:-default} | Use default if $1 unset or null |
${1:=default} | Assign default to $1 if unset/null (positional assignment is special—prefer named vars) |
${1:?message} | Exit if unset/null, print message |
${#1} | Length of $1 string |
Prefer named variables after the first few lines so later logic does not confuse $1 after shift.
shift — walk arguments
shift removes $1 and renumbers: old $2 becomes $1, and $# decreases by one.
#!/bin/bash
# sum.sh — add numeric arguments
if [ $# -eq 0 ]; then
echo "Usage: $0 numbers..." >&2
exit 2
fi
sum=0
while [ $# -gt 0 ]; do
sum=$((sum + $1))
shift
done
echo "$sum"
Option-style parsing (simple):
#!/bin/bash
VERBOSE=0
while [ $# -gt 0 ]; do
case "$1" in
-v) VERBOSE=1; shift ;;
-h|--help) echo "Usage: $0 [-v] file"; exit 0 ;;
--) shift; break ;;
-*) echo "Unknown option: $1" >&2; exit 2 ;;
*) break ;;
esac
done
# remaining "$@" are files
for f in "$@"; do
...
done
Full getopts is optional knowledge; simple case + shift covers many exam scripts.
Interactive input: read (when the task asks)
Most EX200 scripts take CLI args, not prompts. If a task says “ask the user,” use read:
#!/bin/bash
read -r -p "Username: " USER_NAME
if [ -z "$USER_NAME" ]; then
echo "empty name" >&2
exit 2
fi
Do not mix interactive read into scripts the grader runs non-interactively unless the task requires it—your script would hang.
Environment variables as inputs
Sometimes configuration comes from the environment:
#!/bin/bash
MODE="${MODE:-enforce}"
if [ "$MODE" != "enforce" ] && [ "$MODE" != "permissive" ]; then
echo "MODE must be enforce or permissive" >&2
exit 2
fi
Run as MODE=permissive ./script.sh. Prefer explicit $1 when the task lists command-line parameters.
End-to-end exam script
Task: Create /usr/local/bin/hostcheck.sh that takes one hostname argument, pings once, prints OK or FAIL, exits 0/1, and rejects wrong usage.
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 hostname" >&2
exit 2
fi
HOST="$1"
if ping -c1 -W2 "$HOST" &>/dev/null; then
echo "OK $HOST"
exit 0
else
echo "FAIL $HOST"
exit 1
fi
sudo tee /usr/local/bin/hostcheck.sh >/dev/null <<'EOF'
...paste script...
EOF
sudo chmod +x /usr/local/bin/hostcheck.sh
hostcheck.sh server1.example.com
echo $?
Task: Script accepts multiple files; print line counts; skip missing files with a message.
#!/bin/bash
if [ $# -eq 0 ]; then
echo "Usage: $0 file..." >&2
exit 2
fi
for f in "$@"; do
if [ -f "$f" ]; then
lines=$(wc -l < "$f")
echo "$f $lines"
else
echo "missing: $f" >&2
fi
done
Special: $0 in usage messages
Always print usage with "$0" so users see the real invocation name:
echo "Usage: $0 USER DIR" >&2
Hard-coding ./script.sh confuses when the script is installed in /usr/local/bin.
Debugging inputs
bash -x ./script.sh arg1 arg2
# or inside script temporarily:
printf 'DEBUG: count=%s a1=%q a2=%q\n' "$#" "$1" "$2" >&2
%q shell-escapes values for visibility of spaces/newlines.
Common traps
- Using
$1aftershiftwithout re-checking — it is a different value now. $#vs${#1}— count of args vs length of first arg string.- Unquoted
$@— breaks on spaces. - Forgetting
${10}braces —$10≠ tenth argument. - No usage message / always exit 0 — grader cannot distinguish success from silent misuse.
- Reading
$1but documenting two required args — validate$#honestly. - Assuming interactive
readwhen automation supplies args only.
Relationship to other sections
Inputs feed conditionals (empty? exists?) and loops (for arg in "$@"). The next section shows how to capture command output into variables that may also become inputs to later decisions.
Section checkpoint
You should read $1/$2/…, report $#, iterate with "$@", understand "$*" joining, shift through a list, validate counts and types, set defaults with ${var:-default}, print usage via $0, and exit non-zero on bad input—core EX200 script input processing.
A script is run as ./deploy.sh web /var/www. What are $0, $1, $2, and $# inside the script?
Why should you iterate arguments with for f in "$@"; do rather than for f in $*; do when paths may contain spaces?
What does the shift builtin do?
Which snippet best rejects wrong usage when a script requires exactly one argument?