How to Build a Simple Unix Shell in C: Step‑by‑Step Guide
This tutorial walks you through creating a basic Unix shell in C, covering prompt output, command input, parsing with strtok, handling built‑in commands like cd and echo, forking child processes, executing commands via execvp, and assembling the complete source code.
Overview
This article demonstrates how to build a minimal Unix‑style command‑line interpreter (shell) in C. The shell repeatedly reads a line of input, parses it into an argument vector, handles a few built‑in commands, forks a child process, and uses execvp to run external programs while the parent waits for termination.
Main Loop
The shell runs an infinite loop that performs the following steps for each command line:
Display a prompt of the form username@hostname current_path#.
Read the raw line from stdin with fgets.
Strip the trailing newline and split the line into tokens using strtok, storing pointers in myargv.
Check for built‑in commands ( cd and echo) and execute them directly in the parent process.
If the command is not built‑in, fork() a child, call execvp() with the argument vector, and waitpid() for the child to finish.
Prompt Generation
The prompt is printed without a newline so that the cursor stays on the same line. fflush(stdout) is called immediately after printf to avoid buffering delays.
printf("username@hostname current_path#");
fflush(stdout);Reading and Cleaning Input
A static buffer of 1024 bytes is used. After fgets the trailing \n is replaced with a null terminator.
#define NUM 1024
char lineCommand[NUM];
char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
assert(s != NULL);
lineCommand[strlen(lineCommand)-1] = '\0';Tokenisation with strtok
The first token is obtained with strtok(lineCommand, " "). Subsequent tokens are retrieved by repeatedly calling strtok(NULL, " ") until it returns NULL. The resulting pointers are stored in myargv, which is terminated by a NULL entry as required by execvp.
Built‑in Commands
cd : Changing the working directory must affect the parent process, so the shell implements cd as a built‑in. It calls chdir() with the supplied path and then continues the loop without forking.
echo : The shell treats echo as a built‑in. When the argument is $? it prints the exit status and terminating signal of the previous command (stored in lastcode and lastsig).
Process Replacement
For external commands the shell forks a child process. In the child, execvp(myargv[0], myargv) replaces the process image with the requested program. If execvp fails, the child exits with status 1. The parent calls waitpid() to obtain the child's termination status and extracts the exit code and signal number.
pid_t id = fork();
assert(id != -1);
if (id == 0) {
execvp(myargv[0], myargv);
exit(1);
}
int status;
pid_t ret = waitpid(id, &status, 0);
assert(ret > 0);
lastcode = (status >> 8) & 0xFF;
lastsig = status & 0x7F;Complete Source Code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#define NUM 1024
#define OPT_NUM 64
char lineCommand[NUM];
char *myargv[OPT_NUM];
int lastcode = 0;
int lastsig = 0;
int main(void) {
while (1) {
/* 1. Prompt */
printf("username@hostname current_path#");
fflush(stdout);
/* 2. Read input */
char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
assert(s != NULL);
lineCommand[strlen(lineCommand)-1] = '\0';
/* 3. Tokenise */
myargv[0] = strtok(lineCommand, " ");
int i = 1;
if (myargv[0] && strcmp(myargv[0], "ls") == 0) {
myargv[i++] = "--color=auto";
}
while ((myargv[i++] = strtok(NULL, " ")) != NULL) {
/* loop fills myargv, leaving a NULL terminator */
}
/* 4. Built‑in cd */
if (myargv[0] && strcmp(myargv[0], "cd") == 0) {
if (myargv[1]) {
chdir(myargv[1]);
}
continue;
}
/* 5. Built‑in echo */
if (myargv[0] && myargv[1] && strcmp(myargv[0], "echo") == 0) {
if (strcmp(myargv[1], "$?") == 0) {
printf("%d,%d
", lastcode, lastsig);
} else {
printf("%s
", myargv[1]);
}
continue;
}
/* 6. Execute external command */
pid_t id = fork();
assert(id != -1);
if (id == 0) {
execvp(myargv[0], myargv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
assert(ret > 0);
lastcode = (status >> 8) & 0xFF;
lastsig = status & 0x7F;
}
return 0;
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
