| name | standards-shell |
| description | This skill provides Shell/Bash coding standards and is automatically loaded for shell projects. It includes defensive scripting patterns, best practices, and recommended tooling. |
| type | context |
| applies_to | ["bash","sh","shell","zsh","shellcheck","bats"] |
| file_extensions | [".sh",".bash"] |
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 | user_name, file_count |
| Functions | snake_case | get_user_by_id, validate_input |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES, DEFAULT_TIMEOUT |
| Files | kebab-case or snake_case | deploy-app.sh, run_tests.sh |
| Environment Vars | UPPER_SNAKE_CASE | API_URL, DATABASE_HOST |
Script Template
#!/bin/bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
cleanup() {
rm -f "$SCRIPT_DIR"/*.tmp 2>/dev/null || true
}
trap cleanup EXIT
error_handler() {
echo "Error on line $1" >&2
exit 1
}
trap 'error_handler $LINENO' ERR
main() {
echo "Running $SCRIPT_NAME"
}
main "$@"
Defensive Scripting
set -euo pipefail
IFS=$'\n\t'
echo "$var"
echo $var
if [[ -f "$file" ]]; then
if [ -f "$file" ]; then
Parameter Expansion
${var:-default}
${var:=default}
${var:?error message}
${var#pattern}
${var##pattern}
${var%pattern}
${var%%pattern}
${var/old/new}
${var//old/new}
${#var}
file="document.txt"
echo "${file%%.*}"
echo "${file##*.}"
Functions
get_user_name() {
local user_id=$1
local name
name=$(grep "^${user_id}:" /etc/passwd | cut -d: -f5)
echo "$name"
}
result=$(get_user_name "1000")
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
files=("file1.txt" "file2.txt" "file3.txt")
echo "${files[0]}"
echo "${files[@]}"
echo "${#files[@]}"
for file in "${files[@]}"; do
echo "Processing: $file"
done
declare -A config
config[host]="localhost"
config[port]="8080"
echo "${config[host]}:${config[port]}"
File Operations
while IFS= read -r line; do
echo "Line: $line"
done < "input.txt"
mapfile -t lines < "input.txt"
cat > output.txt <<EOF
Line 1
Line 2
EOF
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT
Error Handling
cleanup() {
echo "Cleaning up..."
rm -f "$temp_file"
}
trap cleanup EXIT
error_handler() {
local line=$1
echo "Error occurred on line $line" >&2
}
trap 'error_handler $LINENO' ERR
if ! command -v python3 &>/dev/null; then
echo "Error: python3 not found" >&2
exit 1
fi
command1 && command2
command1 || command2
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))
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" "$@"; }
log_info "Starting process"
log_error "Failed to connect"
Debugging
set -x
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
set +x
bash -x script.sh
bash -n script.sh
Common Patterns
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root" >&2
exit 1
fi
cd "$target_dir" || exit 1
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
echo "Processing: $file"
done
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 |
|---|
shellcheck | Static analysis (required) |
shfmt | Code formatting |
bats-core | Testing framework |
bash 5.x | Modern features (avoid macOS default 3.2) |
ShellCheck Usage
shellcheck script.sh
echo $UNQUOTED_VAR
shellcheck -x script.sh
Testing with bats-core
#!/usr/bin/env 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
if [ -f "file.txt" ]; then
echo "File exists"
fi
set -- "apple" "banana" "cherry"
echo "First: $1"
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
local in functions
- 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
--help option
References