Lesson 9: Input, Arguments & Options

Goal: Build scripts with proper argument parsing, option flags, and interactive input.


Table of Contents

  1. Positional Arguments
  2. Argument Validation
  3. shift — Processing Arguments
  4. getopts — Short Options
  5. Long Options with case
  6. Interactive Input — read
  7. Usage Messages & Help
  8. Exercises

Positional Arguments

#!/bin/bash
# save as greet.sh
echo "Script: $0"
echo "First:  ${1:-none}"
echo "Second: ${2:-none}"
echo "All:    $@"
echo "Count:  $#"
./greet.sh hello world
# Script: ./greet.sh
# First:  hello
# Second: world
# All:    hello world
# Count:  2

Argument Validation

#!/bin/bash
set -euo pipefail

if [[ $# -lt 2 ]]; then
    echo "Usage: $0 <source> <destination>" >&2
    exit 1
fi

SOURCE="$1"
DEST="$2"

[[ -f "$SOURCE" ]] || { echo "Source not found: $SOURCE" >&2; exit 1; }
[[ -d "$(dirname "$DEST")" ]] || { echo "Dest dir doesn't exist" >&2; exit 1; }

cp "$SOURCE" "$DEST"
echo "Copied $SOURCE → $DEST"

shift — Processing Arguments

shift removes the first argument and shifts everything left.

#!/bin/bash
set -euo pipefail

COMMAND="${1:-}"
shift || true  # shift, but don't fail if no args

case "$COMMAND" in
    start)
        echo "Starting with args: $*"
        ;;
    stop)
        echo "Stopping..."
        ;;
    *)
        echo "Usage: $0 {start|stop} [args...]"
        exit 1
        ;;
esac

Process all arguments

#!/bin/bash
while [[ $# -gt 0 ]]; do
    echo "Processing: $1"
    shift
done

getopts — Short Options

Parse standard Unix flags like -v, -f file, -n 5.

#!/bin/bash
set -euo pipefail

VERBOSE=false
OUTPUT=""
COUNT=1

usage() {
    echo "Usage: $0 [-v] [-o output] [-n count] file..."
    echo "  -v         Verbose mode"
    echo "  -o FILE    Output file"
    echo "  -n NUM     Number of iterations"
    echo "  -h         Show help"
    exit 0
}

while getopts "vo:n:h" opt; do
    case "$opt" in
        v) VERBOSE=true ;;
        o) OUTPUT="$OPTARG" ;;
        n) COUNT="$OPTARG" ;;
        h) usage ;;
        ?) echo "Invalid option. Use -h for help." >&2; exit 1 ;;
    esac
done

# Remove parsed options, leaving remaining arguments
shift $((OPTIND - 1))

# Remaining arguments are files
echo "Verbose: $VERBOSE"
echo "Output: ${OUTPUT:-stdout}"
echo "Count: $COUNT"
echo "Files: $*"
./script.sh -v -o result.txt -n 3 file1.txt file2.txt
# Verbose: true
# Output: result.txt
# Count: 3
# Files: file1.txt file2.txt

getopts rules

  • v — flag (no argument)
  • o: — option with required argument (colon after letter)
  • Leading : in optstring — silent error handling

Long Options with case

For --verbose, --output=file style options:

#!/bin/bash
set -euo pipefail

VERBOSE=false
DRY_RUN=false
OUTPUT=""
FILES=()

usage() {
    cat << EOF
Usage: $0 [options] <files...>

Options:
  -v, --verbose      Enable verbose output
  -d, --dry-run      Show what would be done
  -o, --output FILE  Write output to FILE
  -h, --help         Show this help

Examples:
  $0 --verbose -o result.txt file1.txt file2.txt
  $0 --dry-run *.log
EOF
    exit 0
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -v|--verbose)
            VERBOSE=true
            shift
            ;;
        -d|--dry-run)
            DRY_RUN=true
            shift
            ;;
        -o|--output)
            OUTPUT="${2:?Error: --output requires an argument}"
            shift 2
            ;;
        -h|--help)
            usage
            ;;
        --)
            shift
            FILES+=("$@")
            break
            ;;
        -*)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
        *)
            FILES+=("$1")
            shift
            ;;
    esac
done

[[ ${#FILES[@]} -eq 0 ]] && { echo "Error: No files specified" >&2; exit 1; }

echo "Verbose:  $VERBOSE"
echo "Dry run:  $DRY_RUN"
echo "Output:   ${OUTPUT:-stdout}"
echo "Files:    ${FILES[*]}"

Interactive Input — read

# Basic input
read -rp "Enter your name: " NAME
echo "Hello, $NAME!"

# With default
read -rp "Port [3000]: " PORT
PORT="${PORT:-3000}"

# Password (hidden)
read -rsp "Password: " PASS
echo ""
echo "Password length: ${#PASS}"

# Timeout
if read -rt 5 -p "Quick! Enter something (5s): " ANSWER; then
    echo "You entered: $ANSWER"
else
    echo "Too slow!"
fi

# Yes/No confirmation
read -rp "Continue? [y/N] " CONFIRM
if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
    echo "Proceeding..."
else
    echo "Cancelled."
    exit 0
fi

Select menu

PS3="Choose an option: "
select opt in "Start" "Stop" "Restart" "Status" "Quit"; do
    case "$opt" in
        Start)   echo "Starting..."; break ;;
        Stop)    echo "Stopping..."; break ;;
        Restart) echo "Restarting..."; break ;;
        Status)  echo "Status: running" ;;
        Quit)    echo "Bye!"; exit 0 ;;
        *)       echo "Invalid option" ;;
    esac
