Posted on
Artificial Intelligence

Artificial Intelligence Bash Best Practices for Production

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence Bash Best Practices for Production

If your team’s first AI integration started as a quick curl to a model API in a Bash script, you’re not alone. In dev, it “just works.” In production, things change: rate limits bite, JSON breaks, retries explode traffic, secrets leak in logs, and a single stalled process blocks your batch overnight. This guide turns that duct tape into durable, production-grade Bash for AI workflows.

What you’ll get:

  • Why Bash is still a great glue layer for AI in production

  • A hardened Bash skeleton you can reuse

  • Safe API calls with retries, timeouts, and JSON validation

  • Concurrency, rate limiting, and result caching

  • Linting and formatting for long-term maintainability

  • Installation commands for apt, dnf, and zypper


Why Bash for AI in production?

  • It’s everywhere. Bash + coreutils are practically guaranteed on Linux hosts, images, and CI runners.

  • It’s great glue. Orchestrate APIs, data files, cron jobs, and containers with minimal overhead.

  • It’s auditable. Plain-text scripts make ops review, change control, and incident forensics simpler.

  • It’s fast to iterate. A disciplined approach prevents “quick wins” from becoming operational debt.

The catch: you must apply production discipline to scripting. The rest of this post shows how.


Prerequisites and installation

We’ll use these tools:

  • curl: HTTP client with timeouts and robust error handling

  • jq: reliable JSON parsing

  • GNU parallel: optional, but excellent for controlled concurrency

  • ShellCheck: static analysis for Bash

  • shfmt: consistent shell formatting

  • direnv: manage per-project env vars and secrets

Install them as follows.

Debian/Ubuntu (apt):

sudo apt update
sudo apt install -y bash curl jq parallel shellcheck shfmt direnv

Fedora/RHEL (dnf):

sudo dnf install -y bash curl jq parallel ShellCheck shfmt direnv

openSUSE (zypper):

sudo zypper refresh
sudo zypper install -y bash curl jq parallel ShellCheck shfmt direnv

Note: Package names are case-sensitive on some distros (e.g., ShellCheck). If any package isn’t found in your repo, check the distro’s documentation or use alternative installation methods.


1) Start from a production-grade Bash skeleton

Use strict mode, predictable IFS, traps for cleanup, structured logging, and a single-instance lock.

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

script_name="${0##*/}"
log_level="${LOG_LEVEL:-INFO}"

tmpdir="$(mktemp -d)"
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
trap 'echo "$(date -Is) [ERROR] $script_name: line $LINENO" >&2; exit 1' ERR

log() { printf '%s [%s] %s\n' "$(date -Is)" "$1" "${*:2}" >&2; }
log_info() { [[ "$log_level" =~ (INFO|DEBUG) ]] && log INFO "$@"; }
log_warn() { log WARN "$@"; }
log_err()   { log ERROR "$@"; }

# Single-instance lock (adjust path for your environment)
lock_file="${AI_LOCK_FILE:-/tmp/${script_name}.lock}"
exec 9>"$lock_file" || true
if ! flock -n 9; then
  log_warn "Another instance is running (lock: $lock_file)"
  exit 0
fi

Why it matters:

  • set -Eeuo pipefail catches silent failures

  • Trap guarantees tmp cleanup even on error

  • Locking prevents concurrent runs tripping rate limits or stomping the same files


2) Safe configuration and secrets

Load config via .env, keep secrets out of history, and avoid leaking tokens in logs.

# Example .env (DO NOT COMMIT; add to .gitignore)
# AI_API_KEY=sk-...
# AI_API_URL=https://api.openai.com/v1/chat/completions
# AI_MODEL=gpt-4o-mini
# AI_HTTP_TIMEOUT=30

load_env() {
  local env_file="${1:-.env}"
  [[ -f "$env_file" ]] || { log_warn "No $env_file found"; return 0; }
  chmod 600 "$env_file" || true
  set -a
  # shellcheck disable=SC1090
  . "$env_file"
  set +a
}

# Optional: use direnv to auto-load .env in this directory
# After installation, run:
#   echo "dotenv" > .envrc
#   direnv allow

Tips:

  • Use chmod 600 for .env and never echo tokens to stdout

  • Read secrets with read -rs if you must prompt interactively

  • Keep tokens out of process lists by using headers, not query params


3) Safe API calls: retries, timeouts, and JSON validation

Harden your HTTP calls and data handling. Do not parse JSON by hand—use jq.

retry() {
  local max="${1:-5}"; shift || true
  local base_delay="${1:-1}"; shift || true
  local attempt=1
  until "$@"; do
    local ec=$?
    if (( attempt >= max )); then return "$ec"; fi
    local sleep_s=$(( base_delay * 2 ** (attempt - 1) ))
    log_warn "Retry $attempt/$max in ${sleep_s}s (exit $ec)"
    sleep "$sleep_s"
    attempt=$(( attempt + 1 ))
  done
}

ai_chat() {
  local prompt="$1"
  local model="${2:-${AI_MODEL:-gpt-4o-mini}}"
  local out_file="${3:-/dev/stdout}"
  local url="${AI_API_URL:-https://api.openai.com/v1/chat/completions}"
  local timeout="${AI_HTTP_TIMEOUT:-30}"
  local resp="$tmpdir/resp.json"

  # Fail on non-2xx, set reasonable timeouts, and be quiet unless errors occur
  curl -sS --fail-with-body --connect-timeout 5 -m "$timeout" \
    -H "Authorization: Bearer ${AI_API_KEY:?Set AI_API_KEY}" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg m "$model" --arg p "$prompt" \
         '{model:$m, messages:[{role:"user", content:$p}], temperature:0}')" \
    "$url" -o "$resp"

  # Validate and extract text
  jq -e -r '.choices[0].message.content' "$resp" > "$out_file"
}

# Example usage with retries:
# retry 5 1 ai_chat "Summarize: $TEXT" "gpt-4o-mini" "out.txt"

Why it matters:

  • curl --fail-with-body + timeouts = predictable failures

  • retry with exponential backoff matches real-world transient errors

  • jq -e enforces JSON shape; broken payloads fail early, not mid-pipeline


4) Concurrency, rate limiting, and idempotent caching

Don’t DOS your provider. Enforce minimum spacing between calls and cache results to avoid rework.

Rate limiting helper (millisecond precision with GNU date):

with_rate_limit() {
  local key="${1:-default}"; shift
  local min_interval_ms="${1:-250}"; shift
  local state_dir="${AI_STATE_DIR:-/tmp/ai-rl}"
  mkdir -p "$state_dir"
  local state="$state_dir/$key.state"
  local lock="$state_dir/$key.lock"
  exec 200>>"$lock"
  flock 200

  local now_ms last_ms delta remain_ms
  now_ms="$(date +%s%3N)"
  if [[ -f "$state" ]]; then
    read -r last_ms < "$state" || last_ms=0
  else
    last_ms=0
  fi
  delta=$(( now_ms - last_ms ))
  if (( delta < min_interval_ms )); then
    remain_ms=$(( min_interval_ms - delta ))
    printf -v sleep_s "0.%03d" "$remain_ms"
    sleep "$sleep_s"
    now_ms="$(date +%s%3N)"
  fi
  printf '%s\n' "$now_ms" > "$state"

  flock -u 200
  "$@"
}

File-based cache keyed by input:

cache_get_or_run() {
  local cache_key="$1"; shift
  local out_file="$1"; shift
  local cache_dir="${AI_CACHE_DIR:-$HOME/.cache/ai}"
  mkdir -p "$cache_dir"
  local key_hash
  key_hash="$(printf '%s' "$cache_key" | sha256sum | awk '{print $1}')"
  local cached="$cache_dir/$key_hash.txt"

  if [[ -s "$cached" ]]; then
    cp -f "$cached" "$out_file"
    log_info "Cache hit: $key_hash"
    return 0
  fi

  "$@" > "$out_file"
  cp -f "$out_file" "$cached"
  log_info "Cache store: $key_hash"
}

Real-world batch example (two small scripts): 1) ai_base.sh — put the skeleton and helpers above into this file. 2) ai_batch.sh — process a file of tickets with concurrency, rate-limit, and cache.

ai_batch.sh:

#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

# shellcheck source=./ai_base.sh
. "./ai_base.sh"
load_env ".env"

mkdir -p out

process_line() {
  local line="$1"
  local prompt="Classify this ticket into one category: $line"
  local out="out/$(printf '%s' "$prompt" | sha256sum | cut -c1-16).txt"
  cache_get_or_run "$prompt" "$out" \
    with_rate_limit "provider_openai" 250 \
    ai_chat "$prompt" "${AI_MODEL:-gpt-4o-mini}" "$out"
}

export -f process_line cache_get_or_run with_rate_limit ai_chat log_info
export AI_API_KEY AI_API_URL AI_MODEL AI_HTTP_TIMEOUT AI_CACHE_DIR AI_STATE_DIR

# tickets.txt: one ticket per line
# Requires GNU parallel (see install commands above)
parallel --jobs 4 --linebuffer process_line :::: tickets.txt

Why it matters:

  • Concurrency (--jobs 4) gets throughput without overwhelming the API

  • Rate limiting enforces minimum inter-request spacing

  • Caching prevents rework on retries and makes jobs resumable


5) Keep it clean: linting and formatting

Static analysis and formatting reduce production surprises and review friction.

Run locally or in CI:

shellcheck ai_base.sh ai_batch.sh
shfmt -w -i 2 ai_base.sh ai_batch.sh

Add these to your CI pipeline to block regressions before they hit production.


Putting it all together

  • Use the hardened skeleton for every script

  • Load configuration from .env and protect secrets

  • Call AI APIs with curl + timeouts + retries; parse with jq

  • Control concurrency and rate limiting; cache idempotently

  • Lint and format in CI to keep your Bash maintainable

Call to action: 1) Copy the skeleton and helpers into ai_base.sh. 2) Install prerequisites with your package manager. 3) Create ai_batch.sh tailored to your workload and test against a small input. 4) Add ShellCheck/shfmt to CI and schedule the job (cron, systemd, or your orchestrator).

Production-grade AI pipelines don’t have to be complex—just disciplined. Start with these patterns, iterate safely, and ship with confidence.