- Posted on
- • Artificial Intelligence
Artificial Intelligence Bash Coding Standards
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Bash Coding Standards: Write Scripts Humans, Linters, and LLMs Can Trust
Ever asked an AI assistant to fix a Bash script and got back something that “works on my machine,” but breaks in production? The problem isn’t just the AI—it’s that many shell scripts are ambiguous, inconsistent, and hard to reason about. Good standards reduce ambiguity. Great standards make your scripts predictable for humans, robust for CI, and legible for AI tools and large language models (LLMs).
This guide explains why AI-aware Bash standards matter and gives you a practical set of conventions, tools, and examples to adopt today.
Why AI-aware Bash standards?
Bash is permissive by default. Small mistakes (like missing quotes) can silently become big outages. Strict, consistent patterns make behavior obvious.
Linters and formatters can prove entire classes of bugs don’t exist. AIs also rely heavily on consistent structure, descriptive comments, and explicit dependencies to reason correctly.
Teams move faster with confidence when scripts are testable, documented, and observable. That applies to humans and machines.
Below are five actionable practices, with real-world snippets you can paste into your scripts and your CI today.
1) Start strict and predictable
Begin every script with a safe baseline. This boosts reliability and gives AI/code-review tools fewer degrees of freedom to misinterpret your intent.
#!/usr/bin/env bash
# shellcheck shell=bash
# Strict mode: fail fast and avoid surprises
set -Eeuo pipefail
IFS=$'\n\t'
# Require Bash 4+ (associative arrays, better features)
if (( BASH_VERSINFO[0] < 4 )); then
printf 'Bash 4+ required (found %s)\n' "$BASH_VERSION" >&2
exit 2
fi
# Error context on failure
err_report() {
local exit_code=$?
printf 'ERROR: %s:%s: %s\n' \
"${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${BASH_COMMAND}" >&2
exit "$exit_code"
}
trap err_report ERR
# Allow cleanup on exit (even on error); define cleanup() later if needed
cleanup() { :; }
trap 'exit_code=$?; cleanup; exit "$exit_code"' EXIT
Notes:
set -Eeuo pipefailprevents most silent failures and propagates error traps through functions and pipelines.IFS=$'\n\t'eliminates surprising word-splitting on spaces.
2) Document for both humans and machines
Give your scripts a top comment block that’s machine-parseable and obvious to AIs and newcomers. Define a usage function and require all dependencies up-front.
: '
Name: backup.sh
Description: Safely archive a directory to a timestamped .tar.gz
Usage:
backup.sh -s <source_dir> -o <output_dir> [-v] [--json]
Requires:
- bash >= 4
- tar, gzip
Inputs:
- Environment: LOG_FORMAT=plain|json (default: plain)
Outputs:
- Writes an archive and logs to stdout/stderr
Exit Codes:
0: success
2: bad usage or unsupported environment
3: missing dependency or invalid path
4: runtime failure
Examples:
backup.sh -s /var/www -o /backups --json
'
VERSION="1.0.0"
usage() {
cat <<'EOF'
Usage:
backup.sh -s <source_dir> -o <output_dir> [-v] [--json]
Options:
-s Source directory
-o Output directory
-v Verbose
--json Emit JSON logs (requires jq)
-h Show help
EOF
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'Missing required command: %s\n' "$1" >&2
exit 3
}
}
parse_args() {
VERBOSE=0
LOG_FORMAT="${LOG_FORMAT:-plain}"
local short_opts="s:o:vh"
local long_opts="json"
# Transform long options
for arg in "$@"; do
case "$arg" in
--json) LOG_FORMAT=json ;;
esac
done
# Parse short options
while getopts "$short_opts" opt; do
case "$opt" in
s) SRC=$OPTARG ;;
o) OUT=$OPTARG ;;
v) VERBOSE=1 ;;
h) usage; exit 0 ;;
*) usage; exit 2 ;;
esac
done
: "${SRC:?Missing -s <source_dir>}" || { usage; exit 2; }
: "${OUT:?Missing -o <output_dir>}" || { usage; exit 2; }
if [[ $LOG_FORMAT == json ]]; then
require_cmd jq
fi
}
This structure:
Makes intent obvious and machine-readable.
Helps LLMs infer behavior and constraints from a predictable header.
Forces dependencies to be explicit, early.
3) Use safer Bash idioms
Prefer patterns that are robust and easy for tools to analyze.
# Logging with optional JSON
LOG_FORMAT="${LOG_FORMAT:-plain}"
log() {
local level=$1; shift
local msg=$*
if [[ $LOG_FORMAT == json ]]; then
printf '{"ts":"%s","level":"%s","msg":%s}\n' \
"$(date -Is)" "$level" "$(jq -Rs . <<<"$msg")"
else
printf '%s [%s] %s\n' "$(date -Is)" "$level" "$msg"
fi
}
info(){ log INFO "$*"; }
warn(){ log WARN "$*" >&2; }
error(){ log ERROR "$*" >&2; }
# Always quote, use arrays for safety
safe_copy() {
local -r src=$1 dst=$2
local -a tar_args=( -czf )
local outfile
outfile="$(printf '%s/%s.tar.gz' "$dst" "$(basename "$src")_$(date +%Y%m%d%H%M%S)")"
# Use -- to end options, avoid globbing surprises
tar -C "$src" -czf "$outfile" -- .
printf '%s\n' "$outfile"
}
# [[ ... ]] is safer than [ ... ] for many tests
if [[ -d "$SRC" && -w "$OUT" ]]; then
info "Starting backup"
else
error "Invalid paths. SRC=$SRC OUT=$OUT"
exit 3
fi
# Use mktemp for transient files
tmpdir="$(mktemp -d)"
cleanup() { rm -rf -- "$tmpdir"; }
Guidelines:
Prefer
printfoverecho(portable, predictable).Quote all variable expansions unless you explicitly want word splitting.
Use arrays to pass argument lists safely.
Always terminate option parsing with
--before file paths.Create temporary paths with
mktempand clean them up.
4) Enforce quality automatically: ShellCheck + shfmt
Linters catch bugs early; formatters eliminate style debates and help both CI and AI read your code.
Install ShellCheck and shfmt:
Debian/Ubuntu (apt)
sudo apt update sudo apt install -y shellcheck shfmtFedora/RHEL (dnf)
sudo dnf install -y ShellCheck shfmtopenSUSE (zypper)
sudo zypper install -y ShellCheck shfmt
Run them locally and in CI:
# Lint (explain external sources with -x if you source files)
shellcheck -x backup.sh
# Format in place: 2-space indents, simplify redirects, write changes
shfmt -i 2 -sr -w backup.sh
Tip: Fail your CI on any linter errors and ensure shfmt has already run (no diffs).
5) Test behavior and contracts with Bats (+ JSON logs you can parse)
Write black-box tests so humans and machines can trust refactors and AI suggestions.
Install Bats:
Debian/Ubuntu (apt)
sudo apt update sudo apt install -y batsFedora/RHEL (dnf)
sudo dnf install -y batsopenSUSE (zypper)
sudo zypper install -y bats
Optional: Install jq to validate JSON logs in tests.
Debian/Ubuntu (apt)
sudo apt update sudo apt install -y jqFedora/RHEL (dnf)
sudo dnf install -y jqopenSUSE (zypper)
sudo zypper install -y jq
A simple Bats test:
# test/backup.bats
#!/usr/bin/env bats
setup() {
TMPDIR="$(mktemp -d)"
mkdir -p "$TMPDIR/src" "$TMPDIR/out"
echo data > "$TMPDIR/src/file.txt"
}
teardown() {
rm -rf "$TMPDIR"
}
@test "prints usage with -h" {
run ./backup.sh -h
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
}
@test "creates an archive and logs in JSON" {
run env LOG_FORMAT=json ./backup.sh -s "$TMPDIR/src" -o "$TMPDIR/out"
[ "$status" -eq 0 ]
# At least one JSON log line
echo "$output" | head -n1 | jq -e . >/dev/null
# Archive exists
[ "$(ls "$TMPDIR/out"/*.tar.gz | wc -l)" -ge 1 ]
}
Bringing it together: a minimal, AI-friendly backup.sh
#!/usr/bin/env bash
# shellcheck shell=bash
: '
Name: backup.sh
Description: Safely archive a directory to a timestamped .tar.gz
Usage:
backup.sh -s <source_dir> -o <output_dir> [-v] [--json]
Requires: bash>=4, tar, gzip, (jq if --json or LOG_FORMAT=json)
Exit Codes: 0 ok, 2 usage, 3 missing dep/invalid path, 4 runtime
'
set -Eeuo pipefail
IFS=$'\n\t'
if (( BASH_VERSINFO[0] < 4 )); then printf 'Bash 4+ required\n' >&2; exit 2; fi
trap 'printf "ERROR %s:%s: %s\n" "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${BASH_COMMAND}" >&2; exit $?' ERR
cleanup(){ :; }
trap 'rc=$?; cleanup; exit $rc' EXIT
VERSION="1.0.0"
usage() {
cat <<'EOF'
Usage:
backup.sh -s <source_dir> -o <output_dir> [-v] [--json]
EOF
}
require_cmd(){ command -v "$1" >/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 3; }; }
LOG_FORMAT="${LOG_FORMAT:-plain}"
VERBOSE=0
SRC=""
OUT=""
parse_args() {
for arg in "$@"; do
case "$arg" in --json) LOG_FORMAT=json ;; esac
done
while getopts "s:o:vh" opt; do
case "$opt" in
s) SRC=$OPTARG ;;
o) OUT=$OPTARG ;;
v) VERBOSE=1 ;;
h) usage; exit 0 ;;
*) usage; exit 2 ;;
esac
done
: "${SRC:?Missing -s}"; : "${OUT:?Missing -o}"
require_cmd tar; require_cmd gzip
if [[ $LOG_FORMAT == json ]]; then require_cmd jq; fi
}
log() {
local level=$1; shift
local msg=$*
if [[ $LOG_FORMAT == json ]]; then
printf '{"ts":"%s","level":"%s","msg":%s}\n' \
"$(date -Is)" "$level" "$(jq -Rs . <<<"$msg")"
else
printf '%s [%s] %s\n' "$(date -Is)" "$level" "$msg"
fi
}
info(){ log INFO "$*"; }
warn(){ log WARN "$*" >&2; }
error(){ log ERROR "$*" >&2; }
safe_archive() {
local -r src=$1 dst=$2
[[ -d "$src" ]] || { error "Not a directory: $src"; exit 3; }
[[ -d "$dst" && -w "$dst" ]] || { error "Not writable: $dst"; exit 3; }
local outfile
outfile="$(printf '%s/%s_%s.tar.gz' "$dst" "$(basename "$src")" "$(date +%Y%m%d%H%M%S)")"
tar -C "$src" -czf "$outfile" -- . || { error "tar failed"; exit 4; }
printf '%s\n' "$outfile"
}
main() {
parse_args "$@"
[[ $VERBOSE -eq 1 ]] && info "Version $VERSION"
info "Starting backup: $SRC -> $OUT"
local archive
archive="$(safe_archive "$SRC" "$OUT")"
info "Backup complete: $archive"
}
main "$@"
Run quality tools:
shellcheck -x backup.sh
shfmt -i 2 -sr -w backup.sh
Conclusion and next steps
Bash becomes dramatically more reliable—and easier for AI to augment—when you:
Start strict
Document intent and dependencies
Use safe idioms
Lint/format in CI
Test behavior and log in a machine-friendly way
Call to action: 1) Copy the strict header, usage, and logging helpers into your scripts. 2) Install ShellCheck, shfmt, Bats (and jq if you want JSON logs), then wire them into your CI pipeline. 3) Migrate one critical script this week. Add tests. Measure the time you save on reviews and incidents.
Your future self—and your AI tools—will thank you.