done

Usage Messages & Help

Standard pattern

#!/bin/bash
set -euo pipefail

readonly VERSION="1.2.0"
readonly SCRIPT_NAME="$(basename "$0")"

usage() {
    cat << EOF
$SCRIPT_NAME v$VERSION — Deploy application to server

USAGE:
    $SCRIPT_NAME [OPTIONS] <environment>

ARGUMENTS:
    environment    Target environment (dev, staging, prod)

OPTIONS:
    -b, --branch BRANCH    Git branch to deploy (default: main)
    -t, --tag TAG          Specific tag to deploy
    -d, --dry-run          Show what would be done
    -f, --force            Skip confirmation
    -v, --verbose          Enable verbose output
    -h, --help             Show this help message
    --version              Show version

EXAMPLES:
    $SCRIPT_NAME staging
    $SCRIPT_NAME -b feature/auth --dry-run prod
    $SCRIPT_NAME --tag v1.2.0 prod
EOF
    exit 0
}

version() {
    echo "$SCRIPT_NAME v$VERSION"
    exit 0
}

[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
[[ "${1:-}" == "--version" ]] && version

Exercises

Exercise 1: File processor with options

cat > process.sh << 'EOF'
#!/bin/bash
set -euo pipefail

UPPERCASE=false
NUMBERED=false
REVERSE=false

while getopts "unrh" opt; do
    case "$opt" in
        u) UPPERCASE=true ;;
        n) NUMBERED=true ;;
        r) REVERSE=true ;;
        h) echo "Usage: $0 [-u] [-n] [-r] <file>"; exit 0 ;;
        ?) exit 1 ;;
    esac
done
shift $((OPTIND - 1))

FILE="${1:?Error: provide a file}"
[[ -f "$FILE" ]] || { echo "Not found: $FILE" >&2; exit 1; }

CMD="cat"
$REVERSE && CMD="tac"

OUTPUT=$($CMD "$FILE")
$UPPERCASE && OUTPUT=$(echo "$OUTPUT" | tr 'a-z' 'A-Z')
if $NUMBERED; then
    echo "$OUTPUT" | nl -ba
else
    echo "$OUTPUT"
fi
EOF

echo -e "hello\nworld\nfoo\nbar" > /tmp/test.txt
chmod +x process.sh
./process.sh -u -n /tmp/test.txt
./process.sh -r /tmp/test.txt
rm process.sh /tmp/test.txt

Exercise 2: Interactive setup wizard

cat > setup.sh << 'EOF'
#!/bin/bash
set -euo pipefail

echo "=== Application Setup ==="
echo ""
read -rp "App name [myapp]: " APP_NAME
APP_NAME="${APP_NAME:-myapp}"

read -rp "Port [3000]: " PORT
PORT="${PORT:-3000}"

PS3="Select database: "
select DB in "PostgreSQL" "MySQL" "SQLite"; do
    [[ -n "$DB" ]] && break
done

read -rp "Enable debug mode? [y/N] " DEBUG
[[ "$DEBUG" =~ ^[Yy]$ ]] && DEBUG=true || DEBUG=false

echo ""
echo "=== Configuration ==="
echo "App:      $APP_NAME"
echo "Port:     $PORT"
echo "Database: $DB"
echo "Debug:    $DEBUG"

read -rp "Save this configuration? [Y/n] " SAVE
if [[ ! "$SAVE" =~ ^[Nn]$ ]]; then
    cat > "${APP_NAME}.conf" << CONF
app_name=$APP_NAME
port=$PORT
database=$DB
debug=$DEBUG
CONF
    echo "Saved to ${APP_NAME}.conf"
else
    echo "Cancelled"
fi
EOF

chmod +x setup.sh && ./setup.sh
rm -f setup.sh myapp.conf

Key Takeaways

  • $1, $2, $@, $# — positional arguments
  • ${1:-default} — defaults for missing arguments
  • shift — consume arguments one by one
  • getopts "vf:n:" opt — parse short options (-v, -f file)
  • while/case loop — parse long options (--verbose, --file)
  • read -rp "prompt" VAR — interactive input (always use -r)
  • select — interactive menu
  • Always provide -h / --help with usage examples

Next Lesson: Lesson 10: Docker Entrypoints & Init Scripts →