agentsclimarketplace

Bash scripting

Skill ebal/AI-Skills/bash-scripting

A portable, shareable collection of AI agent skills — structured instruction packs that teach coding agents how to perform specific tasks consistently.

Install
npx -y skills add ebal/AI-Skills --skill bash-scripting

Assembled 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 directory
  • readonly for constants prevents accidental mutation
  • IFS=$'\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 pipefail or equivalent error handling
  • Trap set for cleanup (EXIT, ERR)
  • All variables quoted: "$var" not $var
  • Temp files use mktemp
  • Functions use local for 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.