Stage 2 · Tools
Scripting Patterns
Argument Parsing
getopts, getopt, long options, help text generation, and input validation.
Positional Arguments
The simplest argument handling uses positional parameters ($1, $2, etc.). This works for simple scripts but becomes unwieldy with many options.
# Simple positional parsing
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <source> <destination>"
exit 1
fi
SOURCE="$1"
DEST="$2"
echo "Copying $SOURCE to $DEST"
cp "$SOURCE" "$DEST"Positional arguments are simple but have limitations: no named options, no defaults, no validation. For anything beyond basic scripts, use getopts or manual parsing.
getopts for Short Options
getopts is a shell built-in for parsing short options (-v, -h, -o file). It handles option arguments, combined options (-vvv), and error reporting.
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [-v] [-o output] [-h] <file>"
echo " -v Verbose output"
echo " -o OUTPUT Output file (default: stdout)"
echo " -h Show this help"
}
VERBOSE=false
OUTPUT="-"
while getopts "vo:h" opt; do
case "$opt" in
v) VERBOSE=true ;;
o) OUTPUT="$OPTARG" ;;
h) usage; exit 0 ;;
?) usage >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
echo "Error: Missing file argument" >&2
usage >&2
exit 1
fi
INPUT="$1"
echo "Processing $INPUT -> $OUTPUT (verbose: $VERBOSE)"getopts parses short options. The option string 'vo:h' means -v (no arg), -o (requires arg), -h (no arg). OPTARG holds the argument for options that require one. OPTIND tracks position.
After getopts finishes, OPTIND points to the first non-option argument. shift $((OPTIND - 1)) removes parsed options from the argument list, leaving only positional arguments in $1, $2, etc.
getopt for Long Options
getopt (not getopts) supports long options (--verbose, --output=file). It is more powerful but requires careful handling for portability.
#!/usr/bin/env bash
set -euo pipefail
# Parse long options
PARSED=$(getopt -o vo:h --long verbose,output:,help -n "$0" -- "$@")
eval set -- "$PARSED"
VERBOSE=false
OUTPUT="-"
while true; do
case "$1" in
-v|--verbose) VERBOSE=true; shift ;;
-o|--output) OUTPUT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
*) echo "Internal error!" >&2; exit 1 ;;
esac
done
echo "Verbose: $VERBOSE, Output: $OUTPUT"
echo "Remaining args: $@"getopt normalizes long and short options. --long defines long options. The : after option names means they require an argument. eval set -- reassigns the parsed options.
Manual Argument Parsing
For maximum control, parse arguments manually using a while loop and case statement. This is the most common pattern in production scripts.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS] <args>
Options:
-v, --verbose Enable verbose output
-o, --output FILE Output file (default: stdout)
-n, --count NUM Number of iterations (default: 1)
-h, --help Show this help
Examples:
$(basename "$0") -v -o result.txt input.txt
$(basename "$0") --count 10 --output /tmp/data.txt
EOF
}
VERBOSE=false
OUTPUT="-"
COUNT=1
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=true; shift ;;
-o|--output)
[[ -z "{2:-}" ]] && echo "Error: --output requires an argument" >&2 && exit 1
OUTPUT="$2"; shift 2 ;;
-n|--count)
[[ -z "{2:-}" ]] && echo "Error: --count requires an argument" >&2 && exit 1
COUNT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
*) break ;;
esac
done
}
parse_args "$@"
# Remaining arguments
if [[ $# -lt 1 ]]; then
echo "Error: Missing required arguments" >&2
usage >&2
exit 1
fi
echo "Files: $@"
echo "Output: $OUTPUT"
echo "Count: $COUNT"
echo "Verbose: $VERBOSE"Manual parsing gives you complete control over argument handling. The pattern: while loop + case statement + shift. Check for missing arguments with {2:-} and error if empty.
Help Text Generation
#!/usr/bin/env bash
set -euo pipefail
# Extract usage from script comments
show_help() {
head -20 "$0" | grep '^#' | sed 's/^# \{0,1\}//'
}
# Or use a dedicated function
usage() {
cat <<EOF
$(basename "$0") - Brief description
USAGE:
$(basename "$0") [OPTIONS] <required> [optional]
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
-o, --output FILE Output file (default: stdout)
-n, --dry-run Show what would be done
REQUIRED:
<file> Input file to process
EXAMPLES:
$(basename "$0") input.txt
$(basename "$0") -v -o result.txt input.txt
$(basename "$0") --dry-run large_file.txt
EXIT CODES:
0 Success
1 General error
2 Invalid arguments
EOF
}A good usage() function shows: what the script does, how to use it, all options with defaults, required arguments, examples, and exit codes. This makes scripts self-documenting.
Input Validation
#!/usr/bin/env bash
set -euo pipefail
# Validate file exists
validate_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
echo "Error: File not found: $file" >&2
return 1
fi
if [[ ! -r "$file" ]]; then
echo "Error: File not readable: $file" >&2
return 1
fi
}
# Validate number
validate_number() {
local num="$1"
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
echo "Error: Not a valid number: $num" >&2
return 1
fi
}
# Validate email
validate_email() {
local email="$1"
if ! [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Error: Not a valid email: $email" >&2
return 1
fi
}
# Usage in argument parsing
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--count)
validate_number "{2:-}" || exit 1
COUNT="$2"; shift 2 ;;
*)
validate_file "$1" || exit 1
FILES+=("$1"); shift ;;
esac
doneAlways validate inputs before using them. Check: files exist and are readable, numbers are numeric, emails match patterns, URLs are valid. Return meaningful error messages.
Validate arguments before processing. If an argument is invalid, print an error message and exit immediately. Do not silently accept bad input and fail later during processing.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.