50.005 Computer System Engineering
Information Systems Technology and Design
Singapore University of Technology and Design
Natalie Agus (Summer 2025)
CSEShell Starter Code
CSEShell is a simple, custom shell for Unix-based systems, designed to provide an interface for executing system programs. This project includes a basic shell framework, a set of system programs (find, ld, ldr), and some test files.
It incorporates a simple prompt display mechanism and the ability to exit the shell. However, it lacks the typical continuous loop for reading and processing commands, as well as the process forking logic (fork) that is commonly used in shell implementations to execute commands in separate processes.
Usage Explanation
Directory Structure
The project is organized as follows:
.[PROJECT_DIR]
├── AGENTS.md
├── bin
│ ├── find
│ ├── ld
│ └── ldr
├── cseshell
├── files
│ ├── combined.txt
│ ├── file1.txt
│ ├── file2.txt
│ ├── intermediate.txt
│ ├── lorem_ipsum.txt
│ ├── notes.pdf
│ ├── oneline.txt
│ ├── paragraph.txt
│ └── ss.png
├── includes
│ ├── libs
│ │ ├── perms.h
│ │ └── rc_parser.h
│ ├── shell.h
│ └── system_program.h
├── Makefile
├── prompts
│ └── generate-unit-tests.md
├── README.md
├── scripts
│ └── gen_unit_tests.sh
├── source
│ ├── libs
│ │ ├── perms.c
│ │ └── rc_parser.c
│ ├── main.c
│ ├── shell.c
│ └── system_programs
│ ├── find.c
│ ├── ld.c
│ └── ldr.c
└── tests
├── integration
│ ├── test_builtin_cd.sh
│ ├── test_builtin_env.sh
│ ├── test_builtin_help.sh
│ ├── test_exit.sh
│ ├── test_loop.sh
│ └── test_system_programs_bundled.sh
├── unit
│ ├── test_perms.c
│ └── test_rc_parser.c
└── unity
├── unity_internals.h
├── unity.c
└── unity.h
The project separates headers, source files, tests, and generated binaries into different folders.
includes/contains header files used by the shell, system programs, and unit tests. Header files should be included relative to this folder. For example:
#include "libs/perms.h"
#include "libs/rc_parser.h"
#include "shell.h"
source/contains the implementation files. The bulk of shell implementation is insource/shell.c, and themain()(entry point) is inmain.c.source/libs/contains reusable helper modules shared by the shell, system programs, and tests. For example,perms.cimplements permission-formatting helpers, whilerc_parser.cimplements parsing logic for shell configuration files.source/system_programs/contains bundled system programs that are compiled into executables underbin/. These programs are intended to be run bycseshell. Each of these programs contain amain()function.bin/contains compiled system program binaries such asfind,ld, andldr. These are build outputs and can be regenerated by runningmake.tests/unit/contains C unit tests for reusable helper modules. These tests use the Unity test framework intests/unity/.tests/integration/contains shell-script-based integration tests. These tests run the compiled shell and bundled system programs to check end-to-end behaviour.files/contains sample input files used for testing commands and system programs.scripts/contains helper scripts, including scripts for generating unit tests.
This layout keeps public interfaces in includes/, implementation code in source/, and tests in tests/. As a result, source files and tests can include headers consistently without using relative paths such as ../libs/perms.h.
Building the Project
To build the CSEShell and system programs, run the following command in the root project directory:
make
This will compile the source code and place the executable files in the appropriate directories.
Running CSEShell
After building, you can start the shell by running:
./cseshell
From there, you can execute built-in commands and any of the included system programs (e.g., find, ld, ldr).

You can type exit to exit the shell:

As the starter code only contains basic shell framework, it lacks the typical continuous loop for reading an processing commaands. The shell will terminate each time you enter a command. To type another command, you need to run ./cseshell again.
Note that only system programs at [PROJECT_DIR]/bin is currently accessible by the shell, and hence only these three commands: find, ld, ldr are supported.
If you try to type any command that’s commonly available on your system’s shell, such as pwd, you will be met with this error message:

