Skip to main content Link Search Menu Expand Document (external link)

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 in source/shell.c, and the main() (entry point) is in main.c.
  • source/libs/ contains reusable helper modules shared by the shell, system programs, and tests. For example, perms.c implements permission-formatting helpers, while rc_parser.c implements parsing logic for shell configuration files.
  • source/system_programs/ contains bundled system programs that are compiled into executables under bin/. These programs are intended to be run by cseshell. Each of these programs contain a main() function.
  • bin/ contains compiled system program binaries such as find, ld, and ldr. These are build outputs and can be regenerated by running make.
  • tests/unit/ contains C unit tests for reusable helper modules. These tests use the Unity test framework in tests/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.h that presumably contains necessary constants (like MAX_LINE and MAX_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 cmd array.

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 strtok and stores each word in a dynamically allocated array.
  • Copies the parsed words into the provided cmd array, 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