Posted on
Artificial Intelligence

Using Artificial Intelligence to Refactor Legacy Bash Code

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

Using Artificial Intelligence to Refactor Legacy Bash Code

Legacy Bash scripts often sit at the heart of critical automation—backups, deploys, nightly jobs—until a 2 a.m. outage reminds us how fragile they can be. Subtle quoting bugs, whitespace in filenames, race conditions with temp files: you’ve seen it. The good news is that AI can accelerate the modernization of these scripts—if you pair it with the right tooling and guardrails.

This article shows you a practical, test-first, AI-assisted workflow to safely refactor legacy Bash. You’ll get concrete steps, real examples, and install commands for the tools you need.

Why let AI help with shell scripts?

  • Legacy Bash is everywhere but under-tested. LLMs are surprisingly good at suggesting safer idioms (arrays, mapfile/readarray, find -print0, mktemp, trap) and explaining opaque code.

  • You keep control. With tests and linters, you can quickly verify AI suggestions without guessing.

  • Faster learning curve for teams. The AI can explain why something is dangerous (like for f in $(ls)), propose fixes, and you validate them via tooling.

The trick: AI is your pair-programmer, not the source of truth. You freeze behavior with tests, let AI propose changes, then enforce Bash best practices automatically.


Install the essentials

You’ll use these tools:

  • shellcheck: Static analyzer for shell scripts

  • shfmt: Formatter for shells

  • bats: Test framework for Bash

  • jq: Handy for JSON in pipelines

  • git: So you can commit, diff, and revert

If a package name isn’t available on your distro, see the fallback note below.

Apt (Debian/Ubuntu)

sudo apt update
sudo apt install -y shellcheck shfmt bats jq git python3-bashate || true

DNF (Fedora/RHEL/CentOS Stream)

sudo dnf install -y ShellCheck shfmt bats jq git python3-bashate || true

Zypper (openSUSE/SLE)

sudo zypper refresh
sudo zypper install -y ShellCheck shfmt bats jq git python3-bashate || true

Fallbacks if any package is missing:

  • bashate via pip:
python3 -m pip install --user bashate
  • shfmt via Go (if not in your repos):
go install mvdan.cc/sh/v3/cmd/shfmt@latest
# Ensure $GOPATH/bin (often ~/go/bin) is on your PATH

Optional: run a local LLM (no cloud) with Ollama:

curl -fsSL https://ollama.com/install.sh | sh
# Example: pull a code-capable model
ollama pull codellama:7b

The AI-assisted refactor workflow (5 steps)

1) Put guardrails in place

Before touching logic, add minimal safety and observability. At the very top of each script:

#!/usr/bin/env bash
set -Eeu -o pipefail
trap 's=$?; echo "Error on line ${BASH_SOURCE[0]}:${LINENO} (exit $s)" >&2' ERR

Notes:

  • set -euo pipefail is common; -E preserves ERR traps in functions/subshells.

  • Don’t rely solely on -e for correctness—still check status codes where critical.

  • Avoid global IFS changes. Set IFS locally for reads instead:

while IFS= read -r line; do
  ...
done < "$file"

2) Freeze current behavior with unit tests (bats)

Capture what the script does today—warts and all—so refactors don’t change behavior unintentionally.

Install bats (done above), then create test/my_script.bats:

#!/usr/bin/env bats

setup() {
  TMPDIR="$(mktemp -d)"
}

teardown() {
  rm -rf "$TMPDIR"
}

@test "backup script preserves filenames with spaces" {
  run bash ./backup.sh "$TMPDIR" "My File.txt"
  [ "$status" -eq 0 ]
  [ -f "$TMPDIR/My File.txt.bak" ]
}

@test "prints usage when args missing" {
  run bash ./backup.sh
  [ "$status" -ne 0 ]
  [[ "$output" =~ "Usage:" ]]
}

Run all tests:

bats -r test

Tip: write tests for edge cases—spaces/newlines in filenames, empty inputs, non-ASCII, errors in pipelines.

3) Measure and fix obvious issues with linters/formatters

Static analysis gets you easy wins and gives AI better input.

  • Analyze:
find . -type f -name "*.sh" -print0 | xargs -0 -I{} bash -lc 'echo "== {} =="; shellcheck -x "{}"'
  • Auto-format (non-destructive by default, run with -w to write):
shfmt -i 2 -ci -sr -d .
# write changes:
shfmt -i 2 -ci -sr -w .
  • Style/lint for Bash-only style issues:
bashate -i E006,E040 -e E002,E003 . || true

Rinse/repeat until the obvious red flags are gone, and commit once green:

