A fully functional Unix shell built from scratch in C, implementing process management, job control, pipelines, signal handling, and persistent command history.
- Built a POSIX-compliant Unix shell from scratch in C
- Full foreground & background job control (
fg,bg, Ctrl-C, Ctrl-Z) - Supports pipes, I/O redirection, background execution, and sequencing
- Uses process groups and terminal control (
setpgid,tcsetpgrp) - Signal-safe design using
sigaction - Persistent command history across sessions
- Compiled with strict warnings (
-Wall -Wextra -Werror)
This project was designed and implemented entirely by me as part of the
CS3 – Operating Systems & Networks (Monsoon 2025) course at IIIT Hyderabad.
This repository is a cleaned, documented, standalone public version of the work.
- GCC compiler
- Make
- Unix-like operating system (Linux, macOS, WSL)
make clean && makeOutput: The compiled binary is shell.out
./shell.outOr build and run in one command:
make runPress Ctrl-D (EOF). The shell will print "logout" and exit.
# Build the shell
make clean && make
# Run the shell
./shell.out
# You'll see a prompt like:
<username@hostname:~/path>
# Try a simple command
<user@host:~> echo "Hello, World!"
Hello, World!
# Exit with Ctrl-D
<user@host:~> [Ctrl-D]
logoutThe shell can execute any external command available in your system PATH:
<user@host:~> ls -la
<user@host:~> cat file.txt
<user@host:~> grep pattern file.txt
<user@host:~> sleep 5Error handling: If a command doesn't exist, the shell prints Command not found!
Syntax: hop [~ | . | .. | - | path]
Arguments:
~or no arguments: Change to home directory.: Stay in current directory (no-op)..: Move to parent directory-: Move to previous directory (if available)path: Change to specified relative or absolute path
Examples:
<user@host:~/project> hop ~
<user@host:~> hop ..
<user@host:/home> hop user/project
<user@host:~/project> hop -
<user@host:/home> hop user/project .. -
<user@host:~/project>Error: Prints No such directory! if directory doesn't exist.
Syntax: reveal [-a] [-l] [~ | . | .. | - | path]
Flags:
-a: Show all files including hidden ones (files starting with.)-l: Display one entry per line- Flags can be combined:
-la,-al,-aal, etc.
Arguments: Same as hop command (directory navigation)
Examples:
# List current directory
<user@host:~> reveal
file1.txt file2.txt directory1
# List with hidden files
<user@host:~> reveal -a
.git .gitignore file1.txt file2.txt directory1
# List one per line
<user@host:~> reveal -l
file1.txt
file2.txt
directory1
# List hidden files one per line
<user@host:~> reveal -la
.git
.gitignore
file1.txt
file2.txt
directory1
# List specific directory
<user@host:~> reveal ~/projects
project1 project2
# List previous directory
<user@host:~> reveal -Features:
- Files are sorted lexicographically (ASCII order)
- Default format: space-separated (like
ls) - Line format: one entry per line (when
-lflag is used)
Errors:
No such directory!- if directory doesn't existreveal: Invalid Syntax!- if too many path arguments provided
Syntax: log [purge | execute <index>]
Subcommands:
- No arguments: Print all stored commands (oldest to newest)
purge: Clear all historyexecute <index>: Execute command at index (1-indexed, newest to oldest)
Examples:
# View history
<user@host:~> log
reveal ~
hop ..
reveal
hop ~
# Execute most recent command (index 1)
<user@host:~> log execute 1
# Execute second most recent command (index 2)
<user@host:~> log execute 2
# Clear history
<user@host:~> log purge
<user@host:~> logFeatures:
- Stores up to 15 commands
- History persists across shell sessions (saved to
~/.cshell_history) - Doesn't store duplicate consecutive commands
- Doesn't store commands containing
logas atomic command name - Stores entire command as entered
Error: log: Invalid Syntax! for incorrect syntax.
Syntax: activities
Output Format: [pid] : command_name - State
Examples:
<user@host:~> sleep 100 &
[1] 12345
<user@host:~> cat file.txt &
[2] 12346
<user@host:~> activities
[12346] : cat - Running
[12345] : sleep - Running
# After stopping a job
<user@host:~> fg 1
sleep 100
^Z
[1] Stopped sleep
<user@host:~> activities
[12345] : sleep - Stopped
[12346] : cat - RunningFeatures:
- Output sorted lexicographically by command name
- Shows PID, command name, and state (Running/Stopped)
- Automatically removes terminated processes from list
Syntax: ping <pid> <signal_number>
Examples:
# Send SIGTERM (15) to process 12345
<user@host:~> ping 12345 15
Sent signal 15 to process with pid 12345
# Signal number is taken modulo 32
<user@host:~> ping 12345 47
Sent signal 47 to process with pid 12345
# (Actually sends signal 15, since 47 % 32 = 15)Features:
- Signal number is taken modulo 32 before sending
- Validates process existence before signaling
Errors:
No such process found- if process doesn't existInvalid syntax!- if signal number is not valid
Syntax: fg [job_number]
Examples:
# Start a background job
<user@host:~> sleep 100 &
[1] 12345
# Bring it to foreground
<user@host:~> fg 1
sleep 100
# Job is now running in foreground
# Bring most recent job to foreground (no argument)
<user@host:~> fg
sleep 100Features:
- If job is stopped, sends
SIGCONTto resume it - Shell waits for job to complete or stop again
- Prints the entire command when bringing to foreground
- Uses most recently created job if no job number provided
Error: No such job if job number doesn't exist.
Syntax: bg [job_number]
Examples:
# Start and stop a job
<user@host:~> sleep 100 &
[1] 12345
<user@host:~> fg 1
sleep 100
^Z
[1] Stopped sleep
# Resume in background
<user@host:~> bg 1
[1] sleep &
# Resume most recent job (no argument)
<user@host:~> bg
[1] sleep &Features:
- Sends
SIGCONTto resume stopped job - Job continues running in background
- Prints
[job_number] command_name &when resuming
Errors:
Job already running- if job is already runningNo such job- if job number doesn't exist
Syntax: command1 | command2 | ... | commandN
Examples:
# Count lines in file
<user@host:~> cat file.txt | wc -l
# Search and count
<user@host:~> cat file.txt | grep "pattern" | wc -l
# Multiple pipes
<user@host:~> ls -la | grep ".txt" | sort | head -5Syntax: command < filename
Examples:
# Read file contents
<user@host:~> cat < input.txt
# Sort file contents
<user@host:~> sort < data.txtError: No such file or directory if file doesn't exist.
Syntax: command > filename or command >> filename
Examples:
# Overwrite file
<user@host:~> echo "Hello" > output.txt
# Append to file
<user@host:~> echo "World" >> output.txt
# Redirect command output
<user@host:~> ls -la > file_list.txtFeatures:
>: Creates/overwrites file>>: Appends to file- Multiple output redirections: only last one takes effect
Error: Unable to create file for writing if file cannot be created.
Examples:
# Input and output redirection
<user@host:~> cat < input.txt > output.txt
# Pipes with redirection
<user@host:~> cat < input.txt | grep "pattern" > output.txt
# Multiple redirections (last one wins)
<user@host:~> cat < input1.txt < input2.txt > output.txt
# Only input2.txt is usedSyntax: command1 ; command2 ; ... ; commandN
Examples:
# Execute multiple commands
<user@host:~> echo "First" ; echo "Second" ; echo "Third"
First
Second
Third
# Mix with other operators
<user@host:~> ls ; pwd ; whoamiFeatures:
- Executes commands in order
- Waits for each command to complete before starting next
- Continues execution even if a command fails
- Prompt displayed only after all commands finish
Syntax: command &
Examples:
# Run command in background
<user@host:~> sleep 100 &
[1] 12345
<user@host:~> # Shell immediately shows prompt
# Background with pipes
<user@host:~> cat file.txt | grep "pattern" | wc -l &
[2] 12346
# Background with redirection
<user@host:~> cat < input.txt > output.txt &
[3] 12347Features:
- Forks child process but doesn't wait for completion
- Prints
[job_number] process_idwhen job starts - Immediately displays new prompt
- Checks for completed background processes before parsing input
- Prints completion messages:
command_name with pid process_id exited normallycommand_name with pid process_id exited abnormally
- Background processes don't have terminal access for input
Interrupts the current foreground process.
Behavior:
- Sends
SIGINTto foreground process group - Shell remains responsive (doesn't terminate)
- Process is terminated
Example:
<user@host:~> sleep 100
^C
<user@host:~> # Shell continuesStops the current foreground process and moves it to background.
Behavior:
- Sends
SIGTSTPto foreground process group - Process is stopped and moved to background job list
- Prints
[job_number] Stopped command_name - Shell continues accepting commands
Example:
<user@host:~> sleep 100
^Z
[1] Stopped sleep
<user@host:~> # Shell continues, job is stopped
<user@host:~> bg 1
[1] sleep &Exits the shell.
Behavior:
- Detects EOF condition
- Terminates all remaining child processes
- Prints "logout"
- Exits with status 0
Example:
<user@host:~> [Ctrl-D]
logoutDetailed architecture and implementation notes are available in
docs/INTERNALS.md.
- No shell scripting (loops, conditionals)
- No globbing or environment variable expansion
- Designed for learning OS internals, not production use
- Compiler: GCC with C99 standard
- POSIX Compliance: POSIX.1-2008
- Flags:
-Wall -Wextra -Werror(strict warnings) - Tested on: Linux (Ubuntu 22.04), WSL2
- Modular design with clear separation of concerns
- Zero memory leaks with proper resource cleanup
- Robust error handling and signal-safe design