Expert bash/shell scripting system across ALL platforms. PROACTIVELY activate for: (1) ANY bash/shell script task, (2) System automation, (3) DevOps/CI/CD scripts, (4) Build/deployment automation, (5) Script review/debugging, (6) Converting commands to scripts. Provides: Google Shell Style Guide compliance, ShellCheck validation, cross-platform compatibility (Linux/macOS/Windows/containers), POSIX compliance, security hardening, error handling, performance optimization, testing with BATS, and production-ready patterns. Ensures professional-grade, secure, portable scripts every time.
Expert bash/shell scripting system that activates for ANY scripting task, system automation, or DevOps work. Provides Google Shell Style Guide compliance, ShellCheck validation, cross-platform compatibility (Linux/macOS/Windows/containers), security hardening, and production-ready patterns for professional-grade scripts.
/plugin marketplace add JosiahSiegel/claude-plugin-marketplace/plugin install bash-master@claude-plugin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/best_practices.mdreferences/patterns_antipatterns.mdreferences/platform_specifics.mdreferences/platform_specifics.md.backupreferences/resources.mdreferences/windows-git-bash-paths.mdMANDATORY: 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:
D:/repos/project/file.tsxD:\repos\project\file.tsxThis applies to:
NEVER create new documentation files unless explicitly requested by the user.
Comprehensive guide for writing professional, portable, and maintainable bash scripts across all platforms.
Essential Checklist for Every Bash Script:
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
IFS=$'\n\t' # Safe word splitting
# Use: shellcheck your_script.sh before deployment
# Test on target platform(s) before production
Platform Compatibility Quick Check:
# Linux/macOS: ✓ Full bash features
# Git Bash (Windows): ✓ Most features, ✗ Some system calls
# Containers: ✓ Depends on base image
# POSIX mode: Use /bin/sh and avoid bashisms
This skill provides expert bash/shell scripting knowledge for ANY scripting task, ensuring professional-grade quality across all platforms.
MUST use this skill for:
What this skill provides:
set -euo pipefail, trap handlers, exit codesThis skill activates automatically for:
ALWAYS start scripts with safety settings:
#!/usr/bin/env bash
# Fail fast and loud
set -e # Exit on any error
set -u # Exit on undefined variable
set -o pipefail # Exit on pipe failure
set -E # ERR trap inherited by functions
# Optionally:
# set -x # Debug mode (print commands before execution)
# set -C # Prevent file overwrites with redirection
# Safe word splitting
IFS=$'\n\t'
# Script metadata
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
Why this matters:
set -e: Prevents cascading failuresset -u: Catches typos in variable namesset -o pipefail: Catches failures in the middle of pipesIFS=$'\n\t': Prevents word splitting on spaces (security issue)Know when to use which:
# POSIX-compliant (portable across shells)
#!/bin/sh
# Use: [ ] tests, no arrays, no [[ ]], no <(process substitution)
# Bash-specific (modern features, clearer syntax)
#!/usr/bin/env bash
# Use: [[ ]], arrays, associative arrays, <(), process substitution
Decision matrix:
#!/bin/sh and POSIX only#!/usr/bin/env bash#!/usr/bin/env bash# ALWAYS quote variables to prevent word splitting and globbing
bad_cmd=$file_path # ✗ WRONG - word splitting
good_cmd="$file_path" # ✓ CORRECT
# Arrays: Quote expansion
files=("file 1.txt" "file 2.txt")
process "${files[@]}" # ✓ CORRECT - each element quoted
process "${files[*]}" # ✗ WRONG - all elements as one string
# Command substitution: Quote the result
result="$(command)" # ✓ CORRECT
result=$(command) # ✗ WRONG (unless you want word splitting)
# Exception: When you WANT word splitting
# shellcheck disable=SC2086
flags="-v -x -z"
command $flags # Intentional word splitting
ALWAYS run ShellCheck before deployment:
# Install
# Ubuntu/Debian: apt-get install shellcheck
# macOS: brew install shellcheck
# Windows: scoop install shellcheck
# Usage
shellcheck your_script.sh
shellcheck -x your_script.sh # Follow source statements
# In CI/CD
find . -name "*.sh" -exec shellcheck {} +
ShellCheck catches:
ESSENTIAL KNOWLEDGE: Git Bash/MINGW automatically converts Unix-style paths to Windows paths. This is the most common source of cross-platform scripting errors on Windows.
Complete Guide: See references/windows-git-bash-paths.md for comprehensive documentation.
Quick Reference:
# Automatic conversion happens for:
/foo → C:/Program Files/Git/usr/foo
--dir=/tmp → --dir=C:/msys64/tmp
# Disable conversion when needed
MSYS_NO_PATHCONV=1 command /path/that/should/not/convert
# Manual conversion with cygpath
unix_path=$(cygpath -u "C:\Windows\System32") # Windows to Unix
win_path=$(cygpath -w "/c/Users/username") # Unix to Windows
# Shell detection (fastest method)
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "mingw"* ]]; then
echo "Git Bash detected"
# Use path conversion
fi
# Or check $MSYSTEM variable (Git Bash/MSYS2 specific)
case "${MSYSTEM:-}" in
MINGW64|MINGW32|MSYS)
echo "MSYS2/Git Bash environment: $MSYSTEM"
;;
esac
Common Issues:
# Problem: Flags converted to paths
command /e /s # /e becomes C:/Program Files/Git/e
# Solution: Use double slashes or dashes
command //e //s # OR: command -e -s
# Problem: Spaces in paths
cd C:\Program Files\Git # Fails
# Solution: Quote paths
cd "C:\Program Files\Git" # OR: cd /c/Program\ Files/Git
Primary target for most bash scripts:
# Linux-specific features available
/proc filesystem
systemd integration
Linux-specific commands (apt, yum, systemctl)
# Check for Linux
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux-specific code
fi
BSD-based utilities (different from GNU):
# macOS differences
sed -i '' # macOS requires empty string
sed -i # Linux doesn't need it
# Use ggrep, gsed, etc. for GNU versions
if command -v gsed &> /dev/null; then
SED=gsed
else
SED=sed
fi
# Check for macOS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS-specific code
fi
Git Bash limitations:
# Available in Git Bash:
- Most core utils
- File operations
- Process management (limited)
# NOT available:
- systemd
- Some signals (SIGHUP behavior differs)
- /proc filesystem
- Native Windows path handling issues
# Path handling
# Git Bash uses Unix paths: /c/Users/...
# Convert if needed:
winpath=$(cygpath -w "$unixpath") # Unix → Windows
unixpath=$(cygpath -u "$winpath") # Windows → Unix
# Check for Git Bash
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
# Git Bash / Cygwin code
fi
WSL (Windows Subsystem for Linux):
# WSL is essentially Linux, but:
# - Can access Windows filesystem at /mnt/c/
# - Some syscalls behave differently
# - Network configuration differs
# Check for WSL
if grep -qi microsoft /proc/version 2>/dev/null; then
# WSL-specific code
fi
Container-aware scripting:
# Minimal base images may not have bash
# Use #!/bin/sh or install bash explicitly
# Container detection
if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then
# Running in Docker
fi
# Kubernetes detection
if [ -n "$KUBERNETES_SERVICE_HOST" ]; then
# Running in Kubernetes
fi
# Best practices:
# - Minimize dependencies
# - Use absolute paths or PATH
# - Don't assume user/group existence
# - Handle signals properly (PID 1 issues)
#!/usr/bin/env bash
set -euo pipefail
# Detect platform
detect_platform() {
case "$OSTYPE" in
linux-gnu*) echo "linux" ;;
darwin*) echo "macos" ;;
msys*|cygwin*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
PLATFORM=$(detect_platform)
# Platform-specific paths
case "$PLATFORM" in
linux)
SED=sed
;;
macos)
SED=$(command -v gsed || echo sed)
;;
windows)
# Git Bash specifics
;;
esac
# 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, namespacing
# 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)"
# 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=$?
# Cleanup operations
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"
validate_input() {
local input="$1"
# Check if empty
if [[ -z "$input" ]]; then
echo "Error: Input cannot be empty" >&2
return 1
fi
# Check format (example: alphanumeric only)
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: Input contains invalid characters" >&2
return 1
fi
# Check length
if [[ ${#input} -gt 255 ]]; then
echo "Error: Input too long (max 255 characters)" >&2
return 1
fi
return 0
}
# Validate before use
read -r user_input
if validate_input "$user_input"; then
process "$user_input"
fi
# Simple argument 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=""
# Parse arguments
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
# Validate required arguments
if [[ -z "$command" ]]; then
echo "Error: Command is required" >&2
usage >&2
exit 1
fi
# Execute command
case "$command" in
build) do_build ;;
test) do_test ;;
*)
echo "Error: Unknown command: $command" >&2
usage >&2
exit 1
;;
esac
}
main "$@"
# Logging levels
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
# Current log level
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; }
# With timestamps
log_with_timestamp() {
local level="$1"
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}
# Usage
log_info "Starting process"
log_error "Failed to connect to database"
# NEVER use eval with user input
# ✗ WRONG - DANGEROUS
eval "$user_input"
# NEVER use dynamic variable names from user input
# ✗ WRONG - DANGEROUS
eval "var_$user_input=value"
# NEVER concatenate user input into commands
# ✗ WRONG - DANGEROUS
grep "$user_pattern" file.txt # If pattern contains -e flag, injection possible
# ✓ CORRECT - Use arrays
grep_args=("$user_pattern" "file.txt")
grep "${grep_args[@]}"
# ✓ CORRECT - Use -- to separate options from arguments
grep -- "$user_pattern" file.txt
# Sanitize file paths
sanitize_path() {
local path="$1"
# Remove .. components
path="${path//..\/}"
path="${path//\/..\//}"
# Remove leading /
path="${path#/}"
echo "$path"
}
# Validate path is within allowed directory
is_safe_path() {
local file_path="$1"
local base_dir="$2"
# Resolve to absolute path
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
# Check if path starts with base directory
[[ "$real_path" == "$real_base"/* ]]
}
# Usage
if is_safe_path "$user_file" "/var/app/data"; then
process_file "$user_file"
else
echo "Error: Invalid file path" >&2
exit 1
fi
# Check if running as root
if [[ $EUID -eq 0 ]]; then
echo "Error: Do not run this script as root" >&2
exit 1
fi
# Drop privileges if needed
drop_privileges() {
local user="$1"
if [[ $EUID -eq 0 ]]; then
exec sudo -u "$user" "$0" "$@"
fi
}
# Run specific command with elevated privileges
run_as_root() {
if [[ $EUID -ne 0 ]]; then
sudo "$@"
else
"$@"
fi
}
# Create secure temporary files
readonly TEMP_DIR=$(mktemp -d)
readonly TEMP_FILE=$(mktemp)
# Cleanup on exit
cleanup() {
rm -rf "$TEMP_DIR"
rm -f "$TEMP_FILE"
}
trap cleanup EXIT
# Secure temporary file (only readable by owner)
secure_temp=$(mktemp)
chmod 600 "$secure_temp"
# ✗ SLOW - Creates subshell for each iteration
while IFS= read -r line; do
count=$(echo "$count + 1" | bc)
done < file.txt
# ✓ FAST - Arithmetic in bash
count=0
while IFS= read -r line; do
((count++))
done < file.txt
# ✗ SLOW - External commands
dirname=$(dirname "$path")
basename=$(basename "$path")
# ✓ FAST - Parameter expansion
dirname="${path%/*}"
basename="${path##*/}"
# ✗ SLOW - grep for simple checks
if echo "$string" | grep -q "pattern"; then
# ✓ FAST - Bash regex
if [[ "$string" =~ pattern ]]; then
# ✗ SLOW - awk for simple extraction
field=$(echo "$line" | awk '{print $3}')
# ✓ FAST - Read into array
read -ra fields <<< "$line"
field="${fields[2]}"
# When you need to read multiple commands' output
# ✓ GOOD - Process substitution
while IFS= read -r line1 <&3 && IFS= read -r line2 <&4; do
echo "$line1 - $line2"
done 3< <(command1) 4< <(command2)
# Parallel processing
command1 &
command2 &
wait # Wait for all background jobs
# ✓ FAST - Native array operations
files=(*.txt)
echo "Found ${#files[@]} files"
# ✗ SLOW - Parsing ls output
count=$(ls -1 *.txt | wc -l)
# ✓ FAST - Array filtering
filtered=()
for item in "${array[@]}"; do
[[ "$item" =~ ^[0-9]+$ ]] && filtered+=("$item")
done
# ✓ FAST - Array joining
IFS=,
joined="${array[*]}"
IFS=$'\n\t'
# Install BATS
# git clone https://github.com/bats-core/bats-core.git
# cd bats-core && ./install.sh /usr/local
# test/script.bats
#!/usr/bin/env bats
# Load script to test
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 tests
# bats test/script.bats
# integration_test.sh
#!/usr/bin/env bash
set -euo pipefail
# Setup
setup() {
export TEST_DIR=$(mktemp -d)
export TEST_FILE="$TEST_DIR/test.txt"
}
# Teardown
teardown() {
rm -rf "$TEST_DIR"
}
# Test case
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"
return 0
}
# Run tests
main() {
setup
trap teardown EXIT
test_file_creation || exit 1
echo "All tests passed"
}
main
# .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/
# Method 1: set -x (print commands)
set -x
command1
command2
set +x # Turn off
# Method 2: PS4 for better 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.sh
# Trace function calls
trace() {
echo "[TRACE] Function: ${FUNCNAME[1]}, Args: $*" >&2
}
my_function() {
trace "$@"
# Function logic
}
# Execution time profiling
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 arg2
# Issue: Script works in bash but not in sh
# Solution: Check for bashisms
checkbashisms script.sh
# Issue: Works locally but not on server
# Solution: Check PATH and environment
env
echo "$PATH"
# Issue: Whitespace in filenames breaking script
# Solution: Always quote variables
for file in *.txt; do
process "$file" # Not: process $file
done
# Issue: Script behaves differently in cron
# Solution: Set PATH explicitly
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
# Simple key=value config
load_config() {
local config_file="$1"
if [[ ! -f "$config_file" ]]; then
echo "Error: Config file not found: $config_file" >&2
return 1
fi
# Source config (dangerous if not trusted)
# shellcheck source=/dev/null
source "$config_file"
}
# Safe config parsing (no code execution)
read_config() {
local config_file="$1"
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
# Trim whitespace
key=$(echo "$key" | tr -d ' ')
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Export variable
declare -g "$key=$value"
done < "$config_file"
}
# Simple background jobs
process_files_parallel() {
local max_jobs=4
local job_count=0
for file in *.txt; do
# Start background job
process_file "$file" &
# Limit concurrent jobs
((job_count++))
if [[ $job_count -ge $max_jobs ]]; then
wait -n # Wait for any job to finish
((job_count--))
fi
done
# Wait for remaining jobs
wait
}
# GNU Parallel (if available)
parallel_with_gnu() {
parallel -j 4 process_file ::: *.txt
}
# Graceful shutdown
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
# Do work
sleep 1
done
echo "Shutdown complete" >&2
}
main_loop
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/health
Bash Reference Manual
POSIX Shell Command Language
Google Shell Style Guide
Defensive Bash Programming
ShellCheck
BATS (Bash Automated Testing System)
shfmt
Bash Academy
Bash Guide for Beginners
Advanced Bash-Scripting Guide
Bash Pitfalls
explainshell.com
GNU Coreutils Manual
FreeBSD Manual Pages
Git for Windows
WSL Documentation
Stack Overflow - Bash Tag
Unix & Linux Stack Exchange
Reddit - r/bash
Bash Cheat Sheet
ShellCheck Wiki
For deeper coverage of specific topics, see the reference files:
Always activate for:
Key indicators:
A bash script using this skill should:
Quality checklist:
# Run before deployment
shellcheck script.sh # No errors or warnings
bash -n script.sh # Syntax check
bats test/script.bats # Unit tests pass
./script.sh --help # Usage text displays
DEBUG=true ./script.sh # Debug mode works
checkbashisms script.shcommand -v tool_namesed --version (GNU) vs sed (BSD)shellcheck -W SC2086# shellcheck disable=SC2086 reason: intentional word splitting./script.sh >> /tmp/cron.log 2>&1time commandset -xThis skill provides comprehensive bash scripting knowledge. Combined with the reference files, you have access to industry-standard practices and platform-specific guidance for any bash scripting task.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
This skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.