Writing a shell is rarely the kind of project you take on lightly. In this episode, Johnny is joined by Qi Xiao to explore how to go about such a feat in Go.
Featuring
Sponsors
Fly.io – The home of Changelog.com — Deploy your apps close to your users — global Anycast load-balancing, zero-configuration private networking, hardware isolation, and instant WireGuard VPN connections. Push-button deployments that scale to thousands of instances. Check out the speedrun to get started in minutes.
Retool – The low-code platform for developers to build internal tools — Some of the best teams out there trust Retool…Brex, Coinbase, Plaid, Doordash, LegalGenius, Amazon, Allbirds, Peloton, and so many more – the developers at these teams trust Retool as the platform to build their internal tools. Try it free at retool.com/changelog
Timescale – Real-time analytics on Postgres, seriously fast. Over 3 million Timescale databases power loT, sensors, Al, dev tools, crypto, and finance apps — all on Postgres. Postgres, for everything.
Notes & Links
Chapters
Chapter Number | Chapter Start Time | Chapter Title | Chapter Duration |
1 | 00:00 | It's Go Time! | 00:47 |
2 | 00:47 | Sponsor: Fly | 02:45 |
3 | 03:32 | Qi Xiao | 00:48 |
4 | 04:20 | Elvish | 02:30 |
5 | 06:50 | What is a shell? | 09:09 |
6 | 16:00 | Sponsor: Retool | 02:51 |
7 | 18:51 | Why Go? | 04:08 |
8 | 22:59 | Different OS | 03:32 |
9 | 26:30 | Features | 09:05 |
10 | 35:35 | 1,000 line bash file | 02:21 |
11 | 37:56 | The perfect use case for Go | 05:27 |
12 | 43:24 | What Go made harder | 02:56 |
13 | 46:19 | What's next? | 04:05 |
14 | 50:25 | Sponsor: Timescale | 02:17 |
15 | 52:42 | Unpopular Opinions! | 00:26 |
16 | 53:08 | Qi's unpop | 04:37 |
17 | 57:46 | Johnny's unpop | 06:27 |
18 | 1:04:12 | Outro | 01:04 |
Transcript
Play the audio to listen along while you enjoy the transcript. 🎧
Well, hello, hello, hello. Welcome, listener. I’m Johnny Boursiquot. Welcome to another episode of Go Time.
Today, I have a very special guest… Someone who decided to go on a very interesting journey, one that I personally would find very daunting. This person, this individual decided to write themselves a shell. You heard that right. Like the ZSH, or Bash, he decided to write himself a shell, and he did it in Go. Welcome, Qi Xiao.
Hello, hello.
Where do we start? Like, of all the side projects you could have come up with, why a shell?
Right. So I started Elvish about 11 years ago, I think. And at that time, the alternatives you have picking the shell is like - traditionally, there’s Bash, there’s always Bash, and there’s ZSH. And Fish was alone for quite a while… I just felt all the shells were a little bit limiting, and it is something that people use all the time… And the shell is this interesting thing where it’s both a programming language, but it was also a UI people use to access their operating system. It’s why it’s called a shell, as opposed to like a kernel of a system. And I think designing, implementing a programming language is really fun, and also just like doing UI stuff is also pretty fun… So I think a shell would make a good, interesting project.
And you’ve been working on Elvish - that’s what you named your shell, your implementation… You’ve been working on it for how long?
11 years.
11 years. Wow. Wow. Now, is that because that’s an on and off thing? Obviously, it’s not your primary way you make a living, so that’s been – it’s not because it took 11 years to get a working shell, it’s just more like you’ve been working on it for that long, right?
Yeah, it’s an on and off thing. I mean, I started the project when I was still in uni, so I had a bit more spare time… But as I got a full time job, there’s slightly less going on, and I’ve not been working recently, so I’ve spent more time recently on Elvish. But if I go back to full-time employment, I’d probably spend a little bit less time on it.
Right, right.
So a lot of the developments that get you a basic shell happened just in the first year or so. So I started daily driving Elvish just about half a year into developing it. I was switching between ZSH and Fish at that time, but after working on Elvish for about half a year, I decided this has enough features for me to use it as my default terminal shell.
So Elvish is your default right now. Pretty much everything you do that you need to do in a shell, is – you consume your own dog food, as they say.
Yeah, yeah.
I see. I see. So we’ve touched on what a shell is. It’s both a programming language, and an environment for executing or for stringing together – most folks listening to the show will be familiar with the concept of a shell, but what ultimately is a shell? Is it just an execution environment, or how would you describe that to somebody who’s not in the know?
Yeah, it is a very good question, actually. So I think from a purely technical perspective, the way I would describe a shell is that – it’s basically just another terminal application. So it’s on the same level as like Ls or Vim, except that Vim is obviously much more interactive than Ls. But they pretty much function at the same level. They run the terminal, they write some outputs, and they’re capable of taking – well Ls is not capable of taking inputs, but think about CAT; it’s capable of taking inputs. And there’s a lot of obscure terminal API stuff going on, but that’s the essential difference between these programs.
[00:07:59.13] On a technical level, we can think of the shell just like another terminal application, except that the thing you do with the shell is mostly just launching other applications. But on a technical level, they are very similar. But that’s just a purely technical answer, but people often confuse shell with terminal, which I think is not because people are not – on the one level, some people are just not familiar with the way things are done in the Unix world. But on the other level, I think there is a bit of truth to that. When people think about the shell and the terminal, people think of it as not exactly the same, but part of your text-based working environment. So in that sense, on a more conceptual level, you can say a shell is actually slightly different from all the other terminal applications, in that it’s actually more in your environment, instead of like yet another application. I think that’s actually a very interesting phenomenon. And it’s almost a historical incident that Unix decides to do things this way. You have the terminal and you have the shell, and those are two different separate programs. But it’s easy to imagine a world where things don’t work that way. But in the Unix world that we live in, a shell is technically just another terminal application… But it’s a slightly special one in that it’s probably the first terminal application that’s launched by your terminal.
Right, exactly. As you were describing it, I kept thinking, okay, well – every time I launch a program, a terminal program, whether I happen to be using macOS, or there’s the native terminal app that comes with your operating system… And then personally, I use iTerm, which is another popular one, but I’m sure there are others out there. But those things are programs. So by default, they launch a shell. It could be Bash… By default, that’s what the terminal app does. But when I launch iTerm, the program, it launches a shell. And my shell - I basically wanted to not stick with the default, which was Bash. I wanted to use ZSH instead. So I installed ZSH, and I made that my default. So it’s almost like the terminal – could I think of it as the backend, and the shell is the frontend that I use to interact with the backend?
Yeah, I think that’s a good way to think about it. You can almost think of the terminal as a graphics runtime, except that most of them only support graphics in the sense of like just text. Another way to think about it is maybe you can think of the terminal as a browser, and you can think of shell, Vim, Ls, Cat as just like websites. That’s another way of looking at it. Except that the terminal does much less than the browser, because part of its functionality is delegated to those applications. Terminals - sometimes they don’t do tab management, but you can use Tmux for that. And Tmux can run other programs inside it. So it’s like a browser where you can nest tiny other browsers inside those browsers.
Yeah, I remember, there was a time when I used to use Windows, many, many, many moons ago. And I would see in the movies – some movies did actually an okay job of representing what “hacker culture” was. If anybody has ever seen the old school movie Hackers, with Angelina Jolie and some other famous people, then you’ll remember they were actually using a terminal, they were issuing commands… I’m like “Oh, this is so cool. How do I become one of those people?” But I was on Windows, and I’m like – you launched a command program on Windows, and it’s like “Oh, it’s just not as cool”, you know?
[00:12:13.18] So I started trying to use Cygwin back then, which - there was no PowerShell back in those days. And yeah, I kept trying to embrace the shell life, if you will. Eventually, I tried Linux; back then it was still rough on the desktop. Every year [unintelligible 00:12:30.19] Never quite made it. I’m not sure if things have changed since then, but… Eventually I was like “Okay, I need my operating system to get out of my way, so I can get things done, but I also need the power of a shell”, so I settled on macOS. And then basically I started embracing the shell more, because ultimately - and we’ll get into what some of the behaving capabilities of a shell are, and what Elvish does differently… But I started noticing that – or rather when one is learning to use a shell, you go through this metamorphosis of things you used to point and click to get things done, to move around your machine, launching Finder windows, and blah-blah-blah, trying to navigate your system. You start finding that you can type your way through that, and do pretty much virtually all of these things, but using text input instead; you go through this metamorphosis, like “Wow, this is really cool.”
A layperson would be like “What are you doing? This is crazy. Why would you want to use a computer that way?” But no, for a certain kind of individual, that is a dream.
Yeah, yeah. I think a lot of us went through that journey. I think when I first got into Linux, I was also very attracted by like people doing cool stuff in this black window… [laughter] One example I always tell people about is FFmpeg… Because when people want to do - not exactly like video editing, but simple-ish, but not that simple video processing stuff, like “I just want the first 10 minutes of this video.” Or “I want to just extract the audio out of this video.” If you are like in the GUI world… Because there’s just so many different things you can think about doing video processing. Either you try to find a very, very specific GUI app, that’s just like audio extractor, and then you feed it a video file and it spits out an audio file. A very specialized GUI app. Or you go full in and install a video editing app that’s like 100 megabytes – well, not megabytes; 100 gigabytes these days. And then you click through its menus and do this one thing that you want. But when you come to the terminal world, it’s – just google for FFmpeg command. It’s probably like a single line of command, and you can do so many different things… Because the terminal interface is such a a thin layer of abstraction over the underlying code.
I suspect that the CLI of FFmpeg is probably just a small part of the entire codebase. But if you do a GUI application, if you do a GUI equivalent of FFmpeg, most of it is going to be like “How do I organize the menus?”, how to place each button… You have to think a lot about that things. But in the terminal, because it’s such a thin layer of abstraction, you suddenly get access to much, much more, much, much more easily as well.
Break: [00:15:52.05]
So why Go? Why pick Go for such a task?
Right. So think about yourself 11 years ago. And it’s a little bit surprising, but the programming language landscape is actually a little bit different from today. So when I first started working on Elvish, Go actually just came out. It was not literally Go 1.0, but it was 1.1. So it came out like half a year before that. And when I was thinking about if I want to write a new shell, what are my choices? At that time, pretty much the choices are C or C++, Java, or like a more high-level dynamic language, like Python. And Go at that time was this very modern kid, that’s much less verbose than Java, that’s much more expressive than C and C++, and it’s also much faster than Python or Ruby. So it was pretty much a no-brainer at that point, because Go was this super-modern language 11 years ago.
If people try to decide to write a new shell today and decide which language they should do it in, the choice will be slightly tougher, because today we have Rust… Like, Rust didn’t actually come out 11 years ago. I’m sure there was a pre 1.0 version in development, but it wasn’t really known at that time. There’s Rust, there’s Zig, and there’s a bunch of other modern and compiled languages. But I would still do it in Go, because of things we’ll probably discuss later.
Right. Yeah, so that’s interesting. These days - you’re right, most people would think of writing such a project probably using Rust or Zig, and one other one I’m thinking about… It’ll come to me later. But I think Go used to be labeled as the systems language as well. I remember the early days of Go, people used to refer to it – I mean, even I think the Go Doc used to refer to Go as a systems programming language. But I think over time it found this niche, sort of – I mean, you can still do system programming stuff, but I think most people these days would pick a lower-level language, and… Go rules of the cloud stuff. Like, that’s – Go won that battle. But yeah, it’s like watching the programming language itself find its niche within the tech ecosystem has been an interesting journey to watch Go go through.
Yeah. On a side note, I think the debate around system language is actually quite interesting, because I think the original creators of Go, when they say system programming language, they’re probably more thinking about distributed systems instead of operating systems. And I think today the niche of Go does not contradict that, because all the cloud stuff, what you’re doing is fancy distributed systems.
But yes, the niche of something more close to the operating system is probably more occupied by like Rust and Zig these days. But you can still do a lot of not real time low-level stuff, but fairly low-level stuff with Go. Like, Go has a very complete binding for the system calls on Linux, BSDs and Windows. So you can do pretty low-level stuff as long as it’s not like literally real time. If you can live with a little bit of garbage collection latency, then you’re probably fine.
So I’m curious if – obviously, being able to distribute Elvish on all the major operating systems as you’re able to right now, I’m definitely thinking that Go definitely plays a role in that, being able to have a single binary for every single platform… In building those features, did you have to do different things for different targeted OS’es and platforms in your code?
Yes, but not as much as you would think. So one thing I did differently for different platforms is how you read keyboard events from terminals… Because Windows actually has a different API for reading keyboard inputs, that actually gives you – so if you are curious about this subject, you can just look up like “terminal escape sequence.” Some keyboards like that. So the way keyboard input works in the terminal is much less than straightforward. If you type a simple letter, if you type A or B, that’s what the program sees. But if you use a function key like F1, F2, Home, End, or God forbid you use a modifier with your function key, there are 10 different encoding schemes that different terminal emulators use to represent those keys. So you have to – as a shell, you have to parse all those escape sequences and back into keyboard, into like modifiers and like function keys.
[00:24:13.26] But Windows actually has a much more sensible API, that just gives you “This is the function key, this is the modifier key…” So I used that on Windows. But Windows now also supports the – it’s called VT100 style escape sequences. Like, Windows also supports that terrible way of encoding keys. It’s technically no longer needed, but I like the fact that Windows actually has a much cleaner API for that. So this is one place where it’s different code paths and different operating systems. And aside from that, there’s actually surprisingly little platform-dependent code.
So one thing you might think that will be different is how to launch external programs. But Go actually has a very complete abstraction for that in the os/exec package. So it gives you pretty much everything you need in a shell to launch external programs. And if you think about how shells launch external programs, some of it can be pretty fine-grained. You need to control the environment of the program, you need to control where the output goes… Like, if it’s in the pipeline, the output goes to a pipe instead of to the terminal, it probably goes to a file… Things like that. But Go actually gives you everything in a portable way in os/exec. So I didn’t actually have to write – there’s a tiny bit of platform-dependent code there, because os.exec, the argument it takes contains this field… I don’t remember the exact name of it, but it basically says “system-dependent metadata.” And that is the only thing that I need to make it per platform. But other than that, it’s pretty – it’s quite portable.
And obviously, there are some things that are Unix-specific, that has to be platform-dependent. Unix has Umasks, which is a [unintelligible 00:26:17.19] that’s applied when you create a new file. Unix has Ulimits, those kinds of stuff; those are obviously-platform dependent.
Yeah, some of those things don’t carry over quite well to the Windows world. So we haven’t really talked about features of Elvish. What does Elvish have over the mainstays, like a Bash, for example? What does it do? What would wow me, to be like “Wow, Elvish is very different, very powerful.” What would stand out?
Right. So that goes back to the origin story of why I decided to write a new shell. I did it mostly out of frustration. So I had two experiences with more traditional shells at that time. One was I was doing a little bit of system administration for some local Linux servers, and I got exposed to a hundred – not a hundred, a thousand-line Bash script. And I was trying to maintain it, and I just found it impossible… Because it’s full of all those obscure things. You have always have to put your variables in double quotes, because otherwise the white spaces in the variables will do naughty things. And you can never figure out what’s the correct way of comparing two strings. There’s a test command, and Bash has this left bracket command, that’s the same as test… It also has a double left bracket command; that’s not the same as test, but instead it’s something else.
[00:27:58.03] So all those all those details were pretty much driving me insane, so I thought “There must be a way to write more sensible shell scripts.” But at that time, I didn’t actually find a lot of good ways to write sensible shell scripts. So that’s one experience.
Another experience was I was – so I switched from Bash to ZSH, and I was trying to implement directory history, because at that time… That was before things like Oh My Zsh came along, so there wasn’t that many like out of the box ZSH plugins floating on the Internet at that time. So you had to do things yourself. So I was trying to implement directory history, and I read through a lot of ZSH’s man page, and the – because ZSH actually has some fancy UI features. Its tab completion is pretty fancy. So I thought it must have good primitives for doing sensible UI stuff. But the entire UI system – I also got a little bit frustrated. I basically gave up, because he API is pretty – I mean, it can do very powerful things, but it’s not the easiest to use, to be honest.
So these two experiences made me think, one, we need a shell that actually feels like a real programming language, and second, we need a shell that actually has sensible UI features out of the box. So those are the two main themes of Elvish, is one has real programming features… So as a starter, Elvish has lists and maps, and they can be nested, whereas in more traditional shells, it probably – well, if you go back to the very origin, like [unintelligible 00:29:40.19] the only thing it has is strings. And you can split strings on spaces. So you can put spaces in your strings, that you can pretend you have lists, but not really. And some later shells have actual lists, or even maps, but they don’t nest. Like, you can have a list of strings, you can have a map from strings to strings, but you can’t have like a list of lists, or maps of maps. That’s just not a thing in more traditional shells. But Elvish has all of those things. It’s like a real – it’s like a program language; anything you can represent in JSON, you can represent in Elvish.
And Elvish also has lambdas. So it has functional programming. It is a very functional programming – I’m saying programming twice. It’s a very functional programming programming language. So a lot of the code in Elvish will look a bit like Lisp, because it embraces the FP paradigm. So that’s one thing - real programming features [unintelligible 00:30:41.27] out of the box UI features.
One thing – I would give two examples. One is that it has a built-in file manager. So if you just press Ctrl+N in Elvish, it gives you a view of your current directory, your parent directory, and a preview of the file you have selected. So if anybody has used Ranger, the UI is very much a copycat of that.
And another thing is… So in Bash or Zsh you have this – you can press Ctrl+R to search your command history. But it only gives you one match at a time. In Elvish, I kind of stole the same key binding, but if you press Ctrl+R, it doesn’t let you search one command at a time, instead it just gives you a list of all your historical commands, and you can filter that command and you can see all the commands that match your current filter. So you can do things like that.
So you ship with some built-in commands, but you also rely on external commands. I mean, pretty much like you would in any other shell, and basically running Git, or any external tooling. So are the built-in commands geared towards the programming tasks that you normally need to do kind of thing?
[00:32:06.22] Right. So that’s a very good question. I would say mostly yes, but there’s also a bit more consideration about what commands people tend to use, tend to need in their shell. So it’s very much built like a programming language. So one interesting example that I can think of is - for instance, you have a… In Unix you have an mkdir command to make a directory, and that’s very much geared towards interactive use. For instance, you can make multiple directories with it. But in Elvish – Elvish actually also has a built-in mkdir directory, although it lives in a separate namespace. It’s called os:mkdir. But that one actually only takes a single argument, because it actually functions very much like Go’s mkdir function in the Os package. And if you want to make multiple directories, you can just write a loop. [laughter] So it’s definitely more on the programming language side of things.
Right. So you wouldn’t – yeah, so you’re not trying to maintain compatibility with existing tools out there. Normally, you do mkdir with a dash, with a -p flag, and then you can do multiple slashes, and it creates that tree…
But you’re like “Okay, well, I don’t need to do that. If I need to create multiple directories, I’m going to use some for loops.” [laughs]
Right. So it’s slightly more nuanced than that. As a starter, Elvish doesn’t prevent you from using any of the existing programs. So when I’m using Elvish - I use macOS most of the time these days - I can still use the system’s mkdir. And I do use it a lot of times. I still use it as the default way of making a directory. But Elvish has built-in mkdir; it also lives in a separate namespace, as I mentioned. It’s called os:mkdir, instead of just mkdir.
It is useful when, for instance, you are trying to write a portable script that can run on both Unix environments and Windows. Oh, and did I mention Elvish supports Windows? I did not, but Elvish supports Windows. [laughter] So a lot of time, those built-in commands, they really shine when you are trying to write those portable scripts. But if your concern is really just like writing a script you use on your system, then you can just use anything that you want. You’re not restricted in any way to use Elvish’s built-in commands.
So I’m looking through the reference for the modules, beyond the built-in stuff… You have modules for documentation, for editing, there’s a package manager, command line flag parsing, there’s math… There’s quite a few things here. So these things, looking at them, most are really geared towards – I mean, there’s a lot of them that are geared towards directory and file handling and whatnot. But some, like string manipulation, for example, are things that you would need in your day-to-day. If you were like a sysadmin and you needed to write some scripts here and there, those would come in handy, I would imagine.
So there’s a mixture of things here related to system, file and directory manipulation, but also there’s some runtime stuff and whatnot. So it sounds like you built a tool to deal with that thousand-line Bash file. So did you ever go back and modify that thing, or had you moved on? [laughs] Is this a tool you wish you had back then?
[00:35:49.23] Yeah, I didn’t go back to that thousand-line Bash script, because somebody else took over the maintenance of that script. But I did rewrite – so Elvish has scripts; the project REPL has a bunch of scripts inside it. And over the years, I have rewritten all the Bash script in Elvish, and it’s always an improvement… Like the string manipulation thing you were just mentioning. So lBash does have string manipulation functionalities, but they usually have pretty obscure syntax. There’s a built-in way in Bash to remove a prefix from a string, and the way you do it is, say, if your string is $s, what you do is $, and then brace s, and then I think it’s hash, and then hash prefix, and then closing brace. So it uses this very symbolic syntax; it makes extensive use of punctuations. Which I guess if you do this on a daily basis, you will remember it and you will like how concise it is. But if you only do this once per week, you probably are not going to remember it, and you will keep doubting yourself, like “Is this the right punctuation?” But in Elvish, because it’s designed like a programming language, you just go to the Str module, and there’s a function called str trim left. There’s str trim left and str trim right. Oh, sorry, there’s str trim left and str trim prefix. So those are the things you would use, because it’s slightly longer than just writing a hash sign, but it is much more readable. And if you’re a Go programmer, these are implemented using the same functions inside Go’s Strings –
The strings package.
Yeah. And they behave identically, because it’s literally just implemented using the function from Go’s standard library.
Is there a particular feature that you implemented in Elvish, where you were like “Okay, this is why I picked Go. This is the perfect use case.”
Yeah. So there are actually two. One is [unintelligible 00:38:10.22] the standard library. So Go has a great standard library. It’s very high-quality, it’s very well documented. It’s a lot of batteries, and it’s a lot of good batteries. You were just going through the reference menus of Elvish… If you look at that, half of it is basically just Go’s standard library like an Elvish binding. So the str library is just Go’s strings library. The Re library (for regular expression) is just Go’s regex library. I made the names slightly shorter, because in Elvish module names and variable names - actually, they don’t live in the same namespace. So I can afford to make it slightly shorter. And the path library is just Go’s os/path. The Os library is basically Go’s Os library. Math is basically Go’s Math.
So much of Elvish’s standard library comes from not – like, some of them literally just come for free, because there’s a very straightforward way to adapt that Go API to an Elvish API. I used Reflection for that. And some of them requires a little bit of wrapping, but never too much.
So the standard library is one very large part of the reason of why, if I’d do it today, I would still do it in Go, because I think most other languages, they don’t really have as extensive a standard library as Go. Like, if somebody asks “Can I have an HTTP library as part of Elvish’s standard library?”, I can probably just use Go’s HTTP library for that.
Right. Yeah, just create the Elvish bindings, and - yeah, off you go.
[00:40:09.10] Yeah, exactly. Although, because the HTTP library deals with a lot of functions, function bindings are slightly harder, because the way you do functional programming in Go and Elvish are slightly different. Another thing aside from the standard libraries, a very interesting part of the runtime, that I didn’t – I didn’t really think about it when I first started using Go, but Go is a garbage collector language. Elvish is also a garbage collector language. So a happy consequence of this coincidence is that I don’t have to implement my own garbage collector, because Elvish values are just Go values. So if an Elvish value becomes garbage, it also becomes garbage in the Go runtime. So it would be collected by the Go garbage collector.
But if you write a new programming language in Rust, and it’s a garbage collector language, you do have to spend time writing your own garbage collector. Or maybe use one of the existing garbage collectors. But in any case, you would need your own garbage collector. But if you’re writing in Go, you don’t need your own garbage collector.
I keep thinking, these days – when folks are making programming language decisions… Slight tangent here, but the – perhaps it’s just me, but I think the decision to use a programming language or not, the decision-making process, why you pick a language over another, it felt like it used to be more technical more, more stringent; you had specific things you would be looking for… Whether you wanted an interpreted language or a compiled one, or a garbage-collected one or no garbage collection. These decisions, these days it seems like whatever’s cool, grabs attention. But I think for our listeners, I’m sure many are thinking “Okay, the process of selecting a language should be a bit more critical.” It should take a more critical approach, or a critical eye. This goes back to the whole pick the right tool for the job kind of thing. At least from what I’ve seen, most programmers with any sort of medium to long-term career will pick up a few languages, and put that in their tool belt, and use the appropriate language for the given situation. So when you mentioned that “Okay, I’m writing Elvish, it has GC, and I can just piggyback on the languages, the underlying language the GC”, I’m like “Okay, that’s a good reason to pick a language”, and when you’re deciding what tool you’re going to be using to build your own product.
Yeah, definitely. Although it is something I wasn’t actually thinking about when I first picked Go.
It was just a happy accident? [laughs]
Yeah, later when I was reading a lot of articles about implementing Garbage Collector for the new language, I suddenly realized “Oh, I never had to do that.” And I realized I never had to do that because I picked a garbage collected language as my implementation language.
Nice.
So it was the right choice, but it was unconsciously made at that time. But when I look back, it’s a very sensible choice.
Was there any feature that Go made particularly difficult?
Right, so not really, I would say. So I don’t think of a – I can’t really point to one feature in Elvish and say “It’s all Go’s fault that I have to write three times more code compared to implementing it in Rust.” There is not a single feature that’s like that. And I think it’s because Go is actually a very good language when you think about applications. It’s a very good language. It’s very geared towards implementing applications.
[00:43:57.00] I mean, Elvish is obviously an application. You can consume it as a library, but I don’t think of it as a library that much. I don’t worry about it as a library that much. However, there are things that I think does make it slightly more painful, more ugly in Go. Again, it goes back to - I don’t think Go is actually a very good language for implementing a library, because… I would mention two things. One is that Go doesn’t have true enums, and it doesn’t have true tagged unions. And if you’re thinking about implementing an application, you don’t really worry about that too much. But if you’re thinking about implementing a library, a lot of times the API can be much more cleanly modeled if you have true enums, or true tagged unions.
For instance Elvish’s parser - there are maybe 20 different types of nodes in the syntax tree, and it will be very handy if I actually have a Rust-style enum that [unintelligible 00:45:00.19] is one of those 20 different types of nodes. That actually can be quite handy. It makes some code a bit cleaner.
So even though I don’t think of – Elvish itself is not a library, it’s an application, but it’s made up of a small… It’s built as small modules, which themselves are kind of libraries. And internally, it does create a little bit of internal friction, but it doesn’t really show up on the overall application level.
Another example is Go doesn’t have keyword argument, default arguments… And the way people work around that is maybe like have a struct, and whenever you want two keyword arguments, you just make them fields of the struct. It is fine if you’re building a library that’s consumed by a lot of people, but if you’re just building a kind of internal library, an internal API, it does feel like this is a bit too much boilerplate. So it doesn’t make for the – Go doesn’t make for the cleanest API, is my take. So a lot of time I just keep looking, staring at a piece of code and think “I wish this looks a little bit cleaner. I wish this looks a little bit more beautiful.” But you have to accept the limit of the language.
So what’s next for Elvish? Is that something you’re still adding features to? Or are you just fixing bugs at this point? What’s the state of the project?
Yeah, so in the past in the past few months I’ve been working on a new TUI framework in Elvish. So if you think about – it goes back to our very earlier discussion of what a shell really is. If you think about the browsers, there’s a lot of those web frameworks - React, Angular, Vue - that you can use to build very modern, very clean web applications. I think there are some that lets you build good, clean terminal applications, but I think we could probably use some more, especially if it is a TUI framework that you can just access directly from your shell. So if it’s something that you actually use every day, and you can just – you can use the TUI framework to extend Elvish itself, to give it a little bit more interactivity, or you can just use it to implement new TUI applications. You can probably implement Tetris in the new TUI framework.
So that is something that has been in the works for multiple months now, but I’m hoping to merge it into the main branch of Elvish soon. So when that happens, Elvish will become like the UI – like I said before, the two main things about Elvish is that it’s a real programming language, and it has good UI features. And I think if you have a TUI framework in Elvish, this is where those two things actually come together, and become a whole; it becomes a UI thing that you can very easily program. I think that that would be pretty cool.
[00:48:05.21] So that’s the immediate next thing. But if you think about the long-term future - again, this goes back to what we discussed before, is a lot of people confuse shells with terminals. And I think there’s a good insight in that. It should really be like just one runtime where you can manage your programs. And there are a lot of things that you can’t easily do, because these are two separate programs.
A shell has command histories. You can look at which commands you have run, but the command histories don’t tell you what the outputs of the commands are. It’s because the shell actually does not have control over those commands after they have been launched by the shell. The terminal has control of those commands. So you can probably do more cool things if your shell and your terminal is actually more tightly integrated, or even just like one application. But that’s very vague, very futuristic, I guess.
Okay. I’m pleased you didn’t mention “Oh, I’m going to throw an LLM in there, and have that generate commands for you”, and things. [laughter] That’s the new hotness these days. Everybody needs to put gen AI somewhere, in whatever they’re building.
Yeah. But I think with the new TUI framework, maybe somebody will build that into Elvish as a plugin. I’m sure of that. And to be honest, LLMs are used like – I’ve had some success in asking them crafting FFmpeg commands, because FFmpeg just has so many different options… So it’s quite useful. But sometimes it does give you very subtly wrong shell scripts. So I would probably trust LLMs for like one-off commands, but I really wouldn’t trust it for writing scripts.
[laughs] Exactly. Especially with any command where it has a destructive effects, like directive manipulation, deleting files, or anything like that. I would never ask an LLM to generate that for me. The risk there is just too high.
Yeah.
Break: [00:50:21.27]
Okay, okay, so I hope you brought some unpopular opinions. You got one for me?
Yeah, of course. So my unpopular opinion is that when it comes to testing, 100% code coverage should be the minimum, not the end goal.
What? You want a hundred percent test coverage?
Yeah.
Okay, okay… Explain that. I’m pretty sure that’s not going to be popular with a few people. Explain yourself, sir.
Right, right. So let me make this probably slightly less radical, but not by changing what I said, but by saying that… Because all of us want 100% code coverage. But if you look at the test coverage of your application, of your library, it’s probably not 100%… But we all think that all of the code must have been wrong at least once for us to know that it actually functions right. If there are 10 lines of code in your codebase, that just has never been wrong before. Like, why do you even have it? Why do you keep it? And your answer to that may be “I actually did manual testing, and that relies on these 10 lines of code. It’s just not captured by my automatic tests.” And I would say that in spirit, that counts as 100% code coverage.
So it is actually something we all strive for, we just can’t achieve 100% code coverage in automatic tests, in unit tests. But deep in our heart, we all know that if you have written 10 lines of code, you must have run it once to make sure that it’s actually useful.
And the reason I say it’s the bare minimum is that running the code, making sure the code has been run at least once is really just the start. Because if you have written a program that just prints out like A and B, and it’s supposed to print out A and B, but it actually prints out C, and you have run it once, but you never checked what the output is, that code technically has 100% code coverage, but you’re still not sure that it is correct. And to make sure that it is correct, you have to actually put the correct checks in place.
So having run the code once is the start. You also have to make sure that you have actually checked the effect of that code. So this is my take on code coverage, and I think the reason people shy away from 100% test coverage is first, we are not really adequately capturing all the activities we have done into code coverage. If you write a program and you do manual testing, the code that you have triggered during your manual testing is not counted as part of the code coverage. And worse still, sometimes if you do integration testing, the tooling also prevents you from counting that as part of the code coverage.
[00:56:04.16] Most of the metrics only actually count unit tests. But I think Go actually - a few versions back, they actually implemented support for counting end-to-end test coverage. So you can now generate code coverage for end-to-end tests, which I think will make a lot of projects’ code coverage go up a lot… Because when you actually run the binary as a whole, instead of running a single function in a unit test, that usually exercises a lot of code paths. So I think we should all embrace that.
And another reason is that a lot of libraries are just outright untestable. For instance, if a function just calls os.exit, you’re not going to test that in your unit test, because that would just exit the entire unit test program. But why not? You should be able to test those things.
And if your program sends something over the network, that can be a little bit hard to mock… And what people usually do is just extract the computation part of your program, and only test those parts, and you don’t test the part where you actually interact with the real world. But I think you should. And I’m not blaming everybody for not doing that. I’m saying that the ecosystem doesn’t give us enough support to do those tests easily. But I think that is something that should be changed.
Okay. Well, I mean, not an extremely radical take… I can parse some sensibility in there. Alright, alright… Yeah, I have - perhaps not an unpopular opinion, but I recently… As we’re recording this, I think a few days ago there was a keynote that I watched. There was a GitHub Universe conference going on, and there was a keynote. And in the keynote, they were talking about Copilot and its ability to - like the whole Spark system, whatever; its ability to generate apps from scratch, and all that stuff… And that’s usually one of the selling points, the marketed stuff that says “Hey, you can build an app from scratch”, blah, blah. And it’s always like – the pitch is always to generate these new, one off, usually very small, very low-feature applications just to show that “Hey, look at this thing. You don’t even need a program anymore.” But these things are usually very simplistic applications. Not to say that some of them aren’t sophisticated, but when you’re generating things from scratch, like a greenfield, you have the benefit of basically writing everything from scratch. But for existing systems, for existing large systems, that have been around for years, decades even, those are the things that I need help with. Those are the things that when I’m tasked with maintaining a system that I’ve – say I was hired by a company and they’ve been running this software that runs their business for well over a decade, and here I am, tasked to make a change, or add a feature, or whatever… Those are the things that I need help with. And I’m not generating CRUD apps, or to do apps with an LLM from scratch. I’m trying to work into an existing codebase.
So one of the things that they showed was - I think it was just available for Java, but there was something that could help you upgrade your application. Maybe like you have an old school Java app, maybe you need to upgrade which virtual machine version it uses, or whatever the case may be… And it’s going through all the different files in the project, and basically changing the syntax… Maybe you used old school syntax; it uses new syntax to represent something. Maybe you want to use lambdas, or whatever it is. It’s going through and allowing you to change all these different files, basically upgrading an existing application. But I think even that doesn’t go far enough.
[01:00:05.19] And I think there’s always going to be a need for a human to provide some additional context, some backstory for an LLM that wants to change code… Because so much technical debt I’ve seen in my time, a lot of people who no longer work on an organization, they walk out the door with institutional knowledge in their heads… It’s not documented. You see a piece of code and you’re probably in a situation; you change something and you think you are doing the right thing, and then all of a sudden you broke something else somewhere else, not thinking that “Oh wow, I didn’t know why this code was there. It just seemed like a wrong thing, so I just “fixed it.” And then you end up breaking something else you didn’t know was relying on the code to work a particular way. So this technical debt stacks up over time, and it’s not like people are keeping track of the reasons why they can’t change, or shouldn’t change something.
So when you throw an LLM at these kinds of situations – not to say that one day we won’t be able to have models powerful enough to fix those things, but these are the kind of… Like, when they’re doing those marketing presentations and getting everybody excited about large language models and gen AI, those are the kind of demos I want to see. Take an existing app, with a lot of assumptions baked in, and refactor that. Because that’s my job. I’m not generating a 3D chess program for work. That’s not what I’m doing. That’s not how I earn my living. I work on old, legacy systems, with a lot of assumptions, a lot of decisions that were made prior to me getting in front of this codebase. Help me deal with that mess, not the trivial stuff.
So perhaps not an unpopular opinion, but more like a, I guess, a request. Well, I don’t even know what it is. It’s more of a – perhaps letting everybody know that yes, the demos look cool, but we’re still some ways away from truly being able to leverage LLMs to do the things that we really, really care about… Unless you’re building something trivial and greenfield, which is not most of us.
And ironically, I think this is where a hundred percent test coverage will help. [laughter] If your code has really good test coverage, you may be more confident in letting a language model try to make changes to it, because you have some confidence that some of the old behavior is probably kept.
Yeah. Maybe the tests come with the baked in assumptions that were made for the code that they’re testing.
Right. Yeah. And I think language models may also be useful for helping you achieve a higher test coverage, suggesting. They’re really good at generating things, so you can make them generate tests as well.
That is true. Yeah, that is something I’m increasingly relying on models to do. Which is interesting. I’m wondering how TDD purists are dealing with this… Because if I can just focus on making the code work, and then I can then throw an LLM at it and say “Hey, write tests for this”, then I can focus on getting the proof of concept or the working code, I can focus on getting that done. Because I don’t think – I mean, a lot of people I’ve come across, they’d love writing tests, because it helps them through the thinking process. But some of us, we like to write the code first, test later.
So perhaps those of us who like to – I guess you could also have an LLM generate your tests first, but you’d have to be very descriptive in your prompt, to tell it what to generate for you, before you go write the code that passes that test. But yeah, it’s an interesting time to be a developer, I’ll tell you that.
Yeah, definitely.
Alright. Well, it’s been a pleasure having you. It’s been great to actually learn about Elvish, what it does, and how Go has helped along that journey. Yeah, I wish the project continued success. Thanks for coming on.
I’m glad to be here. Thank you.
Our transcripts are open source on GitHub. Improvements are welcome. 💚