How I Built My Own Terminal in C

How I Built My Own Terminal in C

Suleyman Sade

Purpose

I recently got interested in low-level programming and microcontrollers. For the purpose of exploring the area and gaining some experience, I started building my own bash-like terminal. The language of my choosing — unsurprisingly — was C++, as it is commonly used in low-level systems.

I made specific design choices and trade-offs to overcome challenges, which I am going to share in the next few devlogs.

Why do I ditch strings for raw char pointers?

My first task was seemingly simple: separate the input into an array.

Normally, I would have just used a built-in function to split the input. However, since I wanted this to be a challenge that would push me to think at a low level, I decided to opt out of built-in methods. In fact, throughout the entire process, I actively dodged the C++ libraries designed to make a dev’s life easier.

Going back to the question, I first need to explain the difference between strings and char pointers in C++. If you have any experience with programming languages, you already know the string, or text, type. A string is actually just a list of characters, and that is how it is stored under the hood.

The std::string type is just a wrapper around this char list. In fact, C doesn’t even have a native "string" type. Instead, to handle text, you need to define a char array. This is how software works fundamentally, and I went a step further with this approach by using char pointers.

Char pointers just store the memory address of this char list, which is manually allocated by the developer. But that brings up a problem: whenever the char pointer goes out of scope or is destroyed, it only frees the memory space allocated to store the address of the array. The array itself still exists in memory, which causes a memory leak. Because of this, I have to manually call free() (or delete[]) to empty that allocated memory.

I just thought managing this myself would be a great way to better understand how computers work, even though the standard string type normally handles this automatically.

What about scalability?

Soon, I noticed a major issue. Creating these char pointers in various locations throughout the code felt way too disorganized. I also needed to keep track of the maximum size I allocated for each char pointer to hold, since they don’t have a size() function. If that allocated space is exceeded, it can create troublesome, hard-to-debug errors. You are basically writing into an unknown space in memory, which is like playing with fire.

My solution was to create a carr (char array) struct, and equip it with functions to expand, delete, and initialize itself.

I also overloaded some operators, such as =, to directly assign these char pointers. Overloading is simply changing the behavior of an operator for the interactions of that specific struct or class.

A small touch

It is true that I wanted to create a kind of bash clone, but I still didn’t want a one-to-one copy. So, I decided to create my own command names.

That is why when I coded the features to get the current directory and move between directories, I chose “here” and “go” rather than “pwd” and “cd.” It was just a little flavor I wanted to add.

Of course, when I was coding those commands, I still avoided using standard modern libraries like <filesystem> and went for <dirent.h> and <unistd.h>. They were a bit harder to use, but they gave me the exact low-level systems exposure I was looking for.

The switch

So far, I have written about how I tried to avoid C++ features and chose C-style alternatives instead. Well, that begs the question: why not just switch to C entirely?

My initial reasoning for using C++ was simply because I didn’t know C. However, this project actually created a huge opportunity for me to finally learn C, and I took it.

I thought to myself, “How hard could it be since I already coded it in a C-like style?” Well, it turned out to be quite a challenge.

First of all, remember how I added functions to my carr struct and overloaded its operators? Well, that is not possible in C. Because C doesn't support member functions or operator overloading, I had to recode every single place where I manipulated strings—which was a lot.

In the end, though, I gained complete control over my variables and how I manage them. And honestly, that was the whole point.

I did not stop there; I will go on until I implement all the fundamental functionalities of bash. And you can watch the development as it happens on my blog.

If you would like to connect with me, you can check out my socials:

GitHub - SuleymanSade/MyTerminal

Comments

Loading comments…