Skip to content

Latest commit

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

cshell

Minimal Unix shell with pipes and redirection, in C.

A small, self-contained demo written in pure C — no external libraries, just the standard library and POSIX. Part of the Corg-Labs collection of single-file C programs.


How It Works

  1. A command line is split on the pipe character into stages
  2. fork() + execvp() run each stage as a child process
  3. pipe() and dup2() wire one stage's output to the next's input
  4. < and > redirect a stage's input/output to files

Tutorial

This tutorial walks through the source code of cshell.c step by step, explaining each concept and pipeline stage so you can understand — and extend — the shell yourself.

1. Constants and the Trim Helper

The single compile-time constant caps the number of arguments per command:

#defineMAXARGS 64

Before any parsing, the trim helper strips leading spaces and trailing newlines/spaces from a string in place, returning a pointer to the cleaned start:

staticchar*trim(char*s){
while(*s==' ')s++;
char*e=s+strlen(s);
while(e>s&&(e[-1]=='\n'||e[-1]==' '))*--e=0;
returns;
}

Every raw line read from the user passes through trim before the shell tries to interpret it, ensuring blank lines and extra whitespace are handled gracefully.

2. The Read-Eval Loop

main runs a simple while(1) REPL (Read-Eval-Print Loop). It reads one line at a time with fgets, trims it, then dispatches to built-ins or the pipeline runner:

charline[1024];
while(1){
printf("csh$ "); fflush(stdout);
if(!fgets(line,sizeof(line),stdin)) break;
char*cmd=trim(line);
if(!*cmd) continue;
if(!strcmp(cmd,"exit")) break;
if(!strncmp(cmd,"cd",2)){ char*d=trim(cmd+2); if(chdir(*d?d:getenv("HOME"))) perror("cd"); continue; }
...
}

The loop exits on EOF (fgets returns NULL) or when the user types exit.

3. Built-in Commands

Two commands are handled directly in the shell process without forking — they must be built-ins because they affect the shell's own state:

  • exit — breaks the REPL loop and terminates the shell.
  • cd — calls chdir() to change the shell's working directory; falls back to $HOME when no argument is given:
if(!strncmp(cmd,"cd",2)){
char*d=trim(cmd+2);
if(chdir(*d?d:getenv("HOME"))) perror("cd");
continue;
}

Any other command falls through to the pipe-splitting stage.

4. Splitting the Command Line into Pipeline Stages

The trimmed command line is tokenised on | to produce an ordered array of segment strings, each representing one stage of the pipeline:

char*segs[16]; intns=0;
char*s=strtok(cmd,"|");
while(s&&ns<16){ segs[ns++]=s; s=strtok(NULL,"|"); }

Up to 16 stages are supported. For a command like ls | grep .c | wc -l this produces segs = {"ls ", " grep .c ", " wc -l"} with ns = 3.

5. Forking and Wiring the Pipe Chain

For each stage the shell optionally creates a new pipe(), forks a child, then wires file descriptors with dup2 so the previous stage's read end becomes the child's stdin, and the current pipe's write end becomes its stdout:

intprevfd=-1;
for(inti=0;i<ns;i++){
intpipefd[2]={-1,-1};
if(i<ns-1) pipe(pipefd);
pid_tpid=fork();
if(pid==0){
if(prevfd!=-1){ dup2(prevfd,0); close(prevfd); }
if(i<ns-1){ close(pipefd[0]); dup2(pipefd[1],1); close(pipefd[1]); }
charseg[512]; strncpy(seg,segs[i],sizeof(seg)-1); seg[sizeof(seg)-1]=0;
run_segment(trim(seg));
}
if(prevfd!=-1) close(prevfd);
if(i<ns-1){ close(pipefd[1]); prevfd=pipefd[0]; }
}
while(wait(NULL)>0);

After all children are started, the parent calls wait in a loop to reap every child before printing the next prompt, preventing zombie processes.

6. Parsing a Single Segment — Arguments and Redirection

run_segment handles one pipeline stage. It tokenises the segment on spaces, scanning for < and > redirect tokens, and collects the rest as execvp arguments:

staticvoidrun_segment(char*seg){
char*args[MAXARGS]; intn=0;
char*in=NULL,*out=NULL;
char*tok=strtok(seg," ");
while(tok&&n<MAXARGS-1){
if(!strcmp(tok,"<")){ tok=strtok(NULL," "); in=tok; }
elseif(!strcmp(tok,">")){ tok=strtok(NULL," "); out=tok; }
elseargs[n++]=tok;
tok=strtok(NULL," ");
}
args[n]=NULL;
...
}

When a < token is found the next token becomes the input filename; > captures the output filename. All other tokens are pushed onto the args array.

7. Applying Redirection and Executing

With the argument list and optional filenames ready, run_segment opens any redirect files and wires them to stdin/stdout with dup2 before calling execvp:

if(in){ intfd=open(in,O_RDONLY); if(fd<0){perror(in);exit(1);} dup2(fd,0); close(fd); }
if(out){ intfd=open(out,O_WRONLY|O_CREAT|O_TRUNC,0644); if(fd<0){perror(out);exit(1);} dup2(fd,1); close(fd); }
execvp(args[0],args);
fprintf(stderr,"cshell: %s: command not found\n",args[0]);
exit(127);

execvp replaces the child process image with the requested command. If execvp returns at all the command was not found; the shell prints an error and exits with status 127 — the conventional "command not found" exit code.


Build

gcc cshell.c -o cshell

Run

./cshell

Controls

Built-ins: cd, exit. Try ls | grep .c | wc -l or cat < in > out.

About

► Minimal Unix shell with pipes and redirection, in C.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages