Bash scripting
A portable, shareable collection of AI agent skills — structured instruction packs that teach coding agents how to perform specific tasks consistently.
npx -y skills add ebal/AI-Skills --skill bash-scriptingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
3 things to look at
- 22 days oldThe repository was created 22 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guide and patterns for writing robust Bash scripts. Use when the user says "write a bash script", "shell script", "bash script", "write a script", "bash best practices", or needs help with Bash/shell scripting. Covers script structure, error handling, string manipulation, arrays, arithmetic, conditionals, loops, functions, traps, debugging, and common idioms distilled from the Advanced Bash-Scripting Guide.
SKILL.md
25.3 KB, as published. Nobody here has run it
Bash Scripting Skill
Comprehensive patterns and best practices distilled from the Advanced Bash-Scripting Guide by Mendel Cooper.
1. Script Skeleton
Every script should start with a proper shebang, strict mode, and metadata.
#!/usr/bin/env bash
#
# script_name.sh — Brief description
# Author: Name <email>
# Date: YYYY-MM-DD
# License: MIT
# Strict mode (set -euo pipefail is debated — use intentionally)
set -euo pipefail
IFS=$'\n\t'
# --- Global Constants ---
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly VERSION="1.0.0"
# --- Functions ---
usage() {
cat <<EOF
Usage: ${SCRIPT_NAME} [OPTIONS] <args>
Description of what this script does.
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-q, --quiet Suppress output
-V, --version Show version
Example:
${SCRIPT_NAME} --verbose file.txt
EOF
}
# --- Logging ---
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
warn() { log "WARN: $*" >&2; }
error() { log "ERROR: $*" >&2; }
die() { error "$*"; exit 1; }
# --- Main ---
main() {
# Argument parsing
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) VERBOSE=1; shift ;;
-q|--quiet) QUIET=1; shift ;;
-V|--version) echo "${VERSION}"; exit 0 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
# Validate inputs
[[ $# -lt 1 ]] && { usage >&2; die "Missing required argument"; }
# --- Script logic here ---
log "Starting..."
}
# Run main only if script is executed (not sourced)
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi
Key patterns:
BASH_SOURCE[0] == "$0"guard lets the script be sourced for testing without auto-executing$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)is the canonical way to find the script's own directoryreadonlyfor constants prevents accidental mutationIFS=$'\n\t'avoids word-splitting bugs with unquoted variables
2. Error Handling
trap (Ch. 34)
trap is the most underused Bash feature. It catches signals and errors.
# Cleanup on exit (best pattern)
cleanup() {
local exit_code=$?
rm -f "${TEMP_FILE:-}"
# Kill background jobs if any
jobs -p | xargs -r kill 2>/dev/null
return "$exit_code"
}
trap cleanup EXIT
# Catch errors with ERR trap (Bash 3.0+)
on_error() {
local line=$1
local code=$2
error "Script failed at line ${line} with exit code ${code}"
}
trap 'on_error ${LINENO} $?' ERR
# Handle signals
trap 'echo "Interrupted"; exit 130' INT
trap '' TERM # Ignore TERM during critical sections
Error handling patterns:
# Check return codes explicitly
command || { die "command failed"; }
# Capture and check
output=$(some_command 2>&1) || die "some_command failed: ${output}"
# Subshell error capture
if ! output=$(command_that_may_fail); then
warn "Non-fatal: $output"
fi
# Pipe failure detection (requires set -o pipefail)
cmd1 | cmd2 | cmd3
# Without pipefail, only cmd3's exit code matters
Atomic file operations:
# Write atomically using a temp file
write_atomic() {
local target="$1"
local tmp="${target}.tmp.$$"
# ... write to "$tmp" ...
mv -f "$tmp" "$target" # atomic on POSIX
}
3. Variables & Parameter Expansion
Quoting (Ch. 3) — THE MOST IMPORTANT RULE
# ALWAYS quote variables unless you intentionally want word splitting
filename="my file.txt"
cat "$filename" # correct
cat $filename # WRONG — word splits on space
# The only safe use of unquoted $* is in echo for display
echo "Args: $*"
# Array expansion
args=("--flag" "value with spaces" "--other")
cmd "${args[@]}" # correct — each element is a separate word
cmd ${args[@]} # WRONG — spaces break it
Parameter Expansion (Ch. 9):
# Default values
${var:-default} # use default if unset/empty (don't assign)
${var:=default} # assign default if unset/empty
${var:+alternate} # use alternate if set and non-empty
${var:?error msg} # die with error if unset/empty
# String manipulation
${#var} # string length
${var#pattern} # remove shortest match from start
${var##pattern} # remove longest match from start
${var%pattern} # remove shortest match from end
${var%%pattern} # remove longest match from end
${var/pattern/repl} # replace first match
${var//pattern/repl} # replace all matches
${var^} # capitalize first letter (Bash 4+)
${var^^} # uppercase all (Bash 4+)
${var,} # lowercase first letter (Bash 4+)
${var,,} # lowercase all (Bash 4+)
# Practical examples
filepath="/home/user/docs/report.pdf"
echo "${filepath##*/}" # → report.pdf (basename)
echo "${filepath%/*}" # → /home/user/docs (dirname)
echo "${filepath##*.}" # → pdf (extension)
echo "${filepath##*/}" | sed 's/\.[^.]*$//' # → report (stem, no sed needed below)
# Better: pure parameter expansion for stem
temp="${filepath##*/}" # report.pdf
echo "${temp%.*}" # report
# Substring extraction
${var:offset:length} # extract from offset for length
${var:offset} # extract from offset to end
# Case modification (Bash 4+)
declare -u upper="hello" # → HELLO
declare -l lower="HELLO" # → hello
Indirect references (Ch. 27):
# Bash 4+ nameref (preferred)
declare -n ref=original_var
ref="new value"
echo "$original_var" # → new value
# Legacy indirect expansion
varname="myvar"
myvar="hello"
echo "${!varname}" # → hello (Bash 4.2+)
# For arrays
arr=(a b c)
idx=1
echo "${!arr[$idx]}" # → b (Bash 4.3+)
4. Arrays (Ch. 6, 27)
# Declaration
declare -a array_name # indexed array
declare -A map_name # associative array (Bash 4+)
# Initialization
fruits=(apple banana cherry)
declare -A config=(
[host]="localhost"
[port]=8080
[debug]="true"
)
# Access
echo "${fruits[0]}" # → apple (0-indexed)
echo "${fruits[-1]}" # → cherry (last element, Bash 4.3+)
echo "${fruits[@]}" # → all elements (space-separated)
echo "${#fruits[@]}" # → 3 (length)
echo "${!fruits[@]}" # → 0 1 2 (indices)
# Slicing
echo "${fruits[@]:1:2}" # → banana cherry (offset, count)
echo "${fruits[@]:2}" # → cherry (offset to end)
# Appending
fruits+=(date elderberry)
# Unset
unset 'fruits[1]' # remove element at index 1 (keeps other indices)
unset fruits # remove entire array
# Iteration
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Iterate with index
for i in "${!fruits[@]}"; do
echo "$i: ${fruits[$i]}"
done
# Associative array iteration
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# Check if key exists
[[ -v config[debug] ]] # Bash 4.2+
# Common patterns
# Collect args into array
args=()
for arg in "$@"; do
args+=("$arg")
done
# Split string into array
IFS=',' read -ra items <<< "a,b,c,d"
# Array length as a conditional
if (( ${#array[@]} > 0 )); then
echo "Array has elements"
fi
5. Conditionals (Ch. 8, 13)
Test constructs — use [[ ]] not [ ]:
# [[ ]] is safer: no word splitting, regex support, pattern matching
if [[ -f "$file" ]]; then
echo "File exists"
fi
# String tests
[[ -z "$var" ]] # empty or unset
[[ -n "$var" ]] # non-empty
[[ "$a" == "$b" ]] # string equality
[[ "$a" != "$b" ]] # string inequality
[[ "$a" < "$b" ]] # lexicographic less-than
[[ "$a" =~ regex ]] # regex match (no anchoring needed)
[[ "$a" == glob* ]] # glob pattern match
# Numeric tests (inside (()))
(( x > 5 ))
(( x >= 5 && x <= 10 ))
(( x % 2 == 0 )) # even check
# File tests
[[ -f "$f" ]] # regular file [[ -L "$f" ]] # symlink
[[ -d "$d" ]] # directory [[ -e "$f" ]] # exists (any type)
[[ -r "$f" ]] # readable [[ -w "$f" ]] # writable
[[ -x "$f" ]] # executable [[ -s "$f" ]] # non-empty file
[[ -p "$f" ]] # named pipe (FIFO) [[ -S "$f" ]] # socket
[[ -b "$b" ]] # block device [[ -c "$c" ]] # character device
[[ "$a" -nt "$b" ]] # a is newer than b
[[ "$a" -ot "$b" ]] # a is older than b
# Compound conditions
[[ "$a" -gt 5 && "$a" -lt 100 ]]
[[ -f "$f" && -r "$f" ]] # exists and readable
[[ -d "$d" ]] || mkdir -p "$d"
Arithmetic evaluation (( )) (Ch. 7):
(( result = 3 + 4 ))
(( result++ ))
(( x += 10 ))
(( a == b )) # returns 0 (true) or 1 (false)
(( a > b )) && echo "a is bigger"
# Useful for numeric validation
[[ "$input" =~ ^[0-9]+$ ]] && (( input > 0 && input < 1024 ))
Ternary and logical short-circuit:
result=$(( condition ? true_val : false_val ))
(( verbose )) && echo "verbose mode"
(( quiet )) || echo "this prints unless quiet"
6. Loops (Ch. 10, 12)
# C-style for loop
for (( i=0; i<10; i++ )); do
echo "$i"
done
# Range-based
for i in {1..10}; do echo "$i"; done
for i in {0..100..5}; do echo "$i"; done # step by 5
# For loop over files (always quote glob)
for f in *; do
[[ -f "$f" ]] || continue
echo "$f"
done
# While loop with file reading (robust pattern)
while IFS= read -r line; do
echo "$line"
done < "$input_file"
# Process substitution (avoids subshell, preserves variables)
while IFS= read -r line; do
count=$((count + 1))
done < <(command_that_produces_output)
# Read CSV with IFS
while IFS=',' read -r col1 col2 col3; do
echo "$col1 | $col2 | $col3"
done < data.csv
# Read with timeout
read -t 5 -r -p "Enter answer: " answer || echo "Timed out"
# Iterate over command output (use process substitution, not pipes)
# BAD: pipe creates subshell — variables don't persist
find /tmp -name "*.log" | while read -r f; do
echo "$f" # works, but any variables set here die after the loop
done
# GOOD: process substitution
while IFS= read -r f; do
log_files+=("$f")
done < <(find /tmp -name "*.log")
# Until loop (run until success)
until ping -c1 google.com &>/dev/null; do
sleep 1
done
# Breaking and continuing
for f in *; do
[[ "$f" == *.tmp ]] && continue # skip .tmp files
[[ "$f" == stop_here ]] && break # stop completely
process "$f"
done
7. Functions (Ch. 11, 24)
# Function definition (both forms work; keyword form is preferred for clarity)
function greet() {
local name="$1"
local greeting="${2:-Hello}"
printf '%s, %s!\n' "$greeting" "$name"
return 0 # explicit return is good practice
}
# Return values: use echo for data, $? for status
get_latest_version() {
local pkg="$1"
# ... logic ...
echo "2.4.1" # stdout is the return value
}
version=$(get_latest_version "myapp") || die "Failed to get version"
# Local variables — ALWAYS use local
# Without local, variables leak to the caller's scope
risky() { var="leaked"; }
safe() { local var="contained"; }
# Default argument handling
process_file() {
local file="${1:?Usage: process_file <file>}"
local mode="${2:-normal}"
# ...
}
# Readonly function-local (Bash 4.2+)
expensive_calc() {
local -r result="$((RANDOM % 100))" # cannot be reassigned
echo "$result"
}
# Passing arrays to functions
pass_array() {
local -n arr_ref="$1" # nameref (Bash 4.3+)
arr_ref+=(new_element)
}
my_array=(a b)
pass_array my_array
echo "${my_array[@]}" # → a b new_element
# Function sourcing pattern
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
else
# Being sourced — export functions, don't run main
export -f func1 func2
fi
8. String Manipulation (Ch. 9, 15)
# String length
echo "${#string}" # bytes (locale-dependent)
echo "${#string[@]}" # array element count
# Substring extraction
echo "${string:offset}" # from offset to end
echo "${string:offset:length}" # offset + length
# Pattern removal
echo "${string#prefix}" # remove shortest prefix
echo "${string##prefix}" # remove longest prefix (greedy)
echo "${string%suffix}" # remove shortest suffix
echo "${string%%suffix}" # remove longest suffix (greedy)
# Pattern replacement
echo "${string/pattern/replacement}" # first occurrence
echo "${string//pattern/replacement}" # all occurrences
echo "${string/#pattern/replacement}" # only at start
echo "${string/%pattern/replacement}" # only at end
# Practical examples
filepath="/var/log/syslog.1.gz"
echo "${filepath##*/}" # → syslog.1.gz (basename)
echo "${filepath%/*}" # → /var/log (dirname)
echo "${filepath##*.}" # → gz (extension)
name="${filepath##*/}"
echo "${name%.*}" # → syslog.1 (strip extension)
echo "${name%.*%.*}" # → syslog (strip all extensions)
# Padding and alignment (Bash 4.4+)
printf '%-20s %10s\n' "left" "right" # left-justify, right-justify
printf '%05d\n' 42 # → 00042 (zero-pad)
# Here-strings and here-docs
command <<< "$variable" # here-string
cat << 'EOF' # here-doc (single quotes prevent expansion)
No expansion happens here.
$VAR stays literal.
EOF
cat <<- EOF # heredoc with tab stripping
$(echo "This works with tabs")
$VAR is expanded
EOF
# Heredoc to variable
read -r -d '' content << 'EOF'
Multi-line content here.
EOF
9. Arithmetic (Ch. 7)
# (( )) is preferred for arithmetic
(( result = a + b ))
(( result++ ))
(( remainder = dividend % divisor ))
# Declare variables as integers for performance
declare -i num=42
num=$(( num * 2 )) # auto-evaluated as arithmetic
# Factorial
factorial() {
local n=$1 result=1
for (( i=2; i<=n; i++ )); do
(( result *= i ))
done
echo "$result"
}
# Floating point — Bash doesn't support it natively. Use bc:
result=$(echo "scale=4; $a / $b" | bc -l)
# Hex/octal/binary literals (Bash 4.2+)
echo $(( 0xFF )) # → 255
echo $(( 0777 )) # → 511
echo $(( 0b1010 )) # → 10
# Random numbers
(( val = RANDOM % 100 + 1 )) # 1-100
uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)
10. Signals, Traps & Process Management (Ch. 34, 16)
# Comprehensive cleanup trap
cleanup() {
local exit_code=$?
set +e # don't let cleanup errors mask the original exit
# Kill background processes
[[ -n "${PID:-}" ]] && kill "$PID" 2>/dev/null
# Remove temp files
rm -f "${TMPFILES[@]}"
# Unlock lock file
[[ -n "${LOCK_FD:-}" ]] && exec {LOCK_FD}>&-
exit "$exit_code"
}
trap cleanup EXIT
# File locking (prevent concurrent runs)
LOCK_FILE="/tmp/${SCRIPT_NAME}.lock"
exec 9>"$LOCK_FILE"
flock -n 9 || { echo "Already running"; exit 1; }
# Timeout wrapper
timeout 30 long_running_command || {
[[ $? -eq 124 ]] && echo "Timed out"
}
# Common signals:
# SIGHUP (1) — terminal hangup
# SIGINT (2) — interrupt (Ctrl+C)
# SIGQUIT (3) — quit (Ctrl+\)
# SIGTERM (15) — termination (kill default)
# SIGKILL (9) — forced kill (cannot be caught)
# SIGCHLD (20) — child process died
# Ignore signals during critical section
trap '' INT TERM
# ... critical code ...
trap - INT TERM # restore default handling
# Background job management
run_with_timeout() {
local timeout=$1; shift
"$@" &
local pid=$!
( sleep "$timeout" && kill "$pid" 2>/dev/null ) &
local watchdog=$!
wait "$pid" 2>/dev/null
local exit_code=$?
kill "$watchdog" 2>/dev/null
return "$exit_code"
}
11. Here-Documents & Here-Strings (Ch. 18, 20)
# Unquoted delimiter: variables expanded
cat << EOF
Home is $HOME
Path is $PATH
EOF
# Quoted delimiter: literal text
cat << 'EOF'
No expansion: $HOME is literal
EOF
# Dash delimiter strips leading tabs (useful in functions)
function install_service() {
cat <<- EOF
[Unit]
Description=My Service
[Service]
ExecStart=/usr/bin/myapp
EOF
}
# Here-string (Bash 4+)
grep "pattern" <<< "$string_variable"
# Process substitution as input
diff <(sort file1) <(sort file2)
while read -r line; do
process "$line"
done < <(generate_output)
# Null here-doc (for creating files atomically)
cat > "$config_file" << 'EOF'
key=value
debug=true
EOF
12. Regular Expressions (Ch. 19)
# [[ =~ ]] for regex (no quoting the pattern)
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
fi
# Capture groups (Bash 4+)
if [[ "$string" =~ ^([0-9]+)-([0-9]+)$ ]]; then
start="${BASH_REMATCH[1]}"
end="${BASH_REMATCH[2]}"
fi
# Named character classes
[[ "$input" =~ ^[[:digit:]]+$ ]] # digits only
[[ "$input" =~ ^[[:alpha:]]+$ ]] # letters only
[[ "$input" =~ ^[[:alnum:]]+$ ]] # alphanumeric
# BRE vs ERE vs PCRE
# [[ =~ ]] uses ERE by default
# For PCRE: use grep -P or perl
# Practical regex patterns
# IP address (simplified)
[[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]
# Semantic version
[[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]
# Glob patterns in [[ == ]]
[[ "$filename" == *.tar.gz ]] # ends with .tar.gz
[[ "$filename" == data_*_20?? ]] # starts with data_, ends with _20XX
13. Networking & File Operations (Ch. 16, 21)
# File descriptor management
exec 3< input.txt # open fd 3 for reading
exec 4> output.txt # open fd 4 for writing
exec 5<> data.txt # open fd 5 for read/write
exec 3<&- # close fd 3
# Parallel reading with multiple file descriptors
exec 3< file1
exec 4< file2
while IFS= read -r -u 3 line1 && IFS= read -r -u 4 line2; do
echo "File1: $line1, File2: $line2"
done
exec 3<&- 4<&-
# Temporary files (safe pattern)
tmpfile=$(mktemp) # auto-named
tmpdir=$(mktemp -d) # temp directory
tmpfile=$(mktemp /tmp/myapp.XXXXXX) # custom prefix
# Named pipe (FIFO) for IPC
mkfifo /tmp/myfifo
echo "data" > /tmp/myfifo &
read -r line < /tmp/myfifo
# Process substitution for comparison
diff <(ssh server1 cat /etc/config) <(ssh server2 cat /etc/config)
# Check network connectivity
if timeout 2 bash -c "echo >/dev/tcp/google.com/443" 2>/dev/null; then
echo "Online"
fi
# Check if port is in use
if ss -tlnp 2>/dev/null | grep -q ":8080 "; then
echo "Port 8080 in use"
fi
14. Common Patterns & Idioms
Argument parsing (getopts and manual):
# Manual (recommended for long options)
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) VERBOSE=1; shift ;;
-o|--output) OUTPUT="$2"; shift 2 ;;
-) shift; break ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) args+=("$1"); shift ;;
esac
done
}
# getopts (short options only, traditional)
while getopts ":hvqo:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
q) QUIET=1 ;;
o) OUTPUT="$OPTARG" ;;
:) die "Option -$OPTARG requires an argument" ;;
\?) die "Unknown option: -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
Safe file iteration:
# ALWAYS use globstar + nullglob for file operations
shopt -s globstar nullglob
for f in **/*.txt; do
echo "$f"
done
# Or handle the empty case
files=(*.log)
if (( ${#files[@]} == 0 )); then
die "No .log files found"
fi
Retry with backoff:
retry() {
local max_attempts=$1; shift
local delay=1
for (( attempt=1; attempt<=max_attempts; attempt++ )); do
"$@" && return 0
if (( attempt < max_attempts )); then
warn "Attempt $attempt/$max_attempts failed, retrying in ${delay}s..."
sleep "$delay"
(( delay *= 2 )) # exponential backoff
fi
done
return 1
}
retry 5 curl -sf http://example.com
Parallel execution:
# Run N jobs in parallel
pids=()
for item in "${items[@]}"; do
process_item "$item" &
pids+=($!)
done
# Wait and check results
failed=0
for pid in "${pids[@]}"; do
wait "$pid" || (( failed++ ))
done
(( failed > 0 )) && die "$failed jobs failed"
Color output:
# Define colors as constants
readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
readonly BLUE='\033[0;34m' BOLD='\033[1m' RESET='\033[0m'
# Only colorize if stdout is a terminal
if [[ -t 1 ]]; then
info() { printf "${GREEN}[INFO]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[WARN]${RESET} %s\n" "$*" >&2; }
error() { printf "${RED}[ERROR]${RESET} %s\n" "$*" >&2; }
else
info() { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
error() { printf '[ERROR] %s\n' "$*" >&2; }
fi
YAML/JSON/TOML config generation:
# Generate simple YAML
generate_config() {
cat << EOF
app:
name: ${APP_NAME}
version: ${VERSION}
debug: ${DEBUG:-false}
database:
host: ${DB_HOST:-localhost}
port: ${DB_PORT:-5432}
EOF
}
# Parse simple key=value config
while IFS='=' read -r key value; do
[[ "$key" =~ ^[[:space:]]*# ]] && continue # skip comments
[[ -z "$key" ]] && continue # skip empty lines
key="${key## }" # trim leading space
key="${key%% }" # trim trailing space
declare "$key=$value"
done < config.ini
15. Debugging (Ch. 22)
# Enable debug output
set -x # print each command before execution
set -v # print each line as read
set -e # exit on first error
set -u # treat unset variables as errors
set -o pipefail # pipelines fail if any command fails
# Selective debugging
set -x
# ... problematic section ...
set +x
# DEBUG trap (runs before each command)
trap 'echo "DEBUG: $BASH_COMMAND" >&2' DEBUG
# Bash profiling
# Run: bash -x script.sh 2> /tmp/trace.log
# Then: /usr/share/bashdb/bashdb /tmp/trace.log
# Useful debugging variables
echo "Script: ${BASH_SOURCE[0]}"
echo "Line: ${LINENO}"
echo "Func: ${FUNCNAME[0]:-main}"
echo "Exit code: $?"
echo "Args: $@"
# Check for common mistakes
shellcheck script.sh # if shellcheck is installed
16. Security Best Practices (Ch. 28, 32)
# 1. Quote everything
rm -- "$filename" # -- prevents filenames starting with -
# 2. Validate input
[[ "$input" =~ ^[0-9]+$ ]] || die "Not a number"
# 3. Use mktemp for temp files (prevent race conditions)
tmpfile=$(mktemp) # never use predictable names like /tmp/myfile
# 4. Set restrictive permissions
umask 077 # new files created with 600/700 permissions
# 5. Avoid eval
# DANGEROUS: eval "$untrusted_input"
# SAFE: Use arrays and case statements instead
# 6. Prefer built-in checks over external commands
[[ -f "$file" ]] # instead of: test -f "$file" (though equivalent)
[[ "$str" =~ ^[0-9]+$ ]] # instead of: echo "$str" | grep -q '^[0-9]+$'
# 7. Lock sensitive data in memory (Bash 4.4+)
read -r -s -p "Password: " password
# ... use password ...
unset password # try to clear from memory (not guaranteed)
# 8. Sanitize filenames
sanitize_filename() {
local name="$1"
name="${name//[^a-zA-Z0-9._-]/_}" # replace unsafe chars
echo "$name"
}
# 9. Check for TOCTOU races (time-of-check-time-of-use)
# BAD:
[[ -f "$file" ]] && rm "$file" # file could be replaced between check and rm
# GOOD:
rm -f "$file" 2>/dev/null # atomically attempt removal
# 10. Avoid temp files in world-writable directories
# Use $TMPDIR or /tmp with restrictive umask
17. Portable Scripting (Ch. 37)
# For POSIX sh compatibility (Bash not guaranteed):
# Avoid: [[ ]], (( )), arrays, ${var/pattern/}, declare, local (though widely supported)
# Use: [ ], test, case, expr or $(( ))
# Detect Bash version
if ((BASH_VERSINFO[0] < 4)); then
echo "Bash 4+ required" >&2
exit 1
fi
# Feature detection
command -v jq &>/dev/null || { echo "jq required"; exit 1; }
# Portable dirname/basename
dir="$(dirname "$0")"
base="$(basename "$0")"
# Portable readlink (macOS compatibility)
realpath_portable() {
local path="$1"
cd "$(dirname "$path")" && echo "$(pwd)/$(basename "$path")"
}
18. Checklists
Before running any script:
-
set -euo pipefailor equivalent error handling - Trap set for cleanup (EXIT, ERR)
- All variables quoted:
"$var"not$var - Temp files use
mktemp - Functions use
localfor variables - Array expansion uses
"${arr[@]}"not${arr[@]} - File operations use
--to end option parsing - Input validated before use
- Colors disabled when not a terminal
-
usage()function exists and is called on bad args
Source: Advanced Bash-Scripting Guide by Mendel Cooper, distilled into actionable patterns.