shell-dev-practices
Shell scripting best practices for bash, sh, zsh with focus on error handling, portability, and automation.
When & Why to Use This Skill
This Claude skill serves as a comprehensive guide for developing robust, production-grade shell scripts across Bash, Sh, and Zsh environments. It prioritizes reliability and security by enforcing industry standards such as strict mode, POSIX compatibility, and advanced error handling. By integrating these best practices, developers can create portable automation tools and resilient runbooks that minimize runtime failures and simplify debugging in complex IT infrastructures.
Use Cases
- Building fail-safe CI/CD deployment scripts that utilize 'set -euo pipefail' and trap-based cleanup to prevent partial or corrupted releases.
- Standardizing shell script development across engineering teams to ensure scripts run consistently on both macOS and various Linux distributions.
- Refactoring legacy automation scripts to implement secure variable quoting and input validation, reducing vulnerabilities to word splitting and glob expansion.
- Creating professional-grade CLI tools with robust argument parsing using getopts and standardized logging for better observability in automated tasks.
| name | shell-dev-practices |
|---|---|
| description | Shell scripting best practices for bash, sh, zsh with focus on error handling, portability, and automation. |
Shell development practices
Purpose
Guide for shell scripting best practices covering POSIX compatibility, error handling, and robust automation scripts.
When to use
This skill activates when:
- Writing shell scripts
- Creating build/deploy automation
- Writing bash/sh/zsh code
- Debugging shell issues
- Making scripts portable
Core principles
Always use strict mode
#!/usr/bin/env bash
set -euo pipefail
# -e: Exit on error
# -u: Error on undefined variables
# -o pipefail: Fail on pipe errors
Quote all variables
# Bad: Word splitting and glob expansion
echo $filename
rm $path/*
# Good: Properly quoted
echo "$filename"
rm "$path"/*
Error handling
Trap for cleanup
#!/usr/bin/env bash
set -euo pipefail
TEMP_DIR=""
cleanup() {
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
TEMP_DIR=$(mktemp -d)
# Use temp dir...
Check command existence
# Check if command exists
if ! command -v git &> /dev/null; then
echo "Error: git is required but not installed" >&2
exit 1
fi
Validate inputs
validate_args() {
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <filename>" >&2
exit 1
fi
if [[ ! -f "$1" ]]; then
echo "Error: File not found: $1" >&2
exit 1
fi
}
validate_args "$@"
Functions
Function definitions
# Good: Local variables, clear return
process_file() {
local file="$1"
local output=""
if [[ ! -f "$file" ]]; then
return 1
fi
output=$(cat "$file")
echo "$output"
}
Return values
# Use return for status, echo for output
get_value() {
local key="$1"
if [[ -z "$key" ]]; then
return 1
fi
echo "value_for_$key"
}
# Capture output
if value=$(get_value "mykey"); then
echo "Got: $value"
else
echo "Failed to get value"
fi
Conditionals
Test syntax
# Modern syntax with [[]]
if [[ -f "$file" ]]; then
echo "File exists"
fi
# String comparison
if [[ "$string" == "value" ]]; then
echo "Match"
fi
# Numeric comparison
if [[ "$count" -gt 10 ]]; then
echo "Greater than 10"
fi
Common tests
# File tests
[[ -f "$path" ]] # Regular file
[[ -d "$path" ]] # Directory
[[ -e "$path" ]] # Exists
[[ -r "$path" ]] # Readable
[[ -w "$path" ]] # Writable
[[ -x "$path" ]] # Executable
# String tests
[[ -z "$var" ]] # Empty
[[ -n "$var" ]] # Not empty
[[ "$a" == "$b" ]] # Equal
[[ "$a" != "$b" ]] # Not equal
Arrays
# Declare array
declare -a files=()
# Add elements
files+=("file1.txt")
files+=("file2.txt")
# Iterate
for file in "${files[@]}"; do
echo "Processing: $file"
done
# Array length
echo "Count: ${#files[@]}"
Input handling
Reading input
# Read line
read -r line < "$file"
# Read with prompt
read -rp "Enter name: " name
# Read password
read -rsp "Enter password: " password
echo # newline after hidden input
Process arguments
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [-v] [-o output] input"
exit 1
}
verbose=false
output=""
while getopts "vo:" opt; do
case $opt in
v) verbose=true ;;
o) output="$OPTARG" ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
usage
fi
input="$1"
Output
Logging
# Log levels
log_info() { echo "[INFO] $*"; }
log_warn() { echo "[WARN] $*" >&2; }
log_error() { echo "[ERROR] $*" >&2; }
# Usage
log_info "Starting process"
log_error "Failed to open file"
Colors (optional)
# Only use if terminal supports it
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
else
RED=''
GREEN=''
NC=''
fi
echo -e "${GREEN}Success${NC}"
echo -e "${RED}Error${NC}"
Portability
POSIX compatibility
# More portable alternatives
# Instead of: echo -e
printf '%s\n' "$message"
# Instead of: [[ ]]
[ -f "$file" ]
# Instead of: $()
`command` # Though $() is preferred when available
Path handling
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Resolve symlinks
REAL_PATH="$(realpath "$path")"
ShellCheck
Always validate with ShellCheck:
# Install
brew install shellcheck # macOS
apt install shellcheck # Ubuntu
# Run
shellcheck script.sh
Checklist
- Uses
set -euo pipefail - All variables quoted
- Functions use local variables
- Cleanup with trap
- Inputs validated
- ShellCheck passes
Additional resources: