Custom Command shell
Building an commond shell using operating systems concepts
Before we talk about how a shell works internally, we need to know some operating system concepts
Process
A process is a program that is currently running.
A process has its own memory , process ID (PID) .When you run ls, the operating system creates a new process for it. When it finishes, that process is destroyed.
I/O Streams
Every process starts with three default input/output streams:
By default:
stdin→ (where the program reads input from) keyboardstdout→(where the program writes normal output) terminalstderr→ (where the program writes error messages) terminal
String Handling (in C)
In C, input from the terminal arrives as plain text.
Functions commonly used to parse those text:
strlen()– length of a stringstrcmp()– compare stringsstrsep()/strtok()– split stringsstrstr()– find substrings
System Calls
A system call is how a user program asks the operating system to do something privileged.
The most important system calls are:
fork()– create a new processexec()– replace the current processwait()– synchronize with a child process
File Descriptors
File descriptor is simply an index into the file descriptor table. For each process in our operating system, there is a process control block(PCB).
0 → Standard Input (stdin)
1 → Standard Output (stdout)
2 → Standard Error (stderr)
Idea behind using system calls
Before writing any shell features, I had one basic question:
When we type
lsin a terminal, why don’t the terminal close after executing it?
In Unix-like systems, every running program is a process.
A process has its own memory, execution state, and open files.
Get Hruthikreddy’s stories in your inbox
Join Medium for free to get updates from this writer.
If a process wants to run another program, it should call exec()
execvp(args[0], args);But exec() does not start a new process. It replaces the current one. To survive, the shell must separate itself from the commands it runs.
This is where fork() enters the picture.
pid_t pid = fork();fork() creates a new process by duplicating the current one.
- The parent process continues running the shell
- The child process is free to do something else
Right after fork(), both processes are identical.
if (pid == 0) {
execvp(args[0], args);
}The child turns into the requested command . In the parent nothing changes , shell remains intact.
waitpid(pid, &status, 0);If the shell immediately prints a new prompt, the output becomes messy.
The shell must wait until the command finishes. So wait is used to synchronizing two processes.
Putting everything together, the shell becomes an infinite loop:
while (1) {
read input
fork
exec in child
wait in parent
}Parsing of arguments
The shell does not receive structured commands.
ls ## pwd ## whoami ← this is how we receive commands .
Before anything can run, the shell must:
- Break the string into commands
- Break each command into arguments
This is why we use string functions :
strsep()strcmp()strlen()
I/O Streams
When a command prints something, it writes to stdout . By default, that goes to the terminal.
ls > output.txtThe shell changes where file descriptor 1 points.
Connecting Processes with Pipes
Instead of sending output to a file or terminal, the shell connects:
- One process’s
stdout - To another process’s
stdin
pipe(pipes);
dup2(pipes[1], STDOUT_FILENO);
dup2(pipes[0], STDIN_FILENO);Only file descriptors are rearranged.
Preventing Zombie Processes
A shell creates many child processes. If it does not clean them up, they remain as zombies. By handling SIGCHLD, the shell reaps finished children automatically.
signal(SIGCHLD, signal_handler);





