Thank you for your interest in contributing! This guide covers everything you need to add scripts, fix bugs, or improve the test suite.
# Clone the repo
git clone https://github.com/wesleyscholl/bash-scripts.git
cd bash-scripts
# Install BATS test framework (submodules are already committed)
# Run the full test suite
bats $(find tests -name '*.bats' -not -path '*/test_helper/*' | sort)bash-scripts/
βββ scripts/
β βββ lib/
β β βββ utils.sh # Shared helpers β log_info, log_error, check_dependency
β βββ backup/ # Backup & data-protection scripts
β βββ ci-cd/ # CI/CD pipeline scripts
β βββ devops/ # Docker, Git, infrastructure scripts
β βββ kubernetes/ # kubectl / k8s scripts
β βββ monitoring/ # Alerting and observability scripts
β βββ notifications/ # Slack, email, and log aggregation
β βββ system/ # OS-level health and resource scripts
β βββ utils/ # General-purpose utilities
βββ tests/
βββ test_helper/ # BATS support libraries (bats-support, bats-assert)
βββ utils.bats # Tests for lib/utils.sh
βββ <category>/ # One .bats file per script, mirroring scripts/
| Category | Use for |
|---|---|
backup/ |
Data backup, dump, rotation |
ci-cd/ |
Build pipelines, deployment automation |
devops/ |
Docker, Git, infrastructure provisioning |
kubernetes/ |
kubectl operations, pod management |
monitoring/ |
Alerting, health checks, cost monitoring |
notifications/ |
Slack, email, log aggregation |
system/ |
OS resources, process management |
utils/ |
General-purpose helpers, secret management |
Every script must follow this structure:
#!/bin/bash
# script-name.sh β One-line description
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/utils.sh
source "${SCRIPT_DIR}/../lib/utils.sh"
# --- default values ---
MY_FLAG=""
DRY_RUN=false
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Short description of what this script does.
Options:
--my-flag VALUE Description (env: MY_ENV_VAR)
--dry-run Show what would happen, make no changes
-h, --help Show this help message
Examples:
$(basename "$0") --my-flag value
$(basename "$0") --dry-run
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--my-flag) MY_FLAG="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage; exit 0 ;;
*) log_error "Unknown option: $1"; usage; exit 1 ;;
esac
done
# --- validate required args ---
if [[ -z "$MY_FLAG" ]]; then
log_error "--my-flag is required."
usage
exit 1
fi
# --- main logic ---
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would do X with ${MY_FLAG}."
exit 0
fi
# do the real work here
exit 0set -euo pipefailβ mandatory on every script.- Never put
exitinsideusage(). Callusage; exit 0orusage; exit 1at the call sites. - Exit codes:
0= success,1= bad user input / missing argument,2= runtime / system error. - Accept
--dry-runon any script that modifies state. - Accept
--help/-hthat prints a usage example. - For
--quietsupport, redirectlog_infocalls behind a flag check where appropriate. - Source
utils.shusing"${SCRIPT_DIR}/../lib/utils.sh"(scripts live one level belowscripts/). - Make executable:
chmod +x scripts/<category>/your-script.sh.
macOS ships with bash 3.2. Avoid:
| Dangerous | Safe alternative |
|---|---|
mapfile / readarray |
while IFS= read -r line; do arr+=("$line"); done < <(cmd) |
Empty array under set -u: "${arr[@]}" |
Always pre-populate arrays, or check "${#arr[@]}" -gt 0 first |
[[ str =~ regex ]] with complex regex |
Use grep -qE or awk |
Every new script requires a corresponding BATS test file.
scripts/monitoring/my-alert.sh β tests/monitoring/my-alert.bats
#!/usr/bin/env bats
load "${BATS_TEST_DIRNAME}/../test_helper/bats-support/load"
load "${BATS_TEST_DIRNAME}/../test_helper/bats-assert/load"
setup() {
export SCRIPT_PATH="${BATS_TEST_DIRNAME}/../../scripts/<category>/my-script.sh"
export BINSTUB="${BATS_TEST_TMPDIR}/bin"
mkdir -p "$BINSTUB"
# Create stubs for external commands (kubectl, aws, docker, etc.)
cat > "${BINSTUB}/mycmd" <<'STUB'
#!/bin/bash
echo "stub output"
exit 0
STUB
chmod +x "${BINSTUB}/mycmd"
export PATH="${BINSTUB}:${PATH}"
}
# Required tests β every script needs these four:
@test "script exists and is executable" {
[ -f "$SCRIPT_PATH" ]
[ -x "$SCRIPT_PATH" ]
}
@test "--help exits 0 and prints usage" {
run "$SCRIPT_PATH" --help
assert_success
assert_output --partial "Usage:"
}
@test "exits 1 without required argument" {
run "$SCRIPT_PATH"
assert_failure
}
@test "set -euo pipefail is present" {
grep -q 'set -euo pipefail' "$SCRIPT_PATH"
}
# Add behavioural tests below:
@test "dry-run exits 0 and prints dry-run message" {
run "$SCRIPT_PATH" --required-arg value --dry-run
assert_success
assert_output --partial "dry-run"
}tests/utils.bats β test_helper at tests/test_helper/
scripts at scripts/
tests/subdir/foo.bats β test_helper at ${BATS_TEST_DIRNAME}/../test_helper/ (one level up)
scripts at ${BATS_TEST_DIRNAME}/../../scripts/ (two levels up)
- Create executable stubs in
${BATS_TEST_TMPDIR}/bin/and prepend to$PATH. - Stubs should return appropriate exit codes and predictable output.
- Never require live external services (AWS, GCP, Kubernetes) in tests.
- Use
teardown()if you create files outsideBATS_TEST_TMPDIR.
# Full suite
find tests -name '*.bats' -not -path '*/test_helper/*' | sort | xargs bats
# Single file
bats tests/monitoring/aws-cost-alert.bats
# Single category
bats tests/kubernetes/
# Verbose (shows test names as they run)
find tests -name '*.bats' -not -path '*/test_helper/*' | sort | xargs bats --tapAll scripts are linted with ShellCheck. Install it and run:
shellcheck scripts/**/*.sh scripts/lib/utils.shThe CI pipeline runs ShellCheck on every pull request and fails on any warning.
This repo follows Conventional Commits:
type(scope): short description
Body (optional) β explain why, not what.
| Type | Use for |
|---|---|
feat |
New script or feature |
fix |
Bug fix in a script or test |
test |
New or updated BATS tests only |
docs |
Documentation only |
refactor |
Code change with no behaviour change |
chore |
CI, tooling, dependency updates |
Scope = the script name or category (e.g. feat(db-backup), fix(monitoring), test(kubernetes)).
Before opening a PR, verify:
- New script follows the standard template (see above).
-
set -euo pipefailpresent. -
--helpworks and includes an example. -
--dry-runimplemented for any state-modifying script. - Script is executable (
chmod +x). - Corresponding
.batstest file exists in the matchingtests/<category>/directory. - All four required tests are present (exists, --help, missing-arg, set -euo).
- Full suite passes locally:
find tests -name '*.bats' -not -path '*/test_helper/*' | sort | xargs bats. -
shellcheckproduces zero warnings on the new/modified script. - Commit message follows Conventional Commits format.