I started making my own shell in C, and I quickly realized that I need a way to handle text input from stdin. Now, C has some functions for this, the one I'm most familiar with is fgets()
. This function works by taking in a buffer, buffer length, and input source. To understand how this works, we need to know how strings work in C.
C doesn't really have a string data type, strings are arrays of characters in a block of memory that's been allocated for that purpose. I've prepared a visualization to depict how this works.
First, let's look at how a string gets allocated in C:
size_t str_length = 10;
char *str = malloc((str_length + 1) * sizeof(char));
This allocates an array that can fit a string that's 10 characters long, which at first may seem counterintuitive since I allocated space for 11 characters. However, strings are terminated using a null terminator, or a \0
which in memory is just a 0
. Due to this, when you allocate a string you need to allocate (size of your string) + 1
. When you run malloc, it allocates some memory at an address where the memory is not currently allocated, or has been deallocated previously.
I changed the color of the null terminator to show that it's a special character used for terminating the string. The pointer str
points to the first character in the array, in a place in memory marked with an address. If you run printf()
on the pointer you could see it's address. Since the address is typically random and changes each time you run the program, it's not important to know but if you're learning how pointers work it could be a good way to gain understanding of how memory works and what happens under the hood.
Now, stdin
is an abstraction of your OS's standard input handling, which your program typically reads its input from. For all intents and purposes, let's think of this as the stream of input coming from the keyboard. I have written a simple example of using fgets()
to receive input from stdin
(the keyboard) with a maximum of 10 characters and echo it back to us.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
size_t str_length = 10;
char *str = malloc((str_length + 1) * sizeof(char));
printf("Enter some text: ");
fgets(str, str_length, stdin);
printf("%s\n", str);
return 0;
}
This works if your goal is to get input from the user one time, and store it in a buffer of a fixed size. If you're writing an interactive program like a shell that processes commands from stdin
however, things get more complicated. Let's say that you want to have a dynamically allocated buffer size. You could write a function that takes a stream of input, allocates a buffer, if the input exceeds the buffer size the buffer gets reallocated with a new size, afterwards a string gets returned.
This would look something like this:
char *get_line(FILE *stream) {
// For performance reasons memory is being allocated in chunks
const size_t chunk = 4096; // Size of buffer
size_t max_len = chunk;
char *line = malloc(chunk + 1); // Allocate the size of the line buffer
if (!line)
return NULL;
size_t i = 0;
// Get the input from the user and store it in the line string
for (char ch = fgetc(stream); ch != EOF; ch = fgetc(stream)) {
// Allocate more space when the limit is reached
if (i == max_len) {
max_len += chunk;
char *const tmp = realloc(line, max_len + 1);
if (!tmp) {
free(line);
return NULL;
}
line = tmp;
}
// Add the character to the line
line[i] = ch;
i++;
if (ch == '\n')
break;
}
line[i] = '\0';
if (!line[0]) {
free(line);
return NULL;
}
return line;
}
For an interactive program like a shell, you could run this in a loop. Once the command gets processed, return to the loop to process the next.
char *line = NULL;
while (true) {
printf("> ");
line = get_line(stdin);
if (!line)
break;
printf("%s", line);
free(line);
}
This loop reads a line of input, and echos it out. It then loops back around to read and process the next line of input. While this works for a simple interactive shell-like program, there are some issues with this.
A full fledged shell like bash and zsh generally stores previously entered commands in a command history, which you can use the arrow keys to scroll back and forth between each entry. The terminal has two modes, raw
and cooked
mode. In raw
mode, the program handles the processing of input directly. Any input gets passed to the program as a raw stream of characters. By contrast, in cooked
mode the terminal handles the processing of input rather than the program. The input gets buffered, and passed to the program when the Enter
key is pressed. This means we don't have to handle everything from backspace to echoing characters on the terminal ourselves.
This means that handling input from the arrow keys in cooked
mode isn't possible, due to the input being buffered and processed only when the Enter
key is pressed. I looked into implementing input handling myself, and while it is doable it would take a lot of time to implement from scratch. Due to this, there are libraries for handling input for various purposes. I chose to use readline, since it is well suited for an interactive shell. However there are others, and I'd like to explore them in the future. One of these days I may look into writing my own, for the time being readline does the job.
This started out as something that seemed relatively simple to implement, reading a line of input from the user and processing that input accordingly. When looking into it, I learned that there is way more to it than I originally thought. I learned about the different terminal modes, and the basics of how the terminal handles input. Problems can seem simple at first glance, until you dig deeper and understand it's complexities.