A zero-configuration POSIX command-line shell engineered in pure C, featuring dynamic multi-stage execution pipelines and persistent background job tracking.
Built as part of a self-directed C curriculum.
Requires gcc and make. Once the repository is cloned from the remote, you can run these build commands:
runtime builds:
make setup_deps # Clone the dependency repos
make # Compiles the executable cshell binarydev builds:
make test # Compiles and runs the test suite (./tests)
make debug # Builds with debug symbols and sanitisersclean:
make clean_deps # Cleans compiled binaries, .o, .a, and .d files (including compiled dependencies)
make clean # Cleans compiled binaries, .o, and .d files (just for cshell)include/cshell/ # Public interface definitions
├── test_framework.h # Basic test framework macros
├── executor.h # Pipeline execution, forking, and descriptor hygiene
├── expansion.h # Environment variable token identification and translation
├── parser.h # Lexical analysis and Pipeline/Command AST generation
├── prompt.h # Context-aware path formatting and ANSI escape logic
├── tracker.h # Persistent background job tables and state interfaces
├── terminal.h # Raw mode state changes, termios configuration, and teardown
├── history.h # Fixed-capacity circular history ring buffer definitions
├── linereader.h # ANSI escape state machine and horizontal inline editing
├── completion.h # PATH binary and local directory autocomplete scanning
├── filereader.h # Consolidated sequential line stream processing engine
├── handler.h # Multi-pipeline short-circuit operator dispatcher
├── startup.h # Boot configuration sequence initialisation (.cshellrc)
└── runtime.h # Central shell runtime limits and exit state registry
src/ # Core implementation (.c files)
tests/ # Per-module unit tests
To prevent subshell forks from discarding state mutations, cshell routes session state management, directory adjustments, and job manipulation directly to internal functions executing within the parent process context.
| Command | Syntax | Operational Summary |
|---|---|---|
cd |
cd [path] |
Mutates the current working directory via chdir(). Evaluates runtime path expansions (e.g., cd $HOME). |
export |
export KEY=VALUE |
Updates or assigns variables in the session's active environment block via setenv(). |
clear |
clear |
Transmits ANSI screen-erasure codes directly to standard output, resets tracking tabs, and returns status 0. |
source/. |
source <file> |
Streams and processes external script rows synchronously in-place, allowing commands to alter the active shell environment. |
jobs |
jobs |
Lists the sequence ID, PID, operational state (RUNNING/STOPPED), and command text of all background entries. |
fg |
fg <job_id> |
Restores terminal group ownership to a background task via tcsetpgrp(), sends SIGCONT, and blocks until foreground completion. |
bg |
bg <job_id> |
Resumes a suspended background task in place by forwarding SIGCONT without altering foreground terminal group status. |
exit |
exit |
Breaks the interactive REPL execution loop to initiate a graceful shell process teardown. |
The prompt dynamically evaluates the user's workspace on every REPL iteration. To maintain screen real estate, it detects the HOME environment variable prefix and cleanly truncates it to a ~ shorthand while isolating the path visually using high-contrast ANSI colour-coding
cshell:/mnt/c/Users> cd $HOME
cshell:~> cd ./projects/cshell
cshell:~/projects/cshell>Note: The ANSI colour-coding is not possible to convey in this documentation. If you run the
cshellbinary, you will see that the path (between:and>) is in pink and the rest is in default terminal colours.
cshell orchestrates arbitrary N-stage pipelines. It distinguishes between isolated subshell execution and parent-process evaluation: single built-in commands run in-place to mutate the parent environment, whereas pipelined built-ins automatically cascade into isolated child subshells to protect parent state.
cshell:~> echo systems_programming | grep -o programming
programmingExternal process execution safely handles low-level input (<) and output (>) file descriptor redirection, routing byte streams seamlessly across disk boundaries.
cshell:~> echo hello > source.txt # write "hello" to source.txt
cshell:~> cat < source.txt > destination.txt # "hello" copies to destination.txtTrailing & operators intercept parsing boundaries to launch non-blocking background pipelines. Additionally, active foreground processes can be suspended dynamically by sending the CTRL+Z interrupt signal (SIGTSTP), transferring execution ownership from the terminal foreground to the tracking stack.
Job lifetimes are managed natively by a dedicated tracking engine that prints clean lifecycle status updates immediately before rendering the next prompt.
cshell:~> sleep 10 &
[1] 11754
cshell:~> sleep 100
^Z
[2]+ Stopped sleep 100
cshell:~> jobs
[1] Running sleep 10 &
[2]+ Stopped sleep 100
cshell:~> bg 2
[2]+ sleep 100 &
cshell:~> # wait 10 seconds
[1]+ Done sleep 10 &
cshell:~> fg 2
sleep 100
# wait for it to end
cshell:~>Tokens prefixed with $ are scanned, extracted, and resolved via process environment blocks at runtime prior to pipeline execution routing. This includes the unique tracking token $?, which dynamically expands to the 8-bit ASCII string representation of the most recently executed foreground pipeline's exit status.
As discussed in the Native Built-in Commands section, it is possible to create environment variables through export KEY=VALUE.
cshell:~> export PROJECT_DIR=/tmp/test
cshell:~> cd $PROJECT_DIR
cshell:/tmp/test> false
cshell:/tmp/test> echo $?
1Pipelines can be conditionally chained using short-circuiting logical operators (&& and ||). The execution handler evaluates the list sequentially, short-circuiting downstream execution paths as soon as a preceding pipeline's exit status violates the logical invariant.
cshell:~> echo "step 1" && false && echo "step 2 hidden" || echo "step 3 executed"
step 1
step 3 executedThis example shows
&& commandwill execute if the command before returns an exit status of0(hence why&& echo "step 2 hidden"was ignored) and|| commandwill only execute if the command before returns a non-zero exit status (|| echo "step 3 executed"ran becausefalsereturns a non-zero exit status). It is an illegal command to have dangling logical operators (i.e. at the end or at the start).
The prompt transitions the host terminal from standard line-buffered (canonical) mode into raw character-by-character input mode. This powers an internal ANSI escape state machine supporting mid-line gap character insertion/deletion and full terminal line erasure.
Additionally, a persistent 32-slot circular ring buffer tracks unique executed entries, allowing users to scroll sequentially through command histories via the UP and DOWN arrow keys.
cshell:~> echo mid-line text
echo mid-line text
# Pressing UP arrow pulls the previous command line into the buffer:
cshell:~> echo mid-line text
echo mid-line text
# User presses UP arrow once, LEFT arrow 13 times and then types "custom ":
cshell:~> echo custom mid-line text
custom mid-line textNote: Hotkeys such as
CTRL+Linstantly flush ANSI clear sequences (\033[2J\033[3J\033[H) to erase the screen viewport while cleanly preserving and re-rendering active mid-line text buffers at the top-left coordinate.
Pressing the horizontal tab character (\t) intercepts input to calculate prefix matches using an independent scanning architecture.
If the token is the initial command word, cshell evaluates binary candidates across the system PATH environment array; if it is a trailing argument parameter, it scans local directory paths via file-system pointers
cshell:~/projects/cshell> ./t[TAB] # Completes inline to "./tests/"
cshell:~/projects/cshell> ./tests/[TAB] # Completes inline to "./tests/test_"
cshell:~/projects/cshell> ./tests/test_[TAB][TAB] # No single match on first try, prints options on second [TAB]
test_history.c test_parser.c test_executor.c test_completion.... test_expansion.c... test_handler.c
test_filereader.... test_terminal.c test_prompt.c test_tracker.c test_linereader.... test_startup.c
cshell:~/projects/cshell> ./tests/test_ # Re-prints line after listing options
cshell:~/projects/cshell> ffm[TAB] # Completes in-line to "ffmpeg "Upon process initialisation, cshell automatically resolves the user's home path via environment hooks to locate an optional ~/.cshellrc startup file. If detected, the file is processed synchronously line-by-line via a unified file reader, executing workspace defaults and configuration variables directly within the main parent process before spinning up the interactive REPL.
# Inside ~/.cshellrc:
# export PROFILE=developer
# clear
cshell:~> echo $PROFILE
developerThe design of cshell centers around absolute memory determinism and safe systems orchestration without third-party library wrappers.
To eliminate heap fragmentation, tracking overhead, and complex pointer bookkeeping, memory requested during tokenisation and pipeline generation is bound to a single execution epoch. Once a pipeline completes its execution lifecycle, the arena offset is reset to zero in a single, deterministic O(1) operation, bypassing individual node destruction loops.
The multi-stage execution engine drives an N-stage rolling file descriptor allocation loop. Instead of generating a massive global matrix of pipes ahead of time, standard descriptors are created and rotated progressively. Child contexts copy active boundaries via dup2, while rigorous parent-process descriptor hygiene explicitly closes unused write ends to guarantee that pipelines never deadlock or leak descriptors.
cshell utilises signal blocking barriers around critical process forks and transferring process harvesting ownership entirely to a synchronous tracking engine. To cleanly support background execution control without deadlocks, every forked pipeline context invokes setpgid across parent and child scopes to establish distinct process groups.
The parent shell explicitly masks out terminal line disruption signals (SIGTSTP, SIGTTIN, SIGTTOU), gracefully surrendering and reclaiming foreground terminal device group boundaries using tcsetpgrp(). Fully intergrated process monitoring occurs cleanly via non-blocking status calls (waitpid(..., WNOHANG | WUNTRACED | WCONTINUED)) executed strictly at the top of the REPL loop, preserving the SA_RESTART integrity of interactive character-by-character input streams.
Created by WillEdgington