Web Analytics

Command Line Arguments

Advanced ~30 min read

Professional command-line tools accept arguments and options that control their behavior. This lesson teaches you to parse arguments using getopts, handle flags and options, and create user-friendly CLI interfaces that follow Unix conventions!

Positional Arguments

Start with the basics: accessing arguments passed to your script.

Output
Click Run to execute your code

Special Variables

Variable Meaning Example
$0 Script name ./backup.sh
$1, $2... Positional arguments First arg, second arg...
$# Number of arguments 3
$@ All arguments (separate words) "a" "b" "c"
$* All arguments (single string) "a b c"
Always quote $@: Use "$@" to preserve arguments with spaces. for arg in "$@"; do handles "hello world" as one argument.

Parsing with getopts

The getopts builtin handles short options like -v, -f file.

Output
Click Run to execute your code

getopts Syntax

# getopts OPTSTRING VARNAME [ARGS]
#
# OPTSTRING format:
#   "vhf:"
#   v     - Flag (no argument)
#   h     - Flag (no argument)
#   f:    - Option (requires argument)
#   :     - Leading colon enables silent error mode

while getopts ":vhf:o:" opt; do
    case $opt in
        v) verbose=true ;;
        h) show_help; exit 0 ;;
        f) input_file="$OPTARG" ;;
        o) output_file="$OPTARG" ;;
        :) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
        \\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
    esac
done

# Shift processed options away
shift $((OPTIND - 1))

# Remaining arguments are in $@
echo "Remaining args: $@"
OPTIND: After getopts, use shift $((OPTIND - 1)) to remove processed options. Remaining positional arguments are then in $1, $2, ...

Handling Long Options

For --verbose style options, use manual parsing or external tools.

# Manual long option parsing
parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -v|--verbose)
                verbose=true
                shift
                ;;
            -h|--help)
                show_help
                exit 0
                ;;
            -f|--file)
                input_file="$2"
                shift 2
                ;;
            --file=*)
                input_file="${1#*=}"
                shift
                ;;
            -o|--output)
                output_file="$2"
                shift 2
                ;;
            --output=*)
                output_file="${1#*=}"
                shift
                ;;
            --)
                shift
                break
                ;;
            -*)
                echo "Unknown option: $1" >&2
                exit 1
                ;;
            *)
                # Positional argument
                positional+=("$1")
                shift
                ;;
        esac
    done

    # Set remaining positional args
    set -- "${positional[@]}"
}

# Usage: script.sh --verbose --file=data.txt output.txt
parse_args "$@"

Option Styles

Style Example Notes
Short flag -v Boolean, no value
Short with value -f file.txt Space-separated
Combined shorts -vf file.txt Multiple flags
Long flag --verbose Boolean, no value
Long with value --file file.txt Space-separated
Long with equals --file=file.txt Equals-separated

Professional Usage Messages

Good CLI tools provide helpful usage information.

#!/usr/bin/env bash

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

show_help() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]  

Copy files with advanced options.

Arguments:
    source          Source file or directory
    destination     Destination path

Options:
    -r, --recursive     Copy directories recursively
    -f, --force         Overwrite existing files
    -v, --verbose       Show detailed output
    -n, --dry-run       Show what would be done
    -h, --help          Show this help message
    --version           Show version number

Examples:
    $SCRIPT_NAME file.txt backup/
    $SCRIPT_NAME -rv src/ dest/
    $SCRIPT_NAME --force --verbose data.db /backup/

Environment:
    COPY_OPTS       Default options (e.g., "-rv")

Exit Codes:
    0   Success
    1   General error
    2   Invalid arguments

Report bugs to: [email protected]
EOF
}

show_version() {
    echo "$SCRIPT_NAME version $VERSION"
}

# Parse -h and --version early
case "${1:-}" in
    -h|--help)    show_help; exit 0 ;;
    --version)    show_version; exit 0 ;;
esac

Complete Argument Parser

Putting it all together: a robust argument parsing template.

#!/usr/bin/env bash
set -euo pipefail

# Script info
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Defaults
verbose=false
dry_run=false
force=false
output_file=""
declare -a input_files=()

show_help() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE...

Process one or more files.

Options:
    -o, --output FILE   Output file (default: stdout)
    -f, --force         Overwrite existing output
    -n, --dry-run       Show what would be done
    -v, --verbose       Verbose output
    -h, --help          Show this help
EOF
}

die() {
    echo "Error: $*" >&2
    echo "Try '$SCRIPT_NAME --help' for more information." >&2
    exit 1
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                show_help
                exit 0
                ;;
            -v|--verbose)
                verbose=true
                shift
                ;;
            -n|--dry-run)
                dry_run=true
                shift
                ;;
            -f|--force)
                force=true
                shift
                ;;
            -o|--output)
                [[ $# -lt 2 ]] && die "Option $1 requires an argument"
                output_file="$2"
                shift 2
                ;;
            --output=*)
                output_file="${1#*=}"
                shift
                ;;
            --)
                shift
                input_files+=("$@")
                break
                ;;
            -*)
                die "Unknown option: $1"
                ;;
            *)
                input_files+=("$1")
                shift
                ;;
        esac
    done
}

validate_args() {
    # Require at least one input file
    [[ ${#input_files[@]} -eq 0 ]] && die "No input files specified"

    # Check input files exist
    for file in "${input_files[@]}"; do
        [[ -f "$file" ]] || die "File not found: $file"
    done

    # Check output won't overwrite without --force
    if [[ -n "$output_file" && -f "$output_file" && "$force" != "true" ]]; then
        die "Output file exists: $output_file (use --force to overwrite)"
    fi
}

main() {
    parse_args "$@"
    validate_args

    [[ "$verbose" == "true" ]] && echo "Processing ${#input_files[@]} file(s)..."

    for file in "${input_files[@]}"; do
        [[ "$verbose" == "true" ]] && echo "  Processing: $file"
        # Process file...
    done

    echo "Done!"
}

main "$@"

Common Mistakes

1. Not quoting $@

# Wrong - breaks on spaces
for arg in $@; do echo "$arg"; done

# Correct
for arg in "$@"; do echo "$arg"; done

2. Missing required argument check

# Wrong - crashes if no $2
-f) file="$2"; shift 2 ;;

# Correct
-f)
    [[ $# -lt 2 ]] && die "-f requires an argument"
    file="$2"
    shift 2
    ;;

3. Forgetting to shift

# Wrong - infinite loop!
-v) verbose=true ;;

# Correct
-v) verbose=true; shift ;;

Summary

  • Positional: Access with $1, $2, $@, $#
  • getopts: Use for short options (-v, -f file)
  • Long options: Parse manually with while/case loop
  • Always quote: Use "$@" to preserve spaces
  • shift: Remove processed arguments with shift
  • Help: Always provide -h/--help with clear usage

What's Next?

Now let's create reusable Script Templates. You'll learn standard boilerplate patterns that you can use as starting points for new scripts!