A feature-rich Unix shell implementation written in C that provides standard shell functionality along with custom built-in commands, job control, I/O redirection, and command history management.
- Features
- Architecture
- Building and Running
- Built-in Commands
- Features in Detail
- Implementation Details
- File Structure
- Technical Specifications
- Custom Prompt: Displays
<username@hostname:current_directory>format with tilde expansion for home directory - Command Execution: Execute system commands with full path resolution
- Command Chaining: Support for sequential command execution using
; - Background Processes: Execute commands in background using
& - Piping: Chain commands using
|for pipeline execution - I/O Redirection:
- Input redirection with
< - Output redirection with
>(truncate) - Append output with
>>
- Input redirection with
- hop - Enhanced directory navigation
- reveal - Advanced directory listing
- log - Command history management
- activities - Process activity monitoring
- ping - Send signals to processes
- fg/bg - Job control commands
- Signal Handling: Proper handling of
Ctrl+C,Ctrl+Z, andCtrl+D - Job Control: Full background/foreground job management
- Command History: Persistent history with smart duplicate handling
- Process Management: Track and manage spawned processes
- Non-canonical Input: Character-by-character input processing with immediate Ctrl+D detection
The shell is organized into modular components:
C_Shell/
βββ src/
β βββ main.c # Main shell loop and input handling
β βββ prompt.c # Prompt generation and display
β βββ parser.c # Command syntax validation
β βββ intrinsics.c # Built-in command implementations
β βββ exec.c # Command execution and job control
βββ include/
β βββ prompt.h
β βββ parser.h
β βββ intrinsics.h
β βββ exec.h
βββ Makefile
- GCC compiler
- POSIX-compliant Unix/Linux system
- Standard C library
make./shell.outmake cleanNavigate through directories with enhanced features.
Syntax:
hop [path1] [path2] ... [pathN]Special Arguments:
~- Navigate to home directory.- Current directory (no change)..- Parent directory-- Previous directory (toggles between current and previous)
Examples:
hop ~ # Go to home directory
hop .. # Go to parent directory
hop - # Go to previous directory
hop dir1 dir2 # Navigate through dir1, then dir2Features:
- Sequential navigation through multiple directories
- Maintains previous directory for
-argument - Prints "No such directory!" for invalid paths
List directory contents with sorting and filtering options.
Syntax:
reveal [flags] [path]Flags:
-a- Show hidden files (files starting with.)-l- Line-by-line output (one entry per line)- Flags can be combined:
-alor-la
Path Arguments:
~- Home directory.- Current directory..- Parent directory-- Previous directory- Any valid absolute/relative path
Examples:
reveal # List current directory
reveal -a # Show hidden files
reveal -l # Line-by-line format
reveal -al ~ # Show all files in home, line-by-line
reveal ../projects # List specific directoryFeatures:
- Alphabetically sorted output (lexicographic order)
- Excludes
.and..from output - Space-separated default output, line-by-line with
-l
Manage and replay command history with persistence.
Syntax:
log # Display all stored commands
log purge # Clear all history
log execute <index> # Re-execute a command from historyExamples:
log # Show history (oldest to newest)
log execute 3 # Execute the 3rd most recent command
log execute 1 | grep x # Execute command and pipe output
log purge # Clear all historyFeatures:
- Stores up to 15 most recent commands
- Persistent across shell sessions (saved in
~/.osh_history) - Automatic duplicate removal (consecutive duplicates skipped)
- Commands containing
logare never stored - Index is 1-based, where 1 = most recent command
- Can chain
log executeoutput with pipes and redirections
Special Behavior:
- Re-executed commands are NOT added to history again
- History is immediately persisted after each command
- Duplicate commands are moved to the most recent position
Display all processes spawned by the shell that are currently running or stopped.
Syntax:
activitiesOutput Format:
[pid] : command_name - State
Example Output:
[1234] : sleep 100 - Running
[1235] : vim file.txt - Stopped
[1240] : find / -name test - Running
Features:
- Shows only shell-spawned processes
- Lists both running and stopped jobs
- Sorted alphabetically by command name
- Automatically removes terminated processes from display
Send signals to processes by PID.
Syntax:
ping <pid> <signal_number>Examples:
ping 1234 9 # Send SIGKILL to process 1234
ping 5678 15 # Send SIGTERM to process 5678
ping 9999 19 # Send SIGSTOP to process 9999Features:
- Signal number modulo 32 mapping (e.g., 33 β 1)
- Prints confirmation: "Sent signal X to process with pid Y"
- Error handling: "No such process found" for invalid PIDs
Bring background jobs to foreground or resume stopped jobs.
fg - Foreground
Syntax:
fg [job_number]Behavior:
- Without argument: brings most recent job to foreground
- With argument: brings specified job to foreground
- Resumes stopped jobs automatically (sends SIGCONT)
- Waits for job to complete or stop
- If job stops (Ctrl+Z), moves it back to background
bg - Background
Syntax:
bg [job_number]Behavior:
- Without argument: resumes most recent stopped job
- With argument: resumes specified stopped job
- Prints:
[job_id] command & - Error if job is already running
Examples:
fg # Bring most recent job to foreground
fg 2 # Bring job #2 to foreground
bg # Resume most recent stopped job in background
bg 3 # Resume job #3 in backgroundThe shell validates all commands according to a strict grammar:
<command_group> ::= <atomic> [ '|' <atomic> ]* [ '&' ]
<line> ::= <command_group> [ ';' <command_group> ]*
<atomic> ::= <name> [ <arg> | '<' <name> | '>' <name> | '>>' <name> ]*
Valid Examples:
ls -la
cat file.txt | grep error | wc -l
ls > output.txt ; cat output.txt
sleep 100 &
cat < input.txt > output.txt
ls | grep txt ; cat file.txt &Invalid Examples:
| ls # Pipe at start
ls | # Pipe at end
ls > ; cat # Missing filename
& ls # Background at startInput Redirection (<):
sort < unsorted.txt # Read from file
cat < input.txt | grep x # Redirect input in pipelineOutput Redirection (>):
ls > files.txt # Truncate and write
echo "Hello" > output.txt # Overwrite fileAppend Output (>>):
echo "Line 1" >> log.txt # Append to file
date >> log.txt # Append more dataCombined:
sort < input.txt > sorted.txt
cat < file1.txt >> file2.txtChain multiple commands where output of one becomes input of the next:
ls -l | grep ".txt" | wc -l # Count txt files
ps aux | grep chrome | awk '{print $2}' # Get Chrome PIDs
cat file.txt | sort | uniq | wc -l # Count unique sorted linesPipeline Features:
- Unlimited pipeline depth
- Each command runs in separate process
- Proper pipe buffer management
- Error propagation through pipeline
Execute long-running commands without blocking the shell:
sleep 100 & # Run in background
find / -name "*.txt" > results.txt & # Search in background
./long_script.sh & # Script in backgroundBackground Process Features:
- Prints
[job_id] pidwhen started - Tracked by shell for status monitoring
- STDIN redirected to
/dev/null - Can be brought to foreground with
fg - Notified when completed:
command with pid X exited normally/abnormally
Ctrl+C (SIGINT):
- Shell ignores SIGINT
- Foreground process receives SIGINT
- Shell continues running
Ctrl+Z (SIGTSTP):
- Foreground process stops
- Process moved to background job list as "Stopped"
- Job can be resumed with
fgorbg
Ctrl+D (EOF):
- Immediately detected in non-canonical mode
- Kills all background jobs
- Prints "logout"
- Exits shell cleanly
Job Lifecycle:
- Process starts in foreground or background
- If background: tracked in job list with job_id
- If stopped (Ctrl+Z): added to job list as "Stopped"
- Can be resumed with
fg(foreground) orbg(background) - Removed from job list when terminated
Job List Management:
- Each job has unique job_id (sequential)
- Jobs tracked by PID and command
- Status: Running or Stopped
- Automatic cleanup of terminated jobs
Persistence:
- Saved in
~/.osh_historyfile - Loaded on shell startup
- Saved after each command
History Rules:
- Maximum 15 commands stored
- Oldest commands dropped when limit reached
- Consecutive duplicate commands not stored
- Commands containing atomic
lognever stored - Re-executed commands (from
log execute) not stored again - Duplicate commands moved to most recent position
Storage Format:
- Plain text, one command per line
- Most recent command at end of file
- Each background process runs in its own process group
- Foreground pipelines run in a single process group
- Allows proper signal delivery to entire job
- Shell remains in its own process group
The shell operates in non-canonical terminal mode for immediate character processing:
- Characters processed immediately without waiting for newline
- Ctrl+D detected instantly (before any buffering)
- Backspace handled manually with visual feedback
- Echo manually controlled for accurate display
Benefits:
- Immediate Ctrl+D detection even during foreground processes
- Better user experience with instant feedback
- Proper EOF handling without line buffering delays
- Input Reading: Non-canonical mode with immediate character processing
- Syntax Validation: Grammar-based parsing before execution
- History Recording: Store command if it meets criteria
- Intrinsic Check: Determine if built-in or external command
- Execution:
- Built-ins: Execute in shell process
- External: Fork, exec, and wait/track
- Job Management: Update background job statuses
- Prompt Display: Show updated prompt for next command
- All dynamically allocated strings freed appropriately
- Token arrays cleaned up after parsing
- Job list entries freed when jobs terminate
- History persistence ensures no data loss
Graceful Degradation:
- Invalid syntax: Print "Invalid Syntax!" and continue
- Missing files: Print "No such file or directory"
- Invalid directories: Print "No such directory!"
- Invalid commands: Print "Command not found!"
- Process errors: Print "No such process found"
No Shell Crashes:
- All system call failures handled
- Malloc failures checked and handled
- Signal handling prevents unexpected termination
main.c
- Main shell loop
- Non-canonical terminal mode setup
- Input reading with backspace and Ctrl+D handling
- Command re-execution logic
- Signal handler registration
- Terminal restoration on exit
prompt.c
- Prompt initialization: capture home directory, username, hostname
- Prompt display with path resolution
- Tilde expansion for paths under home directory
- PWD environment variable management
parser.c
- Lexical analysis: tokenization into grammar tokens
- Syntax validation: recursive descent parser
- Grammar enforcement: validates command structure
- Token utility functions
intrinsics.c
- Built-in command dispatch
hop: Directory navigation with historyreveal: Directory listing with flagslog: History management (display, purge, execute)- History persistence: load/save from
~/.osh_history - Duplicate detection and removal
exec.c
- External command execution with fork/exec
- Pipeline creation: pipe setup and management
- I/O redirection: file descriptor manipulation
- Background job tracking: job list management
- Signal handlers: SIGINT, SIGTSTP
activities: Process status displayping: Signal sendingfg/bg: Job control implementation- Process group management
- Foreground process tracking for signals
All headers follow consistent structure:
- Include guards
- Function declarations
- Type definitions (where applicable)
- External variable declarations
-std=c99 # C99 standard
-D_POSIX_C_SOURCE=200809L # POSIX.1-2008 features
-D_XOPEN_SOURCE=700 # X/Open 7 features
-Wall -Wextra -Werror # Strict warnings
-Wno-unused-parameter # Allow unused parameters
-fno-asm # No inline assembly- Process Control:
fork,execvp,wait,waitpid,setpgid,getpid - Signals:
signal,sigaction,kill - File Operations:
open,close,read,write,dup2 - Directory:
getcwd,chdir,opendir,readdir,closedir - Terminal:
tcgetattr,tcsetattr - I/O Multiplexing:
poll - Other:
pipe,stat,getenv,setenv
bg_job (Background Job)
typedef struct bg_job {
pid_t pid; // Process ID
int job_id; // Shell-assigned job number
char *command; // Command string
int stopped; // 1 if stopped, 0 if running
struct bg_job *next; // Linked list next pointer
} bg_job;CmdNode (Pipeline Stage)
typedef struct {
char **argv; // NULL-terminated argument vector
char *infile; // Input redirection file (or NULL)
char *outfile; // Output redirection file (or NULL)
int append; // 1 for >>, 0 for >
} CmdNode;- History Size: 15 commands maximum
- Path Length:
PATH_MAX(typically 4096 bytes) - Initial Token Buffer: 16 tokens (grows dynamically)
- Host Name Length: System
_SC_HOST_NAME_MAX
The shell is single-threaded but uses:
volatile sig_atomic_tfor signal handler variables- Async-signal-safe functions in signal handlers
- No shared state between parent and child processes
Paths are displayed with ~ when under home directory:
/home/user β ~
/home/user/docs β ~/docs
/home/other/file β /home/other/file
- Updated only after successful
chdiroperations - Available for
hop -,reveal - - Initially unset; error if used before any directory change
When a command is entered that already exists in history:
- Previous occurrence is removed
- Command is added as most recent
- Result: no duplicates, most recent position maintained
Process Group Isolation:
- Each background job in separate process group
- Prevents terminal signals from affecting background jobs
- Allows independent job control
Status Reporting:
- Check jobs before each prompt
- Non-blocking status checks (
WNOHANG) - Print completion messages asynchronously
Thanks to non-canonical mode and polling:
- Shell polls stdin while waiting for foreground process
- EOF/Ctrl+D detected immediately
- Calls
handle_eof_exit()which kills all jobs - Prints "logout" and exits
This ensures clean shutdown even if a foreground process is running.
| Error | Message |
|---|---|
| Invalid command syntax | Invalid Syntax! |
| Directory not found | No such directory! |
| File not found for redirection | No such file or directory |
| Cannot create output file | Unable to create file for writing |
| Command not in PATH | Command not found! |
| Invalid process ID for ping | No such process found |
| Invalid job number for fg/bg | No such job |
| Job already running for bg | Job already running |
<user@hostname:~> hop ~/projects
<user@hostname:~/projects> reveal -la
.git
.gitignore
README.md
src
tests
<user@hostname:~/projects> cat README.md | grep "TODO" > todos.txt &
[1] 12345
<user@hostname:~/projects> activities
[12345] : cat README.md - Running
<user@hostname:~/projects> sleep 30 &
[2] 12346
[2] 12346
<user@hostname:~/projects> fg 1
cat README.md
^Z
[3] Stopped cat README.md
<user@hostname:~/projects> bg 3
[3] cat README.md &
<user@hostname:~/projects> log
hop ~/projects
reveal -la
cat README.md | grep "TODO" > todos.txt &
sleep 30 &
fg 1
bg 3
<user@hostname:~/projects> log execute 2
.git
.gitignore
README.md
src
tests
<user@hostname:~/projects> ping 12346 9
Sent signal 9 to process with pid 12346
sleep 30 with pid 12346 exited abnormally
<user@hostname:~/projects> ^D
logout- The shell maintains POSIX compliance for maximum portability
- All commands are thoroughly validated before execution
- Memory leaks are prevented through careful resource management
- The implementation prioritizes robustness and user experience
- Signal handling ensures the shell remains responsive under all conditions
When modifying the shell:
- Maintain the existing code structure and style
- Test all features thoroughly, especially edge cases
- Update this README if adding new features
- Ensure compilation with all warning flags enabled
- Verify signal handling and job control behavior