Rewriting Bash As A Statically Typed Precompiled Language
Imagine you have a button. It’s big, glowing. The plastic sheen catches the industrial lighting up above. The button does exactly one thing. It will make everyone’s lives slightly better, at the cost of the next few years of your life. You won’t get compensated for your work, hell most people won’t care, life runs slightly smoother and society is mostly unaware.
Fun hypothetical right? It would be, if I didn’t have to choose an outcome that would effect my real life.
Bash and its cousins are everywhere. It’s the lingua franca of how to use a computer. A lingua franca in the sense that the original lingua franca, French, that got bastardised and muddled with some other nonsense languages before shooting to global dominance after being packaged with some arguably more important forces. After years of conquesting and colonisation, the English speaking world was massive. So massive that even countries without ever being colonised starting picking up the language. It’s the standard, it’s what’s expected. No matter where you are now, we have operating systems that either run Bash, or pretend they run Bash. The analogy has fallen apart now. Point being: Bash is big, we’re not quite in love with it, but we use it anyways.
People seem to like this “shell” thing, why don’t we start writing scripts in it? Bash is basically already a scripting language, right?
Decades later, we sit in the aftermath of those small, awkward thoughts.
The jury is still out on if this was the beginning of the end for humanity. Had our hubris finally met it’s limits? Were there still good things left on this planet? Questions like these have stumped philosophers since the start of the 21st century. We all hate them, call them jank & un-proper. Yet they stay. They’re in our build systems, they’re in our docker containers, they’re in our setup scripts. We’re terrorised by this force, and it might be the biggest existential threat facing humanity.
How could we ever fight, or even make do with this threat? Even after so many lives have been ruined by this language?
Forgive me, I seem to have fallen into a bout of mélodrame. To make a long story short, I made a Bash compiler. Here’s the benchmarks.
| Program | bash (Original) | brush (Rust) | llsh (Precompiled) |
|---|---|---|---|
| 99bottles.sh | 191.225807ms | 82.730431ms | 6.433413ms |
| fizzbuzz.sh | 135.352944ms | 13.337657ms | 4.898361ms |
| argexpansion.sh | 3.565208ms | 5.861579ms | 4.861876ms |
| easypipe.sh | 3.794822ms | 5.550223ms | 5.116035ms |
| embededcalls.sh | 4.763272ms | 5.453661ms | 4.917813ms |
| externalprograms.sh | 9.385963ms | 11.411185ms | 10.189784ms |
| functionreplace.sh | 4.550805ms | 4.582633ms | 4.389438ms |
| math.sh | 4.150426ms | 6.008299ms | 5.366714ms |
| month.sh | 3.317533ms | 13.938127ms | 4.5471ms |
| nestedfunc.sh | 3.512353ms | 4.861521ms | 4.460874ms |
| pipe.sh | 44.973558ms | 44.745652ms | 53.859452ms |
| redirect.sh | 9.27273ms | 9.615638ms | 7.185786ms |
| returnstatus.sh | 4.374092ms | 6.17455ms | 4.989212ms |
| shopt.sh | 4.772037ms | 5.510757ms | 4.822539ms |
| stackedscope.sh | 4.261534ms | 5.431558ms | 4.126424ms |
Over most programs, bash-llvm (or LLSH for short) is a quite revolution. The worst case, it produces an executable on par with traditional Bash. In the best case I significantly improve speeds of your slow script. See both fizzbuzz.sh and 99bottles.sh beating the other two by massive margins.
First off, if you are a fan of running untrusted Bash scripts off the internet, I’m just going to start with the fact that I have a nice and breezy installer:
bash -c "$(COLUMNS=50 curl https://raw.githubusercontent.com/FoxMoss/llsh-installer/refs/heads/main/install.sh -#)"
This is the in-vouge way of installing compiler toolchains, and I am nothing but a sheep in the whims of the tech industry, so I must bleet.
Now that you’ve installed LLSH, and you trust me that it’s not malware, let me breakdown some usage.
LLSH For Dummies
The first thing you should want to do is create an executable from a bash script. As expected, it’s not that hard.
llsh compile script.sh --executable script
Now you have an executable of your script! Awesome. Run ./script like a normal binary.
This is where you’ll get optimal performance. You can pass in -O3 if you want
faster execution.
Advanced Usage
Sadly it does not end there. I mean that. There are many, many features that frankly shouldn’t be used in LLSH.
The shell is a prime example. Boot into the shell like you would expect.
llsh
Okay cool. Minimal shell, actually has more features then base bash but we can do better then this. If you’re using a modern terminal emulator (kitty or ghostty) on the other hand, you can get a slightly prettier experience.
llsh -n
Wow. I hope you have a Nerd Font installed, otherwise you just got a wall of jank.
The shell supports file completions, stars, history (with ctrl-r), syntax highlighting, multi-line editing, brace matching, plus probably more that I forgot to mention. Some of these are nice inclusions given to us for little effort from isocline. Isocline is a rather underrated C library that I think many programs, especially debuggers and shells would benefit from using. It’s built with usability, not a pursuit of minimalist simplistic beauty in mind that I find myself in favor of when designing software.
The syntax highlighting is from Treesitter, after I attempted writing it myself. We already have an AST, how hard would it be just to use that for syntax highlighting? Very hard. It would not have been fun. Treesitter knows what it’s doing.
Whatever it supports, it doesn’t matter! Because you shouldn’t use the shell. I designed bash-llvm first and foremost as a compiler project in LLVM, I don’t even have an interpreted mode. So how am I running the Bash live?
LLVM has a builtin JIT engine for projects like clang-repl. LLVM just compiles your code to assembly like it would in a compiler, except it injects them into a runtime. You can still interact with the runtime with your normal precompiled code, to inject more code or interact with variables. It’s the worst cobbled together way of writing a JIT I’m told. There are choices about warm up time and what to compile and what not to compile, which we’re totally ignoring. Despite the failure to come close to the art of a good JIT, it works. The LLVM JIT just isn’t built for being the next V8. It’s fine, it’s just… slow.
I’m now going to put in an embarrassing benchmark to prove my point.
| Program | bash | brush | llsh but now JIT |
|---|---|---|---|
| argexpansion.sh | 3.565208ms | 5.861579ms | 42.261631ms |
| easypipe.sh | 3.794822ms | 5.550223ms | 43.005392ms |
| embededcalls.sh | 4.763272ms | 5.453661ms | 37.606135ms |
| externalprograms.sh | 9.385963ms | 11.411185ms | 47.474204ms |
| fizzbuzz.sh | 135.352944ms | 13.337657ms | 49.921252ms |
| math.sh | 4.150426ms | 6.008299ms | 38.452509ms |
| month.sh | 3.317533ms | 13.938127ms | 118.545703ms |
| nestedfunc.sh | 3.512353ms | 4.861521ms | 35.987735ms |
| pipe.sh | 44.973558ms | 44.745652ms | 95.935869ms |
| redirect.sh | 9.27273ms | 9.615638ms | 49.405298ms |
| returnstatus.sh | 4.374092ms | 6.17455ms | 39.65925ms |
| shopt.sh | 4.772037ms | 5.510757ms | 42.26621ms |
| stackedscope.sh | 4.261534ms | 5.431558ms | 40.904306ms |
That’s not terrible! Again it’s just worse than what exists yes, but most of that time loss is on core parts of the LLVM JIT process, so things I can’t easily optimize. But that’s not the point really, the is that it needs to exist. And I need to talk about it, otherwise people will complain about it being bad.
So, if the shell is that bad, why does it exist, & why have I spent so much time talking about it? I dog food my own shell, that’s why.
I’ve made many projects that are built for some user group which I have accidentally removed myself from. Once I’m done testing and tweaking, I set it down in forget about it. If I’m going to change that, I need an excuse to every day boot up my own product, and understand where the rough edges lie. That’s why I built the shell, I know what Bash features I’m missing and I know how likely it is to crash.
That does unfortunately lead well into my plans for the future of this project.
Yes, there’s more.
As I hinted at in the beginning, this could very easily become a forever project as a slowly fine-tune perfect bash compatibility, but there’s a reason why I’m releasing this blog post prematurely. LLSH is not complete. I need to implement 40 odd other bash features, I need to make improvements to handling system bash files, I need to implement a modern JIT if I don’t want to be laughed out of the room, and I need to do a lot of work generally if this ever going to be complete. I don’t know if I’m cut out for that work. I don’t know if I can “hit that button”, what I do know is this if I keep working on this project, at the end of the road, it will be done.
Time may look upon this struggles unkindly, it is yet to be seen or unseen.
The source code is fully available at github.com/FoxMoss/bash-llvm.
Have a good day.