Shell, do something!
Only calling os.Exec over and over again is not what I envisioned when I started building my own shell. So, in the spirit of first things first, I implemented a few of the commands I use on a daily basis to make the shell actually usable. This makes the whole thing immediately feel a bit more real and also provides some good learning opportunities.
While I am lending some of the names from GNU core utilities or similar battle tested collections of tools, I have no intention at all to rewrite all of them or even follow the standard command line interface. A good example is rm. „We ain't no cowards over here.“ There is no double check if I really want to remove a directory. Just delete it. „I know what I am doing.“ … at least that is what I’ll claim until I accidentally nuke a directory I did not intent to, then I will implement the -r flag.
A less destructive example is mkdir. Yes, if I specify a path with multiple not existing directories, I really, really, want you to create all of them. No need for additional flags.
I mostly bootstrapped the commands to see if the interface works and play around a bit. Listing directories does not even nicely format the output right now, but we will get there in time.
There is also the first command replacing a trustworthy command line tool. I am certain I will run into some fun problems with the mostly naive implementation over time. But for now when I want to download something via http to a file it is as easy as dlf https://foo.tld/file.zip file.zip. I could obviously continue to rely on curl, but where is the fun in that?
Move!
One of the first commands that reminded me not everything is as simple as wrapping Gos os package in a function was mv. Also I was kind of proud of myself remembering that I once read that there is some magic happening when moving files across drives.
If you move files on the same drive you can get away with a simple os.Rename which wraps the rename OS call. But this also means running into invalid cross-device link when there is more than one drive and you happen to move data from one to another. This is where we have to fall back to actually copying the data.
This also happens to be the first command I implemented a check for to confirm I want to overwrite an existing target, otherwise this would just happen.
What is still missing is one of my most used variations of this command - mv /x/y/z.log .. The fun of writing a shell, you learn how much other tools silently do for you without telling you that they do. In this case the . Moves z.log from /x/y to the current directory. My implementations? Moves it to a file called . in the current directory.
Raw(r)
I gave up on the idea of using Bubble Tea as library for the base shell. While it would certainly look a lot nicer, the whole setup feels too unwieldy. I will most certainly end up using x/term and put the whole shell into raw mode.
This does not mean no nice looking things at all (like a fuzzy search with filter as you type history). Bubble Tea, while not the lightest library, starts up fast enough. So I will implement individual commands to use Bubble Tea for the interface of commands, just not the whole shell.
Putting the terminal into raw mode also means I will have to handle every single keystroke. At the same time this means I have to handle every single keystroke and can easily add behavior for things like Ctrl+c.
I will see how it goes, raw mode and a command history are the next two things on my todo list.
posted on June 7, 2026, 7:19 p.m. in golang, lazerbunny