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, unguarded rm -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/popd or 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 | cut pipelines 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 checks

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