This document defines the Bash coding style used for scripts in this project.
- Scripts must run on Bash 4.0+ and be portable across Linux (Ubuntu) and modern macOS with updated Bash.
⚠️ Incompatibility: macOS ships with Bash 3.2 by default. Features like associative arrays will not work there. Users must install a newer Bash.- Prefer POSIX-compliant constructs, but Bash-specific features (arrays,
[[ ]],${var:-default}) are allowed where beneficial. - Security is the top priority:
- Quote all variable expansions:
"${var}". - Avoid
eval(only allowed with explicit justification). - Use
mktempfor temporary files/directories. - Sanitize and validate all user input, paths, and environment variables.
- Quote all variable expansions:
#!/usr/bin/env bashEach script must include a complete header:
###############################################################################
# NAME : script_name.sh
# DESCRIPTION : <brief description>
# AUTHOR : Your Name
# DATE CREATED : YYYY-MM-DD
###############################################################################
# EDIT HISTORY:
# DATE | EDITED BY | DESCRIPTION
# -----------|--------------|-----------------------------------------------
# YYYY-MM-DD | Your Name | Initial creation
###############################################################################set -uo pipefail
IFS=$'\n\t'- ❌ Never use
set -e— it causes hidden, hard-to-debug failures when commands fail inside subshells, conditionals, or pipelines. - ✅ Instead, always use explicit error checks (
if,||,$?). This makes the script’s behavior predictable and debuggable.
- Functions →
snake_case_name - Variables →
snake_case - Constants →
UPPERCASE(no leading underscores) - File-scope constants →
readonly
Examples:
function validate_paths_writable() {
# Implementation
return 0
}
log_path="/tmp/logs"
readonly log_path
VERSION="1.0.0"
PASS=0
FAIL=1
readonly VERSION PASS FAILEvery function must include proc-doc comments:
###############################################################################
# function_name
#------------------------------------------------------------------------------
# Purpose : What the function does
# Usage : function_name [args]
# Arguments:
# $1 : Description
# $2 : Description
# Returns : exit code meaning
# Globals : vars/functions required
###############################################################################
function function_name() {
return 0
}Rules:
- Functions must return status codes only (0 = success, non-zero = failure).
- Functions must not return data via
return. Usestdoutor arrays for data.
- Structured logging with levels: INFO, WARN, ERROR, FAIL, PASS, DEBUG, VDEBUG.
- Colors if
ENABLE_COLOR=1. - File logging if
ENABLE_FILE_LOGGING=1.
- Provide fallback loggers (
info,warn, etc.) if not defined.
- Manual error checks only (no
set -e). - Use helpers:
validate_commands,validate_env_vars,validate_paths_*. - Exit codes must be explicit.
Example:
if ! command -v foo > /dev/null 2>&1; then
error "Missing required command: foo"
exit 1
fi- Use “display pipes”** for multi-line pipelines.
Rationale: placing|at the start of the line makes diffs cleaner, aligns operators visually, and avoids trailing whitespace issues.
grep "pattern" file.txt \
| sort \
| uniq \
| awk '{ print $1 }'-
Inline short pipes (
cmd1 | cmd2) are fine for simple cases. -
Avoid Useless Use of Cat (UUOC) — piping a file into a command like
cat file | while ...creates an unnecessary process and may introduce subshell issues. Instead, redirect input directly:
# BAD (UUOC)
cat file | while read -r line; do
printf '%s
' "${line}"
done
# GOOD
while read -r line; do
printf '%s
' "${line}"
done < file- Be aware of subshell scope loss:
Variables modified inside a pipeline will not persist outside.
# BAD: result is empty because the while loop runs in a subshell
echo "hello" | while read -r word; do
result="${word}"
done
printf 'Result: %s
' "${result}" # prints empty
# GOOD: process substitution avoids subshell
while read -r word; do
result="${word}"
done < <(echo "hello")
printf 'Result: %s
' "${result}" # prints "hello"Why: Space-delimited strings are unsafe — filenames or arguments containing spaces will break parsing. Arrays handle arbitrary input safely and preserve element boundaries.
- ✅ Use arrays for lists:
arr=("a" "b") - ❌ Never use space-delimited strings for lists:
list="a b c"
Iteration:
arr=("alpha" "beta" "gamma")
for item in "${arr[@]}"; do
printf '%s\n' "${item}"
doneWhy: [[ ... ]] is safer and more predictable than [ ... ].
-
[[ ]]does not perform word splitting or pathname expansion. -
[ ]can break if variables contain-,], or glob characters. -
(( ))is preferred for arithmetic because it's cleaner and avoids string parsing. -
Always use
[[ ... ]](not[ ... ]). -
Arithmetic: use
(( ... )). -
Avoid
[with<or>(interpreted as redirection).
Example:
if [[ "${x}" -gt 3 ]]; then
printf 'x > 3\n'
fi- ✅ Use
$(...)(nestable, readable). - ❌ Ban legacy backticks — they are harder to read, cannot be nested cleanly, and behave inconsistently across shells.
Example:
date_str="$(date +%Y-%m-%d)"
printf 'Today is %s
' "${date_str}"- Always use
read -r(prevents\escaping). - Quote variables when using
read. - Avoid
for f in $(ls)— it breaks on spaces, globbing, and unusual filenames, and spawns an unnecessarylsprocess. ✅ Usefor f in *instead, which is safer and more portable.
Example:
while read -r line; do
printf '%s\n' "${line}"
done < input.txt
for f in *; do
printf 'File: %s\n' "${f}"
done- ✅ Always use
printf(portable, predictable). - ❌ Ban
echo -e— behavior varies across shells (not portable) and can cause unexpected escapes.
Example:
printf 'Hello %s' "${name}"Why: Forking external commands (grep, awk, cut) is slower and less portable when the same result can be achieved with Bash built-ins.
Prefer Bash parameter expansion or built-in string manipulation whenever possible. External tools should only be used when Bash cannot handle the task.
- Avoid
grep | awk | cutunnecessarily — use Bash parameter expansion when possible. - Use external commands only when Bash features cannot handle the task.
Example:
# BAD
echo "${file}" | cut -d. -f1
# GOOD
base="${file%%.*}"
printf 'Base filename: %s\n' "${base}"-
Scripts must trap
SIGINTandSIGTERMgracefully. -
Exit codes should follow the 128 + signal number convention:
- SIGINT (2) → 128 + 2 = 130
- SIGTERM (15) → 128 + 15 = 143
This makes it clear to parent processes or monitoring systems that the script terminated due to a signal.
Example:
function exit_on_signal_sigint() {
warn "Program interrupted (SIGINT)."
exit 130
}
function exit_on_signal_sigterm() {
warn "Program terminated (SIGTERM)."
exit 143
}
trap exit_on_signal_sigint SIGINT
trap exit_on_signal_sigterm SIGTERMScripts should validate environment and dependencies early:
validate_commandsvalidate_env_varsvalidate_paths_readablevalidate_paths_writable
- All code must pass:
- ShellCheck (with project
.shellcheckrc) - shfmt with options:
shfmt -i 4 -ci -bn -kp -sr -ln bash
- ShellCheck (with project
- Indentation: 4 spaces (no tabs).
This style guide enforces:
- Safety-first scripting (no
set -e, validated inputs, traps). - Consistency (4-space indent, snake_case, UPPERCASE constants).
- Readability (structured logging, proc-docs, display pipes).
- Portability (Bash 4+, avoid non-portable constructs).