| name | shell-script-guidelines |
| description | Shell scripting best practices for DevSecOps automation. Covers error handling, argument validation, logging, debugging, POSIX compliance, security patterns, and shellcheck best practices. Use when writing bash/sh scripts, automation tools, CI/CD scripts, or deployment automation. |
Shell Script Development Guidelines
Purpose
Comprehensive shell scripting guidelines for DevSecOps engineers, emphasizing reliability, error handling, security, and maintainability in automation scripts.
When to Use This Skill
Automatically activates when you:
- Work with
.sh files
- Mention bash, shell, scripting keywords
- Write automation scripts
- Create deployment scripts
- Build CI/CD pipeline scripts
Quick Reference
Essential Header (Every Script)
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Core Principles
- Fail Fast -
set -euo pipefail in every script
- Validate Everything - Check all arguments and preconditions
- Log Verbosely - Make debugging easy
- Handle Errors - Trap and cleanup properly
- Quote Variables - Always
"$var", never $var
- Test Scripts - Use shellcheck and bats
Robust Error Handling
Basic Error Handling
#!/usr/bin/env bash
set -euo pipefail
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_INVALID_ARGS=2
error() {
echo "ERROR: $*" >&2
exit "${EXIT_ERROR}"
}
if [[ ! -f "${config_file}" ]]; then
error "Config file not found: ${config_file}"
fi
Advanced with Cleanup
#!/usr/bin/env bash
set -euo pipefail
TEMP_FILES=()
cleanup() {
local exit_code=$?
for temp_file in "${TEMP_FILES[@]}"; do
[[ -f "${temp_file}" ]] && rm -f "${temp_file}"
done
if [[ ${exit_code} -eq 0 ]]; then
log "INFO" "Script completed successfully"
else
log "ERROR" "Script failed with exit code ${exit_code}"
fi
exit "${exit_code}"
}
trap cleanup EXIT INT TERM
temp_file=$(mktemp)
TEMP_FILES+=("${temp_file}")
Argument Validation
Basic Argument Parsing
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: ${0##*/} [OPTIONS] <environment> <version>
Deploy application to specified environment.
Arguments:
environment Target environment (dev|staging|prod)
version Application version to deploy
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-n, --dry-run Perform dry run without changes
Examples:
${0##*/} prod v1.2.3
${0##*/} --dry-run staging v1.2.4-beta
EOF
exit 0
}
VERBOSE=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
;;
-v|--verbose)
VERBOSE=true
shift
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
-*)
error "Unknown option: $1"
;;
*)
break
;;
esac
done
if [[ $# -lt 2 ]]; then
error "Missing required arguments. Use --help for usage."
fi
ENVIRONMENT="$1"
VERSION="$2"
case "${ENVIRONMENT}" in
dev|staging|prod)
;;
*)
error "Invalid environment: ${ENVIRONMENT}. Must be dev, staging, or prod."
;;
esac
if [[ ! "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then
error "Invalid version format: ${VERSION}. Expected format: v1.2.3 or v1.2.3-beta"
fi
Logging
Structured Logging Function
#!/usr/bin/env bash
set -euo pipefail
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
LOG_LEVEL="${LOG_LEVEL:-${LOG_LEVEL_INFO}}"
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local level_num
case "${level}" in
DEBUG) level_num=${LOG_LEVEL_DEBUG} ;;
INFO) level_num=${LOG_LEVEL_INFO} ;;
WARN) level_num=${LOG_LEVEL_WARN} ;;
ERROR) level_num=${LOG_LEVEL_ERROR} ;;
*) level_num=${LOG_LEVEL_INFO} ;;
esac
if [[ ${level_num} -ge ${LOG_LEVEL} ]]; then
local color=""
local reset="\033[0m"
case "${level}" in
DEBUG) color="\033[0;36m" ;;
INFO) color="\033[0;32m" ;;
WARN) color="\033[0;33m" ;;
ERROR) color="\033[0;31m" ;;
esac
if [[ ${level_num} -ge ${LOG_LEVEL_ERROR} ]]; then
echo -e "${color}[${timestamp}] [${level}] ${message}${reset}" >&2
else
echo -e "${color}[${timestamp}] [${level}] ${message}${reset}"
fi
fi
}
log "INFO" "Starting deployment"
log "DEBUG" "Environment: ${ENVIRONMENT}"
log "WARN" "This will modify production"
log "ERROR" "Deployment failed"
Safe Command Execution
Running Commands with Validation
#!/usr/bin/env bash
set -euo pipefail
command_exists() {
command -v "$1" >/dev/null 2>&1
}
run_command() {
local cmd="$*"
log "INFO" "Running: ${cmd}"
if [[ "${DRY_RUN}" == "true" ]]; then
log "INFO" "[DRY RUN] Would execute: ${cmd}"
return 0
fi
if ! ${cmd}; then
error "Command failed: ${cmd}"
fi
}
require_command() {
local cmd="$1"
if ! command_exists "${cmd}"; then
error "Required command not found: ${cmd}"
fi
}
require_command "docker"
require_command "kubectl"
require_command "aws"
run_command docker build -t "app:${VERSION}" .
run_command docker push "app:${VERSION}"
File Operations
Safe File Handling
#!/usr/bin/env bash
set -euo pipefail
check_file() {
local file="$1"
if [[ ! -e "${file}" ]]; then
error "File does not exist: ${file}"
fi
if [[ ! -f "${file}" ]]; then
error "Not a regular file: ${file}"
fi
if [[ ! -r "${file}" ]]; then
error "File not readable: ${file}"
fi
}
safe_copy() {
local source="$1"
local dest="$2"
check_file "${source}"
if [[ -f "${dest}" ]]; then
local backup="${dest}.backup.$(date +%Y%m%d_%H%M%S)"
log "INFO" "Creating backup: ${backup}"
cp "${dest}" "${backup}"
fi
if ! cp "${source}" "${dest}"; then
error "Failed to copy ${source} to ${dest}"
fi
log "INFO" "Copied ${source} to ${dest}"
}
atomic_write() {
local content="$1"
local target="$2"
local temp_file
temp_file=$(mktemp)
TEMP_FILES+=("${temp_file}")
echo "${content}" > "${temp_file}"
if ! mv "${temp_file}" "${target}"; then
error "Failed to write to ${target}"
fi
log "INFO" "Wrote to ${target}"
}
Security Practices
Secrets Handling
#!/usr/bin/env bash
set -euo pipefail
API_KEY="sk-abc123..."
PASSWORD="secret123"
API_KEY="${API_KEY:?API_KEY environment variable not set}"
PASSWORD="${PASSWORD:?PASSWORD environment variable not set}"
read_secret() {
local secret_file="$1"
local perms
perms=$(stat -c "%a" "${secret_file}" 2>/dev/null || stat -f "%A" "${secret_file}" 2>/dev/null)
if [[ "${perms}" != "600" ]] && [[ "${perms}" != "400" ]]; then
error "Secret file has insecure permissions: ${secret_file} (${perms})"
fi
cat "${secret_file}"
}
get_aws_secret() {
local secret_name="$1"
local region="${2:-us-east-1}"
aws secretsmanager get-secret-value \
--secret-id "${secret_name}" \
--region "${region}" \
--query 'SecretString' \
--output text
}
Input Sanitization
#!/usr/bin/env bash
set -euo pipefail
validate_alphanumeric() {
local input="$1"
if [[ ! "${input}" =~ ^[a-zA-Z0-9_-]+$ ]]; then
error "Invalid input: ${input}. Only alphanumeric, underscore, and hyphen allowed."
fi
}
sanitize_filename() {
local filename="$1"
filename="${filename##*/}"
filename="${filename//[^a-zA-Z0-9._-]/}"
echo "${filename:0:255}"
}
escape_sql() {
local input="$1"
echo "${input//\'/\'\'}"
}
Debugging
Debug Mode
#!/usr/bin/env bash
if [[ "${DEBUG:-}" == "true" ]]; then
set -x
fi
set -euo pipefail
debug() {
if [[ "${DEBUG:-}" == "true" ]]; then
echo "DEBUG: $*" >&2
fi
}
debug "Environment: ${ENVIRONMENT}"
debug "Version: ${VERSION}"
show_variables() {
log "DEBUG" "Environment Variables:"
log "DEBUG" " ENVIRONMENT: ${ENVIRONMENT}"
log "DEBUG" " VERSION: ${VERSION}"
log "DEBUG" " DRY_RUN: ${DRY_RUN}"
log "DEBUG" " VERBOSE: ${VERBOSE}"
}
Common Patterns
Parallel Execution
#!/usr/bin/env bash
set -euo pipefail
parallel_run() {
local pids=()
local failed=0
for cmd in "$@"; do
${cmd} &
pids+=($!)
done
for pid in "${pids[@]}"; do
if ! wait "${pid}"; then
log "ERROR" "Process ${pid} failed"
failed=1
fi
done
return "${failed}"
}
parallel_run \
"docker build -t app1 ." \
"docker build -t app2 ." \
"docker build -t app3 ."
Retry Logic
#!/usr/bin/env bash
set -euo pipefail
retry() {
local max_attempts="$1"
shift
local cmd="$*"
local attempt=1
local delay=1
while [[ ${attempt} -le ${max_attempts} ]]; do
log "INFO" "Attempt ${attempt}/${max_attempts}: ${cmd}"
if ${cmd}; then
return 0
fi
if [[ ${attempt} -lt ${max_attempts} ]]; then
log "WARN" "Command failed, retrying in ${delay}s..."
sleep "${delay}"
delay=$((delay * 2))
fi
attempt=$((attempt + 1))
done
error "Command failed after ${max_attempts} attempts: ${cmd}"
}
retry 3 curl -f https://api.example.com/health
Testing
Shellcheck Integration
shellcheck script.sh
readonly VERSION="1.0.0"
BATS Testing
#!/usr/bin/env bats
setup() {
export ENVIRONMENT="test"
export VERSION="v1.0.0"
export DRY_RUN="true"
}
@test "script validates environment" {
run ./deploy.sh invalid v1.0.0
[ "$status" -eq 1 ]
[[ "$output" =~ "Invalid environment" ]]
}
@test "script validates version format" {
run ./deploy.sh prod invalid-version
[ "$status" -eq 1 ]
[[ "$output" =~ "Invalid version format" ]]
}
@test "script runs successfully in dry-run mode" {
run ./deploy.sh --dry-run prod v1.0.0
[ "$status" -eq 0 ]
[[ "$output" =~ "DRY RUN" ]]
}
Complete Example
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly BACKUP_DIR="/tmp/db-backups"
readonly S3_BUCKET="company-db-backups"
TEMP_FILES=()
log() {
local level="$1"
shift
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${timestamp}] [${level}] $*" >&2
}
error() {
log "ERROR" "$*"
exit "${EXIT_ERROR}"
}
cleanup() {
local exit_code=$?
for temp_file in "${TEMP_FILES[@]}"; do
[[ -f "${temp_file}" ]] && rm -f "${temp_file}"
done
[[ -d "${BACKUP_DIR}" ]] && rm -rf "${BACKUP_DIR}"
if [[ ${exit_code} -eq 0 ]]; then
log "INFO" "Backup completed successfully"
else
log "ERROR" "Backup failed"
fi
exit "${exit_code}"
}
trap cleanup EXIT INT TERM
main() {
local environment="$1"
local encrypt="${2:-false}"
log "INFO" "Starting backup for ${environment}"
require_command "pg_dump"
require_command "aws"
mkdir -p "${BACKUP_DIR}"
local backup_file="${BACKUP_DIR}/db-${environment}-$(date +%Y%m%d_%H%M%S).sql"
log "INFO" "Creating backup: ${backup_file}"
pg_dump -h "${DB_HOST}" -U "${DB_USER}" "${DB_NAME}" > "${backup_file}"
if [[ "${encrypt}" == "true" ]]; then
log "INFO" "Encrypting backup"
gpg --encrypt --recipient "${GPG_KEY}" "${backup_file}"
backup_file="${backup_file}.gpg"
fi
log "INFO" "Uploading to S3"
aws s3 cp "${backup_file}" "s3://${S3_BUCKET}/${environment}/"
log "INFO" "Backup completed"
}
main "$@"
Resources