- Posted on
- • Artificial Intelligence
Artificial Intelligence Techniques for Refactoring Old Bash Scripts
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Techniques for Refactoring Old Bash Scripts
If your team has a 900‑line Bash script that everyone fears touching, you’re not alone. Legacy shell code often grows organically: inconsistent style, hidden side effects, brittle error handling, and zero tests. The result is risk. The good news: modern AI assistants, combined with battle-tested CLI tooling, can help you safely refactor, harden, and document old Bash—without rewriting everything from scratch.
This article shows you how to pair AI with standard Linux tools to:
Map and understand large shell codebases quickly
Propose safe, reviewable refactors
Auto-generate initial test scaffolding
Enforce portability, safety, and style in CI
You’ll get concrete commands, examples, and a practical workflow you can try today.
Why this matters
Shell scripts run critical glue in build, deploy, and operations paths. Silent failures or subtle quoting bugs are expensive.
Bash’s footguns (word splitting, globbing, subshell surprises) make “just one small change” risky.
Refactors stall when the original author is gone and there are no tests.
AI excels at reading messy code, spotting patterns, summarizing intent, proposing diffs, and drafting tests—if you combine it with static analysis and a tight feedback loop. You stay in control; AI does the heavy lifting.
Prerequisites: set up your refactoring toolbox
Install these core tools. Commands are provided for apt, dnf, and zypper.
Shell analysis, formatting, search:
# apt (Debian/Ubuntu)
sudo apt update
sudo apt install -y shellcheck shfmt ripgrep jq git
# dnf (Fedora/RHEL/CentOS Stream)
sudo dnf install -y ShellCheck shfmt ripgrep jq git
# zypper (openSUSE/SLE)
sudo zypper refresh
sudo zypper install -y ShellCheck shfmt ripgrep jq git
Bash testing with Bats:
# apt
sudo apt install -y bats
# dnf
sudo dnf install -y bats
# zypper
sudo zypper install -y bats
Container runtime (for running a local LLM server via a container):
# apt
sudo apt install -y podman
# dnf
sudo dnf install -y podman
# zypper
sudo zypper install -y podman
Python CLI packages via pipx (for pre-commit and optional extras):
# apt
sudo apt install -y pipx
pipx ensurepath
# dnf
sudo dnf install -y pipx
pipx ensurepath
# zypper
sudo zypper install -y pipx
pipx ensurepath
Pre-commit hooks:
pipx install pre-commit
Optional: Semgrep for additional policy/security checks (installed via pipx):
pipx install semgrep
Run a local LLM with Ollama (containerized). This keeps your code local while you iterate:
# Start Ollama in a rootless Podman container
podman run -d --name ollama -p 11434:11434 -v ollama:/root/.ollama docker.io/ollama/ollama:latest
# Pull a model (examples: llama3 or codellama)
podman exec -it ollama ollama pull llama3
Quick test:
curl -s http://localhost:11434/api/generate \
-d '{"model":"llama3","prompt":"Say hello to Bash refactoring","stream":false}' | jq -r .response
Technique 1: Use AI to inventory and summarize your scripts
Before refactoring, get the lay of the land.
List candidate scripts:
rg -n --hidden --no-ignore-vcs -e '^#!.*/\(ba\)\?sh' -g '!**/.git/**' -g '*.sh' > scripts.txt
For each script, create a compact summary with a local LLM. Example prompt via Ollama’s HTTP API:
script=./legacy/backup.sh
prompt=$(printf "%s\n\nCODE START\n%s\nCODE END\n\nSummarize purpose, inputs/outputs, side-effects, risks, and dependencies in bullet points. Be concise; no code." \
"You are a senior SRE helping document a legacy Bash script." "$(sed -n '1,400p' "$script")")
curl -s http://localhost:11434/api/generate \
-d "$(jq -n --arg p "$prompt" '{"model":"llama3","prompt":$p,"stream":false}')" \
| jq -r .response > "$script.summary.md"
What you get:
A quick inventory of purpose, flags, environment variables, files touched, and external commands (tar, rsync, aws, etc.)
Pointers to risky areas: unquoted expansions,
for f in $(ls ...),eval, unguardedrm -rf, and set -e pitfalls
Use these summaries to prioritize refactors and design tests.
Technique 2: AI‑guided, reviewable refactors (with ShellCheck and shfmt as guardrails)
Baseline checks:
shellcheck legacy/backup.sh
shfmt -d legacy/backup.sh # -d = diff; use -w to write changes
Ask the LLM for a minimal, reviewable patch. Keep prompts specific and demand diffs.
Example: fix unsafe iteration and quoting.
Prompt:
Provide a unified diff (only) that:
- Replaces 'for f in $(ls "$dir")' with a glob-based loop that handles spaces and empty dirs
- Adds safe defaults: set -euo pipefail; IFS=$'\n\t'
- Quotes variables correctly
- Keeps behavior identical for valid inputs
File: legacy/summarize.sh
--- a/legacy/summarize.sh
+++ b/legacy/summarize.sh
Possible AI output (you review it):
--- a/legacy/summarize.sh
+++ b/legacy/summarize.sh
@@ -1,4 +1,8 @@
-#!/usr/bin/env bash
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
dir="$1"
-for f in $(ls "$dir"); do
- if [ -f $f ]; then echo $f | cut -d'.' -f1; fi
-done
+shopt -s nullglob
+for f in "$dir"/*; do
+ if [[ -f "$f" ]]; then
+ printf '%s\n' "${f##*/}" | cut -d'.' -f1
+ fi
+done
Apply and re-check:
git apply --3way /path/to/patch.diff
shfmt -w legacy/summarize.sh
shellcheck legacy/summarize.sh
Prefer small, mechanical diffs. Let the model explain “why,” but only commit changes that pass ShellCheck and your tests.
Portability note: if you need POSIX sh, request it explicitly in the prompt and avoid Bashisms:
# POSIX-safe loop (no bash-specific [[ ]] or shopt):
for f in "$dir"/*; do
[ -f "$f" ] || continue
base=${f##*/}
printf '%s\n' "${base%%.*}"
done
Technique 3: Auto-generate baseline tests with Bats, then tighten by hand
Even a few tests change the game. Use AI to draft Bats tests from --help output and the script summary, then you refine.
Create tests directory and a smoke test:
mkdir -p test
cat > test/backup_smoke.bats <<'EOF'
#!/usr/bin/env bats
setup() {
TMPDIR="$(mktemp -d)"
}
teardown() {
rm -rf "$TMPDIR"
}
@test "backup.sh shows usage" {
run ./legacy/backup.sh -h
[ "$status" -eq 0 ]
[[ "$output" =~ Usage|--help ]]
}
EOF
Run:
bats test
Ask the LLM to propose additional tests (idempotency, dry-run behavior, failure on missing input, safe handling of spaces/newlines). Feed it your --help text and one or two example runs, then paste its suggested Bats cases into test/*.bats. Tighten assertions yourself.
Technique 4: Hardening and portability sweeps (AI + linters + hooks)
- Add safe defaults early in each script.
set -euo pipefail
IFS=$'\n\t'
Replace dangerous patterns:
for f in $(ls ...)→ glob loops- unquoted
$var→"$var" eval "$user_input"→ avoid or strictly sanitize- implicit cd →
pushd/popdor subshell(cd ...)
Use ShellCheck codes to guide precise fixes:
shellcheck -S style -o all legacy/*.sh
- Enforce style and checks pre-commit:
cat > .pre-commit-config.yaml <<'EOF'
repos:
- repo: https://github.com/mvdan/sh
rev: v3.7.0
hooks:
- id: shfmt
args: [ -i, "2", -ci, -bn ]
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
EOF
pre-commit install
pre-commit run -a
- Optional: Semgrep shell rules for policy checks:
semgrep --config=p/ci --config=p/security-audit --include '*.sh'
Ask the LLM for a “portability pass”:
Review the following script for POSIX sh portability. Replace Bashisms.
Explain each change briefly, then output a unified diff only.
CODE:
<your script here>
Technique 5: Map external dependencies and replace fragile calls
Have the LLM list external commands, environment variables, and files used. Then refactor high-risk surfaces.
Examples:
Replace
grep | awk | cutpipelines with single, robust invocations or built-ins.Use
command -v tool >/dev/null || { echo "tool required" >&2; exit 1; }Handle locales explicitly for parsing:
LC_ALL=C sort
LC_ALL=C grep -E ...
Prompt idea:
List all external commands the script depends on and propose safer/portable alternatives.
Then provide a minimal diff that:
- checks for command availability,
- sets LC_ALL=C for parsing,
- reduces unnecessary subshells.
Output only the unified diff.
A tiny real‑world example
Before:
#!/usr/bin/env bash
dir=$1
for f in $(ls $dir); do
if [ -f $f ]; then echo $f | cut -d'.' -f1; fi
done
After (portable, safe):
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
dir=${1:?Usage: summarize.sh DIR}
for f in "$dir"/*; do
[ -f "$f" ] || continue
base=${f##*/}
printf '%s\n' "${base%%.*}"
done
Tests:
@test "handles files with spaces" {
mkdir -p "$TMPDIR/dir"
touch "$TMPDIR/dir/file one.txt"
run ./summarize.sh "$TMPDIR/dir"
[ "$status" -eq 0 ]
[ "$output" = "file one" ]
}
Putting it all together: a repeatable workflow
1) Inventory and summarize
Use ripgrep to find scripts
Generate summaries with a local LLM
2) Establish guardrails
Add shfmt, ShellCheck, and pre-commit
Create a Bats smoke test
3) Iterate safe refactors
Ask the LLM for small, focused diffs
Apply, run shfmt/ShellCheck/bats
Commit in small steps
4) Harden and port
Enforce
set -euo pipefail, quoting, dependency checksReduce subshells and fragile pipelines
5) Automate
pre-commit for contributors
CI to run bats + ShellCheck on every PR
Conclusion and next step (CTA)
Legacy Bash doesn’t have to be a liability. With a local LLM and the right CLI companions, you can understand, test, and refactor old scripts confidently—one small, verified patch at a time.
Your next step:
Install the toolchain above
Pick one script
Generate a summary, add a smoke test, and apply a single AI‑proposed diff that ShellCheck approves
Rinse and repeat. When you’re done, you’ll have safer scripts, real tests, and a workflow your team can trust. If you want a ready-made starter repo (pre-commit + bats + example prompts), say the word and I’ll share a template you can clone.