In this post, we will write a minimalistic shell for UNIX(-like) operating systems in the Go programming language and it only takes about 60 lines of code. You should be a little bit familiar with Go (e.g. how to build a simple project) and the basic usage of a UNIX shell.
UNIXis very simple, it just needs a genius to understand its simplicity. -Dennis Ritchie
Of course, I’m not a genius, and I’m not even sure if Dennis Ritchie meant to include the userspace tools. Furthermore, a shell is only one small part (and compared to the kernel, it’sreallyan easy part) of a fully functional operating system, but I hope at the end of this post, you are just as astonished as I was, how simple it is to write a shell once you understood the concepts behind it.
What is a shell?
Definitions are always difficult. I would define a shell as the basic user interface to your operating system. You can input commands to the shell and receive the corresponding output. When you need more information or a better definition, you can look up the Wikipediaarticle.
Some examples of shells are:
The graphical user interfaces of Gnome and Windows are shells but most IT related people (or at least I) will refer to a text-based one when talking about shells, e.g. the first two in this list. Of course, this example will describe a simple and non-graphical shell.
In fact, the functionality is explained as: give an input command and receive the output of this command. An example? Run the programto list the content of a directory.
That’s it, super simple. Let’s start!
The input loop
To execute a command, we have to accept inputs. These inputs are done by us, humans, using keyboards ;-)
The keyboard is our standard input device () and we can create a reader to access it. Each time, we press the enter key, a new line is created. The new line is denoted by. While pressing the enter key, everything written is stored in the variable.
Let’s put this in afunction of ourfile, and by adding aloop around thefunction, we can input commands continuously. When an error occurs while reading the input, we will print it to the standard error device (). If we would just usewithout specifying the output device, the error would be directed to the standard output device (). This would not change the functionality of the shell itself, but separate devices allow easy filtering of the output for further processing.
Now, we want to execute the entered command. Let’s create a new functionfor this, which takes a string as an argument. First, we have to remove the newline control characterat the end of the input.
Next, we prepare the command withand assign the corresponding output and error device for this command. Finally, the prepared command is processed with.
We complete ourfunction by adding a fancy input indicator () at the top of the loop, and by adding the newfunction at the bottom of the loop.
It’s time for a first test run. Build and run the shell with. You should see the input indicatorand be able to write something. For example, we could run thecommand.
Wow, it works! The programwas executed and gave us the content of the current directory. You can exit the shell just like most other programs with the key combination.
Let’s get the list in long format with.
It’s not working anymore. This is because our shell tries to run the program, which is not found. The program is justandis a so-called argument, which is parsed by the program itself. Currently, we don’t distinguish between the command and the arguments. To fix this, we have to modify thefunction and split the input on each space.
The program name is now stored inand the arguments in the subsequent indexes. Runningnow works as expected.
Change Directory (cd)
Now we are able to run commands with an arbitrary number of arguments. To have a set of functionality which is necessary for a minimal usability, there is only one thing left (at least according to my opinion). You might already come across this while playing with the shell: you can’t change the directory with thecommand.
No, this is definitely not the content of my root directory. Why does thecommand not work? When you know, it’s easy: there is norealprogram, the functionality is a built-in command of the shell.
Again, we have to modify thefunction. Just after thefunction, we add astatement on the first argument (the command to execute) which is stored in. When the command is, we check if there are subsequent arguments, otherwise, we can not change to a not given directory (in most other shells, you would then change to your home directory). When there is a subsequent argument in(which stores the path), we change the directory with. At the end of case block, we return thefunction to stop further processing of this built-in command.
Because it is so simple, we will just add a built-incommand right below theblock, which stops our shell (an alternative to using).
Yes, the following output looks more like my root directory.
That’s it. We have written a simple shell :-)
When you are not already bored by this, you can try to improve your shell. Here is some inspiration:
Modify the input indicator:
add the working directory
add the machine’s hostname
add the current user
Browse your input history with the up/down keys
We reached the end of this post and I hope you enjoyed it. I think, when you understand the concepts behind it, it’s quite simple.
Go is also one of the more simple programming languages, which helped us to get to the results faster. We didn’t have to do any low-level stuff as managing the memory ourselves. Rob Pike and Ken Thompson, who created Go together with Robert Griesemer, also worked on the creation of UNIX, so I think writing a shell in Go is a nice combination.
As I’m always learning too, please just contact me whenever you find something which should be improved.
Based on the redditcomments, I’m now using the correct output devices.
Below is the full source-code. You can also check therepository, but the code there might already have diverged from the code presented in this post.