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:
- Writing a real installer (that behaves like a polite guest)
- Adding an uninstall option (because not everyone likes guests)
- 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:
- Create a directory for the module
- Install three files (
bash-preexec.sh,hhgttg.sh, andhhgttg.config.sh) - Add a properly marked block to
~/.bashrc - Never duplicate lines
- Never touch anything system-wide
- 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 hookshhgttg.shâ the main modulehhgttg.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.