Lesson 11: Argument Parsing and CLI Interfaces
Lesson 11: Argument Parsing and CLI Interfaces¶
Difficulty: ⭐⭐⭐
Previous: 10_Error_Handling.md | Next: 12_Portability_and_Best_Practices.md
1. Manual Argument Parsing¶
Manual parsing gives you complete control over argument handling.
Basic Argument Loop¶
#!/bin/bash
# Parse arguments manually
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
echo "Usage: $0 [OPTIONS]"
exit 0
;;
-v|--verbose)
VERBOSE=1
shift
;;
-o|--output)
OUTPUT="$2"
shift 2
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
# Positional argument
ARGS+=("$1")
shift
;;
esac
done
# Remaining arguments (after --)
REMAINING_ARGS=("$@")
echo "VERBOSE: ${VERBOSE:-0}"
echo "OUTPUT: ${OUTPUT:-none}"
echo "ARGS: ${ARGS[*]}"
echo "REMAINING: ${REMAINING_ARGS[*]}"
Handling Options with Values¶
#!/bin/bash
# Parse options that take values
parse_args() {
local verbose=0
local output=""
local count=1
local files=()
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
verbose=1
shift
;;
-o|--output)
if [ -z "$2" ] || [[ "$2" == -* ]]; then
echo "Error: --output requires a value" >&2
return 1
fi
output="$2"
shift 2
;;
--output=*)
output="${1#*=}"
shift
;;
-n|--count)
if [ -z "$2" ] || [[ "$2" == -* ]]; then
echo "Error: --count requires a value" >&2
return 1
fi
count="$2"
shift 2
;;
--count=*)
count="${1#*=}"
shift
;;
--)
shift
files=("$@")
break
;;
-*)
echo "Unknown option: $1" >&2
return 1
;;
*)
files+=("$1")
shift
;;
esac
done
# Export parsed values
echo "verbose=$verbose"
echo "output=$output"
echo "count=$count"
echo "files=(${files[*]})"
}
# Test
parse_args -v --output=result.txt --count 5 file1.txt file2.txt
echo "---"
parse_args --verbose -o result.txt -n 3 -- file1.txt file2.txt -special-file
Advanced Manual Parsing¶
#!/bin/bash
# Complete argument parser
declare -A OPTIONS
declare -a POSITIONAL
parse_arguments() {
local expecting_value=""
local option_name=""
while [ $# -gt 0 ]; do
# Handle value for previous option
if [ -n "$expecting_value" ]; then
OPTIONS["$option_name"]="$1"
expecting_value=""
option_name=""
shift
continue
fi
case "$1" in
# Long option with value: --option=value
--*=*)
option_name="${1%%=*}"
option_name="${option_name#--}"
OPTIONS["$option_name"]="${1#*=}"
shift
;;
# Long option without value: --option
--*)
option_name="${1#--}"
# Check if next arg is a value or another option
if [ $# -gt 1 ] && [[ ! "$2" =~ ^- ]]; then
expecting_value=1
else
OPTIONS["$option_name"]=1
fi
shift
;;
# Short option: -o
-[!-])
option_name="${1#-}"
# Check if next arg is a value
if [ $# -gt 1 ] && [[ ! "$2" =~ ^- ]]; then
expecting_value=1
else
OPTIONS["$option_name"]=1
fi
shift
;;
# Combined short options: -abc
-[!-]*)
local opts="${1#-}"
for (( i=0; i<${#opts}; i++ )); do
OPTIONS["${opts:$i:1}"]=1
done
shift
;;
# End of options
--)
shift
POSITIONAL+=("$@")
break
;;
# Positional argument
*)
POSITIONAL+=("$1")
shift
;;
esac
done
# Check if we're still expecting a value
if [ -n "$expecting_value" ]; then
echo "Error: Option --$option_name requires a value" >&2
return 1
fi
}
# Usage
parse_arguments -abc --verbose --output=file.txt --count 5 input1.txt input2.txt
# Display results
echo "Options:"
for key in "${!OPTIONS[@]}"; do
echo " $key = ${OPTIONS[$key]}"
done
echo "Positional arguments:"
for arg in "${POSITIONAL[@]}"; do
echo " $arg"
done
2. getopts (POSIX)¶
getopts is a POSIX built-in for parsing options.
Basic getopts Usage¶
#!/bin/bash
# Parse options with getopts
usage() {
echo "Usage: $0 [-v] [-o OUTPUT] [-n COUNT] FILE..."
exit 1
}
verbose=0
output=""
count=1
while getopts "vo:n:h" opt; do
case "$opt" in
v)
verbose=1
;;
o)
output="$OPTARG"
;;
n)
count="$OPTARG"
;;
h)
usage
;;
\?)
echo "Invalid option: -$OPTARG" >&2
usage
;;
:)
echo "Option -$OPTARG requires an argument" >&2
usage
;;
esac
done
# Shift processed options
shift $((OPTIND - 1))
# Remaining arguments are positional
files=("$@")
echo "verbose=$verbose"
echo "output=$output"
echo "count=$count"
echo "files=(${files[*]})"
getopts Error Handling¶
#!/bin/bash
# Two error handling modes:
# 1. Default (verbose): getopts prints errors
# 2. Silent mode: prepend option string with ":"
# Silent mode (recommended)
while getopts ":vho:n:" opt; do
case "$opt" in
v)
VERBOSE=1
;;
o)
OUTPUT="$OPTARG"
;;
n)
COUNT="$OPTARG"
# Validate it's a number
if ! [[ "$COUNT" =~ ^[0-9]+$ ]]; then
echo "Error: -n requires a number" >&2
exit 1
fi
;;
h)
echo "Help message"
exit 0
;;
:)
# Option requires argument but none provided
echo "Error: -$OPTARG requires an argument" >&2
exit 1
;;
\?)
# Invalid option
echo "Error: Invalid option -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
echo "Parsed successfully"
echo "Remaining args: $*"
getopts with Functions¶
#!/bin/bash
# Parse options in a function
parse_options() {
local OPTIND opt
local verbose=0
local output=""
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=1 ;;
o) output="$OPTARG" ;;
\?) echo "Invalid option: -$OPTARG" >&2; return 1 ;;
:) echo "Option -$OPTARG requires an argument" >&2; return 1 ;;
esac
done
shift $((OPTIND - 1))
# Return parsed values (using global variables or output)
PARSED_VERBOSE=$verbose
PARSED_OUTPUT=$output
PARSED_ARGS=("$@")
}
# Call parser
parse_options -v -o output.txt file1 file2
echo "verbose=$PARSED_VERBOSE"
echo "output=$PARSED_OUTPUT"
echo "args=${PARSED_ARGS[*]}"
Complete getopts Example¶
#!/bin/bash
set -euo pipefail
# Script configuration
SCRIPT_NAME=$(basename "$0")
VERSION="1.0.0"
# Default values
VERBOSE=0
DRY_RUN=0
OUTPUT_FILE=""
INPUT_FILES=()
# Usage message
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE...
Process files with various options.
OPTIONS:
-v Verbose mode
-n Dry run (don't make changes)
-o FILE Output file
-h Show this help message
-V Show version
EXAMPLES:
$SCRIPT_NAME -v input.txt
$SCRIPT_NAME -o output.txt -n input1.txt input2.txt
EOF
exit 0
}
# Version message
version() {
echo "$SCRIPT_NAME version $VERSION"
exit 0
}
# Parse options
while getopts ":vno:hV" opt; do
case "$opt" in
v)
VERBOSE=1
;;
n)
DRY_RUN=1
;;
o)
OUTPUT_FILE="$OPTARG"
;;
h)
usage
;;
V)
version
;;
:)
echo "Error: Option -$OPTARG requires an argument" >&2
echo "Try '$SCRIPT_NAME -h' for more information." >&2
exit 1
;;
\?)
echo "Error: Invalid option -$OPTARG" >&2
echo "Try '$SCRIPT_NAME -h' for more information." >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
# Validate arguments
if [ $# -eq 0 ]; then
echo "Error: No input files specified" >&2
echo "Try '$SCRIPT_NAME -h' for more information." >&2
exit 1
fi
INPUT_FILES=("$@")
# Process files
[ $VERBOSE -eq 1 ] && echo "Processing ${#INPUT_FILES[@]} files..."
[ $DRY_RUN -eq 1 ] && echo "DRY RUN MODE"
for file in "${INPUT_FILES[@]}"; do
[ $VERBOSE -eq 1 ] && echo "Processing: $file"
# Process file here
done
[ -n "$OUTPUT_FILE" ] && echo "Output: $OUTPUT_FILE"
3. getopt (GNU)¶
GNU getopt supports long options and more advanced parsing.
Basic getopt Usage¶
#!/bin/bash
# Note: This requires GNU getopt (not available on macOS by default)
# macOS users: brew install gnu-getopt
# Parse with getopt
OPTS=$(getopt -o "vo:n:" --long "verbose,output:,count:" -n "$0" -- "$@")
if [ $? -ne 0 ]; then
echo "Failed to parse options" >&2
exit 1
fi
# Reset positional parameters
eval set -- "$OPTS"
# Parse options
verbose=0
output=""
count=1
while true; do
case "$1" in
-v|--verbose)
verbose=1
shift
;;
-o|--output)
output="$2"
shift 2
;;
-n|--count)
count="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Internal error!" >&2
exit 1
;;
esac
done
# Remaining arguments
files=("$@")
echo "verbose=$verbose"
echo "output=$output"
echo "count=$count"
echo "files=(${files[*]})"
getopt with Long Options Only¶
#!/bin/bash
# Long options only
OPTS=$(getopt --long "help,version,verbose,output:,dry-run" -n "$0" -- "$@")
if [ $? -ne 0 ]; then
exit 1
fi
eval set -- "$OPTS"
while true; do
case "$1" in
--help)
echo "Help message"
exit 0
;;
--version)
echo "Version 1.0.0"
exit 0
;;
--verbose)
VERBOSE=1
shift
;;
--output)
OUTPUT="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--)
shift
break
;;
*)
echo "Internal error!" >&2
exit 1
;;
esac
done
echo "Parsed options successfully"
getopt vs getopts Comparison¶
| Feature | getopts (POSIX) | getopt (GNU) |
|---|---|---|
| Portability | POSIX (all systems) | GNU (Linux, macOS with install) |
| Long options | No | Yes |
| Option bundling | Limited | Full support |
-- separator |
Manual handling | Built-in |
| Error messages | Basic | Detailed |
| Option reordering | No | Yes |
| Complexity | Simple | More complex |
| Use case | Simple scripts | Complex CLI tools |
Complete getopt Example¶
#!/bin/bash
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
# Check if GNU getopt is available
if ! getopt --test > /dev/null 2>&1; then
if [ $? -ne 4 ]; then
echo "Error: GNU getopt not available" >&2
exit 1
fi
fi
# Parse options
SHORT_OPTS="vno:h"
LONG_OPTS="verbose,dry-run,output:,help,version,config:"
OPTS=$(getopt -o "$SHORT_OPTS" --long "$LONG_OPTS" -n "$SCRIPT_NAME" -- "$@")
if [ $? -ne 0 ]; then
echo "Run '$SCRIPT_NAME --help' for usage" >&2
exit 1
fi
eval set -- "$OPTS"
# Default values
VERBOSE=0
DRY_RUN=0
OUTPUT=""
CONFIG=""
# Parse
while true; do
case "$1" in
-v|--verbose)
VERBOSE=1
shift
;;
-n|--dry-run)
DRY_RUN=1
shift
;;
-o|--output)
OUTPUT="$2"
shift 2
;;
--config)
CONFIG="$2"
if [ ! -f "$CONFIG" ]; then
echo "Error: Config file not found: $CONFIG" >&2
exit 1
fi
shift 2
;;
-h|--help)
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE...
OPTIONS:
-v, --verbose Verbose output
-n, --dry-run Dry run mode
-o, --output FILE Output file
--config FILE Configuration file
-h, --help Show this help
--version Show version
EOF
exit 0
;;
--version)
echo "$SCRIPT_NAME 1.0.0"
exit 0
;;
--)
shift
break
;;
*)
echo "Internal error!" >&2
exit 1
;;
esac
done
# Remaining arguments
FILES=("$@")
if [ ${#FILES[@]} -eq 0 ]; then
echo "Error: No input files specified" >&2
exit 1
fi
# Execute
[ $VERBOSE -eq 1 ] && echo "Processing ${#FILES[@]} files"
[ $DRY_RUN -eq 1 ] && echo "DRY RUN MODE"
for file in "${FILES[@]}"; do
[ $VERBOSE -eq 1 ] && echo "Processing: $file"
done
4. Self-Documenting Help¶
Good help messages make CLI tools user-friendly.
Help Message Template¶
#!/bin/bash
show_help() {
cat << EOF
NAME
$(basename "$0") - Brief description of what the script does
SYNOPSIS
$(basename "$0") [OPTIONS] COMMAND [ARGUMENTS]
DESCRIPTION
Detailed description of what this script does.
Can span multiple lines and include examples.
OPTIONS
-v, --verbose
Enable verbose output
-o, --output FILE
Specify output file (default: stdout)
-n, --count NUMBER
Number of iterations (default: 1)
-h, --help
Show this help message and exit
-V, --version
Show version information and exit
COMMANDS
start Start the service
stop Stop the service
restart Restart the service
status Show service status
EXAMPLES
# Basic usage
$(basename "$0") start
# With options
$(basename "$0") -v --output=log.txt start
# Multiple operations
$(basename "$0") -n 5 process file1.txt file2.txt
EXIT STATUS
0 Success
1 General error
2 Invalid arguments
66 Input file not found
77 Permission denied
AUTHOR
Written by Your Name
REPORTING BUGS
Report bugs to: bugs@example.com
SEE ALSO
Full documentation at: https://example.com/docs
EOF
}
# Call with -h or --help
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
show_help
exit 0
fi
Extracting Help from Comments¶
#!/bin/bash
### NAME
### myscript - Does something useful
###
### SYNOPSIS
### myscript [OPTIONS] FILE...
###
### DESCRIPTION
### This script processes files in various ways.
###
### OPTIONS
### -v, --verbose Verbose output
### -h, --help Show this help
show_help() {
sed -n 's/^### \?//p' "$0"
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
show_help
exit 0
fi
echo "Script running..."
Version Information¶
#!/bin/bash
SCRIPT_NAME=$(basename "$0")
VERSION="1.2.3"
AUTHOR="John Doe"
COPYRIGHT="Copyright (c) 2024"
LICENSE="MIT License"
show_version() {
cat << EOF
$SCRIPT_NAME version $VERSION
$COPYRIGHT $AUTHOR
License: $LICENSE
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by $AUTHOR
EOF
}
if [ "${1:-}" = "--version" ] || [ "${1:-}" = "-V" ]; then
show_version
exit 0
fi
Dynamic Help Generation¶
#!/bin/bash
# Define options structure
declare -A OPTIONS_HELP=(
["-v|--verbose"]="Enable verbose output"
["-o|--output FILE"]="Specify output file"
["-n|--count NUM"]="Number of iterations"
["-h|--help"]="Show this help message"
)
generate_help() {
echo "Usage: $(basename "$0") [OPTIONS] FILE..."
echo ""
echo "OPTIONS:"
for key in $(echo "${!OPTIONS_HELP[@]}" | tr ' ' '\n' | sort); do
printf " %-25s %s\n" "$key" "${OPTIONS_HELP[$key]}"
done
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
generate_help
exit 0
fi
5. Color Output¶
Colors improve readability of CLI output.
ANSI Color Codes¶
#!/bin/bash
# Standard colors
BLACK='\033[0;30m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[0;37m'
# Bold colors
BOLD_BLACK='\033[1;30m'
BOLD_RED='\033[1;31m'
BOLD_GREEN='\033[1;32m'
BOLD_YELLOW='\033[1;33m'
BOLD_BLUE='\033[1;34m'
BOLD_MAGENTA='\033[1;35m'
BOLD_CYAN='\033[1;36m'
BOLD_WHITE='\033[1;37m'
# Background colors
BG_BLACK='\033[40m'
BG_RED='\033[41m'
BG_GREEN='\033[42m'
BG_YELLOW='\033[43m'
BG_BLUE='\033[44m'
BG_MAGENTA='\033[45m'
BG_CYAN='\033[46m'
BG_WHITE='\033[47m'
# Text styles
BOLD='\033[1m'
DIM='\033[2m'
UNDERLINE='\033[4m'
BLINK='\033[5m'
REVERSE='\033[7m'
HIDDEN='\033[8m'
# Reset
NC='\033[0m' # No Color
# Usage
echo -e "${RED}Error message${NC}"
echo -e "${GREEN}Success message${NC}"
echo -e "${YELLOW}Warning message${NC}"
echo -e "${BLUE}Info message${NC}"
echo -e "${BOLD}${WHITE}Important${NC}"
echo -e "${UNDERLINE}Underlined text${NC}"
echo -e "${BG_RED}${WHITE}Alert${NC}"
Complete Color Table¶
| Code | Color | Bold Code | Bold Color |
|---|---|---|---|
\033[0;30m |
Black | \033[1;30m |
Bold Black |
\033[0;31m |
Red | \033[1;31m |
Bold Red |
\033[0;32m |
Green | \033[1;32m |
Bold Green |
\033[0;33m |
Yellow | \033[1;33m |
Bold Yellow |
\033[0;34m |
Blue | \033[1;34m |
Bold Blue |
\033[0;35m |
Magenta | \033[1;35m |
Bold Magenta |
\033[0;36m |
Cyan | \033[1;36m |
Bold Cyan |
\033[0;37m |
White | \033[1;37m |
Bold White |
tput Commands¶
#!/bin/bash
# Using tput (more portable)
tput_setup() {
# Check if terminal supports colors
if [ -t 1 ] && [ $(tput colors) -ge 8 ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
MAGENTA=$(tput setaf 5)
CYAN=$(tput setaf 6)
WHITE=$(tput setaf 7)
BOLD=$(tput bold)
UNDERLINE=$(tput smul)
RESET=$(tput sgr0)
else
RED=""
GREEN=""
YELLOW=""
BLUE=""
MAGENTA=""
CYAN=""
WHITE=""
BOLD=""
UNDERLINE=""
RESET=""
fi
}
tput_setup
echo "${RED}Red text${RESET}"
echo "${GREEN}Green text${RESET}"
echo "${BOLD}${YELLOW}Bold yellow${RESET}"
Conditional Coloring¶
#!/bin/bash
# Detect if output is to terminal
if [ -t 1 ]; then
# Terminal detected, use colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
else
# Not a terminal (pipe, file, etc.), no colors
RED=''
GREEN=''
YELLOW=''
NC=''
fi
# Respect NO_COLOR environment variable
if [ -n "${NO_COLOR:-}" ]; then
RED=''
GREEN=''
YELLOW=''
NC=''
fi
echo -e "${GREEN}This is green in terminal${NC}"
echo -e "${RED}This is red in terminal${NC}"
# Test: ./script.sh (colored)
# ./script.sh | cat (not colored)
# NO_COLOR=1 ./script.sh (not colored)
Color Helper Functions¶
#!/bin/bash
# Setup colors
setup_colors() {
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
BOLD=$(tput bold)
RESET=$(tput sgr0)
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# Helper functions
error() {
echo "${RED}ERROR: $*${RESET}" >&2
}
success() {
echo "${GREEN}SUCCESS: $*${RESET}"
}
warning() {
echo "${YELLOW}WARNING: $*${RESET}" >&2
}
info() {
echo "${BLUE}INFO: $*${RESET}"
}
bold() {
echo "${BOLD}$*${RESET}"
}
setup_colors
# Usage
error "Something went wrong"
success "Operation completed"
warning "This might be a problem"
info "FYI: Some information"
bold "Important message"
6. Progress Indicators¶
Show progress for long-running operations.
Spinner Animation¶
#!/bin/bash
# Spinner animation
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Usage
(sleep 5) &
echo -n "Processing..."
spinner $!
echo "Done!"
# Alternative spinner with more frames
spinner_fancy() {
local pid=$1
local delay=0.1
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
while kill -0 "$pid" 2>/dev/null; do
for frame in "${frames[@]}"; do
printf "\r%s Processing..." "$frame"
sleep $delay
if ! kill -0 "$pid" 2>/dev/null; then
break 2
fi
done
done
printf "\r✓ Done! \n"
}
# Test fancy spinner
(sleep 3) &
spinner_fancy $!
Progress Bar¶
#!/bin/bash
# Progress bar
progress_bar() {
local current=$1
local total=$2
local width=${3:-50}
local percent=$((current * 100 / total))
local completed=$((width * current / total))
local remaining=$((width - completed))
printf "\r["
printf "%${completed}s" | tr ' ' '='
printf "%${remaining}s" | tr ' ' ' '
printf "] %3d%%" "$percent"
if [ "$current" -eq "$total" ]; then
echo ""
fi
}
# Usage
total=100
for i in $(seq 1 $total); do
progress_bar $i $total
sleep 0.05
done
# Percentage-based progress
show_progress() {
local percent=$1
local width=50
local completed=$((width * percent / 100))
local remaining=$((width - completed))
printf "\rProgress: ["
printf "%${completed}s" | tr ' ' '█'
printf "%${remaining}s" | tr ' ' '░'
printf "] %3d%%" "$percent"
}
# Test
for i in $(seq 0 5 100); do
show_progress $i
sleep 0.2
done
echo ""
File Download Progress¶
#!/bin/bash
# Simulate file download with progress
download_with_progress() {
local url=$1
local output=$2
local total_size=${3:-1000000} # Bytes
echo "Downloading: $url"
local downloaded=0
local chunk_size=10000
while [ $downloaded -lt $total_size ]; do
# Simulate download
sleep 0.1
downloaded=$((downloaded + chunk_size))
if [ $downloaded -gt $total_size ]; then
downloaded=$total_size
fi
# Calculate progress
local percent=$((downloaded * 100 / total_size))
local mb_downloaded=$((downloaded / 1024 / 1024))
local mb_total=$((total_size / 1024 / 1024))
# Show progress
printf "\r[%-50s] %d%% (%dMB/%dMB)" \
$(printf '%*s' $((percent / 2)) | tr ' ' '=') \
"$percent" \
"$mb_downloaded" \
"$mb_total"
done
echo ""
echo "Download complete: $output"
}
# Test
download_with_progress "https://example.com/file.zip" "file.zip" 5000000
Multi-line Progress Display¶
#!/bin/bash
# Multi-line progress (useful for parallel tasks)
show_multi_progress() {
local -n tasks=$1
# Save cursor position
tput sc
while true; do
local all_done=1
# Restore cursor position
tput rc
for i in "${!tasks[@]}"; do
local task="${tasks[$i]}"
local status=$(get_task_status "$task")
local percent=$(get_task_percent "$task")
printf "Task %d: [%-30s] %3d%%\n" \
"$i" \
"$(printf '%*s' $((percent * 30 / 100)) | tr ' ' '=')" \
"$percent"
if [ "$percent" -lt 100 ]; then
all_done=0
fi
done
[ $all_done -eq 1 ] && break
sleep 0.5
done
}
# Simpler version for demonstration
demo_multi_progress() {
local tasks=("Task 1" "Task 2" "Task 3")
local progress=(0 0 0)
while true; do
clear
echo "=== Progress Dashboard ==="
echo ""
local all_done=1
for i in "${!tasks[@]}"; do
printf "%s: [%-30s] %3d%%\n" \
"${tasks[$i]}" \
"$(printf '%*s' $((progress[$i] * 30 / 100)) | tr ' ' '#')" \
"${progress[$i]}"
if [ ${progress[$i]} -lt 100 ]; then
all_done=0
progress[$i]=$((progress[$i] + RANDOM % 20))
if [ ${progress[$i]} -gt 100 ]; then
progress[$i]=100
fi
fi
done
[ $all_done -eq 1 ] && break
sleep 0.5
done
echo ""
echo "All tasks completed!"
}
demo_multi_progress
7. Interactive Input¶
Gathering user input effectively.
Basic Input¶
#!/bin/bash
# Simple input
read -p "Enter your name: " name
echo "Hello, $name!"
# Input with default value
read -p "Enter filename [default.txt]: " filename
filename=${filename:-default.txt}
echo "Using: $filename"
# Input with timeout
if read -t 5 -p "Enter something (5s timeout): " input; then
echo "You entered: $input"
else
echo -e "\nTimeout!"
fi
Password Input¶
#!/bin/bash
# Hidden input (for passwords)
read -sp "Enter password: " password
echo ""
echo "Password length: ${#password}"
# Password with confirmation
read_password() {
local password
local password_confirm
while true; do
read -sp "Enter password: " password
echo ""
read -sp "Confirm password: " password_confirm
echo ""
if [ "$password" = "$password_confirm" ]; then
echo "$password"
return 0
else
echo "Passwords don't match. Try again."
fi
done
}
# Usage
user_password=$(read_password)
echo "Password set successfully"
Yes/No Confirmation¶
#!/bin/bash
# Simple yes/no
ask_yes_no() {
local prompt=$1
local default=${2:-}
if [ "$default" = "y" ]; then
prompt="$prompt [Y/n]: "
elif [ "$default" = "n" ]; then
prompt="$prompt [y/N]: "
else
prompt="$prompt [y/n]: "
fi
while true; do
read -p "$prompt" response
# Use default if no response
if [ -z "$response" ] && [ -n "$default" ]; then
response=$default
fi
case "$response" in
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) echo "Please answer yes or no." ;;
esac
done
}
# Usage
if ask_yes_no "Do you want to continue?" "y"; then
echo "Continuing..."
else
echo "Aborted"
exit 1
fi
Menu Selection¶
#!/bin/bash
# Menu selection
show_menu() {
local prompt=$1
shift
local options=("$@")
echo "$prompt"
echo ""
for i in "${!options[@]}"; do
echo " $((i + 1)). ${options[$i]}"
done
echo ""
while true; do
read -p "Enter choice [1-${#options[@]}]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && \
[ "$choice" -ge 1 ] && \
[ "$choice" -le "${#options[@]}" ]; then
echo "$((choice - 1))"
return 0
else
echo "Invalid choice. Please try again."
fi
done
}
# Usage
options=("Option A" "Option B" "Option C" "Quit")
selected=$(show_menu "Please select an option:" "${options[@]}")
echo "You selected: ${options[$selected]}"
Advanced Input with Validation¶
#!/bin/bash
# Input with validation
read_validated() {
local prompt=$1
local validator=$2
local error_msg=$3
while true; do
read -p "$prompt" input
if eval "$validator"; then
echo "$input"
return 0
else
echo "$error_msg" >&2
fi
done
}
# Validators
is_number() { [[ "$input" =~ ^[0-9]+$ ]]; }
is_email() { [[ "$input" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; }
is_not_empty() { [ -n "$input" ]; }
# Usage
name=$(read_validated "Enter your name: " "is_not_empty" "Name cannot be empty")
age=$(read_validated "Enter your age: " "is_number" "Age must be a number")
email=$(read_validated "Enter email: " "is_email" "Invalid email format")
echo "Name: $name"
echo "Age: $age"
echo "Email: $email"
8. Complete CLI Tool Example¶
Putting it all together into a professional CLI tool.
#!/bin/bash
set -euo pipefail
# ============================================================================
# CONFIGURATION
# ============================================================================
SCRIPT_NAME=$(basename "$0")
VERSION="1.0.0"
AUTHOR="Your Name"
# Colors
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
BOLD=$(tput bold)
RESET=$(tput sgr0)
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
# Default values
VERBOSE=0
DRY_RUN=0
OUTPUT_FILE=""
LOG_FILE="/tmp/${SCRIPT_NAME}.log"
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
error() { echo "${RED}ERROR: $*${RESET}" >&2; }
success() { echo "${GREEN}SUCCESS: $*${RESET}"; }
warning() { echo "${YELLOW}WARNING: $*${RESET}" >&2; }
info() { echo "${BLUE}INFO: $*${RESET}"; }
verbose() { [ $VERBOSE -eq 1 ] && echo "${BLUE}VERBOSE: $*${RESET}"; }
die() {
local code=$1
shift
error "$*"
exit "$code"
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
progress_bar() {
local current=$1
local total=$2
local width=40
local percent=$((current * 100 / total))
local completed=$((width * current / total))
printf "\r${BLUE}Progress:${RESET} ["
printf "%${completed}s" | tr ' ' '='
printf "%$((width - completed))s" | tr ' ' ' '
printf "] %3d%%" "$percent"
[ "$current" -eq "$total" ] && echo ""
}
# ============================================================================
# USAGE AND VERSION
# ============================================================================
show_version() {
cat << EOF
$SCRIPT_NAME version $VERSION
Written by $AUTHOR
EOF
exit 0
}
show_help() {
cat << EOF
${BOLD}NAME${RESET}
$SCRIPT_NAME - Process files with various options
${BOLD}SYNOPSIS${RESET}
$SCRIPT_NAME [OPTIONS] FILE...
${BOLD}DESCRIPTION${RESET}
This tool processes files with configurable options.
It demonstrates best practices for CLI argument parsing.
${BOLD}OPTIONS${RESET}
-v, --verbose
Enable verbose output
-n, --dry-run
Perform a dry run without making changes
-o, --output FILE
Specify output file (default: stdout)
-l, --log FILE
Specify log file (default: /tmp/$SCRIPT_NAME.log)
-h, --help
Show this help message
-V, --version
Show version information
${BOLD}EXAMPLES${RESET}
# Basic usage
$SCRIPT_NAME file1.txt file2.txt
# Verbose mode with output file
$SCRIPT_NAME -v --output=result.txt input.txt
# Dry run
$SCRIPT_NAME -n *.txt
${BOLD}EXIT STATUS${RESET}
0 Success
1 General error
2 Invalid arguments
${BOLD}AUTHOR${RESET}
Written by $AUTHOR
EOF
exit 0
}
# ============================================================================
# ARGUMENT PARSING
# ============================================================================
parse_arguments() {
local files=()
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=1
shift
;;
-n|--dry-run)
DRY_RUN=1
shift
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
--output=*)
OUTPUT_FILE="${1#*=}"
shift
;;
-l|--log)
LOG_FILE="$2"
shift 2
;;
--log=*)
LOG_FILE="${1#*=}"
shift
;;
-h|--help)
show_help
;;
-V|--version)
show_version
;;
--)
shift
files+=("$@")
break
;;
-*)
die 2 "Unknown option: $1\nRun '$SCRIPT_NAME --help' for usage"
;;
*)
files+=("$1")
shift
;;
esac
done
# Validate
if [ ${#files[@]} -eq 0 ]; then
die 2 "No input files specified\nRun '$SCRIPT_NAME --help' for usage"
fi
# Check files exist
for file in "${files[@]}"; do
if [ ! -f "$file" ]; then
die 1 "File not found: $file"
fi
done
echo "${files[@]}"
}
# ============================================================================
# MAIN LOGIC
# ============================================================================
process_file() {
local file=$1
verbose "Processing file: $file"
log "Processing: $file"
# Simulate work
sleep 0.5
verbose "Completed: $file"
log "Completed: $file"
}
main() {
log "Script started"
verbose "Verbose mode enabled"
[ $DRY_RUN -eq 1 ] && warning "DRY RUN MODE"
# Parse arguments
local files
IFS=' ' read -ra files <<< "$(parse_arguments "$@")"
info "Processing ${#files[@]} file(s)..."
# Process files
local count=0
local total=${#files[@]}
for file in "${files[@]}"; do
((count++))
progress_bar $count $total
if [ $DRY_RUN -eq 0 ]; then
process_file "$file"
fi
done
success "All files processed successfully"
log "Script completed"
# Save output
if [ -n "$OUTPUT_FILE" ]; then
echo "Results saved to: $OUTPUT_FILE" > "$OUTPUT_FILE"
info "Output saved to: $OUTPUT_FILE"
fi
}
# ============================================================================
# ENTRY POINT
# ============================================================================
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
9. Practice Problems¶
Problem 1: Advanced Option Parser¶
Create a flexible option parser that supports:
- Short options (-v, -o file)
- Long options (--verbose, --output=file)
- Combined short options (-vxf)
- Optional vs required option arguments
- Boolean flags and value options
- Positional arguments
- -- separator
- Validation for each option type
- Auto-generated help from option definitions
Problem 2: Configuration File Integration¶
Build a CLI tool that:
- Accepts options from command line, config file, and environment variables
- Uses priority: CLI > environment > config file > defaults
- Supports multiple config file formats (INI, JSON, YAML)
- Validates all configuration values
- Can output current effective configuration
- Includes --config option to specify config file path
Problem 3: Interactive Setup Wizard¶
Create an interactive setup wizard that: - Guides user through configuration - Validates each input - Shows menu for multi-choice options - Allows going back to previous steps - Confirms before saving - Generates a config file - Has both interactive and non-interactive modes (for automation) - Includes colored output and progress indicators
Problem 4: Git-Style Subcommand Interface¶
Implement a CLI tool with git-style subcommands:
- Main command: mytool <subcommand> [options]
- Multiple subcommands (init, add, remove, list, etc.)
- Each subcommand has its own options and help
- Shared global options (--verbose, --config)
- Tab completion support (bash-completion script)
- Man page generation from help text
- Consistent error handling across all subcommands
Problem 5: CLI Dashboard¶
Build an interactive CLI dashboard that: - Shows real-time status of multiple processes - Updates display every second without scrolling - Uses colors and Unicode characters for visual appeal - Accepts keyboard commands (q=quit, r=refresh, p=pause) - Shows progress bars for running tasks - Logs all events to a file - Can run in non-interactive mode for scripts
Previous: 10_Error_Handling.md | Next: 12_Portability_and_Best_Practices.md