Sitemap
Hruthikreddy

Hruthikreddy

Curious software engineer

Custom Command shell

3 min readFeb 10, 2026

--

Building an commond shell using operating systems concepts

Press enter or click to view image in full size

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) keyboard
  • stdout →(where the program writes normal output) terminal
  • stderr → (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 string
  • strcmp() – compare strings
  • strsep() / strtok() – split strings
  • strstr() – 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 process
  • exec() – replace the current process
  • wait() – 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 ls in 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.txt

The 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);

--

--