git add -A && git commit -m "chore: baseline with shellcheck/shfmt + tests"

4) Use AI to propose refactors (then verify with tests)

Feed an LLM the script plus your ShellCheck findings and goals. Keep prompts tight and test-driven.

Prompt example:

You are refactoring a legacy Bash script to be safe and idempotent.

Constraints:

- Bash 4.2+ compatibility

- No process substitution on non-Bash shells

- Preserve behavior verified by tests (bats)

- Fix ShellCheck warnings, prefer arrays/mapfile, use find -print0 for filenames

- Quote expansions, avoid for-in-$(cmd), avoid UUOC

- Use mktemp + trap for temps

Here is the script:
<<<BEGIN_SCRIPT
...script content...
END_SCRIPT

Here are current ShellCheck warnings:
<<<BEGIN_SHELLCHECK
...shellcheck output...
END_SHELLCHECK

Propose a minimal diff that keeps the same behavior but fixes the issues. Explain any behavior changes.

Local example with Ollama:

ollama run codellama:7b
# paste the prompt above

Review diffs, run tests, and repeat until green:

git diff
bats -r test
shellcheck -x **/*.sh

Example: unsafe loops and quoting

Legacy:

for f in $(ls *.txt); do
  cat $f | while read line; do
    echo $line | cut -d: -f1
  done
done

Refactor:

shopt -s nullglob
for f in ./*.txt; do
  while IFS= read -r line; do
    printf '%s\n' "${line%%:*}"
  done < "$f"
done

Why this is safer:

  • No command substitution for filenames (handles spaces/newlines).

  • No UUOC (cat file | while read).

  • Quoted variable expansion; read -r avoids backslash interpretation.

  • ${line%%:*} avoids an external cut.

Example: robust file discovery

Legacy:

files=$(find . -name "*.log")
for f in $files; do
  rm $f
done

Refactor:

mapfile -d '' -t files < <(find . -type f -name '*.log' -print0)
for f in "${files[@]}"; do
  rm -- "$f"
done
  • Uses NUL separation to safely handle all filenames.

  • Arrays prevent word-splitting issues.

  • -- guards against filenames beginning with a dash.

Example: safe temp files and cleanup

Legacy:

TMP=/tmp/tmpfile
echo data > $TMP
# ...
rm $TMP

Refactor:

tmpfile="$(mktemp)"
cleanup() { rm -f -- "$tmpfile"; }
trap cleanup EXIT
printf '%s\n' "data" > "$tmpfile"

5) Automate checks in your repo

Add a simple script you can run locally and in CI:

#!/usr/bin/env bash
set -Eeuo pipefail

echo "== Formatting =="
shfmt -i 2 -ci -sr -d . || { echo "Run: shfmt -i 2 -ci -sr -w ."; exit 1; }

echo "== Linting =="
shellcheck -x $(git ls-files '*.sh')

echo "== Tests =="
bats -r test

Wire it into your CI system so every AI-assisted change passes format + lint + tests.


Real-world mini-case: modernizing a backup script

Symptoms:

  • Broke on filenames with spaces

  • Smashed permissions

  • Left temp files behind on failure

Steps taken: 1) Added set -Eeu -o pipefail and a cleanup trap with mktemp. 2) Wrote bats tests for spaces, newlines, and permission preservation. 3) Ran shellcheck and fixed quoting and UUOC. 4) Prompted an LLM to replace for f in $(ls) with globbing + nullglob, moved to cp --preserve=mode,timestamps, added -- for safety, and switched to mapfile for find output. 5) All tests passed; measurable improvements in reliability and readability.


Tips to get the most from AI on Bash

  • Always give the model context: your tests, shellcheck output, target Bash version, and constraints.

  • Ask for small, reviewable diffs; prefer “minimal behavior change.”

  • Reject refactors that introduce non-portable features if you don’t control the environment.

  • Never paste secrets/tokens; redact or use a local model.

  • Let tests decide. If behavior must change, add/adjust tests first, then refactor.


Conclusion and next steps

AI won’t magically fix shell scripts, but with tests, linters, and a clear workflow, it becomes a powerful accelerant. Start with one high-impact script:

  • Install the tools (shellcheck, shfmt, bats, jq) with your package manager.

  • Add guardrails and write 2–5 bats tests for critical behavior.

  • Use an LLM to propose minimal refactors.

  • Verify with tests and static analysis, then iterate.

Your next step: 1) Pick a legacy script that scares you most. 2) Baseline it with tests and shellcheck. 3) Try one focused AI-assisted refactor cycle today.

If you want a companion checklist or example repo, reply and I’ll share a starter template you can clone.