Building a Shell in C
Yqsh is a side project of mine that I started because I wanted to learn how a Linux shell works and brush up a bit on my C skills. This page is a compilation of learnings and challenges I come across while building it. The source code is available online.
The Basics
At the simplest level, a shell presents you with a prompt, waits for you to type a program to run, then runs the program and presents you with the output. There is so much more to it than that, but this gives me three things to implement as a starting point:
- Reading input
- Running the given program
- Initialization
Given this, we can write out a header file to define the functions we need, as well as some constants.
// yqsh.h
#include <stdlib.h>
#ifndef YQSH_H
#define YQSH_H
#define NULL_CHECK(ptr) if (ptr == NULL) { fprintf(stderr, "OOM"); exit(1); }
#define YQSH_BUFSIZE 1024
#define YQSH_MAX_ARGS 256
#define YQSH_ARG_LEN 256
#define YQSH_VERSION "0.0.1"
void yqsh_read_line(char* linebuf);
void yqsh_parse_args(char* line, char** args, size_t* nargs);
int yqsh_loop(int, const char**);
#endif
Reading Input
Reading input is pretty straightforward with fgets
, which will read a line from stdin and store it in the buffer passed to it.
// yqsh.c
void yqsh_read_line(char* linebuf)
{
char* str = fgets(linebuf, YQSH_BUFSIZE, stdin);
if (feof(stdin))
{
return;
}
if (str == NULL)
{
fprintf(stderr, "Fgets error");
linebuf[0] = '\0';
}
}
linebuf
will be a character buffer created and passed to this function later, fgets
will read YQSH_BUFSIZE - 1
characters into it (to leave space for the null-terminator) and return the string. If fgets
reads in EOF (end of file) and nothing else, it will return NULL
. If it encounters and error, it will also return NULL
. Therefore, the function must check for both cases so that an error message is not printed when there is no more user input to be read. In the case of an error, I place a null-terminator at the beginning of linebuf
, to make it a valid string for future operations.
Parsing Input and Running the Program
Now that the user input is read, the shell can parse it and turn it into a command to run. This was actually more challenging than I was expecting, mostly because my C is rusty and string handling is way more painful than modern languages because of the null-terminator.
Initialization
Initialization is actually the thing I will tackle last, since to get a running demo, I just need to immediately start parsing input. For now I’ll print out a version number before going into that. Later, I’ll have the shell read a configuration file before waiting for input.
// main.c
#include <stdio.h>
#include "yqsh.h"
int main(int argc, char const *argv[])
{
// Initialization
// TODO: read config file
printf("yqsh version %s\n", YQSH_VERSION);
int rc = yqsh_loop(argc, argv);
return rc;
}