Error-First Pattern: Writing Self-Documenting Bash
There’s a pattern in shell scripts that makes code harder to read than it needs to be. It looks like this:
if [[ -f "$config_file" ]]; then
if [[ -r "$config_file" ]]; then
source "$config_file"
if [[ -n "$DATABASE_URL" ]]; then
connect_to_database
if [[ $? -eq 0 ]]; then
run_migrations
# ... and so on
else
echo "Connection failed" >&2
exit 1
fi
else
echo "DATABASE_URL not set" >&2
exit 1
fi
else
echo "Config not readable" >&2
exit 1
fi
else
echo "Config not found" >&2
exit 1
fi
You’ve seen this. You’ve probably written this. I haven’t. Sorry, not sorry.
Have You Got the SKILLs? Teaching AI to Write Better Bash
— “Elmo’s got the moves” playing in my head since I typed this article’s title —
I wrote a book on Bash scripting that I’m about to publish. It teaches you how to write Bash, and more importantly, how to write it right.
But cool kids don’t write code anymore, do they?

Maybe the time will never come again when we’ll need to read and understand code ourselves—AI agents will have us covered. Maybe it’s fine. Maybe you don’t need to know the basics.
On Bash Testing: Design, Not Just Safety
Testing as a topic is a minefield already when it comes to full-fledged programming languages, let’s not even talk about it for shell scripting.
Or shall we?
You see, whether you write software to test other software, or you just fire up a script from the command line and keep running it over and over again until you nail it, that’s still testing, right?
Have you ever been woken up at night because a script failed six months after you wrote it? Or one year later? Or five years later? — For the love of my life, that thing was supposed to be temporary! There’s nothing more permanent than a temporary solution, they say.
Designing Modular Bash: Functions, Namespaces, and Library Patterns
You’ve written Bash scripts before. Maybe you’ve automated a deployment, wrangled some log files, or stitched together a few commands to save time. But as your scripts grow, something changes. What started as a clean 20-line helper becomes a 200-line sprawl. Functions call functions that modify global variables. You copy-paste code between scripts because extracting shared logic feels harder than it should be. The script works, but it’s fragile, and you’re the only person who can maintain it.