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.