Hell: Shell scripting Haskell dialect

Hell is a shell scripting language that is a tiny dialect of Haskell that I wrote for my own shell scripting purposes. As of February, I’m using Hell to generate this blog, instead of Hakyll.1

My 2024 New Year’s Resolution is to write more shell scripts in the name of automation. I’ve always avoided this because of the downsides of bash. And other problems.

Bash, zsh, fish, etc. have problems: They’re incomprehensible gobbledegook. They use quotation (x=$(ls -1) ..) which makes it easy to make mistakes. They lean far too heavily on sub processes to do basic things. Therefore things like equality, arithmetic, ordering, etc. are completely unprincipled. Absolutely full of pitfalls.2

But, bash does have some upsides: It’s stable, it’s simple, and it works the same on every machine. You can write a bash script and keep it running for years while never having to change any code. The code you wrote last year will be the same next year.

So in the interest of defining a language that I would like to use, let’s discuss the anatomy of a shell scripting language: It should be very basic. It should run immediately (no visible compilation steps). It should have no module system. It should have no package system. It should have no abstraction capabilities (classes, data types, polymorphic functions, etc.). And it does not change in backwards-incompatible ways.3 Why no module or package system? They make it harder for a system to be “done.” There always some other integration that you can do; some other feature. I’d prefer Hell to be cold-blooded software, there’s beauty in finished software.

Based on the above I can define a “Scripting Threshold” meaning, when you reach for a module system or a package system, or abstraction capabilities, or when you want more than what’s in the standard library, then you probably want a general purpose programming language instead.

Taking this into consideration, I opted for making a Haskell dialect4 because: I know Haskell. It’s my go-to. It has a good story about equality, ordering, etc., it has a good runtime capable of trivially doing concurrency, it’s garbage collected, no funny business, it distinguishes bytes and text properly, it can be compiled to a static Linux x86 binary, it performs well, and it has static types!

I made the following decisions when designing the language: Use a faithful Haskell syntax parser. It’s better that way; you get re-use. It has no imports/modules/packages. It doesn’t support recursive definitions, but can use fix to do so. It supports basic type-classes (Eq, Ord, Show, Monad), which are needed for e.g. List.lookup and familiar equality things. It does not support polytypes. That’s a kind of abstraction and not needed. It use all the same names for things (List.lookup, Monad.forM, Async.race, etc.) that are already used in Haskell, which lets me re-use intuitions.

You can download statically-linked Linux binaries from the releases page. To read about the implementation internals, see Tour of Hell which is a set of slides I made for presenting Hell at work.

  1. Tired of issues like this.↩︎

  2. Just check out the huge list of linting issues in ShellCheck.↩︎

  3. See also: Escaping the Hamster Wheel of Backwards Incompatibility↩︎

  4. And not using some other alt. shell scripting language or using Elixir, or Oil.↩︎