đŸȘ Making Bash Modules Civilized: Installers, Uninstallers, and Tests That Don’t Panic

After publishing my Bash module, I made it easier to use by building a proper installer and adding an uninstall option. I also introduced automated tests with Bats to ensure everything works reliably. This post explores how the installer works and how the testing setup is built.

đŸȘ Making Bash Modules Civilized: Installers, Uninstallers, and Tests That Don’t Panic
Photo by Brett Jordan / Unsplash

If you’ve ever hacked together a Bash script, felt proud, shipped it to your .bashrc, and then immediately forgotten how it works — congratulations, you’re one of us. Bash scripting lives in that beautiful twilight zone between “I made fire!” and “why is my prompt yelling at me?”

In the previous post, we talked about publishing the hhgttg module on GitHub. But as soon as you share code publicly, something terrifying happens:
People may actually use it.

This post is about making sure they can — safely, predictably, and without summoning an eldritch creature from /usr/bin/env.

So today, we’ll talk about:

  1. Writing a real installer (that behaves like a polite guest)
  2. Adding an uninstall option (because not everyone likes guests)
  3. Building automated tests with Bats (so everything works today, tomorrow, and when future-you forgets how any of this works)

Buckle up and don’t panic — this is where your Bash module starts becoming a real piece of software.

1. The Problem With Bash Scripts Living Free-range

Before we added tests or installers, hhgttg had a few
 quirks:

  • You had to manually copy files
  • You had to manually edit .bashrc
  • You had to manually clean things up if something went wrong
  • You had to remember what you did three days later
  • (You didn’t.)

It worked, but installing it felt like performing an ancient ritual:

cp hhgttg.sh ~/.local/shell.d/hhgttg/
source ~/.bashrc
pray

That’s not user-friendly.
It’s not even developer-friendly.
It’s barely mammal-friendly.

So I decided the module deserved a proper, predictable lifecycle.

2. Enter the Installer: A Script That Knows What It’s Doing

The new installer script (install.sh) is deliberately simple, readable, and polite.
Its job is to:

  1. Create a directory for the module
  2. Install three files (bash-preexec.sh, hhgttg.sh, and hhgttg.config.sh)
  3. Add a properly marked block to ~/.bashrc
  4. Never duplicate lines
  5. Never touch anything system-wide
  6. Make uninstalling easy later

Let’s walk through how it works.

Step 1: A Safe Home for the Module

By default, the module installs itself into:

$HOME/.local/shell.d/hhgttg

This is the perfect place for user-level scripts — neat, isolated, and out of the way.
The installer makes sure it exists:

ensure_target_dir() {
  if [[ ! -d "$TARGET_DIR" ]]; then
    mkdir -p "$TARGET_DIR"
    echo "Created target directory: $TARGET_DIR"
  fi
}

Clean. Predictable. No surprises.

Step 2: Installing the Module Files

There are three essential files:

  • bash-preexec.sh — the engine that powers pre-exec hooks
  • hhgttg.sh — the main module
  • hhgttg.config.sh — user overrides

The installer looks for local copies first (useful during development), and falls back to downloading if needed:

install_module_files() {
  for file in "${MODULE_FILES[@]}"; do
    src_path="${SCRIPT_DIR}/${file}"
    dst_path="${TARGET_DIR}/${file}"

    if [[ -f "$src_path" ]]; then
      cp -f "$src_path" "$dst_path"
      chmod 0644 "$dst_path"
      echo "Installed $file → $dst_path"
    else
      # downloading bash-preexec.sh...
    fi
  done
}

The best part?

Idempotency

Run the installer 100 times — nothing breaks, nothing duplicates, nothing catches fire.

Step 3: Adding to .bashrc Without Duplicating

Here’s where things get serious.

Many install scripts just append blindly to .bashrc.
This is how you end up with:

source ~/module.sh
source ~/module.sh
source ~/module.sh
source ~/module.sh

and then you spend two hours debugging why your prompt blinks twice.

So the installer uses start/end markers, checks for duplicates, and inserts only once.

$BASHRC_START_MARK
...
$BASHRC_END_MARK

The block looks like this:

if [[ -n "$PS1" ]]; then
  export PATH="$PATH:${TARGET_DIR}"
  [[ -f "${TARGET_DIR}/hhgttg.sh" ]] && source "${TARGET_DIR}/hhgttg.sh"
fi

Interactive shells only.
Simple. Clean. Predictable.

3. The Uninstaller: Because Sometimes You Just Want It Gone

An installer without an uninstaller is like a guest who shows up, rearranges your fridge, and refuses to leave.

So I built a proper removal routine.

The uninstall process does three things:

Step 1: Remove the module files

for file in "${MODULE_FILES[@]}"; do
  rm -f "$TARGET_DIR/$file"
done

Missing files?
No problem.
It prints a message and moves on politely.

Step 2: Remove the .bashrc block

This is where things get clever.

Using a sed range delete, the script removes the block including the markers:

sed -i "/$BASHRC_START_MARK/,/$BASHRC_END_MARK/d" "$bashrc"

It even supports both GNU and BSD/macOS variants of sed.
Yes, really.

Step 3: Remove the directory (if empty)

if [[ -d "$TARGET_DIR" && -z "$(ls -A "$TARGET_DIR")" ]]; then
  rmdir "$TARGET_DIR"
fi

No files left behind.
No digital dust bunnies.

4. Adding Automated Tests With Bats

Now that the module installs and removes cleanly, it’s time to ensure it stays that way.

Enter Bats, the Bash Automated Testing System — a tiny testing framework that makes writing tests actually pleasant.

A typical test looks like this:

@test "installer creates target directory" {
  run env HOME="$TEST_HOME" ./install.sh
  [ -d "$TEST_HOME/.local/shell.d/hhgttg" ]
}

You read that right:

  • Override $HOME → no real user files touched
  • Run the installer
  • Check the filesystem
  • No side effects
  • No drama

The tests now verify:

✔ Installer creates dirs
✔ Installer copies or downloads files
✔ .bashrc gets modified exactly once
✔ Running installer twice does not duplicate lines
✔ Running uninstall removes files
✔ Running uninstall removes .bashrc block
✔ Running uninstall twice is safe

Which means your module now behaves like real software — not a mysterious blob of Bash that randomly edits text files.

5. Why This Matters (Especially for Bash)

Bash scripts age like milk.
A year from now:

  • you forget how they work
  • small changes break them
  • your bashrc becomes a haunted forest
  • debugging becomes archaeology

By adding:

  • an installer
  • an uninstaller
  • automated tests
  • predictable structure


we transform a little fun module into something discoverable, maintainable, and shareable.

Future-you will thank present-you.
(Probably. Maybe. As long as you left comments.)

Final Thoughts (and No Towels Required)

The hhgttg Bash module started as a small personal experiment.
Now it’s becoming a polished tool with:

  • clean installation
  • clean uninstallation
  • reproducible behavior
  • automated tests
  • and a roadmap for future improvements

Next up in the series, we’ll dive deeper into continuous integration and GitHub Actions, because if you thought installing your module was fun, wait until robots do it for you.

Until then:

Don’t panic, keep coding, and may your prompts always be snappy.

Read next