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 (Ctrl-C, Ctrl-Z,
&) - Supports pipes, I/O redirection, background execution, and sequential commands
- Dynamic prompt showing
user@hostname:path$with live~home directory substitution - Uses process groups and terminal control (
setpgid,tcsetpgrp) - Signal-safe design using
sigactionwithoutSA_RESTART - Environment variable expansion (
$VAR) before execution - Persistent command history across sessions (
~/.Psh_history) - Recursive descent parser validates syntax before any process is forked
- GCC compiler
- Make
- Linux (Ubuntu recommended)
git clone https://github.com/perxeusss/Psh-shell.git
cd Psh-shell
make./pshType exit or press Ctrl-D. The shell prints logout and exits cleanly,
killing all background jobs before exiting.
make && ./psh
# Dynamic prompt shows user, hostname, and current path
perxeuss@hostname:~$
# ~ substitution — home directory shown as ~
perxeuss@hostname:~$ cd /tmp
perxeuss@hostname:/tmp$ cd ~
perxeuss@hostname:~$
# Try a command
perxeuss@hostname:~$ echo Hello World
Hello World
# Run a pipeline
perxeuss@hostname:~$ ls -la | grep ".c" | wc -l
9
# Exit
perxeuss@hostname:~$ exitThe prompt is generated live on every command using real system calls:
perxeuss@hostname:~$
perxeuss@hostname:~/projects$
perxeuss@hostname:/tmp/build$
Format: user@hostname:path$
user— read from$USERenvironment variablehostname— fetched viagethostname()path— current working directory fromgetcwd(), with home directory replaced by~
The ~ substitution works by comparing the current working directory against the
shell's recorded home at startup. If the path starts with the home prefix, it is
replaced with ~ — matching real shell behavior.
Psh can run any binary available on your system PATH:
perxeuss@hostname:~$ ls -la
perxeuss@hostname:~$ gcc main.c -o main
perxeuss@hostname:~$ python3 script.pyError handling: If a command doesn't exist, the shell prints Command not found!
Syntax: cd [~ | - | path]
Arguments:
- No argument or
~: Change to home directory -: Go back to previous directorypath: Change to specified relative or absolute path
Examples:
perxeuss@hostname:~$ cd /tmp
perxeuss@hostname:/tmp$ cd ~
perxeuss@hostname:~$ cd -
perxeuss@hostname:/tmp$ cd ../home
perxeuss@hostname:/home$Error: Prints cd: No such file or directory if path doesn't exist.
Syntax: pwd
perxeuss@hostname:~/projects$ pwd
/home/perxeuss/projectsSyntax: echo [-n] [args...]
Flags:
-n: Suppress trailing newline
perxeuss@hostname:~$ echo Hello World
Hello World
perxeuss@hostname:~$ echo -n no newline
no newline perxeuss@hostname:~$
perxeuss@hostname:~$ echo $HOME
/home/perxeussSyntax: env
perxeuss@hostname:~$ env
PATH=/usr/local/sbin:/usr/local/bin:...
HOME=/home/perxeuss
USER=perxeuss
...Syntax: setenv VAR=value or setenv VAR value
perxeuss@hostname:~$ setenv FOO=bar
perxeuss@hostname:~$ echo $FOO
barSyntax: unsetenv VAR
perxeuss@hostname:~$ unsetenv FOO
perxeuss@hostname:~$ echo $FOO
# empty — variable removedSyntax: which command
perxeuss@hostname:~$ which gcc
/usr/bin/gcc
perxeuss@hostname:~$ which cd
cd: shell built-in command
perxeuss@hostname:~$ which fakecommand
fakecommand not foundSyntax: exit
Kills all active background jobs and exits cleanly.
Syntax: command1 | command2 | ... | commandN
Each command runs in its own process. stdout of one is wired to stdin of the
next. Ctrl-C and Ctrl-Z affect the entire pipeline group, not just the last process.
perxeuss@hostname:~$ cat /etc/passwd | grep root | wc -l
2
perxeuss@hostname:~$ ls -la | sort | head -5
perxeuss@hostname:~$ cat file.txt | grep "error" | sort | uniq > errors.txtSyntax: command < filename
perxeuss@hostname:~$ sort < data.txt
perxeuss@hostname:~$ wc -l < file.txtSyntax: command > filename or command >> filename
perxeuss@hostname:~$ echo "Hello" > output.txt
perxeuss@hostname:~$ echo "World" >> output.txt
perxeuss@hostname:~$ ls -la > listing.txt>creates or overwrites the file>>appends to the file
perxeuss@hostname:~$ cat < input.txt > output.txt
perxeuss@hostname:~$ cat < input.txt | grep "pattern" > results.txt
perxeuss@hostname:~$ ls | sort > sorted_list.txtSyntax: command1 ; command2 ; ... ; commandN
perxeuss@hostname:~$ echo "First" ; echo "Second" ; echo "Third"
First
Second
Third
perxeuss@hostname:/tmp$ cd ~ ; ls ; pwdSyntax: command &
perxeuss@hostname:~$ sleep 10 &
perxeuss@hostname:~$ # prompt returns immediately
perxeuss@hostname:~$ make build &Completion messages appear before the next prompt:
sleep with pid 12345 exited normally
gcc with pid 12346 exited abnormally
$VAR is expanded before execution across all commands, not just builtins:
perxeuss@hostname:~$ echo $HOME
/home/perxeuss
perxeuss@hostname:~$ ls $HOME
perxeuss@hostname:~$ cd $HOME/projects
perxeuss@hostname:~/projects$Psh saves every command to ~/.Psh_history and loads it on startup.
Features:
- Stores up to 15 commands
- Persists across shell sessions
- No duplicate consecutive commands stored
- Commands containing
logas a token are not stored
perxeuss@hostname:~$ echo hello
perxeuss@hostname:~$ ls -la
perxeuss@hostname:~$ pwd
# View saved history
perxeuss@hostname:~$ cat ~/.Psh_history
echo hello
ls -la
pwdInterrupts and terminates the current foreground process or pipeline.
perxeuss@hostname:~$ sleep 100
^C
perxeuss@hostname:~$ # shell continues, sleep is killedFor pipelines, the entire process group is killed — not just one process.
Stops the current foreground process and moves it to the background job list.
perxeuss@hostname:~$ sleep 100
^Z
[1] Stopped sleep with pid 12345
perxeuss@hostname:~$ # shell continues, sleep is suspendedExits the shell.
perxeuss@hostname:~$ [Ctrl-D]
logoutDetailed architecture and implementation notes — including execution flow, data
structures, the double-setpgid race condition fix, and signal handling design —
are available in docs/INTERNALS.md.
Psh-shell/
├── src/
│ ├── main.c # Shell loop, initialization
│ ├── execute.c # fork/exec, pipelines, redirection
│ ├── runner.c # Command sequencing, builtin dispatch
│ ├── signals.c # Signal handlers, fg process group tracking
│ ├── jobs.c # Background job table management
│ ├── builtins.c # cd, echo, env, which, setenv, etc.
│ ├── parser.c # Syntax validation
│ ├── history.c # Command history load/save
│ ├── prompt.c # Dynamic prompt with ~ substitution
│ └── helpers.c # Shared utilities
├── include/ # Header files
├── docs/
│ └── INTERNALS.md # Architecture and implementation deep dive
├── Makefile
└── README.md
- No shell scripting (loops, conditionals, functions)
- No glob expansion (
*.c,file?.txt) - No
&&exit-code-aware chaining (treated as;) - No arrow key history navigation (yet)
- Designed for learning OS internals, not production use
- Compiler: GCC with C11 standard
- Platform: Linux (Ubuntu 22.04+)
- System calls used:
fork,execvp,waitpid,pipe,dup2,open,setpgid,tcsetpgrp,sigaction,kill,getcwd,gethostname,chdir - Modular design with clear separation of concerns
- Proper resource cleanup and job termination on exit