Shell Scripts Best Practices (Community) Comprehensive best practices guide for shell scripting, designed for AI agents and LLMs. Contains 49 rules across 9 categories, prioritized by impact from critical (safety, portability) to incremental (style). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics. When to Apply Reference these guidelines when: - Writing new bash or POSIX shell scripts - Reviewing shell scripts for security vulnerabilities - Debugging scripts that fail silently or behave unexpectedly -…

)\nif [[ -n \"$changed_scripts\" ]]; then\n shellcheck $changed_scripts || exit 1\nfi\n```\n\nReference: [ShellCheck Wiki](https://github.com/koalaman/shellcheck/wiki)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3171,"content_sha256":"d0d8772ee91ff069f520d02fc87cbae397a6f042d9843518d3fe703b44f19726"},{"filename":"references/err-stderr-messages.md","content":"---\ntitle: Send Error Messages to stderr\nimpact: HIGH\nimpactDescription: enables proper output piping and filtering\ntags: err, stderr, stdout, output, messages\n---\n\n## Send Error Messages to stderr\n\nError messages sent to stdout mix with program output, breaking pipes and making automation fail. Always separate errors (stderr) from data (stdout).\n\n**Incorrect (errors to stdout):**\n\n```bash\n#!/bin/bash\n# Error messages go to stdout, breaking pipelines\nif [[ ! -f \"$file\" ]]; then\n echo \"Error: File not found\" # Goes to stdout\nfi\n\n# This breaks:\n# ./script.sh | process_output\n# Error message gets piped to process_output!\n```\n\n**Correct (errors to stderr):**\n\n```bash\n#!/bin/bash\n# Redirect error messages to stderr\nif [[ ! -f \"$file\" ]]; then\n echo \"Error: File not found\" >&2\nfi\n\n# Define error function for consistency\nerr() {\n echo \"[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*\" >&2\n}\n\nwarn() {\n echo \"[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*\" >&2\n}\n\n# Use throughout script\nif [[ ! -d \"$output_dir\" ]]; then\n err \"Output directory does not exist: $output_dir\"\n exit 1\nfi\n```\n\n**Complete logging pattern:**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\n# Log levels\nreadonly LOG_ERROR=0\nreadonly LOG_WARN=1\nreadonly LOG_INFO=2\nreadonly LOG_DEBUG=3\n\nLOG_LEVEL=${LOG_LEVEL:-$LOG_INFO}\n\nlog() {\n local level=$1\n shift\n local msg=\"$*\"\n\n if [[ $level -le $LOG_LEVEL ]]; then\n local prefix\n case $level in\n $LOG_ERROR) prefix=\"ERROR\" ;;\n $LOG_WARN) prefix=\"WARN\" ;;\n $LOG_INFO) prefix=\"INFO\" ;;\n $LOG_DEBUG) prefix=\"DEBUG\" ;;\n esac\n echo \"[$(date +'%Y-%m-%d %H:%M:%S')] $prefix: $msg\" >&2\n fi\n}\n\n# Usage\nlog $LOG_INFO \"Starting process\"\nlog $LOG_ERROR \"Failed to connect\"\n```\n\n**Proper output separation:**\n\n```bash\n#!/bin/bash\n# stdout: Data output (can be piped)\n# stderr: Progress, status, errors (goes to terminal)\n\nprocess_files() {\n for file in \"$@\"; do\n echo \"Processing: $file\" >&2 # Status to stderr\n cat \"$file\" # Data to stdout\n done\n}\n\n# Usage: ./script.sh file1 file2 > output.txt\n# Status messages appear on terminal, data goes to file\n```\n\nReference: [Google Shell Style Guide - STDOUT vs STDERR](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2232,"content_sha256":"c0376f97e3695f0a2c87fd11d1d304598e374ceea23816faf43c86244c57e91f"},{"filename":"references/err-strict-mode.md","content":"---\ntitle: Use Strict Mode for Error Detection\nimpact: HIGH\nimpactDescription: catches 60-80% of common failures when combined with explicit checks\ntags: err, strict-mode, errexit, nounset, pipefail\n---\n\n## Use Strict Mode for Error Detection\n\nWithout strict mode, scripts continue after failures, undefined variables expand to empty strings, and pipeline errors are hidden. This causes silent data corruption.\n\n**Incorrect (no error checking):**\n\n```bash\n#!/bin/bash\n# Script continues after failures silently\ncd /nonexistent/directory\nrm -rf * # Deletes files in WRONG directory!\n\n# Undefined variable expands to empty\nrm -rf \"${TEMP_DIR}/\"* # If unset: rm -rf /*\n\n# Pipeline failure hidden\ncurl http://example.com | process_data\n# Even if curl fails, process_data runs with empty input\n```\n\n**Correct (strict mode enabled):**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\n# -e (errexit): Exit on any command failure\n# -u (nounset): Error on undefined variables\n# -o pipefail: Pipeline fails if any command fails\n\ncd /nonexistent/directory # Script exits here\nrm -rf * # Never reached\n\n# Undefined variable causes error\nrm -rf \"${TEMP_DIR}/\"* # Error: TEMP_DIR: unbound variable\n\n# Pipeline failure detected\ncurl http://example.com | process_data # Exits if curl fails\n```\n\n**Handle intentional failures:**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\n# Method 1: || true for commands that may fail\ngrep \"pattern\" file.txt || true\n\n# Method 2: Conditional check\nif grep -q \"pattern\" file.txt; then\n echo \"Found\"\nelse\n echo \"Not found\"\nfi\n\n# Method 3: Temporarily disable errexit\nset +e\nrisky_command\nstatus=$?\nset -e\n```\n\n**Additional safety options:**\n\n```bash\n#!/bin/bash\nset -euo pipefail\nshopt -s inherit_errexit # Subshells inherit errexit\nshopt -s nullglob # Globs expand to nothing if no match\n```\n\n**Caveats — `set -e` does NOT catch everything:**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\n# 1. Disabled inside if/while/until conditions\nif some_failing_command; then # No exit — set -e disabled in condition\n echo \"success\"\nfi\n\n# 2. Disabled in left side of && / ||\nfailing_command && echo \"ok\" # No exit — set -e disabled before &&\n\n# 3. Does NOT propagate into command substitutions (bash \u003c 4.4)\nresult=$(failing_command) # May not exit without inherit_errexit\n# Fix: shopt -s inherit_errexit (bash 4.4+)\n\n# 4. local masks exit status (even with set -e)\nmy_func() {\n local config=$(cat /missing/file) # No exit! local succeeds\n}\n# Fix: separate declaration and assignment (see port-export-syntax)\n```\n\nAlways combine `set -e` with explicit error checks for critical operations. Do not rely on it as the sole safety mechanism.\n\nReference: [Greg's Wiki - BashFAQ/105](https://mywiki.wooledge.org/BashFAQ/105)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2755,"content_sha256":"59ca216a9b30de31e97beb3a0f51ede1071b81b3cf5c9076c4d7d3fd93088fc6"},{"filename":"references/err-trap-cleanup.md","content":"---\ntitle: Use trap for Cleanup on Exit\nimpact: HIGH\nimpactDescription: prevents resource leaks and orphaned processes\ntags: err, trap, cleanup, signals, exit\n---\n\n## Use trap for Cleanup on Exit\n\nWithout cleanup traps, scripts leave behind temporary files, running background processes, and held locks when interrupted or on errors.\n\n**Incorrect (no cleanup handling):**\n\n```bash\n#!/bin/bash\nbuild_log=$(mktemp)\nstaging_dir=$(mktemp -d)\n\n# If script is interrupted (Ctrl+C) or fails,\n# these files are never cleaned up\nprocess_data > \"$build_log\"\n# ... more operations\nrm \"$build_log\"\nrm -rf \"$staging_dir\" # May never be reached\n```\n\n**Correct (trap-based cleanup):**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\n# Global cleanup variables\nBUILD_LOG=\"\"\nSTAGING_DIR=\"\"\nWORKER_PID=\"\"\n\ncleanup() {\n local exit_code=$?\n\n # Remove working files\n [[ -n \"$BUILD_LOG\" && -f \"$BUILD_LOG\" ]] && rm -f \"$BUILD_LOG\"\n [[ -n \"$STAGING_DIR\" && -d \"$STAGING_DIR\" ]] && rm -rf \"$STAGING_DIR\"\n\n # Kill background processes\n [[ -n \"$WORKER_PID\" ]] && kill \"$WORKER_PID\" 2>/dev/null || true\n\n exit \"$exit_code\"\n}\n\n# Register cleanup on EXIT only — EXIT fires on all shell exits\n# including signal-induced exits (INT, TERM), avoiding double execution\ntrap cleanup EXIT\n\n# Now create resources\nBUILD_LOG=$(mktemp)\nSTAGING_DIR=$(mktemp -d)\n\n# Script work here - cleanup runs automatically on exit\nprocess_data > \"$BUILD_LOG\"\n```\n\n**Common mistake — trapping EXIT + signals causes double execution:**\n\n```bash\n#!/bin/bash\n# WRONG: cleanup runs TWICE on SIGINT/SIGTERM\n# (once for signal handler, once for EXIT when shell exits)\ntrap cleanup EXIT ERR INT TERM\n\n# CORRECT: EXIT alone catches all exit paths including signals\ntrap cleanup EXIT\n```\n\n**Separate handlers for signal-specific behavior:**\n\n```bash\n#!/bin/bash\n# Use separate handlers only when different behavior is needed per signal\ntrap 'echo \"Interrupted\" >&2; exit 130' INT\ntrap 'echo \"Terminated\" >&2; exit 143' TERM\ntrap 'cleanup' EXIT\n\n# ERR trap for debugging (separate concern from cleanup)\ntrap 'echo \"Error on line $LINENO: $BASH_COMMAND\" >&2' ERR\n```\n\n**Lock file pattern with trap:**\n\n```bash\n#!/bin/bash\nLOCKFILE=\"/var/run/myapp.lock\"\n\nacquire_lock() {\n if ! mkdir \"$LOCKFILE\" 2>/dev/null; then\n echo \"Another instance is running\" >&2\n exit 1\n fi\n trap 'rm -rf \"$LOCKFILE\"' EXIT\n}\n\nacquire_lock\n# Script runs exclusively\n```\n\nReference: [Greg's Wiki - BashFAQ/105](https://mywiki.wooledge.org/BashFAQ/105)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2473,"content_sha256":"c7b97271185d30a5da57ed588721e71061c6f21c9f71a32cb16c552b47cea518"},{"filename":"references/func-avoid-aliases.md","content":"---\ntitle: Prefer Functions Over Aliases\nimpact: MEDIUM\nimpactDescription: enables arguments and proper scoping\ntags: func, aliases, reusability, arguments\n---\n\n## Prefer Functions Over Aliases\n\nAliases are simple text substitution with limitations: no arguments in the middle, no local variables, unpredictable expansion. Functions handle all cases properly.\n\n**Incorrect (using aliases):**\n\n```bash\n#!/bin/bash\n# Aliases have severe limitations\n\n# Can't use arguments in the middle\nalias greet='echo \"Hello, $1\"'\ngreet World # Prints \"Hello, \" then \"World\" as separate command\n\n# Alias expands unexpectedly\nalias rm='rm -i'\n# Then: rm -f file.txt\n# Becomes: rm -i -f file.txt (not what you expected)\n\n# Can't use local variables or logic\nalias random_name='echo \"prefix_${RANDOM}\"'\n# RANDOM evaluated at definition time, not call time!\n\n# Can't have multi-line logic\nalias complex='cmd1 && cmd2' # Works but ugly\n```\n\n**Correct (use functions):**\n\n```bash\n#!/bin/bash\n# Functions solve all alias limitations\n\n# Arguments work naturally\ngreet() {\n echo \"Hello, $1\"\n}\ngreet World # Prints \"Hello, World\"\n\n# Logic and local variables\nsafe_rm() {\n local file=\"$1\"\n if [[ -f \"$file\" ]]; then\n rm -i \"$file\"\n else\n echo \"File not found: $file\" >&2\n return 1\n fi\n}\n\n# Dynamic values evaluated at call time\nrandom_name() {\n echo \"prefix_${RANDOM}\"\n}\nrandom_name # Different each time\n\n# Multi-line logic is clean\nbackup_and_edit() {\n local file=\"$1\"\n cp \"$file\" \"${file}.bak\"\n \"${EDITOR:-vim}\" \"$file\"\n}\n```\n\n**When aliases are acceptable:**\n\n```bash\n# Interactive shell shortcuts (in .bashrc, NOT scripts)\nalias ll='ls -la'\nalias ..='cd ..'\nalias grep='grep --color=auto'\n\n# These are OK because:\n# - Used interactively, not in scripts\n# - Simple substitution is sufficient\n# - No arguments needed in middle\n```\n\n**Converting aliases to functions:**\n\n```bash\n# Alias version (limited)\nalias gitlog='git log --oneline -n 10'\n\n# Function version (flexible)\ngitlog() {\n local count=\"${1:-10}\"\n git log --oneline -n \"$count\"\n}\ngitlog # Shows 10\ngitlog 20 # Shows 20\n\n# Alias with complex options\nalias findbig='find . -size +10M -exec ls -lh {} \\;'\n\n# Function version (configurable)\nfindbig() {\n local size=\"${1:-10M}\"\n local dir=\"${2:-.}\"\n find \"$dir\" -size \"+$size\" -exec ls -lh {} \\;\n}\nfindbig # Default: 10M in current dir\nfindbig 100M /var # 100M in /var\n```\n\n**Functions can call functions:**\n\n```bash\n#!/bin/bash\n# Composition that aliases can't do\nlog() {\n echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $*\" >&2\n}\n\ninfo() { log \"INFO: $*\"; }\nwarn() { log \"WARN: $*\"; }\nerror() { log \"ERROR: $*\"; }\n\ninfo \"Starting process\"\nwarn \"Low disk space\"\nerror \"Connection failed\"\n```\n\nReference: [Google Shell Style Guide - Features to Avoid](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2821,"content_sha256":"c5867f1ba6ed372414ea79b1695674ba74a6c8537a82b3032b7efec4b92703f8"},{"filename":"references/func-documentation.md","content":"---\ntitle: Document Functions with Header Comments\nimpact: MEDIUM\nimpactDescription: enables maintenance and API understanding\ntags: func, documentation, comments, headers\n---\n\n## Document Functions with Header Comments\n\nComplex functions without documentation become unmaintainable. Header comments explain purpose, parameters, and return values for future readers and users.\n\n**Incorrect (no documentation):**\n\n```bash\n#!/bin/bash\nprocess_data() {\n local f=\"$1\"\n local o=\"$2\"\n local v=\"${3:-false}\"\n # What do these parameters mean?\n # What does this return?\n # What are the side effects?\n}\n```\n\n**Correct (documented function):**\n\n```bash\n#!/bin/bash\n#######################################\n# Process input data and generate output.\n#\n# Reads the input file, applies transformation rules,\n# and writes results to output file or stdout.\n#\n# Globals:\n# CONFIG_FILE: Path to configuration (read)\n# PROCESSED_COUNT: Incremented for each file (modified)\n#\n# Arguments:\n# $1 - Input file path (required)\n# $2 - Output file path (optional, defaults to stdout)\n# $3 - Verbose mode: true/false (optional, default: false)\n#\n# Outputs:\n# Writes processed data to stdout or output file.\n# Progress messages to stderr if verbose.\n#\n# Returns:\n# 0 - Success\n# 1 - Input file not found\n# 2 - Permission denied\n# 3 - Invalid input format\n#######################################\nprocess_data() {\n local input_file=\"$1\"\n local output_file=\"${2:-}\"\n local verbose=\"${3:-false}\"\n\n # Implementation...\n}\n```\n\n**When to document:**\n\n```bash\n#!/bin/bash\n# Document these:\n# - Library functions (used by other scripts)\n# - Complex logic (non-obvious behavior)\n# - Public API functions\n# - Functions with side effects\n# - Functions with multiple parameters\n\n# Skip documentation for:\n# - Trivial one-liners that are self-explanatory\n# - Private helper functions with obvious names\n\n# Trivial - name is self-documenting\ndie() {\n echo \"$*\" >&2\n exit 1\n}\n\n# Non-obvious - needs documentation\n#######################################\n# Retries a command with exponential backoff.\n# Arguments:\n# $1 - Max attempts\n# $@ - Command to run\n# Returns:\n# Exit status of command, or 1 if all retries fail\n#######################################\nretry_with_backoff() {\n # ...\n}\n```\n\n**Minimal documentation template:**\n\n```bash\n#!/bin/bash\n# Short description of what the function does.\n# Arguments: $1 - description, $2 - description\n# Returns: 0 on success, 1 on error\nfunction_name() {\n # ...\n}\n```\n\n**File header template:**\n\n```bash\n#!/bin/bash\n#\n# Script name and one-line description.\n#\n# Longer description of what this script does, when to use it,\n# and any important caveats or prerequisites.\n#\n# Usage: script.sh [options] \u003crequired_arg>\n#\n# Options:\n# -h, --help Show help\n# -v, --verbose Verbose output\n#\n# Examples:\n# script.sh input.txt\n# script.sh -v -o output.txt input.txt\n#\n# Dependencies:\n# - jq (JSON processing)\n# - curl (HTTP requests)\n#\n\nset -euo pipefail\n```\n\nReference: [Google Shell Style Guide - Function Comments](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3119,"content_sha256":"ee1aab94fe887784b9f38420331c5411d735f4694271488df03c51fd35eee898"},{"filename":"references/func-main-pattern.md","content":"---\ntitle: Use main() Function Pattern\nimpact: MEDIUM\nimpactDescription: enables testing and prevents execution on source\ntags: func, main, structure, sourcing, testing\n---\n\n## Use main() Function Pattern\n\nCode at file level executes when sourced for testing. Wrapping logic in `main()` with a guard allows both direct execution and sourcing without side effects.\n\n**Incorrect (top-level execution):**\n\n```bash\n#!/bin/bash\n# All code runs immediately, even when sourced\nconfig_file=\"/etc/app.conf\"\nload_config\n\nprocess_args \"$@\"\ndo_work\ncleanup\n\n# Cannot source this file for testing without side effects\n# source script.sh # Immediately runs everything!\n```\n\n**Correct (main function with guard):**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\n# Constants and function definitions\nreadonly CONFIG_FILE=\"/etc/app.conf\"\n\nload_config() {\n # ...\n}\n\nprocess() {\n local input=\"$1\"\n # ...\n}\n\ncleanup() {\n # ...\n}\n\nmain() {\n local args=(\"$@\")\n\n load_config\n process \"${args[@]}\"\n cleanup\n}\n\n# Only run main if executed directly (not sourced)\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n main \"$@\"\nfi\n```\n\n**Testing sourced functions:**\n\n```bash\n#!/bin/bash\n# test_script.sh\n\n# Source the script without running main\nsource ./my_script.sh\n\n# Now test individual functions\ntest_process() {\n local result\n result=$(process \"test input\")\n if [[ \"$result\" != \"expected output\" ]]; then\n echo \"FAIL: process returned '$result'\"\n return 1\n fi\n echo \"PASS: process\"\n}\n\ntest_process\n```\n\n**Alternative guards:**\n\n```bash\n#!/bin/bash\n# Method 1: BASH_SOURCE comparison (recommended)\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n main \"$@\"\nfi\n\n# Method 2: Function existence check\nif ! declare -f main > /dev/null; then\n main() { :; } # Define no-op for sourcing\nfi\n\n# Method 3: Environment variable\nif [[ \"${SCRIPT_TESTING:-}\" != \"true\" ]]; then\n main \"$@\"\nfi\n```\n\n**Script structure template:**\n\n```bash\n#!/bin/bash\n#\n# Description of what this script does.\n#\nset -euo pipefail\n\n# Constants\nreadonly SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nreadonly SCRIPT_NAME=\"$(basename \"${BASH_SOURCE[0]}\")\"\n\n# Global variables with defaults\n: \"${LOG_LEVEL:=info}\"\n\n# Function definitions (alphabetical or logical order)\ncleanup() { :; }\nparse_args() { :; }\nprocess() { :; }\nusage() { :; }\n\n# Main entry point\nmain() {\n trap cleanup EXIT\n parse_args \"$@\"\n process\n}\n\n# Run if executed directly\n[[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]] && main \"$@\"\n```\n\nReference: [Google Shell Style Guide - main](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2560,"content_sha256":"15cae577b6ef29b8187726f9a0606b4eae0f2e0224f3d1545f12f1fbda31b62a"},{"filename":"references/func-return-values.md","content":"---\ntitle: Use Return Values Correctly\nimpact: MEDIUM\nimpactDescription: enables proper error propagation and testing\ntags: func, return, exit-status, output\n---\n\n## Use Return Values Correctly\n\nFunctions communicate results via exit status (return code) and stdout. Mixing status and output or ignoring return values causes silent failures.\n\n**Incorrect (mixed output and status):**\n\n```bash\n#!/bin/bash\n# Returns data AND error message on same channel\nget_value() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"Error: File not found\" # Goes to stdout!\n return 1\n fi\n cat \"$file\"\n}\n\n# Caller gets error message as data\nvalue=$(get_value missing.txt)\necho \"Value: $value\" # Prints \"Value: Error: File not found\"\n```\n\n**Correct (separate status and output):**\n\n```bash\n#!/bin/bash\n# Status via return, errors to stderr, data to stdout\nget_value() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"Error: File not found: $file\" >&2 # stderr\n return 1 # Non-zero status\n fi\n cat \"$file\" # stdout\n}\n\n# Caller checks status and captures output\nif value=$(get_value missing.txt); then\n echo \"Value: $value\"\nelse\n echo \"Failed to get value\" >&2\nfi\n```\n\n**Return values for different scenarios:**\n\n```bash\n#!/bin/bash\n# Boolean check - use return directly\nis_valid_email() {\n local email=\"$1\"\n [[ \"$email\" =~ ^[^@]+@[^@]+\\.[^@]+$ ]]\n # Return status of [[ ]] implicitly\n}\n\nif is_valid_email \"$input\"; then\n echo \"Valid\"\nfi\n\n# Computation - output to stdout\ncalculate_sum() {\n local a=\"$1\"\n local b=\"$2\"\n echo \"$((a + b))\"\n}\n\nresult=$(calculate_sum 5 3)\n\n# Multiple outputs - use arrays or delimiter\nget_file_info() {\n local file=\"$1\"\n local size name\n size=$(wc -c \u003c \"$file\" 2>/dev/null) || return 1\n name=$(basename \"$file\")\n echo \"$size:$name\" # Colon-separated\n}\n\n# Read multiple values\nIFS=: read -r size name \u003c \u003c(get_file_info \"/path/to/file\")\n```\n\n**Preserve exit status:**\n\n```bash\n#!/bin/bash\n# Exit status is lost after any command\nrun_and_log() {\n local result\n result=$(some_command)\n local status=$? # Capture IMMEDIATELY\n\n echo \"Result: $result\" >> log.txt\n return \"$status\" # Propagate original status\n}\n\n# Or use || for error handling\nrun_safely() {\n some_command || {\n local status=$?\n echo \"Failed with status $status\" >&2\n return \"$status\"\n }\n}\n```\n\n**Avoid return with command output:**\n\n```bash\n#!/bin/bash\n# WRONG: return with command output\nbad_function() {\n return $(some_command) # Word splitting issues!\n}\n\n# CORRECT: Store in variable first\ngood_function() {\n local status\n some_command\n status=$?\n return \"$status\"\n}\n\n# CORRECT: Implicit return of last command\nsimple_function() {\n some_command\n # Exit status of some_command is returned\n}\n```\n\nReference: [Google Shell Style Guide - Calling Functions](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2854,"content_sha256":"c3edf25090a73baf7f00cad8209562da857c1dfd0c8e21ef71130f778ec25021"},{"filename":"references/func-single-purpose.md","content":"---\ntitle: Write Single-Purpose Functions\nimpact: MEDIUM\nimpactDescription: reduces function complexity from O(n) concerns to O(1), enabling isolated testing\ntags: func, design, single-responsibility, modularity\n---\n\n## Write Single-Purpose Functions\n\nFunctions that do multiple tasks are hard to test, reuse, and debug. Each function should do one task well and compose with others for complex operations.\n\n**Incorrect (multi-purpose function):**\n\n```bash\n#!/bin/bash\n# Does too many concerns: parse args, validate, process, output\nprocess_file() {\n local file=\"$1\"\n local verbose=\"$2\"\n local output=\"$3\"\n\n # Validation\n if [[ ! -f \"$file\" ]]; then\n echo \"Error: File not found\" >&2\n return 1\n fi\n\n # Processing\n local result\n result=$(grep -c \"pattern\" \"$file\")\n\n # Output (mixed concerns)\n if [[ \"$verbose\" == \"true\" ]]; then\n echo \"Processing $file...\"\n fi\n\n if [[ -n \"$output\" ]]; then\n echo \"$result\" > \"$output\"\n else\n echo \"$result\"\n fi\n}\n```\n\n**Correct (focused functions):**\n\n```bash\n#!/bin/bash\n# Each function has a single responsibility\n\nvalidate_file() {\n local file=\"$1\"\n if [[ ! -f \"$file\" ]]; then\n echo \"Error: File not found: $file\" >&2\n return 1\n fi\n if [[ ! -r \"$file\" ]]; then\n echo \"Error: File not readable: $file\" >&2\n return 1\n fi\n}\n\ncount_pattern() {\n local file=\"$1\"\n local pattern=\"$2\"\n grep -c \"$pattern\" \"$file\"\n}\n\nlog_verbose() {\n local message=\"$1\"\n if [[ \"${VERBOSE:-false}\" == \"true\" ]]; then\n echo \"$message\" >&2\n fi\n}\n\nwrite_output() {\n local content=\"$1\"\n local output_file=\"${2:-}\"\n\n if [[ -n \"$output_file\" ]]; then\n echo \"$content\" > \"$output_file\"\n else\n echo \"$content\"\n fi\n}\n\n# Compose functions\nprocess_file() {\n local file=\"$1\"\n local pattern=\"${2:-pattern}\"\n local output=\"${3:-}\"\n\n validate_file \"$file\" || return 1\n log_verbose \"Processing $file...\"\n\n local result\n result=$(count_pattern \"$file\" \"$pattern\")\n write_output \"$result\" \"$output\"\n}\n```\n\n**Function naming guidelines:**\n\n```bash\n#!/bin/bash\n# Use verb_noun format\nget_config() # Retrieves configuration\nset_option() # Assigns an option value\nvalidate_input() # Checks input constraints\nprocess_file() # Transforms file content\ncheck_status() # Verifies system state\n\n# Boolean functions: use is_, has_, can_, should_\nis_valid()\nhas_permission()\ncan_write()\nshould_retry()\n\n# Private/internal functions: prefix with underscore\n_parse_internal()\n_helper_function()\n```\n\n**Short functions are better:**\n\n```bash\n#!/bin/bash\n# Aim for functions that fit on one screen (~20-30 lines)\n# If longer, break into smaller functions\n\n# Too long? Extract helpers:\nprocess_all() {\n local -a files\n gather_files files\n validate_all files\n transform_all files\n output_results files\n}\n```\n\nReference: [Google Shell Style Guide - Function Comments](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2892,"content_sha256":"6d123eded092537ba44c9980db9daaf7309c2000798bde956cf8072503aace54"},{"filename":"references/perf-avoid-subshells.md","content":"---\ntitle: Avoid Unnecessary Subshells\nimpact: LOW-MEDIUM\nimpactDescription: reduces fork overhead and variable scope issues\ntags: perf, subshells, fork, pipes\n---\n\n## Avoid Unnecessary Subshells\n\nSubshells create new processes with copied state. Pipelines, `$()`, and `()` create subshells. Variables modified in subshells are lost when they exit.\n\n**Incorrect (unnecessary subshells):**\n\n```bash\n#!/bin/bash\n# Pipeline creates subshell - variable lost\ncat file.txt | while read -r line; do\n ((count++))\ndone\necho \"Count: $count\" # Still 0! count modified in subshell\n\n# Useless use of cat creates subshell\ncat file | grep pattern\n\n# Command substitution for simple output\necho $(pwd)\n\n# Unnecessary ( ) grouping\n(cd /var/cache/app && rm -f *.log) # cd affects only subshell\n```\n\n**Correct (avoid subshells):**\n\n```bash\n#!/bin/bash\n# Process substitution keeps loop in main shell\nwhile read -r line; do\n ((count++))\ndone \u003c \u003c(cat file.txt)\necho \"Count: $count\" # Correct value\n\n# Or redirect directly\nwhile read -r line; do\n ((count++))\ndone \u003c file.txt\n\n# Remove useless cat\ngrep pattern file\n\n# Variable instead of command substitution\necho \"$PWD\"\n\n# Use { } for grouping without subshell\n{\n cd /var/cache/app && rm -f *.log\n} # cd affects current shell!\n\n# Or be explicit about wanting subshell isolation\n(\n cd /var/cache/app # Only affects subshell\n rm -f *.log\n)\n# Still in original directory here\n```\n\n**When subshells are created:**\n\n```bash\n#!/bin/bash\n# Pipeline (each command in pipeline)\ncmd1 | cmd2 | cmd3 # Three subshells\n\n# Command substitution\nvar=$(command) # One subshell\n\n# Explicit subshell\n( commands ) # One subshell\n\n# Process substitution\n\u003c(command) # One subshell\n>(command) # One subshell\n\n# Background processes\ncommand & # One subshell\n```\n\n**Passing data from subshells:**\n\n```bash\n#!/bin/bash\n# Problem: Can't use variables from subshell\necho \"hello\" | read -r var\necho \"$var\" # Empty!\n\n# Solution 1: Here-string\nread -r var \u003c\u003c\u003c \"hello\"\necho \"$var\" # \"hello\"\n\n# Solution 2: Process substitution\nread -r var \u003c \u003c(echo \"hello\")\necho \"$var\" # \"hello\"\n\n# Solution 3: File or named pipe\nstaging_file=$(mktemp)\ncommand > \"$staging_file\"\nread -r var \u003c \"$staging_file\"\nrm \"$staging_file\"\n\n# Solution 4: Command output to array\nmapfile -t lines \u003c \u003c(command)\nfor line in \"${lines[@]}\"; do\n process \"$line\"\ndone\n```\n\n**Check subshell level:**\n\n```bash\n#!/bin/bash\necho \"Main: $BASH_SUBSHELL\" # 0\n(\n echo \"Subshell: $BASH_SUBSHELL\" # 1\n (\n echo \"Nested: $BASH_SUBSHELL\" # 2\n )\n)\n```\n\n**Intentional subshell uses:**\n\n```bash\n#!/bin/bash\n# Isolate cd - don't affect main script\n(cd /some/dir && do_work)\n\n# Isolate variable changes\n(\n export TEMP_VAR=\"value\"\n run_with_temp_env\n)\n# TEMP_VAR doesn't exist here\n\n# Parallel execution\n(slow_task1) &\n(slow_task2) &\nwait\n```\n\nReference: [Greg's Wiki - SubShell](https://mywiki.wooledge.org/SubShell)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2927,"content_sha256":"0c047330a504619d9a54848bba24fa6563f34ed319e7544c01214273d2a03c91"},{"filename":"references/perf-batch-operations.md","content":"---\ntitle: Batch Operations Instead of Loops\nimpact: LOW-MEDIUM\nimpactDescription: single command vs N process spawns\ntags: perf, batch, loops, find, xargs\n---\n\n## Batch Operations Instead of Loops\n\nLooping over files and running a command for each spawns N processes. Batch commands like `find -exec +`, `xargs`, or glob expansion run one process for many files.\n\n**Incorrect (one command per file):**\n\n```bash\n#!/bin/bash\n# Spawns grep N times\nfor file in *.log; do\n grep \"error\" \"$file\"\ndone\n\n# Spawns chmod N times\nfor file in $(find . -name \"*.sh\"); do\n chmod +x \"$file\"\ndone\n\n# Spawns rm N times\nfor file in /var/cache/myapp/*; do\n rm \"$file\"\ndone\n```\n\n**Correct (batch operations):**\n\n```bash\n#!/bin/bash\n# One grep with multiple files\ngrep \"error\" *.log\n\n# find -exec with + (batches arguments)\nfind . -name \"*.sh\" -exec chmod +x {} +\n\n# rm with glob (one invocation)\nrm /var/cache/myapp/*\n\n# xargs for complex batching\nfind . -name \"*.log\" -print0 | xargs -0 grep \"error\"\n```\n\n**find -exec + vs \\;:**\n\n```bash\n#!/bin/bash\n# \\; runs command once per file (slow)\nfind . -name \"*.txt\" -exec grep \"pattern\" {} \\;\n# Equivalent to: grep pattern file1; grep pattern file2; ...\n\n# + batches files into single command (fast)\nfind . -name \"*.txt\" -exec grep \"pattern\" {} +\n# Equivalent to: grep pattern file1 file2 file3 ...\n\n# Handles ARG_MAX automatically - splits if too many\n```\n\n**xargs for complex batching:**\n\n```bash\n#!/bin/bash\n# Basic xargs\nfind . -name \"*.log\" | xargs rm\n\n# Handle spaces and special chars with -0\nfind . -name \"*.log\" -print0 | xargs -0 rm\n\n# Limit batch size\nfind . -name \"*.log\" -print0 | xargs -0 -n 100 rm\n\n# Parallel execution\nfind . -name \"*.log\" -print0 | xargs -0 -P 4 -n 10 compress\n\n# With placeholder\nfind . -name \"*.txt\" -print0 | xargs -0 -I {} cp {} /backup/\n\n# Run if no input (--no-run-if-empty)\nfind . -name \"*.bak\" -print0 | xargs -0 --no-run-if-empty rm\n```\n\n**When loops are necessary:**\n\n```bash\n#!/bin/bash\n# When you need shell logic per file\nfor file in *.txt; do\n if [[ -s \"$file\" ]]; then\n # Complex shell logic\n base=\"${file%.txt}\"\n mv \"$file\" \"${base}_$(date +%Y%m%d).txt\"\n fi\ndone\n\n# When you need variables from iteration\ntotal=0\nfor file in *.csv; do\n count=$(wc -l \u003c \"$file\")\n ((total += count))\ndone\necho \"Total lines: $total\"\n```\n\n**Parallel batch operations:**\n\n```bash\n#!/bin/bash\n# GNU parallel for complex parallel batching\nfind . -name \"*.jpg\" | parallel convert {} -resize 50% {.}_thumb.jpg\n\n# xargs parallel\nfind . -name \"*.gz\" -print0 | xargs -0 -P $(nproc) gunzip\n\n# Background jobs (manual parallelism)\nfor file in *.dat; do\n process \"$file\" &\ndone\nwait # Wait for all background jobs\n```\n\n**Safe batch operations:**\n\n```bash\n#!/bin/bash\n# Always handle special filenames\n# Use -print0 and -0 for null-separated\n\n# Dangerous (spaces, newlines break this)\nfind . -name \"*.txt\" | xargs rm\n\n# Safe\nfind . -name \"*.txt\" -print0 | xargs -0 rm\n\n# Or use find -exec directly\nfind . -name \"*.txt\" -exec rm {} +\n```\n\nReference: [GNU Findutils Manual](https://www.gnu.org/software/findutils/manual/html_mono/find.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3099,"content_sha256":"5265bf3534836c88a5e803174eebf25075a8b69c7d1051954b095a82bda3f6cd"},{"filename":"references/perf-builtins-over-external.md","content":"---\ntitle: Use Builtins Over External Commands\nimpact: LOW-MEDIUM\nimpactDescription: 10-100× faster by avoiding fork/exec overhead\ntags: perf, builtins, external, fork\n---\n\n## Use Builtins Over External Commands\n\nEvery external command spawns a new process (fork + exec). In loops, this overhead multiplies. Builtins execute in the same process, avoiding this cost.\n\n**Incorrect (external commands in loops):**\n\n```bash\n#!/bin/bash\n# Each iteration spawns external processes\nfor i in $(seq 1 1000); do # External: seq\n result=$(expr $i + 1) # External: expr\n echo \"$result\" | cat >> output # External: cat\ndone\n# Total: 3000+ process spawns!\n\n# Using external commands for simple operations\nif [ \"$(echo \"$var\" | wc -c)\" -gt 10 ]; then\n echo \"long\"\nfi\n```\n\n**Correct (use builtins):**\n\n```bash\n#!/bin/bash\n# Builtins - no process spawning\nfor ((i = 1; i \u003c= 1000; i++)); do # Builtin: (( ))\n ((result = i + 1)) # Builtin: (( ))\n echo \"$result\" >> output # Builtin: echo, redirection\ndone\n# Total: 0 external processes\n\n# Parameter expansion instead of external commands\nif [[ ${#var} -gt 10 ]]; then\n echo \"long\"\nfi\n```\n\n**Common replacements:**\n\n```bash\n#!/bin/bash\n# Arithmetic\n# BAD: result=$(expr $a + $b)\n# GOOD: ((result = a + b))\n\n# String length\n# BAD: len=$(echo \"$str\" | wc -c)\n# GOOD: len=${#str}\n\n# Substring\n# BAD: sub=$(echo \"$str\" | cut -c1-5)\n# GOOD: sub=${str:0:5}\n\n# Basename\n# BAD: name=$(basename \"$path\")\n# GOOD: name=${path##*/}\n\n# Dirname\n# BAD: dir=$(dirname \"$path\")\n# GOOD: dir=${path%/*}\n\n# Search and replace\n# BAD: new=$(echo \"$str\" | sed 's/old/new/g')\n# GOOD: new=${str//old/new}\n\n# Upper/lowercase (bash 4+)\n# BAD: lower=$(echo \"$str\" | tr 'A-Z' 'a-z')\n# GOOD: lower=${str,,}\n# GOOD: upper=${str^^}\n\n# Sequence generation\n# BAD: for i in $(seq 1 10); do\n# GOOD: for ((i = 1; i \u003c= 10; i++)); do\n\n# Reading files line by line\n# BAD: cat file | while read line; do\n# GOOD: while read -r line; do ... done \u003c file\n```\n\n**Check if command is builtin:**\n\n```bash\n#!/bin/bash\n# type shows if command is builtin\ntype echo # echo is a shell builtin\ntype cat # cat is /bin/cat\ntype [[ # [[ is a shell keyword\n\n# Use help for builtin documentation\nhelp echo\nhelp read\nhelp printf\n```\n\n**When external commands are fine:**\n\n```bash\n#!/bin/bash\n# Outside loops - one-time cost is negligible\ndate=$(date +%Y-%m-%d)\nhostname=$(hostname)\n\n# Complex text processing - sed/awk are optimized\n# Piping many lines through sed is faster than bash loop\nsed 's/old/new/g' large_file.txt\n\n# When builtin doesn't exist or is limited\n# sort, uniq, grep for large data sets\n```\n\nReference: [Greg's Wiki - Builtins](https://mywiki.wooledge.org/BashGuide/CommandsAndArguments#Types_of_Commands)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2786,"content_sha256":"e073429da8ddf6506db50484501ac3bfa2989dffbae0574126df0497cb179193"},{"filename":"references/perf-parameter-expansion.md","content":"---\ntitle: Use Parameter Expansion for String Operations\nimpact: LOW-MEDIUM\nimpactDescription: avoids external commands for common transformations\ntags: perf, parameter-expansion, strings, substitution\n---\n\n## Use Parameter Expansion for String Operations\n\nExternal commands like `basename`, `dirname`, `sed`, `cut` spawn processes. Parameter expansion handles most string operations as builtins, eliminating fork overhead.\n\n**Incorrect (external commands):**\n\n```bash\n#!/bin/bash\npath=\"/home/user/documents/report.txt\"\n\n# External commands for string manipulation\nfilename=$(basename \"$path\") # Spawns process\ndirectory=$(dirname \"$path\") # Spawns process\nextension=$(echo \"$path\" | sed 's/.*\\.//') # Two processes\nname=$(echo \"$filename\" | sed 's/\\.[^.]*$//') # Two processes\nupper=$(echo \"$str\" | tr 'a-z' 'A-Z') # Two processes\n```\n\n**Correct (parameter expansion):**\n\n```bash\n#!/bin/bash\npath=\"/home/user/documents/report.txt\"\n\n# Builtins - no external processes\nfilename=${path##*/} # report.txt\ndirectory=${path%/*} # /home/user/documents\nextension=${path##*.} # txt\nname=${filename%.*} # report\nupper=${str^^} # UPPERCASE (bash 4+)\nlower=${str,,} # lowercase (bash 4+)\n```\n\n**Parameter expansion reference:**\n\n```bash\n#!/bin/bash\nvar=\"hello.world.txt\"\n\n# Remove prefix (shortest match)\n${var#*.} # world.txt\n\n# Remove prefix (longest match)\n${var##*.} # txt\n\n# Remove suffix (shortest match)\n${var%.*} # hello.world\n\n# Remove suffix (longest match)\n${var%%.*} # hello\n\n# Substitution (first occurrence)\n${var/world/there} # hello.there.txt\n\n# Substitution (all occurrences)\n${var//./-} # hello-world-txt\n\n# Length\n${#var} # 15\n\n# Substring\n${var:0:5} # hello\n${var:6} # world.txt\n${var: -3} # txt (note the space before -)\n${var:(-3)} # txt (alternative)\n\n# Case conversion (bash 4+)\n${var^} # Hello.world.txt (first char upper)\n${var^^} # HELLO.WORLD.TXT (all upper)\n${var,} # hello.world.txt (first char lower)\n${var,,} # hello.world.txt (all lower)\n```\n\n**Common use cases:**\n\n```bash\n#!/bin/bash\n# Extract filename and extension\nfilepath=\"/path/to/image.tar.gz\"\nfilename=${filepath##*/} # image.tar.gz\ndir=${filepath%/*} # /path/to\nbase=${filename%%.*} # image\next=${filename#*.} # tar.gz\n\n# Change extension\nnewfile=${filepath%.tar.gz}.zip # /path/to/image.zip\n\n# Strip leading/trailing whitespace (bash 4.4+)\ntrimmed=${string#\"${string%%[![:space:]]*}\"}\ntrimmed=${trimmed%\"${trimmed##*[![:space:]]}\"}\n\n# Or simpler with read\nread -r trimmed \u003c\u003c\u003c \"$string\"\n\n# Add prefix/suffix to all elements\nfiles=(*.txt)\nprefixed=(\"${files[@]/#/backup_}\") # backup_file1.txt ...\nsuffixed=(\"${files[@]/%/.bak}\") # file1.txt.bak ...\n\n# Check prefix/suffix\nif [[ \"$var\" == prefix* ]]; then\n echo \"Starts with prefix\"\nfi\nif [[ \"$var\" == *suffix ]]; then\n echo \"Ends with suffix\"\nfi\n```\n\n**Default values:**\n\n```bash\n#!/bin/bash\n# Use default if unset or empty\n${var:-default}\n\n# Set default if unset or empty\n${var:=default}\n\n# Error if unset or empty\n${var:?error message}\n\n# Use alternate if set\n${var:+alternate}\n```\n\nReference: [Bash Manual - Parameter Expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3455,"content_sha256":"40eeeb09b5b567b3d76ddf0eb432a7f839b9dc1fc44c8f566d5b04dc42420481"},{"filename":"references/perf-process-substitution.md","content":"---\ntitle: Use Process Substitution for Temp Files\nimpact: LOW-MEDIUM\nimpactDescription: eliminates file I/O and cleanup overhead\ntags: perf, process-substitution, intermediate-files, pipes\n---\n\n## Use Process Substitution for Temp Files\n\nCreating intermediate files for intermediate data requires file I/O and cleanup. Process substitution `\u003c()` and `>()` provides files that are actually pipes, avoiding disk overhead.\n\n**Incorrect (intermediate files for intermediate data):**\n\n```bash\n#!/bin/bash\n# Creating intermediate files is slow and needs cleanup\nsorted1=$(mktemp)\nsorted2=$(mktemp)\ntrap 'rm -f \"$sorted1\" \"$sorted2\"' EXIT\n\nsort file1.txt > \"$sorted1\"\nsort file2.txt > \"$sorted2\"\ndiff \"$sorted1\" \"$sorted2\"\n\n# Multiple intermediates with risky predictable names\nfiltered_first=$(mktemp)\nfiltered_second=$(mktemp)\ntrap 'rm -f \"$filtered_first\" \"$filtered_second\"' EXIT\ngrep \"pattern1\" input > \"$filtered_first\"\ngrep \"pattern2\" \"$filtered_first\" > \"$filtered_second\"\nwc -l \"$filtered_second\"\n```\n\n**Correct (process substitution):**\n\n```bash\n#!/bin/bash\n# No intermediate files, no cleanup needed\ndiff \u003c(sort file1.txt) \u003c(sort file2.txt)\n\n# Chain processing without files\nwc -l \u003c(grep \"pattern2\" \u003c(grep \"pattern1\" input))\n\n# Or simpler with pipes for linear chains\ngrep \"pattern1\" input | grep \"pattern2\" | wc -l\n```\n\n**Process substitution types:**\n\n```bash\n#!/bin/bash\n# \u003c(command) - command output as file (reading)\n# Compare output of two commands\ndiff \u003c(ls dir1) \u003c(ls dir2)\n\n# Feed multiple sources to command expecting files\npaste \u003c(cut -f1 file1) \u003c(cut -f2 file2)\n\n# >(command) - write to command as file (writing)\n# Tee to multiple processors\n./generate_data | tee >(grep \"error\" > errors.log) >(grep \"warn\" > warns.log)\n\n# Avoid file for single-use data\ncommand > >(process_output)\n```\n\n**Practical examples:**\n\n```bash\n#!/bin/bash\n# Compare sorted versions of files\ndiff \u003c(sort -u file1.txt) \u003c(sort -u file2.txt)\n\n# Compare remote and local files\ndiff \u003c(curl -s http://example.com/file) local_file.txt\n\n# Join data from different sources\njoin \u003c(sort file1.txt) \u003c(sort file2.txt)\n\n# Feed compressed data to commands expecting files\nzcat large_file.gz > decompressed_log.txt\nwc -l decompressed_log.txt\nrm decompressed_log.txt\n# Better:\nwc -l \u003c(zcat large_file.gz)\n\n# Source from process output\nsource \u003c(generate_config)\n\n# Compare directory listings with filters\ndiff \u003c(ls -la dir1 | awk '{print $9, $5}') \\\n \u003c(ls -la dir2 | awk '{print $9, $5}')\n```\n\n**Combining with while loops:**\n\n```bash\n#!/bin/bash\n# Reading from process - keeps variables in main shell\ncount=0\nwhile read -r line; do\n ((count++))\n process \"$line\"\ndone \u003c \u003c(find . -name \"*.txt\")\necho \"Processed $count files\"\n\n# Without process substitution, need intermediate file or lost variables\n```\n\n**Limitations:**\n\n```bash\n#!/bin/bash\n# Process substitution is bash/ksh/zsh only - not POSIX\n# In POSIX sh, use intermediate files or pipes\n\n# Can't seek in process substitution (it's a pipe)\n# This won't work:\nhead -1 \u003c(generate_data)\ntail -1 \u003c(generate_data) # Data is gone!\n\n# Instead:\ndata=$(generate_data)\necho \"$data\" | head -1\necho \"$data\" | tail -1\n```\n\nReference: [Bash Manual - Process Substitution](https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3273,"content_sha256":"5e5dc3c220c5085f1c764223c56028ac26aae4ab78d8815423e43f0e31e90b83"},{"filename":"references/perf-read-files.md","content":"---\ntitle: Read Files Efficiently\nimpact: LOW-MEDIUM\nimpactDescription: prevents O(n) line reads and subshell overhead\ntags: perf, files, reading, loops\n---\n\n## Read Files Efficiently\n\nReading files line-by-line in bash is slow. For large files, use specialized tools (awk, sed, grep). When shell processing is needed, use efficient patterns.\n\n**Incorrect (slow patterns):**\n\n```bash\n#!/bin/bash\n# Spawns cat + creates subshell\ncat file.txt | while read -r line; do\n process \"$line\"\ndone\n\n# Reading file multiple times\nline_count=$(wc -l \u003c file.txt)\nfirst_line=$(head -1 file.txt)\nlast_line=$(tail -1 file.txt)\n\n# Processing large files in shell loop\nwhile read -r line; do\n echo \"${line//old/new}\"\ndone \u003c large_file.txt # Extremely slow\n```\n\n**Correct (efficient patterns):**\n\n```bash\n#!/bin/bash\n# Direct redirection - no cat, no subshell variable loss\nwhile IFS= read -r line; do\n process \"$line\"\ndone \u003c file.txt\n\n# Read entire file at once for small files\ncontent=$(\u003cfile.txt)\n\n# Read into array\nmapfile -t lines \u003c file.txt\nfor line in \"${lines[@]}\"; do\n process \"$line\"\ndone\n\n# Use awk/sed for large file transformations\nsed 's/old/new/g' large_file.txt > output.txt\nawk '{print $1, $3}' large_file.txt > output.txt\n```\n\n**Choosing the right approach:**\n\n```bash\n#!/bin/bash\n# Small files (\u003c 100 lines) - shell is fine\nwhile IFS= read -r line; do\n # Shell processing OK\ndone \u003c small_file.txt\n\n# Medium files (100-10000 lines) - consider awk/sed\n# If you need shell variables, mapfile is good\nmapfile -t lines \u003c medium_file.txt\n\n# Large files (> 10000 lines) - always use awk/sed/grep\n# Shell loops are too slow\ngrep \"pattern\" huge_file.txt | head -100\nawk '/pattern/ {print $2}' huge_file.txt\n```\n\n**Reading specific parts:**\n\n```bash\n#!/bin/bash\n# First N lines\nhead -n 10 file.txt\n\n# Last N lines\ntail -n 10 file.txt\n\n# Lines M to N\nsed -n '5,10p' file.txt\n\n# Read first line into variable\nIFS= read -r first_line \u003c file.txt\n\n# Read first N lines into array\nmapfile -t first_10 -n 10 \u003c file.txt\n```\n\n**Handling special characters:**\n\n```bash\n#!/bin/bash\n# IFS= prevents leading/trailing whitespace trimming\n# -r prevents backslash interpretation\nwhile IFS= read -r line; do\n echo \"[$line]\" # Preserves exact content\ndone \u003c file.txt\n\n# Handle files without final newline\nwhile IFS= read -r line || [[ -n \"$line\" ]]; do\n process \"$line\"\ndone \u003c file.txt\n\n# Handle null-separated data (find -print0)\nwhile IFS= read -r -d '' file; do\n process \"$file\"\ndone \u003c \u003c(find . -name \"*.txt\" -print0)\n```\n\n**Processing fields efficiently:**\n\n```bash\n#!/bin/bash\n# Parse fields in one read\nwhile IFS=: read -r user _ uid gid _ home shell; do\n echo \"User $user uses $shell\"\ndone \u003c /etc/passwd\n\n# Process CSV\nwhile IFS=, read -r field1 field2 field3; do\n process \"$field1\" \"$field2\" \"$field3\"\ndone \u003c data.csv\n\n# But for complex CSV, use a real parser\n# Bash can't handle quoted fields with embedded commas\n```\n\nReference: [Greg's Wiki - BashFAQ/001](https://mywiki.wooledge.org/BashFAQ/001)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2992,"content_sha256":"12654d09411b369664c5976dda9119ca87e5cb4354d296447ea72c76304780fe"},{"filename":"references/port-avoid-bashisms.md","content":"---\ntitle: Avoid Bashisms in POSIX Scripts\nimpact: CRITICAL\nimpactDescription: prevents failures on dash/ash/busybox systems\ntags: port, bashisms, posix, dash, compatibility\n---\n\n## Avoid Bashisms in POSIX Scripts\n\nScripts with `#!/bin/sh` shebang must avoid bash-specific features. On Ubuntu/Debian, `/bin/sh` is dash; on Alpine/BusyBox, it's ash. Bashisms cause silent failures or syntax errors.\n\n**Incorrect (bashisms in /bin/sh script):**\n\n```sh\n#!/bin/sh\n# These fail on dash/ash:\n\n# Arrays don't exist\nfiles=(one two three)\n\n# [[ ]] is bash-only\n[[ -f \"$file\" ]]\n\n# source is bash-only\nsource ./lib.sh\n\n# &> redirection is bash-only\ncommand &> /dev/null\n\n# Process substitution is bash-only\ndiff \u003c(cmd1) \u003c(cmd2)\n\n# echo -e is not portable\necho -e \"line1\\nline2\"\n```\n\n**Correct (POSIX-compliant alternatives):**\n\n```sh\n#!/bin/sh\n# POSIX equivalents:\n\n# Use positional parameters or newline-separated strings\nset -- one two three\nfor file in \"$@\"; do echo \"$file\"; done\n\n# Use [ ] single brackets\n[ -f \"$file\" ]\n\n# Use . (dot) instead of source\n. ./lib.sh\n\n# Use explicit redirections\ncommand > /dev/null 2>&1\n\n# Use mktemp instead of process substitution\ndiff_left=$(mktemp) diff_right=$(mktemp)\ntrap 'rm -f \"$diff_left\" \"$diff_right\"' EXIT\ncmd1 > \"$diff_left\"\ncmd2 > \"$diff_right\"\ndiff \"$diff_left\" \"$diff_right\"\n\n# Use printf instead of echo -e\nprintf 'line1\\nline2\\n'\n```\n\n**Common bashisms to avoid:**\n| Bashism | POSIX Alternative |\n|---------|------------------|\n| `[[ ]]` | `[ ]` with proper quoting |\n| `source` | `. ` (dot space) |\n| `&>` | `> file 2>&1` |\n| `

Shell Scripts Best Practices (Community) Comprehensive best practices guide for shell scripting, designed for AI agents and LLMs. Contains 49 rules across 9 categories, prioritized by impact from critical (safety, portability) to incremental (style). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics. When to Apply Reference these guidelines when: - Writing new bash or POSIX shell scripts - Reviewing shell scripts for security vulnerabilities - Debugging scripts that fail silently or behave unexpectedly -…

...'` | `printf` |\n| `array=()` | positional parameters |\n| `${var,,}` | `tr '[:upper:]' '[:lower:]'` |\n| `{1..10}` | `seq 1 10` |\n\nReference: [ShellCheck SC2039](https://www.shellcheck.net/wiki/SC2039)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1779,"content_sha256":"6f581c3aadc2fccae627423958dd17661b92c2f1134572bb81de56343ad5468c"},{"filename":"references/port-export-syntax.md","content":"---\ntitle: Separate Local Declaration from Command Substitution\nimpact: CRITICAL\nimpactDescription: prevents masked exit codes in 100% of local+assignment cases\ntags: port, export, local, variables, posix, exit-status\n---\n\n## Separate Local Declaration from Command Substitution\n\nCombining `local` (or `declare`) with command substitution masks the exit status. The `local` builtin always succeeds, overwriting `$?` from the substituted command. This causes silent failures even with `set -e`.\n\n**Incorrect (combined declaration and assignment):**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\nprocess_config() {\n # local succeeds, masking the failed command's exit status\n local config_data=$(cat /nonexistent/config.yaml)\n echo \"Status: $?\" # Always 0 — local succeeded!\n echo \"$config_data\"\n}\n\nprocess_config # No error raised despite missing file\n```\n\n**Correct (separate declaration and assignment):**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\nprocess_config() {\n local config_data\n config_data=$(cat /nonexistent/config.yaml) # Exits here with set -e\n echo \"Status: $?\" # Shows actual exit status\n echo \"$config_data\"\n}\n\nprocess_config # Correctly fails if file is missing\n```\n\n**Multiple variables:**\n\n```bash\n#!/bin/bash\nset -euo pipefail\n\ndeploy_service() {\n # Declare all locals first\n local image_tag\n local registry_url\n local deploy_status\n\n # Then assign — exit status is preserved\n image_tag=$(get_latest_tag \"$service_name\")\n registry_url=$(resolve_registry \"$environment\")\n\n docker pull \"${registry_url}/${service_name}:${image_tag}\"\n}\n```\n\n**Applies to both bash and POSIX sh:**\n\n```sh\n#!/bin/sh\n# POSIX sh has the same issue with local (where supported)\nfetch_user() {\n local user_json\n user_json=$(curl -sf \"https://api.example.com/users/$1\") || return 1\n echo \"$user_json\"\n}\n```\n\n**Note on export:** The `export VAR=value` combined syntax is valid POSIX (IEEE Std 1003.1-2001). Unlike `local`, `export VAR=$(cmd)` does preserve exit status in most shells, but separating them is still clearer and avoids confusion.\n\nReference: [ShellCheck SC2155](https://www.shellcheck.net/wiki/SC2155)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2128,"content_sha256":"3f85fda8ee551bc33687d3b166f12dd46cda268de5f976aaf23d3465c6583e7e"},{"filename":"references/port-printf-over-echo.md","content":"---\ntitle: Use printf Instead of echo for Portability\nimpact: CRITICAL\nimpactDescription: eliminates 5+ echo behavior differences across bash, dash, zsh, and busybox\ntags: port, printf, echo, output, compatibility\n---\n\n## Use printf Instead of echo for Portability\n\n`echo` behavior varies across shells and systems. Some interpret `-n`, `-e` flags; others print them literally. `printf` is standardized and behaves consistently everywhere.\n\n**Incorrect (echo with non-portable options):**\n\n```bash\n#!/bin/sh\n# Behavior varies by system:\necho -n \"Enter name: \" # Some shells print \"-n \"\necho -e \"line1\\nline2\" # Some shells print \"-e line1\\nline2\"\necho \"Path: $PATH\" # Some echo versions expand backslashes\n\n# Even without flags, echo is problematic:\necho \"$var\" # If var=\"-n\", output is blank\n```\n\n**Correct (printf is consistent):**\n\n```sh\n#!/bin/sh\n# printf works the same everywhere:\nprintf \"Enter name: \" # No trailing newline\nprintf \"line1\\nline2\\n\" # Escapes always interpreted\nprintf \"Path: %s\\n\" \"$PATH\" # Safe with any content\n\n# Safe with arbitrary data:\nprintf \"%s\\n\" \"$var\" # Works even if var=\"-n\"\nprintf \"%s\" \"$var\" # No trailing newline\n```\n\n**printf format specifiers:**\n\n```sh\n#!/bin/sh\nname=\"Alice\"\ncount=42\nprice=19.99\n\n# String interpolation\nprintf \"User: %s\\n\" \"$name\"\n\n# Integer formatting\nprintf \"Count: %d items\\n\" \"$count\"\n\n# Floating point\nprintf \"Price: %.2f\\n\" \"$price\"\n\n# Multiple arguments\nprintf \"%s has %d items\\n\" \"$name\" \"$count\"\n\n# Width and padding\nprintf \"%-10s %5d\\n\" \"$name\" \"$count\"\n```\n\n**When echo is acceptable:**\n- Bash-only scripts (`#!/bin/bash`)\n- Simple strings without variables\n- No special characters or flags needed\n- Performance-critical loops (echo is slightly faster)\n\nReference: [Rich's POSIX sh tricks](http://www.etalabs.net/sh_tricks.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1860,"content_sha256":"43b459c7a1ba2f265a9813d53ebb62d73c82d888ab17907e838b196cb1642899"},{"filename":"references/port-shebang-selection.md","content":"---\ntitle: Choose Shebang Based on Portability Needs\nimpact: CRITICAL\nimpactDescription: prevents 100% of bashism failures by matching interpreter to features used\ntags: port, shebang, posix, bash, compatibility\n---\n\n## Choose Shebang Based on Portability Needs\n\nThe shebang line determines which interpreter runs your script. Using `#!/bin/sh` implies POSIX compliance, while `#!/bin/bash` enables bash-specific features but reduces portability.\n\n**Incorrect (mismatched shebang and features):**\n\n```bash\n#!/bin/sh\n# Uses bash-specific features with sh shebang\n# Fails on systems where /bin/sh is dash, ash, or busybox\n\ndeclare -a array=(one two three) # bash-only\n[[ $var == pattern ]] # bash-only\necho \"value is $((x + 1))\" # mostly portable\nsource ./config.sh # bash-only (use . instead)\n```\n\n**Correct (match shebang to features used):**\n\n```bash\n#!/bin/bash\n# Bash script - use bash-specific features freely\nset -euo pipefail\n\ndeclare -a files=()\nwhile IFS= read -r -d '' file; do\n files+=(\"$file\")\ndone \u003c \u003c(find . -type f -print0)\n\nif [[ \"${#files[@]}\" -gt 0 ]]; then\n process_files \"${files[@]}\"\nfi\n```\n\n**Correct (POSIX-compliant script):**\n\n```sh\n#!/bin/sh\n# POSIX script - avoid bashisms for maximum portability\nset -eu\n\n# Use . instead of source\n. ./config.sh\n\n# Use [ ] instead of [[ ]]\nif [ -n \"$var\" ]; then\n echo \"var is set\"\nfi\n\n# No arrays - use positional parameters or files\nset -- one two three\nfor item in \"$@\"; do\n echo \"$item\"\ndone\n```\n\n**Decision guide:**\n- `#!/bin/bash` - Complex scripts, arrays, `[[ ]]`, process substitution\n- `#!/bin/sh` - Simple scripts, containers, embedded systems, CI pipelines\n- `#!/usr/bin/env bash` - When bash location varies (macOS vs Linux)\n\nReference: [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1829,"content_sha256":"e80d9fdf9b59aac8b4526fcfae8d2be62f7c1bd87505865d9c05a754a110a98b"},{"filename":"references/port-test-portability.md","content":"---\ntitle: Use Portable Test Constructs\nimpact: CRITICAL\nimpactDescription: prevents silent logic failures across shells\ntags: port, test, conditionals, posix, brackets\n---\n\n## Use Portable Test Constructs\n\n`[[ ]]` is bash/ksh/zsh-only. POSIX shells only support `[ ]` (test). Using `[[ ]]` in `/bin/sh` scripts causes syntax errors or silent failures.\n\n**Incorrect (bash-only tests in sh script):**\n\n```sh\n#!/bin/sh\n# [[ ]] is not POSIX - fails on dash/ash\nif [[ -f \"$file\" ]]; then\n echo \"exists\"\nfi\n\n# Regex matching is bash-only\nif [[ \"$input\" =~ ^[0-9]+$ ]]; then\n echo \"numeric\"\nfi\n\n# Pattern matching without quotes is bash-only\nif [[ $var == *.txt ]]; then\n echo \"text file\"\nfi\n```\n\n**Correct (POSIX-compliant tests):**\n\n```sh\n#!/bin/sh\n# Use [ ] with proper quoting\nif [ -f \"$file\" ]; then\n echo \"exists\"\nfi\n\n# Use case for pattern matching (POSIX)\ncase \"$input\" in\n *[!0-9]*|\"\")\n echo \"not numeric\"\n ;;\n *)\n echo \"numeric\"\n ;;\nesac\n\n# Use case for glob patterns\ncase \"$var\" in\n *.txt)\n echo \"text file\"\n ;;\nesac\n```\n\n**Test operator portability:**\n\n```sh\n#!/bin/sh\n# These work in POSIX [ ]:\n[ -f \"$file\" ] # File exists and is regular\n[ -d \"$dir\" ] # Directory exists\n[ -n \"$var\" ] # String is non-empty\n[ -z \"$var\" ] # String is empty\n[ \"$a\" = \"$b\" ] # String equality (single =)\n[ \"$a\" != \"$b\" ] # String inequality\n[ \"$a\" -eq \"$b\" ] # Numeric equality\n[ \"$a\" -lt \"$b\" ] # Numeric less than\n[ -r \"$file\" ] # File is readable\n\n# Combine with -a (and) and -o (or), or use separate [ ]:\n[ -f \"$file\" ] && [ -r \"$file\" ] # Preferred\n```\n\nReference: [ShellCheck SC3010](https://www.shellcheck.net/wiki/SC3010)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1719,"content_sha256":"e87033c35aef6ed049bae23433ec0155c4207585c812404b228f1b6aa135962a"},{"filename":"references/quote-always-quote-variables.md","content":"---\ntitle: Always Quote Variable Expansions\nimpact: MEDIUM-HIGH\nimpactDescription: prevents word splitting and glob expansion bugs\ntags: quote, variables, word-splitting, globbing\n---\n\n## Always Quote Variable Expansions\n\nUnquoted variables undergo word splitting (on IFS) and pathname expansion (globbing). A filename with spaces becomes multiple arguments; `*` expands to all files.\n\n**Incorrect (unquoted variables):**\n\n```bash\n#!/bin/bash\nfile=\"my file.txt\"\nrm $file # Tries to remove \"my\" and \"file.txt\"\n\npattern=\"*.log\"\necho $pattern # Expands to all .log files!\n\ndir=\"/path/with spaces\"\ncd $dir # Fails: /path/with and spaces are separate\n\n# Dangerous with user input\nuser_input=\"foo; rm -rf /\"\ngrep $user_input file.txt\n```\n\n**Correct (quoted variables):**\n\n```bash\n#!/bin/bash\nfile=\"my file.txt\"\nrm \"$file\" # Removes \"my file.txt\"\n\npattern=\"*.log\"\necho \"$pattern\" # Prints literal \"*.log\"\n\ndir=\"/path/with spaces\"\ncd \"$dir\" # Changes to \"/path/with spaces\"\n\n# Safe with user input\nuser_input=\"foo; rm -rf /\"\ngrep \"$user_input\" file.txt # Searches for literal string\n```\n\n**When NOT to quote:**\n\n```bash\n#!/bin/bash\n# Intentional globbing - document it!\n# shellcheck disable=SC2086\nfor file in $file_glob; do\n process \"$file\"\ndone\n\n# Integer arithmetic context (already unquoted)\ncount=5\n(( count++ ))\nif (( count > 10 )); then\n echo \"Done\"\nfi\n\n# Array expansion with [@] already handles it\nfiles=(\"one\" \"two\" \"three\")\nprocess \"${files[@]}\" # Already properly split\n```\n\n**Variable expansion in different contexts:**\n\n```bash\n#!/bin/bash\nvar=\"value\"\n\n# Always quote in these contexts:\necho \"$var\"\n[[ \"$var\" == \"value\" ]]\nif [ -n \"$var\" ]; then ...; fi\ncmd --option=\"$var\"\ncmd --option \"$var\"\n\n# Brace syntax is preferred for clarity\necho \"${var}\"\necho \"${var}_suffix\"\n\n# Inside [[ ]], right side of == can be unquoted for patterns\n[[ \"$var\" == val* ]] # Pattern match (unquoted)\n[[ \"$var\" == \"val*\" ]] # Literal match (quoted)\n```\n\nReference: [ShellCheck SC2086](https://www.shellcheck.net/wiki/SC2086)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2074,"content_sha256":"b1466e4e7ab4fc3c04015beea8b6354f855ea403ffd82111ab80dfc01f4c3ba0"},{"filename":"references/quote-brace-expansion.md","content":"---\ntitle: Use Braces for Variable Clarity\nimpact: MEDIUM-HIGH\nimpactDescription: prevents ambiguous variable boundaries\ntags: quote, braces, variables, expansion, clarity\n---\n\n## Use Braces for Variable Clarity\n\nWithout braces, variable boundaries are ambiguous. `$var_suffix` looks for variable `var_suffix`, not `$var` + `_suffix`. Braces make boundaries explicit.\n\n**Incorrect (ambiguous boundaries):**\n\n```bash\n#!/bin/bash\nprefix=\"file\"\n# What variable is this?\necho $prefix_name.txt # Looks for $prefix_name, not $prefix\necho $prefix1 # Looks for $prefix1, not $prefix + \"1\"\n\n# Array access without braces fails\nfiles=(one two three)\necho $files[0] # Wrong: prints \"one[0]\"\n```\n\n**Correct (explicit braces):**\n\n```bash\n#!/bin/bash\nprefix=\"file\"\necho \"${prefix}_name.txt\" # Clear: $prefix + \"_name.txt\"\necho \"${prefix}1\" # Clear: $prefix + \"1\"\n\n# Array access requires braces\nfiles=(one two three)\necho \"${files[0]}\" # Correct: prints \"one\"\necho \"${files[@]}\" # All elements\necho \"${#files[@]}\" # Array length\n```\n\n**When braces are required:**\n\n```bash\n#!/bin/bash\nvar=\"value\"\n\n# Adjacent to valid identifier characters\necho \"${var}_suffix\" # Required\necho \"${var}123\" # Required\necho \"${var}text\" # Required\n\n# Array operations\necho \"${array[0]}\" # Required\necho \"${array[@]}\" # Required\necho \"${#array[@]}\" # Required\n\n# Parameter expansion operations\necho \"${var:-default}\" # Required\necho \"${var:0:5}\" # Required (substring)\necho \"${var^^}\" # Required (uppercase)\necho \"${var//old/new}\" # Required (substitution)\n```\n\n**When braces are optional but recommended:**\n\n```bash\n#!/bin/bash\n# Optional but clearer\necho \"${var}\" # Consistent style\necho \"${1}\" # Positional parameters\n\n# Can omit for simple cases\necho \"$var\" # OK if followed by space/newline\necho \"$1 $2 $3\" # OK with spaces\n\n# Special parameters (braces optional)\necho \"$?\" # Exit status\necho \"$\" # PID\necho \"$!\" # Background PID\n```\n\n**Consistency recommendation:**\n\n```bash\n#!/bin/bash\n# Google Style Guide recommends:\n# - Always use braces: \"${var}\"\n# - Exception: simple $1, $2, etc. in clear context\n\n# This is acceptable:\necho \"Processing $1\"\nfor arg in \"$@\"; do ...\n\n# But this is clearer and safer:\necho \"Processing ${1}\"\necho \"File: ${filename}\"\n```\n\nReference: [Google Shell Style Guide - Quoting](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2554,"content_sha256":"a7c19414b5aa1991964ab06aacc302b6012557d03982d5838441496f8c2e261e"},{"filename":"references/quote-command-substitution.md","content":"---\ntitle: Quote Command Substitutions\nimpact: MEDIUM-HIGH\nimpactDescription: prevents word splitting of command output\ntags: quote, command-substitution, word-splitting, output\n---\n\n## Quote Command Substitutions\n\nCommand substitution output undergoes word splitting and glob expansion like variables. A command returning \"my file.txt\" becomes two words when unquoted.\n\n**Incorrect (unquoted command substitution):**\n\n```bash\n#!/bin/bash\n# Output with spaces is split\nfile=$(find_first_file) # Returns \"my file.txt\"\nrm $file # Tries to rm \"my\" and \"file.txt\"\n\n# Glob patterns in output expand\npattern_cmd=$(get_pattern) # Returns \"*.log\"\necho $pattern_cmd # Expands to all .log files!\n\n# Nested substitution issues\nresult=$(process $(get_input)) # Inner output may split\n```\n\n**Correct (quoted command substitution):**\n\n```bash\n#!/bin/bash\n# Quote the result\nfile=$(find_first_file)\nrm \"$file\" # Correctly handles \"my file.txt\"\n\n# Preserves literal patterns\npattern_cmd=$(get_pattern)\necho \"$pattern_cmd\" # Prints \"*.log\" literally\n\n# Nested - quote inner too\nresult=$(process \"$(get_input)\")\n```\n\n**$() vs backticks:**\n\n```bash\n#!/bin/bash\n# Prefer $() over backticks for readability and nesting\n# INCORRECT (hard to read and nest)\nresult=`command \\`nested\\``\n\n# CORRECT (clear nesting)\nresult=$(command \"$(nested)\")\n\n# Complex nesting is clear with $()\nvalue=$(echo \"$(cat \"$(find_config)\")\")\n```\n\n**Common patterns:**\n\n```bash\n#!/bin/bash\n# Assign to variable (quotes on use, not assignment)\noutput=$(my_command)\necho \"$output\"\n\n# Direct use in arguments\ngrep \"pattern\" \"$(get_filename)\"\n\n# In conditionals\nif [[ \"$(get_status)\" == \"ready\" ]]; then\n proceed\nfi\n\n# Capturing exit status\noutput=$(my_command)\nstatus=$? # Get status IMMEDIATELY after\n\n# When you want splitting (rare, document it)\n# shellcheck disable=SC2046\nset -- $(get_list) # Intentionally split into positional args\n```\n\n**Handle empty output:**\n\n```bash\n#!/bin/bash\n# Check for empty result\nresult=$(find_something)\nif [[ -z \"$result\" ]]; then\n echo \"Nothing found\" >&2\n exit 1\nfi\n\n# Or use default\nresult=$(find_something)\n: \"${result:=default_value}\"\n```\n\nReference: [ShellCheck SC2046](https://www.shellcheck.net/wiki/SC2046)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2260,"content_sha256":"3afef0285c7f809ca9ec32b01a68c8313a8da64d76ef65cddab554581fb652c9"},{"filename":"references/quote-dollar-at.md","content":"---\ntitle: Use \"$@\" for Argument Passing\nimpact: MEDIUM-HIGH\nimpactDescription: preserves arguments with spaces correctly\ntags: quote, arguments, dollar-at, dollar-star\n---\n\n## Use \"$@\" for Argument Passing\n\n`$*` joins all arguments into a single string. `$@` unquoted splits on spaces. Only `\"$@\"` preserves argument boundaries, handling spaces and special characters correctly.\n\n**Incorrect (using $* or unquoted $@):**\n\n```bash\n#!/bin/bash\n# $* joins everything into one argument\nwrapper() {\n my_command $* # \"arg with space\" becomes three args\n}\nwrapper \"arg with space\" second\n\n# Unquoted $@ also splits\nwrapper() {\n my_command $@ # Same problem\n}\n\n# \"$*\" joins with IFS\nwrapper() {\n my_command \"$*\" # All args become ONE argument\n}\nwrapper one two three # my_command receives \"one two three\"\n```\n\n**Correct (use \"$@\"):**\n\n```bash\n#!/bin/bash\n# \"$@\" preserves each argument exactly\nwrapper() {\n my_command \"$@\" # Arguments passed through correctly\n}\nwrapper \"arg with space\" second # Two args: \"arg with space\", \"second\"\n\n# Iterate over arguments\nprocess_all() {\n for arg in \"$@\"; do\n echo \"Processing: $arg\"\n done\n}\nprocess_all \"file one.txt\" \"file two.txt\" # Two iterations\n```\n\n**Common patterns:**\n\n```bash\n#!/bin/bash\n# Pass all arguments to another command\nexec_wrapper() {\n exec \"$@\"\n}\n\n# Add arguments before/after\nrun_with_prefix() {\n local prefix=\"$1\"\n shift\n echo \"$prefix: $*\"\n command \"$@\"\n}\n\n# Filter arguments\nrun_verbose() {\n local verbose=false\n local -a args=()\n\n for arg in \"$@\"; do\n case \"$arg\" in\n -v|--verbose) verbose=true ;;\n *) args+=(\"$arg\") ;;\n esac\n done\n\n if [[ \"$verbose\" == true ]]; then\n set -x\n fi\n my_command \"${args[@]}\"\n}\n```\n\n**Using shift with arguments:**\n\n```bash\n#!/bin/bash\nprocess() {\n local first=\"$1\"\n shift # Remove first argument\n\n echo \"First: $first\"\n echo \"Remaining: $@\"\n\n # Pass remaining to another command\n sub_command \"$@\"\n}\n```\n\n**Difference summary:**\n\n| Syntax | Result |\n|--------|--------|\n| `$*` | All args as separate words (split on spaces) |\n| `$@` | Same as $* |\n| `\"$*\"` | All args as ONE string, joined by first char of IFS |\n| `\"$@\"` | Each arg as separate quoted string (preserves spaces) |\n\nReference: [Google Shell Style Guide - Quoting](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2321,"content_sha256":"e0732a46d822e29cf1a94d5861cd50d33bfecc9997ef8aa820f803dfe39894b2"},{"filename":"references/quote-glob-safety.md","content":"---\ntitle: Control Glob Expansion Explicitly\nimpact: MEDIUM-HIGH\nimpactDescription: prevents unintended file matching in commands\ntags: quote, glob, wildcards, expansion, nullglob\n---\n\n## Control Glob Expansion Explicitly\n\nUnquoted wildcards expand to matching files. If no files match, the literal pattern is passed (error-prone). If too many match, argument limits may be exceeded.\n\n**Incorrect (uncontrolled globbing):**\n\n```bash\n#!/bin/bash\npattern=\"*.log\"\nrm $pattern # Expands - might delete wrong files\necho $pattern # Prints filenames, not pattern\n\n# No matches: literal passed\nrm *.xyz # If no .xyz files: rm \"*.xyz\" (error)\n\n# In find, glob should NOT expand\nfind . -name *.log # Shell expands BEFORE find sees it!\n```\n\n**Correct (controlled globbing):**\n\n```bash\n#!/bin/bash\n# Quote to prevent expansion\npattern=\"*.log\"\necho \"$pattern\" # Prints \"*.log\" literally\n\n# Let find handle the pattern\nfind . -name \"*.log\" # find sees \"*.log\" pattern\nfind . -name '*.log' # Single quotes also work\n\n# Intentional glob with safety\nshopt -s nullglob # No matches = empty list\nfor file in *.log; do\n rm \"$file\" # Only runs if files exist\ndone\n```\n\n**nullglob and failglob:**\n\n```bash\n#!/bin/bash\n# Default: unmatched glob is passed literally\necho *.nonexistent # Prints \"*.nonexistent\"\n\n# nullglob: unmatched glob expands to nothing\nshopt -s nullglob\necho *.nonexistent # Prints nothing\nfor f in *.nonexistent; do\n echo \"$f\" # Loop body never runs\ndone\n\n# failglob: unmatched glob is an error\nshopt -s failglob\necho *.nonexistent # Error: no match\n\n# Restore defaults\nshopt -u nullglob failglob\n```\n\n**Safe iteration over files:**\n\n```bash\n#!/bin/bash\nshopt -s nullglob # Set once at script start\n\n# Safe - no iteration if no matches\nfor file in /path/to/logs/*.log; do\n process \"$file\"\ndone\n\n# Safe array building\nfiles=(/path/to/logs/*.log)\nif [[ ${#files[@]} -eq 0 ]]; then\n echo \"No log files found\"\nelse\n process_files \"${files[@]}\"\nfi\n```\n\n**Extended globs:**\n\n```bash\n#!/bin/bash\nshopt -s extglob nullglob\n\n# Extended patterns (bash only)\necho *.@(jpg|png|gif) # Match .jpg, .png, or .gif\necho !(*.log) # Match everything except .log\necho *.+(o|a) # One or more of .o or .a\necho file?.txt # Single character wildcard\necho file[0-9].txt # Character class\n```\n\n**Glob in case statements:**\n\n```bash\n#!/bin/bash\n# Globs work in case patterns without expansion\ncase \"$filename\" in\n *.tar.gz|*.tgz)\n tar -xzf \"$filename\"\n ;;\n *.zip)\n unzip \"$filename\"\n ;;\n *)\n echo \"Unknown format\"\n ;;\nesac\n```\n\nReference: [Greg's Wiki - Glob](https://mywiki.wooledge.org/glob)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2697,"content_sha256":"44eaed203770c4d32464cd90fe6acb1ac72e3be1a70bac1e1e40ed9ca69d8612"},{"filename":"references/quote-here-documents.md","content":"---\ntitle: Use Here Documents for Multi-line Strings\nimpact: MEDIUM-HIGH\nimpactDescription: avoids quoting complexity in long strings\ntags: quote, heredoc, multiline, strings\n---\n\n## Use Here Documents for Multi-line Strings\n\nBuilding multi-line strings with quotes and escapes is error-prone. Here documents provide clean multi-line text with clear variable expansion control.\n\n**Incorrect (escaped multi-line strings):**\n\n```bash\n#!/bin/bash\n# Messy escaping and concatenation\nmessage=\"Line 1\\n\\\nLine 2 with \\\"quotes\\\"\\n\\\nLine 3 with \\$variable\"\n\n# Hard to read SQL\nquery=\"SELECT * FROM users \\\nWHERE name = '$name' \\\nAND status = 'active' \\\nORDER BY created_at\"\n```\n\n**Correct (here documents):**\n\n```bash\n#!/bin/bash\n# Clean multi-line text (variables expand)\ncat \u003c\u003c EOF\nLine 1\nLine 2 with \"quotes\"\nLine 3 with $variable\nEOF\n\n# SQL query\nread -r -d '' query \u003c\u003c EOF\nSELECT *\nFROM users\nWHERE name = '$name'\nAND status = 'active'\nORDER BY created_at\nEOF\n\n# Assign to variable\nmessage=$(cat \u003c\u003c EOF\nHello $user,\n\nYour order #$order_id has shipped.\nTracking: $tracking_number\nEOF\n)\n```\n\n**Quoted delimiter prevents expansion:**\n\n```bash\n#!/bin/bash\n# 'EOF' or \"EOF\" prevents variable expansion\ncat \u003c\u003c 'EOF'\nThis $variable is literal\nBackslashes are literal: \\n \\t\n$(commands) are not executed\nEOF\n\n# Useful for generating scripts\ncat \u003c\u003c 'SCRIPT' > /tmp/generated.sh\n#!/bin/bash\necho \"Arguments: $@\"\necho \"PID: $\"\nSCRIPT\n```\n\n**Indented here documents:**\n\n```bash\n#!/bin/bash\n# \u003c\u003c- strips leading TABS (not spaces!)\nmain() {\n\tcat \u003c\u003c- EOF\n\t\tThis text can be indented with tabs\n\t\tThe tabs before each line are stripped\n\t\tBut the delimiter must also be indented with tabs\n\tEOF\n}\n\n# Note: Only tabs work, not spaces\n# Most editors need configuration to insert tabs\n```\n\n**Here strings for single lines (bash only):**\n\n```bash\n#!/bin/bash\n# \u003c\u003c\u003c for single-line input — NOT available in POSIX sh, dash, or ash\ngrep \"pattern\" \u003c\u003c\u003c \"$variable\"\n\n# Instead of echo | pipe\necho \"$variable\" | grep \"pattern\" # Works but spawns subshell\ngrep \"pattern\" \u003c\u003c\u003c \"$variable\" # More efficient (bash/zsh/ksh only)\n\n# Read into variable\nread -r first rest \u003c\u003c\u003c \"$line\"\n```\n\n**POSIX alternative to here strings:**\n\n```sh\n#!/bin/sh\n# Use printf | pipe in POSIX sh (dash, ash, busybox)\nprintf '%s\\n' \"$variable\" | grep \"pattern\"\n\n# Or use a here document for single-line input\ngrep \"pattern\" \u003c\u003c EOF\n$variable\nEOF\n```\n\n**Common patterns:**\n\n```bash\n#!/bin/bash\n# Generate config files\ncat \u003c\u003c EOF > /etc/myapp.conf\n[database]\nhost = $DB_HOST\nport = $DB_PORT\nname = $DB_NAME\nEOF\n\n# Multi-line usage message\nusage() {\n cat \u003c\u003c EOF\nUsage: $0 [options] \u003cfile>\n\nOptions:\n -h, --help Show this help\n -v, --verbose Verbose output\n -o FILE Output file\n\nExamples:\n $0 input.txt\n $0 -v -o output.txt input.txt\nEOF\n}\n```\n\nReference: [Bash Manual - Here Documents](https://www.gnu.org/software/bash/manual/html_node/Redirections.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2917,"content_sha256":"837a0430805b148ff594f4fa5d95995e0a73ec22a9de3983033edb837b230035"},{"filename":"references/safety-absolute-paths.md","content":"---\ntitle: Use Explicit PATH for External Commands\nimpact: CRITICAL\nimpactDescription: prevents PATH hijacking attacks in privileged scripts\ntags: safety, path, security, execution\n---\n\n## Use Explicit PATH for External Commands\n\nRelying on inherited `$PATH` for command resolution allows attackers to place malicious executables earlier in the path. Scripts running with elevated privileges are especially vulnerable.\n\n**Incorrect (relies on inherited PATH):**\n\n```bash\n#!/bin/bash\n# Attacker could create ~/bin/rm that exfiltrates data first\nrm -rf /var/cache/deploy\ncp release.tar.gz /opt/releases/\nmail -s \"Deploy Complete\" [email protected] \u003c deploy.log\n```\n\n**Correct (set a known-safe PATH at script start):**\n\n```bash\n#!/bin/bash\n# Primary defense: reset PATH to known-safe directories\nPATH=/usr/local/bin:/usr/bin:/bin\nexport PATH\n\n# Commands now resolve from safe locations only\nrm -rf /var/cache/deploy\ncp release.tar.gz /opt/releases/\n```\n\n**Alternative (verify command locations dynamically):**\n\n```bash\n#!/bin/bash\n# Useful when expected paths vary across platforms\n# (macOS vs Linux, UsrMerge systems)\nverify_command() {\n local cmd_name=\"$1\"\n local cmd_path\n cmd_path=$(command -v \"$cmd_name\") || {\n echo \"Error: $cmd_name not found in PATH\" >&2\n return 1\n }\n case \"$cmd_path\" in\n /usr/local/bin/*|/usr/bin/*|/bin/*) ;;\n *) echo \"Error: $cmd_name at untrusted location: $cmd_path\" >&2; return 1 ;;\n esac\n}\n\nverify_command rm\nverify_command cp\n```\n\n**Key practices:**\n- Set `PATH` explicitly at script start — this is the primary defense\n- Avoid hardcoding paths like `/bin/rm` — locations differ across platforms (`/bin` vs `/usr/bin` on macOS vs Linux, UsrMerge systems)\n- Use `command -v` to verify locations when platform portability is needed\n- Never trust inherited `PATH` in cron jobs or privileged scripts\n\nReference: [Apple Shell Script Security](https://developer.apple.com/library/archive/documentation/OpenSource/Conceptual/ShellScripting/ShellScriptSecurity/ShellScriptSecurity.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2031,"content_sha256":"116675c54b1bd179c0d29346476a847be9b27619515d95f557ed4935da3db29a"},{"filename":"references/safety-argument-injection.md","content":"---\ntitle: Prevent Argument Injection with Double Dash\nimpact: CRITICAL\nimpactDescription: prevents options interpreted as filenames\ntags: safety, arguments, injection, double-dash\n---\n\n## Prevent Argument Injection with Double Dash\n\nFilenames starting with `-` are interpreted as command options. User-controlled filenames can inject flags that change command behavior, enabling attacks without shell metacharacters.\n\n**Incorrect (filename interpreted as option):**\n\n```bash\n#!/bin/bash\nfilename=\"$1\"\n# User passes \"-rf\" as filename\n# rm interprets it as options, not a file\nrm $filename\n\n# User passes \"--help\" → leaks command info\ncat $filename\n\n# User passes \"-e /etc/shadow\" → reads sensitive file\ngrep \"pattern\" $filename\n```\n\n**Correct (use -- to end option parsing):**\n\n```bash\n#!/bin/bash\nfilename=\"$1\"\n\n# -- signals end of options; everything after is an operand\nrm -- \"$filename\"\ncat -- \"$filename\"\ngrep -- \"pattern\" \"$filename\"\n\n# Or use ./ prefix for current directory files\nrm \"./$filename\"\n```\n\n**Correct (for wildcards/globs):**\n\n```bash\n#!/bin/bash\n# DANGEROUS: * might expand to files starting with -\nrm *\n\n# SAFE: Explicit path prefix\nrm ./*\n\n# SAFE: Use -- before glob\nrm -- *\n```\n\n**Commands that need -- protection:**\n- `rm`, `cp`, `mv`, `cat`, `grep`, `sed`, `awk`\n- `git`, `docker`, `kubectl` (most CLI tools)\n- Any command that accepts filenames and options\n\nReference: [ShellCheck SC2035](https://www.shellcheck.net/wiki/SC2035)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1459,"content_sha256":"0e67ac77f80731ce0e15128aaa71c57a56edf907dacbc7f8601ed4f39ba3e29a"},{"filename":"references/safety-command-injection.md","content":"---\ntitle: Prevent Command Injection from User Input\nimpact: CRITICAL\nimpactDescription: prevents arbitrary code execution\ntags: safety, injection, security, input-validation\n---\n\n## Prevent Command Injection from User Input\n\nUser input passed to shell commands without validation enables arbitrary code execution. Attackers can inject shell metacharacters like `;`, `|`, `$()`, or backticks to run malicious commands.\n\n**Incorrect (direct user input in command):**\n\n```bash\n#!/bin/bash\n# User provides filename\nfilename=\"$1\"\n\n# DANGEROUS: User could pass \"; rm -rf /\" as filename\ncat $filename\ngrep \"pattern\" $filename\n```\n\n**Correct (validate and quote input):**\n\n```bash\n#!/bin/bash\nfilename=\"$1\"\n\n# Validate input against whitelist pattern\nif [[ ! \"$filename\" =~ ^[a-zA-Z0-9._-]+$ ]]; then\n echo \"Error: Invalid filename\" >&2\n exit 1\nfi\n\n# Always quote variables\ncat -- \"$filename\"\ngrep \"pattern\" -- \"$filename\"\n```\n\n**Alternative (use printf %q for dynamic commands):**\n\n```bash\n#!/bin/bash\n# When you must build commands dynamically\nuser_arg=\"$1\"\nsafe_arg=$(printf '%q' \"$user_arg\")\n\n# Still prefer arrays over eval\ndeclare -a cmd=(grep -r \"$user_arg\" .)\n\"${cmd[@]}\"\n```\n\n**Key protections:**\n- Validate input with whitelist regex\n- Always quote variables: `\"$var\"` not `$var`\n- Use `--` to end option parsing\n- Prefer arrays over string concatenation for commands\n- Never use `eval` with user data\n\nReference: [Apple Shell Script Security](https://developer.apple.com/library/archive/documentation/OpenSource/Conceptual/ShellScripting/ShellScriptSecurity/ShellScriptSecurity.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1590,"content_sha256":"da88dfeabb8f0ef17977f689059add42e69e129e2a5ecdcc6126e4eb283e239f"},{"filename":"references/safety-eval-avoidance.md","content":"---\ntitle: Avoid eval for Dynamic Commands\nimpact: CRITICAL\nimpactDescription: eliminates code injection vector\ntags: safety, eval, injection, security\n---\n\n## Avoid eval for Dynamic Commands\n\n`eval` interprets its arguments as shell code, making it extremely dangerous with any variable data. Even carefully escaped data can be exploited through edge cases.\n\n**Incorrect (using eval):**\n\n```bash\n#!/bin/bash\n# Building command dynamically with eval\ncmd=\"ls\"\nopts=\"-la\"\ndir=\"$1\"\n\neval \"$cmd $opts $dir\"\n# User passes: \"; rm -rf /\" → executes destructive command\n```\n\n**Correct (use arrays for command building):**\n\n```bash\n#!/bin/bash\n# Build commands safely with arrays\ndeclare -a cmd=(ls -la)\ndir=\"$1\"\n\n# Add arguments safely\ncmd+=(\"$dir\")\n\n# Execute without shell interpretation\n\"${cmd[@]}\"\n```\n\n**Alternative (indirect variable expansion):**\n\n```bash\n#!/bin/bash\n# When you need variable indirection\nvar_name=\"PATH\"\n\n# Instead of: eval \"echo \\$var_name\"\n# Use bash indirect expansion:\necho \"${!var_name}\"\n\n# For associative data, use associative arrays:\ndeclare -A config\nconfig[database]=\"mydb\"\nconfig[host]=\"localhost\"\n\nkey=\"database\"\necho \"${config[$key]}\"\n```\n\n**When eval seems necessary, alternatives exist:**\n- Command building → arrays with `\"${array[@]}\"`\n- Variable indirection → `${!var}` or `declare -n`\n- Dynamic assignment → `declare \"$name=$value\"`\n- Arithmetic → `$(( expression ))`\n\nReference: [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1506,"content_sha256":"4009c0ede18501a3c003652e0a0ec9fa239f7683afb589c808a8469abd4384b2"},{"filename":"references/safety-suid-forbidden.md","content":"---\ntitle: Never Use SUID/SGID on Shell Scripts\nimpact: CRITICAL\nimpactDescription: prevents privilege escalation vulnerabilities\ntags: safety, suid, sgid, privileges, security\n---\n\n## Never Use SUID/SGID on Shell Scripts\n\nShell scripts cannot be made secure with SUID/SGID due to race conditions between the kernel reading the shebang and the interpreter opening the file. Many systems ignore SUID on scripts entirely.\n\n**Incorrect (SUID shell script):**\n\n```bash\n#!/bin/bash\n# File: /usr/local/bin/admin-task\n# Permissions: -rwsr-xr-x (SUID set)\n# DANGEROUS: Multiple attack vectors exist\n\n# Race condition: attacker can replace script between\n# kernel reading shebang and bash opening file\nrm -rf /var/cache/app/*\n```\n\n**Correct (use sudo with specific permissions):**\n\n```bash\n#!/bin/bash\n# File: /usr/local/bin/admin-task\n# Permissions: -rwxr-xr-x (no SUID)\n\n# Check if running with required privileges\nif [[ $EUID -ne 0 ]]; then\n echo \"This script must be run with sudo\" >&2\n exit 1\nfi\n\nrm -rf /var/cache/app/*\n```\n\n**sudoers configuration:**\n\n```bash\n# /etc/sudoers.d/admin-task\n# Allow specific users to run specific script\nappuser ALL=(root) NOPASSWD: /usr/local/bin/admin-task\n```\n\n**Alternative (compiled wrapper):**\n\n```c\n/* For complex cases, use a compiled SUID wrapper */\n/* that validates arguments before exec'ing script */\n#include \u003cunistd.h>\nint main(int argc, char *argv[]) {\n /* Validate environment, clear dangerous vars */\n clearenv();\n setenv(\"PATH\", \"/usr/bin:/bin\", 1);\n execl(\"/usr/local/lib/admin-task.sh\", \"admin-task\", NULL);\n return 1;\n}\n```\n\nReference: [Google Shell Style Guide - SUID/SGID](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1694,"content_sha256":"4db0b66d44158c19caaef8cd5e8ac8bba0f951bcea38168c1c1e81278fb76f00"},{"filename":"references/safety-temp-files.md","content":"---\ntitle: Create Secure Temporary Files\nimpact: CRITICAL\nimpactDescription: prevents symlink attacks and race conditions\ntags: safety, temporary-files, security, mktemp\n---\n\n## Create Secure Temporary Files\n\nPredictable temporary file names enable symlink attacks where attackers create links to sensitive files. Race conditions between checking and creating files can be exploited.\n\n**Incorrect (predictable temp file):**\n\n```bash\n#!/bin/bash\n# DANGEROUS: Predictable name, race condition\ntmpfile=\"/tmp/myapp.$\"\n\n# Attacker creates: ln -s /etc/passwd /tmp/myapp.1234\n# Before this runs, overwriting /etc/passwd\necho \"data\" > \"$tmpfile\"\n```\n\n**Correct (use mktemp):**\n\n```bash\n#!/bin/bash\n# mktemp creates file with secure permissions atomically\ntmpfile=$(mktemp) || exit 1\ntmpdir=$(mktemp -d) || exit 1\n\n# Use trap to clean up on exit\ntrap 'rm -rf \"$tmpfile\" \"$tmpdir\"' EXIT\n\necho \"data\" > \"$tmpfile\"\n```\n\n**Alternative (template with mktemp):**\n\n```bash\n#!/bin/bash\n# Use template for readable names (X's are replaced)\ntmpfile=$(mktemp /tmp/myapp.XXXXXX) || exit 1\ntmpdir=$(mktemp -d /tmp/myapp.XXXXXX) || exit 1\n\ntrap 'rm -rf \"$tmpfile\" \"$tmpdir\"' EXIT\n\n# Secure: mktemp uses O_EXCL for atomic creation\n# with mode 0600 (owner read/write only)\n```\n\n**Never do:**\n- Use `$` (PID) alone for temp names\n- Create files in `/tmp` without `mktemp`\n- Check existence then create (TOCTOU race)\n- Forget cleanup on script exit\n\nReference: [CWE-377: Insecure Temporary File](https://cwe.mitre.org/data/definitions/377.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1520,"content_sha256":"44a7079fafdf1bccda99dc9172c6d8b2c1ced5a32918edd56cb022cf2b5ad5a2"},{"filename":"references/style-comments.md","content":"---\ntitle: Write Useful Comments\nimpact: LOW\nimpactDescription: reduces code review clarification questions by 30-50%\ntags: style, comments, documentation\n---\n\n## Write Useful Comments\n\nComments that repeat the code add noise. Good comments explain why something is done, document non-obvious behavior, and mark incomplete work.\n\n**Incorrect (useless comments):**\n\n```bash\n#!/bin/bash\n# Increment counter\n((counter++))\n\n# Check if file exists\nif [[ -f \"$file\" ]]; then\n # Read the file\n content=$(\u003c\"$file\")\nfi\n\n# Loop through items\nfor item in \"${items[@]}\"; do\n # Process item\n process \"$item\"\ndone\n```\n\n**Correct (useful comments):**\n\n```bash\n#!/bin/bash\n# Retry count starts at 1 because the initial attempt isn't a \"retry\"\n((counter++))\n\n# Legacy systems create zero-byte marker files; treat as non-existent\nif [[ -f \"$file\" && -s \"$file\" ]]; then\n content=$(\u003c\"$file\")\nfi\n\n# Process in reverse order to handle dependencies correctly\n# (items may reference later items in the array)\nfor ((i = ${#items[@]} - 1; i >= 0; i--)); do\n process \"${items[i]}\"\ndone\n```\n\n**Comment types:**\n\n```bash\n#!/bin/bash\n\n# TODO(username): Implement retry logic for network failures (issue #123)\n# FIXME: This workaround breaks on filenames with newlines\n# HACK: Temporary fix until upstream patches the library\n# NOTE: This assumes UTC timezone; local time will break calculations\n\n# Explain non-obvious code\n# The seemingly redundant `|| true` prevents errexit from triggering\n# on expected \"file not found\" errors from grep\ngrep \"pattern\" file.txt || true\n\n# Document unexplained constants\nreadonly MAX_CONNECTIONS=100 # Limit from database license\nreadonly TIMEOUT_MS=30000 # Match nginx upstream timeout\n\n# Explain regex patterns\n# Pattern matches: [email protected] (basic email validation)\nif [[ \"$email\" =~ ^[^@]+@[^@]+\\.[^@]+$ ]]; then\n valid=true\nfi\n```\n\n**When to comment:**\n\n```bash\n#!/bin/bash\n# DO comment:\n# - Why a non-obvious approach was chosen\n# - Workarounds for bugs or limitations\n# - Performance considerations\n# - Security implications\n# - External dependencies or assumptions\n# - Complex regex or parameter expansion\n\n# DON'T comment:\n# - What the code literally does (read the code)\n# - Every function or variable\n# - Obvious operations\n```\n\n**Inline vs block comments:**\n\n```bash\n#!/bin/bash\n# Block comment for multi-line explanation\n# This function implements exponential backoff because the API\n# rate-limits aggressive callers. The jitter prevents thundering\n# herd problems when multiple instances retry simultaneously.\nretry_with_backoff() {\n # ...\n}\n\n# Inline comment for single clarification\nreadonly BATCH_SIZE=1000 # Matches API page size limit\n```\n\n**Disabled code:**\n\n```bash\n#!/bin/bash\n# Don't leave commented-out code without explanation\n# BAD:\n# old_function \"$arg\"\n# new_function \"$arg\"\n\n# GOOD: Remove old code entirely, use version control\nnew_function \"$arg\"\n\n# Or if needed, explain why it's kept:\n# Disabled pending migration to new API (tracking: PROJECT-456)\n# old_function \"$arg\"\n```\n\nReference: [Google Shell Style Guide - Comments](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3135,"content_sha256":"932ccb2191ec8e6d9f6a9767e357ecb43342a9640030b1a2c90a4078449b382a"},{"filename":"references/style-file-structure.md","content":"---\ntitle: Follow Consistent File Structure\nimpact: LOW\nimpactDescription: enables quick navigation and maintenance\ntags: style, structure, organization, layout\n---\n\n## Follow Consistent File Structure\n\nScripts without consistent structure are hard to navigate. Following a standard layout helps readers find what they need quickly.\n\n**Incorrect (unstructured):**\n\n```bash\n#!/bin/bash\nprocess() { ... }\nreadonly VAR=1\nset -e\nanother_func() { ... }\n# Random comment\nCONFIG=/etc/app\nmain() { ... }\nsource ./lib.sh\nmain \"$@\"\n```\n\n**Correct (structured layout):**\n\n```bash\n#!/bin/bash\n# deploy.sh — Deploy application to staging environment\n# Usage: deploy.sh [-v] [-e environment] \u003cversion>\n\nset -euo pipefail\n\n# 1. Constants\nreadonly SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nreadonly DEPLOY_CONFIG=\"/etc/myapp/deploy.conf\"\n\n# 2. Configurable defaults (override via environment)\n: \"${LOG_LEVEL:=info}\"\n: \"${DRY_RUN:=false}\"\n\n# 3. Source dependencies\nsource \"${SCRIPT_DIR}/lib/logging.sh\"\n\n# 4. Global variables (mutable state)\nVERBOSE=false\nTARGET_ENV=\"staging\"\n\n# 5. Functions\ncleanup() { :; }\nparse_args() { :; }\ndeploy_release() { :; }\n\n# 6. Main entry point\nmain() {\n trap cleanup EXIT\n parse_args \"$@\"\n deploy_release\n}\n\n[[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]] && main \"$@\"\n```\n\n**Section order:**\n\n1. Shebang and file header comment\n2. `set` options (strict mode)\n3. Constants (`readonly`)\n4. Configurable defaults\n5. Source dependencies\n6. Global variables\n7. Function definitions\n8. Main function\n9. Main invocation guard\n\n**File naming:**\n\n```bash\n# Executables\nmy-script # No extension, executable\nmy-script.sh # With extension (if build system renames)\n\n# Libraries (sourced, not executed)\nlib/common.sh # Always .sh extension\nlib/logging.sh # Not executable\n```\n\nReference: [Google Shell Style Guide - File Organization](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1918,"content_sha256":"5cefcd9e42d91ec731138c62663253ae999e786c181023a6a50a24c00ffc9ae3"},{"filename":"references/style-indentation.md","content":"---\ntitle: Use Consistent Indentation\nimpact: LOW\nimpactDescription: reduces misreading of nested control flow by 2-3× in complex scripts\ntags: style, indentation, formatting, whitespace\n---\n\n## Use Consistent Indentation\n\nInconsistent indentation makes control flow hard to follow. Use 2 spaces (Google style) consistently throughout the script.\n\n**Incorrect (inconsistent indentation):**\n\n```bash\n#!/bin/bash\nif [[ -f \"$file\" ]]; then\necho \"exists\"\n if [[ -r \"$file\" ]]; then\n echo \"readable\"\n\t# Mixed tabs and spaces\n fi\nfi\n\nfor item in \"${items[@]}\"; do\nprocess \"$item\"\ndone\n```\n\n**Correct (consistent 2-space indentation):**\n\n```bash\n#!/bin/bash\nif [[ -f \"$file\" ]]; then\n echo \"exists\"\n if [[ -r \"$file\" ]]; then\n echo \"readable\"\n fi\nfi\n\nfor item in \"${items[@]}\"; do\n process \"$item\"\ndone\n```\n\n**Formatting guidelines:**\n\n```bash\n#!/bin/bash\n# Maximum line length: 80 characters\n# Indent: 2 spaces (no tabs except in heredocs)\n\n# If/then on same line\nif [[ condition ]]; then\n commands\nfi\n\n# For/do on same line\nfor item in list; do\n commands\ndone\n\n# While/do on same line\nwhile [[ condition ]]; do\n commands\ndone\n\n# Case indentation\ncase \"$var\" in\n pattern1)\n commands\n ;;\n pattern2)\n commands\n ;;\nesac\n```\n\n**Long lines:**\n\n```bash\n#!/bin/bash\n# Break long commands with backslash\ncommand \\\n --long-option=\"value\" \\\n --another-option=\"another value\" \\\n file.txt\n\n# Or use arrays for complex commands\ndeclare -a args=(\n --long-option=\"value\"\n --another-option=\"another value\"\n)\ncommand \"${args[@]}\" file.txt\n\n# Pipelines - one per line\ncommand1 \\\n | command2 \\\n | command3 \\\n | command4\n```\n\n**Function formatting:**\n\n```bash\n#!/bin/bash\n# Opening brace on same line as function name\nmy_function() {\n local var=\"$1\"\n\n if [[ -n \"$var\" ]]; then\n process \"$var\"\n fi\n}\n\n# Not this:\nmy_function()\n{\n # ...\n}\n```\n\n**Heredoc indentation:**\n\n```bash\n#!/bin/bash\n# Only tabs work with \u003c\u003c- (not spaces)\nmy_function() {\n\tcat \u003c\u003c- EOF\n\t\tThis text is indented with tabs.\n\t\tThe tabs are stripped from output.\n\tEOF\n}\n\n# Or don't indent heredoc content\nmy_function() {\n cat \u003c\u003c EOF\nThis text is not indented.\nOutput appears at column 0.\nEOF\n}\n```\n\nReference: [Google Shell Style Guide - Formatting](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2295,"content_sha256":"2145c98f4139e4d0dc9a37630f525d6142a236e2be423c8ec6c6956295dd32b3"},{"filename":"references/test-arithmetic.md","content":"---\ntitle: Use (( )) for Arithmetic Comparisons\nimpact: MEDIUM\nimpactDescription: provides clearer syntax and prevents string comparison bugs\ntags: test, arithmetic, numeric, comparison\n---\n\n## Use (( )) for Arithmetic Comparisons\n\nUsing `[ ]` or `[[ ]]` with `-eq`, `-lt` for numbers is error-prone. `(( ))` provides natural math syntax and fails clearly on non-numeric input.\n\n**Incorrect (string-based numeric comparison):**\n\n```bash\n#!/bin/bash\ncount=\"10\"\n\n# Confusing operators\nif [ \"$count\" -gt 5 ]; then\n echo \"greater\"\nfi\n\n# String comparison mistake\nif [[ \"$count\" > \"5\" ]]; then # String comparison! \"10\" \u003c \"5\"\n echo \"greater\" # Not printed! \"10\" sorts before \"5\" alphabetically\nfi\n\n# Using let (deprecated)\nlet \"count = count + 1\"\n\n# Using expr (external command, slow)\ncount=$(expr $count + 1)\n\n# Using $[ ] (deprecated)\ncount=$[ count + 1 ]\n```\n\n**Correct (arithmetic context):**\n\n```bash\n#!/bin/bash\ncount=10\n\n# Natural comparison syntax\nif (( count > 5 )); then\n echo \"greater\"\nfi\n\n# Arithmetic assignment\n(( count++ ))\n(( count += 5 ))\n(( count = count * 2 ))\n\n# Arithmetic in expressions\nresult=$(( count + 5 ))\nresult=$(( (count + 5) * 2 ))\n\n# Multiple conditions\nif (( count > 5 && count \u003c 20 )); then\n echo \"in range\"\nfi\n```\n\n**Arithmetic operators:**\n\n```bash\n#!/bin/bash\na=10\nb=3\n\n# Arithmetic expression operators\n(( sum = a + b )) # Addition: 13\n(( diff = a - b )) # Subtraction: 7\n(( prod = a * b )) # Multiplication: 30\n(( quot = a / b )) # Division (integer): 3\n(( rem = a % b )) # Modulo: 1\n(( pow = a ** 2 )) # Exponentiation: 100\n\n# Comparison operators (return 0=true, 1=false)\n(( a == b )) # Equal\n(( a != b )) # Not equal\n(( a > b )) # Greater than\n(( a >= b )) # Greater or equal\n(( a \u003c b )) # Less than\n(( a \u003c= b )) # Less or equal\n\n# Increment/decrement\n(( a++ )) # Post-increment\n(( ++a )) # Pre-increment\n(( a-- )) # Post-decrement\n(( a += 5 )) # Add and assign\n(( a *= 2 )) # Multiply and assign\n\n# Ternary operator\n(( max = a > b ? a : b ))\n```\n\n**Combining with conditionals:**\n\n```bash\n#!/bin/bash\n# (( )) returns exit status 0 if non-zero, 1 if zero\ncount=0\n\nif (( count )); then\n echo \"count is non-zero\"\nelse\n echo \"count is zero\"\nfi\n\n# Use in while loops\nwhile (( count \u003c 10 )); do\n echo \"$count\"\n (( count++ ))\ndone\n\n# C-style for loop\nfor (( i = 0; i \u003c 10; i++ )); do\n echo \"$i\"\ndone\n```\n\n**Note: Variables don't need $ inside (( )):**\n\n```bash\n#!/bin/bash\nx=5\ny=10\n\n# $ is optional inside (( ))\n(( z = x + y )) # Works\n(( z = $x + $y )) # Also works, but unnecessary\n\n# But needed for special variables\n(( z = ${array[0]} )) # Array access needs ${}\n```\n\nReference: [Bash Manual - Arithmetic Evaluation](https://www.gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2939,"content_sha256":"44cb9cfeee822900a6f59903aa8ee8c12989dbb030839e8d4a7e4abc6ee6648a"},{"filename":"references/test-case-patterns.md","content":"---\ntitle: Use case for Pattern Matching\nimpact: MEDIUM\nimpactDescription: 40-60% fewer lines than if/elif chains for 3+ patterns, with built-in glob matching\ntags: test, case, patterns, conditionals\n---\n\n## Use case for Pattern Matching\n\nMultiple if/elif chains for pattern matching are verbose and error-prone. `case` is cleaner, supports glob patterns natively, and is POSIX-compliant.\n\n**Incorrect (if/elif chains):**\n\n```bash\n#!/bin/bash\n# Verbose and repetitive\nif [[ \"$1\" == \"-h\" || \"$1\" == \"--help\" ]]; then\n show_help\nelif [[ \"$1\" == \"-v\" || \"$1\" == \"--version\" ]]; then\n show_version\nelif [[ \"$1\" == \"-q\" || \"$1\" == \"--quiet\" ]]; then\n quiet=true\nelif [[ \"$1\" == \"-\"* ]]; then\n echo \"Unknown option: $1\"\n exit 1\nfi\n\n# File type detection\nif [[ \"$file\" == *.tar.gz || \"$file\" == *.tgz ]]; then\n tar -xzf \"$file\"\nelif [[ \"$file\" == *.tar.bz2 || \"$file\" == *.tbz2 ]]; then\n tar -xjf \"$file\"\nelif [[ \"$file\" == *.zip ]]; then\n unzip \"$file\"\nfi\n```\n\n**Correct (case statement):**\n\n```bash\n#!/bin/bash\n# Clean pattern matching\ncase \"$1\" in\n -h|--help)\n show_help\n ;;\n -v|--version)\n show_version\n ;;\n -q|--quiet)\n quiet=true\n ;;\n -*)\n echo \"Unknown option: $1\" >&2\n exit 1\n ;;\nesac\n\n# File type detection\ncase \"$file\" in\n *.tar.gz|*.tgz)\n tar -xzf \"$file\"\n ;;\n *.tar.bz2|*.tbz2)\n tar -xjf \"$file\"\n ;;\n *.tar.xz|*.txz)\n tar -xJf \"$file\"\n ;;\n *.zip)\n unzip \"$file\"\n ;;\n *)\n echo \"Unknown format: $file\" >&2\n return 1\n ;;\nesac\n```\n\n**Case pattern features:**\n\n```bash\n#!/bin/bash\ninput=\"$1\"\n\ncase \"$input\" in\n # Exact match\n start|stop|restart)\n handle_command \"$input\"\n ;;\n\n # Glob patterns\n *.txt)\n echo \"Text file\"\n ;;\n\n # Character classes\n [0-9]*)\n echo \"Starts with digit\"\n ;;\n\n [a-zA-Z]*)\n echo \"Starts with letter\"\n ;;\n\n # Negation (bash extended)\n !(*.bak|*.tmp))\n echo \"Not a backup or temp file\"\n ;;\n\n # Default case (always put last)\n *)\n echo \"No match\"\n ;;\nesac\n```\n\n**Fall-through with ;&:**\n\n```bash\n#!/bin/bash\n# Bash 4+ feature: fall-through\nlevel=\"$1\"\n\ncase \"$level\" in\n debug)\n enable_debug=true\n ;& # Fall through\n verbose)\n enable_verbose=true\n ;& # Fall through\n normal)\n enable_logging=true\n ;;\nesac\n\n# Continue matching with ;;&\ncase \"$option\" in\n --all)\n all=true\n ;;& # Continue checking\n --verbose|--all)\n verbose=true\n ;;& # Continue checking\n --debug|--all)\n debug=true\n ;;\nesac\n```\n\n**Option parsing with case:**\n\n```bash\n#!/bin/bash\nparse_args() {\n while [[ $# -gt 0 ]]; do\n case \"$1\" in\n -h|--help)\n usage\n exit 0\n ;;\n -v|--verbose)\n VERBOSE=true\n shift\n ;;\n -o|--output)\n OUTPUT=\"$2\"\n shift 2\n ;;\n --)\n shift\n break\n ;;\n -*)\n echo \"Unknown option: $1\" >&2\n exit 1\n ;;\n *)\n break\n ;;\n esac\n done\n ARGS=(\"$@\")\n}\n```\n\nReference: [Bash Manual - Case Statement](https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3100,"content_sha256":"53035e7ee21c5f11814f21cfe88ca46fa72f4034abd6d308d982c8336255f2b3"},{"filename":"references/test-double-brackets.md","content":"---\ntitle: Use [[ ]] for Tests in Bash\nimpact: MEDIUM\nimpactDescription: prevents word splitting and enables regex\ntags: test, conditionals, double-brackets, bash\n---\n\n## Use [[ ]] for Tests in Bash\n\nIn bash scripts, `[[ ]]` is safer than `[ ]`: no word splitting on variables, supports `&&`/`||` inside, regex matching, and better error messages.\n\n**Incorrect (single brackets with issues):**\n\n```bash\n#!/bin/bash\nfile=\"my file.txt\"\n\n# Word splitting breaks this\nif [ -f $file ]; then # Error: too many arguments\n echo \"exists\"\nfi\n\n# Empty variable causes error\nif [ $unset == \"value\" ]; then # Error: unary operator expected\n echo \"match\"\nfi\n\n# Can't use && inside [ ]\nif [ -f \"$file\" && -r \"$file\" ]; then # Syntax error\n echo \"readable\"\nfi\n```\n\n**Correct (double brackets):**\n\n```bash\n#!/bin/bash\nfile=\"my file.txt\"\n\n# No word splitting inside [[ ]]\nif [[ -f $file ]]; then # Works! (but still quote for clarity)\n echo \"exists\"\nfi\n\n# Empty/unset variables are safe\nif [[ $unset == \"value\" ]]; then # Works, evaluates to false\n echo \"match\"\nfi\n\n# Logical operators inside [[ ]]\nif [[ -f \"$file\" && -r \"$file\" ]]; then # Works!\n echo \"readable\"\nfi\n\n# Regex matching\nif [[ \"$email\" =~ ^[^@]+@[^@]+\\.[^@]+$ ]]; then\n echo \"Valid email format\"\nfi\n\n# Pattern matching (without quotes on right side)\nif [[ \"$file\" == *.txt ]]; then\n echo \"Text file\"\nfi\n```\n\n**Single bracket use cases:**\n\n```bash\n#!/bin/bash\n# Use [ ] only when:\n\n# 1. POSIX compliance required (#!/bin/sh)\n#!/bin/sh\nif [ -f \"$file\" ]; then\n echo \"exists\"\nfi\n\n# 2. Combining with -a and -o (though avoid these)\n# Prefer: [ cond1 ] && [ cond2 ]\n# Over: [ cond1 -a cond2 ]\n```\n\n**Pattern vs literal matching:**\n\n```bash\n#!/bin/bash\nvar=\"hello.txt\"\n\n# Pattern matching (unquoted right side)\n[[ \"$var\" == *.txt ]] # True - pattern match\n[[ \"$var\" == \"*.txt\" ]] # False - literal match\n\n# Regex matching\n[[ \"$var\" =~ \\.txt$ ]] # True - regex match\n\n# Common patterns\n[[ \"$str\" == *substring* ]] # Contains\n[[ \"$str\" == prefix* ]] # Starts with\n[[ \"$str\" == *suffix ]] # Ends with\n```\n\n**Test operators reference:**\n\n```bash\n#!/bin/bash\n# String tests\n[[ -z \"$var\" ]] # Empty string\n[[ -n \"$var\" ]] # Non-empty string\n[[ \"$a\" == \"$b\" ]] # String equality\n[[ \"$a\" != \"$b\" ]] # String inequality\n[[ \"$a\" \u003c \"$b\" ]] # String comparison (alphabetical)\n\n# File tests\n[[ -f \"$file\" ]] # Regular file exists\n[[ -d \"$dir\" ]] # Directory exists\n[[ -e \"$path\" ]] # Exists (any type)\n[[ -r \"$file\" ]] # Readable\n[[ -w \"$file\" ]] # Writable\n[[ -x \"$file\" ]] # Executable\n[[ -s \"$file\" ]] # Non-empty file\n[[ \"$f1\" -nt \"$f2\" ]] # f1 newer than f2\n\n# Numeric comparison (use (( )) instead for clarity)\n[[ \"$a\" -eq \"$b\" ]] # Equal\n[[ \"$a\" -lt \"$b\" ]] # Less than\n```\n\nReference: [Google Shell Style Guide - Test](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2965,"content_sha256":"601d7f7dc07e7d4f818a72bae149268c8519b23465b982c7dc325cf9b304e557"},{"filename":"references/test-explicit-empty.md","content":"---\ntitle: Use Explicit Empty/Non-empty String Tests\nimpact: MEDIUM\nimpactDescription: prevents misreads and silent failures\ntags: test, strings, empty, explicit\n---\n\n## Use Explicit Empty/Non-empty String Tests\n\nImplicit string tests like `[[ \"$var\" ]]` are ambiguous. Explicit `-z` (empty) and `-n` (non-empty) make intent clear and prevent bugs with special values.\n\n**Incorrect (implicit tests):**\n\n```bash\n#!/bin/bash\n# What does this test? Existence? Non-empty? Boolean?\nif [[ \"$var\" ]]; then\n echo \"true\" # When exactly?\nfi\n\n# Empty vs unset confusion\nif [[ $var ]]; then # False for both unset and empty\n echo \"has value\"\nfi\n\n# String \"false\" is truthy!\nflag=\"false\"\nif [[ \"$flag\" ]]; then\n echo \"truthy\" # Prints! \"false\" is a non-empty string\nfi\n```\n\n**Correct (explicit tests):**\n\n```bash\n#!/bin/bash\n# Explicit empty check\nif [[ -z \"$var\" ]]; then\n echo \"var is empty or unset\"\nfi\n\n# Explicit non-empty check\nif [[ -n \"$var\" ]]; then\n echo \"var has a value\"\nfi\n\n# Boolean values - compare explicitly\nflag=\"false\"\nif [[ \"$flag\" == \"true\" ]]; then\n echo \"flag is true\"\nfi\n\n# Or use true/false commands\nis_enabled=true\nif \"$is_enabled\"; then # Runs the command 'true'\n echo \"enabled\"\nfi\n```\n\n**Common patterns:**\n\n```bash\n#!/bin/bash\n# Check before using\ninput=\"$1\"\nif [[ -z \"$input\" ]]; then\n echo \"Error: Input required\" >&2\n exit 1\nfi\n\n# Default if empty\nconfig=\"${CONFIG:-}\"\nif [[ -z \"$config\" ]]; then\n config=\"/etc/default.conf\"\nfi\n\n# Or use parameter expansion\nconfig=\"${CONFIG:-/etc/default.conf}\"\n```\n\n**Testing for set vs unset:**\n\n```bash\n#!/bin/bash\n# -z doesn't distinguish unset from empty\nunset var1\nvar2=\"\"\n\n[[ -z \"$var1\" ]] # True (unset)\n[[ -z \"$var2\" ]] # True (empty)\n\n# To distinguish, use parameter expansion\nif [[ -z \"${var+x}\" ]]; then\n echo \"var is unset\"\nfi\n\nif [[ -z \"${var-}\" ]]; then\n echo \"var is unset or empty\"\nfi\n\n# Or with set -u active:\nif [[ \"${var:-}\" == \"\" ]]; then\n echo \"var is unset or empty (safe with set -u)\"\nfi\n```\n\n**Boolean patterns:**\n\n```bash\n#!/bin/bash\n# Pattern 1: String comparison\nverbose=\"true\"\nif [[ \"$verbose\" == \"true\" ]]; then\n set -x\nfi\n\n# Pattern 2: Command-based (true/false are builtins)\nenabled=true # No quotes - this is the command name\nif $enabled; then\n echo \"Enabled\"\nfi\n\n# Pattern 3: Integer (0=false, non-zero=true)\ndebug=1\nif (( debug )); then\n echo \"Debug mode\"\nfi\n```\n\n**Avoid double-negative:**\n\n```bash\n#!/bin/bash\n# Hard to read\nif [[ ! -z \"$var\" ]]; then\n echo \"not empty\" # Double negative\nfi\n\n# Clear\nif [[ -n \"$var\" ]]; then\n echo \"has value\"\nfi\n```\n\nReference: [Google Shell Style Guide - Testing Strings](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2680,"content_sha256":"acc9a6f458d3fcf7d116bf262c1b7b5ce1c67339bcc7e834a7b81fa172d5cbb4"},{"filename":"references/test-file-operators.md","content":"---\ntitle: Use Correct File Test Operators\nimpact: MEDIUM\nimpactDescription: prevents logic errors with symlinks and special files\ntags: test, files, operators, symlinks\n---\n\n## Use Correct File Test Operators\n\nDifferent file tests check different things. Using `-e` when you need `-f` accepts directories. Not checking `-L` misses symlink-specific issues.\n\n**Incorrect (wrong operator):**\n\n```bash\n#!/bin/bash\n# -e accepts directories, devices, symlinks\nif [[ -e \"$path\" ]]; then\n cat \"$path\" # Fails if it's a directory!\nfi\n\n# Doesn't handle broken symlinks\nif [[ -f \"$link\" ]]; then\n # False for broken symlinks (target doesn't exist)\n process \"$link\"\nfi\n\n# Checking read without checking existence\nif [[ -r \"$file\" ]]; then\n cat \"$file\" # -r is false for non-existent, but error message unclear\nfi\n```\n\n**Correct (appropriate operators):**\n\n```bash\n#!/bin/bash\n# Check specific file type\nif [[ -f \"$path\" ]]; then\n # Regular file only\n cat \"$path\"\nfi\n\nif [[ -d \"$path\" ]]; then\n # Directory only\n ls \"$path\"\nfi\n\n# Chain checks for clarity\nif [[ -f \"$file\" && -r \"$file\" ]]; then\n cat \"$file\"\nfi\n\n# Handle symlinks explicitly\nif [[ -L \"$path\" ]]; then\n echo \"It's a symbolic link\"\n if [[ -e \"$path\" ]]; then\n echo \"Link target exists\"\n else\n echo \"Broken symlink!\"\n fi\nfi\n```\n\n**File test operators reference:**\n\n```bash\n#!/bin/bash\n# Existence tests\n[[ -e \"$f\" ]] # Exists (any type)\n[[ -f \"$f\" ]] # Regular file\n[[ -d \"$f\" ]] # Directory\n[[ -L \"$f\" ]] # Symbolic link (doesn't follow)\n[[ -h \"$f\" ]] # Symbolic link (same as -L)\n[[ -p \"$f\" ]] # Named pipe (FIFO)\n[[ -S \"$f\" ]] # Socket\n[[ -b \"$f\" ]] # Block device\n[[ -c \"$f\" ]] # Character device\n\n# Permission tests\n[[ -r \"$f\" ]] # Readable\n[[ -w \"$f\" ]] # Writable\n[[ -x \"$f\" ]] # Executable\n[[ -u \"$f\" ]] # SUID bit set\n[[ -g \"$f\" ]] # SGID bit set\n[[ -k \"$f\" ]] # Sticky bit set\n\n# Size tests\n[[ -s \"$f\" ]] # Non-empty file (size > 0)\n\n# Comparison tests\n[[ \"$f1\" -nt \"$f2\" ]] # f1 newer than f2\n[[ \"$f1\" -ot \"$f2\" ]] # f1 older than f2\n[[ \"$f1\" -ef \"$f2\" ]] # Same file (hard link or same inode)\n\n# Special tests\n[[ -t 0 ]] # stdin is terminal\n[[ -t 1 ]] # stdout is terminal\n```\n\n**Common patterns:**\n\n```bash\n#!/bin/bash\n# Safe file processing\nprocess_file() {\n local file=\"$1\"\n\n if [[ ! -e \"$file\" ]]; then\n echo \"Error: File does not exist: $file\" >&2\n return 1\n fi\n\n if [[ -d \"$file\" ]]; then\n echo \"Error: Expected file, got directory: $file\" >&2\n return 1\n fi\n\n if [[ ! -r \"$file\" ]]; then\n echo \"Error: Cannot read file: $file\" >&2\n return 1\n fi\n\n cat \"$file\"\n}\n\n# Directory creation with check\nensure_dir() {\n local dir=\"$1\"\n if [[ ! -d \"$dir\" ]]; then\n mkdir -p \"$dir\" || return 1\n fi\n}\n\n# Detect terminal vs pipe\nif [[ -t 1 ]]; then\n # stdout is terminal - use colors\n echo -e \"\\033[32mSuccess\\033[0m\"\nelse\n # stdout is piped - plain text\n echo \"Success\"\nfi\n```\n\nReference: [Bash Manual - Conditional Expressions](https://www.gnu.org/software/bash/manual/html_node/Bash-Conditional-Expressions.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3094,"content_sha256":"8c7708d0e8c2a346454d7e63047ba6682eb115f1bb693849da5f6a2d6358a3e7"},{"filename":"references/var-default-values.md","content":"---\ntitle: Use Parameter Expansion for Defaults\nimpact: HIGH\nimpactDescription: 4-6× fewer lines than if/else for default value assignment\ntags: var, defaults, parameter-expansion, unset\n---\n\n## Use Parameter Expansion for Defaults\n\nChecking for unset variables with if/else is verbose and error-prone. Parameter expansion provides concise, safe defaults that work with `set -u`.\n\n**Incorrect (verbose conditional checks):**\n\n```bash\n#!/bin/bash\n# Verbose and error-prone\nif [[ -z \"$CONFIG_FILE\" ]]; then\n CONFIG_FILE=\"/etc/default.conf\"\nfi\n\nif [[ -z \"$TIMEOUT\" ]]; then\n TIMEOUT=30\nfi\n\n# Or ignoring the issue:\necho \"Using: $UNDEFINED_VAR\" # Empty string, potential bug\n```\n\n**Correct (parameter expansion):**\n\n```bash\n#!/bin/bash\nset -u # Error on undefined variables\n\n# ${var:-default} - Use default if unset or empty\nconfig_file=\"${CONFIG_FILE:-/etc/default.conf}\"\ntimeout=\"${TIMEOUT:-30}\"\n\n# ${var:=default} - Set AND use default if unset or empty\n: \"${LOG_LEVEL:=info}\" # Sets LOG_LEVEL if not already set\n\n# ${var:?message} - Error if unset or empty\ninput_file=\"${1:?Usage: $0 \u003cinput_file>}\"\n\n# ${var:+value} - Use value only if var IS set\n# Useful for optional flags\nverbose_flag=\"${VERBOSE:+--verbose}\"\nmy_command $verbose_flag # Only adds --verbose if VERBOSE is set\n```\n\n**Difference between :- and - (colon matters):**\n\n```bash\n#!/bin/bash\n# With colon: treats empty string same as unset\nempty_var=\"\"\necho \"${empty_var:-default}\" # Outputs: default\necho \"${empty_var-default}\" # Outputs: (empty)\n\n# ${var:-default} = use default if unset OR empty\n# ${var-default} = use default only if unset\n```\n\n**Common patterns:**\n\n```bash\n#!/bin/bash\n# Script configuration with defaults\n: \"${DEBUG:=false}\"\n: \"${LOG_DIR:=/var/log/myapp}\"\n: \"${MAX_WORKERS:=4}\"\n: \"${CONFIG_FILE:=/etc/myapp/config.ini}\"\n\n# Environment-based configuration\ndb_host=\"${DB_HOST:-localhost}\"\ndb_port=\"${DB_PORT:-5432}\"\ndb_name=\"${DB_NAME:-myapp}\"\n\n# Required parameters\n: \"${API_KEY:?Error: API_KEY environment variable is required}\"\n\n# Optional verbose mode\nif [[ \"${VERBOSE:-false}\" == \"true\" ]]; then\n set -x\nfi\n```\n\n**With arrays:**\n\n```bash\n#!/bin/bash\n# Default array if not set\n: \"${TARGETS[*]:=localhost}\"\n\n# Or check array length\nif [[ ${#TARGETS[@]} -eq 0 ]]; then\n TARGETS=(localhost)\nfi\n```\n\nReference: [Bash Manual - Parameter Expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2427,"content_sha256":"ca8f71908da23daef578903c828e4d55beffb7bbbd7998b2dbf370062f56f0ad"},{"filename":"references/var-local-scope.md","content":"---\ntitle: Use local for Function Variables\nimpact: HIGH\nimpactDescription: prevents namespace pollution and hidden bugs\ntags: var, local, scope, functions, namespace\n---\n\n## Use local for Function Variables\n\nVariables in functions are global by default. Without `local`, functions can accidentally overwrite caller variables, creating mysterious bugs that are hard to trace.\n\n**Incorrect (global variables leak):**\n\n```bash\n#!/bin/bash\nresult=\"\"\n\nprocess() {\n result=\"processing\" # Overwrites global!\n for i in 1 2 3; do # Global 'i'\n echo \"$i\"\n done\n}\n\nmain() {\n result=\"initial\"\n for i in a b c; do\n process\n echo \"i is now: $i\" # Prints \"3\", not \"a\", \"b\", \"c\"!\n done\n echo \"Result: $result\" # \"processing\", not \"initial\"\n}\n```\n\n**Correct (use local):**\n\n```bash\n#!/bin/bash\nprocess() {\n local result=\"processing\" # Scoped to function\n local i\n for i in 1 2 3; do\n echo \"$i\"\n done\n echo \"$result\"\n}\n\nmain() {\n local result=\"initial\"\n local i\n for i in a b c; do\n process\n echo \"i is: $i\" # Correctly prints \"a\", \"b\", \"c\"\n done\n echo \"Result: $result\" # \"initial\"\n}\n```\n\n**Declare local variables at function start:**\n\n```bash\n#!/bin/bash\nprocess_file() {\n # Declare all local variables at top\n local file=\"$1\"\n local line_count\n local word_count\n local result\n\n # Now use them\n line_count=$(wc -l \u003c \"$file\")\n word_count=$(wc -w \u003c \"$file\")\n\n # Separate declaration from command substitution\n # to preserve exit status\n result=$(process \"$file\") || return 1\n\n echo \"Lines: $line_count, Words: $word_count\"\n}\n```\n\n**Note: local masks exit status:**\n\n```bash\n#!/bin/bash\n# WRONG: Exit status lost\nbad_function() {\n local result=$(failing_command) # Status masked!\n echo $? # Always 0\n}\n\n# CORRECT: Separate declaration\ngood_function() {\n local result\n result=$(failing_command)\n echo $? # Actual exit status\n}\n```\n\n**Use declare -g for intentional globals:**\n\n```bash\n#!/bin/bash\n# When you need a function to set a global\nsetup_config() {\n declare -g CONFIG_PATH=\"/etc/myapp\"\n declare -g CONFIG_FILE=\"$CONFIG_PATH/config.ini\"\n}\n\nsetup_config\necho \"$CONFIG_PATH\" # Accessible\n```\n\nReference: [Google Shell Style Guide - Local Variables](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2255,"content_sha256":"70f47c51fb0a01b91896184c82c5e37b1dd3e3aaf0c57928784ef978b830a379"},{"filename":"references/var-naming-conventions.md","content":"---\ntitle: Follow Variable Naming Conventions\nimpact: HIGH\nimpactDescription: prevents collisions with environment and builtins\ntags: var, naming, conventions, constants, environment\n---\n\n## Follow Variable Naming Conventions\n\nInconsistent naming causes collisions with environment variables and makes code harder to read. Following conventions prevents bugs and improves maintainability.\n\n**Incorrect (inconsistent naming):**\n\n```bash\n#!/bin/bash\n# Collides with environment variables\nPATH=\"/my/files\" # Breaks command execution!\nHOME=\"/tmp\" # Breaks ~ expansion!\nUSER=\"admin\" # May conflict\n\n# Inconsistent style\nfileName=\"test.txt\"\nfile_count=0\nFILESIZE=1024\n```\n\n**Correct (follow conventions):**\n\n```bash\n#!/bin/bash\n# Constants and environment exports: UPPER_CASE\nreadonly CONFIG_PATH=\"/etc/myapp\"\nreadonly MAX_RETRIES=3\nexport MY_APP_DEBUG=1\n\n# Regular variables: lower_case\nfile_name=\"test.txt\"\nfile_count=0\nfile_size=1024\n\n# Loop variables: lower_case\nfor file in \"${files[@]}\"; do\n process \"$file\"\ndone\n```\n\n**Naming conventions summary:**\n\n```bash\n#!/bin/bash\n\n# CONSTANTS (readonly, set once)\nreadonly DATABASE_URL=\"postgres://localhost/db\"\nreadonly MAX_CONNECTIONS=100\n\n# ENVIRONMENT VARIABLES (exported)\nexport MY_APP_CONFIG=\"/etc/myapp.conf\"\nexport MY_APP_LOG_LEVEL=\"info\"\n\n# Regular variables (script-local)\ninput_file=\"\"\noutput_dir=\"\"\nline_count=0\n\n# Function-local variables\nmy_function() {\n local temp_file\n local result_code\n}\n\n# Private/internal variables (convention: leading underscore)\n_internal_state=\"\"\n_cache_valid=false\n```\n\n**Avoid collisions:**\n\n```bash\n#!/bin/bash\n# Dangerous - common environment variables\n# PATH, HOME, USER, SHELL, PWD, OLDPWD\n# IFS, PS1, PS2, TERM, EDITOR, LANG\n\n# Use prefixes for your variables\nreadonly MY_APP_PATH=\"/opt/myapp\"\nreadonly MY_APP_CONFIG=\"/etc/myapp\"\n\n# Or use descriptive names that won't collide\nreadonly config_file_path=\"/etc/myapp/config\"\nreadonly installation_directory=\"/opt/myapp\"\n```\n\n**Boolean naming pattern:**\n\n```bash\n#!/bin/bash\n# Use is_, has_, should_ prefixes for booleans\nis_verbose=false\nhas_errors=false\nshould_continue=true\n\nif [[ \"$is_verbose\" == true ]]; then\n echo \"Verbose mode enabled\"\nfi\n```\n\nReference: [Google Shell Style Guide - Naming Conventions](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2316,"content_sha256":"8f4aeb9ddd41e8313a8bc18b4e05a1498f37c7721b82735e1f62656dc498ab38"},{"filename":"references/var-readonly-constants.md","content":"---\ntitle: Use readonly for Constants\nimpact: HIGH\nimpactDescription: prevents accidental modification of configuration values\ntags: var, readonly, constants, immutable\n---\n\n## Use readonly for Constants\n\nConstants that should never change after initialization can be accidentally overwritten. `readonly` prevents modifications and signals intent to readers.\n\n**Incorrect (mutable constants):**\n\n```bash\n#!/bin/bash\n# Can be accidentally modified\nCONFIG_FILE=\"/etc/myapp/config\"\nMAX_RETRIES=3\nVERSION=\"1.0.0\"\n\n# Later in script, typo causes bug:\nCONFIG_FILE=\"/etc/myapp/conf\" # Oops, meant to use it\n\n# Or in function:\nprocess() {\n MAX_RETRIES=10 # Accidentally changes global constant\n}\n```\n\n**Correct (immutable constants):**\n\n```bash\n#!/bin/bash\n# Constants declared at top of file\nreadonly CONFIG_FILE=\"/etc/myapp/config\"\nreadonly MAX_RETRIES=3\nreadonly VERSION=\"1.0.0\"\n\n# Attempt to modify causes error:\n# CONFIG_FILE=\"/other\" # Error: CONFIG_FILE: readonly variable\n\n# declare -r is equivalent\ndeclare -r DATABASE_URL=\"postgres://localhost/db\"\ndeclare -r TIMEOUT_SECONDS=30\n```\n\n**Group related constants:**\n\n```bash\n#!/bin/bash\n# Exit codes\nreadonly E_SUCCESS=0\nreadonly E_INVALID_ARGS=1\nreadonly E_FILE_NOT_FOUND=2\nreadonly E_PERMISSION_DENIED=3\n\n# Paths\nreadonly SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nreadonly CONFIG_DIR=\"/etc/myapp\"\nreadonly LOG_DIR=\"/var/log/myapp\"\nreadonly DATA_DIR=\"/var/lib/myapp\"\n\n# Application settings\nreadonly APP_NAME=\"myapp\"\nreadonly APP_VERSION=\"1.2.3\"\nreadonly DEFAULT_PORT=8080\n```\n\n**readonly vs declare -r:**\n\n```bash\n#!/bin/bash\n# Both work the same for global scope\nreadonly GLOBAL_CONST=\"value\"\ndeclare -r GLOBAL_CONST2=\"value\"\n\n# In functions, use local -r for local constants\nmy_function() {\n local -r local_const=\"value\" # Readonly within function\n echo \"$local_const\"\n}\n```\n\n**Export readonly for child processes:**\n\n```bash\n#!/bin/bash\n# Make readonly AND export for subprocesses\ndeclare -rx EXPORTED_CONST=\"value\"\n\n# Or separately\nreadonly MY_CONST=\"value\"\nexport MY_CONST\n```\n\nReference: [Google Shell Style Guide - Constants and Environment Variables](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2186,"content_sha256":"16aa742a397555224e418f37b3006b3571d4670a8a82dcc29c3ccc6a6113bd29"},{"filename":"references/var-use-arrays.md","content":"---\ntitle: Use Arrays for Lists Instead of Strings\nimpact: HIGH\nimpactDescription: prevents word splitting bugs in argument handling\ntags: var, arrays, lists, arguments, word-splitting\n---\n\n## Use Arrays for Lists Instead of Strings\n\nStoring lists in strings leads to word splitting issues with spaces, requires fragile quoting, and often forces unsafe `eval` usage. Arrays handle all edge cases correctly.\n\n**Incorrect (string-based lists):**\n\n```bash\n#!/bin/bash\n# Breaks with spaces in filenames\nfiles=\"file1.txt file2.txt my file.txt\"\nfor f in $files; do\n echo \"$f\" # \"my\" and \"file.txt\" are separate!\ndone\n\n# Building commands in strings\ncmd=\"ls -la\"\nopts=\"-h\"\n$cmd $opts # Breaks with complex arguments\ncmd=\"grep 'hello world'\" # Quotes don't survive expansion\n```\n\n**Correct (use arrays):**\n\n```bash\n#!/bin/bash\n# Arrays preserve elements with spaces\nfiles=(\"file1.txt\" \"file2.txt\" \"my file.txt\")\nfor f in \"${files[@]}\"; do\n echo \"$f\" # Correctly handles \"my file.txt\"\ndone\n\n# Building commands safely\ndeclare -a cmd=(ls -la)\ncmd+=(-h)\n\"${cmd[@]}\" # Executes correctly\n\n# Arguments with spaces work\ndeclare -a grep_args=(-r \"hello world\" .)\ngrep \"${grep_args[@]}\"\n```\n\n**Array operations:**\n\n```bash\n#!/bin/bash\n# Declaration\ndeclare -a empty_array=()\ndeclare -a with_values=(\"one\" \"two\" \"three\")\n\n# Append\nwith_values+=(\"four\")\nwith_values+=(\"five\" \"six\") # Append multiple\n\n# Length\necho \"Count: ${#with_values[@]}\"\n\n# Iterate\nfor item in \"${with_values[@]}\"; do\n process \"$item\"\ndone\n\n# Access by index\necho \"First: ${with_values[0]}\"\necho \"Last: ${with_values[-1]}\"\n\n# Slice\necho \"Middle: ${with_values[@]:1:2}\" # Elements 1-2\n\n# All indices\nfor i in \"${!with_values[@]}\"; do\n echo \"Index $i: ${with_values[i]}\"\ndone\n```\n\n**Read files into array safely:**\n\n```bash\n#!/bin/bash\n# Handle filenames with spaces, newlines, etc.\ndeclare -a files=()\nwhile IFS= read -r -d '' file; do\n files+=(\"$file\")\ndone \u003c \u003c(find . -type f -print0)\n\n# Process safely\nfor file in \"${files[@]}\"; do\n process \"$file\"\ndone\n```\n\nReference: [Google Shell Style Guide - Arrays](https://google.github.io/styleguide/shellguide.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2128,"content_sha256":"2f650f21076d517ff51f41a4f355c7dfa5824b9f1a04b8c25206ec48cd96303c"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Shell Scripts Best Practices (Community)","type":"text"}]},{"type":"paragraph","content":[{"text":"Comprehensive best practices guide for shell scripting, designed for AI agents and LLMs. Contains 49 rules across 9 categories, prioritized by impact from critical (safety, portability) to incremental (style). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Apply","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference these guidelines when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Writing new bash or POSIX shell scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reviewing shell scripts for security vulnerabilities","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Debugging scripts that fail silently or behave unexpectedly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Porting scripts between Linux, macOS, and containers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optimizing shell script performance","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up CI/CD pipelines with shell scripts","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rule Categories by Priority","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Priority","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prefix","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rules","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Safety & Security","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"safety-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Portability","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"port-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error Handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"err-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Variables & Data","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"var-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quoting & Expansion","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM-HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"quote-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Functions & Structure","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"func-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Testing & Conditionals","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"test-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Performance","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LOW-MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"perf-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Style & Formatting","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LOW","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"style-","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Safety & Security (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safety-command-injection","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-command-injection.md","title":null}},{"type":"code_inline"}]},{"text":" - Prevent command injection from user input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safety-eval-avoidance","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-eval-avoidance.md","title":null}},{"type":"code_inline"}]},{"text":" - Avoid eval for dynamic commands","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safety-absolute-paths","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-absolute-paths.md","title":null}},{"type":"code_inline"}]},{"text":" - Use absolute paths for external commands","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safety-temp-files","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-temp-files.md","title":null}},{"type":"code_inline"}]},{"text":" - Create secure temporary files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safety-suid-forbidden","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-suid-forbidden.md","title":null}},{"type":"code_inline"}]},{"text":" - Never use SUID/SGID on shell scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safety-argument-injection","type":"text","marks":[{"type":"link","attrs":{"href":"references/safety-argument-injection.md","title":null}},{"type":"code_inline"}]},{"text":" - Prevent argument injection with double dash","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Portability (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"port-shebang-selection","type":"text","marks":[{"type":"link","attrs":{"href":"references/port-shebang-selection.md","title":null}},{"type":"code_inline"}]},{"text":" - Choose shebang based on portability needs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"port-avoid-bashisms","type":"text","marks":[{"type":"link","attrs":{"href":"references/port-avoid-bashisms.md","title":null}},{"type":"code_inline"}]},{"text":" - Avoid bashisms in POSIX scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"port-printf-over-echo","type":"text","marks":[{"type":"link","attrs":{"href":"references/port-printf-over-echo.md","title":null}},{"type":"code_inline"}]},{"text":" - Use printf instead of echo for portability","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"port-export-syntax","type":"text","marks":[{"type":"link","attrs":{"href":"references/port-export-syntax.md","title":null}},{"type":"code_inline"}]},{"text":" - Use portable export syntax","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"port-test-portability","type":"text","marks":[{"type":"link","attrs":{"href":"references/port-test-portability.md","title":null}},{"type":"code_inline"}]},{"text":" - Use portable test constructs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Error Handling (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-strict-mode","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-strict-mode.md","title":null}},{"type":"code_inline"}]},{"text":" - Use strict mode for error detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-exit-codes","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-exit-codes.md","title":null}},{"type":"code_inline"}]},{"text":" - Use meaningful exit codes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-trap-cleanup","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-trap-cleanup.md","title":null}},{"type":"code_inline"}]},{"text":" - Use trap for cleanup on exit","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-stderr-messages","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-stderr-messages.md","title":null}},{"type":"code_inline"}]},{"text":" - Send error messages to stderr","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-pipefail","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-pipefail.md","title":null}},{"type":"code_inline"}]},{"text":" - Use pipefail to catch pipeline errors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-check-commands","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-check-commands.md","title":null}},{"type":"code_inline"}]},{"text":" - Check command success explicitly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-shellcheck","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-shellcheck.md","title":null}},{"type":"code_inline"}]},{"text":" - Use ShellCheck for static analysis","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-debug-tracing","type":"text","marks":[{"type":"link","attrs":{"href":"references/err-debug-tracing.md","title":null}},{"type":"code_inline"}]},{"text":" - Use debug tracing with set -x and PS4","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Variables & Data (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"var-use-arrays","type":"text","marks":[{"type":"link","attrs":{"href":"references/var-use-arrays.md","title":null}},{"type":"code_inline"}]},{"text":" - Use arrays for lists instead of strings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"var-local-scope","type":"text","marks":[{"type":"link","attrs":{"href":"references/var-local-scope.md","title":null}},{"type":"code_inline"}]},{"text":" - Use local for function variables","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"var-naming-conventions","type":"text","marks":[{"type":"link","attrs":{"href":"references/var-naming-conventions.md","title":null}},{"type":"code_inline"}]},{"text":" - Follow variable naming conventions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"var-readonly-constants","type":"text","marks":[{"type":"link","attrs":{"href":"references/var-readonly-constants.md","title":null}},{"type":"code_inline"}]},{"text":" - Use readonly for constants","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"var-default-values","type":"text","marks":[{"type":"link","attrs":{"href":"references/var-default-values.md","title":null}},{"type":"code_inline"}]},{"text":" - Use parameter expansion for defaults","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Quoting & Expansion (MEDIUM-HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quote-always-quote-variables","type":"text","marks":[{"type":"link","attrs":{"href":"references/quote-always-quote-variables.md","title":null}},{"type":"code_inline"}]},{"text":" - Always quote variable expansions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quote-dollar-at","type":"text","marks":[{"type":"link","attrs":{"href":"references/quote-dollar-at.md","title":null}},{"type":"code_inline"}]},{"text":" - Use \"$@\" for argument passing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quote-command-substitution","type":"text","marks":[{"type":"link","attrs":{"href":"references/quote-command-substitution.md","title":null}},{"type":"code_inline"}]},{"text":" - Quote command substitutions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quote-brace-expansion","type":"text","marks":[{"type":"link","attrs":{"href":"references/quote-brace-expansion.md","title":null}},{"type":"code_inline"}]},{"text":" - Use braces for variable clarity","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quote-here-documents","type":"text","marks":[{"type":"link","attrs":{"href":"references/quote-here-documents.md","title":null}},{"type":"code_inline"}]},{"text":" - Use here documents for multi-line strings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"quote-glob-safety","type":"text","marks":[{"type":"link","attrs":{"href":"references/quote-glob-safety.md","title":null}},{"type":"code_inline"}]},{"text":" - Control glob expansion explicitly","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Functions & Structure (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"func-main-pattern","type":"text","marks":[{"type":"link","attrs":{"href":"references/func-main-pattern.md","title":null}},{"type":"code_inline"}]},{"text":" - Use main() function pattern","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"func-single-purpose","type":"text","marks":[{"type":"link","attrs":{"href":"references/func-single-purpose.md","title":null}},{"type":"code_inline"}]},{"text":" - Write single-purpose functions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"func-return-values","type":"text","marks":[{"type":"link","attrs":{"href":"references/func-return-values.md","title":null}},{"type":"code_inline"}]},{"text":" - Use return values correctly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"func-documentation","type":"text","marks":[{"type":"link","attrs":{"href":"references/func-documentation.md","title":null}},{"type":"code_inline"}]},{"text":" - Document functions with header comments","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"func-avoid-aliases","type":"text","marks":[{"type":"link","attrs":{"href":"references/func-avoid-aliases.md","title":null}},{"type":"code_inline"}]},{"text":" - Prefer functions over aliases","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Testing & Conditionals (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-double-brackets","type":"text","marks":[{"type":"link","attrs":{"href":"references/test-double-brackets.md","title":null}},{"type":"code_inline"}]},{"text":" - Use [[ ]] for tests in bash","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-arithmetic","type":"text","marks":[{"type":"link","attrs":{"href":"references/test-arithmetic.md","title":null}},{"type":"code_inline"}]},{"text":" - Use (( )) for arithmetic comparisons","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-explicit-empty","type":"text","marks":[{"type":"link","attrs":{"href":"references/test-explicit-empty.md","title":null}},{"type":"code_inline"}]},{"text":" - Use explicit empty/non-empty string tests","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-file-operators","type":"text","marks":[{"type":"link","attrs":{"href":"references/test-file-operators.md","title":null}},{"type":"code_inline"}]},{"text":" - Use correct file test operators","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-case-patterns","type":"text","marks":[{"type":"link","attrs":{"href":"references/test-case-patterns.md","title":null}},{"type":"code_inline"}]},{"text":" - Use case for pattern matching","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. Performance (LOW-MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-builtins-over-external","type":"text","marks":[{"type":"link","attrs":{"href":"references/perf-builtins-over-external.md","title":null}},{"type":"code_inline"}]},{"text":" - Use builtins over external commands","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-avoid-subshells","type":"text","marks":[{"type":"link","attrs":{"href":"references/perf-avoid-subshells.md","title":null}},{"type":"code_inline"}]},{"text":" - Avoid unnecessary subshells","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-process-substitution","type":"text","marks":[{"type":"link","attrs":{"href":"references/perf-process-substitution.md","title":null}},{"type":"code_inline"}]},{"text":" - Use process substitution for temp files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-read-files","type":"text","marks":[{"type":"link","attrs":{"href":"references/perf-read-files.md","title":null}},{"type":"code_inline"}]},{"text":" - Read files efficiently","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-parameter-expansion","type":"text","marks":[{"type":"link","attrs":{"href":"references/perf-parameter-expansion.md","title":null}},{"type":"code_inline"}]},{"text":" - Use parameter expansion for string operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-batch-operations","type":"text","marks":[{"type":"link","attrs":{"href":"references/perf-batch-operations.md","title":null}},{"type":"code_inline"}]},{"text":" - Batch operations instead of loops","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"9. Style & Formatting (LOW)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-indentation","type":"text","marks":[{"type":"link","attrs":{"href":"references/style-indentation.md","title":null}},{"type":"code_inline"}]},{"text":" - Use consistent indentation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-file-structure","type":"text","marks":[{"type":"link","attrs":{"href":"references/style-file-structure.md","title":null}},{"type":"code_inline"}]},{"text":" - Follow consistent file structure","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-comments","type":"text","marks":[{"type":"link","attrs":{"href":"references/style-comments.md","title":null}},{"type":"code_inline"}]},{"text":" - Write useful comments","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Read individual reference files for detailed explanations and code examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Section definitions","type":"text","marks":[{"type":"link","attrs":{"href":"references/_sections.md","title":null}}]},{"text":" - Category structure and impact levels","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rule template","type":"text","marks":[{"type":"link","attrs":{"href":"assets/templates/_template.md","title":null}}]},{"text":" - Template for adding new rules","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Files","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"File","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AGENTS.md","type":"text","marks":[{"type":"link","attrs":{"href":"AGENTS.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Complete compiled guide with all rules","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/_sections.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/_sections.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category definitions and ordering","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"assets/templates/_template.md","type":"text","marks":[{"type":"link","attrs":{"href":"assets/templates/_template.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Template for new rules","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.json","type":"text","marks":[{"type":"link","attrs":{"href":"metadata.json","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Version and reference information","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Sources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Google Shell Style Guide","type":"text","marks":[{"type":"link","attrs":{"href":"https://google.github.io/styleguide/shellguide.html","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ShellCheck","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.shellcheck.net/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Greg's Wiki (wooledge.org)","type":"text","marks":[{"type":"link","attrs":{"href":"https://mywiki.wooledge.org/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"POSIX Shell Specification","type":"text","marks":[{"type":"link","attrs":{"href":"https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html","title":null}}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"shell","author":"@skillopedia","source":{"stars":220,"repo_name":"dot-skills","origin_url":"https://github.com/pproenca/dot-skills/blob/HEAD/skills/.experimental/shell/SKILL.md","repo_owner":"pproenca","body_sha256":"7b6d32ccd5f65bd79b02bce3d18bba1f0ff8104be847b2456f88f27c154d2cb6","cluster_key":"d42949f6a11dd8b7a4bb9a5613bb0cc9f1149c81c042b134d6cef49568490380","clean_bundle":{"format":"clean-skill-bundle-v1","source":"pproenca/dot-skills/skills/.experimental/shell/SKILL.md","text_bytes":136112,"source_commit":"cf93c57cac89d6fc3e4194686000411567f5caf3","skills_sh_rank":16086,"attachment_count":54,"text_attachments":54,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/.experimental/shell/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"software-engineering","import_tag":"skills-sh-top20k-2026-11","description":"Shell scripting best practices for writing safe, portable, and maintainable bash/sh scripts. Use when writing, reviewing, or refactoring shell scripts, Dockerfile RUN commands, Makefile recipes, CI pipeline scripts, cron jobs, or systemd ExecStart directives. Triggers on bash, sh, POSIX, ShellCheck, error handling, quoting, variables, set -euo pipefail."}},"renderedAt":1793670985060}

Shell Scripts Best Practices (Community) Comprehensive best practices guide for shell scripting, designed for AI agents and LLMs. Contains 49 rules across 9 categories, prioritized by impact from critical (safety, portability) to incremental (style). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics. When to Apply Reference these guidelines when: - Writing new bash or POSIX shell scripts - Reviewing shell scripts for security vulnerabilities - Debugging scripts that fail silently or behave unexpectedly -…