A comprehensive Unix-like shell implementation in C++ featuring command parsing, I/O redirection, pipes, background processes, and wildcard expansion.
- Overview
- Project Structure
- Architecture & Workflow
- Features
- Building & Running
- Usage Examples
- Testing
- Implementation Details
- Error Handling
This mini shell is a command-line interpreter that provides core shell functionality similar to bash or sh. It parses user commands, handles I/O redirection and piping, manages processes, and supports advanced features like wildcard expansion.
- Execute system commands with arguments
- Input/Output/Error redirection
- Command piping (chaining multiple commands)
- Background process execution
- Built-in commands (
cd,exit) - Signal handling (Ctrl-C protection)
- Process logging
- Wildcard expansion (
*,?)
mini-shell/
βββ command.h # Command data structures
βββ command.cc # Core implementation (parsing & execution)
βββ tokenizer.h # Token type definitions
βββ tokenizer.cc # Lexical analysis (tokenization)
βββ Makefile # Build configuration
βββ log.txt # Process termination log (created at runtime)
βββ testcases/ # Test scripts
βββ basic_ls_al.sh
βββ echo_hello_os_lab.sh
βββ redirect_create_output1.sh
βββ ... (10 test scripts total)
βββββββββββββββββββββββββββββββββββββββββββββββ
β main() - Entry Point β
β - Signal handlers setup (SIGINT, SIGCHLD) β
β - Log file initialization β
β - Main input loop β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β tokenizer.cc / tokenizer.h β
β - Lexical analysis of input string β
β - Converts input β vector<Token> β
β - Recognizes operators: | > >> < 2> >>& & β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β parse() - Syntax Analysis β
β - Builds Command & SimpleCommand structs β
β - Handles redirection operators β
β - Handles pipe operators β
β - Expands wildcards β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Command::execute() - Execution Engine β
β - Built-in commands (cd, exit) β
β - Process creation (fork/exec) β
β - File descriptor management β
β - Pipe construction β
β - Process waiting & logging β
βββββββββββββββββββββββββββββββββββββββββββββββ
Represents a single command with its arguments.
struct SimpleCommand {
int _numberOfArguments;
char **_arguments; // NULL-terminated array
// Methods: insertArgument()
}Example: ls -al β _arguments = ["ls", "-al", NULL]
Represents a complete shell command (may contain multiple SimpleCommands connected by pipes).
struct Command {
SimpleCommand **_simpleCommands; // Array of simple commands
int _numberOfSimpleCommands;
char *_outFile; // Output redirection file
char *_inputFile; // Input redirection file
char *_errFile; // Error redirection file
int _background; // Background flag
int _append; // Append mode flag
int _out_error; // Redirect stderr to stdout flag
// Methods: execute(), print(), clear(), prompt()
}Example: ls -al | grep txt > out.txt creates:
- 2 SimpleCommands:
["ls", "-al"]and["grep", "txt"] _outFile = "out.txt"
enum TokenType {
TOKEN_COMMAND, // First word in command
TOKEN_ARGUMENT, // Arguments and filenames
TOKEN_REDIRECT, // >
TOKEN_APPEND, // >>
TOKEN_INPUT, // <
TOKEN_ERROR, // 2>
TOKEN_PIPE, // |
TOKEN_BACKGROUND, // &
TOKEN_REDIRECT_AND_ERROR, // >>&
TOKEN_EOF // End of input
}Input: "ls -al | grep txt > output.txt"
β
Tokenizer splits by whitespace and identifies operators
β
Tokens: [
{TOKEN_COMMAND, "ls"},
{TOKEN_ARGUMENT, "-al"},
{TOKEN_PIPE, "|"},
{TOKEN_COMMAND, "grep"},
{TOKEN_ARGUMENT, "txt"},
{TOKEN_REDIRECT, ">"},
{TOKEN_ARGUMENT, "output.txt"},
{TOKEN_EOF, ""}
]
- Initialize: Create empty Command and SimpleCommand
- Iterate tokens:
- Commands/Arguments: Add to current SimpleCommand (with wildcard expansion)
- Pipe
|: Finalize current SimpleCommand, start new one - Redirection
>,>>,<,2>,>>&: Mark next token as filename - Background
&: Set background flag
- Finalize: Add last SimpleCommand to Command
- Execute: Call
Command::execute()
Input: "cat < input.txt | grep error > output.txt"
Parse Steps:
1. "cat" β Create SimpleCommand, add "cat"
2. "<" β Set expectingFilename = true, redirectType = INPUT
3. "input.txt" β Set _inputFile = "input.txt"
4. "|" β Insert SimpleCommand #1, create new SimpleCommand
5. "grep" β Add "grep" to SimpleCommand #2
6. "error" β Add "error" to SimpleCommand #2
7. ">" β Set expectingFilename = true, redirectType = REDIRECT
8. "output.txt" β Set _outFile = "output.txt"
9. EOF β Insert SimpleCommand #2, call execute()
Result Command:
- SimpleCommand[0]: ["cat"]
- SimpleCommand[1]: ["grep", "error"]
- _inputFile: "input.txt"
- _outFile: "output.txt"
βββββββββββββββββββββββββββββββββββββββ
β 1. Check for Built-in Commands β
β - exit β Quit shell β
β - cd β Change directory β
ββββββββββββββ¬βββββββββββββββββββββββββ
β (External command)
βΌ
βββββββββββββββββββββββββββββββββββββββ
β 2. Save Default File Descriptors β
β defaultin = dup(0) β
β defaultout = dup(1) β
β defaulterr = dup(2) β
ββββββββββββββ¬βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β 3. Setup Input Redirection β
β if (_inputFile) β
β fdin = open(_inputFile, RD) β
ββββββββββββββ¬βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β 4. FOR EACH SimpleCommand: β
β β
β A. Redirect stdin β
β dup2(fdin, 0) β
β β
β B. Setup Output: β
β - If LAST command: β
β * _outFile β open file β
β * _errFile β open error file β
β - Else (not last): β
β * Create pipe() β
β * fdout = pipe[1] β
β * fdin = pipe[0] (for next) β
β β
β C. Redirect stdout & stderr β
β dup2(fdout, 1) β
β dup2(fderr, 2) β
β β
β D. Fork & Execute β
β pid = fork() β
β if (pid == 0) β
β execvp(command, args) β
β β
ββββββββββββββ¬βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β 5. Restore Default File Descriptorsβ
β dup2(defaultin, 0) β
β dup2(defaultout, 1) β
β dup2(defaulterr, 2) β
ββββββββββββββ¬βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β 6. Wait for Process (if foreground) β
β waitpid(last_pid, &status, 0) β
β Log termination to log.txt β
βββββββββββββββββββββββββββββββββββββββ
- Foreground: Shell waits using
waitpid()and logs termination - Background: Shell doesn't wait; SIGCHLD handler logs when process finishes
myshell> ls -al
myshell> pwd
myshell> echo "Hello World"# Output redirection (overwrite)
myshell> ls -al > output.txt
# Output redirection (append)
myshell> echo "new line" >> output.txt
# Input redirection
myshell> cat < input.txt
# Error redirection
myshell> ls nonexistent 2> error.txt
# Redirect both stdout and stderr
myshell> command >>& output.txt# Single pipe
myshell> ls -al | grep txt
# Multiple pipes
myshell> cat file.txt | grep error | wc -l
# Pipes with redirection
myshell> ls -al | grep txt > results.txtmyshell> sleep 10 &
myshell> long_running_command &The shell returns immediately; process runs in background.
myshell> cd /tmp # Change to /tmp
myshell> cd .. # Go up one directory
myshell> cd # Go to HOME directory
myshell> cd ~ # Go to HOME directoryOutput:
Changing to directory '/tmp'
You are now in /tmp
myshell> exit
Good bye!!- Ctrl-C (SIGINT): Shell ignores it; prints newline and new prompt
- SIGCHLD: Automatically logs child process terminations
Every terminated child process is logged to log.txt:
[Sun Oct 26 14:23:45 2025] Child process 12345 terminated with status 0
[Sun Oct 26 14:23:46 2025] Child process 12346 terminated with status 0
myshell> echo * # All files in current directory
myshell> echo *.cc # All .cc files
myshell> echo test*.txt # Files starting with "test", ending with ".txt"
myshell> ls /tmp/* # All files in /tmpmyshell> echo file?.txt # file1.txt, file2.txt, fileA.txt
myshell> ls test??.cc # test01.cc, test23.ccmyshell> echo M*f* # Files starting with M, containing f
myshell> ls /*bin*/ls # /bin/ls, /usr/bin/ls, etc.Implementation: Uses glob() from <glob.h>, results are sorted alphabetically.
- Compiler: g++ with C++11 support
- OS: Linux/Unix-based system
- Libraries: Standard C++ library, POSIX APIs
makeOutput: Creates myshell executable
make cleanRemoves: *.o, myshell, log.txt
make rebuildEquivalent to: make clean && make
make runEquivalent to: make && ./myshell
./myshellOutput:
myshell>
myshell> ls -al
myshell> cat file.txt | grep pattern > results.txt
myshell> exit
Good bye!!echo -e "ls\npwd\nexit" | ./myshellmyshell> ls -al > files.txtWhat happens:
- Tokenize:
["ls", "-al", ">", "files.txt"] - Parse: Create SimpleCommand
["ls", "-al"], set_outFile = "files.txt" - Execute: Fork β Open
files.txtfor writing β Redirect stdout βexecvp("ls", ["ls", "-al"])
myshell> cat data.txt | grep ERROR | wc -lWhat happens:
- Parse: 3 SimpleCommands:
["cat", "data.txt"],["grep", "ERROR"],["wc", "-l"] - Execute:
- Fork β
cat data.txtβ stdout β pipe1 - Fork β pipe1 β stdin,
grep ERROR, stdout β pipe2 - Fork β pipe2 β stdin,
wc -l, stdout β terminal
- Fork β
myshell> sleep 5 &What happens:
- Parse: Set
_background = 1 - Execute: Fork β
execvp("sleep", ["sleep", "5"])β Don't wait, return to prompt immediately - SIGCHLD handler logs when sleep finishes
myshell> echo *.ccWhat happens:
- Tokenize:
["echo", "*.cc"] - Parse: Call
expandWildcard("*.cc")β Returns["command.cc", "tokenizer.cc"] - SimpleCommand:
["echo", "command.cc", "tokenizer.cc"] - Execute: Prints
command.cc tokenizer.cc
Located in testcases/ directory with 10 automated tests:
| Test Script | Description | Points |
|---|---|---|
basic_ls_al.sh |
Tests basic command execution | 2 |
echo_hello_os_lab.sh |
Tests echo with quoted strings | 2 |
redirect_create_output1.sh |
Tests output redirection creates file | 2 |
cat_input_output1.sh |
Tests input redirection | 2 |
append_and_grep_line.sh |
Tests append mode and piping | 2 |
count_ls_wc.sh |
Tests pipe with ls and wc | 2 |
background_sleep2.sh |
Tests background execution | 2 |
cd_up_message.sh |
Tests cd command with messages | 2 |
stderr_redirection.sh |
Tests stderr redirection (2>) | 2 |
pwd_test.sh |
Tests pwd command | 2 |
log_file_linecount.sh |
Tests log.txt has correct line count | 2 |
bash run_all_and_sum.shOutput:
basic_ls_al.sh -> 2
echo_hello_os_lab.sh -> 2
redirect_create_output1.sh -> 2
...
TOTAL=22
cd testcases
bash basic_ls_al.shSome tests depend on previous tests:
redirect_create_output1.shcreatesoutput1.txtcat_input_output1.shreads fromoutput1.txtappend_and_grep_line.shappends tooutput1.txt
Run tests in order specified in run_all_and_sum.sh.
- Dynamic Allocation: Uses
malloc()/realloc()for arrays - Cleanup:
Command::clear()frees all allocated memory - String Duplication: Uses
strdup()for storing strings
// Save defaults
int defaultin = dup(0);
int defaultout = dup(1);
int defaulterr = dup(2);
// ... perform redirections ...
// Restore
dup2(defaultin, 0);
dup2(defaultout, 1);
dup2(defaulterr, 2);
close(defaultin);
close(defaultout);
close(defaulterr);Why? Prevents side effects between commands.
int fdpipe[2];
pipe(fdpipe); // Create pipe
// Child 1: dup2(fdpipe[1], 1); // Write end β stdout
// Child 2: dup2(fdpipe[0], 0); // Read end β stdin// SIGINT: Prevent Ctrl-C from killing shell
sa_int.sa_handler = sigint_handler;
sa_int.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa_int, nullptr);
// SIGCHLD: Log terminated background processes
sa_chld.sa_handler = sigchld_handler;
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa_chld, nullptr);- Command Not Found:
execvp()fails β Print "execvp: No such file or directory" - File Not Found: Input file doesn't exist β Print "open input file: No such file or directory"
- Permission Denied: Can't open file β Print "open output file: Permission denied"
- Invalid Pipe: Pipe before command β Print "Error: Invalid pipe usage"
- Fork Failure: Can't create process β Print "fork: Resource temporarily unavailable"
- Missing Redirection Filename:
ls >with no filename β Print "Error: Expected filename after redirection operator"
myshell> cat nonexistent.txt
cat: nonexistent.txt: No such file or directory
myshell> ls > /root/file.txt
open output file: Permission denied
myshell> | grep test
Error: Invalid pipe usageUncomment print() in Command::execute() to see the parsed command table:
void Command::execute() {
print(); // Uncomment this line
// ...
}Output:
COMMAND TABLE
# Simple Commands
--- ----------------------------------------------------------
0 "ls" "-al"
Output Input Error Err&Out Background
------------ ------------ ------------ ------------ ------------
output.txt default default NO NO
- No command history: Up/down arrows don't work
- No tab completion: Can't auto-complete filenames
- No job control: Can't use
fg,bg,jobscommands - No aliases: Can't create command shortcuts
- No environment variable expansion:
$HOMEnot supported (except incd)
- Add command history with readline library
- Implement job control (
fg,bg,jobs) - Support environment variables (
$VAR) - Add command aliases
- Implement command substitution (
$(command))
Course: CC373 - Operating Systems
Institution: Alexandria University, Faculty of Engineering
Program: Computer & Communication Engineering
Lab: Lab 3 - Mini Shell
Implementation Features:
- β Command parsing and execution
- β I/O redirection (>, >>, <, 2>, >>&)
- β Pipes (single and multiple)
- β Background processes (&)
- β Built-in commands (cd, exit)
- β Signal handling (SIGINT, SIGCHLD)
- β Process logging
- β Bonus: Wildcard expansion (*, ?)
This project is for educational purposes as part of the Operating Systems course curriculum.
Happy Shelling! π# Mini-Shell