Bash Romance

Fri, May 22, 2020. Tags: Tech.

It’s not a particularly new or exciting take to say that we’ve outgrown Bash. The Unix shell environment was novel and powerful in its day, but its age is really starting to show.

The shell is meant to be three things: an interactive interface with the computer, a scripting language to automate interactions with the computer, and a portable lowest-common-denominator scripting language across different machines.

Linux is extremely dominant in the server space, and Linux and MacOS are both popular desktop operating systems for developers. Since Bash is installed by default on all these platforms, it’s certainly fulfilling its duty as a portable scripting language.

As an interactive interface, Bash certainly has some sore spots that other shells such as Fish and Zsh have smoothed over. Quality-of-life features like multi-line editing are conspicuously absent, and uncomfortable design choices like splitting unquoted variables can lead to a lot of cognitive load.

But the real problem is how poorly Bash performs as a scripting language. Bash has only one primitive type: everything is a string.It has arrays and key/value tables, but they’re cumbersome, and aren’t first-class citizens. Beyond those, Bash doesn’t really have any data structures.

The closest it has to error handling is set -e, which will crash the script immediately if a command returns a non-zero value. If you forget to invoke it, or if a command fails but returns 0 anyway (like whereis), your script will trundle on as usual and quite likely botch whatever it was supposed to do in a very irritating (and dangerous) way. Not to mention smaller oddities, like the strange inconsistency between if/fi, case/esac, and for/done.

Sometimes it seems like there’s a whole secret library of best practices which are mandatory for writing shell scripts. Every individual shell command has its own unique idiosyncracies, and each one of those delivers only a single stream of plain text to stdout. That gets parsed using text-transforming tools like sed and tr, which often aren’t smart enough to know the difference between a delimiting space and a space inside a field. All of these tools have different conventions and different flags, and some of them have subtly different behaviour on other platforms! Looking at you, MacOS, BSD. And when you string together dozens of these commands in sequence, you often wind up with a very delicate and dangerous script. Shell is fragile, hacky, and hard to write safely.

So why do I love it so much?

There’s something very appealing about how a shell is both an interactive interface and a scripting language. Part of it is that it blurs the line between the two domains. Your interactive commands can contain as much logic as you want. And if you find a series of commands useful, add a shebang and just like that, it’s a script. If you need to debug your script, well, you’re already in a REPL environment. And you can open up a REPL from the middle of your script just by inserting the command bash.

There’s also a wonderful conciseness to shell commands. It comes from necessity — an interactive shell has to have short commands without too much punctuation. In Python, changing directories is done with os.chdir("/home/user/Documents"). In shell, the command is cd ~/Documents, and you only need to type cd ~/Doc<Tab> before the full thing is tab-completed for you. In Python, recursively removing empty directories from a folder requires this snippet of code:

1
2
3
4
5
6
7
import os

for (parentDir, childDirs, _) in os.walk(basePath):
    for childDirName in childDirs:
        childDirPath = os.path.join(parentDir, childDirName)
        if not os.listdir(childDirPath):
            os.rmdir(childDirPath)

In Bash, you can do that with find "$basePath" -type d -empty -delete.

At its best, working with Bash can be great. The Unix tools feel powerful, and can do the things you need them to do with minimal code glue. Sometimes plain text pipes really are the tool for the job. There’s merit to Unix philosophy. And often you’ll write your script much more quickly and concisely than if you’d tried to work it all out in something like Python.

But it doesn’t happen like that very often, especially as the scripts become more than a couple lines long.

Nonetheless, I suspect there’s a part of me that will always love shell scripting. When I first installed Linux on my laptop and started fooling around on the command line, I found it amazing. There’s so much power available there that you can’t tap into with GUI applications. You can bulk-rename files just by writing a for loop. It feels like a revelation.

There’s something very handy about that portable/interactive/scriptable niche that shell languages fill. Ideally, we could replace Bash with a more robust alternative, so that there’s no incentive to turn to less convenient languages like Python when it comes to scripting. Sadly, it doesn’t seem like any of the alternatives that have sprung up over the years can really solve our problem.

Zsh is what I currently use. It’s Bash with the rough edges filed off: similar enough to be mostly compatible, and similar enough to keep most of Bash’s issues. Fish takes some risks and fixes some of those problems, but isn’t used very widely. Elvish particularly impressed me — it even has real exception handling! — but I’ve barely heard it discussed at all.

Typically, by the point when people start discussing the idea of installing an alternate shell on all their servers in order to have more robust scripting capabilities, they’ve already made the responsible decision and written whatever shell script they intended to write using Python instead. And that’s the right move. But it does have the effect of perpetually preserving Bash’s position as the default shell, and I think that’s a real shame. We could do so much better.

I’ve spent a while trying to understand the role shell scripting should have in the software field. I’m fond of it, and I want to keep using it despite my better instincts. But so long as “shell scripting” means “Bash” in practice, the best role for it seems to be the smallest role possible. And I think that’s really unfortunate, because there’s a lot of merit to the idea of shell scripting, if you can look past all the awful baggage that Bash drags along for the ride.

Sadly, the particular scripting niche that Bash fills isn’t one that needs to be filled. Using Bash interactively but scripting in better programming languages is perfectly viable. So there’s no pressure to replace Bash with something better, and we all suffer a little for that. Shell scripts are relegated to bundling together simple filesystem operations, and anything with more than a tiny bit of logic gets written in a safer, less interactive language.

Just because a tool has a unique feature that you really like, or just because you’re particularly attached to it, doesn’t make it the right tool for the job. It doesn’t even make it a good tool. Bash is trash, even though I like it, and I wouldn’t trust it with anything too complicated.

That being said, I still believe in the idea of shell scripting, and I hope we’ll see a new shell that can compete with real scripting languages some day.