Zsh read, $1, getopts Explained - Shell Scripting Tutorial (Lesson 8)
Video: Zsh read, $1, getopts Explained - Shell Scripting Tutorial (Lesson 8) by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 8: Script Input — $1, read, getopts
Scripts get arguments via
$1,$2, ...$@is all args,$#is count,$0is the script name.readfor interactive prompts.getoptsfor short option flags-n alice -a 25.
A script that doesn't take input is a one-shot. Real tools accept arguments — names, paths, flags.
Positional arguments: $1 $2 $3 ...
#!/usr/bin/env zsh
echo "Script name: $0"
echo "First: $1"
echo "Second: $2"
echo "Third: $3"
echo "All: $@"
echo "Count: $#"
./show_args.sh hello world foo
# Script name: ./show_args.sh
# First: hello
# Second: world
# Third: foo
# All: hello world foo
# Count: 3
Special vars:
$0— the script's name.$1,$2, ... — positional arguments.$@— all positional arguments as separate words (when quoted:"$@").$*— all positional arguments as one string.$#— number of positional arguments.
$@ vs $*
args=("$@") # array — preserves separations
echo $# # count
The difference matters with quoting:
# Args: "hello world" "foo"
# "$@" → "hello world" "foo" (two args, preserved)
# "$*" → "hello world foo" (one string, joined by IFS)
Always use "$@" (quoted) when forwarding all arguments to another command. It's the only form that preserves args correctly.
Defaults for missing args
NAME="${1:-World}"
AGE="${2:-25}"
echo "Hello, $NAME! Age: $AGE"
./greet.sh
# Hello, World! Age: 25
./greet.sh Alice 30
# Hello, Alice! Age: 30
${VAR:-default} returns default if VAR is empty or unset. Standard pattern for optional args.
Validating required args
if [[ -z "$1" ]]; then
echo "Usage: $0 <name>" >&2
exit 1
fi
NAME="$1"
echo "Hello, $NAME!"
-z tests "zero length." Refuse to run without the required argument; print usage to stderr; exit non-zero.
$$, $?, $!
A few more special variables:
echo "PID: $$" # current shell's process ID
echo "Last exit: $?" # exit status of last command
echo "Last bg PID: $!" # PID of last background command
$? is huge. After every command, $? holds its exit code. Use it to check success:
some_command
if [[ $? -ne 0 ]]; then
echo "It failed!" >&2
fi
Or directly with if:
if some_command; then
echo "ok"
else
echo "failed" >&2
fi
read: interactive input
echo "What's your name?"
read NAME
echo "Hi, $NAME!"
read reads a line from stdin, splits on whitespace by default, and assigns to the named variables.
read -p "Name: " NAME # prompt inline
read -p "Age: " AGE
read -sp "Password: " PASS # -s = silent (no echo)
echo # newline after silent input
read -t 5 -p "Hurry: " ANSWER # -t timeout in seconds
Multiple variables:
read FIRST LAST <<< "Alice Smith"
echo "$FIRST $LAST"
# Alice Smith
Reading a file line by line
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
IFS= keeps leading/trailing whitespace. -r disables backslash interpretation. The combination is the canonical "read a file" pattern.
getopts: short flags (-n alice)
For tools with options like -n alice -a 25:
#!/usr/bin/env zsh
# profile.sh - Option parsing with getopts
name="Unknown"
age="N/A"
while getopts "n:a:h" opt; do
case $opt in
n) name=$OPTARG ;;
a) age=$OPTARG ;;
h)
echo "Usage: $0 [-n name] [-a age] [-h]"
exit 0
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
echo "Name: $name"
echo "Age: $age"
./profile.sh -n Alice -a 25
# Name: Alice
# Age: 25
./profile.sh -h
# Usage: ./profile.sh [-n name] [-a age] [-h]
How getopts works:
getopts "n:a:h" opt— defines flags.n:anda:take values;hdoesn't.- The current option ends up in
$opt; the value (if any) in$OPTARG. getoptsreturns 0 while there are options to parse, non-zero when done.
After parsing, shift past the options:
shift $((OPTIND - 1))
echo "Remaining args: $@"
$OPTIND is the index of the next argument to process — what's left after all flags.
Long options (--name)
getopts only supports short options. For long options (--name alice), you have two choices:
- Loop with case — manual parsing:
while [[ $# -gt 0 ]]; do
case "$1" in
--name) NAME="$2"; shift 2 ;;
--age) AGE="$2"; shift 2 ;;
--help) echo "Usage..."; exit 0 ;;
*) echo "Unknown: $1" >&2; exit 1 ;;
esac
done
- Use
getopt(nos) — GNU extended parser. Available on Linux; needs install on macOS (brew install gnu-getopt).
For most scripts, the manual while/case loop is fine.
A complete script with all input methods
#!/usr/bin/env zsh
set -euo pipefail
# Defaults
NAME="${USER}"
COUNT=1
VERBOSE=0
# Parse short options
while getopts "n:c:vh" opt; do
case $opt in
n) NAME="$OPTARG" ;;
c) COUNT="$OPTARG" ;;
v) VERBOSE=1 ;;
h)
echo "Usage: $0 [-n name] [-c count] [-v] [GREETING]"
exit 0
;;
\?) exit 1 ;;
esac
done
shift $((OPTIND - 1))
GREETING="${1:-Hello}"
# Validate
if [[ ! "$COUNT" =~ ^[0-9]+$ ]]; then
echo "Error: count must be a number" >&2
exit 2
fi
# Run
for ((i = 1; i <= COUNT; i++)); do
if (( VERBOSE )); then
echo "[iteration $i] $GREETING, $NAME!"
else
echo "$GREETING, $NAME!"
fi
done
./greet.sh -n Alice -c 3 Howdy
# Howdy, Alice! (x3)
./greet.sh -v -c 2
# [iteration 1] Hello, alice!
# [iteration 2] Hello, alice!
Short flags + positional args + defaults + validation. The standard skeleton.
Reading from stdin (piped input)
# script.sh
while IFS= read -r line; do
echo "got: $line"
done
# Run with piped input
echo "hello\nworld" | ./script.sh
# got: hello
# got: world
# Or with file input
./script.sh < input.txt
For tools that should work both interactively and in pipelines.
Common stumbles
$1 == "" not catching missing args. [[ -z "$1" ]] is the right way; combined with set -u you may need [[ -z "${1:-}" ]].
Forgetting OPTARG. case $opt in n) name=$1 ;; is wrong — the value is in $OPTARG, not $1.
Not shifting after getopts. Subsequent $1 $2 still reference the options. shift $((OPTIND - 1)) consumes them.
Quoting "$@". Forward all args with "$@", not $@ or $*. Without quotes, args with spaces get split.
read with multiple vars and few values. read A B C <<< "one" → A=one, B=, C= (no error).
Missing IFS= in read loops. while read line; do ... strips leading whitespace from each line. while IFS= read -r line preserves it.
Using getopt(1) (no s) on macOS. macOS ships the BSD version, which doesn't support long options. Either brew install gnu-getopt or use manual parsing.
What's next
Lesson 9: conditionals. if, [[ ]], file tests, case.
Recap
Positional args: $1 $2 ... $@ "$@" $# $0. Defaults via ${1:-default}. Required: check [[ -z "$1" ]]. read VAR for interactive input (-p prompt, -s silent, -t timeout). getopts "n:a:h" for short options; value in $OPTARG, shift $((OPTIND-1)) after. For long options, use a manual while/case loop. Always quote "$@" when forwarding.
Next lesson: conditionals.