| name | shell-scripting |
| description | Shell scripting best practices and patterns. Use when writing bash/zsh scripts, automating tasks, creating CLI tools, or debugging shell commands. |
| author | Joseph OBrien |
| status | unpublished |
| updated | 2025-12-23 |
| version | 1.0.1 |
| tag | skill |
| type | skill |
Shell Scripting
Comprehensive shell scripting skill covering bash/zsh patterns, automation, error handling, and CLI tool development.
When to Use This Skill
- Writing automation scripts
- Creating CLI tools
- System administration tasks
- Build and deployment scripts
- Log processing and analysis
- File manipulation and batch operations
- Cron jobs and scheduled tasks
Script Structure
Template
#!/usr/bin/env bash
# Script: name.sh
# Description: What this script does
# Usage: ./name.sh [options] <args>
set -euo pipefail # Exit on error, undefined vars, pipe failures
IFS=$'\n\t' # Safer word splitting
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Default values
VERBOSE=false
DRY_RUN=false
# Functions
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [options] <argument>
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-n, --dry-run Show what would be done
EOF
}
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
}
error() {
log "ERROR: $*"
exit 1
}
# Main logic
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
*)
break
;;
esac
done
# Your logic here
}
main "$@"
Error Handling
Set Options
set -e # Exit on any error
set -u # Error on undefined variables
set -o pipefail # Pipe failure is script failure
set -x # Debug: print each command (use sparingly)
Trap for Cleanup
cleanup() {
rm -f "$TEMP_FILE"
log "Cleanup complete"
}
trap cleanup EXIT
# Also handle specific signals
trap 'error "Script interrupted"' INT TERM
Error Checking Patterns
# Check command exists
command -v jq >/dev/null 2>&1 || error "jq is required but not installed"
# Check file exists
[[ -f "$FILE" ]] || error "File not found: $FILE"
# Check directory exists
[[ -d "$DIR" ]] || mkdir -p "$DIR"
# Check variable is set
[[ -n "${VAR:-}" ]] || error "VAR is not set"
# Check exit status explicitly
if ! some_command; then
error "some_command failed"
fi
Variables & Substitution
Variable Expansion
# Default values
${VAR:-default} # Use default if VAR is unset or empty
${VAR:=default} # Set VAR to default if unset or empty
${VAR:+value} # Use value if VAR is set
${VAR:?error msg} # Error if VAR is unset or empty
# String manipulation
${VAR#pattern} # Remove shortest prefix match
${VAR##pattern} # Remove longest prefix match
${VAR%pattern} # Remove shortest suffix match
${VAR%%pattern} # Remove longest suffix match
${VAR/old/new} # Replace first occurrence
${VAR//old/new} # Replace all occurrences
${#VAR} # Length of VAR
Arrays
# Declare array
declare -a ARRAY=("one" "two" "three")
# Access elements
echo "${ARRAY[0]}" # First element
echo "${ARRAY[@]}" # All elements
echo "${#ARRAY[@]}" # Number of elements
echo "${!ARRAY[@]}" # All indices
# Iterate
for item in "${ARRAY[@]}"; do
echo "$item"
done
# Append
ARRAY+=("four")
Associative Arrays
declare -A MAP
MAP["key1"]="value1"
MAP["key2"]="value2"
# Access
echo "${MAP[key1]}"
# Check key exists
[[ -v MAP[key1] ]] && echo "key1 exists"
# Iterate
for key in "${!MAP[@]}"; do
echo "$key: ${MAP[$key]}"
done
Control Flow
Conditionals
# String comparison
[[ "$str" == "value" ]]
[[ "$str" != "value" ]]
[[ -z "$str" ]] # Empty
[[ -n "$str" ]] # Not empty
# Numeric comparison
[[ "$num" -eq 5 ]] # Equal
[[ "$num" -ne 5 ]] # Not equal
[[ "$num" -lt 5 ]] # Less than
[[ "$num" -gt 5 ]] # Greater than
# File tests
[[ -f "$file" ]] # File exists
[[ -d "$dir" ]] # Directory exists
[[ -r "$file" ]] # Readable
[[ -w "$file" ]] # Writable
[[ -x "$file" ]] # Executable
# Logical operators
[[ "$a" && "$b" ]] # AND
[[ "$a" || "$b" ]] # OR
[[ ! "$a" ]] # NOT
Loops
# For loop
for i in {1..10}; do
echo "$i"
done
# While loop
while read -r line; do
echo "$line"
done < "$file"
# Process substitution
while read -r line; do
echo "$line"
done < <(command)
# C-style for
for ((i=0; i<10; i++)); do
echo "$i"
done
Input/Output
Reading Input
# Read from user
read -r -p "Enter name: " name
# Read password (hidden)
read -r -s -p "Password: " password
# Read with timeout
read -r -t 5 -p "Quick! " answer
# Read file line by line
while IFS= read -r line; do
echo "$line"
done < "$file"
Output & Redirection
# Redirect stdout
command > file # Overwrite
command >> file # Append
# Redirect stderr
command 2> file
# Redirect both
command &> file
command > file 2>&1
# Discard output
command > /dev/null 2>&1
# Tee (output and save)
command | tee file
Text Processing
Common Patterns
# Find and process files
find . -name "*.log" -exec grep "ERROR" {} +
# Process CSV
while IFS=, read -r col1 col2 col3; do
echo "$col1: $col2"
done < file.csv
# JSON processing (with jq)
jq '.key' file.json
jq -r '.items[]' file.json
# AWK one-liners
awk '{print $1}' file # First column
awk -F: '{print $1}' /etc/passwd # Custom delimiter
awk 'NR > 1' file # Skip header
# SED one-liners
sed 's/old/new/g' file # Replace all
sed -i 's/old/new/g' file # In-place edit
sed -n '10,20p' file # Print lines 10-20
Best Practices
Do
- Quote all variable expansions:
"$VAR" - Use
[[ ]]over[ ]for tests - Use
$(command)over backticks - Check return values
- Use
readonlyfor constants - Use
localin functions - Provide
--helpoption - Use meaningful exit codes
Don't
- Parse
lsoutput - Use
evalwith untrusted input - Assume paths don't have spaces
- Ignore shellcheck warnings
- Write one giant script (modularize)
Reference Files
references/one_liners.md- Useful one-liner commands
Integration with Other Skills
- developer-experience - For tooling automation
- debugging - For script debugging
- testing - For script testing patterns