Building my own Shell in C

Programmierung einer Shell in C

On Linux systems, bash is the most widely used shell and provides a wide range of commonly used features such as piping, output redirection or simply executing programs. To further improve my knowledge about the Linux operating system as well as the C programming language, I looked for a suitable project that would cover both. The website Codecrafters.io offers coding challenges that are not just simple copy-and-paste tutorials. That's why I choose their shell coding challenge for this project.

Auf Linux-Systemen ist Bash die am weitesten verbreitete Shell und bietet eine Vielzahl häufig genutzer Funktionen, wie z. B. Pipes, Ausgabeumleitung sowie die Ausführung externer Programme. Um meine Kentnisse in Linux und C weiter zu vertiefen, suchte ich nach einem geeigneten Projekt, das beide Bereiche abdeckt. Die Platform Codecrafters.io stellt praxisnahe Programmieraufgaben bereit, die keine einfachen Copy-and-Paste-Tutorials sind. Deshalb entschied ich mich für deren Shell-Challenge als Grundlage für dieses Projekt.


I begin the project by adding basic features such as handling invalid commands, locating executables via the PATH variable, and adding builtins. Builtins are commands that are implemented directly within the shell, such as exit, echo, type, pwd and cd. To run external executables, I use fork to create a child process in combination with execvp, which replaces the child process with the program to be executed. To navigate between different directories, I use the chdir function, which invokes a system call to change the current working directory. I'm also adding support for relative paths and the '~' symbol to switch to the user's home directory. To retrieve the home directory as well as the PATH, I use getenv to access the environment variables.

Zu Beginn implementiere ich grundlegende Funktionen wie die Behandlung ungültiger Befehle, das Auffinden von ausführbaren Dateien mithilfe der PATH-Variable, sowie die Intergration von Builtin-Kommandos. Builtins sind direkt in der Shell implementiert Befehle, wie z.B. exit, echo, type, pwd und cd. Um externe Programme auszuführen, verwende ich fork zur Erzeugung eines Childprozess und execvp, das den Childprozess durch das auszuführende Programm ersetzt. Für die Navigation zwischen verschiedenen Verzeichnissen nutze ich die Funktion chdir, die einen Systemcall zum Ändern des aktuellen Arbeitsverzeichnisses ausführt. Darüber hinaus implementiere ich die Unterstützung relativer Pfade sowie des Symbols `~`, um in das Home-Verzeichnis des Benutzers zu wechseln. Das Home-Verzeichnis und die PATH-Variable ermittle ich mithilfe von getenv, indem ich auf die entsprechenden Umgebungsvariablen zugreife.

Building my own Shell in C

Programmierung einer Shell in C

The correct handling of quotes is an important feature of a shell. For this, I implemented support for single and double quotes, as well as an escape character. The backslash "\" is used to tell the shell, that the following character should not be interpreted with its special meaning. With this, it is possible to use a command such as echo \'hello\' and receive the output 'hello' instead of just hello without the quotes.

Der korrekte Umgang mit Anführungszeichen ist eine zentrale Funktion einer Shell. Daher habe ich die Unterstützung für einfache und doppelte Anführungszeichen sowie für ein Escape-Zeichen implementiert. Der Backslash "\" signalisiert der Shell, dass das unmittelbar folgende Zeichen nicht mit seiner speziellen Bedeutung interpretiert werden soll. Dadurch ist es beispielsweise möglich, den Befehl echo 'hello' zu verwenden und als Ausgabe 'hello' zu erhalten, anstatt hello ohne Anführungszeichen.


Standard output (stdout) is the standard data stream that programs use to send data to the terminal or the user. In this context, there is also standard input (stdin) for input and standard error (stderr) for errors messages. To support operators such as ">" or ">>" for redirection, I use dup2 to replace the stdout file descriptor with a file descriptor that refers to a file on disk. For the connection between a file and a file descriptor, I use the open function and set appropriate flags to enable either truncation or appending.

