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.