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 pipefail prevents 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 printf over echo (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 mktemp and 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 shfmt
    
  • Fedora/RHEL (dnf)

    sudo dnf install -y ShellCheck shfmt
    
  • openSUSE (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 bats
    
  • Fedora/RHEL (dnf)

    sudo dnf install -y bats
    
  • openSUSE (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 jq
    
  • Fedora/RHEL (dnf)

    sudo dnf install -y jq
    
  • openSUSE (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.