Die Standardausgabe (stdout) ist der Datenstrom, über den Programme ihre Ausgaben an das Terminal oder den Benutzer senden. Ergänzend dazu existieren die Standardeingabe (stdin) für Eingaben sowie die Standardfehlerausgabe (stderr) für Fehlermeldungen. Zur Unterstützung von Umleitungsoperatoren wie ">" oder ">>" verwende ich dup2, um den stdout-Dateideskriptor durch einen Dateideskriptor zu ersetzen, der auf eine Datei im Dateisystem verweist. Die Verknüpfung zwischen Datei und Dateideskriptor erfolgt über die Funktion open, wobei ich die entsprechenden Flags setze, um entweder das Überschreiben oder Anhängen an die Datei zu realisieren.


The next part is to support autocompletion for builtins and executables when pressing the TAB key. The inital approach used fgets to read user input into a buffer. To avoid implementing autocompletion from scratch, I replaced this with the readline library. To obtain a list of executables from the PATH variable, I implemented dynamic memory allocation using malloc and realloc. Finally, I created a custom name generator function for readline to provide the detected executables and builtins for completion.

Im nächsten Schritt implementiere ich die Autovervollständigung für Builtins und ausführbare Programme beim Drücken der TAB-Taste. Ursprünglich wurde fgets verwendet, um die Benutzereingaben in einen Buffer einzulesen. Um die Autovervollständigung nicht vollständig selbst entwickeln zu müssen, verwende ich die readline Library. Zur Ermittlung der ausführbaren Programme aus der PATH-Variable setze ich dynamische Speicherverwaltung mittels malloc und realloc ein. Abschließend implementiere ich eine benutzerdefinierte Generatorfunktion für readline, die die erkannten Builtins und ausführbaren Programme für die Vervollständigung bereitstellt.


Pipelines are used to connect the standard output of one command to the standard input of the next command. To create a pipe the "|" operator is used, so I first check whether it is present in the input. Next, I create a pipe file descriptor and call the pipe function to setup the reader and writer ends. This is done by splitting the command string at "|" into seperate parts to obtain the individual commands. Afterward, I run the already implemented command-handling logic for each command and apply the appropiate pipe redirection.

Pipelines verbinden die Standardausgabe eines Befehls mit der Standardeingabe des darauffolgenden Befehls. Um eine Pipe zu erstellen, wird der Operator "|" verwendet. Daher prüfe ich zunächst, ob dieser in der Eingabe vorhanden ist. Anschließend erstelle ich ein Pipe-Dateideskriptorpaar und rufe die Funktion pipe auf, um das Lesende und Schreibende Ende der Pipe einzurichten. Dazu wird der Command am Operator "|" in einzelne Teilbefehle zerlegt. Für jeden dieser Befehle führe ich anschließend die bereits implementierte Befehlsverarbeitungslogik aus und richte die Pipe-Umleitungen ein.


Another useful shell feature is command history, which is also required for features such as reverse-search. The history command itself is implemented as a shell builtin that stores previously executed commands in a buffer. The GNU History Library provides functions such as add_history for list management, which I use in my shell. Because i use the readline library, navigation with the up and down arrow keys works without additional implementation. Finally, I implement the "-r" "-w" "-a" flags to read, write, and append the stored history buffer to a file. With this final step, the shell is fully functional and all tests pass, as demonstrated in the demo video.

Eine weitere zentrale Funktion einer Shell ist die Befehls-Historie, die unter anderem die Reverse Search ermöglicht. Der history-Befehl wird als Builtin implementiert und speichert zuvor ausgeführte Befehle in einem internen Buffer. Die GNU History Library stellt Funktionen wie add_history zur Verwaltung der Historienliste bereit, die ich für meine Shell nutze. Durch die Verwendung der readline-Bibliothek funktioniert zudem die Navigation durch die Befehlshistorie mittels der Pfeiltasten nach oben und unten ohne zusätzlichen Aufwand. Abschließend implementiere ich die Optionen "-r", "-w" und "-a" um den gespeicherten Verlauf aus einer Datei zu lesen, in eine Datei zu schreiben oder anzuhängen. Mit diesem letzten Schritt ist die Shell vollständig funktionsfähig und besteht alle Tests, wie im Demo-Video gezeigt wird.