- Posted on
- • Artificial Intelligence
Artificial Intelligence Bash Coding Standards for Production
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Bash Coding Standards for Production
Your CI just merged an AI-generated Bash script. It “worked on my machine,” passed a basic smoke test, and then a week later a cron job silently skipped half its work because of an unquoted variable with a space in it. Sound familiar?
AI is excellent at producing Bash quickly—but speed without standards invites subtle, costly bugs. This article distills production-grade Bash coding standards you can hand to AI (and your team) to ensure maintainability, safety, and reproducibility. You’ll get a hardened script template, actionable guidelines, tooling you can install with apt, dnf, and zypper, and examples you can paste into code review comments.
TL;DR: A production-ready Bash script template
Start every AI-generated Bash script from a hardened template. It encodes error handling, logging, cleanup, and argument parsing so you don’t re-learn old lessons.
#!/usr/bin/env bash
# shellcheck disable=SC2155
# Require modern Bash
if ((BASH_VERSINFO[0] < 4)); then
printf 'Error: Bash >= 4 is required (found %s)\n' "${BASH_VERSION:-unknown}" >&2
exit 1
fi
set -Eeuo pipefail
IFS=$'\n\t'
# Optional: pin a predictable locale for parsing commands
export LC_ALL=C.UTF-8 LANG=C.UTF-8
# Globals
SCRIPT_NAME=${0##*/}
TMP_DIR=""
log() { printf '[%(%Y-%m-%dT%H:%M:%S%z)T] %s\n' -1 "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
die() { local code=${2:-1}; printf '[ERROR] %s\n' "$1" >&2; exit "$code"; }
cleanup() {
[[ -n "${TMP_DIR:-}" && -d "$TMP_DIR" ]] && rm -rf -- "$TMP_DIR"
}
trap cleanup EXIT INT TERM
trap 'die "Failure at line $LINENO: $BASH_COMMAND" $?' ERR
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [-n] -i INPUT -o OUTPUT
Options:
-i FILE Input file (required)
-o DIR Output directory (required)
-n Dry-run (no changes)
-h Help
EOF
}
# Defaults
DRY_RUN=0
INPUT=""
OUTPUT=""
while getopts ":i:o:nh" opt; do
case "$opt" in
i) INPUT=$OPTARG ;;
o) OUTPUT=$OPTARG ;;
n) DRY_RUN=1 ;;
h) usage; exit 0 ;;
:) die "Option -$OPTARG requires an argument" ;;
\?) die "Unknown option: -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
[[ -n "$INPUT" ]] || die "Missing -i INPUT"
[[ -n "$OUTPUT" ]] || die "Missing -o OUTPUT"
require_cmd awk
require_cmd jq # Example external dep if you parse JSON
TMP_DIR=$(mktemp -d -t "${SCRIPT_NAME}.XXXXXX")
main() {
log "Starting: INPUT=$INPUT OUTPUT=$OUTPUT DRY_RUN=$DRY_RUN"
# Safe file loop example (null-safe)
find "$OUTPUT" -maxdepth 1 -type f -name '*.txt' -print0 |
while IFS= read -r -d '' path; do
[[ $DRY_RUN -eq 1 ]] && { log "[DRY] Would process: $path"; continue; }
log "Processing: $path"
# do work...
done
log "Done"
}
main "$@"
Copy this into your repo as a starter for AI and humans alike.
Why “AI Bash standards” are necessary
AI often glosses over quoting, locale, and IFS rules that break with spaces, Unicode, or odd filenames.
Error handling defaults are unsafe: pipelines hide failures, traps are missing, and temporary directories aren’t cleaned up.
Reproducibility is weak: scripts rely on unstated external tools or system-specific behavior.
Code reviews are expensive if every script uses a different pattern. A standard template + linter/test suite makes reviews faster and safer.
Actionable standards (with real-world examples)
1) Turn on strict, predictable execution
Always start with
set -Eeuo pipefailandIFS=$'\n\t'.Use
trapto surface failing commands and clean up temp files.Pin locale if parsing command output:
export LC_ALL=C.UTF-8 LANG=C.UTF-8.
Unsafe:
for f in $(ls *.txt); do cp $f /backup; done
Safe and robust:
shopt -s nullglob
for f in ./*.txt; do
cp -- "$f" /backup/
done
shopt -u nullglob
Or null-safe with find:
find . -maxdepth 1 -type f -name '*.txt' -print0 |
while IFS= read -r -d '' f; do
cp -- "$f" /backup/
done
Why: Word splitting breaks on spaces/newlines; nullglob avoids copying literal *.txt; -- prevents weird filenames starting with - from being treated as flags.
2) Quote, array, and pipe safely
Double-quote all variable expansions:
"$var".Prefer arrays to
for x in $(cmd):mapfile -t arr < <(cmd).Make pipelines fail fast with
set -o pipefail.
Bad:
grep pattern file | awk '{print $1}'
Better with failure surfaced:
set -o pipefail
awk '{print $1}' <(grep -F -- 'pattern' file)
Or check explicitly:
if ! grep -Fq -- 'pattern' file; then
die "pattern not found"
fi
3) Parse inputs defensively; never eval
Use
getoptsfor flags and arguments.Validate required parameters before doing work.
Use parameter expansions for defaults and required vars:
"${VAR:?VAR is required}","${VAR:-default}".Avoid
eval, backticks, and untrusted input in command strings.
Bad:
DIR=$1
rm -rf $DIR/*
Safe:
DIR=${1:?Usage: script DIR}
[[ -d "$DIR" ]] || die "Not a directory: $DIR"
rm -rf -- "$DIR"/*
4) Make linting and testing non-optional
Lint all scripts in CI with ShellCheck.
Add smoke tests with Bats for critical scripts (help text, basic flows, failure cases).
Fail the build if linters/tests fail.
ShellCheck example:
shellcheck -x ./scripts/*.sh
Bats test example:
# tests/my_script.bats
#!/usr/bin/env bats
setup() { chmod +x ./my_script.sh; }
@test "shows usage with -h" {
run ./my_script.sh -h
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
}
@test "fails without required args" {
run ./my_script.sh
[ "$status" -ne 0 ]
[[ "$output" == *"Missing -i INPUT"* ]]
}
GitHub Actions CI snippet:
name: bash-ci
on: [push, pull_request]
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y shellcheck bats
- run: shellcheck -x $(git ls-files '*.sh')
- run: bats -r tests
5) Declare and check dependencies explicitly
Fail early if required commands are missing:
require_cmd jq.Log versions at startup for traceability.
Prefer stable flags and formats; specify
--before path args.
Example:
require_cmd rsync
log "rsync version: $(rsync --version | head -1)"
rsync -a --delete -- "$SRC_DIR"/ "$DEST_DIR"/
Install the tooling (apt, dnf, zypper)
Install ShellCheck (linter), Bats (tests), and jq (useful for JSON parsing in scripts).
- Debian/Ubuntu (apt):
sudo apt-get update
sudo apt-get install -y shellcheck bats jq
- Fedora/CentOS Stream/RHEL (dnf):
# On RHEL/CentOS, enable EPEL for ShellCheck and Bats if needed:
sudo dnf install -y epel-release || true
sudo dnf install -y ShellCheck bats jq
- openSUSE Leap/Tumbleweed (zypper):
sudo zypper refresh
sudo zypper install -y ShellCheck bats jq
Note:
Package names are case-sensitive on some distros (e.g.,
ShellCheckon Fedora/openSUSE).If
batsis outdated in your repo, considerbats-corefrom source, but the above works for most CI scenarios.
Before/After: Fixing a common AI-generated snippet
AI often writes:
files=$(find "$DIR" -type f)
for f in $files; do
echo "Processing $f"
done
Problems: breaks on spaces/newlines; fails if find prints nothing but set -u is on; loses exit codes.
Production-grade rewrite:
find "$DIR" -type f -print0 |
while IFS= read -r -d '' f; do
printf 'Processing %s\n' "$f"
done
Or if you need an array:
mapfile -d '' -t files < <(find "$DIR" -type f -print0)
for f in "${files[@]}"; do
printf 'Processing %s\n' "$f"
done
A prompt you can hand to your AI
If you’re using an AI to generate Bash, include this instruction:
Write a Bash script using:
- #!/usr/bin/env bash, set -Eeuo pipefail, IFS=$'\n\t'
- traps for ERR and EXIT with cleanup
- getopts for args; print Usage on -h
- double-quote all expansions; use arrays and find -print0 for filenames
- no eval/backticks; prefer printf over echo
- require_cmd for external deps; log versions at start
- ready for ShellCheck (no SC20xx violations)
Return only the script, no commentary.
This reduces unsafe defaults before you even review the code.
Conclusion and next steps (CTA)
Production Bash is less about clever one-liners and more about repeatable, defensive engineering. Standardize now:
1) Copy the hardened template into your repo’s scripts directory.
2) Add ShellCheck and Bats to CI and make them blocking.
3) Require the standards in code review (or bake them into your AI prompts).
4) Refactor your top 3 critical scripts using these patterns this week.
Your future incidents channel will thank you.
If you want a quick start, paste your script into ShellCheck, write a two-test Bats suite, and run both locally. Need help hardening a specific script? Share the snippet and the intended behavior, and I’ll propose a production-ready rewrite.