Claude-code-setup standards-shell
This skill provides Shell/Bash coding standards and is automatically loaded for shell projects. It includes defensive scripting patterns, best practices, and recommended tooling.
install
source · Clone the upstream repo
git clone https://github.com/b33eep/claude-code-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/b33eep/claude-code-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/standards-shell" ~/.claude/skills/b33eep-claude-code-setup-standards-shell && rm -rf "$T"
manifest:
skills/standards-shell/SKILL.mdsource content
Shell/Bash Coding Standards
Core Principles
- Simplicity: Simple, understandable scripts
- Readability: Readability over cleverness
- Maintainability: Scripts that are easy to maintain
- Testability: Scripts that are easy to test
- DRY: Don't Repeat Yourself - but don't overdo it
- Defensiveness: Fail early, fail loudly
General Rules
- Defensive Header: Always use
set -euo pipefail - Quote Variables: Always quote variables
"$var" - Descriptive Names: Meaningful names for variables and functions
- Minimal Changes: Only change relevant code parts
- No Over-Engineering: No unnecessary complexity
- ShellCheck Clean: All scripts must pass ShellCheck
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Variables | snake_case | , |
| Functions | snake_case | , |
| Constants | UPPER_SNAKE_CASE | , |
| Files | kebab-case or snake_case | , |
| Environment Vars | UPPER_SNAKE_CASE | , |
Script Template
#!/bin/bash set -euo pipefail readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" # Cleanup on exit cleanup() { rm -f "$SCRIPT_DIR"/*.tmp 2>/dev/null || true } trap cleanup EXIT # Error handler error_handler() { echo "Error on line $1" >&2 exit 1 } trap 'error_handler $LINENO' ERR main() { # Script logic here echo "Running $SCRIPT_NAME" } main "$@"
Defensive Scripting
# REQUIRED: Always start with this set -euo pipefail # -e: Exit on error # -u: Error on undefined variables # -o pipefail: Pipe fails if any command fails # RECOMMENDED: Safer IFS IFS=$'\n\t' # REQUIRED: Quote all variables echo "$var" # Good echo $var # Bad - word splitting # REQUIRED: Use [[ ]] for conditionals (Bash) if [[ -f "$file" ]]; then # Good if [ -f "$file" ]; then # POSIX only
Parameter Expansion
# Defaults and validation ${var:-default} # Use default if unset ${var:=default} # Assign default if unset ${var:?error message} # Error if unset # String manipulation ${var#pattern} # Remove prefix (shortest) ${var##pattern} # Remove prefix (longest) ${var%pattern} # Remove suffix (shortest) ${var%%pattern} # Remove suffix (longest) ${var/old/new} # Replace first ${var//old/new} # Replace all ${#var} # Length # Examples file="document.txt" echo "${file%%.*}" # "document" (remove extension) echo "${file##*.}" # "txt" (get extension)
Functions
# REQUIRED: Use local variables get_user_name() { local user_id=$1 local name name=$(grep "^${user_id}:" /etc/passwd | cut -d: -f5) echo "$name" } # Return values via stdout result=$(get_user_name "1000") # Return status codes validate_file() { local file=$1 if [[ ! -f "$file" ]]; then echo "Error: File not found: $file" >&2 return 1 fi return 0 } if validate_file "$input_file"; then process_file "$input_file" fi
Arrays
# Indexed arrays files=("file1.txt" "file2.txt" "file3.txt") echo "${files[0]}" # First element echo "${files[@]}" # All elements echo "${#files[@]}" # Array length # Iterate safely for file in "${files[@]}"; do echo "Processing: $file" done # Associative arrays (Bash 4+) declare -A config config[host]="localhost" config[port]="8080" echo "${config[host]}:${config[port]}"
File Operations
# Read file line by line while IFS= read -r line; do echo "Line: $line" done < "input.txt" # Read into array mapfile -t lines < "input.txt" # Write to file (heredoc) cat > output.txt <<EOF Line 1 Line 2 EOF # Temp files with cleanup temp_file=$(mktemp) trap 'rm -f "$temp_file"' EXIT
Error Handling
# Trap for cleanup cleanup() { echo "Cleaning up..." rm -f "$temp_file" } trap cleanup EXIT # Trap for errors error_handler() { local line=$1 echo "Error occurred on line $line" >&2 } trap 'error_handler $LINENO' ERR # Check command exists if ! command -v python3 &>/dev/null; then echo "Error: python3 not found" >&2 exit 1 fi # Conditional execution command1 && command2 # Run command2 only if command1 succeeds command1 || command2 # Run command2 only if command1 fails
Argument Parsing with getopts
usage() { echo "Usage: $0 [-v] [-o output] [-h]" echo " -v Verbose mode" echo " -o FILE Output file" echo " -h Show help" exit 1 } verbose=false output_file="" while getopts "vo:h" opt; do case $opt in v) verbose=true ;; o) output_file="$OPTARG" ;; h) usage ;; *) usage ;; esac done shift $((OPTIND - 1)) # Remaining args in $@
Logging
log() { local level=$1 shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >&2 } log_info() { log "INFO" "$@"; } log_warn() { log "WARN" "$@"; } log_error() { log "ERROR" "$@"; } # Usage log_info "Starting process" log_error "Failed to connect"
Debugging
# Enable debugging set -x # Print commands PS4='+ ${BASH_SOURCE}:${LINENO}: ' # Better debug output # Debug specific section set -x # code to debug set +x # Run script with debug bash -x script.sh bash -n script.sh # Syntax check only
Common Patterns
# Check if root if [[ $EUID -ne 0 ]]; then echo "This script must be run as root" >&2 exit 1 fi # Safe directory change cd "$target_dir" || exit 1 # Process files safely (handles spaces) find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do echo "Processing: $file" done # Retry pattern retry() { local max_attempts=$1 local delay=$2 shift 2 local attempt=1 while [[ $attempt -le $max_attempts ]]; do if "$@"; then return 0 fi log_warn "Attempt $attempt failed, retrying in ${delay}s..." sleep "$delay" ((attempt++)) done return 1 } retry 3 5 curl -f "https://api.example.com/health"
Recommended Tooling
| Tool | Purpose |
|---|---|
| Static analysis (required) |
| Code formatting |
| Testing framework |
| Modern features (avoid macOS default 3.2) |
ShellCheck Usage
# Run ShellCheck shellcheck script.sh # Disable specific warning (sparingly) # shellcheck disable=SC2086 echo $UNQUOTED_VAR # Follow sourced files shellcheck -x script.sh
Testing with bats-core
#!/usr/bin/env bats # File: test_script.bats source ./script.sh @test "add function returns correct sum" { result=$(add 5 3) [ "$result" = "8" ] } @test "validate_file fails on missing file" { run validate_file "nonexistent.txt" [ "$status" -eq 1 ] }
Run tests:
bats tests/
POSIX Compatibility
For maximum portability (sh, dash, ash):
#!/bin/sh # Use [ ] instead of [[ ]] if [ -f "file.txt" ]; then echo "File exists" fi # No arrays, use positional parameters set -- "apple" "banana" "cherry" echo "First: $1" # No $() in older shells, use backticks current_date=`date +%Y-%m-%d`
Production Best Practices
- Defensive header - Always
set -euo pipefail - Quote everything - Prevent word splitting and glob expansion
- Local variables - Use
in functionslocal - ShellCheck clean - No warnings before commit
- Cleanup traps - Always clean up temp files
- Meaningful exit codes - 0 for success, non-zero for errors
- Logging to stderr - Keep stdout for data, stderr for logs
- Check dependencies - Verify required commands exist
- Handle signals - Trap SIGTERM for graceful shutdown
- Document usage - Include
option--help
References
- Based on moai-lang-shell by AJBcoding