System Programs
find.c- Searches for files in a directory.ld.c- List the contents of a directory.ldr.c- List the contents of a directory recursively.
Each program can be executed from the CSEShell once it is running.
Files Directory
The files/ directory contains various text, PDF, and image files for testing the functionality of the CSEShell and its system programs.
Makefile
The Makefile contains rules for compiling the shell and system programs. You can clean the build by running:
make clean
Source Directory
Contains all the necessary source code for the shell and system programs. It is divided into the shell implementation (shell.c, shell.h) and system programs (system_programs/).
Detailed Explanation of shell.c
shell.c contains a basic implementation of a shell that reads commands from the user, parses them, and executes them as processes. It consists of three main functions (read_command, type_prompt, and main). Below is an expanded documentation for each part of the code, including both the existing comments and additional explanations where needed.
Header and Global Definitions
#include "shell.h"
Purpose
Includes the header file
shell.hthat presumably contains necessary constants (likeMAX_LINEandMAX_ARGS) and function declarations related to the shell implementation.
Function: read_command
void read_command(char **cmd)
Purpose
Reads a single command from the standard input (stdin), parses it into arguments, and stores the result in the provided
cmdarray.
Parameters
char **cmd- A pointer to an array of strings, where the command and its arguments will be stored.
Functionality:
- Reads characters from stdin until a newline (
'\n') or the maximum line length (MAX_LINE) is reached. - If the command is too long, it prints an error message and exits.
- Parses the read line into words using
strtokand stores each word in a dynamically allocated array. - Copies the parsed words into the provided
cmdarray, ensuring it’s NULL-terminated.
Function: type_prompt
void type_prompt()
Purpose
Displays the shell prompt to the user.
Functionality:
- If it’s the first time the function is called, clears the screen.
- Prints the prompt (
$$) to the standard output (stdout).
Function: main
int main(void)
Purpose
Implements the main functionality of the shell. It displays a prompt, reads and parses a command, and then executes it.
Error Handling and System Calls
Proper error checking is performed for critical operations like reading from stdin and executing commands using execv system calls. It uses exit(1) to terminate the program upon encountering fatal errors.
// in read_command(char** cmd)
// If the command exceeds the maximum length, print an error and exit
if (count >= MAX_LINE)
{
printf("Command is too long, unable to process\n");
exit(1);
}
...
// in main()
execv(full_path, cmd);
// If execv returns, command execution has failed
printf("Command %s not found\n", cmd[0]);
exit(0);
Portability
Uses preprocessor directives to clear the screen in a way that is compatible with both Windows (cls) and Unix-like systems (clear).
#ifdef _WIN32
system("cls"); // Windows command to clear screen
#else
system("clear"); // UNIX/Linux command to clear screen
#endif
first_time = 0;
Makefile Notes
This project uses a single Makefile to build the main shell executable, the bundled system programs, and the unit/integration tests.
It is important for you to know how it works. Similar Makefile is used for PA2 later on and we will not explain it again.
The compiler is configured using:
CC = gcc
CFLAGS = -I$(INC_DIR) -Wall -Wextra
The -I$(INC_DIR) flag tells the compiler to look for header files inside the includes/ directory. This allows source files and test files to include headers cleanly, for example:
#include "shell.h"
#include "libs/perms.h"
#include "libs/rc_parser.h"
This avoids fragile relative includes such as:
#include "../libs/perms.h"
The main source folders are:
SRC_ROOT = ./source
SRC_DIR = $(SRC_ROOT)/system_programs
LIB_DIR = $(SRC_ROOT)/libs
INC_DIR = ./includes
BIN_DIR = ./bin
SRC_ROOT points to the main source folder.
SRC_DIR points to the bundled system programs.
LIB_DIR points to reusable helper modules.
INC_DIR points to public header files.
BIN_DIR is where compiled system program binaries are placed.
The reusable library source files are discovered automatically:
LIB_SOURCES = $(wildcard $(LIB_DIR)/*.c)
This means every .c file under source/libs/ is compiled into programs that need the shared helper code.
The bundled system programs are also discovered automatically:
SYSTEM_PROGRAM_SOURCES = $(wildcard $(SRC_DIR)/*.c)
SYSTEM_PROGRAM_BINS = $(SYSTEM_PROGRAM_SOURCES:$(SRC_DIR)/%.c=$(BIN_DIR)/%)
For example:
source/system_programs/find.c -> bin/find
source/system_programs/ld.c -> bin/ld
source/system_programs/ldr.c -> bin/ldr
The main shell executable is built from:
MAIN_SOURCES = \
$(wildcard $(SRC_ROOT)/*.c) \
$(LIB_SOURCES)
This compiles every .c file directly under source/, such as main.c and shell.c, together with every reusable helper file under source/libs/.
It intentionally does not compile files under source/system_programs/ into cseshell, because those files are separate programs with their own main() functions.
The main build target is:
all: $(SYSTEM_PROGRAM_BINS) $(MAIN_EXEC)
Running:
make
builds both:
cseshell
bin/find
bin/ld
bin/ldr
The rule for building bundled system programs is:
$(BIN_DIR)/%: $(SRC_DIR)/%.c $(LIB_SOURCES)
@mkdir -p $(BIN_DIR)
$(CC) $(CFLAGS) $^ -o $@
This compiles each system program together with the shared library source files. The $^ variable means “all prerequisites”, and $@ means “the output target”.
The rule for building the main shell is:
$(MAIN_EXEC): $(MAIN_SOURCES)
$(CC) $(CFLAGS) $^ -o $@
This links main.c, shell.c, and the shared library files into the final executable cseshell.
The unit test infrastructure is configured using:
TESTS_DIR = ./tests
UNIT_DIR = $(TESTS_DIR)/unit
UNITY_DIR = $(TESTS_DIR)/unity
UNIT_BIN_DIR = $(UNIT_DIR)/bin
Unit tests are discovered automatically:
UNIT_SOURCES = $(wildcard $(UNIT_DIR)/test_*.c)
UNIT_BINS = $(UNIT_SOURCES:$(UNIT_DIR)/%.c=$(UNIT_BIN_DIR)/%)
So any file named:
tests/unit/test_something.c
is compiled into:
tests/unit/bin/test_something
Unit tests are compiled with:
TEST_CFLAGS = -I$(INC_DIR) -I$(UNITY_DIR) -Wall -Wextra
This lets tests include both project headers and Unity headers:
#include "libs/perms.h"
#include "unity.h"
The unit test build rule is:
$(UNIT_BIN_DIR)/test_%: $(UNIT_DIR)/test_%.c $(UNITY_DIR)/unity.c $(LIB_SOURCES)
@mkdir -p $(UNIT_BIN_DIR)
$(CC) $(TEST_CFLAGS) $^ $(EXTRA_SRC) -o $@
Each unit test is compiled together with Unity and all reusable library source files.
Running:
make unit
builds and runs all unit tests.
Running:
make integration
builds cseshell and the bundled system programs, then runs every shell script under:
tests/integration/
Running:
make test
runs both unit tests and integration tests.
The ai-unit-tests target is a helper target for generating unit tests:
make ai-unit-tests MODULE=rc_parser
This expects the module to have:
includes/libs/rc_parser.h
source/libs/rc_parser.c
and generates:
tests/unit/test_rc_parser.c
The clean target removes generated build outputs:
clean:
rm -f $(SYSTEM_PROGRAM_BINS) $(MAIN_EXEC)
rm -rf $(UNIT_BIN_DIR)
This removes:
cseshell
bin/find
bin/ld
bin/ldr
tests/unit/bin/
In summary, the Makefile follows this structure and you should respect this structure when completing the assignment:
includes/ Header files
source/ Main shell source files
source/libs/ Reusable helper implementations
source/system_programs/ Bundled standalone system programs
tests/unit/ C unit tests
tests/integration/ Shell-script integration tests
tests/unity/ Unity test framework
bin/ Compiled system program binaries
50.005 CSE