How To Write A Tiny Shell In C

I was wondering how complex some shells are. That got me thinking what a very minimal -but useable- shell would look like. Could I write one in less than 100 lines of code? Let’s see!

A shell needs to execute commands. This can be done by overloading the current program with a new program and executing it (a lot more happens here, actually, but that is for another time). This can be done with the exec() family of functions. That seems to be a nice starting point. In a file named myshell.c I wrote:

#include <unistd.h>

int main(void) {
    execvp("date", (char *[]){"date", NULL});
}

Then I compiled the code:

$ gcc -Wall -pedantic -static myshell.c -o mysh

And executed it:

$ ./mysh
Mon Jun 18 18:51:38 UTC 2018

Cool! I hardcoded date here. But actually, a shell should prompt for the command to execute. So I went on and made the shell ask for a command to run.

#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define PRMTSIZ 255

int main(void) {
    char input[PRMTSIZ + 1] = { 0x0 };
    fgets(input, PRMTSIZ, stdin);
    input[strlen(input) - 1] = '\0'; // remove trailing \n

    execvp(input, (char *[]){input, NULL});
}

And ran it:

./mysh
date
Mon Jun 18 19:27:21 UTC 2018

OK, nice. But that fails if I want to run a command with parameters:

./mysh
ls /

Nothing happens. I need to split the input into an array of char pointers. Each pointer shall point to a string containing a parameter:

#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define PRMTSIZ 255
#define MAXARGS 63

int main(void) {
    char input[PRMTSIZ + 1] = { 0x0 };
    char *ptr = input;
    char *args[MAXARGS + 1] = { NULL };

    // prompt
    fgets(input, PRMTSIZ, stdin);

    // convert input line to list of arguments
    for (int i = 0; i < sizeof(args) && *ptr; ptr++) {
        if (*ptr == ' ') continue;
        if (*ptr == '\n') break;
        for (args[i++] = ptr; *ptr && *ptr != ' ' && *ptr != '\n'; ptr++);
        *ptr = '\0';
    }

    execvp(args[0], args);
}

And running it yields:

./mysh
ls /
bin  boot  dev	etc  home  lib	lib64  ✂️

It worked! I am getting closer. Wouldn’t it be great if the shell would not exit after one command, but ask for further commands every time the current command terminated? I think it is time for my old friend fork() to enter the scene!

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

#define PRMTSIZ 255
#define MAXARGS 63

int main(void) {
    for (;;) {
        char input[PRMTSIZ + 1] = { 0x0 };
        char *ptr = input;
        char *args[MAXARGS + 1] = { NULL };

        // prompt
        fgets(input, PRMTSIZ, stdin);

        // convert input line to list of arguments
        for (int i = 0; i < sizeof(args) && *ptr; ptr++) {
            if (*ptr == ' ') continue;
            if (*ptr == '\n') break;
            for (args[i++] = ptr; *ptr && *ptr != ' ' && *ptr != '\n'; ptr++);
            *ptr = '\0';
        }

        if (fork() == 0) exit(execvp(args[0], args));
        wait(NULL);
    }
}

I now fork a child every time I want to execute a command. The child exits and uses the return value of execvp() as exit code. This can help the parent process to detect errors if they are any during program overload. The parent process waits for the child to finish. Everything happens in an infinite loop for(;;) to allow more than just one command.

./mysh
date
Mon Jun 18 19:44:27 UTC 2018
ls /
bin  boot  dev	etc  home  lib	lib64  ✂️

Despite being very limited in functionality, I think this now counts as a shell.

I couldn’t stop myself from adding a few more things:

  • Disable signal SIGINT in the parent: This means I can interrupt (ctrl-c) a child process without killing my shell. Very useful 😅
  • Add a visual prompt: $ for users and # for superusers.
  • Print the exit code of the child, e.g. <1> or <0>
  • Check for empty input: Because segfaulting is not nice. 🙈

Here is the final code:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

#define PRMTSIZ 255
#define MAXARGS 63
#define EXITCMD "exit"

int main(void) {
    for (;;) {
        char input[PRMTSIZ + 1] = { 0x0 };
        char *ptr = input;
        char *args[MAXARGS + 1] = { NULL };
        int wstatus;

        // prompt
        printf("%s ", getuid() == 0 ? "#" : "$");
        fgets(input, PRMTSIZ, stdin);

        // ignore empty input
        if (*ptr == '\n') continue;

        // convert input line to list of arguments
        for (int i = 0; i < sizeof(args) && *ptr; ptr++) {
            if (*ptr == ' ') continue;
            if (*ptr == '\n') break;
            for (args[i++] = ptr; *ptr && *ptr != ' ' && *ptr != '\n'; ptr++);
            *ptr = '\0';
        }

        // built-in: exit
        if (strcmp(EXITCMD, args[0]) == 0) return 0;

        // fork child and execute program
        signal(SIGINT, SIG_DFL);
        if (fork() == 0) exit(execvp(args[0], args));
        signal(SIGINT, SIG_IGN);

        // wait for program to finish and print exit status
        wait(&wstatus);
        if (WIFEXITED(wstatus)) printf("<%d>", WEXITSTATUS(wstatus));
    }
}

Running as root in a container:

./mysh
# ls /
bin  boot  dev	etc  home  lib	lib64  ✂️
<0># date
Mon Jun 18 19:50:09 UTC 2018
<0># nonexistent-command
<255># false
<1># true
<0># exit

In the end, I was able to write a tiny shell with limited capabilities in less than 50 lines of C code. That is less than half of what I aimed for.