
Bash Master
- 413 installs
- 50 repo stars
- Updated June 18, 2026
- josiahsiegel/claude-plugin-marketplace
bash-master is a Claude agent skill that writes, reviews, and hardens Bash shell scripts with Bash 5.3 patterns, ShellCheck validation, and Google Shell Style Guide compliance for developers automating dev and CI workflo
About
bash-master is an expert Bash scripting skill from the josiahsiegel/claude-plugin-marketplace that activates for script creation, DevOps automation, CI/CD pipelines, build scripts, and debugging across Linux, macOS, Windows Git Bash, and containers. It enforces Google Shell Style Guide formatting, ShellCheck v0.11.0 checks, POSIX portability, set -euo pipefail error handling, injection prevention, and optional BATS testing. The parent Bash Master plugin v2.0.0 bundles 10 related skills covering arrays, parallel processing, and security-first 2025 patterns. Developers reach for bash-master when converting manual commands into production shell glue, reviewing fragile scripts, or building cross-platform automation under roughly 50 lines per Google style guidance.
- Idiomatic Bash scripts
- Error handling patterns
- CLI automation
- Pipeline glue
- Cross-tool scripting
Bash Master by the numbers
- 413 all-time installs (skills.sh)
- +7 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #133 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill bash-masterAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 413 |
|---|---|
| repo stars | ★ 50 |
| Last updated | June 18, 2026 |
| Repository | josiahsiegel/claude-plugin-marketplace ↗ |
How do you write production-safe Bash automation scripts?
Write, debug, and automate Bash scripts for local dev workflows, CI tasks, file transforms, and shell glue connecting tools during implementation.
Who is it for?
Developers writing DevOps, CI/CD, build, or local automation shell scripts who want ShellCheck-clean, portable Bash with modern 5.3 features.
Skip if: Developers building application logic in Python, Go, or Node who do not need shell glue, or teams standardized on PowerShell-only automation.
When should I use this skill?
The task mentions bash, shell, script, CI automation, deployment glue, or asks to review, debug, or convert commands into a shell script.
What you get
ShellCheck-validated Bash scripts with Google Style formatting, robust error handling, security hardening, and optional BATS test scaffolding.
- production Bash script
- ShellCheck-clean automation
- optional BATS test file
By the numbers
- Parent Bash Master plugin v2.0.0 bundles 10 related bash scripting skills
- Targets ShellCheck v0.11.0 and Bash 5.3 feature patterns
Files
Bash Scripting Mastery
Scope and platform contract
This skill targets Bash itself, wherever Bash runs - Linux, macOS, WSL, Git Bash / MSYS2 on Windows, and Bash-based container images. It does not cover native PowerShell: a PowerShell script is a different language and should use powershell-master. On Windows, bash-master assumes the user is running Bash inside Git Bash, WSL, or a similar Bash environment, and addresses the MSYS path-translation quirks that result.
Repository conventions
Project-level conventions (Windows backslashes in tool calls, documentation discipline, etc.) live in the agent body and the windows-path-master plugin. This skill focuses on Bash content; do not duplicate that boilerplate here.
Quick reference
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
IFS=$'\n\t' # Safe word splitting
# Run shellcheck your_script.sh before deployment.
# Test on every target platform before production.Bash-portability quick check:
# Linux/macOS: Full bash features
# Git Bash (Windows): Most features, some system calls missing (no systemd, /proc differs)
# WSL: Effectively Linux; /mnt/c for Windows filesystem
# Containers: Depends on base image - alpine ships /bin/sh, not bash
# POSIX mode: Use /bin/sh and avoid bashismsWhen to use this skill
Always activate for:
- Writing or modifying any bash/shell script
- Reviewing or refactoring existing scripts
- Debugging shell script failures
- DevOps automation, CI/CD pipelines, system administration
- Cross-environment Bash portability (Linux <-> macOS <-> WSL <-> Git Bash <-> container)
Do not use this skill for:
- PowerShell scripts - use
powershell-master - Batch (
.cmd/.bat) scripting - Generic command help unrelated to scripting
Core principles
1. Safety first
Every script should open with the safety preamble:
#!/usr/bin/env bash
set -e # Exit on any error
set -u # Exit on undefined variable
set -o pipefail # Catch failures mid-pipeline
set -E # Inherit ERR trap into functions
IFS=$'\n\t' # Avoid word splitting on spaces
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"2. POSIX vs Bash
Need to run on any UNIX sh | #!/bin/sh, no [[ ]], no arrays, no process substitution |
|---|---|
| Modern Linux/macOS with Bash | #!/usr/bin/env bash, prefer [[ ]], arrays, regex |
| Alpine/minimal containers | Either install bash explicitly or write POSIX-compliant sh |
3. Quoting
# Always quote expansions
process "$file_path" # correct
process $file_path # word-splitting bug
# Arrays
files=("file 1.txt" "file 2.txt")
process "${files[@]}" # each element kept separate
process "${files[*]}" # joined as one string - usually wrong4. ShellCheck
Run shellcheck on every script. Only disable warnings with a justification comment: # shellcheck disable=SC2086 reason: intentional word splitting.
See references/best_practices.md for the full quoting/style table and references/patterns_antipatterns.md for the common pitfalls.
Platform-specific considerations
Git Bash / MSYS2 (Windows)
Git Bash auto-converts Unix-style arguments to Windows paths. This is the largest single source of cross-platform Bash bugs on Windows.
# The conversion: /foo becomes C:/Program Files/Git/usr/foo
# Disable per-command:
MSYS_NO_PATHCONV=1 command /path/that/should/stay/unix
# Manual conversion
unix_path=$(cygpath -u "C:\Windows\System32")
win_path=$(cygpath -w "/c/Users/username")
# Detect Git Bash
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "mingw"* ]]; then
: # Git Bash
fi
case "${MSYSTEM:-}" in
MINGW64|MINGW32|MSYS) : ;; # MSYS2 / Git Bash environment
esac
# Flags that look like paths
command //e //s # double-slash to suppress conversion
command -e -s # or use dash-optionsFull Git Bash + Windows path notes live in references/windows-git-bash-paths.md.
Linux
GNU coreutils, /proc, systemd integration. Detect via [[ "$OSTYPE" == "linux-gnu"* ]].
macOS
BSD utilities behave differently from GNU. Most common gotchas: sed -i '' (empty string required), date flags differ, readlink -f not available on stock macOS.
if command -v gsed >/dev/null; then SED=gsed; else SED=sed; fiWSL
Effectively Linux. The Windows filesystem is mounted at /mnt/c/. Detect via grep -qi microsoft /proc/version.
Containers
Alpine images ship only /bin/sh (BusyBox). Write POSIX-compliant scripts or apk add bash. Container init quirks: PID 1 must reap children and handle signals. Detect via [ -f /.dockerenv ] or [ -n "$KUBERNETES_SERVICE_HOST" ].
Portable platform-detection template
detect_platform() {
case "$OSTYPE" in
linux-gnu*) echo "linux" ;;
darwin*) echo "macos" ;;
msys*|cygwin*) echo "windows" ;;
*) echo "unknown" ;;
esac
}Full per-platform tables (BSD-vs-GNU coreutils flags, WSL networking, container init patterns) live in references/platform_specifics.md.
Best practices (summary)
The full patterns - function design, error handling, input validation, argument parsing, logging - live in references/in-depth-patterns.md. The headline rules:
- One concern per function; locals declared first; validate input; return non-zero on error.
- Constants
UPPER_CASE; localslower_case; mark immutable valuesreadonly. - Always check exit codes (
if ! cmd,||, traps, or a centralerror_exithelper). - Validate every external input - empty, format, length, charset.
- Use
getoptsor acase-based argument parser; print usage and exit 1 on bad input. - Use a leveled logger that writes to stderr.
Security, performance, testing, debugging, advanced patterns
These each have dedicated sections in references/in-depth-patterns.md:
| Topic | What it covers |
|---|---|
| Security | Command-injection prevention, path-traversal guards, privilege management, secure temp files |
| Performance | Avoiding subshells, bash built-ins vs externals, process substitution, array ops |
| Testing | BATS unit tests, integration test patterns, CI/CD wiring |
| Debugging | set -x, PS4, conditional debug helpers, tracing and profiling |
| Advanced patterns | Safe config parsing, parallel processing, signal handling, retries with backoff |
Read that reference any time you need the canonical code template for one of those topics.
Reference files
- `references/platform_specifics.md` - Detailed platform differences and workarounds
- `references/best_practices.md` - Comprehensive industry standards and guidelines
- `references/patterns_antipatterns.md` - Common patterns and pitfalls with solutions
- `references/windows-git-bash-paths.md` - Git Bash / MSYS path-translation reference
- `references/in-depth-patterns.md` - Function design, security, performance, testing, debugging, advanced patterns
- `references/resources.md` - Official docs, style guides, tooling, and learning links
Success criteria
A Bash script written with this skill should:
1. Pass shellcheck with no warnings 2. Begin with set -euo pipefail 3. Quote every variable expansion 4. Print usage on -h/--help 5. Decompose into testable functions 6. Handle empty input, missing files, and unexpected arguments 7. Run on every target platform (Linux/macOS/WSL/Git Bash/container) where it claims support 8. Match the Google Shell Style Guide 9. Clean up on exit (trap EXIT) 10. Be unit-tested with BATS where logic is non-trivial
# Pre-deployment checklist
shellcheck script.sh
bash -n script.sh
bats test/script.bats
./script.sh --help
DEBUG=true ./script.shTroubleshooting
Script fails on a different platform
checkbashisms script.shto surface non-portable constructs.command -v toolto verify a required tool is installed.- Diff command flags between GNU and BSD (
sed --versionetc.).
ShellCheck warnings
- Read the rule explanation (
shellcheck -W SC2086). - Fix the underlying issue; only disable a rule with a justification comment.
Works interactively but fails in cron
- Cron has a minimal
PATH- setPATHexplicitly. - Use absolute paths.
- Redirect stdout/stderr:
./script.sh >> /tmp/cron.log 2>&1.
Performance issues
- Profile with
time. - Enable
set -xto find slow steps. - Replace external invocations with Bash built-ins where possible.
Bash Scripting Best Practices & Industry Standards
Comprehensive guide to professional bash scripting following industry standards including Google Shell Style Guide, ShellCheck recommendations, and community best practices.
---
Table of Contents
1. Script Structure 2. Safety and Robustness 3. Style Guidelines 4. Functions 5. Variables 6. Error Handling 7. Input/Output 8. Security 9. Performance 10. Documentation 11. Testing 12. Maintenance
---
🚨 CRITICAL GUIDELINES
Windows File Path Requirements
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/).
Examples:
- ❌ WRONG:
D:/repos/project/file.tsx - ✅ CORRECT:
D:\repos\project\file.tsx
This applies to:
- Edit tool file_path parameter
- Write tool file_path parameter
- All file operations on Windows systems
Documentation Guidelines
NEVER create new documentation files unless explicitly requested by the user.
- Priority: Update existing README.md files rather than creating new documentation
- Repository cleanliness: Keep repository root clean - only README.md unless user requests otherwise
- Style: Documentation should be concise, direct, and professional - avoid AI-generated tone
- User preference: Only create additional .md files when user specifically asks for documentation
---
Script Structure
Standard Template
#!/usr/bin/env bash
#
# Script Name: script_name.sh
# Description: Brief description of what this script does
# Author: Your Name
# Date: 2024-01-01
# Version: 1.0.0
#
# Usage: script_name.sh [OPTIONS] <arguments>
#
# Options:
# -h, --help Show help message
# -v, --verbose Enable verbose output
#
# Dependencies:
# - bash >= 4.0
# - jq
# - curl
#
# Exit Codes:
# 0 - Success
# 1 - General error
# 2 - Invalid arguments
# 3 - Missing dependency
#
set -euo pipefail
IFS=$'\n\t'
# Script metadata
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_VERSION="1.0.0"
# Global constants
readonly DEFAULT_TIMEOUT=30
readonly CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/config.conf}"
# Global variables
VERBOSE=false
DRY_RUN=false
#------------------------------------------------------------------------------
# Functions
#------------------------------------------------------------------------------
# Show usage information
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <command>
Description of what the script does.
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
-n, --dry-run Show what would be done without doing it
-V, --version Show version
COMMANDS:
build Build the project
test Run tests
deploy Deploy to production
EXAMPLES:
$SCRIPT_NAME build
$SCRIPT_NAME --verbose test
$SCRIPT_NAME deploy --dry-run
EOF
}
# Cleanup function
cleanup() {
local exit_code=$?
# Remove temporary files
[[ -n "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR"
exit "$exit_code"
}
# Main function
main() {
# Parse arguments
parse_arguments "$@"
# Validate dependencies
check_dependencies
# Main script logic here
echo "Script execution complete"
}
#------------------------------------------------------------------------------
# Script execution
#------------------------------------------------------------------------------
# Set up cleanup trap
trap cleanup EXIT INT TERM
# Run main function with all arguments
main "$@"File Organization
# For larger projects, organize code into modules
# project/
# ├── bin/
# │ └── main.sh # Entry point
# ├── lib/
# │ ├── common.sh # Shared utilities
# │ ├── config.sh # Configuration handling
# │ └── logger.sh # Logging functions
# ├── config/
# │ └── default.conf # Default configuration
# ├── test/
# │ ├── test_common.bats # Unit tests
# │ └── test_config.bats
# └── README.md
# In main.sh:
# Source library files
readonly LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" && pwd)"
# shellcheck source=lib/common.sh
source "$LIB_DIR/common.sh"
# shellcheck source=lib/logger.sh
source "$LIB_DIR/logger.sh"---
Safety and Robustness
Essential Safety Settings
# ALWAYS use these at the start of scripts
set -e # Exit immediately if a command exits with a non-zero status
set -u # Treat unset variables as an error
set -o pipefail # Return value of a pipeline is status of last command to exit with non-zero status
set -E # ERR trap is inherited by shell functions
# Optionally add:
set -x # Print commands before executing (debugging)
set -C # Prevent output redirection from overwriting existing filesSafe Word Splitting
# Default IFS causes issues with filenames containing spaces
# OLD IFS: space, tab, newline
IFS=$' \t\n'
# SAFE IFS: only tab and newline
IFS=$'\n\t'
# This prevents word splitting on spaces, which is a common source of bugs:
files="file1.txt file2.txt"
for file in $files; do # Without proper IFS, this splits on spaces!
echo "$file"
doneQuoting Rules
# ALWAYS quote variable expansions
command "$variable" # ✓ CORRECT
command $variable # ✗ WRONG (word splitting and globbing)
# Arrays: Proper expansion
files=("file1.txt" "file 2.txt" "file 3.txt")
process "${files[@]}" # ✓ CORRECT (each element separate)
process "${files[*]}" # ✗ WRONG (all elements as one string)
process ${files[@]} # ✗ WRONG (unquoted, word splitting)
# Command substitution: Quote the result
result="$(command)" # ✓ CORRECT
result=$(command) # ✗ WRONG (unless word splitting is desired)
# Glob patterns: Don't quote when you want globbing
for file in *.txt; do # ✓ CORRECT (globbing intended)
echo "$file" # ✓ CORRECT (no globbing inside)
done
for file in "*.txt"; do # ✗ WRONG (literal "*.txt", no globbing)
echo "$file"
doneHandling Special Characters
# Filenames with special characters
# Use quotes and proper escaping
# Create array from find output
mapfile -t files < <(find . -name "*.txt" -print0 | xargs -0)
# Or modern bash:
files=()
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find . -name "*.txt" -print0)
# Process files safely
for file in "${files[@]}"; do
[[ -f "$file" ]] && process "$file"
done---
Style Guidelines
Based on Google Shell Style Guide and community standards.
Naming Conventions
# Constants: UPPER_CASE with underscores
readonly MAX_RETRIES=3
readonly DEFAULT_TIMEOUT=30
readonly CONFIG_DIR="/etc/myapp"
# Environment variables: UPPER_CASE (by convention)
export DATABASE_URL="postgres://localhost/db"
export LOG_LEVEL="INFO"
# Global variables: UPPER_CASE or lower_case (be consistent in your project)
GLOBAL_COUNTER=0
current_state="initialized"
# Local variables: lower_case with underscores
local user_name="john"
local file_count=0
local error_message=""
# Functions: lower_case with underscores
function_name() {
local var="value"
}
# Private functions: Prefix with underscore
_internal_function() {
# Helper function not meant to be called externally
}Indentation and Formatting
# Use 4 spaces for indentation (not tabs)
# Or 2 spaces (be consistent)
# Function definition
my_function() {
local arg="$1"
if [[ -n "$arg" ]]; then
echo "Processing $arg"
else
echo "No argument provided"
return 1
fi
return 0
}
# Conditional blocks
if [[ condition ]]; then
# code
elif [[ other_condition ]]; then
# code
else
# code
fi
# Loops
for item in "${array[@]}"; do
# code
done
while [[ condition ]]; do
# code
done
# Case statement
case "$variable" in
pattern1)
# code
;;
pattern2)
# code
;;
*)
# default
;;
esac
# Line length: Prefer < 80 characters, max 100
# Break long lines with backslash
long_command \
--option1 value1 \
--option2 value2 \
--option3 value3
# Or use arrays for readability
command_args=(
--option1 value1
--option2 value2
--option3 value3
)
command "${command_args[@]}"Comments
# Single-line comments: Start with # followed by space
# This is a comment
# Function documentation (before function definition)
#######################################
# Description of what this function does
# Globals:
# GLOBAL_VAR - Description
# Arguments:
# $1 - First argument description
# $2 - Second argument description (optional)
# Outputs:
# Writes result to stdout
# Returns:
# 0 on success, non-zero on error
#######################################
my_function() {
# Implementation
}
# Inline comments: Use sparingly, only when necessary
result=$(complex_calculation) # Result in milliseconds
# TODO comments
# TODO(username): Description of what needs to be done
# FIXME(username): Description of what needs to be fixed
# HACK(username): Description of workaround and why it's needed
# Section separators for long scripts
#------------------------------------------------------------------------------
# Configuration Section
#------------------------------------------------------------------------------
#######################################
# Database Functions
#######################################Test Constructs
# Prefer [[ ]] over [ ] for tests in bash
# [[ ]] is a bash keyword with better behavior:
# - No word splitting
# - No pathname expansion
# - More operators available
# String comparison
if [[ "$string1" == "$string2" ]]; then # ✓ CORRECT
if [ "$string1" = "$string2" ]; then # ✓ CORRECT (POSIX)
if [ $string1 == $string2 ]; then # ✗ WRONG (word splitting, not POSIX)
# String matching with patterns
if [[ "$file" == *.txt ]]; then # ✓ CORRECT (pattern matching)
if [[ "$file" =~ \.txt$ ]]; then # ✓ CORRECT (regex)
# Numeric comparison
if [[ $num -gt 10 ]]; then # ✓ CORRECT
if (( num > 10 )); then # ✓ CORRECT (arithmetic context)
# File tests
if [[ -f "$file" ]]; then # ✓ CORRECT (regular file)
if [[ -d "$dir" ]]; then # ✓ CORRECT (directory)
if [[ -e "$path" ]]; then # ✓ CORRECT (exists)
if [[ -r "$file" ]]; then # ✓ CORRECT (readable)
if [[ -w "$file" ]]; then # ✓ CORRECT (writable)
if [[ -x "$file" ]]; then # ✓ CORRECT (executable)
# Logical operators
if [[ condition1 && condition2 ]]; then # ✓ CORRECT (AND)
if [[ condition1 || condition2 ]]; then # ✓ CORRECT (OR)
if [[ ! condition ]]; then # ✓ CORRECT (NOT)
# Empty/non-empty string
if [[ -z "$var" ]]; then # ✓ CORRECT (empty)
if [[ -n "$var" ]]; then # ✓ CORRECT (non-empty)---
Functions
Function Best Practices
# Good function structure
process_file() {
# 1. Declare local variables
local file="$1"
local output_dir="${2:-.}" # Default to current directory
local result=""
# 2. Input validation
if [[ ! -f "$file" ]]; then
echo "Error: File not found: $file" >&2
return 1
fi
if [[ ! -d "$output_dir" ]]; then
echo "Error: Output directory not found: $output_dir" >&2
return 1
fi
# 3. Main logic
result=$(perform_operation "$file")
# 4. Output
echo "$result" > "$output_dir/result.txt"
# 5. Return status
return 0
}
# Use return codes to indicate success/failure
# 0 = success, non-zero = error
validate_input() {
local input="$1"
if [[ ! "$input" =~ ^[a-zA-Z0-9]+$ ]]; then
return 1 # Invalid input
fi
return 0 # Valid input
}
# Usage
if validate_input "$user_input"; then
process "$user_input"
else
echo "Invalid input" >&2
exit 1
fiFunction Documentation
#######################################
# Process a file and generate output
# Globals:
# OUTPUT_FORMAT - Output format (json/xml/csv)
# Arguments:
# $1 - Input file path (required)
# $2 - Output directory (optional, default: .)
# Outputs:
# Writes processed data to stdout
# Writes result file to output directory
# Returns:
# 0 on success
# 1 if file not found
# 2 if processing fails
# Example:
# process_file "input.txt" "/tmp/output"
#######################################
process_file() {
# Implementation
}Local Variables
# ALWAYS use local for function variables
bad_function() {
counter=0 # ✗ WRONG - Global variable!
}
good_function() {
local counter=0 # ✓ CORRECT - Local to function
}
# Declare local before assignment
good_practice() {
local result
result=$(command_that_might_fail) || return 1
echo "$result"
}
# This won't catch command failure:
bad_practice() {
local result=$(command_that_might_fail) # ✗ WRONG
echo "$result"
}---
Variables
Variable Declaration
# Readonly for constants
readonly MAX_RETRIES=3
declare -r MAX_RETRIES=3 # Alternative syntax
# Arrays
files=("file1.txt" "file2.txt" "file3.txt")
declare -a files=("file1.txt" "file2.txt")
# Associative arrays (bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
# Integer variables
declare -i count=0
count+=1 # Arithmetic operation
# Export for environment
export DATABASE_URL="postgres://localhost/db"
declare -x DATABASE_URL="postgres://localhost/db"Variable Expansion
# Default values
value="${var:-default}" # Use default if var is unset or empty
value="${var-default}" # Use default only if var is unset
value="${var:=default}" # Assign default if var is unset or empty
value="${var+alternative}" # Use alternative if var is set
# String length
length="${#string}"
# Substring
substring="${string:0:5}" # First 5 characters
substring="${string:5}" # From 5th character to end
# Pattern matching (prefix removal)
filename="/path/to/file.txt"
basename="${filename##*/}" # file.txt (remove longest match of */)
dirname="${filename%/*}" # /path/to (remove shortest match of /*)
# Pattern matching (suffix removal)
file="document.tar.gz"
name="${file%.gz}" # document.tar (remove shortest .gz)
name="${file%%.*}" # document (remove longest .*)
# Search and replace
string="hello world"
new_string="${string/world/universe}" # First occurrence
new_string="${string//o/0}" # All occurrences
new_string="${string/#hello/hi}" # Prefix match
new_string="${string/%world/earth}" # Suffix match
# Case modification (bash 4+)
upper="${string^^}" # TO UPPERCASE
lower="${string,,}" # to lowercase
capitalize="${string^}" # Capitalize first letterCommand Substitution
# Modern syntax: $()
result=$(command) # ✓ CORRECT (preferred)
result=`command` # ✓ CORRECT (old style, avoid)
# Nested command substitution
outer=$(echo "$(echo inner)") # ✓ CORRECT (easy to nest)
outer=`echo \`echo inner\`` # ✗ WRONG (hard to nest, requires escaping)
# Process substitution
diff <(command1) <(command2) # Compare outputs
while read -r line; do
echo "$line"
done < <(command) # Read command output---
Error Handling
Exit Codes
# Standard exit codes
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_INVALID_ARGS=2
readonly EXIT_MISSING_DEPENDENCY=3
# Use meaningful exit codes
validate_args() {
if [[ $# -lt 1 ]]; then
echo "Error: Missing required argument" >&2
exit "$EXIT_INVALID_ARGS"
fi
}
# Check command success
if ! command_that_might_fail; then
echo "Error: Command failed" >&2
exit "$EXIT_ERROR"
fi
# Alternative syntax
command_that_might_fail || {
echo "Error: Command failed" >&2
exit "$EXIT_ERROR"
}Error Messages
# ALWAYS write errors to stderr
echo "Error: Something went wrong" >&2
# Use consistent error message format
error() {
local message="$1"
local code="${2:-$EXIT_ERROR}"
echo "ERROR: $message" >&2
return "$code"
}
# Usage
if ! validate_input "$input"; then
error "Invalid input: $input" "$EXIT_INVALID_ARGS"
fiTrap Handlers
# Cleanup on exit
cleanup() {
local exit_code=$?
# Cleanup operations
[[ -n "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR"
[[ -n "${LOCKFILE:-}" ]] && rm -f "$LOCKFILE"
# Don't mask errors
exit "$exit_code"
}
trap cleanup EXIT
# Handle specific signals
handle_sigterm() {
echo "Received SIGTERM, shutting down..." >&2
# Graceful shutdown logic
exit 143 # 128 + 15 (SIGTERM)
}
trap handle_sigterm TERM
# ERR trap (bash 4.1+)
error_handler() {
local line="$1"
echo "Error on line $line" >&2
}
trap 'error_handler ${LINENO}' ERRDefensive Programming
# Validate all inputs
process_file() {
local file="$1"
# Check file exists
if [[ ! -f "$file" ]]; then
echo "Error: File not found: $file" >&2
return 1
fi
# Check file is readable
if [[ ! -r "$file" ]]; then
echo "Error: File not readable: $file" >&2
return 1
fi
# Process file
}
# Check dependencies before use
check_dependencies() {
local deps=(curl jq awk sed)
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
missing+=("$dep")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Error: Missing dependencies: ${missing[*]}" >&2
exit "$EXIT_MISSING_DEPENDENCY"
fi
}
# Validate environment
if [[ -z "${REQUIRED_VAR:-}" ]]; then
echo "Error: REQUIRED_VAR must be set" >&2
exit 1
fi---
Input/Output
Reading User Input
# Simple read
read -rp "Enter your name: " name
echo "Hello, $name"
# Read with timeout
if read -rt 10 -p "Enter value (10s timeout): " value; then
echo "You entered: $value"
else
echo "Timeout or error"
fi
# Read password (no echo)
read -rsp "Enter password: " password
echo # New line after password input
# Read confirmation
confirm() {
local prompt="${1:-Are you sure?}"
local response
read -rp "$prompt [y/N] " response
case "$response" in
[yY][eE][sS]|[yY])
return 0
;;
*)
return 1
;;
esac
}
# Usage
if confirm "Delete all files?"; then
rm -rf *
fiReading Files
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# Skip empty lines and comments
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip empty lines
[[ -z "$line" ]] && continue
# Skip comments
[[ "$line" =~ ^[[:space:]]*# ]] && continue
echo "Processing: $line"
done < file.txt
# Read into array
mapfile -t lines < file.txt
# Or
readarray -t lines < file.txt
# Read with null delimiter (for filenames with spaces)
while IFS= read -r -d '' file; do
echo "File: $file"
done < <(find . -type f -print0)Writing Output
# Stdout vs stderr
echo "Normal output" # stdout
echo "Error message" >&2 # stderr
# Redirect output
command > output.txt # Overwrite
command >> output.txt # Append
command 2> errors.txt # Stderr only
command &> all_output.txt # Both stdout and stderr
command > output.txt 2>&1 # Both (POSIX way)
# Here documents
cat <<EOF > file.txt
Line 1
Line 2
Variables are expanded: $VAR
EOF
# Here documents (no expansion)
cat <<'EOF' > file.txt
Line 1
Line 2
Variables are NOT expanded: $VAR
EOF
# Here strings
grep "pattern" <<< "$variable"---
Security
Input Validation
# Validate input format
validate_email() {
local email="$1"
local regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if [[ "$email" =~ $regex ]]; then
return 0
else
return 1
fi
}
# Sanitize file paths
sanitize_path() {
local path="$1"
# Remove directory traversal attempts
path="${path//..\/}"
# Remove leading slashes (if restricting to relative paths)
path="${path#/}"
echo "$path"
}
# Whitelist validation (preferred over blacklist)
validate_action() {
local action="$1"
local valid_actions=("start" "stop" "restart" "status")
for valid in "${valid_actions[@]}"; do
if [[ "$action" == "$valid" ]]; then
return 0
fi
done
return 1
}Command Injection Prevention
# NEVER use eval with user input
# ✗ DANGEROUS
eval "$user_input"
# NEVER concatenate user input into commands
# ✗ DANGEROUS
grep "$user_pattern" file.txt # If pattern contains flags, command injection!
# ✓ SAFE - Use -- to separate options from arguments
grep -- "$user_pattern" file.txt
# ✓ SAFE - Use arrays for complex commands
command_args=(
--option1 "$user_value1"
--option2 "$user_value2"
)
command "${command_args[@]}"
# ✓ SAFE - Use printf %q for shell escaping
safe_value=$(printf %q "$user_input")
eval "command $safe_value" # Now safe, but avoid if possibleTemporary Files
# Use mktemp for secure temporary files
TEMP_FILE=$(mktemp) || {
echo "Error: Cannot create temp file" >&2
exit 1
}
# Cleanup on exit
trap 'rm -f "$TEMP_FILE"' EXIT
# Secure temp file (mode 600)
SECURE_TEMP=$(mktemp)
chmod 600 "$SECURE_TEMP"
# Temporary directory
TEMP_DIR=$(mktemp -d) || {
echo "Error: Cannot create temp directory" >&2
exit 1
}
trap 'rm -rf "$TEMP_DIR"' EXITSecrets Management
# Don't hardcode secrets
# ✗ WRONG
PASSWORD="secret123"
# ✓ CORRECT - Read from environment
PASSWORD="${DATABASE_PASSWORD:-}"
if [[ -z "$PASSWORD" ]]; then
echo "Error: DATABASE_PASSWORD must be set" >&2
exit 1
fi
# ✓ CORRECT - Read from file
if [[ -f "$HOME/.config/app/password" ]]; then
PASSWORD=$(cat "$HOME/.config/app/password")
fi
# ✓ CORRECT - Prompt user
read -rsp "Enter password: " PASSWORD
echo
# Don't log secrets
# ✗ WRONG
echo "Connecting with password: $PASSWORD"
# ✓ CORRECT
echo "Connecting to database..."
# Mask secrets in process list
# ✗ WRONG - Password visible in ps
mysql -pSecret123
# ✓ CORRECT - Use config file or environment variable
export MYSQL_PWD="$PASSWORD"
mysql
# Clear secrets from environment when done
unset PASSWORD---
Performance
Avoid Unnecessary Subshells
# ✗ SLOW - Creates subshell
value=$(expr $a + $b)
# ✓ FAST - Bash arithmetic
value=$((a + b))
# ✗ SLOW - External command
value=$(echo "$string" | wc -c)
# ✓ FAST - Parameter expansion
value=${#string}Use Bash Built-ins
# ✗ SLOW - External commands
basename=$(basename "$path")
dirname=$(dirname "$path")
# ✓ FAST - Parameter expansion
basename="${path##*/}"
dirname="${path%/*}"
# ✗ SLOW - grep
if echo "$string" | grep -q "pattern"; then
# ✓ FAST - Bash regex
if [[ "$string" =~ pattern ]]; then
# ✗ SLOW - awk/cut
field=$(echo "$line" | awk '{print $3}')
# ✓ FAST - Read into array
read -ra fields <<< "$line"
field="${fields[2]}"Efficient Loops
# ✗ SLOW - Running external command in loop
for i in {1..1000}; do
result=$(date +%s)
done
# ✓ FAST - Call once
timestamp=$(date +%s)
for i in {1..1000}; do
result=$timestamp
done
# ✗ SLOW - Multiple passes
cat file | grep pattern | sort | uniq
# ✓ FAST - Single pass where possible
grep pattern file | sort -u---
Documentation
Script Header
#!/usr/bin/env bash
#
# backup.sh - Automated backup script
#
# Description:
# Creates incremental backups of specified directories
# to a remote server using rsync.
#
# Usage:
# backup.sh [OPTIONS] <source> <destination>
#
# Options:
# -h, --help Show this help message
# -v, --verbose Enable verbose output
# -n, --dry-run Show what would be done
# -c, --config FILE Use alternative config file
#
# Arguments:
# source Directory to backup
# destination Remote destination (user@host:/path)
#
# Examples:
# backup.sh /home/user user@backup:/backups/
# backup.sh -v -c custom.conf /data remote:/store/
#
# Dependencies:
# - rsync >= 3.0
# - ssh
#
# Environment Variables:
# BACKUP_CONFIG Path to configuration file
# BACKUP_VERBOSE Enable verbose mode if set
#
# Exit Codes:
# 0 Success
# 1 General error
# 2 Invalid arguments
# 3 Missing dependency
# 4 Backup failed
#
# Author: Your Name <email@example.com>
# Version: 1.2.0
# Date: 2024-01-01
# License: MIT
#Inline Documentation
# Document complex logic
# This algorithm uses binary search to find the optimal value
# Time complexity: O(log n)
# Space complexity: O(1)
# Explain workarounds
# HACK: Sleep needed because API has rate limiting without proper headers
sleep 1
# Document assumptions
# Assumes file is in CSV format with header row
# Link to external resources
# See: https://docs.example.com/api for API documentationREADME and CHANGELOG
Every non-trivial script should have:
1. README.md - Installation, usage, examples 2. CHANGELOG.md - Version history 3. LICENSE - Licensing information
---
Testing
Unit Tests with BATS
# test/backup.bats
#!/usr/bin/env bats
# Setup runs before each test
setup() {
# Create temp directory for tests
TEST_DIR="$(mktemp -d)"
export TEST_DIR
}
# Teardown runs after each test
teardown() {
rm -rf "$TEST_DIR"
}
@test "backup creates archive" {
run ./backup.sh "$TEST_DIR" backup.tar.gz
[ "$status" -eq 0 ]
[ -f backup.tar.gz ]
}
@test "backup fails with invalid source" {
run ./backup.sh /nonexistent backup.tar.gz
[ "$status" -eq 1 ]
[ "${lines[0]}" = "Error: Source directory not found" ]
}
@test "backup validates dependencies" {
# Mock missing dependency
function tar() { return 127; }
export -f tar
run ./backup.sh "$TEST_DIR" backup.tar.gz
[ "$status" -eq 3 ]
}Integration Tests
# integration_test.sh
#!/usr/bin/env bash
set -euo pipefail
# Test end-to-end workflow
test_full_workflow() {
echo "Testing full workflow..."
# Setup
local test_dir="/tmp/test_$$"
mkdir -p "$test_dir"
# Execute
./script.sh create "$test_dir/output"
./script.sh process "$test_dir/output"
./script.sh verify "$test_dir/output"
# Verify
if [[ -f "$test_dir/output/result.txt" ]]; then
echo "✓ Full workflow test passed"
rm -rf "$test_dir"
return 0
else
echo "✗ Full workflow test failed"
rm -rf "$test_dir"
return 1
fi
}
# Run all tests
main() {
local failed=0
test_full_workflow || ((failed++))
if [[ $failed -eq 0 ]]; then
echo "All tests passed"
exit 0
else
echo "$failed test(s) failed"
exit 1
fi
}
main---
Maintenance
Version Control
# Include version in script
readonly VERSION="1.2.0"
show_version() {
echo "$SCRIPT_NAME version $VERSION"
}
# Semantic versioning: MAJOR.MINOR.PATCH
# - MAJOR: Breaking changes
# - MINOR: New features (backward compatible)
# - PATCH: Bug fixesDeprecation
# Deprecation warning
deprecated_function() {
echo "Warning: deprecated_function is deprecated, use new_function instead" >&2
new_function "$@"
}
# Version-based deprecation
if [[ "${SCRIPT_VERSION%%.*}" -ge 2 ]]; then
# Remove deprecated feature in version 2.0
unset deprecated_function
fiBackward Compatibility
# Support old parameter names
if [[ -n "${OLD_PARAM:-}" && -z "${NEW_PARAM:-}" ]]; then
echo "Warning: OLD_PARAM is deprecated, use NEW_PARAM" >&2
NEW_PARAM="$OLD_PARAM"
fi
# Support multiple config file locations
for config in "$XDG_CONFIG_HOME/app/config" "$HOME/.config/app/config" "$HOME/.apprc"; do
if [[ -f "$config" ]]; then
CONFIG_FILE="$config"
break
fi
done---
Summary Checklist
Before considering a bash script production-ready:
- [ ] Passes ShellCheck with no warnings
- [ ] Uses
set -euo pipefail - [ ] All variables quoted properly
- [ ] Functions use local variables
- [ ] Has usage/help message
- [ ] Validates all inputs
- [ ] Checks dependencies
- [ ] Proper error messages (to stderr)
- [ ] Uses meaningful exit codes
- [ ] Includes cleanup trap
- [ ] Has inline documentation
- [ ] Follows consistent style
- [ ] Has unit tests (BATS)
- [ ] Has integration tests
- [ ] Tested on target platforms
- [ ] Has README documentation
- [ ] Version controlled (git)
- [ ] Reviewed by peer
Additional for production:
- [ ] Has CI/CD pipeline
- [ ] Logging implemented
- [ ] Monitoring/alerting configured
- [ ] Security reviewed
- [ ] Performance tested
- [ ] Disaster recovery plan
- [ ] Runbook/operational docs
This ensures professional, maintainable, and robust bash scripts.
Bash in-depth patterns reference
Detailed patterns extracted from SKILL.md to keep the core skill at a navigable size. Covers function design, security, performance, testing, debugging, and advanced patterns. Pair with best_practices.md, patterns_antipatterns.md, and platform_specifics.md for full breadth.
Function Design
# Good function structure
function_name() {
# 1. Local variables first
local arg1="$1"
local arg2="${2:-default_value}"
local result=""
# 2. Input validation
if [[ -z "$arg1" ]]; then
echo "Error: arg1 is required" >&2
return 1
fi
# 3. Main logic
result=$(some_operation "$arg1" "$arg2")
# 4. Output/return
echo "$result"
return 0
}
# Use functions, not scripts-in-scripts
# Benefits: testability, reusability, namespacingVariable Naming
# Constants: UPPER_CASE
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/app/config.conf"
# Global variables: UPPER_CASE or lower_case (be consistent)
GLOBAL_STATE="initialized"
# Local variables: lower_case
local user_name="john"
local file_count=0
# Environment variables: UPPER_CASE (by convention)
export DATABASE_URL="postgres://..."
# Readonly when possible
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"Error Handling
# Method 1: Check exit codes explicitly
if ! command_that_might_fail; then
echo "Error: Command failed" >&2
return 1
fi
# Method 2: Use || for alternative actions
command_that_might_fail || {
echo "Error: Command failed" >&2
return 1
}
# Method 3: Trap for cleanup
cleanup() {
local exit_code=$?
rm -f "$TEMP_FILE"
exit "$exit_code"
}
trap cleanup EXIT
# Method 4: Custom error handler
error_exit() {
local message="$1"
local code="${2:-1}"
echo "Error: $message" >&2
exit "$code"
}
# Usage
[[ -f "$config_file" ]] || error_exit "Config file not found: $config_file"Input Validation
validate_input() {
local input="$1"
if [[ -z "$input" ]]; then
echo "Error: Input cannot be empty" >&2
return 1
fi
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: Input contains invalid characters" >&2
return 1
fi
if [[ ${#input} -gt 255 ]]; then
echo "Error: Input too long (max 255 characters)" >&2
return 1
fi
return 0
}
read -r user_input
if validate_input "$user_input"; then
process "$user_input"
fiArgument Parsing
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <command>
Options:
-h, --help Show this help
-v, --verbose Verbose output
-f, --file FILE Input file
-o, --output DIR Output directory
Commands:
build Build the project
test Run tests
EOF
}
main() {
local verbose=false
local input_file=""
local output_dir="."
local command=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) verbose=true; shift ;;
-f|--file) input_file="$2"; shift 2 ;;
-o|--output) output_dir="$2"; shift 2 ;;
-*)
echo "Error: Unknown option: $1" >&2
usage >&2; exit 1 ;;
*) command="$1"; shift; break ;;
esac
done
if [[ -z "$command" ]]; then
echo "Error: Command is required" >&2
usage >&2; exit 1
fi
case "$command" in
build) do_build ;;
test) do_test ;;
*)
echo "Error: Unknown command: $command" >&2
usage >&2; exit 1 ;;
esac
}
main "$@"Logging
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO}
log_debug() { [[ $LOG_LEVEL -le $LOG_LEVEL_DEBUG ]] && echo "[DEBUG] $*" >&2; }
log_info() { [[ $LOG_LEVEL -le $LOG_LEVEL_INFO ]] && echo "[INFO] $*" >&2; }
log_warn() { [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]] && echo "[WARN] $*" >&2; }
log_error() { [[ $LOG_LEVEL -le $LOG_LEVEL_ERROR ]] && echo "[ERROR] $*" >&2; }
log_with_timestamp() {
local level="$1"
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}
log_info "Starting process"
log_error "Failed to connect to database"Security
Command Injection Prevention
# NEVER use eval with user input - DANGEROUS
eval "$user_input" # WRONG
eval "var_$user_input=value" # WRONG
# NEVER concatenate user input into commands
grep "$user_pattern" file.txt # If pattern contains -e flag, injection possible
# Use arrays
grep_args=("$user_pattern" "file.txt")
grep "${grep_args[@]}"
# Use -- to separate options from arguments
grep -- "$user_pattern" file.txtPath Traversal Prevention
sanitize_path() {
local path="$1"
path="${path//..\/}"
path="${path//\/..\//}"
path="${path#/}"
echo "$path"
}
is_safe_path() {
local file_path="$1"
local base_dir="$2"
local real_path
real_path=$(readlink -f "$file_path" 2>/dev/null) || return 1
local real_base
real_base=$(readlink -f "$base_dir" 2>/dev/null) || return 1
[[ "$real_path" == "$real_base"/* ]]
}
if is_safe_path "$user_file" "/var/app/data"; then
process_file "$user_file"
else
echo "Error: Invalid file path" >&2
exit 1
fiPrivilege Management
# Refuse to run as root
if [[ $EUID -eq 0 ]]; then
echo "Error: Do not run this script as root" >&2
exit 1
fi
drop_privileges() {
local user="$1"
if [[ $EUID -eq 0 ]]; then
exec sudo -u "$user" "$0" "$@"
fi
}
run_as_root() {
if [[ $EUID -ne 0 ]]; then
sudo "$@"
else
"$@"
fi
}Temporary File Handling
readonly TEMP_DIR=$(mktemp -d)
readonly TEMP_FILE=$(mktemp)
cleanup() {
rm -rf "$TEMP_DIR"
rm -f "$TEMP_FILE"
}
trap cleanup EXIT
# Secure temporary file (owner-only)
secure_temp=$(mktemp)
chmod 600 "$secure_temp"Performance Optimization
Avoid Unnecessary Subshells
# SLOW - subshell per iteration
while IFS= read -r line; do
count=$(echo "$count + 1" | bc)
done < file.txt
# FAST - bash arithmetic
count=0
while IFS= read -r line; do
((count++))
done < file.txtUse Bash Built-ins
# Slow: external commands
length=$(echo "$string" | wc -c)
upper=$(echo "$string" | tr '[:lower:]' '[:upper:]')
# Fast: bash built-ins
length=${#string}
upper=${string^^}
# String contains check
if [[ "$haystack" == *"$needle"* ]]; then
echo "Found"
fiProcess Substitution vs Pipes
# Pipes start a subshell - variables don't persist
count=0
echo "data" | while read -r line; do
((count++)) # changes lost in subshell
done
# Process substitution keeps the current shell
count=0
while read -r line; do
((count++))
done < <(echo "data")Array Operations
arr=("one" "two" "three")
length=${#arr[@]}
last_index=$((${#arr[@]} - 1))
arr+=("four")
unset 'arr[1]' # remove by index
arr=("${arr[@]}") # reindex after unset
# Iterate
for item in "${arr[@]}"; do
echo "$item"
done
# Iterate with index
for i in "${!arr[@]}"; do
echo "$i: ${arr[$i]}"
doneTesting
Unit Testing with BATS
# test/script.bats
#!/usr/bin/env bats
load '../script.sh'
@test "function returns correct value" {
result=$(my_function "input")
[ "$result" = "expected" ]
}
@test "function handles empty input" {
run my_function ""
[ "$status" -eq 1 ]
[ "${lines[0]}" = "Error: Input cannot be empty" ]
}
@test "function validates input format" {
run my_function "invalid@input"
[ "$status" -eq 1 ]
}
# Run: bats test/script.batsIntegration Testing
# integration_test.sh
#!/usr/bin/env bash
set -euo pipefail
setup() {
export TEST_DIR=$(mktemp -d)
export TEST_FILE="$TEST_DIR/test.txt"
}
teardown() {
rm -rf "$TEST_DIR"
}
test_file_creation() {
./script.sh create "$TEST_FILE"
if [[ ! -f "$TEST_FILE" ]]; then
echo "FAIL: File was not created"
return 1
fi
echo "PASS: File creation works"
}
main() {
setup
trap teardown EXIT
test_file_creation || exit 1
echo "All tests passed"
}
mainCI/CD Integration
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install shellcheck
run: sudo apt-get install -y shellcheck
- name: Run shellcheck
run: find . -name "*.sh" -exec shellcheck {} +
- name: Install bats
run: |
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local
- name: Run tests
run: bats test/Debugging Techniques
Debug Mode
# Method 1: set -x
set -x
command1
command2
set +x
# Method 2: PS4 for richer trace output
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
# Method 3: Conditional debugging
DEBUG=${DEBUG:-false}
debug() {
if [[ "$DEBUG" == "true" ]]; then
echo "[DEBUG] $*" >&2
fi
}
# Usage: DEBUG=true ./script.shTracing and Profiling
trace() {
echo "[TRACE] Function: ${FUNCNAME[1]}, Args: $*" >&2
}
my_function() {
trace "$@"
# Function logic
}
profile() {
local start=$(date +%s%N)
"$@"
local end=$(date +%s%N)
local duration=$(( (end - start) / 1000000 ))
echo "[PROFILE] Command '$*' took ${duration}ms" >&2
}
# Usage
profile slow_command arg1 arg2Common Issues and Solutions
# Script works in bash but not in sh - check bashisms
checkbashisms script.sh
# Works locally but not on server - check PATH/env
env
echo "$PATH"
# Whitespace in filenames breaking script - always quote
for file in *.txt; do
process "$file" # not: process $file
done
# Different behavior in cron - set PATH explicitly
PATH=/usr/local/bin:/usr/bin:/bin
export PATHAdvanced Patterns
Configuration File Parsing
# Simple sourcing (dangerous if file not trusted)
load_config() {
local config_file="$1"
if [[ ! -f "$config_file" ]]; then
echo "Error: Config file not found: $config_file" >&2
return 1
fi
# shellcheck source=/dev/null
source "$config_file"
}
# Safe parsing - no code execution
read_config() {
local config_file="$1"
while IFS='=' read -r key value; do
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
key=$(echo "$key" | tr -d ' ')
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
declare -g "$key=$value"
done < "$config_file"
}Parallel Processing
process_files_parallel() {
local max_jobs=4
local job_count=0
for file in *.txt; do
process_file "$file" &
((job_count++))
if [[ $job_count -ge $max_jobs ]]; then
wait -n
((job_count--))
fi
done
wait
}
# Using GNU Parallel
parallel_with_gnu() {
parallel -j 4 process_file ::: *.txt
}Signal Handling
shutdown_requested=false
handle_sigterm() {
echo "Received SIGTERM, shutting down gracefully..." >&2
shutdown_requested=true
}
trap handle_sigterm SIGTERM SIGINT
main_loop() {
while [[ "$shutdown_requested" == "false" ]]; do
sleep 1
done
echo "Shutdown complete" >&2
}
main_loopRetries with Exponential Backoff
retry_with_backoff() {
local max_attempts=5
local timeout=1
local attempt=1
local exitCode=0
while [[ $attempt -le $max_attempts ]]; do
if "$@"; then
return 0
else
exitCode=$?
fi
echo "Attempt $attempt failed! Retrying in $timeout seconds..." >&2
sleep "$timeout"
attempt=$((attempt + 1))
timeout=$((timeout * 2))
done
echo "Command failed after $max_attempts attempts!" >&2
return "$exitCode"
}
# Usage
retry_with_backoff curl -f https://api.example.com/healthCommon Bash Patterns and Anti-Patterns
Collection of proven patterns and common mistakes in bash scripting with explanations and solutions.
---
Table of Contents
1. Variable Handling 2. Command Execution 3. File Operations 4. String Processing 5. Arrays and Loops 6. Conditionals and Tests 7. Functions 8. Error Handling 9. Process Management 10. Security Patterns
---
🚨 CRITICAL GUIDELINES
Windows File Path Requirements
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/).
Examples:
- ❌ WRONG:
D:/repos/project/file.tsx - ✅ CORRECT:
D:\repos\project\file.tsx
This applies to:
- Edit tool file_path parameter
- Write tool file_path parameter
- All file operations on Windows systems
Documentation Guidelines
NEVER create new documentation files unless explicitly requested by the user.
- Priority: Update existing README.md files rather than creating new documentation
- Repository cleanliness: Keep repository root clean - only README.md unless user requests otherwise
- Style: Documentation should be concise, direct, and professional - avoid AI-generated tone
- User preference: Only create additional .md files when user specifically asks for documentation
---
Variable Handling
Pattern: Safe Variable Expansion
# ✓ GOOD: Always quote variables
echo "$variable"
cp "$source" "$destination"
rm -rf "$directory"
# ✗ BAD: Unquoted variables
echo $variable # Word splitting and globbing
cp $source $destination # Breaks with spaces
rm -rf $directory # VERY DANGEROUS unquotedWhy: Unquoted variables undergo word splitting and pathname expansion, leading to unexpected behavior.
Pattern: Default Values
# ✓ GOOD: Use parameter expansion for defaults
timeout="${TIMEOUT:-30}"
config="${CONFIG_FILE:-$HOME/.config/app.conf}"
# ✗ BAD: Manual check
if [ -z "$TIMEOUT" ]; then
timeout=30
else
timeout="$TIMEOUT"
fiWhy: Parameter expansion is concise, readable, and handles edge cases correctly.
Anti-Pattern: Confusing Assignment and Comparison
# ✗ VERY BAD: Using = instead of ==
if [ "$var" = "value" ]; then # Assignment in POSIX test!
echo "Match"
fi
# ✓ GOOD: Use == or = correctly
if [[ "$var" == "value" ]]; then # Comparison in bash
echo "Match"
fi
# ✓ GOOD: POSIX-compliant
if [ "$var" = "value" ]; then # Single = is correct in [ ]
echo "Match"
fiWhy: In [[ ]], both = and == work. In [ ], only = is POSIX-compliant.
Anti-Pattern: Unset Variable Access
# ✗ BAD: Accessing undefined variables
echo "Value: $undefined_variable" # Silent error, prints "Value: "
# ✓ GOOD: Use set -u
set -u
echo "Value: $undefined_variable" # Error: undefined_variable: unbound variable
# ✓ GOOD: Provide default
echo "Value: ${undefined_variable:-default}"Why: set -u catches typos and logic errors early.
---
Command Execution
Pattern: Check Command Existence
# ✓ GOOD: Use command -v
if command -v jq &> /dev/null; then
echo "jq is installed"
else
echo "jq is not installed" >&2
exit 1
fi
# ✗ BAD: Using which
if which jq; then # Deprecated, not POSIX
echo "jq is installed"
fi
# ✗ BAD: Using type
if type jq; then # Verbose output
echo "jq is installed"
fiWhy: command -v is POSIX-compliant, silent, and reliable.
Pattern: Command Substitution
# ✓ GOOD: Modern syntax with $()
result=$(command arg1 arg2)
timestamp=$(date +%s)
# ✗ BAD: Backticks (hard to nest)
result=`command arg1 arg2`
timestamp=`date +%s`
# ✓ GOOD: Nested substitution
result=$(echo "Outer: $(echo "Inner")")
# ✗ BAD: Nested backticks (requires escaping)
result=`echo "Outer: \`echo \"Inner\"\`"`Why: $() is easier to read, nest, and maintain.
Anti-Pattern: Useless Use of Cat
# ✗ BAD: UUOC (Useless Use of Cat)
cat file.txt | grep "pattern"
# ✓ GOOD: Direct input
grep "pattern" file.txt
# ✗ BAD: Multiple cats
cat file1 | grep pattern | cat | sort | cat
# ✓ GOOD: Direct pipeline
grep pattern file1 | sortWhy: Unnecessary cat wastes resources and adds extra processes.
Anti-Pattern: Using ls in Scripts
# ✗ BAD: Parsing ls output
for file in $(ls *.txt); do
echo "$file"
done
# ✓ GOOD: Use globbing
for file in *.txt; do
[[ -f "$file" ]] || continue # Skip if no matches
echo "$file"
done
# ✗ BAD: Counting files with ls
count=$(ls -1 | wc -l)
# ✓ GOOD: Use array
files=(*)
count=${#files[@]}Why: ls output is meant for humans, not scripts. Parsing it breaks with spaces, newlines, etc.
---
File Operations
Pattern: Safe File Reading
# ✓ GOOD: Preserve leading/trailing whitespace and backslashes
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# ✗ BAD: Without IFS= (strips leading/trailing whitespace)
while read -r line; do
echo "Line: $line"
done < file.txt
# ✗ BAD: Without -r (interprets backslashes)
while IFS= read line; do
echo "Line: $line"
done < file.txtWhy: IFS= prevents trimming, -r prevents backslash interpretation.
Pattern: Null-Delimited Files
# ✓ GOOD: For filenames with special characters
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
echo "Processing: $file"
done
# Or with mapfile (bash 4+)
mapfile -d '' -t files < <(find . -name "*.txt" -print0)
for file in "${files[@]}"; do
echo "Processing: $file"
done
# ✗ BAD: Newline-delimited (breaks with newlines in filenames)
find . -name "*.txt" | while IFS= read -r file; do
echo "Processing: $file"
doneWhy: Filenames can contain any character except null and slash.
Anti-Pattern: Testing File Existence Incorrectly
# ✗ BAD: Using ls to test existence
if ls file.txt &> /dev/null; then
echo "File exists"
fi
# ✓ GOOD: Use test operators
if [[ -f file.txt ]]; then
echo "File exists"
fi
# ✓ GOOD: Different tests
[[ -e path ]] # Exists (file or directory)
[[ -f file ]] # Regular file
[[ -d dir ]] # Directory
[[ -L link ]] # Symbolic link
[[ -r file ]] # Readable
[[ -w file ]] # Writable
[[ -x file ]] # ExecutableWhy: Test operators are the correct, efficient way to check file properties.
Pattern: Temporary Files
# ✓ GOOD: Secure temporary file
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT
# Use temp file
echo "data" > "$temp_file"
# ✗ BAD: Insecure temp file
temp_file="/tmp/myapp.$$"
echo "data" > "$temp_file"
# No cleanup!
# ✓ GOOD: Temporary directory
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXITWhy: mktemp creates secure, unique files and prevents race conditions.
---
String Processing
Pattern: String Manipulation with Parameter Expansion
# ✓ GOOD: Use bash parameter expansion
filename="document.tar.gz"
basename="${filename%%.*}" # document
extension="${filename##*.}" # gz
name="${filename%.gz}" # document.tar
# ✗ BAD: Using external commands
basename=$(echo "$filename" | sed 's/\..*$//')
extension=$(echo "$filename" | awk -F. '{print $NF}')Why: Parameter expansion is faster and doesn't spawn processes.
Pattern: String Comparison
# ✓ GOOD: Use [[ ]] for strings
if [[ "$string1" == "$string2" ]]; then
echo "Equal"
fi
# ✓ GOOD: Pattern matching
if [[ "$filename" == *.txt ]]; then
echo "Text file"
fi
# ✓ GOOD: Regex matching
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
fi
# ✗ BAD: Using grep for simple string check
if echo "$string" | grep -q "substring"; then
echo "Found"
fi
# ✓ GOOD: Use substring matching
if [[ "$string" == *"substring"* ]]; then
echo "Found"
fiWhy: [[ ]] is bash-native, faster, and more readable.
Anti-Pattern: Word Splitting Issues
# ✗ BAD: Unquoted expansion with spaces
var="file1.txt file2.txt"
for file in $var; do # Splits on spaces!
echo "$file" # file1.txt, then file2.txt
done
# ✓ GOOD: Use array
files=("file1.txt" "file2.txt")
for file in "${files[@]}"; do
echo "$file"
done
# ✗ BAD: Word splitting in command arguments
file="my file.txt"
rm $file # Tries to remove "my" and "file.txt"!
# ✓ GOOD: Quote variables
rm "$file"Why: Word splitting on spaces is a major source of bugs.
---
Arrays and Loops
Pattern: Array Declaration and Use
# ✓ GOOD: Array declaration
files=("file1.txt" "file2.txt" "file 3.txt")
# ✓ GOOD: Array expansion (each element quoted)
for file in "${files[@]}"; do
echo "$file"
done
# ✗ BAD: Unquoted array expansion
for file in ${files[@]}; do # Word splitting!
echo "$file"
done
# ✓ GOOD: Add to array
files+=("file4.txt")
# ✓ GOOD: Array length
echo "Count: ${#files[@]}"
# ✓ GOOD: Array indices
for i in "${!files[@]}"; do
echo "File $i: ${files[$i]}"
doneWhy: Proper array handling prevents word splitting and globbing issues.
Pattern: Reading Command Output into Array
# ✓ GOOD: mapfile/readarray (bash 4+)
mapfile -t lines < file.txt
# ✓ GOOD: With command substitution
mapfile -t files < <(find . -name "*.txt")
# ✗ BAD: Word splitting
files=($(find . -name "*.txt")) # Breaks with spaces in filenames!
# ✓ GOOD: Alternative (POSIX-compatible)
while IFS= read -r file; do
files+=("$file")
done < <(find . -name "*.txt")Why: mapfile is efficient and handles special characters correctly.
Anti-Pattern: C-Style For Loops for Arrays
# ✗ BAD: C-style loop for arrays
for ((i=0; i<${#files[@]}; i++)); do
echo "${files[$i]}"
done
# ✓ GOOD: For-in loop
for file in "${files[@]}"; do
echo "$file"
done
# ✓ ACCEPTABLE: When you need the index
for i in "${!files[@]}"; do
echo "Index $i: ${files[$i]}"
doneWhy: For-in loops are simpler and less error-prone.
Pattern: Loop over Range
# ✓ GOOD: Brace expansion
for i in {1..10}; do
echo "$i"
done
# ✓ GOOD: With variables (bash 4+)
start=1
end=10
for i in $(seq $start $end); do
echo "$i"
done
# ✓ GOOD: C-style (arithmetic)
for ((i=1; i<=10; i++)); do
echo "$i"
done
# ✗ BAD: Using seq in a loop unnecessarily
for i in $(seq 1 1000000); do # Creates huge string in memory!
echo "$i"
done
# ✓ GOOD: Use C-style for large ranges
for ((i=1; i<=1000000; i++)); do
echo "$i"
doneWhy: Choose the right loop construct based on the use case.
---
Conditionals and Tests
Pattern: File Tests
# ✓ GOOD: Use appropriate test
if [[ -f "$file" ]]; then # Regular file
if [[ -d "$dir" ]]; then # Directory
if [[ -e "$path" ]]; then # Exists (any type)
if [[ -L "$link" ]]; then # Symbolic link
if [[ -r "$file" ]]; then # Readable
if [[ -w "$file" ]]; then # Writable
if [[ -x "$file" ]]; then # Executable
if [[ -s "$file" ]]; then # Non-empty file
# ✗ BAD: Incorrect test
if [[ -e "$file" ]]; then # Exists, but could be directory!
cat "$file" # Fails if directory
fi
# ✓ GOOD: Specific test
if [[ -f "$file" ]]; then
cat "$file"
fiWhy: Use the most specific test for your use case.
Pattern: Numeric Comparison
# ✓ GOOD: Arithmetic context
if (( num > 10 )); then
echo "Greater than 10"
fi
# ✓ GOOD: Test operator
if [[ $num -gt 10 ]]; then
echo "Greater than 10"
fi
# ✗ BAD: String comparison for numbers
if [[ "$num" > "10" ]]; then # Lexicographic comparison!
echo "Greater than 10" # "9" > "10" is true!
fiWhy: Use numeric comparison operators for numbers.
Anti-Pattern: Testing Boolean Strings
# ✗ BAD: Comparing to string "true"
if [[ "$flag" == "true" ]]; then
do_something
fi
# ✓ GOOD: Use boolean variable directly
flag=false # or true
if $flag; then
do_something
fi
# ✓ BETTER: Use integers for flags
flag=0 # false
flag=1 # true
if (( flag )); then
do_something
fi
# ✓ GOOD: For command success/failure
if command; then
echo "Success"
fiWhy: Boolean strings are error-prone; use actual booleans or return codes.
Pattern: Multiple Conditions
# ✓ GOOD: Logical operators
if [[ condition1 && condition2 ]]; then
echo "Both true"
fi
if [[ condition1 || condition2 ]]; then
echo "At least one true"
fi
if [[ ! condition ]]; then
echo "False"
fi
# ✗ BAD: Separate tests
if [ condition1 -a condition2 ]; then # Deprecated
echo "Both true"
fi
# ✗ BAD: Nested ifs for AND
if [[ condition1 ]]; then
if [[ condition2 ]]; then
echo "Both true"
fi
fiWhy: && and || in [[ ]] are clearer and recommended.
---
Functions
Pattern: Function Return Values
# ✓ GOOD: Return status, output to stdout
get_value() {
local value="result"
if [[ -n "$value" ]]; then
echo "$value"
return 0
else
return 1
fi
}
# Usage
if result=$(get_value); then
echo "Got: $result"
else
echo "Failed"
fi
# ✗ BAD: Using return for data
get_value() {
return 42 # Can only return 0-255!
}
result=$? # Gets 42, but limited rangeWhy: return is for exit status (0-255), not data. Output to stdout for data.
Pattern: Local Variables in Functions
# ✓ GOOD: Declare local variables
my_function() {
local arg="$1"
local result=""
result=$(process "$arg")
echo "$result"
}
# ✗ BAD: Global variables
my_function() {
arg="$1" # Pollutes global namespace!
result="" # Global variable!
result=$(process "$arg")
echo "$result"
}Why: Local variables prevent unexpected side effects.
Anti-Pattern: Capturing Local Command Failure
# ✗ BAD: Local declaration masks command failure
my_function() {
local result=$(command_that_fails) # $? is from 'local', not 'command'!
echo "$result"
}
# ✓ GOOD: Separate declaration and assignment
my_function() {
local result
result=$(command_that_fails) || return 1
echo "$result"
}
# ✓ GOOD: Check command separately
my_function() {
local result
if ! result=$(command_that_fails); then
return 1
fi
echo "$result"
}Why: Combining local and command substitution hides command failure.
---
Error Handling
Pattern: Check Command Success
# ✓ GOOD: Direct check
if ! command; then
echo "Command failed" >&2
exit 1
fi
# ✓ GOOD: With logical operator
command || {
echo "Command failed" >&2
exit 1
}
# ✓ GOOD: Capture output and check
if ! output=$(command 2>&1); then
echo "Command failed: $output" >&2
exit 1
fi
# ✗ BAD: Not checking status
command # What if it fails?
next_commandWhy: Always check if commands succeed unless failure is acceptable.
Pattern: Error Messages to stderr
# ✓ GOOD: Errors to stderr
echo "Error: Invalid argument" >&2
# ✗ BAD: Errors to stdout
echo "Error: Invalid argument"
# ✓ GOOD: Error function
error() {
echo "ERROR: $*" >&2
}
error "Something went wrong"Why: stderr is for errors, stdout is for data output.
Pattern: Cleanup on Exit
# ✓ GOOD: Trap for cleanup
temp_file=$(mktemp)
cleanup() {
rm -f "$temp_file"
}
trap cleanup EXIT
# Do work with temp_file
# ✗ BAD: Manual cleanup (might not run)
temp_file=$(mktemp)
# Do work
rm -f "$temp_file" # Doesn't run if script exits early!Why: Trap ensures cleanup runs on exit, even on errors.
Anti-Pattern: Silencing Errors
# ✗ BAD: Silencing errors
command 2>/dev/null # What if it fails?
next_command
# ✓ GOOD: Check status even if silencing output
if ! command 2>/dev/null; then
echo "Command failed" >&2
exit 1
fi
# ✓ ACCEPTABLE: When failure is expected and acceptable
if command 2>/dev/null; then
echo "Command succeeded"
else
echo "Command failed (expected)"
fiWhy: Silencing errors without checking status leads to silent failures.
---
Process Management
Pattern: Background Jobs
# ✓ GOOD: Track background jobs
long_running_task &
pid=$!
# Wait for completion
if wait "$pid"; then
echo "Task completed successfully"
else
echo "Task failed" >&2
fi
# ✓ GOOD: Multiple background jobs
job1 &
pid1=$!
job2 &
pid2=$!
wait "$pid1" "$pid2"Why: Proper job management prevents zombie processes.
Pattern: Timeout for Commands
# ✓ GOOD: Use timeout command (if available)
if timeout 30 long_running_command; then
echo "Completed within timeout"
else
echo "Timed out or failed" >&2
fi
# ✓ GOOD: Manual timeout implementation
timeout_command() {
local timeout=$1
shift
"$@" &
local pid=$!
( sleep "$timeout"; kill "$pid" 2>/dev/null ) &
local killer=$!
if wait "$pid" 2>/dev/null; then
kill "$killer" 2>/dev/null
wait "$killer" 2>/dev/null
return 0
else
return 1
fi
}
timeout_command 30 long_running_commandWhy: Prevents scripts from hanging indefinitely.
Anti-Pattern: Killing Processes Unsafely
# ✗ BAD: kill -9 immediately
kill -9 "$pid"
# ✓ GOOD: Graceful shutdown first
kill -TERM "$pid"
sleep 2
if kill -0 "$pid" 2>/dev/null; then
echo "Process still running, forcing..." >&2
kill -KILL "$pid"
fi
# ✓ GOOD: With timeout
graceful_kill() {
local pid=$1
local timeout=${2:-10}
kill -TERM "$pid" 2>/dev/null || return 0
for ((i=0; i<timeout; i++)); do
if ! kill -0 "$pid" 2>/dev/null; then
return 0
fi
sleep 1
done
echo "Forcing kill of $pid" >&2
kill -KILL "$pid" 2>/dev/null
}Why: SIGTERM allows graceful shutdown; SIGKILL should be last resort.
---
Security Patterns
Pattern: Input Validation
# ✓ GOOD: Whitelist validation
validate_action() {
local action=$1
case "$action" in
start|stop|restart|status)
return 0
;;
*)
echo "Error: Invalid action: $action" >&2
return 1
;;
esac
}
# ✗ BAD: No validation
action="$1"
systemctl "$action" myservice # User can pass arbitrary commands!
# ✓ GOOD: Validate first
if validate_action "$1"; then
systemctl "$1" myservice
else
exit 1
fiWhy: Whitelist validation prevents command injection.
Pattern: Avoid eval
# ✗ BAD: eval with user input
eval "$user_command" # DANGEROUS!
# ✓ GOOD: Use arrays
command_args=("$arg1" "$arg2" "$arg3")
command "${command_args[@]}"
# ✗ BAD: Dynamic variable names
eval "var_$name=value"
# ✓ GOOD: Associative arrays (bash 4+)
declare -A vars
vars[$name]="value"Why: eval with user input is a security vulnerability.
Pattern: Safe PATH
# ✓ GOOD: Set explicit PATH
export PATH="/usr/local/bin:/usr/bin:/bin"
# ✓ GOOD: Use absolute paths for critical commands
/usr/bin/rm -rf "$directory"
# ✗ BAD: Trusting user's PATH
rm -rf "$directory" # What if there's a malicious 'rm' in PATH?Why: Prevents PATH injection attacks.
---
Summary
Most Critical Patterns:
1. Always quote variable expansions: "$var" 2. Use set -euo pipefail for safety 3. Prefer [[ ]] over [ ] in bash 4. Use arrays for lists: "${array[@]}" 5. Check command success: if ! command; then 6. Use local variables in functions 7. Errors to stderr: echo "Error" >&2 8. Use mktemp for temporary files 9. Cleanup with traps: trap cleanup EXIT 10. Validate all user input
Most Dangerous Anti-Patterns:
1. Unquoted variables: $var 2. Parsing ls output 3. Using eval with user input 4. Silencing errors without checking 5. Not using set -u or defaults 6. Global variables in functions 7. Word splitting on filenames 8. Testing strings with > for numbers 9. kill -9 without trying graceful shutdown 10. Trusting user PATH
Following these patterns and avoiding anti-patterns will result in robust, secure, and maintainable bash scripts.
Platform-Specific Bash Scripting
Comprehensive guide to handling platform differences in bash scripts across Linux, macOS, Windows (Git Bash/WSL), and containers.
---
⚠️ WINDOWS GIT BASH / MINGW PATH CONVERSION
CRITICAL REFERENCE: For complete Windows Git Bash path conversion and shell detection guidance, see:
📄 [windows-git-bash-paths.md](./windows-git-bash-paths.md)
This comprehensive guide covers:
- Automatic path conversion behavior (Unix → Windows)
- MSYS_NO_PATHCONV and MSYS2_ARG_CONV_EXCL usage
- cygpath manual conversion tool
- Shell detection methods ($OSTYPE, uname, $MSYSTEM)
- Claude Code specific issues (#2602 snapshot path conversion)
- Common problems and solutions
- Cross-platform scripting patterns
Git Bash path conversion is the #1 source of Windows bash scripting issues. Always consult the dedicated guide when working with Windows/Git Bash.
---
WARNING: WINDOWS GIT BASH / MINGW PATH CONVERSION
CRITICAL REFERENCE: For complete Windows Git Bash path conversion and shell detection guidance, see:
[windows-git-bash-paths.md](./windows-git-bash-paths.md)
This comprehensive guide covers:
- Automatic path conversion behavior (Unix to Windows)
- MSYS_NO_PATHCONV and MSYS2_ARG_CONV_EXCL usage
- cygpath manual conversion tool
- Shell detection methods ($OSTYPE, uname, $MSYSTEM)
- Claude Code specific issues (#2602 snapshot path conversion)
- Common problems and solutions
- Cross-platform scripting patterns
Git Bash path conversion is the #1 source of Windows bash scripting issues. Always consult the dedicated guide when working with Windows/Git Bash.
---
Table of Contents
1. Platform Detection 2. Linux Specifics 3. macOS Specifics 4. Windows (Git Bash) - See windows-git-bash-paths.md for complete guide - See windows-git-bash-paths.md for complete guide 5. Windows (WSL) 6. Container Environments 7. Cross-Platform Patterns 8. Command Compatibility Matrix
---
🚨 CRITICAL GUIDELINES
Windows File Path Requirements
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/).
Examples:
- ❌ WRONG:
D:/repos/project/file.tsx - ✅ CORRECT:
D:\repos\project\file.tsx
This applies to:
- Edit tool file_path parameter
- Write tool file_path parameter
- All file operations on Windows systems
Documentation Guidelines
NEVER create new documentation files unless explicitly requested by the user.
- Priority: Update existing README.md files rather than creating new documentation
- Repository cleanliness: Keep repository root clean - only README.md unless user requests otherwise
- Style: Documentation should be concise, direct, and professional - avoid AI-generated tone
- User preference: Only create additional .md files when user specifically asks for documentation
---
Platform Detection
Comprehensive Detection Script
#!/usr/bin/env bash
detect_os() {
case "$OSTYPE" in
linux-gnu*)
if grep -qi microsoft /proc/version 2>/dev/null; then
echo "wsl"
else
echo "linux"
fi
;;
darwin*)
echo "macos"
;;
msys*|mingw*|cygwin*)
echo "gitbash"
;;
*)
echo "unknown"
;;
esac
}
detect_distro() {
# Only for Linux
if [[ -f /etc/os-release ]]; then
# shellcheck source=/dev/null
source /etc/os-release
echo "$ID"
elif [[ -f /etc/redhat-release ]]; then
echo "rhel"
elif [[ -f /etc/debian_version ]]; then
echo "debian"
else
echo "unknown"
fi
}
detect_container() {
if [[ -f /.dockerenv ]]; then
echo "docker"
elif grep -q docker /proc/1/cgroup 2>/dev/null; then
echo "docker"
elif [[ -n "$KUBERNETES_SERVICE_HOST" ]]; then
echo "kubernetes"
else
echo "none"
fi
}
# Usage
OS=$(detect_os)
DISTRO=$(detect_distro)
CONTAINER=$(detect_container)
echo "OS: $OS"
echo "Distro: $DISTRO"
echo "Container: $CONTAINER"Environment Variables for Detection
# Check various environment indicators
check_environment() {
echo "OSTYPE: $OSTYPE"
echo "MACHTYPE: $MACHTYPE"
echo "HOSTTYPE: $HOSTTYPE"
# Kernel info
uname -s # Operating system name
uname -r # Kernel release
uname -m # Machine hardware
uname -p # Processor type
# More detailed
uname -a # All information
}
# Platform-specific variables
# Linux: OSTYPE=linux-gnu
# macOS: OSTYPE=darwin20.0
# Git Bash: OSTYPE=msys
# Cygwin: OSTYPE=cygwin
# WSL: OSTYPE=linux-gnu (but with Microsoft in /proc/version)---
Linux Specifics
Linux-Only Features
# /proc filesystem
get_process_info() {
local pid=$1
if [[ -d "/proc/$pid" ]]; then
echo "Command: $(cat /proc/$pid/cmdline | tr '\0' ' ')"
echo "Working dir: $(readlink /proc/$pid/cwd)"
echo "Executable: $(readlink /proc/$pid/exe)"
fi
}
# systemd
check_systemd() {
if command -v systemctl &> /dev/null; then
systemctl status my-service
systemctl is-active my-service
systemctl is-enabled my-service
fi
}
# cgroups
check_cgroups() {
if [[ -d /sys/fs/cgroup ]]; then
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
fi
}
# inotify for file watching
watch_directory() {
if command -v inotifywait &> /dev/null; then
inotifywait -m -r -e modify,create,delete /path/to/watch
fi
}Distribution-Specific Commands
# Package management
install_package() {
local package=$1
if command -v apt-get &> /dev/null; then
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y "$package"
elif command -v yum &> /dev/null; then
# RHEL/CentOS
sudo yum install -y "$package"
elif command -v dnf &> /dev/null; then
# Fedora
sudo dnf install -y "$package"
elif command -v pacman &> /dev/null; then
# Arch
sudo pacman -S --noconfirm "$package"
elif command -v zypper &> /dev/null; then
# openSUSE
sudo zypper install -y "$package"
elif command -v apk &> /dev/null; then
# Alpine
sudo apk add "$package"
else
echo "Error: No supported package manager found" >&2
return 1
fi
}
# Service management
manage_service() {
local action=$1
local service=$2
if command -v systemctl &> /dev/null; then
# systemd (most modern distros)
sudo systemctl "$action" "$service"
elif command -v service &> /dev/null; then
# SysV init
sudo service "$service" "$action"
else
echo "Error: No supported service manager found" >&2
return 1
fi
}GNU Coreutils (Linux Standard)
# GNU-specific features
# These work on Linux but may not work on macOS/BSD
# sed with -i (in-place editing)
sed -i 's/old/new/g' file.txt # Linux
sed -i '' 's/old/new/g' file.txt # macOS requires empty string
# date with flexible parsing
date -d "yesterday" +%Y-%m-%d # Linux
date -v-1d +%Y-%m-%d # macOS
# stat with -c format
stat -c "%s" file.txt # Linux (file size)
stat -f "%z" file.txt # macOS
# readlink with -f (canonicalize)
readlink -f /path/to/file # Linux
# macOS doesn't have -f, use greadlink or:
python -c "import os; print(os.path.realpath('$file'))"
# GNU find with -printf
find . -type f -printf "%p %s\n" # Linux
find . -type f -exec stat -f "%N %z" {} \; # macOS---
macOS Specifics
BSD vs GNU Commands
# Detect and use GNU versions if available
setup_commands_macos() {
# Install GNU commands: brew install coreutils gnu-sed gnu-tar findutils
if command -v gsed &> /dev/null; then
SED=gsed
else
SED=sed
fi
if command -v ggrep &> /dev/null; then
GREP=ggrep
else
GREP=grep
fi
if command -v greadlink &> /dev/null; then
READLINK=greadlink
else
READLINK=readlink
fi
if command -v gdate &> /dev/null; then
DATE=gdate
else
DATE=date
fi
if command -v gstat &> /dev/null; then
STAT=gstat
else
STAT=stat
fi
export SED GREP READLINK DATE STAT
}
# Usage
setup_commands_macos
$SED -i 's/old/new/g' file.txt # Works on both platformsmacOS-Specific Features
# macOS filesystem (case-insensitive by default on APFS/HFS+)
check_case_sensitivity() {
touch /tmp/test_case
if [[ -f /tmp/TEST_CASE ]]; then
echo "Filesystem is case-insensitive"
else
echo "Filesystem is case-sensitive"
fi
rm -f /tmp/test_case /tmp/TEST_CASE
}
# macOS extended attributes
# Set extended attribute
xattr -w com.example.myattr "value" file.txt
# Get extended attribute
xattr -p com.example.myattr file.txt
# List all extended attributes
xattr -l file.txt
# Remove extended attribute
xattr -d com.example.myattr file.txt
# macOS Spotlight
# Disable indexing for directory
mdutil -i off /path/to/directory
# Search with mdfind (Spotlight from command line)
mdfind "kMDItemFSName == 'filename.txt'"
# macOS clipboard
# Copy to clipboard
echo "text" | pbcopy
# Paste from clipboard
pbpaste
# macOS notifications
# Display notification
osascript -e 'display notification "Build complete" with title "Build Status"'
# macOS open command
# Open file with default application
open file.pdf
# Open URL
open https://example.com
# Open current directory in Finder
open .Homebrew Package Management
# Check if Homebrew is installed
if command -v brew &> /dev/null; then
# Install package
brew install package-name
# Update Homebrew
brew update
# Upgrade packages
brew upgrade
# Search for package
brew search package-name
# Get package info
brew info package-name
fi---
Windows (Git Bash)
Git Bash Environment
# Git Bash uses MSYS2 runtime
# Provides Unix-like environment on Windows
# Path handling
convert_path() {
local path=$1
if command -v cygpath &> /dev/null; then
# Convert Unix path to Windows
windows_path=$(cygpath -w "$path")
echo "$windows_path"
# Convert Windows path to Unix
unix_path=$(cygpath -u "C:\\Users\\user\\file.txt")
echo "$unix_path"
else
# Manual conversion (Git Bash)
# /c/Users/user → C:\Users\user
echo "${path//\//\\}" | sed 's/^\\//'
fi
}
# Git Bash path conventions
# C:\Users\user → /c/Users/user
# D:\data → /d/data
# Home directory
echo "$HOME" # /c/Users/username
echo "$USERPROFILE" # Windows-style path
# Temp directory
echo "$TEMP" # Windows temp
echo "$TMP" # Windows temp
echo "/tmp" # Git Bash temp (usually C:\Users\username\AppData\Local\Temp)Limited Features in Git Bash
# Features NOT available in Git Bash:
# 1. No systemd
# Use Windows services instead:
# sc query ServiceName
# net start ServiceName
# 2. Limited signal support
# SIGTERM works, but some signals behave differently
# 3. No /proc filesystem
# Use wmic or PowerShell:
# wmic process get processid,commandline
# 4. Process handling differences
# ps command is available but limited
ps -W # Show Windows processes
# 5. File permissions are simulated
# chmod works but doesn't map directly to Windows ACLs
# 6. Symbolic links require administrator privileges
# Or Developer Mode enabled in Windows 10+Windows-Specific Workarounds
# Run PowerShell commands from Git Bash
run_powershell() {
local command=$1
powershell.exe -Command "$command"
}
# Example: Get Windows version
run_powershell "Get-ComputerInfo | Select-Object WindowsVersion"
# Run cmd.exe commands
run_cmd() {
local command=$1
cmd.exe /c "$command"
}
# Example: Set Windows environment variable
run_cmd "setx MY_VAR value"
# Check if running with admin privileges
is_admin() {
net session &> /dev/null
return $?
}
if is_admin; then
echo "Running with administrator privileges"
else
echo "Not running as administrator"
fi
# Windows line endings (CRLF vs LF)
fix_line_endings() {
local file=$1
# Convert CRLF to LF
dos2unix "$file"
# Or with sed
sed -i 's/\r$//' "$file"
# Convert LF to CRLF
unix2dos "$file"
# Or with sed
sed -i 's/$/\r/' "$file"
}Git Bash Best Practices
# Always handle spaces in Windows paths
process_file() {
local file="$1" # Always quote!
# Windows paths often have spaces
# C:\Program Files\...
}
# Use forward slashes when possible
cd /c/Program\ Files/Git # Works
cd "C:\Program Files\Git" # Also works, but...
cd C:\\Program\ Files\\Git # Avoid
# Set Git config for line endings
git config --global core.autocrlf true # Windows
git config --global core.autocrlf input # Linux/macOS
# Check Git Bash version
bash --version
uname -a # Shows MINGW or MSYS---
Windows (WSL)
WSL1 vs WSL2
# Detect WSL version
detect_wsl_version() {
if grep -qi microsoft /proc/version; then
if [[ $(uname -r) =~ microsoft ]]; then
echo "WSL 1"
elif [[ $(uname -r) =~ WSL2 ]]; then
echo "WSL 2"
else
# Check kernel version
if [[ $(uname -r) =~ ^4\. ]]; then
echo "WSL 1"
else
echo "WSL 2"
fi
fi
else
echo "Not WSL"
fi
}
# WSL1 limitations:
# - No full syscall compatibility
# - File I/O slower on Windows filesystem
# - No Docker/containers (needs WSL2)
# WSL2 improvements:
# - Full Linux kernel
# - Better filesystem performance
# - Docker/container support
# - Near-native Linux performanceWindows Filesystem Access
# Access Windows drives from WSL
# Mounted at /mnt/c, /mnt/d, etc.
# List Windows drives
ls /mnt/
# Access Windows user directory
WINDOWS_HOME="/mnt/c/Users/$USER"
cd "$WINDOWS_HOME"
# File permissions on Windows filesystem
# Files on /mnt/c are owned by root but accessible
# Permissions are simulated
# Best practice: Use WSL filesystem for Linux files
# Use /home/username, not /mnt/c/...
# Much faster, especially in WSL1WSL Interoperability
# Run Windows executables from WSL
# .exe files are automatically executable
# Run Windows commands
cmd.exe /c dir
notepad.exe file.txt
explorer.exe . # Open current directory in Windows Explorer
# Run PowerShell
powershell.exe -Command "Get-Date"
# Pipe between Linux and Windows
cat file.txt | clip.exe # Copy to Windows clipboard
# Environment variables
# Windows environment is accessible with WSLENV
# Share environment variable from Windows to WSL
# In PowerShell:
# $env:WSLENV = "MYVAR/p"
# This converts Windows paths to WSL pathsWSL-Specific Configuration
# /etc/wsl.conf configuration
cat > /etc/wsl.conf << 'EOF'
[automount]
enabled = true
root = /mnt/
options = "metadata,umask=22,fmask=11"
[network]
generateHosts = true
generateResolvConf = true
[interop]
enabled = true
appendWindowsPath = true
EOF
# Apply: wsl.exe --shutdown (from PowerShell)
# Network differences
# WSL1: Shares network with Windows
# WSL2: NAT network, different IP
# Get WSL IP address
ip addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'
# Access Windows services from WSL2
# Use Windows IP, not localhost
# Or use: localhost (WSL2 has localhost forwarding)---
Container Environments
Docker Considerations
# Minimal base images often lack bash
# alpine: Only has /bin/sh by default
# debian:slim: Has bash
# ubuntu: Has bash
# Check if bash is available
if [ -f /bin/bash ]; then
exec /bin/bash "$@"
else
exec /bin/sh "$@"
fi
# Container detection
is_docker() {
if [[ -f /.dockerenv ]] || grep -q docker /proc/1/cgroup 2>/dev/null; then
return 0
else
return 1
fi
}
# PID 1 problem in containers
# Your script might be PID 1, which means:
# - Zombie process reaping is your responsibility
# - Signals behave differently
# Solution: Use tini or dumb-init
# Or handle signals explicitly
handle_sigterm() {
# Forward to child processes
kill -TERM "$child_pid" 2>/dev/null
wait "$child_pid"
exit 0
}
trap handle_sigterm SIGTERM
# Start main process
main_process &
child_pid=$!
wait "$child_pid"Kubernetes Considerations
# Kubernetes-specific environment variables
if [[ -n "$KUBERNETES_SERVICE_HOST" ]]; then
echo "Running in Kubernetes"
# Access Kubernetes API
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
KUBE_CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Get pod name
POD_NAME=${POD_NAME:-$(hostname)}
# Get namespace
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
fi
# Health checks
# Kubernetes expects:
# - HTTP probe on specific port
# - Or command that exits 0 for success
# Liveness probe handler
handle_health_check() {
# Check if application is healthy
if check_health; then
exit 0
else
exit 1
fi
}
# Readiness probe handler
handle_readiness_check() {
# Check if ready to serve traffic
if is_ready; then
exit 0
else
exit 1
fi
}
# Graceful shutdown for rolling updates
# Kubernetes sends SIGTERM, waits (default 30s), then SIGKILL
trap 'graceful_shutdown' SIGTERM
graceful_shutdown() {
echo "Received SIGTERM, shutting down gracefully..."
# Stop accepting new connections
# Finish processing existing requests
# Close connections
# Exit
exit 0
}Container Best Practices
# Don't assume specific users/groups exist
# Many containers run as non-root or random UID
# Check current user
if [[ $EUID -eq 0 ]]; then
echo "Running as root"
else
echo "Running as user $EUID"
fi
# Handle arbitrary UIDs (OpenShift)
# Files in mounted volumes may not be owned by container user
# Solution: Add current user to group, use group permissions
# Minimal dependencies
# Container images should be small
# Don't install unnecessary packages
# Use absolute paths or set PATH explicitly
export PATH=/usr/local/bin:/usr/bin:/bin
# Environment variables for configuration
# Don't hardcode values, use env vars
DATABASE_URL=${DATABASE_URL:-postgres://localhost/db}
# Logging to stdout/stderr
# Container orchestrators capture these
echo "Log message" # To stdout
echo "Error message" >&2 # To stderr
# Don't write to filesystem (except for tmpfs)
# Containers are ephemeral
# Use volumes for persistent data---
Cross-Platform Patterns
Portable Command Wrapper
# Create wrappers for platform-specific commands
setup_portable_commands() {
local os
os=$(detect_os)
case "$os" in
linux)
SED=sed
READLINK="readlink -f"
DATE=date
STAT="stat -c"
GREP=grep
;;
macos)
# Prefer GNU versions if available
SED=$(command -v gsed || echo sed)
READLINK=$(command -v greadlink || echo "echo") # No -f on BSD
DATE=$(command -v gdate || echo date)
STAT=$(command -v gstat || echo stat)
GREP=$(command -v ggrep || echo grep)
;;
gitbash)
SED=sed
READLINK=readlink # Git Bash has GNU tools
DATE=date
STAT=stat
GREP=grep
;;
esac
export SED READLINK DATE STAT GREP
}
# Use the wrappers
setup_portable_commands
$SED -i 's/old/new/g' file.txtCross-Platform Temp Files
# Portable temporary file creation
create_temp_file() {
# Works on all platforms
local temp_file
temp_file=$(mktemp) || {
# Fallback if mktemp doesn't exist
temp_file="/tmp/script.$$.$RANDOM"
touch "$temp_file"
}
echo "$temp_file"
}
# Portable temporary directory
create_temp_dir() {
local temp_dir
temp_dir=$(mktemp -d) || {
# Fallback
temp_dir="/tmp/script.$$.$RANDOM"
mkdir -p "$temp_dir"
}
echo "$temp_dir"
}
# Clean up temp files on exit
TEMP_DIR=$(create_temp_dir)
trap 'rm -rf "$TEMP_DIR"' EXITCross-Platform File Paths
# Normalize paths across platforms
normalize_path() {
local path="$1"
# Remove trailing slashes
path="${path%/}"
# Convert backslashes to forward slashes (Windows)
path="${path//\\//}"
# Resolve . and ..
# Use Python for reliable normalization
if command -v python3 &> /dev/null; then
path=$(python3 -c "import os; print(os.path.normpath('$path'))")
elif command -v python &> /dev/null; then
path=$(python -c "import os; print(os.path.normpath('$path'))")
fi
echo "$path"
}
# Get absolute path (cross-platform)
get_absolute_path() {
local path="$1"
# Try readlink -f (Linux, Git Bash)
if readlink -f "$path" &> /dev/null; then
readlink -f "$path"
# Try realpath (most platforms)
elif command -v realpath &> /dev/null; then
realpath "$path"
# Fallback to Python
elif command -v python3 &> /dev/null; then
python3 -c "import os; print(os.path.abspath('$path'))"
# Fallback to cd
elif [[ -d "$path" ]]; then
(cd "$path" && pwd)
else
(cd "$(dirname "$path")" && echo "$(pwd)/$(basename "$path")")
fi
}Cross-Platform Process Management
# Find process by name (cross-platform)
find_process() {
local process_name="$1"
if command -v pgrep &> /dev/null; then
pgrep -f "$process_name"
else
ps aux | grep "$process_name" | grep -v grep | awk '{print $2}'
fi
}
# Kill process by name (cross-platform)
kill_process() {
local process_name="$1"
if command -v pkill &> /dev/null; then
pkill -f "$process_name"
else
local pids
pids=$(find_process "$process_name")
if [[ -n "$pids" ]]; then
kill $pids
fi
fi
}---
Command Compatibility Matrix
| Command | Linux | macOS | Git Bash | Notes |
|---|---|---|---|---|
sed -i | ✓ | ✓* | ✓ | macOS needs sed -i '' |
date -d | ✓ | ✗ | ✓ | macOS uses -v |
readlink -f | ✓ | ✗ | ✓ | macOS needs greadlink |
stat -c | ✓ | ✗ | ✓ | macOS uses -f |
grep -P | ✓ | ✗ | ✓ | macOS doesn't support PCRE |
find -printf | ✓ | ✗ | ✓ | macOS doesn't have -printf |
xargs -r | ✓ | ✗ | ✓ | macOS doesn't have -r |
ps aux | ✓ | ✓ | ✓* | Git Bash has limited output |
ls --color | ✓ | ✗ | ✓ | macOS uses -G |
du -b | ✓ | ✗ | ✓ | macOS doesn't support bytes |
mktemp | ✓ | ✓ | ✓ | Works on all platforms |
timeout | ✓ | ✗ | ✓ | macOS needs gtimeout |
Legend:
- ✓ = Supported
- ✗ = Not supported
- ✓* = Supported with limitations
---
Testing Across Platforms
# Test script on multiple platforms
test_platforms() {
local script="$1"
echo "Testing on current platform: $(detect_os)"
bash -n "$script" || {
echo "Syntax error!"
return 1
}
# Run ShellCheck
if command -v shellcheck &> /dev/null; then
shellcheck "$script" || return 1
fi
# Run the script
bash "$script" || return 1
echo "Tests passed on $(detect_os)"
}
# CI/CD matrix testing
# Use GitHub Actions, GitLab CI, etc. to test on multiple platformsExample GitHub Actions matrix:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: Test script
run: bash test.sh---
Summary
Key takeaways for cross-platform bash scripts:
1. Always detect the platform before using platform-specific features 2. Use portable commands or provide fallbacks 3. Test on all target platforms (CI/CD with matrix builds) 4. Avoid platform-specific assumptions (file paths, users, services) 5. Use ShellCheck to catch portability issues 6. Prefer POSIX compliance when possible for maximum portability 7. Document platform requirements in script comments 8. Provide GNU alternatives on macOS when needed 9. Handle path differences carefully (especially Windows) 10. Test in containers if that's your deployment target
For maximum portability: stick to POSIX shell (#!/bin/sh) and avoid bashisms unless you control the deployment environment.
Bash Scripting Resources
Comprehensive directory of authoritative sources, tools, and learning resources for bash scripting.
---
Table of Contents
1. Official Documentation 2. Style Guides and Standards 3. Tools and Utilities 4. Learning Resources 5. Community Resources 6. Books 7. Cheat Sheets and Quick References 8. Testing and Quality 9. Platform-Specific Resources 10. Advanced Topics
---
🚨 CRITICAL GUIDELINES
Windows File Path Requirements
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/).
Examples:
- ❌ WRONG:
D:/repos/project/file.tsx - ✅ CORRECT:
D:\repos\project\file.tsx
This applies to:
- Edit tool file_path parameter
- Write tool file_path parameter
- All file operations on Windows systems
Documentation Guidelines
NEVER create new documentation files unless explicitly requested by the user.
- Priority: Update existing README.md files rather than creating new documentation
- Repository cleanliness: Keep repository root clean - only README.md unless user requests otherwise
- Style: Documentation should be concise, direct, and professional - avoid AI-generated tone
- User preference: Only create additional .md files when user specifically asks for documentation
---
Official Documentation
Bash Manual
GNU Bash Reference Manual
- URL: https://www.gnu.org/software/bash/manual/
- Description: The authoritative reference for bash features, syntax, and built-ins
- Use for: Detailed feature documentation, syntax clarification, version-specific features
Bash Man Page
man bash # Complete bash documentation
man bash-builtins # Built-in commands- Use for: Quick reference on local system, offline documentation
POSIX Standards
POSIX Shell Command Language
- URL: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
- Description: IEEE/Open Group specification for portable shell scripting
- Use for: Writing portable scripts, understanding sh vs bash differences
POSIX Utilities
- URL: https://pubs.opengroup.org/onlinepubs/9699919799/idx/utilities.html
- Description: Standard utilities available in POSIX-compliant systems
- Use for: Portable command usage, cross-platform compatibility
Command Documentation
GNU Coreutils Manual
- URL: https://www.gnu.org/software/coreutils/manual/
- Description: Documentation for core GNU utilities (ls, cat, grep, etc.)
- Use for: Understanding Linux command behavior, GNU-specific features
Man Pages Online
- URL: https://man7.org/linux/man-pages/
- URL: https://www.freebsd.org/cgi/man.cgi (BSD/macOS)
- Description: Online searchable man pages
- Use for: Quick online reference, comparing Linux vs BSD commands
---
Style Guides and Standards
Google Shell Style Guide
URL: https://google.github.io/styleguide/shellguide.html
Key Points:
- Industry-standard practices from Google
- Covers naming conventions, formatting, best practices
- When to use shell vs other languages
- Safety and portability guidelines
Use for: Professional code style, team standards, code reviews
Defensive Bash Programming
URL: https://kfirlavi.herokuapp.com/blog/2012/11/14/defensive-bash-programming
Key Points:
- Writing robust bash scripts
- Error handling patterns
- Safe coding practices
- Code organization
Use for: Improving script reliability, avoiding common pitfalls
Shell Style Guide (GitHub)
URL: https://github.com/bahamas10/bash-style-guide
Key Points:
- Community-driven style guidelines
- Practical examples
- Modern bash features
Use for: Alternative perspectives on style, community standards
---
Tools and Utilities
ShellCheck
Website: https://www.shellcheck.net/ GitHub: https://github.com/koalaman/shellcheck Online Tool: https://www.shellcheck.net/ (paste code for instant feedback)
Description: Static analysis tool for shell scripts
Installation:
# Ubuntu/Debian
apt-get install shellcheck
# macOS
brew install shellcheck
# Windows (Scoop)
scoop install shellcheck
# Via Docker
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck script.shUsage:
shellcheck script.sh # Check script
shellcheck -x script.sh # Follow source statements
shellcheck -f json script.sh # JSON output
shellcheck -e SC2086 script.sh # Exclude specific warningsShellCheck Wiki: https://www.shellcheck.net/wiki/
- Detailed explanations of every warning
- Use for: Understanding and fixing ShellCheck warnings
shfmt
GitHub: https://github.com/mvdan/sh
Description: Shell script formatter
Installation:
# macOS
brew install shfmt
# Go
go install mvdan.cc/sh/v3/cmd/shfmt@latestUsage:
shfmt -i 4 -w script.sh # Format with 4-space indent
shfmt -d script.sh # Show diff without modifying
shfmt -l script.sh # List files that would be changedUse for: Consistent code formatting, automated formatting in CI
BATS (Bash Automated Testing System)
GitHub: https://github.com/bats-core/bats-core
Description: Testing framework for bash scripts
Installation:
git clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/localUsage:
bats test/ # Run all tests
bats test/script.bats # Run specific test file
bats --tap test/ # TAP output formatDocumentation: https://bats-core.readthedocs.io/
Use for: Unit testing bash scripts, CI/CD integration
bashate
GitHub: https://github.com/openstack/bashate
Description: Style checker (used by OpenStack)
Installation:
pip install bashateUsage:
bashate script.sh
bashate -i E006 script.sh # Ignore specific errorsUse for: Additional style checking beyond ShellCheck
checkbashisms
Package: devscripts (Debian)
Description: Checks for bashisms in sh scripts
Installation:
apt-get install devscripts # Ubuntu/DebianUsage:
checkbashisms script.sh
checkbashisms -f script.sh # Force check even if #!/bin/bashUse for: Ensuring POSIX compliance, portable scripts
---
Learning Resources
Interactive Tutorials
Bash Academy
- URL: https://www.bash.academy/
- Description: Modern, comprehensive bash tutorial
- Topics: Basics, scripting, advanced features
- Use for: Learning bash from scratch, structured learning path
Learn Shell
- URL: https://www.learnshell.org/
- Description: Interactive bash tutorial with exercises
- Use for: Hands-on practice, beginners
Bash Scripting Tutorial
- URL: https://linuxconfig.org/bash-scripting-tutorial
- Description: Comprehensive tutorial series
- Use for: Step-by-step learning, examples
Guides and Documentation
Bash Guide for Beginners
- URL: https://tldp.org/LDP/Bash-Beginners-Guide/html/
- Author: The Linux Documentation Project
- Description: Comprehensive guide covering basics to intermediate
- Use for: Structured learning, reference material
Advanced Bash-Scripting Guide
- URL: https://tldp.org/LDP/abs/html/
- Description: In-depth coverage of advanced bash topics
- Topics: Complex scripting, text processing, system administration
- Use for: Advanced techniques, real-world examples
Bash Hackers Wiki
- URL: https://wiki.bash-hackers.org/
- Alternative: https://flokoe.github.io/bash-hackers-wiki/ (maintained mirror)
- Description: Community-driven bash documentation
- Use for: In-depth explanations, advanced topics, edge cases
Greg's Wiki (Wooledge)
- URL: https://mywiki.wooledge.org/
- Key Pages:
- https://mywiki.wooledge.org/BashFAQ
- https://mywiki.wooledge.org/BashPitfalls
- https://mywiki.wooledge.org/BashGuide
- Description: High-quality bash Q&A and guides
- Use for: Common questions, avoiding pitfalls, best practices
Video Courses
Bash Scripting on Linux (Udemy)
- Description: Comprehensive video course
- Use for: Visual learners
Shell Scripting: Discover How to Automate Command Line Tasks (Udemy)
- Description: Practical shell scripting course
- Use for: Automation-focused learning
LinkedIn Learning - Learning Bash Scripting
- Description: Professional development course
- Use for: Structured corporate training
---
Community Resources
Stack Overflow
Bash Tag
- URL: https://stackoverflow.com/questions/tagged/bash
- Use for: Specific problems, code review, troubleshooting
Top Questions:
- URL: https://stackoverflow.com/questions/tagged/bash?tab=Votes
- Use for: Common problems and solutions
Unix & Linux Stack Exchange
URL: https://unix.stackexchange.com/
Shell Tag: https://unix.stackexchange.com/questions/tagged/shell Bash Tag: https://unix.stackexchange.com/questions/tagged/bash
Use for: Unix/Linux-specific questions, system administration
/r/bash
- URL: https://www.reddit.com/r/bash/
- Description: Bash scripting community
- Use for: Discussions, learning resources, help
/r/commandline
- URL: https://www.reddit.com/r/commandline/
- Description: Command-line interface community
- Use for: CLI tips, tools, productivity
IRC/Chat
Freenode #bash
- URL: irc://irc.freenode.net/bash
- Description: Real-time bash help channel
- Use for: Live help, quick questions
Libera.Chat #bash
- URL: irc://irc.libera.chat/bash
- Description: Alternative IRC channel
- Use for: Live community support
---
Books
"Classic Shell Scripting" by Arnold Robbins & Nelson Beebe
Publisher: O'Reilly ISBN: 978-0596005955
Topics:
- Shell basics and portability
- Text processing and filters
- Shell programming patterns
Use for: Comprehensive reference, professional development
"Learning the bash Shell" by Cameron Newham
Publisher: O'Reilly ISBN: 978-0596009656
Topics:
- Bash basics
- Command-line editing
- Shell programming
Use for: Systematic learning, reference
"Bash Cookbook" by Carl Albing & JP Vossen
Publisher: O'Reilly ISBN: 978-1491975336
Topics:
- Solutions to common problems
- Recipes and patterns
- Real-world examples
Use for: Problem-solving, practical examples
"Wicked Cool Shell Scripts" by Dave Taylor & Brandon Perry
Publisher: No Starch Press ISBN: 978-1593276027
Topics:
- Creative shell scripting
- System administration
- Fun and practical scripts
Use for: Inspiration, practical applications
"The Linux Command Line" by William Shotts
Publisher: No Starch Press ISBN: 978-1593279523 Free PDF: https://linuxcommand.org/tlcl.php
Topics:
- Command-line basics
- Shell scripting fundamentals
- Linux system administration
Use for: Beginners, comprehensive introduction
---
Cheat Sheets and Quick References
Bash Cheat Sheet (DevHints)
URL: https://devhints.io/bash
Content:
- Quick syntax reference
- Common patterns
- Parameter expansion
- Conditionals and loops
Use for: Quick lookups, syntax reminders
Bash Scripting Cheat Sheet (GitHub)
URL: https://github.com/LeCoupa/awesome-cheatsheets/blob/master/languages/bash.sh
Content:
- Comprehensive syntax guide
- Examples and explanations
- Best practices
Use for: Single-file reference
explainshell.com
URL: https://explainshell.com/
Description: Interactive tool that explains shell commands
Example: Paste tar -xzvf file.tar.gz to get detailed explanation of each flag
Use for: Understanding complex commands, learning command options
Command Line Fu
URL: https://www.commandlinefu.com/
Description: Community-contributed command-line snippets
Use for: One-liners, clever solutions, learning new commands
tldr Pages
URL: https://tldr.sh/ GitHub: https://github.com/tldr-pages/tldr
Description: Simplified man pages with examples
Installation:
npm install -g tldr
# Or
brew install tldrUsage:
tldr tar
tldr grep
tldr findUse for: Quick command examples, practical usage
---
Testing and Quality
Testing Frameworks
BATS (Bash Automated Testing System)
- URL: https://github.com/bats-core/bats-core
- Documentation: https://bats-core.readthedocs.io/
- Use for: Unit testing
shUnit2
- URL: https://github.com/kward/shunit2
- Description: xUnit-based unit testing framework
- Use for: Alternative to BATS
Bash Unit
- URL: https://github.com/pgrange/bash_unit
- Description: Bash unit testing
- Use for: Lightweight testing
CI/CD Integration
GitHub Actions Example
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install ShellCheck
run: sudo apt-get install -y shellcheck
- name: Run ShellCheck
run: find . -name "*.sh" -exec shellcheck {} +
- name: Install BATS
run: |
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local
- name: Run Tests
run: bats test/GitLab CI Example
test:
image: koalaman/shellcheck-alpine
script:
- find . -name "*.sh" -exec shellcheck {} +
bats:
image: bats/bats
script:
- bats test/Code Coverage
bashcov
- URL: https://github.com/infertux/bashcov
- Description: Code coverage for bash
- Installation:
gem install bashcov - Use for: Measuring test coverage
---
Platform-Specific Resources
Linux
Linux Man Pages
- URL: https://man7.org/linux/man-pages/
- Use for: Linux-specific command documentation
systemd Documentation
- URL: https://www.freedesktop.org/software/systemd/man/
- Use for: systemd service management
macOS
macOS Man Pages
- URL: https://www.freebsd.org/cgi/man.cgi
- Description: BSD-based commands (similar to macOS)
- Use for: macOS command differences
Homebrew
- URL: https://brew.sh/
- Use for: Installing GNU tools on macOS
Windows
Git for Windows
- URL: https://gitforwindows.org/
- Documentation: https://github.com/git-for-windows/git/wiki
- Use for: Git Bash on Windows
WSL Documentation
- URL: https://docs.microsoft.com/en-us/windows/wsl/
- Use for: Windows Subsystem for Linux
Cygwin
- URL: https://www.cygwin.com/
- Use for: POSIX environment on Windows
Containers
Docker Bash Best Practices
- URL: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- Use for: Bash in containers
Container Best Practices
- URL: https://cloud.google.com/architecture/best-practices-for-building-containers
- Use for: Production container scripts
---
Advanced Topics
Process Substitution
Greg's Wiki:
- URL: https://mywiki.wooledge.org/ProcessSubstitution
- Use for: Understanding
<()syntax
Parameter Expansion
Bash Hackers Wiki:
- URL: https://wiki.bash-hackers.org/syntax/pe
- Use for: Complete parameter expansion reference
Regular Expressions
Bash Regex:
- URL: https://mywiki.wooledge.org/RegularExpression
- Use for: Regex in bash
[[ =~ ]]
PCRE vs POSIX:
- URL: https://www.regular-expressions.info/posix.html
- Use for: Understanding regex flavors
Parallel Processing
GNU Parallel:
- URL: https://www.gnu.org/software/parallel/
- Tutorial: https://www.gnu.org/software/parallel/parallel_tutorial.html
- Use for: Parallel command execution
Job Control
Bash Job Control:
- URL: https://www.gnu.org/software/bash/manual/html_node/Job-Control.html
- Use for: Background jobs, job management
---
Troubleshooting Resources
Debugging Tools
bashdb
- URL: http://bashdb.sourceforge.net/
- Description: Bash debugger
- Use for: Step-by-step debugging
xtrace
set -x # Enable
set +x # Disable- Use for: Trace command execution
PS4 for Better Trace Output
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -xCommon Issues
Bash Pitfalls
- URL: https://mywiki.wooledge.org/BashPitfalls
- Description: 50+ common mistakes in bash
- Use for: Avoiding and fixing common errors
Bash FAQ
- URL: https://mywiki.wooledge.org/BashFAQ
- Description: Frequently asked questions
- Use for: Quick answers to common questions
---
Summary: Where to Find Information
| Question Type | Resource |
|---|---|
| Syntax reference | Bash Manual, DevHints cheat sheet |
| Best practices | Google Shell Style Guide, ShellCheck |
| Portable scripting | POSIX specification, checkbashisms |
| Quick examples | tldr, explainshell.com |
| Common mistakes | Bash Pitfalls, ShellCheck Wiki |
| Advanced topics | Bash Hackers Wiki, Greg's Wiki |
| Testing | BATS documentation |
| Platform differences | Platform-specific docs, Stack Overflow |
| Troubleshooting | Stack Overflow, Unix & Linux SE |
| Learning path | Bash Academy, TLDP guides |
---
Quick Resource Lookup
When writing a new script: 1. Start with template from Google Style Guide 2. Use ShellCheck while developing 3. Reference Bash Manual for specific features 4. Check Bash Pitfalls for common mistakes
When debugging: 1. Use set -x for tracing 2. Check ShellCheck warnings 3. Search Bash Pitfalls 4. Search Stack Overflow for specific error
When learning: 1. Start with Bash Academy or TLDP 2. Use explainshell.com for commands 3. Read Greg's Wiki for in-depth topics 4. Practice with BATS tests
When ensuring quality: 1. Run ShellCheck 2. Run shellcheck 3. Format with shfmt 4. Write BATS tests 5. Review against Google Style Guide
These resources provide authoritative, up-to-date information for all aspects of bash scripting.
Related skills
How it compares
Use bash-master over generic shell help when you need ShellCheck-enforced, style-guide-compliant scripts for CI/CD and cross-platform DevOps automation.
FAQ
When should bash-master activate?
bash-master activates for any Bash or shell script task, including system automation, DevOps and CI/CD pipelines, build and deployment scripts, script review or debugging, and converting manual commands into reusable shell glue across platforms.
What quality standards does bash-master enforce?
bash-master enforces Google Shell Style Guide formatting, ShellCheck v0.11.0 validation, POSIX portability where needed, set -euo pipefail error handling, command-injection prevention, and optional BATS unit tests for production-ready scripts.
Does bash-master support Windows environments?
bash-master explicitly covers cross-platform compatibility for Linux, macOS, Windows Git Bash and WSL, and containerized environments, including Git Bash path conversion patterns documented in the broader Bash Master plugin.