Bash scripting
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.From its SKILL.md
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.
2 things to look at
- 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.
SKILL.md
25.3 KB, ~7.5k tokens by cl100k_base, 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.
What ships with it: 1 file
280 B alongside SKILL.md
- README.md280 B