Essential patterns for reliable shell scripts - portable shebangs, error handling, quoting rules, functions, testing with Bats, and ShellCheck integration.
Industry-standard shell scripting practices for writing reliable, maintainable, and secure bash scripts.
#!/usr/bin/env bash
# Finds bash in PATH - works across systems
# Never use #!/bin/bash (non-portable)
#!/usr/bin/env bash
set -euo pipefail
# -e: Exit on error | -u: Exit on undefined | -o pipefail: Exit on pipe failure
trap cleanup EXIT ERR
cleanup() {
local exit_code=$?
rm -f "$temp_file"
exit "$exit_code"
}
# ALWAYS quote variables to prevent word splitting
echo "$variable" # Correct
echo $variable # Wrong - word splitting
# Arrays need different quoting
files=("file 1.txt" "file 2.txt")
for file in "${files[@]}"; do
echo "$file"
done
# Command substitution
current_dir="$(pwd)" # Modern (correct)
current_dir=`pwd` # Deprecated (avoid)
function_name() {
local param1="$1"
local param2="${2:-default}"
if [[ -z "$param1" ]]; then
echo "Error: param1 required" >&2
return 1
fi
echo "Result"
return 0
}
usage() {
cat << USAGE
Usage: ${0##*/} [-v] [-o OUTPUT] FILE
Options:
-v Verbose mode
-o OUTPUT Output file
-h Show this help
USAGE
}
verbose=false
output=""
while getopts "vo:h" opt; do
case "$opt" in
v) verbose=true ;;
o) output="$OPTARG" ;;
h) usage; exit 0 ;;
*) usage >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
file="${1:?Error: FILE required}"
readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# Safe file checks
[[ -f "$file" ]] # File exists and is regular
[[ -d "$dir" ]] # Directory exists
[[ -r "$file" ]] # File is readable
# Safe temp files
temp_file=$(mktemp)
temp_dir=$(mktemp -d)
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < "$file"
# Check if command exists
command_exists() { command -v "$1" >/dev/null 2>&1; }
# Retry logic
retry() {
local max=3 attempt=1
while ((attempt <= max)); do
"$@" && return 0
echo "Attempt $attempt failed" >&2
((attempt++)); sleep 2
done
return 1
}
# Confirm prompt
confirm() {
read -rp "${1:-Are you sure?} [y/N] " response
[[ "$response" =~ ^[Yy]$ ]]
}
# Install: apt-get install shellcheck | brew install shellcheck
shellcheck script.sh
# Disable specific warnings (sparingly)
# shellcheck disable=SC2086
echo $unquoted_on_purpose
# CI/CD integration
find . -name "*.sh" -exec shellcheck {} +
#!/usr/bin/env bash
#
# Script Name: backup-manager.sh
# Description: Manages automated backups with rotation
# Version: 1.0.0
#
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly VERSION="1.0.0"
readonly EXIT_SUCCESS=0 EXIT_ERROR=1 EXIT_USAGE=2
main() {
parse_arguments "$@"
validate_dependencies
perform_backup
log_info "Backup completed successfully"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
error_handler() {
local line_no=$1 error_code=$2
log_error "Error on line $line_no: exit code $error_code"
cleanup
exit "$error_code"
}
trap 'error_handler ${LINENO} $?' ERR
cleanup() {
local exit_code=$?
[[ -n "${temp_dir:-}" && -d "$temp_dir" ]] && rm -rf "$temp_dir"
[[ -n "${lock_file:-}" && -f "$lock_file" ]] && rm -f "$lock_file"
return "$exit_code"
}
trap cleanup EXIT INT TERM
# Lock file pattern (prevent concurrent runs)
acquire_lock() {
local lock_file="$1" lock_fd=200
eval "exec $lock_fd>\"$lock_file\""
flock -n "$lock_fd" || { log_error "Another instance running"; exit 1; }
}
readonly LOG_LEVEL_DEBUG=0 LOG_LEVEL_INFO=1 LOG_LEVEL_WARN=2 LOG_LEVEL_ERROR=3
log_level=$LOG_LEVEL_INFO
log_message() {
local level=$1 color=$2 prefix=$3; shift 3
if ((level >= log_level)); then
echo -e "${color}[${prefix}]${NC} $*" >&2
[[ -w "$(dirname "${LOG_FILE:-/tmp/log}")" ]] && \
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${prefix}] $*" >> "${LOG_FILE:-/tmp/log}"
fi
}
log_debug() { log_message $LOG_LEVEL_DEBUG "$BLUE" "DEBUG" "$@"; }
log_info() { log_message $LOG_LEVEL_INFO "$GREEN" "INFO" "$@"; }
log_warn() { log_message $LOG_LEVEL_WARN "$YELLOW" "WARN" "$@"; }
log_error() { log_message $LOG_LEVEL_ERROR "$RED" "ERROR" "$@"; }
validate_file() {
local file="$1" desc="${2:-File}"
[[ -z "$file" ]] && { log_error "$desc path is empty"; return 1; }
[[ ! -f "$file" ]] && { log_error "$desc does not exist: $file"; return 1; }
[[ ! -r "$file" ]] && { log_error "$desc is not readable: $file"; return 1; }
return 0
}
validate_directory() {
local dir="$1" desc="${2:-Directory}" writable="${3:-false}"
[[ -z "$dir" ]] && { log_error "$desc path is empty"; return 1; }
[[ ! -d "$dir" ]] && { log_error "$desc does not exist: $dir"; return 1; }
[[ "$writable" == "true" && ! -w "$dir" ]] && { log_error "$desc not writable: $dir"; return 1; }
return 0
}
validate_number() {
local val="$1" desc="${2:-Value}" min="${3:-}" max="${4:-}"
[[ ! "$val" =~ ^[0-9]+$ ]] && { log_error "$desc must be a number: $val"; return 1; }
[[ -n "$min" && "$val" -lt "$min" ]] && { log_error "$desc must be >= $min"; return 1; }
[[ -n "$max" && "$val" -gt "$max" ]] && { log_error "$desc must be <= $max"; return 1; }
return 0
}
validate_dependencies() {
local missing=() required=(rsync gzip tar date mktemp)
for cmd in "${required[@]}"; do
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
done
if ((${#missing[@]} > 0)); then
log_error "Missing required commands: ${missing[*]}"
exit 1
fi
}
check_optional_tool() {
local tool="$1" fallback="$2"
command -v "$tool" >/dev/null 2>&1 && echo "$tool" || echo "$fallback"
}
# test-script.bats
#!/usr/bin/env bats
setup() {
TEST_DIR="$(mktemp -d)"
source ./script.sh
}
teardown() {
rm -rf "$TEST_DIR"
}
@test "validate_file succeeds for existing file" {
touch "$TEST_DIR/test.txt"
run validate_file "$TEST_DIR/test.txt"
[ "$status" -eq 0 ]
}
@test "validate_file fails for missing file" {
run validate_file "$TEST_DIR/missing.txt"
[ "$status" -eq 1 ]
[[ "$output" =~ "does not exist" ]]
}
# 1. Never eval user input
case "$user_input" in
start) start_service ;;
stop) stop_service ;;
*) log_error "Invalid command" ;;
esac
# 2. Sanitize paths
sanitize_path() {
local path allowed_dir="/home/user/allowed"
path="$(realpath -m "$1")"
[[ "$path" != "$allowed_dir"* ]] && { log_error "Path outside allowed"; return 1; }
echo "$path"
}
# 3. Secure temporary files
temp_file="$(mktemp)" && chmod 600 "$temp_file"
# 4. Avoid command injection - use arrays
local -a files
mapfile -t files < <(find . -name "${pattern}*.txt")
# 5. Validate environment
[[ "$PATH" =~ (^|:)\.(:|$) ]] && { log_error "PATH contains ."; return 1; }
# 6. Secure password handling
read_password() {
read -rsp "Enter password: " password; echo >&2
some_command <<< "$password"
unset password
}
# Use built-ins instead of external commands
echo "${file%/*}" # Fast (built-in)
dirname "$file" # Slow (spawns process)
# Avoid unnecessary subshells
count=$(wc -l < file.txt) # Fast
count=$(cat file.txt | wc -l) # Slow
# Use bash built-ins for string operations
result="${item^^}" # Uppercase (Bash 4+)
# Batch operations
cp *.txt /dest/ # Single operation
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
CYGWIN*|MINGW*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
portable_sed() {
[[ "$(detect_os)" == "macos" ]] && sed -i '' "$@" || sed -i "$@"
}
| Code | Issue | Fix |
|------|-------|-----|
| SC2086 | Unquoted variable | echo "$var" |
| SC2006 | Backticks deprecated | $(cmd) |
| SC2155 | Declare and assign separately | local var; var=$(cmd) |
| SC2034 | Unused variable | Prefix with _ |
| SC2064 | Unquoted trap | trap 'cleanup' EXIT |
See REFERENCE.md for:
config/.shellcheckrc - ShellCheck configurationtemplates/script-template.sh - Production script templatetemplates/library.sh - Reusable function librarytemplates/test-template.bats - Bats testing templatescripts/install-shell-tools.sh - Automated tooling setupresources/shell-best-practices.md - Comprehensive reference#!/usr/bin/env bashset -euo pipefailtrap cleanup EXIT"$var" not $var[[ instead of [ for tests$()Last Updated: 2025-12-30 Skill Version: 2.0.0
npx skills add williamzujkowski/shell-scripting-standards下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer