- 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.