- Posted on
- • Artificial Intelligence
Artificial Intelligence Bash Refactoring Guide
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Bash Refactoring Guide
Your tiny Bash script that started as a quick fix is now mission critical. It’s growing, it’s fragile, and everyone is afraid to touch it. Good news: with a small toolchain and a careful “AI-in-the-loop” process, you can refactor Bash safely, improve reliability, and speed up maintenance.
This guide shows you how to combine static analysis, formatting, tests, and an AI-assisted review loop to modernize Bash scripts with confidence.
Why this matters
Bash isn’t going away. It’s everywhere: provisioning, CI pipelines, containers, glue code.
“Works on my box” isn’t good enough. Scripts need guardrails, consistency, and tests.
AI is great at surfacing refactoring ideas and generating diffs, but it must be paired with automated checks to stay safe.
A small investment in tooling pays off with fewer production issues and faster iteration.
Prerequisites: install the toolchain
We’ll use:
shellcheck: static analysis
shfmt: auto-formatter
bats: test framework for Bash
jq: JSON parsing
curl: HTTP client
git: version control (and to apply patches from AI)
Install them with your distro’s package manager.
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y git curl jq shellcheck shfmt bats
Fedora/RHEL/CentOS (dnf):
sudo dnf install -y git curl jq shellcheck shfmt bats
openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y git curl jq shellcheck shfmt bats
Notes:
If any package isn’t found, try searching (e.g.,
dnf search shfmt,zypper search shellcheck) or consult your distro’s docs.As a last resort for shfmt, you can download a static binary:
sudo curl -sSL -o /usr/local/bin/shfmt \
https://github.com/mvdan/sh/releases/latest/download/shfmt_linux_amd64
sudo chmod +x /usr/local/bin/shfmt
1) Baseline and harden your script
Before any refactor, add guardrails and get the script into version control.
Add a proper shebang and strict mode.
Set a safe IFS to avoid word-splitting surprises.
Fail fast and propagate errors.
Before:
#!/bin/bash
OUT=$(grep foo $1 | awk '{print $2}')
echo $OUT
After (safer):
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
main() {
local input_file=${1:-}
if [[ -z "${input_file}" ]]; then
echo "Usage: $0 <file>" >&2
exit 2
fi
local out
out=$(grep -F "foo" -- "${input_file}" | awk '{print $2}' || true)
printf '%s\n' "${out:-}"
}
main "$@"
Initialize a repo and commit the baseline:
git init
git add .
git commit -m "baseline: add strict mode and main()"
2) Static analysis and auto-formatting
Let tools catch issues humans miss.
Run shellcheck:
shellcheck your_script.sh
Fix findings—quoting, globs, unbound variables, arrays, and subshells are common pain points.
Format with shfmt (idempotent):
shfmt -w -i 2 -ci -sr your_script.sh
-w: write in place-i 2: 2-space indent-ci: indent switch cases-sr: simplify[[ ]], etc.
Pro tip: add a lint script:
#!/usr/bin/env bash
set -Eeuo pipefail
shfmt -d -i 2 -ci -sr .
shellcheck -S style -e SC1090,SC1091 $(git ls-files '*.sh' '*.bash')
3) Add tests with bats
Tests ensure refactors don’t break behavior.
Example test/example.bats:
#!/usr/bin/env bats
setup() {
TEST_DIR="$(mktemp -d)"
printf 'foo 123\nbar 456\n' > "${TEST_DIR}/data.txt"
}
teardown() {
rm -rf "${TEST_DIR}"
}
@test "prints second field of lines containing foo" {
run ./your_script.sh "${TEST_DIR}/data.txt"
[ "$status" -eq 0 ]
[ "$output" = "123" ]
}
@test "errors when no file is provided" {
run ./your_script.sh
[ "$status" -eq 2 ]
}
Run tests:
bats -r test
Lock in behavior now; future refactors (AI or manual) must pass these tests.
4) Put AI in the loop — safely
Use an LLM as a pair programmer to propose diffs. Always gate changes with shellcheck, shfmt, and bats.
Example script ai-refactor.sh that asks an OpenAI-compatible endpoint for a unified diff against your current file, then applies it if checks pass:
#!/usr/bin/env bash
set -Eeuo pipefail
: "${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://api.openai.com/v1}"
OPENAI_MODEL="${OPENAI_MODEL:-gpt-4o-mini}" # Any OpenAI-compatible model
FILE="${1:-}"
if [[ -z "${FILE}" || ! -f "${FILE}" ]]; then
echo "Usage: $0 path/to/script.sh" >&2
exit 2
fi
prompt=$(
cat <<'EOF'
You are a senior Bash engineer. Refactor the provided Bash file for safety, portability, and clarity.
- Keep the same behavior.
- Use set -Eeuo pipefail, safe IFS, and functions.
- Prefer POSIX where feasible, but it's ok to target bash.
- Return ONLY a unified diff (patch) with correct file paths. Do not wrap in code fences.
EOF
)
content=$(printf '---\n%s\n---\n%s\n' "${prompt}" "$(cat "${FILE}")")
resp=$(curl -sS -X POST "${OPENAI_BASE_URL}/chat/completions" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"model": "${OPENAI_MODEL}",
"temperature": 0.2,
"messages": [
{"role": "system", "content": "You write precise, minimal patches."},
{"role": "user", "content": ${content@Q}}
]
}
JSON
)
diff_text=$(printf '%s' "${resp}" | jq -r '.choices[0].message.content')
if [[ -z "${diff_text}" || "${diff_text}" == "null" ]]; then
echo "No diff received" >&2
exit 1
fi
echo "Proposed diff:"
echo "--------------------------------"
printf '%s\n' "${diff_text}"
echo "--------------------------------"
# Dry-run apply
if ! printf '%s\n' "${diff_text}" | git apply --check -; then
echo "Patch does not apply cleanly." >&2
exit 1
fi
# Apply, format, lint, test
printf '%s\n' "${diff_text}" | git apply -
shfmt -w -i 2 -ci -sr "${FILE}"
shellcheck "${FILE}"
bats -r test
echo "AI refactor applied and validated."
How to use:
Set credentials and, if needed, a base URL for an OpenAI-compatible server.
Run
./ai-refactor.sh your_script.sh.Review the diff, rerun tests, and commit if good.
Why this works:
AI proposes the change.
Tools and tests verify it.
You remain the editor-in-chief.
5) Automate gates with Make and CI
A small Makefile standardizes local checks:
SHELL := /usr/bin/env bash
.PHONY: fmt lint test check
fmt:
shfmt -w -i 2 -ci -sr .
lint:
shellcheck -S style $(git ls-files '*.sh' '*.bash')
test:
bats -r test
check: fmt lint test
GitHub Actions CI to enforce the same in PRs (.github/workflows/ci.yml):
name: ci
on: [push, pull_request]
jobs:
bash-ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt update && sudo apt install -y shellcheck shfmt bats
- run: shfmt -d -i 2 -ci -sr .
- run: shellcheck -S style $(git ls-files '*.sh' '*.bash')
- run: bats -r test
Now every change—human or AI—must pass formatting, linting, and tests.
Real-world example: reducing subshell costs
Before:
count=$(ls -1 "$DIR" | wc -l)
echo "files: $count"
After:
shopt -s nullglob
files=("$DIR"/*)
printf 'files: %d\n' "${#files[@]}"
Avoids a subshell pipeline
Works safely with spaces and globbing
Faster on large directories
Conclusion and next steps
Refactoring Bash safely is about combining guardrails (strict mode), consistency (shfmt), clarity (shellcheck), confidence (bats), and leverage (AI-generated diffs). You stay in control; your tools catch regressions.
Your next step: 1) Install the toolchain using apt, dnf, or zypper. 2) Harden one script with strict mode and commit it. 3) Add a bats test or two. 4) Run shellcheck and shfmt. 5) Try a single AI-assisted refactor behind your new safety net.
Have a gnarly script you want to tame? Start with the worst one—and let the tools carry the weight.