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 pipefail and IFS=$'\n\t'.

  • Use trap to 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 getopts for 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., ShellCheck on Fedora/openSUSE).

  • If bats is outdated in your repo, consider bats-core from 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.