Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ set(CMAKE_C_STANDARD 11)

include_directories(include)

set(SOURCES
src/main.c
set(LIB_SOURCES
src/input.c
src/execute.c
src/builtins.c
Expand All @@ -18,4 +17,21 @@ set(HEADERS
include/builtins.h
)

add_executable(shell ${SOURCES} ${HEADERS})
add_executable(shell src/main.c ${LIB_SOURCES} ${HEADERS})

enable_testing()

add_executable(test_input tests/test_input.c src/input.c)
add_test(NAME InputTests COMMAND test_input)

add_executable(test_builtins tests/test_builtins.c src/builtins.c)
add_test(NAME BuiltinsTests COMMAND test_builtins)

add_executable(test_execute tests/test_execute.c src/execute.c src/builtins.c)
add_test(NAME ExecuteTests COMMAND test_execute)

add_custom_target(run_tests
COMMAND ${CMAKE_CTEST_COMMAND} --verbose
DEPENDS test_input test_builtins test_execute
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,63 @@
# Trigger

## Overview
This project aims to create a clean, efficient shell. The current README was written quickly, but the code itself will be written without AI assistance. I plan to build the shell based on solid fundamentals and personal expertise.
This project aims to create a clean, efficient shell. The current README was written quickly, but the code itself will be written without AI assistance - exception being repetitive tasks like folder organization and testing. I plan to build the shell based on solid fundamentals and personal expertise.

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor grammatical issue: "exception being" should be "with the exception of" or "the exception being" for better clarity and grammatical correctness.

Suggested change
This project aims to create a clean, efficient shell. The current README was written quickly, but the code itself will be written without AI assistance - exception being repetitive tasks like folder organization and testing. I plan to build the shell based on solid fundamentals and personal expertise.
This project aims to create a clean, efficient shell. The current README was written quickly, but the code itself will be written without AI assistance—with the exception of repetitive tasks like folder organization and testing. I plan to build the shell based on solid fundamentals and personal expertise.

Copilot uses AI. Check for mistakes.

## Project Structure
The project is now organized into a clean, modular structure:

```
shell/
├── include/ # Header files
│ ├── input.h # Input reading and parsing
│ ├── execute.h # Command execution
│ └── builtins.h # Built-in commands
├── src/ # Source files
│ ├── main.c # Main entry point
│ ├── input.c # Input handling implementation
│ ├── execute.c # Command execution implementation
│ └── builtins.c # Built-in commands implementation
├── tests/ # Unit tests
│ ├── test_framework.h # Simple testing framework
│ ├── test_input.c # Input module tests
│ ├── test_execute.c # Execute module tests
│ └── test_builtins.c # Built-ins module tests
└── CMakeLists.txt # Build configuration

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project structure diagram is missing run_tests.sh, which is a new file added in this PR and mentioned in the Testing section. Consider adding it to the structure diagram for completeness.

Suggested change
── CMakeLists.txt # Build configuration
── CMakeLists.txt # Build configuration
└── run_tests.sh # Test runner script

Copilot uses AI. Check for mistakes.
```

## Building

```bash
cmake -B cmake-build-debug -S .
cmake --build cmake-build-debug
```

## Running

```bash
./cmake-build-debug/shell
```

## Testing

The project includes comprehensive unit tests for all modules. To run the tests:

```bash
# Quick way

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instructions for running the test script don't mention that the script may need to be made executable first with 'chmod +x run_tests.sh'. Consider adding this note to help users who encounter permission errors.

Suggested change
# Quick way
# Quick way
# If you get a permission error, make the script executable first:
# chmod +x run_tests.sh

Copilot uses AI. Check for mistakes.
./run_tests.sh

# Or manually
cd cmake-build-debug
ctest --verbose
```

All tests use a custom lightweight testing framework that provides colored output and clear assertion messages.

## Goals
- Develop a functional, well‑structured shell.
- Ensure the shell is performant and easy to use.
- Continuously improve the project as I learn more.
- Maintain high code quality with unit tests.

## Learning Roadmap
To support development, I’ll study the following topics in my spare time:
Expand All @@ -26,8 +77,11 @@ I’ll start by adapting the simple shell tutorial from Brennan Baker:
This will serve as a solid foundation, which I’ll later expand and customize. The resulting shell, named **Trigger**, will combine the tutorial’s core ideas with additional features and optimizations.

## Project Status
- Initial prototype based on the tutorial is under development.
- Future enhancements will be added incrementally as I deepen my understanding of the relevant topics.
- ✅ Modular architecture with separated concerns
- ✅ Comprehensive unit test coverage
- ✅ Clean project structure with include/ and src/ directories
- 🔄 Basic shell functionality (cd, help, exit, external commands)
- 🔜 Future enhancements will be added incrementally

---

Expand Down
17 changes: 17 additions & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

set -e

echo "Building project..."
cmake -B cmake-build-debug -S .
cmake --build cmake-build-debug

echo ""
echo "Running tests..."
echo "================="
cd cmake-build-debug
ctest --output-on-failure

echo ""
echo "All tests completed successfully!"

84 changes: 84 additions & 0 deletions tests/test_builtins.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "../include/builtins.h"
#include "test_framework.h"

void test_num_builtins() {
int count = trigger_num_builtins();
ASSERT_EQUAL(3, count, "Should have 3 built-in commands");
}

void test_builtin_arrays_match() {
int count = trigger_num_builtins();

// Verify that builtin_str and builtin_func have the same size
ASSERT_TRUE(count > 0, "Should have at least one built-in command");

// Check that known builtins exist
int found_cd = 0, found_help = 0, found_exit = 0;
for (int i = 0; i < count; i++) {
if (strcmp(builtin_str[i], "cd") == 0) found_cd = 1;
if (strcmp(builtin_str[i], "help") == 0) found_help = 1;
if (strcmp(builtin_str[i], "exit") == 0) found_exit = 1;
}

ASSERT_TRUE(found_cd, "Should have 'cd' built-in");
ASSERT_TRUE(found_help, "Should have 'help' built-in");
ASSERT_TRUE(found_exit, "Should have 'exit' built-in");
}

void test_cd_no_args() {
char *args[] = {"cd", NULL};
int result = trigger_cd(args);

// Should return true (1) even when no directory provided
ASSERT_TRUE(result, "cd with no args should return true");
}

void test_cd_to_tmp() {
char cwd_before[1024];
char cwd_after[1024];

getcwd(cwd_before, sizeof(cwd_before));

char *args[] = {"cd", "/tmp", NULL};
int result = trigger_cd(args);

getcwd(cwd_after, sizeof(cwd_after));
Comment on lines +44 to +49

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getcwd call on line 49 could return NULL if the current working directory path exceeds the buffer size or if there's an error. In such cases, cwd_after would contain an indeterminate value, and the subsequent ASSERT_STR_EQUAL on line 52 would have undefined behavior. Consider checking if getcwd returns NULL and handling that case appropriately, or using a larger buffer size.

Suggested change
getcwd(cwd_before, sizeof(cwd_before));
char *args[] = {"cd", "/tmp", NULL};
int result = trigger_cd(args);
getcwd(cwd_after, sizeof(cwd_after));
if (getcwd(cwd_before, sizeof(cwd_before)) == NULL) {
ASSERT_TRUE(0, "getcwd failed to get initial current directory");
return;
}
char *args[] = {"cd", "/tmp", NULL};
int result = trigger_cd(args);
if (getcwd(cwd_after, sizeof(cwd_after)) == NULL) {
ASSERT_TRUE(0, "getcwd failed to get current directory after cd");
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +49

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getcwd call on line 44 could return NULL if the current working directory path exceeds the buffer size or if there's an error. In such cases, cwd_before would contain an indeterminate value, which would cause undefined behavior when used with chdir on line 55. Consider checking if getcwd returns NULL and handling that case appropriately, or using a larger buffer size.

Suggested change
getcwd(cwd_before, sizeof(cwd_before));
char *args[] = {"cd", "/tmp", NULL};
int result = trigger_cd(args);
getcwd(cwd_after, sizeof(cwd_after));
if (getcwd(cwd_before, sizeof(cwd_before)) == NULL) {
ASSERT_TRUE(0, "Failed to get current working directory before cd");
return;
}
char *args[] = {"cd", "/tmp", NULL};
int result = trigger_cd(args);
if (getcwd(cwd_after, sizeof(cwd_after)) == NULL) {
ASSERT_TRUE(0, "Failed to get current working directory after cd");
// Attempt to restore original directory even if checking after cd fails
chdir(cwd_before);
return;
}

Copilot uses AI. Check for mistakes.

ASSERT_TRUE(result, "cd to /tmp should return true");
ASSERT_STR_EQUAL("/tmp", cwd_after, "Should be in /tmp directory");

// Restore original directory
chdir(cwd_before);
}

void test_help_command() {
char *args[] = {"help", NULL};
int result = trigger_help(args);

ASSERT_TRUE(result, "help command should return true");
}

void test_exit_command() {
char *args[] = {"exit", NULL};
int result = trigger_exit(args);

ASSERT_EQUAL(EXIT_SUCCESS, result, "exit command should return EXIT_SUCCESS");
}

int main() {
TEST_SUITE_START("Built-ins Module Tests");

test_num_builtins();
test_builtin_arrays_match();
test_cd_no_args();
test_cd_to_tmp();
test_help_command();
test_exit_command();

TEST_SUITE_END();
}

55 changes: 55 additions & 0 deletions tests/test_execute.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../include/execute.h"
#include "test_framework.h"

void test_execute_empty_command() {
char *args[] = {NULL};
int result = trigger_execute(args);

ASSERT_TRUE(result, "Empty command should return true");
}

void test_execute_builtin_help() {
char *args[] = {"help", NULL};
int result = trigger_execute(args);

ASSERT_TRUE(result, "Execute 'help' should return true");
}

void test_execute_builtin_exit() {
char *args[] = {"exit", NULL};
int result = trigger_execute(args);

ASSERT_EQUAL(EXIT_SUCCESS, result, "Execute 'exit' should return EXIT_SUCCESS");
}

void test_execute_external_command() {
// Test with 'true' command which always succeeds
char *args[] = {"/bin/true", NULL};
int result = trigger_execute(args);

ASSERT_TRUE(result, "Execute '/bin/true' should return true");
}

void test_launch_simple_command() {
// Test launching echo command
char *args[] = {"/bin/echo", "test", NULL};
int result = trigger_launch(args);

ASSERT_TRUE(result, "Launch '/bin/echo test' should return true");
}

int main() {
TEST_SUITE_START("Execute Module Tests");

test_execute_empty_command();
test_execute_builtin_help();
test_execute_builtin_exit();
test_execute_external_command();
test_launch_simple_command();

TEST_SUITE_END();
}

64 changes: 64 additions & 0 deletions tests/test_framework.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#ifndef TEST_FRAMEWORK_H
#define TEST_FRAMEWORK_H

#include <stdio.h>
#include <string.h>

// Test statistics
static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;

// Color codes for output
#define COLOR_GREEN "\033[0;32m"
#define COLOR_RED "\033[0;31m"
#define COLOR_YELLOW "\033[0;33m"
#define COLOR_RESET "\033[0m"

// Assertion macros
#define ASSERT_TRUE(condition, message) \
do { \
tests_run++; \
if (condition) { \
tests_passed++; \
printf(COLOR_GREEN "✓" COLOR_RESET " %s\n", message); \
} else { \
tests_failed++; \
printf(COLOR_RED "✗" COLOR_RESET " %s (line %d)\n", message, __LINE__); \
} \
} while(0)

#define ASSERT_FALSE(condition, message) \
ASSERT_TRUE(!(condition), message)

#define ASSERT_EQUAL(expected, actual, message) \
ASSERT_TRUE((expected) == (actual), message)

#define ASSERT_STR_EQUAL(expected, actual, message) \
ASSERT_TRUE(strcmp((expected), (actual)) == 0, message)

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ASSERT_STR_EQUAL macro calls strcmp directly without checking if either argument is NULL. If either 'expected' or 'actual' is NULL, this will result in undefined behavior (likely a segmentation fault). Consider adding NULL checks or documenting that NULL values should be tested with ASSERT_NULL instead. While current usage is safe, this could be a source of bugs as the test suite grows.

Suggested change
ASSERT_TRUE(strcmp((expected), (actual)) == 0, message)
ASSERT_TRUE((((expected) == NULL && (actual) == NULL) || \
((expected) != NULL && (actual) != NULL && strcmp((expected), (actual)) == 0)), \
message)

Copilot uses AI. Check for mistakes.

#define ASSERT_NOT_NULL(ptr, message) \
ASSERT_TRUE((ptr) != NULL, message)

#define ASSERT_NULL(ptr, message) \
ASSERT_TRUE((ptr) == NULL, message)

// Test suite management
#define TEST_SUITE_START(name) \
printf("\n" COLOR_YELLOW "Running test suite: %s" COLOR_RESET "\n", name); \
tests_run = 0; \
tests_passed = 0; \
tests_failed = 0;

#define TEST_SUITE_END() \
printf("\n"); \
if (tests_failed == 0) { \
printf(COLOR_GREEN "All tests passed! (%d/%d)" COLOR_RESET "\n", tests_passed, tests_run); \
} else { \
printf(COLOR_RED "%d test(s) failed out of %d" COLOR_RESET "\n", tests_failed, tests_run); \
} \
printf("\n"); \
return tests_failed;

#endif // TEST_FRAMEWORK_H

Loading