From 50b6373482e213b942bd5956397f6fbab3f5836b Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 13:57:51 -0500 Subject: [PATCH 01/14] Clang Format ran --- bench/bench_parser.cpp | 8 +- docs/config_di_examples.cpp | 62 ++--- docs/input_source_examples.cpp | 55 ++-- docs/io_di_examples.cpp | 128 ++++----- src/lib/ast/ast_printer.cpp | 57 ++-- src/lib/executor/executor_posix.cpp | 17 +- src/lib/executor/executor_win32.cpp | 71 +++-- src/lib/executor/shell_process_context.cpp | 3 +- src/lib/parser/config.cpp | 28 +- src/lib/parser/input_source.cpp | 26 +- src/lib/parser/lexer.cpp | 21 +- src/lib/parser/output_destination.cpp | 34 ++- src/lib/parser/parser.cpp | 205 ++++++-------- src/main/main.cpp | 23 +- test/line_continuation_tests.cpp | 6 +- test/test_command_parser.cpp | 303 ++++++++------------- 16 files changed, 456 insertions(+), 591 deletions(-) diff --git a/bench/bench_parser.cpp b/bench/bench_parser.cpp index d821e50..8ba94cb 100644 --- a/bench/bench_parser.cpp +++ b/bench/bench_parser.cpp @@ -1,14 +1,14 @@ #include "shell/lexer.hpp" #include "shell/parser.hpp" -#include + #include #include -using namespace wshell; +#include -static void BM_LexParse(benchmark::State& state) { +using namespace wshell; -} +static void BM_LexParse(benchmark::State& state) {} BENCHMARK(BM_LexParse); BENCHMARK_MAIN(); diff --git a/docs/config_di_examples.cpp b/docs/config_di_examples.cpp index b3f1664..45c07d6 100644 --- a/docs/config_di_examples.cpp +++ b/docs/config_di_examples.cpp @@ -5,26 +5,27 @@ // This demonstrates how to use fake/mock sources for testing #include "shell/config.hpp" + #include #include #include // Example 1: Create a fake input source for testing class FakeInputSource : public wshell::IInputSource { -public: + public: explicit FakeInputSource(std::string content, bool should_fail = false) : content_(std::move(content)), should_fail_(should_fail) {} - + std::expected read() override { if (should_fail_) { return std::unexpected("Simulated read failure"); } return content_; } - + std::string source_name() const override { return "fake_source"; } -private: + private: std::string content_; bool should_fail_; }; @@ -32,13 +33,12 @@ class FakeInputSource : public wshell::IInputSource { // Example 2: Using the built-in StringConfigSource void example_string_source() { std::cout << "=== Example: String Input Source ===\n"; - + auto source = std::make_unique( - "VAR1=value1\nVAR2=value2\n# Comment\nVAR3=value3" - ); - + "VAR1=value1\nVAR2=value2\n# Comment\nVAR3=value3"); + auto config = wshell::DefaultConfig::load_from_source(std::move(source)); - + if (config) { std::cout << "Loaded " << config->variables().size() << " variables\n"; for (auto const& [name, value] : config->variables()) { @@ -50,12 +50,12 @@ void example_string_source() { // Example 3: Using the built-in StreamConfigSource void example_stream_source() { std::cout << "\n=== Example: Stream Input Source ===\n"; - + std::istringstream stream("DEBUG=true\nLOG_LEVEL=verbose\n"); wshell::StreamInputSource source(stream, "test_stream"); - + auto config = wshell::DefaultConfig::load_from_source(source); - + if (config) { if (auto val = config->get("DEBUG")) { std::cout << "DEBUG = " << *val << "\n"; @@ -69,19 +69,19 @@ void example_stream_source() { // Example 4: Using custom fake source void example_fake_source() { std::cout << "\n=== Example: Custom Fake Source ===\n"; - + // Test successful read auto source = std::make_unique("TEST=fake_value"); auto config = wshell::DefaultConfig::load_from_source(std::move(source)); - + if (config) { std::cout << "Success: " << config->get("TEST").value() << "\n"; } - + // Test failed read source = std::make_unique("", true); config = wshell::DefaultConfig::load_from_source(std::move(source)); - + if (!config) { std::cout << "Expected failure: " << config.error().message << "\n"; } @@ -90,34 +90,32 @@ void example_fake_source() { // Example 5: Using different validation policies void example_validation_policies() { std::cout << "\n=== Example: Validation Policies ===\n"; - + // Default policy (generous limits) auto default_config = wshell::DefaultConfig::parse("VAR=value"); - std::cout << "Default policy: " - << (default_config ? "SUCCESS" : "FAIL") << "\n"; - + std::cout << "Default policy: " << (default_config ? "SUCCESS" : "FAIL") << "\n"; + // Strict policy (tighter limits) auto strict_config = wshell::StrictConfig::parse("VAR=value"); - std::cout << "Strict policy: " - << (strict_config ? "SUCCESS" : "FAIL") << "\n"; + std::cout << "Strict policy: " << (strict_config ? "SUCCESS" : "FAIL") << "\n"; } // Example 6: Memory-safe API (no dangling pointers) void example_memory_safety() { std::cout << "\n=== Example: Memory-Safe API ===\n"; - + wshell::DefaultConfig config; config.set("TEST", "value"); - + // Old API (UNSAFE - could dangle): // auto* ptr = config.get("TEST"); // Returns raw pointer - + // New API (SAFE - returns copy or optional): auto value = config.get("TEST"); // Returns std::optional if (value) { std::cout << "Safe value: " << *value << "\n"; } - + // Alternative: get_view for efficiency (but document lifetime) auto view = config.get_view("TEST"); // Returns std::optional if (view) { @@ -128,22 +126,22 @@ void example_memory_safety() { // Example 7: Error handling with std::expected and source_location void example_error_handling() { std::cout << "\n=== Example: Error Handling with Source Location ===\n"; - + wshell::DefaultConfig config; - + // set() now returns std::expected auto result = config.set("VALID_VAR", "value"); if (result) { std::cout << "Set succeeded\n"; } - + // Invalid variable name result = config.set("123invalid", "value"); if (!result) { auto const& err = result.error(); std::cout << "Set failed: " << err.message << "\n"; std::cout << "Error code: " << static_cast(err.code) << "\n"; - + // C++23 source_location provides detailed debugging info std::cout << "Error occurred at:\n"; std::cout << " File: " << err.location.file_name() << "\n"; @@ -156,14 +154,14 @@ void example_error_handling() { int main() { std::cout << "Config Class Dependency Injection Examples\n"; std::cout << "==========================================\n\n"; - + example_string_source(); example_stream_source(); example_fake_source(); example_validation_policies(); example_memory_safety(); example_error_handling(); - + std::cout << "\nAll examples completed successfully!\n"; return 0; } diff --git a/docs/input_source_examples.cpp b/docs/input_source_examples.cpp index 123d4a4..594291a 100644 --- a/docs/input_source_examples.cpp +++ b/docs/input_source_examples.cpp @@ -5,6 +5,7 @@ // Demonstrates how the refactored input sources can be used beyond config files #include "shell/input_source.hpp" + #include #include @@ -12,23 +13,23 @@ void example_interactive_shell() { std::cout << "=== Interactive Shell Example ===\n"; std::cout << "Type commands (or 'exit' to quit):\n\n"; - + // Wrap stdin in a StreamInputSource wshell::StreamInputSource stdin_source(std::cin, "stdin"); - + while (true) { std::cout << "wshell> "; std::cout.flush(); - + std::string line; if (!std::getline(std::cin, line)) { break; // EOF or error } - + if (line == "exit") { break; } - + // Process the command (placeholder) std::cout << "Would execute: " << line << "\n"; } @@ -37,29 +38,29 @@ void example_interactive_shell() { /// @brief Example: Batch process commands from a file void example_batch_processing(std::string const& script_path) { std::cout << "\n=== Batch Processing Example ===\n"; - + // Use FileInputSource to read a script file wshell::FileInputSource script_source(script_path); - + auto content = script_source.read(); if (!content) { std::cerr << "Error reading script: " << content.error() << "\n"; return; } - + // Process commands line by line std::istringstream lines(*content); std::string line; int line_num = 0; - + while (std::getline(lines, line)) { ++line_num; - + // Skip empty lines and comments if (line.empty() || line[0] == '#') { continue; } - + std::cout << "[" << line_num << "] Executing: " << line << "\n"; // Execute command here } @@ -68,22 +69,20 @@ void example_batch_processing(std::string const& script_path) { /// @brief Example: Test shell commands with fake input void example_testing_with_string_source() { std::cout << "\n=== Testing Example ===\n"; - + // Simulate user input for testing - wshell::StringInputSource test_input( - "ls -la\n" - "cd /tmp\n" - "echo 'Hello, World!'\n" - "exit\n", - "test_commands" - ); - + wshell::StringInputSource test_input("ls -la\n" + "cd /tmp\n" + "echo 'Hello, World!'\n" + "exit\n", + "test_commands"); + auto commands = test_input.read(); if (commands) { std::cout << "Processing test commands:\n"; std::istringstream lines(*commands); std::string line; - + while (std::getline(lines, line)) { if (!line.empty()) { std::cout << " > " << line << "\n"; @@ -95,13 +94,13 @@ void example_testing_with_string_source() { /// @brief Example: Polymorphic input handling void process_input_source(shell::IInputSource& source) { std::cout << "\n=== Processing input from: " << source.source_name() << " ===\n"; - + auto content = source.read(); if (!content) { std::cerr << "Error: " << content.error() << "\n"; return; } - + std::cout << "Read " << content->size() << " bytes\n"; std::cout << "Content:\n" << *content << "\n"; } @@ -109,22 +108,22 @@ void process_input_source(shell::IInputSource& source) { int main() { std::cout << "Input Source Examples for Shell Usage\n"; std::cout << "=====================================\n\n"; - + // Example 1: Testing with StringInputSource example_testing_with_string_source(); - + // Example 2: Polymorphic processing wshell::StringInputSource str_src("echo 'test'", "inline_command"); process_input_source(str_src); - + // Example 3: Stream from stringstream (simulating stdin) std::istringstream fake_input("ls\npwd\nexit\n"); wshell::StreamInputSource stream_src(fake_input, "fake_stdin"); process_input_source(stream_src); - + std::cout << "\nAll examples completed!\n"; std::cout << "\nNote: Uncomment example_interactive_shell() to try interactive mode\n"; // example_interactive_shell(); // Uncomment for interactive demo - + return 0; } diff --git a/docs/io_di_examples.cpp b/docs/io_di_examples.cpp index 9ea3246..f7a1357 100644 --- a/docs/io_di_examples.cpp +++ b/docs/io_di_examples.cpp @@ -6,6 +6,7 @@ #include "shell/input_source.hpp" #include "shell/output_destination.hpp" + #include #include @@ -13,10 +14,10 @@ namespace shell { /// @brief Simple shell that uses DI for input and output class TestableShell { -public: + public: TestableShell(IInputSource& input, IOutputDestination& output, IOutputDestination& error) : input_(input), output_(output), error_(error) {} - + /// @brief Run the shell: read commands and execute them void run() { auto content = input_.read(); @@ -24,29 +25,29 @@ class TestableShell { error_.write("Error reading input: " + content.error() + "\n"); return; } - + output_.write("Welcome to testable shell!\n"); output_.write("Processing commands from: " + input_.source_name() + "\n\n"); - + std::istringstream lines(*content); std::string line; int line_num = 0; - + while (std::getline(lines, line)) { ++line_num; - + if (line.empty() || line[0] == '#') { continue; // Skip empty lines and comments } - + if (line == "exit") { output_.write("Exiting shell.\n"); break; } - + // Simulate command execution output_.write("[" + std::to_string(line_num) + "] Executing: " + line + "\n"); - + // Simulate some commands if (line.find("error") != std::string::npos) { error_.write("ERROR: Command failed: " + line + "\n"); @@ -54,18 +55,18 @@ class TestableShell { output_.write(" -> Success\n"); } } - + output_.flush(); error_.flush(); } -private: + private: IInputSource& input_; IOutputDestination& output_; IOutputDestination& error_; }; -} // namespace shell +} // namespace shell //============================================================================== // Examples @@ -75,11 +76,11 @@ class TestableShell { void example_production() { std::cout << "=== Production Example ===\n"; std::cout << "Type commands (or 'exit' to quit):\n"; - + wshell::StreamInputSource input(std::cin, "stdin"); wshell::StreamOutputDestination output(std::cout, "stdout"); wshell::StreamOutputDestination error(std::cerr, "stderr"); - + wshell::TestableShell shell(input, output, error); // shell.run(); // Uncomment for interactive demo } @@ -87,30 +88,28 @@ void example_production() { /// @brief Example 2: Testing with fake I/O void example_testing() { std::cout << "\n=== Testing Example ===\n"; - + // Setup fake input - wshell::StringInputSource input( - "# Test script\n" - "ls -la\n" - "cd /tmp\n" - "error command\n" - "echo 'success'\n" - "exit\n", - "test_script" - ); - + wshell::StringInputSource input("# Test script\n" + "ls -la\n" + "cd /tmp\n" + "error command\n" + "echo 'success'\n" + "exit\n", + "test_script"); + // Capture output and errors wshell::StringOutputDestination output("test_output"); wshell::StringOutputDestination error("test_error"); - + // Run the shell wshell::TestableShell shell(input, output, error); shell.run(); - + // Verify output std::cout << "Captured Output:\n"; std::cout << output.captured_output() << "\n"; - + std::cout << "Captured Errors:\n"; std::cout << error.captured_output() << "\n"; } @@ -118,85 +117,76 @@ void example_testing() { /// @brief Example 3: Logging to file while displaying to console void example_logging() { std::cout << "\n=== Logging Example ===\n"; - + // Input from string (simulating user commands) - wshell::StringInputSource input( - "command1\n" - "command2\n" - "error command3\n" - "exit\n", - "commands" - ); - + wshell::StringInputSource input("command1\n" + "command2\n" + "error command3\n" + "exit\n", + "commands"); + // Output to both console and file wshell::StreamOutputDestination console_out(std::cout, "console"); wshell::FileOutputDestination file_out("/tmp/shell_output.log", - wshell::FileOutputDestination::Mode::Truncate); - + wshell::FileOutputDestination::Mode::Truncate); + wshell::StreamOutputDestination error_out(std::cerr, "stderr"); - + // For this example, just use console wshell::TestableShell shell(input, console_out, error_out); shell.run(); - + std::cout << "\nOutput also logged to: /tmp/shell_output.log\n"; } /// @brief Example 4: Unit testing helper class ShellTester { -public: - void run_test(std::string const& commands, - std::string const& expected_output, + public: + void run_test(std::string const& commands, std::string const& expected_output, std::string const& expected_error) { wshell::StringInputSource input(commands, "test"); wshell::StringOutputDestination output("output"); wshell::StringOutputDestination error("error"); - + wshell::TestableShell shell(input, output, error); shell.run(); - + std::cout << "\n=== Test Results ===\n"; std::cout << "Expected output: " << expected_output << "\n"; - std::cout << "Actual output contains: " - << (output.captured_output().find(expected_output) != std::string::npos - ? "YES ✓" : "NO ✗") << "\n"; - + std::cout << "Actual output contains: " + << (output.captured_output().find(expected_output) != std::string::npos ? "YES ✓" + : "NO ✗") + << "\n"; + std::cout << "Expected error: " << expected_error << "\n"; - std::cout << "Actual error contains: " - << (error.captured_output().find(expected_error) != std::string::npos - ? "YES ✓" : "NO ✗") << "\n"; + std::cout << "Actual error contains: " + << (error.captured_output().find(expected_error) != std::string::npos ? "YES ✓" + : "NO ✗") + << "\n"; } }; void example_unit_testing() { std::cout << "\n=== Unit Testing Example ===\n"; - + ShellTester tester; - + // Test successful command - tester.run_test( - "ls\nexit\n", - "Executing: ls", - "" - ); - + tester.run_test("ls\nexit\n", "Executing: ls", ""); + // Test error command - tester.run_test( - "error test\nexit\n", - "Executing: error test", - "ERROR: Command failed" - ); + tester.run_test("error test\nexit\n", "Executing: error test", "ERROR: Command failed"); } int main() { std::cout << "Input/Output Dependency Injection Examples\n"; std::cout << "==========================================\n"; - + example_testing(); example_unit_testing(); - + std::cout << "\nNote: Uncomment example_production() for interactive mode\n"; // example_production(); // Uncomment for interactive demo - + return 0; } diff --git a/src/lib/ast/ast_printer.cpp b/src/lib/ast/ast_printer.cpp index 3592862..d88e66b 100644 --- a/src/lib/ast/ast_printer.cpp +++ b/src/lib/ast/ast_printer.cpp @@ -1,4 +1,5 @@ #include "shell/ast_printer.hpp" + #include namespace wshell { @@ -17,9 +18,15 @@ void indent(std::ostream& os, int level) { void print_redirection(const Redirection& r, std::ostream& os, int indent_level) { indent(os, indent_level); switch (r.kind) { - case RedirectKind::Input: os << "< "; break; - case RedirectKind::OutputTruncate: os << "> "; break; - case RedirectKind::OutputAppend: os << ">> "; break; + case RedirectKind::Input: + os << "< "; + break; + case RedirectKind::OutputTruncate: + os << "> "; + break; + case RedirectKind::OutputAppend: + os << ">> "; + break; } os << r.target << "\n"; } @@ -69,34 +76,36 @@ void print_sequence(const SequenceNode& seq, std::ostream& os, int indent_level) // ----------------------------------------------------------------------------- void print_node(const StatementNode& stmt, std::ostream& os, int indent_level) { - std::visit([&](auto const& node) { - using T = std::decay_t; + std::visit( + [&](auto const& node) { + using T = std::decay_t; - if constexpr (std::is_same_v) { - indent(os, indent_level); - os << "Comment: " << node.text << "\n"; + if constexpr (std::is_same_v) { + indent(os, indent_level); + os << "Comment: " << node.text << "\n"; - } else if constexpr (std::is_same_v) { - indent(os, indent_level); - os << "Assignment: " << node.variable << " = " << node.value << "\n"; + } else if constexpr (std::is_same_v) { + indent(os, indent_level); + os << "Assignment: " << node.variable << " = " << node.value << "\n"; - } else if constexpr (std::is_same_v) { - print_command(node, os, indent_level); + } else if constexpr (std::is_same_v) { + print_command(node, os, indent_level); - } else if constexpr (std::is_same_v) { - print_pipeline(node, os, indent_level); + } else if constexpr (std::is_same_v) { + print_pipeline(node, os, indent_level); - } else if constexpr (std::is_same_v) { - print_sequence(node, os, indent_level); + } else if constexpr (std::is_same_v) { + print_sequence(node, os, indent_level); - } else { - indent(os, indent_level); - os << "\n"; - } - }, stmt); + } else { + indent(os, indent_level); + os << "\n"; + } + }, + stmt); } -} // namespace +} // namespace // ----------------------------------------------------------------------------- // Public API @@ -123,4 +132,4 @@ std::string to_string(const ProgramNode& program) { return oss.str(); } -} // namespace wshell \ No newline at end of file +} // namespace wshell \ No newline at end of file diff --git a/src/lib/executor/executor_posix.cpp b/src/lib/executor/executor_posix.cpp index 28dd0cb..87fb149 100644 --- a/src/lib/executor/executor_posix.cpp +++ b/src/lib/executor/executor_posix.cpp @@ -37,7 +37,6 @@ std::optional get_home_directory() { return std::nullopt; } - namespace fs = std::filesystem; // Function to find the full path of an executable by searching the PATH environment variable @@ -62,7 +61,7 @@ std::string findExecutableInPath(const std::string& executable_name) { current_path += c; } } - directories.push_back(current_path); // Add the last directory + directories.push_back(current_path); // Add the last directory // Search each directory for the executable for (const std::string& dir : directories) { @@ -70,7 +69,8 @@ std::string findExecutableInPath(const std::string& executable_name) { // Check if the file exists and is executable // The X_OK flag in access() checks for execute permission - if (fs::exists(full_path) && fs::is_regular_file(full_path) && access(full_path.c_str(), X_OK) == 0) { + if (fs::exists(full_path) && fs::is_regular_file(full_path) && + access(full_path.c_str(), X_OK) == 0) { return full_path.string(); } } @@ -134,8 +134,7 @@ class EnvironmentCache { std::mutex mutex_; }; -std::vector -PlatformExecutionPolicy::convertEnvironment(const Command& cmd) { +std::vector PlatformExecutionPolicy::convertEnvironment(const Command& cmd) { // Set up environment variables std::unordered_map env_map; env_map.insert(cmd.env.begin(), cmd.env.end()); @@ -146,15 +145,14 @@ PlatformExecutionPolicy::convertEnvironment(const Command& cmd) { } std::vector envp; for (const auto& arg : env_map) { - envp.push_back((arg.first+ "=" + arg.second).c_str()); + envp.push_back((arg.first + "=" + arg.second).c_str()); } envp.push_back(nullptr); // NULL-terminated return envp; } -std::vector -PlatformExecutionPolicy::convertArgv(const Command& cmd) { +std::vector PlatformExecutionPolicy::convertArgv(const Command& cmd) { // Convert command args to C-style argv std::vector argv; argv.push_back(cmd.executable.c_str()); @@ -196,7 +194,6 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { // Open file and redirect stdin (TODO) } if (std::holds_alternative(cmd.stdout_)) { - const auto& file_target = std::get(cmd.stdout_); std::cout << "Redirecting stdout to file: " << file_target.path.c_str() << std::endl; // TODO umask @@ -265,7 +262,7 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { } std::cout << std::endl; */ - //lookup executable in PATH if not an absolute or relative path + // lookup executable in PATH if not an absolute or relative path std::string found_path = findExecutableInPath(cmd.executable); auto rc = execve(found_path.c_str(), const_cast(argv.data()), static_cast(envp.data())); diff --git a/src/lib/executor/executor_win32.cpp b/src/lib/executor/executor_win32.cpp index 03313e4..dcb96ac 100644 --- a/src/lib/executor/executor_win32.cpp +++ b/src/lib/executor/executor_win32.cpp @@ -5,9 +5,11 @@ #if defined(_WIN32) -#include "shell/execution_policy.hpp" -#include -#include + #include "shell/execution_policy.hpp" + + #include + + #include namespace wshell { @@ -41,7 +43,7 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { cmdline << " " << arg; } std::string cmdline_str = cmdline.str(); - + // Setup process startup info STARTUPINFOA si{}; si.cb = sizeof(si); @@ -49,64 +51,57 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); si.hStdError = GetStdHandle(STD_ERROR_HANDLE); - + PROCESS_INFORMATION pi{}; - + // Create process // Note: CreateProcessA modifies the command line, so we need a mutable copy std::vector cmdline_buf(cmdline_str.begin(), cmdline_str.end()); cmdline_buf.push_back('\0'); - - BOOL success = CreateProcessA( - nullptr, // Application name (use command line) - cmdline_buf.data(), // Command line - nullptr, // Process security attributes - nullptr, // Thread security attributes - TRUE, // Inherit handles - 0, // Creation flags - nullptr, // Environment - nullptr, // Current directory - &si, // Startup info - &pi // Process information + + BOOL success = CreateProcessA(nullptr, // Application name (use command line) + cmdline_buf.data(), // Command line + nullptr, // Process security attributes + nullptr, // Thread security attributes + TRUE, // Inherit handles + 0, // Creation flags + nullptr, // Environment + nullptr, // Current directory + &si, // Startup info + &pi // Process information ); - + if (!success) { DWORD error; printWindowsErrMsg(error); - return ExecutionResult{ - .exit_code = platform::EXIT_FAILURE_STATUS, - .error_message = "Failed to create process (error " + std::to_string(error) + ")" - }; + return ExecutionResult{.exit_code = platform::EXIT_FAILURE_STATUS, + .error_message = "Failed to create process (error " + + std::to_string(error) + ")"}; } - + // Wait for process to complete WaitForSingleObject(pi.hProcess, INFINITE); - + // Get exit code DWORD exit_code = platform::EXIT_FAILURE_STATUS; GetExitCodeProcess(pi.hProcess, &exit_code); - + // Clean up handles CloseHandle(pi.hProcess); CloseHandle(pi.hThread); - - return ExecutionResult{ - .exit_code = static_cast(exit_code), - .error_message = std::nullopt - }; + + return ExecutionResult{.exit_code = static_cast(exit_code), .error_message = std::nullopt}; } ExecutionResult PlatformExecutionPolicy::execute(const Pipeline& pipeline) const { // Phase 1: No pipeline support yet - just execute first command if (pipeline.empty()) { - return ExecutionResult{ - .exit_code = platform::EXIT_FAILURE_STATUS, - .error_message = "Empty pipeline" - }; + return ExecutionResult{.exit_code = platform::EXIT_FAILURE_STATUS, + .error_message = "Empty pipeline"}; } - + // For now, just execute the first command // Phase 2 will add proper pipeline support return execute(pipeline.commands[0]); @@ -117,6 +112,6 @@ void PlatformExecutionPolicy::init_job_control() const { // Would use Job Objects here if needed } -} // namespace wshell +} // namespace wshell -#endif // _WIN32 +#endif // _WIN32 diff --git a/src/lib/executor/shell_process_context.cpp b/src/lib/executor/shell_process_context.cpp index 9b19565..9bc8123 100644 --- a/src/lib/executor/shell_process_context.cpp +++ b/src/lib/executor/shell_process_context.cpp @@ -5,7 +5,6 @@ #include "shell/shell_process_context.h" static ShellProcessContext ctx; -ShellProcessContext & shell_process_context() { +ShellProcessContext& shell_process_context() { return ctx; } - diff --git a/src/lib/parser/config.cpp b/src/lib/parser/config.cpp index 916eb58..14c4dc3 100644 --- a/src/lib/parser/config.cpp +++ b/src/lib/parser/config.cpp @@ -2,15 +2,17 @@ // SPDX-License-Identifier: BSD-2-Clause #include "shell/config.hpp" + #include "shell/input_source.hpp" + #include #include #include #ifdef _WIN32 -#include + #include #else -#include -#include + #include + #include #endif namespace wshell { @@ -40,11 +42,9 @@ bool DefaultValidationPolicy::is_valid_name(std::string_view name) { return true; } -bool DefaultValidationPolicy::check_limits(std::size_t name_len, - std::size_t value_len, +bool DefaultValidationPolicy::check_limits(std::size_t name_len, std::size_t value_len, std::size_t var_count) { - return name_len <= MAX_NAME_LENGTH && - value_len <= MAX_VALUE_LENGTH && + return name_len <= MAX_NAME_LENGTH && value_len <= MAX_VALUE_LENGTH && var_count < MAX_VARIABLE_COUNT; } @@ -53,15 +53,13 @@ bool StrictValidationPolicy::is_valid_name(std::string_view name) { return DefaultValidationPolicy::is_valid_name(name); } -bool StrictValidationPolicy::check_limits(std::size_t name_len, - std::size_t value_len, +bool StrictValidationPolicy::check_limits(std::size_t name_len, std::size_t value_len, std::size_t var_count) { - return name_len <= MAX_NAME_LENGTH && - value_len <= MAX_VALUE_LENGTH && + return name_len <= MAX_NAME_LENGTH && value_len <= MAX_VALUE_LENGTH && var_count < MAX_VARIABLE_COUNT; } -template +template void Config::showEnvironmentVariables() { std::ranges::for_each(variables_, [](auto const& kv) { std::cout << std::format("{:<20} = {}\n", kv.first, kv.second); @@ -72,10 +70,10 @@ void Config::showEnvironmentVariables() { // Static Helper Functions //============================================================================== -template +template std::filesystem::path Config::default_config_path() { std::filesystem::path home; - + #ifdef _WIN32 // Use Windows-safe _dupenv_s char* userprofile = nullptr; @@ -114,4 +112,4 @@ std::filesystem::path Config::default_config_path() { template class Config; template class Config; -} // namespace shell +} // namespace wshell diff --git a/src/lib/parser/input_source.cpp b/src/lib/parser/input_source.cpp index 5e27418..a2d0e9d 100644 --- a/src/lib/parser/input_source.cpp +++ b/src/lib/parser/input_source.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-2-Clause #include "shell/input_source.hpp" + #include #include @@ -11,8 +12,7 @@ namespace wshell { // FileInputSource Implementation //============================================================================== -FileInputSource::FileInputSource(std::filesystem::path path) - : path_(std::move(path)) {} +FileInputSource::FileInputSource(std::filesystem::path path) : path_(std::move(path)) {} std::expected FileInputSource::read() { // Check file existence @@ -39,7 +39,7 @@ std::expected FileInputSource::read() { std::stringstream buffer; buffer << file.rdbuf(); - + if (file.fail() && !file.eof()) { return std::unexpected("Error reading file"); } @@ -60,32 +60,32 @@ StreamInputSource::StreamInputSource(std::istream& stream, std::string name) std::expected StreamInputSource::read() { std::stringstream buffer; - + // Read with size limit std::size_t total_read = 0; char chunk[4096]; - + while (stream_.read(chunk, sizeof(chunk)) || stream_.gcount() > 0) { auto count = stream_.gcount(); total_read += count; - + if (total_read > MAX_STREAM_SIZE) { return std::unexpected("Stream exceeds maximum size (1MB)"); } - + buffer.write(chunk, count); } - + if (stream_.bad()) { return std::unexpected("Error reading from stream"); } - + return buffer.str(); } std::expected StreamInputSource::read_line() { std::string line; - + if (!std::getline(stream_, line)) { if (stream_.eof()) { return std::unexpected("End of input"); @@ -95,13 +95,13 @@ std::expected StreamInputSource::read_line() { } return std::unexpected("Failed to read line"); } - + // Security: Check line length static constexpr std::size_t MAX_LINE_SIZE = 10'240; // 10KB per line if (line.size() > MAX_LINE_SIZE) { return std::unexpected("Line exceeds maximum size (10KB)"); } - + return line; } @@ -124,4 +124,4 @@ std::string StringInputSource::source_name() const { return name_; } -} // namespace shell +} // namespace wshell diff --git a/src/lib/parser/lexer.cpp b/src/lib/parser/lexer.cpp index f4323a4..9315b98 100644 --- a/src/lib/parser/lexer.cpp +++ b/src/lib/parser/lexer.cpp @@ -2,22 +2,26 @@ // SPDX-License-Identifier: BSD-2-Clause #include "shell/lexer.hpp" + #include namespace wshell { char Lexer::current() const noexcept { - if (is_at_end()) return '\0'; + if (is_at_end()) + return '\0'; return source_[position_]; } char Lexer::peek(std::size_t offset) const noexcept { - if (position_ + offset >= source_.size()) return '\0'; + if (position_ + offset >= source_.size()) + return '\0'; return source_[position_ + offset]; } void Lexer::advance() noexcept { - if (is_at_end()) return; + if (is_at_end()) + return; if (current() == '\n') { line_++; @@ -40,7 +44,7 @@ Token Lexer::make_token(TokenType type, std::string value) { Token Lexer::lex_comment() { std::size_t start = position_; - advance(); // Skip '#' + advance(); // Skip '#' while (!is_at_end() && current() != '\n') { advance(); @@ -64,12 +68,11 @@ Token Lexer::lex_word() { std::size_t start = position_; auto is_operator = [&](char ch) { - return ch == '=' || ch == '#' || ch == '|' || ch == '&' || - ch == ';' || ch == '<' || ch == '>'; + return ch == '=' || ch == '#' || ch == '|' || ch == '&' || ch == ';' || ch == '<' || + ch == '>'; }; - while (!is_at_end() && - !std::isspace(static_cast(current())) && + while (!is_at_end() && !std::isspace(static_cast(current())) && !is_operator(current())) { advance(); } @@ -180,4 +183,4 @@ std::vector Lexer::tokenize(std::string_view source) { return tokens; } -} // namespace wshell \ No newline at end of file +} // namespace wshell \ No newline at end of file diff --git a/src/lib/parser/output_destination.cpp b/src/lib/parser/output_destination.cpp index fe9ac28..064c389 100644 --- a/src/lib/parser/output_destination.cpp +++ b/src/lib/parser/output_destination.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-2-Clause #include "shell/output_destination.hpp" + #include namespace wshell { @@ -15,21 +16,21 @@ StreamOutputDestination::StreamOutputDestination(std::ostream& stream, std::stri std::expected StreamOutputDestination::write(std::string_view content) { stream_ << content; - + if (stream_.fail()) { return std::unexpected("Failed to write to stream"); } - + return {}; } std::expected StreamOutputDestination::flush() { stream_.flush(); - + if (stream_.fail()) { return std::unexpected("Failed to flush stream"); } - + return {}; } @@ -41,8 +42,7 @@ std::string StreamOutputDestination::destination_name() const { // StringOutputDestination Implementation //============================================================================== -StringOutputDestination::StringOutputDestination(std::string name) - : name_(std::move(name)) {} +StringOutputDestination::StringOutputDestination(std::string name) : name_(std::move(name)) {} std::expected StringOutputDestination::write(std::string_view content) { try { @@ -68,11 +68,9 @@ std::string StringOutputDestination::destination_name() const { FileOutputDestination::FileOutputDestination(std::filesystem::path path, Mode mode) : path_(std::move(path)) { - - auto open_mode = (mode == Mode::Append) - ? (std::ios::out | std::ios::app) - : (std::ios::out | std::ios::trunc); - + auto open_mode = (mode == Mode::Append) ? (std::ios::out | std::ios::app) + : (std::ios::out | std::ios::trunc); + stream_ = std::make_unique(path_, open_mode); } @@ -86,13 +84,13 @@ std::expected FileOutputDestination::write(std::string_view c if (!stream_) { return std::unexpected("File not open: " + path_.string()); } - + *stream_ << content; - + if (stream_->fail()) { return std::unexpected("Failed to write to file: " + path_.string()); } - + return {}; } @@ -100,13 +98,13 @@ std::expected FileOutputDestination::flush() { if (!stream_) { return std::unexpected("File not open: " + path_.string()); } - + stream_->flush(); - + if (stream_->fail()) { return std::unexpected("Failed to flush file: " + path_.string()); } - + return {}; } @@ -114,4 +112,4 @@ std::string FileOutputDestination::destination_name() const { return path_.string(); } -} // namespace shell +} // namespace wshell diff --git a/src/lib/parser/parser.cpp b/src/lib/parser/parser.cpp index c64ae7c..622c9ee 100644 --- a/src/lib/parser/parser.cpp +++ b/src/lib/parser/parser.cpp @@ -2,24 +2,28 @@ // SPDX-License-Identifier: BSD-2-Clause #include "shell/parser.hpp" -#include "shell/lexer.hpp" + #include "shell/ast.hpp" #include "shell/ast_printer.hpp" +#include "shell/lexer.hpp" -#include #include +#include + namespace wshell { namespace { RedirectKind redirect_kind_from_lexeme(const std::string& s) { - if (s == ">") return RedirectKind::OutputTruncate; - if (s == ">>") return RedirectKind::OutputAppend; - return RedirectKind::Input; // "<" + if (s == ">") + return RedirectKind::OutputTruncate; + if (s == ">>") + return RedirectKind::OutputAppend; + return RedirectKind::Input; // "<" } -} // namespace +} // namespace // ----------------------------------------------------------------------------- // Token helpers @@ -40,7 +44,7 @@ bool Parser::check(TokenType type) { bool Parser::match(TokenType type) { Token t = lexer_.peek_token(); if (t.type == type) { - (void)lexer_.next_token(); // consume + (void)lexer_.next_token(); // consume return true; } return false; @@ -65,14 +69,12 @@ ParseError Parser::make_error(ParseErrorKind theKind, const std::string& msg) { // Comments // ----------------------------------------------------------------------------- -std::expected -Parser::parse_comment() { +std::expected Parser::parse_comment() { if (!check(TokenType::Comment)) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected comment")); + return std::unexpected(make_error(ParseErrorKind::SyntaxError, "Expected comment")); } - Token tok = lexer_.next_token(); // consume the comment token + Token tok = lexer_.next_token(); // consume the comment token return make_comment(tok.value); } @@ -80,27 +82,25 @@ Parser::parse_comment() { // Assignments: let x = value // ----------------------------------------------------------------------------- -std::expected -Parser::parse_assignment() { +std::expected Parser::parse_assignment() { // 'let' if (!match(TokenType::Let)) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected 'let' keyword")); + return std::unexpected(make_error(ParseErrorKind::SyntaxError, "Expected 'let' keyword")); } // identifier if (!check(TokenType::Identifier)) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected variable name after 'let'")); + return std::unexpected( + make_error(ParseErrorKind::SyntaxError, "Expected variable name after 'let'")); } Token var_tok = current_token(); std::string variable = var_tok.value; - (void)lexer_.next_token(); // consume identifier + (void)lexer_.next_token(); // consume identifier // '=' if (!match(TokenType::Equals)) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected '=' after variable name")); + return std::unexpected( + make_error(ParseErrorKind::SyntaxError, "Expected '=' after variable name")); } // skip comments immediately after '=' @@ -109,36 +109,29 @@ Parser::parse_assignment() { } if (check(TokenType::Semicolon)) { - return std::unexpected(make_error( - ParseErrorKind::SyntaxError, - "Expected expression after '='" - )); + return std::unexpected( + make_error(ParseErrorKind::SyntaxError, "Expected expression after '='")); } // detect incomplete input: let x =, let x = , let x = # comment if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { - return std::unexpected(make_error( - ParseErrorKind::IncompleteInput, - "Expected expression after '='" - )); + return std::unexpected( + make_error(ParseErrorKind::IncompleteInput, "Expected expression after '='")); } // collect value tokens until newline/EOF/semicolon std::string value; - while (!check(TokenType::Newline) && - !check(TokenType::EndOfFile) && + while (!check(TokenType::Newline) && !check(TokenType::EndOfFile) && !check(TokenType::Semicolon)) { - Token t = current_token(); - if (!value.empty()) value.push_back(' '); + if (!value.empty()) + value.push_back(' '); value += t.value; - (void)lexer_.next_token(); // consume + (void)lexer_.next_token(); // consume } if (value.empty()) { - return std::unexpected(make_error( - ParseErrorKind::IncompleteInput, - "Expected expression after '='" - )); + return std::unexpected( + make_error(ParseErrorKind::IncompleteInput, "Expected expression after '='")); } return make_assignment(std::move(variable), std::move(value)); @@ -148,55 +141,46 @@ Parser::parse_assignment() { // Redirections: <, >, >> (attach to a command) // ----------------------------------------------------------------------------- -std::expected -Parser::parse_redirection() { +std::expected Parser::parse_redirection() { if (!check(TokenType::Redirect)) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected redirection operator")); + return std::unexpected( + make_error(ParseErrorKind::SyntaxError, "Expected redirection operator")); } - Token op = lexer_.next_token(); // consume operator + Token op = lexer_.next_token(); // consume operator // Detect incomplete input: "cmd >" or "cmd <" if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { - return std::unexpected(ParseError{ - ParseErrorKind::SyntaxError, - "Expected redirection target", - op.line, - op.column - }); + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Expected redirection target", op.line, op.column}); } Token target = current_token(); // Comment after redirect is a syntax error, not incomplete input if (target.type == TokenType::Comment) { - return std::unexpected(make_error( - ParseErrorKind::SyntaxError, - "Expected redirection target" - )); + return std::unexpected( + make_error(ParseErrorKind::SyntaxError, "Expected redirection target")); } if (target.type != TokenType::Identifier) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected redirection target")); + return std::unexpected( + make_error(ParseErrorKind::SyntaxError, "Expected redirection target")); } - (void)lexer_.next_token(); // consume target + (void)lexer_.next_token(); // consume target - return Redirection{ redirect_kind_from_lexeme(op.value), target.value }; + return Redirection{redirect_kind_from_lexeme(op.value), target.value}; } // ----------------------------------------------------------------------------- // Simple command: name + args (no redirects, no &, no |, no ;) // ----------------------------------------------------------------------------- -std::expected -Parser::parse_simple_command() { +std::expected Parser::parse_simple_command() { Token cmd_tok = current_token(); if (cmd_tok.type != TokenType::Identifier) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Expected command name")); + return std::unexpected(make_error(ParseErrorKind::SyntaxError, "Expected command name")); } std::string name = cmd_tok.value; @@ -206,17 +190,13 @@ Parser::parse_simple_command() { (void)lexer_.next_token(); // collect arguments until a control token - while (!check(TokenType::Newline) && - !check(TokenType::EndOfFile) && - !check(TokenType::Pipe) && - !check(TokenType::Semicolon) && - !check(TokenType::Background) && + while (!check(TokenType::Newline) && !check(TokenType::EndOfFile) && !check(TokenType::Pipe) && + !check(TokenType::Semicolon) && !check(TokenType::Background) && !check(TokenType::Redirect)) { - Token t = current_token(); if (t.type == TokenType::Identifier || t.type == TokenType::Equals) { args.push_back(t.value); - (void)lexer_.next_token(); // consume the argument + (void)lexer_.next_token(); // consume the argument } else { break; } @@ -229,8 +209,7 @@ Parser::parse_simple_command() { // Full command: simple + redirections + optional background // ----------------------------------------------------------------------------- -std::expected -Parser::parse_command() { +std::expected Parser::parse_command() { auto simple = parse_simple_command(); if (!simple) { return std::unexpected(simple.error()); @@ -259,8 +238,7 @@ Parser::parse_command() { // Pipeline: cmd ('|' cmd)* // ----------------------------------------------------------------------------- -std::expected -Parser::parse_pipeline() { +std::expected Parser::parse_pipeline() { auto first = parse_command(); if (!first) { return std::unexpected(first.error()); @@ -270,9 +248,8 @@ Parser::parse_pipeline() { cmds.push_back(std::move(*first)); while (check(TokenType::Pipe)) { - Token pipe_tok = peek_token(); - [[maybe_unused]] bool consumed = match(TokenType::Pipe); // consume '|' + [[maybe_unused]] bool consumed = match(TokenType::Pipe); // consume '|' // Skip comments after a pipe while (match(TokenType::Comment)) { @@ -281,12 +258,9 @@ Parser::parse_pipeline() { // Detect incomplete input: "cmd |", "cmd | # comment" if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { - return std::unexpected(ParseError{ - ParseErrorKind::IncompleteInput, - "Expected command after '|'", - pipe_tok.line, - pipe_tok.column - }); + return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, + "Expected command after '|'", pipe_tok.line, + pipe_tok.column}); } auto next = parse_command(); @@ -298,19 +272,18 @@ Parser::parse_pipeline() { } if (cmds.size() == 1) { - return StatementNode{ std::move(cmds.front()) }; + return StatementNode{std::move(cmds.front())}; } PipelineNode pipeline = make_pipeline(std::move(cmds)); - return StatementNode{ std::move(pipeline) }; + return StatementNode{std::move(pipeline)}; } // ----------------------------------------------------------------------------- // List / sequence: pipeline (';' pipeline)* // ----------------------------------------------------------------------------- -std::expected -Parser::parse_list() { +std::expected Parser::parse_list() { auto first_pipe = parse_pipeline(); if (!first_pipe.has_value()) { return std::unexpected(first_pipe.error()); @@ -326,17 +299,13 @@ Parser::parse_list() { stmts.push_back(std::move(*first_pipe)); while (match(TokenType::Semicolon)) { - // Detect incomplete input: "cmd ;" if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { if (repl_mode_) { // In REPL, semicolon at end means incomplete input - return std::unexpected(ParseError{ - ParseErrorKind::IncompleteInput, - "Expected command after ';'", - peek_token().line, - peek_token().column - }); + return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, + "Expected command after ';'", peek_token().line, + peek_token().column}); } else { // In scripts or multi-statement sequences, trailing semicolon is allowed break; @@ -349,34 +318,28 @@ Parser::parse_list() { } // After parsing a pipeline inside a list, check for dangling operators - if (check(TokenType::Pipe) || - check(TokenType::Redirect)) { - return std::unexpected(ParseError{ - ParseErrorKind::IncompleteInput, - "Incomplete command at end of line", - peek_token().line, - peek_token().column - }); + if (check(TokenType::Pipe) || check(TokenType::Redirect)) { + return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, + "Incomplete command at end of line", + peek_token().line, peek_token().column}); } stmts.push_back(std::move(*next_pipe)); } SequenceNode seq = make_sequence(std::move(stmts)); - return StatementNode{ std::move(seq) }; + return StatementNode{std::move(seq)}; } // ----------------------------------------------------------------------------- // Statement: comment | assignment | list(pipeline/sequence/command) // ----------------------------------------------------------------------------- -std::expected -Parser::parse_statement() { +std::expected Parser::parse_statement() { skip_newlines(); if (check(TokenType::EndOfFile)) { - return std::unexpected(make_error(ParseErrorKind::SyntaxError, - "Unexpected end of input")); + return std::unexpected(make_error(ParseErrorKind::SyntaxError, "Unexpected end of input")); } // Comment @@ -385,7 +348,7 @@ Parser::parse_statement() { if (!c) { return std::unexpected(c.error()); } - return StatementNode{ std::move(*c) }; + return StatementNode{std::move(*c)}; } // Assignment @@ -394,7 +357,7 @@ Parser::parse_statement() { if (!a) { return std::unexpected(a.error()); } - return StatementNode{ std::move(*a) }; + return StatementNode{std::move(*a)}; } // Command / Pipeline / Sequence @@ -410,8 +373,7 @@ Parser::parse_statement() { // Program: many statements (for full scripts) // ----------------------------------------------------------------------------- -std::expected, ParseError> -Parser::parse_program() { +std::expected, ParseError> Parser::parse_program() { auto program = std::make_unique(); skip_newlines(); @@ -431,8 +393,7 @@ Parser::parse_program() { // ----------------------------------------------------------------------------- // parse_line: a single logical line (for REPL) // ----------------------------------------------------------------------------- -std::expected, ParseError> -Parser::parse_line() { +std::expected, ParseError> Parser::parse_line() { auto program = std::make_unique(); // Empty input is fine @@ -463,28 +424,20 @@ Parser::parse_line() { Token last = peek_token(); // Dangling operator at end of line → continuation - if (last.type == TokenType::Pipe || - last.type == TokenType::Redirect || + if (last.type == TokenType::Pipe || last.type == TokenType::Redirect || last.type == TokenType::Semicolon) { - - return std::unexpected(ParseError{ - ParseErrorKind::IncompleteInput, - "Incomplete command at end of line", - last.line, - last.column - }); + return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, + "Incomplete command at end of line", last.line, + last.column}); } // Otherwise it's a real syntax error - return std::unexpected(ParseError{ - ParseErrorKind::SyntaxError, - "Unexpected tokens after statement", - last.line, - last.column - }); + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Unexpected tokens after statement", last.line, + last.column}); } return program; } -} // namespace wshell \ No newline at end of file +} // namespace wshell \ No newline at end of file diff --git a/src/main/main.cpp b/src/main/main.cpp index 3a61814..a4d78af 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -15,7 +15,6 @@ #include int main(int argc, char* argv[]) { - std::cout << "wshell version " << wshell::version() << "\n"; ShellProcessContext ctx = ShellProcessContext(); @@ -29,10 +28,11 @@ int main(int argc, char* argv[]) { } wshell::Config config; - if ( std::filesystem::exists(config_path) ) { + if (std::filesystem::exists(config_path)) { std::cout << "Loading configuration from " << config_path.string() << "\n"; - wshell::FileInputSource * file_source = new wshell::FileInputSource(config_path); - auto config_result = config.loadFromSource(std::unique_ptr(file_source)); + wshell::FileInputSource* file_source = new wshell::FileInputSource(config_path); + auto config_result = + config.loadFromSource(std::unique_ptr(file_source)); if (!config_result) { std::cerr << "Error loading config: " << config_result.error().message << "\n"; } @@ -42,13 +42,12 @@ int main(int argc, char* argv[]) { } std::span args(argv, static_cast(argc)); - //parse args and set flags + // parse args and set flags auto command_args = args.subspan(1); if (!command_args.empty()) { - //parse args and set flags + // parse args and set flags } else { - wshell::StreamInputSource stdin_source(std::cin, "stdin"); wshell::StreamOutputDestination stdout_dest(std::cout, "stdout"); wshell::StreamOutputDestination stderr_dest(std::cerr, "stderr"); @@ -56,10 +55,10 @@ int main(int argc, char* argv[]) { auto prompt = config.get("PS1").value_or("wshell> "); auto cont_prompt = config.get("PS2").value_or("> "); - wshell::ShellInterpreter interpreter(stdout_dest, stderr_dest); + wshell::ShellInterpreter interpreter(stdout_dest, + stderr_dest); while (true) { - std::string full_input; // --- First prompt (PS1) --- @@ -92,8 +91,7 @@ int main(int argc, char* argv[]) { // --- Handle continuation --- while (!parse_result && - parse_result.error().kind_ == wshell::ParseErrorKind::IncompleteInput) - { + parse_result.error().kind_ == wshell::ParseErrorKind::IncompleteInput) { // Print continuation prompt (PS2) if (auto rc = stdout_dest.write(cont_prompt); !rc) { (void)stderr_dest.write("Error writing prompt: " + rc.error() + "\n"); @@ -127,7 +125,8 @@ int main(int argc, char* argv[]) { int exit_code = interpreter.execute_program(**parse_result); if (exit_code != 0) { - (void)stderr_dest.write("Command exited with code: " + std::to_string(exit_code) + "\n"); + (void)stderr_dest.write("Command exited with code: " + std::to_string(exit_code) + + "\n"); } } } diff --git a/test/line_continuation_tests.cpp b/test/line_continuation_tests.cpp index e6d1df8..9e4f056 100644 --- a/test/line_continuation_tests.cpp +++ b/test/line_continuation_tests.cpp @@ -1,8 +1,8 @@ -#include - -#include "shell/parser.hpp" #include "shell/ast.hpp" #include "shell/ast_printer.hpp" +#include "shell/parser.hpp" + +#include using namespace wshell; diff --git a/test/test_command_parser.cpp b/test/test_command_parser.cpp index 5c69dab..b855d7d 100644 --- a/test/test_command_parser.cpp +++ b/test/test_command_parser.cpp @@ -1,21 +1,19 @@ +#include "shell/ast.hpp" +#include "shell/ast_printer.hpp" +#include "shell/parser.hpp" + #include #include -#include "shell/parser.hpp" -#include "shell/ast.hpp" -#include "shell/ast_printer.hpp" - using namespace wshell; // ----------------------------------------------------------------------------- // Helpers (NO macros) // ----------------------------------------------------------------------------- -static void assert_parse_ok_and_ast_equals( - std::string_view input, - const std::string& expected_ast) -{ +static void assert_parse_ok_and_ast_equals(std::string_view input, + const std::string& expected_ast) { auto result = parse_line(input); if (!result.has_value()) { const auto& err = result.error(); @@ -30,16 +28,16 @@ static void assert_parse_ok_and_ast_equals( if (actual != expected_ast) { FAIL() << "AST mismatch\n" - << "Input:\n" << input << "\n" - << "Expected:\n" << expected_ast - << "Actual:\n" << actual; + << "Input:\n" + << input << "\n" + << "Expected:\n" + << expected_ast << "Actual:\n" + << actual; } } -static void assert_parse_program_ok_and_ast_equals( - std::string_view input, - const std::string& expected_ast) -{ +static void assert_parse_program_ok_and_ast_equals(std::string_view input, + const std::string& expected_ast) { auto result = parse_program(input); if (!result.has_value()) { const auto& err = result.error(); @@ -54,19 +52,22 @@ static void assert_parse_program_ok_and_ast_equals( if (actual != expected_ast) { FAIL() << "AST mismatch\n" - << "Input:\n" << input << "\n" - << "Expected:\n" << expected_ast - << "Actual:\n" << actual; + << "Input:\n" + << input << "\n" + << "Expected:\n" + << expected_ast << "Actual:\n" + << actual; } } -static void assert_parse_fails(std::string_view input) -{ +static void assert_parse_fails(std::string_view input) { auto result = parse_line(input); if (result.has_value()) { FAIL() << "Expected parse failure but succeeded.\n" - << "Input:\n" << input << "\n" - << "AST:\n" << to_string(*result.value()); + << "Input:\n" + << input << "\n" + << "AST:\n" + << to_string(*result.value()); } } @@ -75,34 +76,22 @@ static void assert_parse_fails(std::string_view input) // ----------------------------------------------------------------------------- TEST(ParserTests, SimpleCommand_NoArgs) { - assert_parse_ok_and_ast_equals( - "ls\n", - "Command: ls\n" - ); + assert_parse_ok_and_ast_equals("ls\n", "Command: ls\n"); } TEST(ParserTests, SimpleCommand_WithArgs) { - assert_parse_ok_and_ast_equals( - "echo hello world\n", - "Command: echo\n" - " Args: hello world\n" - ); + assert_parse_ok_and_ast_equals("echo hello world\n", "Command: echo\n" + " Args: hello world\n"); } TEST(ParserTests, Command_WithFlagsAndPaths) { - assert_parse_ok_and_ast_equals( - "grep -R foo ./src\n", - "Command: grep\n" - " Args: -R foo ./src\n" - ); + assert_parse_ok_and_ast_equals("grep -R foo ./src\n", "Command: grep\n" + " Args: -R foo ./src\n"); } TEST(ParserTests, Command_WhitespaceVariations) { - assert_parse_ok_and_ast_equals( - " echo hi there \n", - "Command: echo\n" - " Args: hi there\n" - ); + assert_parse_ok_and_ast_equals(" echo hi there \n", "Command: echo\n" + " Args: hi there\n"); } // ----------------------------------------------------------------------------- @@ -110,24 +99,16 @@ TEST(ParserTests, Command_WhitespaceVariations) { // ----------------------------------------------------------------------------- TEST(ParserTests, Assignment_Simple) { - assert_parse_ok_and_ast_equals( - "let x = 42\n", - "Assignment: x = 42\n" - ); + assert_parse_ok_and_ast_equals("let x = 42\n", "Assignment: x = 42\n"); } TEST(ParserTests, Assignment_Expression) { - assert_parse_ok_and_ast_equals( - "let y = 1 + 2 * 3\n", - "Assignment: y = 1 + 2 * 3\n" - ); + assert_parse_ok_and_ast_equals("let y = 1 + 2 * 3\n", "Assignment: y = 1 + 2 * 3\n"); } TEST(ParserTests, Assignment_Whitespace) { - assert_parse_ok_and_ast_equals( - "let var = value here\n", - "Assignment: var = value here\n" - ); + assert_parse_ok_and_ast_equals("let var = value here\n", + "Assignment: var = value here\n"); } // ----------------------------------------------------------------------------- @@ -135,17 +116,11 @@ TEST(ParserTests, Assignment_Whitespace) { // ----------------------------------------------------------------------------- TEST(ParserTests, Comment_Only) { - assert_parse_ok_and_ast_equals( - "# hello world\n", - "Comment: hello world\n" - ); + assert_parse_ok_and_ast_equals("# hello world\n", "Comment: hello world\n"); } TEST(ParserTests, Comment_WithLeadingWhitespace) { - assert_parse_ok_and_ast_equals( - " # spaced\n", - "Comment: spaced\n" - ); + assert_parse_ok_and_ast_equals(" # spaced\n", "Comment: spaced\n"); } // ----------------------------------------------------------------------------- @@ -153,49 +128,34 @@ TEST(ParserTests, Comment_WithLeadingWhitespace) { // ----------------------------------------------------------------------------- TEST(ParserTests, Redirect_Output) { - assert_parse_program_ok_and_ast_equals( - "ls > out.txt\n", - "Command: ls\n" - " Redirections:\n" - " > out.txt\n" - ); + assert_parse_program_ok_and_ast_equals("ls > out.txt\n", "Command: ls\n" + " Redirections:\n" + " > out.txt\n"); } TEST(ParserTests, Redirect_Output2) { - assert_parse_program_ok_and_ast_equals( - "ls > out.txt", - "Command: ls\n" - " Redirections:\n" - " > out.txt\n" - ); + assert_parse_program_ok_and_ast_equals("ls > out.txt", "Command: ls\n" + " Redirections:\n" + " > out.txt\n"); } TEST(ParserTests, Redirect_Append) { - assert_parse_ok_and_ast_equals( - "echo log >> app.log\n", - "Command: echo\n" - " Args: log\n" - " Redirections:\n" - " >> app.log\n" - ); + assert_parse_ok_and_ast_equals("echo log >> app.log\n", "Command: echo\n" + " Args: log\n" + " Redirections:\n" + " >> app.log\n"); } TEST(ParserTests, Redirect_Input) { - assert_parse_ok_and_ast_equals( - "cat < input.txt\n", - "Command: cat\n" - " Redirections:\n" - " < input.txt\n" - ); + assert_parse_ok_and_ast_equals("cat < input.txt\n", "Command: cat\n" + " Redirections:\n" + " < input.txt\n"); } TEST(ParserTests, Redirect_Multiple) { - assert_parse_ok_and_ast_equals( - "cmd < in > out\n", - "Command: cmd\n" - " Redirections:\n" - " < in\n" - " > out\n" - ); + assert_parse_ok_and_ast_equals("cmd < in > out\n", "Command: cmd\n" + " Redirections:\n" + " < in\n" + " > out\n"); } // ----------------------------------------------------------------------------- @@ -203,20 +163,14 @@ TEST(ParserTests, Redirect_Multiple) { // ----------------------------------------------------------------------------- TEST(ParserTests, Background_Simple) { - assert_parse_ok_and_ast_equals( - "sleep 10 &\n", - "Command: sleep &\n" - " Args: 10\n" - ); + assert_parse_ok_and_ast_equals("sleep 10 &\n", "Command: sleep &\n" + " Args: 10\n"); } TEST(ParserTests, Background_WithRedirect) { - assert_parse_ok_and_ast_equals( - "cmd > out &\n", - "Command: cmd &\n" - " Redirections:\n" - " > out\n" - ); + assert_parse_ok_and_ast_equals("cmd > out &\n", "Command: cmd &\n" + " Redirections:\n" + " > out\n"); } // ----------------------------------------------------------------------------- @@ -224,49 +178,37 @@ TEST(ParserTests, Background_WithRedirect) { // ----------------------------------------------------------------------------- TEST(ParserTests, Pipeline_TwoCommands) { - assert_parse_ok_and_ast_equals( - "ls | grep foo\n", - "Pipeline:\n" - " Command: ls\n" - " Command: grep\n" - " Args: foo\n" - ); + assert_parse_ok_and_ast_equals("ls | grep foo\n", "Pipeline:\n" + " Command: ls\n" + " Command: grep\n" + " Args: foo\n"); } TEST(ParserTests, Pipeline_ThreeCommands) { - assert_parse_ok_and_ast_equals( - "cat file | grep foo | sort\n", - "Pipeline:\n" - " Command: cat\n" - " Args: file\n" - " Command: grep\n" - " Args: foo\n" - " Command: sort\n" - ); + assert_parse_ok_and_ast_equals("cat file | grep foo | sort\n", "Pipeline:\n" + " Command: cat\n" + " Args: file\n" + " Command: grep\n" + " Args: foo\n" + " Command: sort\n"); } TEST(ParserTests, Pipeline_RedirectionOnLast) { - assert_parse_ok_and_ast_equals( - "cat file | grep foo > out\n", - "Pipeline:\n" - " Command: cat\n" - " Args: file\n" - " Command: grep\n" - " Args: foo\n" - " Redirections:\n" - " > out\n" - ); + assert_parse_ok_and_ast_equals("cat file | grep foo > out\n", "Pipeline:\n" + " Command: cat\n" + " Args: file\n" + " Command: grep\n" + " Args: foo\n" + " Redirections:\n" + " > out\n"); } TEST(ParserTests, Pipeline_BackgroundOnLast) { - assert_parse_ok_and_ast_equals( - "cat file | grep foo &\n", - "Pipeline:\n" - " Command: cat\n" - " Args: file\n" - " Command: grep &\n" - " Args: foo\n" - ); + assert_parse_ok_and_ast_equals("cat file | grep foo &\n", "Pipeline:\n" + " Command: cat\n" + " Args: file\n" + " Command: grep &\n" + " Args: foo\n"); } // ----------------------------------------------------------------------------- @@ -274,40 +216,32 @@ TEST(ParserTests, Pipeline_BackgroundOnLast) { // ----------------------------------------------------------------------------- TEST(ParserTests, Sequence_TwoCommands) { - assert_parse_program_ok_and_ast_equals( - "echo one; echo two\n", - "Sequence:\n" - " Command: echo\n" - " Args: one\n" - " Command: echo\n" - " Args: two\n" - ); + assert_parse_program_ok_and_ast_equals("echo one; echo two\n", "Sequence:\n" + " Command: echo\n" + " Args: one\n" + " Command: echo\n" + " Args: two\n"); } TEST(ParserTests, Sequence_WithPipeline) { - assert_parse_program_ok_and_ast_equals( - "echo start; ls | grep txt; echo end\n", - "Sequence:\n" - " Command: echo\n" - " Args: start\n" - " Pipeline:\n" - " Command: ls\n" - " Command: grep\n" - " Args: txt\n" - " Command: echo\n" - " Args: end\n" - ); + assert_parse_program_ok_and_ast_equals("echo start; ls | grep txt; echo end\n", + "Sequence:\n" + " Command: echo\n" + " Args: start\n" + " Pipeline:\n" + " Command: ls\n" + " Command: grep\n" + " Args: txt\n" + " Command: echo\n" + " Args: end\n"); } TEST(ParserTests, Sequence_TrailingSemicolon) { - assert_parse_program_ok_and_ast_equals( - "echo one; echo two;\n", - "Sequence:\n" - " Command: echo\n" - " Args: one\n" - " Command: echo\n" - " Args: two\n" - ); + assert_parse_program_ok_and_ast_equals("echo one; echo two;\n", "Sequence:\n" + " Command: echo\n" + " Args: one\n" + " Command: echo\n" + " Args: two\n"); } // ----------------------------------------------------------------------------- @@ -315,27 +249,22 @@ TEST(ParserTests, Sequence_TrailingSemicolon) { // ----------------------------------------------------------------------------- TEST(ParserTests, Mixed_CommentsAssignmentsCommands) { - assert_parse_program_ok_and_ast_equals( - "# header\nlet x = 5\necho x\n", - "Comment: header\n" - "Assignment: x = 5\n" - "Command: echo\n" - " Args: x\n" - ); + assert_parse_program_ok_and_ast_equals("# header\nlet x = 5\necho x\n", "Comment: header\n" + "Assignment: x = 5\n" + "Command: echo\n" + " Args: x\n"); } TEST(ParserTests, Mixed_PipelineBackgroundAssignment) { - assert_parse_program_ok_and_ast_equals( - "let f = data.txt\ncat $f | grep foo > out &\n", - "Assignment: f = data.txt\n" - "Pipeline:\n" - " Command: cat\n" - " Args: $f\n" - " Command: grep &\n" - " Args: foo\n" - " Redirections:\n" - " > out\n" - ); + assert_parse_program_ok_and_ast_equals("let f = data.txt\ncat $f | grep foo > out &\n", + "Assignment: f = data.txt\n" + "Pipeline:\n" + " Command: cat\n" + " Args: $f\n" + " Command: grep &\n" + " Args: foo\n" + " Redirections:\n" + " > out\n"); } // ----------------------------------------------------------------------------- @@ -367,11 +296,11 @@ TEST(ParserTests, Tricky_SequenceWithMissingCommand) { } TEST(ParserTests, Tricky_DoublePipe) { - assert_parse_fails("ls || grep foo\n"); // not supported yet + assert_parse_fails("ls || grep foo\n"); // not supported yet } TEST(ParserTests, Tricky_DoubleAmpersand) { - assert_parse_fails("ls && echo hi\n"); // not supported yet + assert_parse_fails("ls && echo hi\n"); // not supported yet } TEST(ParserTests, Tricky_WhitespaceOnly) { @@ -385,5 +314,3 @@ TEST(ParserTests, Tricky_EmptyProgram) { ASSERT_TRUE(result.has_value()); ASSERT_TRUE(result.value()->empty()); } - - From 6c7206ac7c54ed7dc571021082bedb05d439494f Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 14:01:17 -0500 Subject: [PATCH 02/14] new file: COPILOT_INSTRUCTIONS.md --- COPILOT_INSTRUCTIONS.md | 674 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 COPILOT_INSTRUCTIONS.md diff --git a/COPILOT_INSTRUCTIONS.md b/COPILOT_INSTRUCTIONS.md new file mode 100644 index 0000000..849edc3 --- /dev/null +++ b/COPILOT_INSTRUCTIONS.md @@ -0,0 +1,674 @@ +# AI Coding Assistant Guidelines for wshell Project + +**Repository:** https://github.com/wsollers/shell + +## AI Assistant Roles + +You are acting as a combined expert with the following roles: +- **Senior Application Security Engineer** - Focus on security-first development +- **C++ Expert** - Deep knowledge of modern C++ and best practices +- **Senior Software Engineer** - Architecture, design patterns, and software engineering principles + +## Target AI Systems + +This prompt is designed for: +- Claude (Anthropic) +- ChatGPT (OpenAI) +- GitHub Copilot + +## Project Overview + +This is a command shell interpreter written in C++23. Security, performance, and maintainability are paramount concerns. + +## Design Philosophy and Style + +This project embraces a **hybrid programming paradigm** that combines object-oriented and functional programming styles, with an emphasis on policy-based design and modern C++ best practices. + +### Core Design Principles + +**SOLID Principles (Mandatory)** +- **Single Responsibility Principle (SRP)**: Each class/module has one reason to change +- **Open/Closed Principle (OCP)**: Open for extension, closed for modification +- **Liskov Substitution Principle (LSP)**: Derived classes must be substitutable for base classes +- **Interface Segregation Principle (ISP)**: Prefer fine-grained interfaces over monolithic ones +- **Dependency Inversion Principle (DIP)**: Depend on abstractions, not concretions + +**Policy-Based Design (Preferred)** +- Use policy classes as template parameters to customize behavior at compile-time +- Separate concerns into orthogonal policies (allocation, threading, error handling, etc.) +- Enable zero-cost abstractions through template instantiation +- Leverage concepts to constrain and document policy requirements +- Example: `template class CommandProcessor` + +**Object-Oriented Programming** +- Use classes to encapsulate state and behavior +- Favor composition over inheritance +- Apply the Rule of Zero when possible; explicit copy/move semantics when needed +- Use virtual functions sparingly; prefer static polymorphism via templates when appropriate +- Create small, focused classes with clear responsibilities + +**Functional Programming Style** +- Prefer immutability and const correctness +- Use pure functions (no side effects) where practical +- Leverage ranges and algorithms from `` and `` +- Use `std::function`, lambdas, and higher-order functions for flexible interfaces +- Favor value semantics and avoid shared mutable state +- Chain operations using functional composition + +**Additional Design Guidelines** +- **Type Safety**: Use strong types and type aliases to prevent misuse +- **Compile-Time Programming**: Leverage `constexpr`, `consteval`, and `if constexpr` to move work to compile-time +- **Error Handling**: Use `std::expected` or `std::optional` for recoverable errors; exceptions for exceptional cases +- **Minimal Coupling**: Design loosely coupled modules with clear boundaries +- **Testability**: Write code that is easy to unit test; prefer dependency injection +- **Performance by Design**: Zero-cost abstractions; measure before optimizing +- **Documentation**: Self-documenting code through clear naming; use Doxygen comments for public APIs + +## C++ Standards and Best Practices + +### Language Standard +- **Use C++23 exclusively** +- Leverage modern C++23 features where appropriate (ranges, concepts, modules when stable) +- Avoid legacy C++ patterns and deprecated features + +### Core Guidelines +- **Follow the C++ Core Guidelines** (https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) + - Use RAII for resource management + - Prefer `std::unique_ptr` and `std::shared_ptr` over raw pointers + - Use `std::string_view` for non-owning string references + - Follow the "Rule of Zero" when possible + - Use `constexpr` and `consteval` where applicable + - Prefer `std::span` for array views + - Use concepts for template constraints + +### Effective C++ Guidelines +- Follow Scott Meyers' "Effective C++" principles +- Prefer const correctness throughout +- Use initialization over assignment +- Understand copy/move semantics +- Make interfaces easy to use correctly and hard to use incorrectly +- Prefer compile-time errors over runtime errors + +### Modern C++ Idioms +- Use structured bindings for multiple return values +- Leverage `std::optional` for optional values instead of pointers + +### Copyright and License Headers +**REQUIRED:** Every source file (.cpp, .hpp, .h) must start with: +```cpp +// Copyright (c) 2024 William Sollers +// SPDX-License-Identifier: BSD-2-Clause +``` +- Place copyright header at the very top of the file (line 1) +- Include the SPDX license identifier for SBOM compliance +- Use BSD-2-Clause license as specified in LICENSE file +- Add missing copyright headers during pre-push validation +- Use `std::variant` for type-safe unions +- Prefer `if constexpr` for compile-time branching +- Use `[[nodiscard]]` on functions where ignoring return values is a bug +- Apply `[[likely]]` and `[[unlikely]]` for performance hints + +## Security Requirements + +Security is a **PRIMARY CONCERN**. All code must adhere to the following: + +### CWE (Common Weakness Enumeration) Compliance +- **Zero tolerance** for CWE vulnerabilities +- Pay special attention to: + - CWE-119: Buffer Overflow + - CWE-120: Buffer Copy without Checking Size + - CWE-78: OS Command Injection + - CWE-89: SQL Injection (if applicable) + - CWE-416: Use After Free + - CWE-415: Double Free + - CWE-190: Integer Overflow + - CWE-787: Out-of-bounds Write + - CWE-476: NULL Pointer Dereference + +### OWASP Application Security Verification Standard (ASVS) +- Follow the OWASP ASVS checklist +- Input validation for all external data +- Proper error handling that doesn't leak sensitive information +- Secure defaults for all configurations + +### NIST Compliance +- Adhere to NIST security guidelines +- Follow NIST SP 800-218 (Secure Software Development Framework) +- Implement defense in depth + +### Compiler and Linker Security Flags + +#### Microsoft Visual C++ (MSVC) +Follow Microsoft Security Development Lifecycle (SDL): +```cmake +# MSVC Security Flags +/GS # Buffer Security Check +/DYNAMICBASE # Address Space Layout Randomization (ASLR) +/NXCOMPAT # Data Execution Prevention (DEP) +/guard:cf # Control Flow Guard +/sdl # Enable Additional Security Checks +/W4 # Warning Level 4 +/WX # Treat Warnings as Errors +``` + +#### Clang/GCC +Follow Clang Hardening Guide: +```cmake +# Clang/GCC Security Flags +-D_FORTIFY_SOURCE=3 # Buffer overflow detection +-fstack-protector-strong # Stack canaries +-fstack-clash-protection # Stack clash protection +-fcf-protection=full # Control Flow Integrity (Intel CET) +-fPIE -pie # Position Independent Executable +-Wl,-z,relro,-z,now # Full RELRO +-Wl,-z,noexecstack # Non-executable stack +-Wall -Wextra -Wpedantic # Comprehensive warnings +-Werror # Treat warnings as errors +-Wformat=2 # Format string vulnerabilities +-Wformat-security # Additional format checks +-Wnull-dereference # NULL pointer dereference detection +-Wstack-protector # Stack protection warnings +-Wtrampolines # Warn about trampolines +-fno-common # Prevent global variable collision +``` + +### Secure Coding Practices +- Validate all inputs (length, type, range, format) +- Use safe string operations (`std::string`, not C strings) +- Avoid raw pointers; prefer smart pointers +- Check all return values and error conditions +- Use `std::array` instead of C arrays +- Never use `gets()`, `strcpy()`, `sprintf()` - use safe alternatives +- Implement proper exception handling +- Zero sensitive data before destruction +- Use constant-time comparison for security-sensitive data + +## Project Structure + +``` +shell/ +├── src/ # Source code +│ ├── lib/ # Library implementation (wshell.so/dll) +│ └── main/ # Main executable (wshell/wshell.exe) +├── test/ # Unit tests (Google Test) +├── fuzz/ # Fuzz tests (libFuzzer, AFL++) +├── scripts/ # Utility scripts (bash AND PowerShell) +│ ├── configure.sh/ps1 +│ ├── build.sh/ps1 +│ ├── test.sh/ps1 +│ ├── fuzz.sh/ps1 +│ └── benchmark.sh/ps1 +├── docs/ # Documentation (Doxygen) +├── .github/ +│ └── workflows/ # CI/CD pipelines +├── .gitignore # C/C++ gitignore +├── LICENSE # 2-Clause BSD License +├── CMakeLists.txt # Root CMake configuration +└── README.md +``` + +### Build Artifacts +- **Executable:** `wshell` (Linux/macOS) or `wshell.exe` (Windows) +- **Library:** `libwshell.so` (Linux), `libwshell.dylib` (macOS), or `wshell.dll` (Windows) +- Dynamic linking between executable and library + +### License +- **2-Clause BSD License** +- Copyright holder: William Sollers +- Use current date for copyright year + +## Build System (CMake) + +### Build Targets + +#### 1. Release +```cmake +# Release target characteristics: +- Optimizations: -O3 or /O2 +- Link-Time Optimization (LTO/IPO) +- Strip debug symbols +- All security flags enabled +- No sanitizers +- Produces smallest, fastest binaries +``` + +#### 2. Debug +```cmake +# Debug target characteristics: +- Optimizations: -O0 or /Od +- Full debug symbols (-g3 or /Zi) +- No inlining +- All security flags enabled +- No sanitizers by default +- Assertions enabled +``` + +#### 3. Test (RelWithDebInfo + Sanitizers) +```cmake +# Test target characteristics: +- Based on Release build with debug info +- Optimizations: -O2 or /O2 +- Debug symbols included +- ALL sanitizers enabled: + * AddressSanitizer (ASan) - memory errors + * ThreadSanitizer (TSan) - data races + * MemorySanitizer (MSan) - uninitialized reads + * UndefinedBehaviorSanitizer (UBSan) - undefined behavior + * LeakSanitizer (LSan) - memory leaks +- Security flags enabled +- Used for CI/CD testing +``` + +#### 4. Coverage (Suggested Addition) +```cmake +# Coverage target characteristics: +- Optimizations: -O0 +- Coverage instrumentation (--coverage or /Coverage) +- Debug symbols +- For code coverage analysis +- Upload to Codecov +``` + +#### 5. Benchmark (Suggested Addition) +```cmake +# Benchmark target: +- Same as Release +- Links Google Benchmark +- For performance regression testing +``` + +### Utility Scripts + +All scripts in `scripts/` directory **MUST** have both: +1. Bash version (`.sh`) for Linux/macOS +2. PowerShell version (`.ps1`) for Windows + +**CRITICAL: Dependency Management** +Whenever you add a new dependency or tool to the project (CMakeLists.txt, scripts, CI/CD workflows), you **MUST**: +1. Verify if it needs to be added to prerequisites scripts: + - `scripts/prerequisites.sh` (Linux/macOS) + - `scripts/prerequisites.ps1` (Windows) +2. Check these locations for new dependencies: + - CMakeLists.txt: find_package(), FetchContent, external libraries + - Build scripts: tools invoked by scripts (lcov, yamllint, clang-tidy, etc.) + - CI workflows: tools used in GitHub Actions + - Documentation: tools mentioned in README or QUICKSTART +3. Update both prerequisite scripts simultaneously to maintain cross-platform consistency +4. Test prerequisites scripts on clean systems when possible + +Examples of tools that must be in prerequisites: +- Build tools: cmake, ninja, clang, gcc, msvc +- Code quality: clang-tidy, cppcheck, clang-format +- Testing: lcov (coverage), sanitizers +- Validation: yamllint, shellcheck +- SBOM: Python packages (reuse, spdx-tools, ntia-conformance-checker) + +Required scripts: +- `configure.[sh|ps1]` - Configure CMake build +- `build.[sh|ps1]` - Build the project +- `test.[sh|ps1]` - Run unit tests +- `fuzz.[sh|ps1]` - Run fuzz tests +- `benchmark.[sh|ps1]` - Run benchmarks +- `clean.[sh|ps1]` - Clean build artifacts +- `coverage.[sh|ps1]` - Generate coverage reports + +## Testing Strategy + +### Unit Tests (Google Test) +- Place in `test/` directory +- Test file naming: `test_.cpp` +- Aim for >90% code coverage +- Test edge cases and error conditions +- Mock external dependencies +- Run on every commit + +### Fuzz Tests +- Place in `fuzz/` directory +- Use libFuzzer or AFL++ +- Fuzz all input parsing functions +- Fuzz command execution paths +- Run on every commit (time-limited) +- Continuous fuzzing on dedicated infrastructure if possible + +### Benchmarks (Google Benchmark) +- Measure performance of critical paths +- Track performance regression +- Generate reports for GitHub Pages +- Compare against baseline + +## CI/CD Pipeline (GitHub Actions) + +### Workflow Validation + +**REQUIRED:** Any modifications to GitHub Actions workflows (`.github/workflows/*.yml`) must be validated using the **GitHub Actions for VS Code** extension (`github.vscode-github-actions`) before committing: + +1. Open the workflow file in VS Code +2. The extension will provide: + - Syntax validation and error highlighting + - IntelliSense for workflow syntax + - Real-time schema validation + - Action version checking +3. Ensure no errors or warnings are shown +4. Validate YAML syntax with `yamllint`: + ```bash + yamllint .github/workflows/*.yml + ``` + +**Why this matters:** +- Invalid workflows fail silently or with cryptic errors +- Prevents CI/CD pipeline breakage +- Catches typos in action names and versions +- Validates secret references and context variables +- Ensures proper YAML formatting + +### Required Workflows + +#### 1. Build and Test +```yaml +- Build on: Linux (Ubuntu latest), macOS (latest), Windows (latest) +- Compilers: GCC (latest), Clang (latest), MSVC (latest) +- Build types: Debug, Release, Test +- Run unit tests with sanitizers +- Upload test results +``` + +#### 2. Static Analysis +```yaml +- copilot review +- CodeQL analysis (C++) +- clang-tidy +- cppcheck +- Codacy Security Scan +- Dev Skim +- Microsoft C++ Code Analysis +- dependency review +- Microsoft Defender for Devops +- Ossar +- OSV Scanner +- Trivy +- PSScript analyzer against all powershell .ps1 files +- Additional SAST tools: + * Semgrep + * Coverity (if available) + * SonarCloud (community edition) +``` + +#### 3. Code Coverage +```yaml +- Run tests with coverage instrumentation +- Generate coverage report (gcov/lcov) +- Upload to Codecov with token +``` + +Example Codecov upload: +```yaml +- name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: wsollers/shell + fail_ci_if_error: true +``` + +#### 4. Fuzz Testing +```yaml +- Run fuzz tests for limited time (5-10 minutes) +- Save corpus for future runs +- Fail on crashes +``` + +#### 5. Documentation +```yaml +- Generate Doxygen documentation +- Generate benchmark reports +- Deploy to GitHub Pages +``` + +### GitHub Pages Structure +Hosted at: https://wsollers.github.io/shell/ + +Contents: +- Doxygen API documentation +- Benchmark results and performance graphs +- Code coverage reports (linked from Codecov) +- Architecture diagrams +- User manual + +## Code Quality Standards + +### Documentation +- Use Doxygen-style comments for all public APIs +- Document parameters, return values, exceptions +- Include usage examples in complex functions +- Maintain up-to-date README.md + +### Code Style +- Follow a consistent style guide (suggest: LLVM or Google C++ Style) +- Use clang-format with project `.clang-format` file +- Maximum line length: 100 characters +- Use meaningful variable and function names +- Avoid abbreviations unless widely understood + +### Code Review Checklist +Before submitting code, ensure: +- [ ] All tests pass (unit, fuzz, sanitizers) +- [ ] No compiler warnings +- [ ] Static analysis passes +- [ ] Code coverage maintained or improved +- [ ] Documentation updated +- [ ] Security considerations addressed +- [ ] Performance impact assessed +- [ ] Cross-platform compatibility verified + +## Common Security Pitfalls to Avoid + +1. **Command Injection**: Sanitize all user input before shell execution +2. **Path Traversal**: Validate and normalize all file paths +3. **Buffer Overflows**: Use `std::string`, `std::vector`, bounds checking +4. **Integer Overflows**: Check arithmetic operations, use safe math libraries +5. **Race Conditions**: Proper synchronization, avoid TOCTOU +6. **Resource Leaks**: RAII, smart pointers, proper exception safety +7. **Unvalidated Input**: Whitelist validation, reject unknown input +8. **Information Disclosure**: Sanitize error messages, secure logging +9. **Privilege Escalation**: Principle of least privilege, drop privileges when possible +10. **Cryptographic Weaknesses**: Use vetted crypto libraries (OpenSSL, libsodium), never roll your own + +## Performance Considerations + +- Profile before optimizing +- Use `-march=native` for local builds (not for distribution) +- Consider cache-friendly data structures +- Minimize allocations in hot paths +- Use `std::string_view` to avoid copies +- Benchmark changes that affect critical paths +- Use `constexpr` for compile-time computation + +## Cross-Platform Compatibility + +- Test on Linux, macOS, and Windows +- Use CMake for platform abstraction +- Avoid platform-specific APIs unless necessary +- Use `std::filesystem` for path operations +- Handle endianness if relevant +- Consider Windows-specific security (e.g., DEP, ASLR) + +## AI Assistant Instructions + +When generating code for this project: + +1. **Default to secure implementations** - assume security-first mindset +2. **Use modern C++23** - avoid legacy patterns +3. **Include error handling** - every function should handle failure cases +4. **Add appropriate tests** - suggest unit tests for new code +5. **Consider cross-platform** - highlight platform-specific code +6. **Document thoroughly** - include Doxygen comments +7. **Validate inputs** - all external data must be validated +8. **Check return values** - never ignore error returns +9. **Use RAII** - automatic resource management +10. **Think about attack vectors** - consider how code could be exploited + +When reviewing code: +1. Check for security vulnerabilities first +2. Verify C++23 best practices +3. Ensure proper error handling +4. Validate test coverage +5. Review documentation completeness +6. Assess performance implications +7. Verify cross-platform compatibility + +## Questions to Ask + +Before implementing features, consider: +- What are the security implications? +- How will this be tested? +- What edge cases exist? +- What can go wrong? +- How does this perform at scale? +- Is this cross-platform compatible? +- What happens on failure? +- Can this be exploited? + +## Pre-Push Validation Requirements + +**MANDATORY:** Before pushing any changes to GitHub, ensure the following steps complete successfully: + +### 1. Prerequisites Installation +Ensure all build dependencies and SBOM tools are installed: +```bash +# Linux/macOS +./scripts/prerequisites.sh + +# Windows (as Administrator) +.\scripts\prerequisites.ps1 +``` + +### 2. Clean Build +Remove all previous build artifacts to ensure a clean state: +```bash +# Linux/macOS +./scripts/clean.sh + +# Windows +.\scripts\clean.ps1 +``` + +### 3. Configure +Configure the build system with appropriate preset: +```bash +# Linux - Release build +cmake --preset linux-release + +# macOS - Release build +cmake --preset macos-release + +# Windows - Release build +cmake --preset windows-msvc-release +``` + +### 4. Build +Compile the project: +```bash +# Linux/macOS +cmake --build build/linux-release --parallel +# or +./scripts/build.sh + +# Windows +cmake --build build\windows-msvc-release --config Release --parallel +# or +.\scripts\build.ps1 +``` + +### 5. Test +Run all unit tests: +```bash +# Linux/macOS +cd build/linux-release && ctest --output-on-failure +# or +./scripts/test.sh + +# Windows +cd build\windows-msvc-release && ctest -C Release --output-on-failure +# or +.\scripts\test.ps1 +``` + +### 6. Fuzz Testing +Run fuzz tests (Linux/macOS only): +```bash +./scripts/fuzz.sh +# Runs short fuzz tests (30 seconds per target) +``` + +### 7. Benchmarks +Run performance benchmarks: +```bash +# Linux/macOS +./scripts/benchmark.sh + +# Windows +.\scripts\benchmark.ps1 +``` + +### 8. SBOM Generation (Release builds only) +Verify SBOM is generated correctly: +```bash +# Linux/macOS +cmake --install build/linux-release + +# Windows +cmake --install build\windows-msvc-release + +# Verify SBOM files exist: +# - wshell-sbom.spdx (tag-value format) +# - wshell-sbom.spdx.json (JSON format) +``` + +### 9. Code Quality Checks +- Run clang-tidy on modified files +- Ensure no compiler warnings (`-Wall -Wextra -Wpedantic`) +- Verify YAML files pass yamllint (`.github/workflows/*.yml`) +- **Validate GitHub Actions workflows** using the GitHub Actions extension in VS Code + - Open each modified workflow file + - Ensure no errors or warnings from the extension + - Verify action versions are current +- Check all source files have copyright headers: + ```bash + # Check for missing copyright headers + find src include test bench fuzz -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \) \ + -exec grep -L 'Copyright (c) 2024 William Sollers' {} \; + ``` + +### Pre-Push Checklist +- [ ] Clean build completed successfully +- [ ] All tests pass (31+ tests) +- [ ] Fuzz tests run without crashes +- [ ] Benchmarks complete +- [ ] SBOM generated (release builds) +- [ ] No compiler warnings +- [ ] No clang-tidy errors +- [ ] Code follows C++23 best practices +- [ ] Security vulnerabilities checked +- [ ] All source files have copyright headers (Copyright (c) 2024 William Sollers + SPDX-License-Identifier: BSD-2-Clause) +- [ ] GitHub Actions workflows validated with VS Code extension (if modified) +- [ ] Documentation updated (README, QUICKSTART, etc.) +- [ ] Commit messages are clear and descriptive + +**Rationale:** This comprehensive validation ensures: +- Code quality and correctness +- Security vulnerabilities are caught early +- Performance regressions are detected +- Build system integrity +- Supply chain security (SBOM) +- Cross-platform compatibility + +**Automation:** The CI/CD pipeline performs these checks automatically, but local validation prevents: +- Failed CI builds +- Wasted CI/CD resources +- Blocking other developers +- Security issues reaching production + +--- + +**Remember:** Security, correctness, and maintainability come before performance. Write clear, secure code first, then optimize if needed. +**When in doubt:** Ask for clarification, suggest alternatives, highlight risks. \ No newline at end of file From 64eb53e8b560bccceb91c1c75e9a6bfb6da38303 Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 20:20:37 -0500 Subject: [PATCH 03/14] saving progress 1 test to go --- include/shell/ast.hpp | 91 +++++----- include/shell/ast_to_command_model.hpp | 55 ++++++ include/shell/command_model.hpp | 223 ++++++++++++------------ include/shell/history.hpp | 4 + include/shell/lexer.hpp | 7 +- include/shell/parser.hpp | 5 +- include/shell/shell_interpreter.hpp | 92 ++++++---- src/lib/executor/executor_posix.cpp | 4 +- src/lib/parser/ast_to_command_model.cpp | 5 + src/lib/parser/input_source.cpp | 2 + src/lib/parser/parser.cpp | 147 +++++++++++----- test/CMakeLists.txt | 7 +- test/line_continuation_tests.cpp | 14 +- test/shell_history_tests.cpp | 38 ++++ test/shell_substitution_tests.cpp | 123 +++++++++++++ 15 files changed, 571 insertions(+), 246 deletions(-) create mode 100644 include/shell/ast_to_command_model.hpp create mode 100644 src/lib/parser/ast_to_command_model.cpp create mode 100644 test/shell_history_tests.cpp create mode 100644 test/shell_substitution_tests.cpp diff --git a/include/shell/ast.hpp b/include/shell/ast.hpp index 1ab381c..c7b3631 100644 --- a/include/shell/ast.hpp +++ b/include/shell/ast.hpp @@ -3,29 +3,51 @@ #pragma once -#include #include #include + +#include #include +#include namespace wshell { // ============================================================================ -// Redirection +// Word struct for shell words (quoted, needs expansion, etc.) // ============================================================================ +struct Word { + std::string text; + bool quoted = false; + bool needs_expansion = false; + Word(std::string t, bool q = false, bool n = false) + : text(std::move(t)), quoted(q), needs_expansion(n) {} + Word() = default; +}; + +// Stream output for Word (for AST printing) +inline std::ostream& operator<<(std::ostream& os, const Word& w) { + if (w.quoted) { + os.put('"'); + os << w.text; + os.put('"'); + } else { + os << w.text; + } + return os; +} + enum class RedirectKind { - Input, // < - OutputTruncate, // > - OutputAppend // >> + Input, // < + OutputTruncate, // > + OutputAppend // >> }; struct Redirection { RedirectKind kind; - std::string target; + Word target; - Redirection(RedirectKind k, std::string t) - : kind(k), target(std::move(t)) {} + Redirection(RedirectKind k, Word t) : kind(k), target(std::move(t)) {} }; // ============================================================================ @@ -52,37 +74,27 @@ struct AssignmentNode { }; struct CommandNode { - std::string command_name; - std::vector arguments; + Word command_name; + std::vector arguments; std::vector redirections; bool background = false; }; struct PipelineNode { - std::vector commands; // by value + std::vector commands; // by value }; struct SequenceNode { - std::vector> statements; + std::vector> + statements; }; // ============================================================================ // Statement Variant (NO pointers) // ============================================================================ -using StatementNode = std::variant< - CommentNode, - AssignmentNode, - CommandNode, - PipelineNode, - SequenceNode ->; +using StatementNode = + std::variant; // ============================================================================ // Program Node @@ -91,9 +103,7 @@ using StatementNode = std::variant< struct ProgramNode { std::vector statements; - void add_statement(StatementNode stmt) { - statements.push_back(std::move(stmt)); - } + void add_statement(StatementNode stmt) { statements.push_back(std::move(stmt)); } [[nodiscard]] bool empty() const noexcept { return statements.empty(); } [[nodiscard]] std::size_t size() const noexcept { return statements.size(); } @@ -104,33 +114,24 @@ struct ProgramNode { // ============================================================================ inline CommentNode make_comment(std::string text) { - return CommentNode{ std::move(text) }; + return CommentNode{std::move(text)}; } inline AssignmentNode make_assignment(std::string var, std::string value) { - return AssignmentNode{ std::move(var), std::move(value) }; + return AssignmentNode{std::move(var), std::move(value)}; } -inline CommandNode make_command( - std::string name, - std::vector args = {}, - std::vector redirs = {}, - bool background = false) -{ - return CommandNode{ - std::move(name), - std::move(args), - std::move(redirs), - background - }; +inline CommandNode make_command(Word name, std::vector args = {}, + std::vector redirs = {}, bool background = false) { + return CommandNode{std::move(name), std::move(args), std::move(redirs), background}; } inline PipelineNode make_pipeline(std::vector cmds) { - return PipelineNode{ std::move(cmds) }; + return PipelineNode{std::move(cmds)}; } inline SequenceNode make_sequence(std::vector stmts) { - return SequenceNode{ std::move(stmts) }; + return SequenceNode{std::move(stmts)}; } -} // namespace wshell \ No newline at end of file +} // namespace wshell \ No newline at end of file diff --git a/include/shell/ast_to_command_model.hpp b/include/shell/ast_to_command_model.hpp new file mode 100644 index 0000000..1bde109 --- /dev/null +++ b/include/shell/ast_to_command_model.hpp @@ -0,0 +1,55 @@ +// Copyright (c) 2024 William Sollers +// SPDX-License-Identifier: BSD-2-Clause + +#pragma once + +#include "shell/ast.hpp" +#include "shell/command_model.hpp" + +#include + +namespace wshell { + +// Convert AST Word to command model ShellArg +inline ShellArg ast_word_to_model(const Word& w) { + return ShellArg{w.text, w.quoted, w.needs_expansion}; +} + +// Convert AST Redirection to command model IO (FileTarget) +inline IO ast_redir_to_model(const Redirection& r) { + // Only file redirections for now + OpenMode mode = OpenMode::WriteTruncate; + switch (r.kind) { + case RedirectKind::Input: + mode = OpenMode::Read; + break; + case RedirectKind::OutputTruncate: + mode = OpenMode::WriteTruncate; + break; + case RedirectKind::OutputAppend: + mode = OpenMode::WriteAppend; + break; + } + return FileTarget{r.target.text, mode}; +} + +// Convert AST CommandNode to command model Command +inline Command ast_cmd_to_model(const CommandNode& node) { + Command cmd; + cmd.executable = node.command_name.text; + for (const auto& arg : node.arguments) { + cmd.args.push_back(ast_word_to_model(arg)); + } + for (const auto& redir : node.redirections) { + // Only handle stdout redirection for now + if (redir.kind == RedirectKind::OutputTruncate || + redir.kind == RedirectKind::OutputAppend) { + cmd.stdout_ = ast_redir_to_model(redir); + } else if (redir.kind == RedirectKind::Input) { + cmd.stdin_ = ast_redir_to_model(redir); + } + } + return cmd; +} + +} // namespace wshell diff --git a/include/shell/command_model.hpp b/include/shell/command_model.hpp index cd3078b..5ca805d 100644 --- a/include/shell/command_model.hpp +++ b/include/shell/command_model.hpp @@ -4,26 +4,38 @@ // command_model.hpp #pragma once +#include +#include +#include + #include #include #include #include -#include #include -#include #include -#include #include #include "platform_types.hpp" // Cross-platform process/job control types namespace wshell { +/// ShellArg with expansion context (for proper quote handling and variable expansion) +struct ShellArg { + std::string value; + bool is_quoted{false}; // "foo" vs foo (affects expansion and word splitting) + bool needs_expansion{true}; // $var, $(cmd), etc. need expansion + // Convenience constructor + explicit ShellArg(std::string val, bool quoted = false, bool expand = true) + : value(std::move(val)), is_quoted(quoted), needs_expansion(expand) {} +}; + using String = std::string; using Strings = std::vector; -using EnvironmentVariable = std::pair; -using EnvMap = std::vector; // preserves order, allows duplicates if you want to mirror POSIX envp. +using EnvironmentVariable = std::pair; +using EnvMap = std::vector; // preserves order, allows duplicates if you want + // to mirror POSIX envp. enum class Stream { Stdin, Stdout, Stderr }; @@ -57,13 +69,14 @@ struct InheritTarget { using IO = std::variant; struct Command { - std::filesystem::path executable; // absolute or relative; resolution is exec-layer policy - std::optional work_dir; // nullopt = inherit current working directory + std::filesystem::path executable; // absolute or relative; resolution is exec-layer policy + std::optional work_dir; // nullopt = inherit current working directory // argv[0] policy: keep argv[0] separate (recommended). - // exec-layer can build argv = {executable.filename().string(), args...} or use provided "argv0". + // exec-layer can build argv = {executable.filename().string(), args...} or use provided + // "argv0". std::optional argv0; - Strings args; + std::vector args; // Arguments as ShellArgs for expansion/quoting // env policy: if env_inherit is true, overlay "env" onto current environment. // if false, use exactly env. @@ -71,9 +84,9 @@ struct Command { EnvMap env; // stdio endpoints - IO stdin_ { InheritTarget{} }; - IO stdout_ { InheritTarget{} }; - IO stderr_ { InheritTarget{} }; + IO stdin_{InheritTarget{}}; + IO stdout_{InheritTarget{}}; + IO stderr_{InheritTarget{}}; }; // A pipeline is just an ordered list of commands. @@ -91,28 +104,42 @@ using Job = std::variant; // // Helpers (optional but nice) // -inline Command make_command(std::filesystem::path exe, Strings args = {}) { +inline Command make_command(std::filesystem::path exe, std::vector args = {}) { Command c; c.executable = std::move(exe); c.args = std::move(args); return c; } +// Overload for string arguments (for compatibility) +inline Command make_command(std::filesystem::path exe, Strings args) { + std::vector shell_args; + for (auto& s : args) + shell_args.emplace_back(std::move(s), false, true); + return make_command(std::move(exe), std::move(shell_args)); +} + inline Pipeline pipe(std::vector cmds) { - return Pipeline{ .commands = std::move(cmds) }; + return Pipeline{.commands = std::move(cmds)}; } inline IO to_file(std::filesystem::path p, OpenMode m = OpenMode::WriteTruncate) { - return FileTarget{ .path = std::move(p), .mode = m }; + return FileTarget{.path = std::move(p), .mode = m}; } inline IO from_file(std::filesystem::path p) { - return FileTarget{ .path = std::move(p), .mode = OpenMode::Read }; + return FileTarget{.path = std::move(p), .mode = OpenMode::Read}; } -inline IO capture() { return CaptureTarget{}; } -inline IO null_io() { return NullTarget{}; } -inline IO inherit() { return InheritTarget{}; } +inline IO capture() { + return CaptureTarget{}; +} +inline IO null_io() { + return NullTarget{}; +} +inline IO inherit() { + return InheritTarget{}; +} // ============================================================================ // EXTENDED MODEL - Control Structures, Job Control, and Runtime Context @@ -126,52 +153,40 @@ enum class CommandType { Alias // Command aliases }; -/// Word with expansion context (for proper quote handling and variable expansion) -struct Word { - std::string value; - bool is_quoted{false}; // "foo" vs foo (affects expansion and word splitting) - bool needs_expansion{true}; // $var, $(cmd), etc. need expansion - - // Convenience constructor - explicit Word(std::string val, bool quoted = false, bool expand = true) - : value(std::move(val)), is_quoted(quoted), needs_expansion(expand) {} -}; /// Job with runtime state (for job control: bg, fg, jobs commands) struct JobWithState { Job content; // Command or Pipeline - + // Job control metadata - int job_id{platform::INVALID_JOB_ID}; // Job number [1], [2], etc. - platform::ProcessGroup process_group; // Process group (POSIX) or Job Object (Windows) - bool background{false}; // Started with '&' - + int job_id{platform::INVALID_JOB_ID}; // Job number [1], [2], etc. + platform::ProcessGroup process_group; // Process group (POSIX) or Job Object (Windows) + bool background{false}; // Started with '&' + // Job lifecycle state enum class State { - Running, // Currently executing - Stopped, // Suspended (Ctrl-Z) - Done, // Completed successfully - Terminated // Killed or failed + Running, // Currently executing + Stopped, // Suspended (Ctrl-Z) + Done, // Completed successfully + Terminated // Killed or failed }; State state{State::Running}; - + // Helper methods - [[nodiscard]] bool has_valid_process_group() const noexcept { - return process_group.is_valid(); - } + [[nodiscard]] bool has_valid_process_group() const noexcept { return process_group.is_valid(); } }; /// Conditional execution (if/then/else/fi) struct Conditional { - Job condition; // Test command (e.g., "test -f file.txt" or "[ -f file.txt ]") - std::vector then_branch; // Execute if condition returns 0 - std::vector else_branch; // Execute if condition returns non-zero (optional) + Job condition; // Test command (e.g., "test -f file.txt" or "[ -f file.txt ]") + std::vector then_branch; // Execute if condition returns 0 + std::vector else_branch; // Execute if condition returns non-zero (optional) }; /// While loop (while condition; do body; done) struct WhileLoop { - Job condition; // Loop condition - std::vector body; // Loop body + Job condition; // Loop condition + std::vector body; // Loop body }; /// For loop (for var in values; do body; done) @@ -183,18 +198,18 @@ struct ForLoop { /// User-defined shell function struct Function { - std::string name; // Function name - std::vector parameters; // Optional: named parameters (bash doesn't use these) - std::vector body; // Function body statements + std::string name; // Function name + std::vector parameters; // Optional: named parameters (bash doesn't use these) + std::vector body; // Function body statements }; /// Logical operators for command chaining enum class JobOperator { - None, // Single command - And, // cmd1 && cmd2 (execute cmd2 only if cmd1 succeeds) - Or, // cmd1 || cmd2 (execute cmd2 only if cmd1 fails) - Background, // cmd1 & (run in background) - Sequential // cmd1 ; cmd2 (always execute both) + None, // Single command + And, // cmd1 && cmd2 (execute cmd2 only if cmd1 succeeds) + Or, // cmd1 || cmd2 (execute cmd2 only if cmd1 fails) + Background, // cmd1 & (run in background) + Sequential // cmd1 ; cmd2 (always execute both) }; /// Job sequence with operators (for chaining: cmd1 && cmd2 || cmd3) @@ -202,7 +217,7 @@ struct JobSequence { Job job; JobOperator op{JobOperator::None}; std::unique_ptr next; // Recursive chain - + // Helper to build chains static std::unique_ptr make(Job j, JobOperator op = JobOperator::None) { auto seq = std::make_unique(); @@ -225,16 +240,15 @@ struct Comment { }; /// Statement - anything that can appear at the top level -using Statement = std::variant< - Job, // Simple command or pipeline - JobSequence, // Chained commands with operators - Conditional, // if/then/else - WhileLoop, // while loop - ForLoop, // for loop - Function, // function definition - Assignment, // variable assignment - Comment // comment (for AST preservation) ->; +using Statement = std::variant; /// Program - collection of statements (script or interactive session) struct Program { @@ -246,58 +260,56 @@ struct Program { struct ExecutionContext { // Variables (shell-local, not exported) std::map variables; - + // Environment variables (exported to child processes) std::map environment; - + // User-defined functions std::map functions; - + // Job control table std::vector jobs; int next_job_id{1}; - + // Execution state int last_exit_status{platform::EXIT_SUCCESS_STATUS}; // $? - exit status of last command - std::filesystem::path cwd; // Current working directory - + std::filesystem::path cwd; // Current working directory + // Shell mode - bool interactive{true}; // Interactive vs. script mode - + bool interactive{true}; // Interactive vs. script mode + // Process ID (for reference) platform::ProcessId shell_pid{platform::INVALID_PROCESS_ID}; - + // Helper methods [[nodiscard]] int get_exit_status() const noexcept { return last_exit_status; } void set_exit_status(int status) noexcept { last_exit_status = status; } - - [[nodiscard]] bool is_success() const noexcept { - return last_exit_status == platform::EXIT_SUCCESS_STATUS; + + [[nodiscard]] bool is_success() const noexcept { + return last_exit_status == platform::EXIT_SUCCESS_STATUS; } - + [[nodiscard]] int add_job(JobWithState job) { job.job_id = next_job_id++; jobs.push_back(std::move(job)); return job.job_id; } - + void remove_job(int job_id) { - jobs.erase( - std::remove_if(jobs.begin(), jobs.end(), - [job_id](const JobWithState& j) { return j.job_id == job_id; }), - jobs.end() - ); + jobs.erase(std::remove_if(jobs.begin(), jobs.end(), + [job_id](const JobWithState& j) { return j.job_id == job_id; }), + jobs.end()); } - + [[nodiscard]] JobWithState* find_job(int job_id) noexcept { auto it = std::find_if(jobs.begin(), jobs.end(), - [job_id](const JobWithState& j) { return j.job_id == job_id; }); + [job_id](const JobWithState& j) { return j.job_id == job_id; }); return it != jobs.end() ? &(*it) : nullptr; } - + [[nodiscard]] const JobWithState* find_job(int job_id) const noexcept { auto it = std::find_if(jobs.begin(), jobs.end(), - [job_id](const JobWithState& j) { return j.job_id == job_id; }); + [job_id](const JobWithState& j) { return j.job_id == job_id; }); return it != jobs.end() ? &(*it) : nullptr; } }; @@ -308,38 +320,33 @@ struct ExecutionContext { /// Create a simple command with string arguments (convenience) inline Command make_simple_command(std::string name, std::vector args = {}) { + std::vector shell_args; + for (auto& s : args) + shell_args.emplace_back(std::move(s), false, true); Command c; c.executable = std::move(name); - c.args = std::move(args); + c.args = std::move(shell_args); return c; } /// Create a conditional -inline Conditional make_conditional(Job condition, std::vector then_branch, - std::vector else_branch = {}) { - return Conditional{ - .condition = std::move(condition), - .then_branch = std::move(then_branch), - .else_branch = std::move(else_branch) - }; +inline Conditional make_conditional(Job condition, std::vector then_branch, + std::vector else_branch = {}) { + return Conditional{.condition = std::move(condition), + .then_branch = std::move(then_branch), + .else_branch = std::move(else_branch)}; } /// Create a while loop inline WhileLoop make_while(Job condition, std::vector body) { - return WhileLoop{ - .condition = std::move(condition), - .body = std::move(body) - }; + return WhileLoop{.condition = std::move(condition), .body = std::move(body)}; } /// Create a for loop -inline ForLoop make_for(std::string variable, std::vector values, - std::vector body) { +inline ForLoop make_for(std::string variable, std::vector values, + std::vector body) { return ForLoop{ - .variable = std::move(variable), - .values = std::move(values), - .body = std::move(body) - }; + .variable = std::move(variable), .values = std::move(values), .body = std::move(body)}; } -} // namespace wshell +} // namespace wshell diff --git a/include/shell/history.hpp b/include/shell/history.hpp index 43663d5..b2a75c8 100644 --- a/include/shell/history.hpp +++ b/include/shell/history.hpp @@ -9,6 +9,8 @@ #include #include +#include "execution_policy.hpp" // for get_home_directory + constexpr size_t HISTORY_DEFAULT_SIZE = 1000; namespace wshell { @@ -98,6 +100,8 @@ class History { } } + + private: diff --git a/include/shell/lexer.hpp b/include/shell/lexer.hpp index 9ee01fe..6868589 100644 --- a/include/shell/lexer.hpp +++ b/include/shell/lexer.hpp @@ -16,7 +16,6 @@ namespace wshell { /// Token type enumeration enum class TokenType { - Identifier, // command names, variable names Let, // 'let' keyword Equals, // '=' @@ -25,6 +24,12 @@ enum class TokenType { Whitespace, // ' ', '\t' (usually skipped) EndOfFile, // End of input + // Variable and substitution tokens + Dollar, // $ + Variable, // $VAR or ${VAR} + LBrace, // { + RBrace, // } + //added Pipe, // '|' Redirect, // '>', '<', '>>' diff --git a/include/shell/parser.hpp b/include/shell/parser.hpp index 63ea77d..c4f215f 100644 --- a/include/shell/parser.hpp +++ b/include/shell/parser.hpp @@ -49,8 +49,8 @@ struct ParseError { class Parser { public: /// Construct parser with input source - explicit Parser(std::string_view source, bool repl_mode = true) - : lexer_(source), repl_mode_{repl_mode} {} + explicit Parser(std::string_view source, bool /*repl_mode*/ = true) + : lexer_(source) {} /// Parse the entire program [[nodiscard]] std::expected, ParseError> @@ -62,7 +62,6 @@ class Parser { private: Lexer lexer_; - bool repl_mode_; // Parser methods (all updated to match the new AST) [[nodiscard]] std::expected parse_statement(); diff --git a/include/shell/shell_interpreter.hpp b/include/shell/shell_interpreter.hpp index 7424653..0940301 100644 --- a/include/shell/shell_interpreter.hpp +++ b/include/shell/shell_interpreter.hpp @@ -96,6 +96,45 @@ class ShellInterpreter { history_.push(command); }; + + std::string expand_variables(std::string_view input) { + std::string result; + const std::string str(input); + size_t i = 0; + while (i < str.size()) { + if (str[i] == '$') { + size_t var_start = i + 1; + size_t var_end = var_start; + std::string var_name; + if (var_start < str.size() && str[var_start] == '{') { + // ${VAR} syntax + var_start++; + var_end = var_start; + while (var_end < str.size() && str[var_end] != '}') ++var_end; + var_name = str.substr(var_start, var_end - var_start); + i = (var_end < str.size()) ? var_end + 1 : str.size(); + } else { + // $VAR syntax + while (var_end < str.size() && (std::isalnum(str[var_end]) || str[var_end] == '_')) ++var_end; + var_name = str.substr(var_start, var_end - var_start); + i = var_end; + } + if (!var_name.empty()) { + auto it = variables_.find(var_name); + if (it != variables_.end()) { + result += it->second; + } + } else { + result += '$'; + } + } else { + result += str[i]; + ++i; + } + } + return result; + } + private: Executor executor_; std::map variables_; @@ -155,54 +194,36 @@ class ShellInterpreter { return platform::EXIT_SUCCESS_STATUS; } - /* - struct Command { - std::filesystem::path executable; // absolute or relative; resolution is exec-layer policy - std::optional work_dir; // nullopt = inherit current working directory - - // argv[0] policy: keep argv[0] separate (recommended). - // exec-layer can build argv = {executable.filename().string(), args...} or use provided "argv0". - std::optional argv0; - Strings args; - - // env policy: if env_inherit is true, overlay "env" onto current environment. - // if false, use exactly env. - bool env_inherit{true}; - EnvMap env; - - // stdio endpoints - IO stdin_ { InheritTarget{} }; - IO stdout_ { InheritTarget{} }; - IO stderr_ { InheritTarget{} }; -}; - - struct CommandNode { - std::string command_name; - std::vector arguments; - std::vector redirections; - bool background = false; -}; - */ /// Execute a command [[nodiscard]] std::expected execute_command(const CommandNode& node) { Command cmd; - cmd.executable = node.command_name; - cmd.args = node.arguments; + cmd.executable = expand_variables(node.command_name.text); + cmd.args.reserve(node.arguments.size()); + for (const auto& arg : node.arguments) { + std::string expanded_arg; + if (arg.quoted) { + // Only expand variables, do not split words + expanded_arg = expand_variables(arg.text); + } else { + // Expand variables and allow word splitting (future) + expanded_arg = expand_variables(arg.text); + } + cmd.args.emplace_back(expanded_arg, arg.quoted, arg.needs_expansion); + } if (!node.redirections.empty()) { std::cout << "Processing redirections for command: " << cmd.executable << "\n"; for (const auto& redir : node.redirections) { - //cmd.redirections.push_back(redir); if (redir.kind == RedirectKind::Input) { std::cout << " Input redirection from: " << redir.target << "\n"; - cmd.stdin_ = from_file(redir.target); + cmd.stdin_ = from_file(expand_variables(redir.target.text)); } else if (redir.kind == RedirectKind::OutputTruncate) { std::cout << " Output redirection to: " << redir.target << "\n"; - cmd.stdout_ = to_file(redir.target, OpenMode::WriteTruncate); + cmd.stdout_ = to_file(expand_variables(redir.target.text), OpenMode::WriteTruncate); } else if (redir.kind == RedirectKind::OutputAppend) { std::cout << " Output append redirection to: " << redir.target << "\n"; - cmd.stdout_ = to_file(redir.target, OpenMode::WriteAppend); + cmd.stdout_ = to_file(expand_variables(redir.target.text), OpenMode::WriteAppend); } } } else { @@ -260,9 +281,6 @@ class ShellInterpreter { return history_.items(); }; - std::string expand_variables(std::string_view input) { - - } }; diff --git a/src/lib/executor/executor_posix.cpp b/src/lib/executor/executor_posix.cpp index 87fb149..4684d1a 100644 --- a/src/lib/executor/executor_posix.cpp +++ b/src/lib/executor/executor_posix.cpp @@ -157,7 +157,7 @@ std::vector PlatformExecutionPolicy::convertArgv(const Command& cmd std::vector argv; argv.push_back(cmd.executable.c_str()); for (const auto& arg : cmd.args) { - argv.push_back(arg.c_str()); + argv.push_back(arg.value.c_str()); } argv.insert(argv.begin(), cmd.executable.filename().c_str()); argv.push_back(nullptr); // NULL-terminated @@ -234,7 +234,7 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { std::vector argv; argv.push_back(cmd.executable.c_str()); for (const auto& arg : cmd.args) { - argv.push_back(arg.c_str()); + argv.push_back(arg.value.c_str()); } argv.push_back(nullptr); // NULL-terminated diff --git a/src/lib/parser/ast_to_command_model.cpp b/src/lib/parser/ast_to_command_model.cpp new file mode 100644 index 0000000..5dcf94b --- /dev/null +++ b/src/lib/parser/ast_to_command_model.cpp @@ -0,0 +1,5 @@ +// Copyright (c) 2024 William Sollers +// SPDX-License-Identifier: BSD-2-Clause + +#include "shell/ast_to_command_model.hpp" +// Implementation is header-only for now. diff --git a/src/lib/parser/input_source.cpp b/src/lib/parser/input_source.cpp index a2d0e9d..2f99e29 100644 --- a/src/lib/parser/input_source.cpp +++ b/src/lib/parser/input_source.cpp @@ -124,4 +124,6 @@ std::string StringInputSource::source_name() const { return name_; } + + } // namespace wshell diff --git a/src/lib/parser/parser.cpp b/src/lib/parser/parser.cpp index 622c9ee..4f4102c 100644 --- a/src/lib/parser/parser.cpp +++ b/src/lib/parser/parser.cpp @@ -108,32 +108,22 @@ std::expected Parser::parse_assignment() { // do nothing } - if (check(TokenType::Semicolon)) { - return std::unexpected( - make_error(ParseErrorKind::SyntaxError, "Expected expression after '='")); - } - // detect incomplete input: let x =, let x = , let x = # comment - if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { - return std::unexpected( - make_error(ParseErrorKind::IncompleteInput, "Expected expression after '='")); + // Allow empty value: let VAR = + if (check(TokenType::Semicolon) || check(TokenType::EndOfFile) || check(TokenType::Newline)) { + return make_assignment(std::move(variable), ""); } // collect value tokens until newline/EOF/semicolon std::string value; + bool first = true; while (!check(TokenType::Newline) && !check(TokenType::EndOfFile) && !check(TokenType::Semicolon)) { Token t = current_token(); - if (!value.empty()) - value.push_back(' '); + if (!first) value.push_back(' '); + first = false; value += t.value; (void)lexer_.next_token(); // consume } - - if (value.empty()) { - return std::unexpected( - make_error(ParseErrorKind::IncompleteInput, "Expected expression after '='")); - } - return make_assignment(std::move(variable), std::move(value)); } @@ -168,9 +158,11 @@ std::expected Parser::parse_redirection() { make_error(ParseErrorKind::SyntaxError, "Expected redirection target")); } + // For now, treat all redirection targets as unquoted, needs_expansion=true + Word target_word{target.value, false, true}; (void)lexer_.next_token(); // consume target - return Redirection{redirect_kind_from_lexeme(op.value), target.value}; + return Redirection{redirect_kind_from_lexeme(op.value), std::move(target_word)}; } // ----------------------------------------------------------------------------- @@ -183,11 +175,11 @@ std::expected Parser::parse_simple_command() { return std::unexpected(make_error(ParseErrorKind::SyntaxError, "Expected command name")); } - std::string name = cmd_tok.value; - std::vector args; + // Command name as Word (unquoted, needs_expansion=true) + Word name_word{cmd_tok.value, false, true}; + std::vector args; - // consume the command name - (void)lexer_.next_token(); + (void)lexer_.next_token(); // consume the command name // collect arguments until a control token while (!check(TokenType::Newline) && !check(TokenType::EndOfFile) && !check(TokenType::Pipe) && @@ -195,14 +187,39 @@ std::expected Parser::parse_simple_command() { !check(TokenType::Redirect)) { Token t = current_token(); if (t.type == TokenType::Identifier || t.type == TokenType::Equals) { - args.push_back(t.value); - (void)lexer_.next_token(); // consume the argument + std::string val = t.value; + bool is_quoted = false; + // Handle quoted arguments (may contain spaces or nested quotes) + if (!val.empty() && val.front() == '"') { + is_quoted = true; + val.erase(0, 1); // remove leading quote + // Collect until closing quote (may span multiple tokens) + while (true) { + // If ends with quote, remove and break + if (!val.empty() && val.back() == '"') { + val.pop_back(); + break; + } + // Otherwise, add a space and next token + (void)lexer_.next_token(); + if (check(TokenType::Newline) || check(TokenType::EndOfFile) || check(TokenType::Pipe) || + check(TokenType::Semicolon) || check(TokenType::Background) || check(TokenType::Redirect)) { + // Unterminated quote, treat as is + break; + } + Token next = current_token(); + val += ' '; + val += next.value; + } + } + args.emplace_back(val, is_quoted, true); + (void)lexer_.next_token(); // consume the argument (or last part of quoted) } else { break; } } - return make_command(std::move(name), std::move(args)); + return make_command(std::move(name_word), std::move(args)); } // ----------------------------------------------------------------------------- @@ -249,13 +266,27 @@ std::expected Parser::parse_pipeline() { while (check(TokenType::Pipe)) { Token pipe_tok = peek_token(); - [[maybe_unused]] bool consumed = match(TokenType::Pipe); // consume '|' + // Save lexer state + auto lexer_state = lexer_; + (void)match(TokenType::Pipe); // consume '|' // Skip comments after a pipe while (match(TokenType::Comment)) { // nothing } + // Double pipe: SyntaxError + if (check(TokenType::Pipe)) { + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Unexpected '|' after '|'", peek_token().line, + peek_token().column}); + } + // Pipe followed by semicolon: SyntaxError + if (check(TokenType::Semicolon)) { + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Unexpected ';' after '|'", peek_token().line, + peek_token().column}); + } // Detect incomplete input: "cmd |", "cmd | # comment" if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, @@ -265,7 +296,13 @@ std::expected Parser::parse_pipeline() { auto next = parse_command(); if (!next) { - return std::unexpected(next.error()); + // If the error is IncompleteInput, propagate it upward (dangling pipe) + if (next.error().kind_ == ParseErrorKind::IncompleteInput) { + return std::unexpected(next.error()); + } + // Restore lexer state so parse_list can see the pipe + lexer_ = lexer_state; + break; } cmds.push_back(std::move(*next)); @@ -299,29 +336,35 @@ std::expected Parser::parse_list() { stmts.push_back(std::move(*first_pipe)); while (match(TokenType::Semicolon)) { - // Detect incomplete input: "cmd ;" + // Accept trailing semicolon at end of input (REPL or script) if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { - if (repl_mode_) { - // In REPL, semicolon at end means incomplete input - return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, - "Expected command after ';'", peek_token().line, - peek_token().column}); - } else { - // In scripts or multi-statement sequences, trailing semicolon is allowed - break; - } + break; + } + + // If the next token is a pipe or redirect, treat as incomplete input (continuation) + if (check(TokenType::Pipe) || check(TokenType::Redirect)) { + Token op_tok = peek_token(); + return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, + "Incomplete command at end of line", + op_tok.line, op_tok.column}); } auto next_pipe = parse_pipeline(); if (!next_pipe.has_value()) { + // Always propagate IncompleteInput upward for continuation + if (next_pipe.error().kind_ == ParseErrorKind::IncompleteInput) { + return std::unexpected(next_pipe.error()); + } + // Otherwise propagate any error return std::unexpected(next_pipe.error()); } - // After parsing a pipeline inside a list, check for dangling operators + // After parsing a pipeline inside a list, check for dangling operators (pipe/redirect) if (check(TokenType::Pipe) || check(TokenType::Redirect)) { + Token op_tok = peek_token(); return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, "Incomplete command at end of line", - peek_token().line, peek_token().column}); + op_tok.line, op_tok.column}); } stmts.push_back(std::move(*next_pipe)); @@ -414,22 +457,44 @@ std::expected, ParseError> Parser::parse_line() { program->add_statement(std::move(*stmt)); - // Allow a single trailing newline + + // Allow a single trailing newline or semicolon if (check(TokenType::Newline)) { (void)lexer_.next_token(); } + if (check(TokenType::Semicolon)) { + (void)lexer_.next_token(); + } // If leftover tokens exist, check if they indicate continuation if (!check(TokenType::EndOfFile)) { Token last = peek_token(); // Dangling operator at end of line → continuation - if (last.type == TokenType::Pipe || last.type == TokenType::Redirect || - last.type == TokenType::Semicolon) { + if (last.type == TokenType::Pipe || last.type == TokenType::Redirect) { return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, "Incomplete command at end of line", last.line, last.column}); } + // If a semicolon is present, allow a single trailing semicolon, but check for pipe after + if (last.type == TokenType::Semicolon) { + (void)lexer_.next_token(); + if (!check(TokenType::EndOfFile)) { + Token next = peek_token(); + if (next.type == TokenType::Pipe || next.type == TokenType::Redirect) { + // Always treat as IncompleteInput + return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, + "Incomplete command at end of line", next.line, + next.column}); + } + // Any other leftover tokens are a syntax error + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Unexpected tokens after statement", next.line, + next.column}); + } + // If only a trailing semicolon, accept + return program; + } // Otherwise it's a real syntax error return std::unexpected(ParseError{ParseErrorKind::SyntaxError, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f5ea775..ab04e2f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,8 +1,11 @@ if(ENABLE_TESTING) add_executable(wshell_tests + shell_substitution_tests.cpp + shell_history_tests.cpp test_command_parser.cpp - ../src/lib/ast/ast_printer.cpp - line_continuation_tests.cpp + ../src/lib/ast/ast_printer.cpp + line_continuation_tests.cpp + ) target_include_directories(wshell_tests diff --git a/test/line_continuation_tests.cpp b/test/line_continuation_tests.cpp index 9e4f056..bbbeef2 100644 --- a/test/line_continuation_tests.cpp +++ b/test/line_continuation_tests.cpp @@ -95,11 +95,11 @@ TEST(ParserContinuation_RedirectSyntaxErrors, RedirectHash) { // ----------------------------------------------------------------------------- TEST(ParserContinuation_AssignmentNeedsMoreInput, LetXEquals) { - expect_incomplete("let x ="); + expect_ok("let x ="); } TEST(ParserContinuation_AssignmentNeedsMoreInput, LetVarEqualsSpaces) { - expect_incomplete("let var = "); + expect_ok("let var = "); } // ----------------------------------------------------------------------------- @@ -119,11 +119,11 @@ TEST(ParserContinuation_AssignmentSyntaxErrors, MissingEquals) { // ----------------------------------------------------------------------------- TEST(ParserContinuation_SequenceNeedsMoreInput, EchoHiSemicolon) { - expect_incomplete("echo hi;"); + expect_ok("echo hi;"); } TEST(ParserContinuation_SequenceNeedsMoreInput, LsDashLSemicolonSpaces) { - expect_incomplete("ls -l ; "); + expect_ok("ls -l ; "); } // ----------------------------------------------------------------------------- @@ -143,7 +143,7 @@ TEST(ParserContinuation_SequenceSyntaxErrors, DoubleSemicolon) { // ----------------------------------------------------------------------------- TEST(ParserContinuation_MixedContinuationCases, PipeThenSemicolon) { - expect_incomplete("echo hi | grep h ;"); + expect_ok("echo hi | grep h ;"); } TEST(ParserContinuation_MixedContinuationCases, AssignmentThenPipe) { @@ -163,7 +163,7 @@ TEST(ParserContinuation_MixedSyntaxErrors, PipeSemicolon) { } TEST(ParserContinuation_MixedSyntaxErrors, LetXEqualsSemicolon) { - expect_syntax_error("let x = ;"); + expect_ok("let x = ;"); } TEST(ParserContinuation_MixedSyntaxErrors, RedirectPipeGrep) { @@ -207,7 +207,7 @@ TEST(ParserContinuation_EdgeCases, PipeComment) { } TEST(ParserContinuation_EdgeCases, LetComment) { - expect_incomplete("let x = # comment"); + expect_ok("let x = # comment"); } TEST(ParserContinuation_EdgeCases, RedirectComment) { diff --git a/test/shell_history_tests.cpp b/test/shell_history_tests.cpp new file mode 100644 index 0000000..9b32078 --- /dev/null +++ b/test/shell_history_tests.cpp @@ -0,0 +1,38 @@ +// shell_history_tests.cpp +// Unit tests for shell history functionality + +#include "gtest/gtest.h" +#include "shell/history.hpp" +#include + +using wshell::History; + +TEST(HistoryTests, PushAndSize) { + History history; + EXPECT_TRUE(history.empty()); + history.push("ls -l"); + history.push("echo hello"); + EXPECT_EQ(history.size(), 2); + const auto& items = history.items(); + ASSERT_EQ(items[0], "ls -l"); + ASSERT_EQ(items[1], "echo hello"); +} + +TEST(HistoryTests, SetMaxTrimsHistory) { + History history; + history.push("one"); + history.push("two"); + history.push("three"); + history.set_max(2); + EXPECT_EQ(history.size(), 2); + const auto& items = history.items(); + EXPECT_EQ(items[0], "two"); + EXPECT_EQ(items[1], "three"); +} + +TEST(HistoryTests, EmptyHistory) { + History history; + EXPECT_TRUE(history.empty()); + history.push("foo"); + EXPECT_FALSE(history.empty()); +} diff --git a/test/shell_substitution_tests.cpp b/test/shell_substitution_tests.cpp new file mode 100644 index 0000000..44603f6 --- /dev/null +++ b/test/shell_substitution_tests.cpp @@ -0,0 +1,123 @@ +// shell_substitution_tests.cpp +// Integration tests for shell variable substitution and expansion + +#include "gtest/gtest.h" +#include "shell/ast.hpp" +#include "shell/parser.hpp" +#include "shell/ast_to_command_model.hpp" +#include "shell/shell_interpreter.hpp" +#include "execution_policy.hpp" +#include +#include + +using namespace wshell; + + +TEST(ShellSubstitution, VariableExpansionInCommand) { + // Setup: let VAR=world; echo Hello $VAR + std::string input = "let VAR = world\necho Hello $VAR\n"; + auto parse_result = parse_program(input); + ASSERT_TRUE(parse_result.has_value()); + ProgramNode& program = *parse_result.value(); + // Setup interpreter and assign variable + wshell::StreamOutputDestination stdout_dest(std::cout, "stdout"); + wshell::StreamOutputDestination stderr_dest(std::cerr, "stderr"); + ShellInterpreter<> interp(stdout_dest, stderr_dest ); + interp.set_variable("VAR", "world"); + // Simulate expansion (replace $VAR with value) + // For this test, we just check that the variable is present in the AST and can be expanded + const auto& stmt = program.statements[1]; + ASSERT_TRUE(std::holds_alternative(stmt)); + const CommandNode& cmd = std::get(stmt); + ASSERT_EQ(cmd.command_name.text, "echo"); + ASSERT_EQ(cmd.arguments.size(), 2); + EXPECT_EQ(cmd.arguments[0].text, "Hello"); + EXPECT_EQ(cmd.arguments[1].text, "$VAR"); + // Now expand using interpreter logic + std::string expanded = interp.expand_variables(cmd.arguments[1].text); + EXPECT_EQ(expanded, "world"); +} + +TEST(ShellSubstitution, VariableExpansionWithQuotes) { + std::string input = "let X = 42\necho \"$X\"\n"; + auto parse_result = parse_program(input); + ASSERT_TRUE(parse_result.has_value()); + ProgramNode& program = *parse_result.value(); + wshell::StreamOutputDestination stdout_dest(std::cout, "stdout"); + wshell::StreamOutputDestination stderr_dest(std::cerr, "stderr"); + ShellInterpreter<> interp(stdout_dest, stderr_dest ); + interp.set_variable("X", "42"); + const auto& stmt = program.statements[1]; + ASSERT_TRUE(std::holds_alternative(stmt)); + const CommandNode& cmd = std::get(stmt); + ASSERT_EQ(cmd.arguments.size(), 1); + EXPECT_EQ(cmd.arguments[0].text, "$X"); + EXPECT_TRUE(cmd.arguments[0].quoted); + std::string expanded = interp.expand_variables(cmd.arguments[0].text); + EXPECT_EQ(expanded, "42"); +} + + +TEST(ShellSubstitution, VariableExpansion_EmptyVariable) { + std::string input = "let EMPTY = \necho $EMPTY\n"; + auto parse_result = parse_program(input); + ASSERT_TRUE(parse_result.has_value()); + ProgramNode& program = *parse_result.value(); + wshell::StreamOutputDestination stdout_dest(std::cout, "stdout"); + wshell::StreamOutputDestination stderr_dest(std::cerr, "stderr"); + ShellInterpreter<> interp(stdout_dest, stderr_dest ); + interp.set_variable("EMPTY", ""); + const auto& stmt = program.statements[1]; + ASSERT_TRUE(std::holds_alternative(stmt)); + const CommandNode& cmd = std::get(stmt); + ASSERT_EQ(cmd.arguments.size(), 1); + EXPECT_EQ(cmd.arguments[0].text, "$EMPTY"); + std::string expanded = interp.expand_variables(cmd.arguments[0].text); + EXPECT_EQ(expanded, ""); +} + +TEST(ShellSubstitution, VariableExpansion_MixedQuotes) { + std::string input = "let A = foo\nlet B = bar\necho \"$A $B\" $A\n"; + auto parse_result = parse_program(input); + ASSERT_TRUE(parse_result.has_value()); + ProgramNode& program = *parse_result.value(); + wshell::StreamOutputDestination stdout_dest(std::cout, "stdout"); + wshell::StreamOutputDestination stderr_dest(std::cerr, "stderr"); + ShellInterpreter<> interp(stdout_dest, stderr_dest ); + interp.set_variable("A", "foo"); + interp.set_variable("B", "bar"); + const auto& stmt = program.statements[2]; + ASSERT_TRUE(std::holds_alternative(stmt)); + const CommandNode& cmd = std::get(stmt); + ASSERT_EQ(cmd.arguments.size(), 2); + // First arg is quoted, should expand both vars, no splitting + EXPECT_EQ(cmd.arguments[0].text, "$A $B"); + EXPECT_TRUE(cmd.arguments[0].quoted); + std::string expanded0 = interp.expand_variables(cmd.arguments[0].text); + EXPECT_EQ(expanded0, "foo bar"); + // Second arg is unquoted, should expand $A + EXPECT_EQ(cmd.arguments[1].text, "$A"); + EXPECT_FALSE(cmd.arguments[1].quoted); + std::string expanded1 = interp.expand_variables(cmd.arguments[1].text); + EXPECT_EQ(expanded1, "foo"); +} + +TEST(ShellSubstitution, VariableExpansion_NestedQuotesLiteral) { + std::string input = "let X = 42\necho \"'Value: $X'\"\n"; + auto parse_result = parse_program(input); + ASSERT_TRUE(parse_result.has_value()); + ProgramNode& program = *parse_result.value(); + wshell::StreamOutputDestination stdout_dest(std::cout, "stdout"); + wshell::StreamOutputDestination stderr_dest(std::cerr, "stderr"); + ShellInterpreter<> interp(stdout_dest, stderr_dest ); + interp.set_variable("X", "42"); + const auto& stmt = program.statements[1]; + ASSERT_TRUE(std::holds_alternative(stmt)); + const CommandNode& cmd = std::get(stmt); + ASSERT_EQ(cmd.arguments.size(), 1); + // The argument is quoted, but contains a literal single quote + EXPECT_EQ(cmd.arguments[0].text, "'Value: $X'"); + EXPECT_TRUE(cmd.arguments[0].quoted); + std::string expanded = interp.expand_variables(cmd.arguments[0].text); + EXPECT_EQ(expanded, "'Value: 42'"); +} From 395eb89ce02004d8e04348fa8b4692fae6473f16 Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 21:58:34 -0500 Subject: [PATCH 04/14] built ins begun --- .github/AI_CODING_GUIDELINES.md | 5 + COPILOT_INSTRUCTIONS.md | 5 + include/shell/built_ins.hpp | 111 +++++++++----- include/shell/history.hpp | 6 +- include/shell/parser.hpp | 10 +- include/shell/shell_interpreter.hpp | 53 ++++--- out.txt | 24 +++ src/lib/CMakeLists.txt | 6 + src/lib/builtins/built_ins.cpp | 1 + src/lib/builtins/cd_builtin.cpp | 25 ++++ src/lib/builtins/exit_builtin.cpp | 15 ++ src/lib/builtins/history_builtin.cpp | 16 ++ src/lib/builtins/kill_builtin.cpp | 33 +++++ src/lib/builtins/pwd_builtin.cpp | 19 +++ src/lib/parser/built_ins.cpp | 0 src/lib/parser/parser.cpp | 113 +++++++------- test/line_continuation_tests.cpp | 212 +++++++++++++++++++++++++-- 17 files changed, 529 insertions(+), 125 deletions(-) create mode 100644 out.txt create mode 100644 src/lib/builtins/built_ins.cpp create mode 100644 src/lib/builtins/cd_builtin.cpp create mode 100644 src/lib/builtins/exit_builtin.cpp create mode 100644 src/lib/builtins/history_builtin.cpp create mode 100644 src/lib/builtins/kill_builtin.cpp create mode 100644 src/lib/builtins/pwd_builtin.cpp create mode 100644 src/lib/parser/built_ins.cpp diff --git a/.github/AI_CODING_GUIDELINES.md b/.github/AI_CODING_GUIDELINES.md index 849edc3..90ddc9b 100644 --- a/.github/AI_CODING_GUIDELINES.md +++ b/.github/AI_CODING_GUIDELINES.md @@ -277,6 +277,11 @@ shell/ - For performance regression testing ``` +#### 6. Acceptance test +All text from unit test that is used to test shell parsing must be validated in the appropriate shell + + + ### Utility Scripts All scripts in `scripts/` directory **MUST** have both: diff --git a/COPILOT_INSTRUCTIONS.md b/COPILOT_INSTRUCTIONS.md index 849edc3..90ddc9b 100644 --- a/COPILOT_INSTRUCTIONS.md +++ b/COPILOT_INSTRUCTIONS.md @@ -277,6 +277,11 @@ shell/ - For performance regression testing ``` +#### 6. Acceptance test +All text from unit test that is used to test shell parsing must be validated in the appropriate shell + + + ### Utility Scripts All scripts in `scripts/` directory **MUST** have both: diff --git a/include/shell/built_ins.hpp b/include/shell/built_ins.hpp index f736a1b..17a93fa 100644 --- a/include/shell/built_ins.hpp +++ b/include/shell/built_ins.hpp @@ -1,10 +1,11 @@ -#pragma once +#pragma once #include #include #include #include +#include #include #include #include @@ -14,54 +15,98 @@ #include "shell_process_context.h" namespace wshell { +// Interface for all builtin functions + +// Interface for all builtin functions +class BuiltinFunction { + public: + virtual ~BuiltinFunction() = default; + // args: arguments passed to the builtin + // ctx: shell process context (for env, cwd, etc) + // Returns: exit code (0 = success) + virtual int invoke(const std::vector& args, ShellProcessContext& ctx) = 0; +}; + +// Implementation for 'cd' +class CdBuiltin : public BuiltinFunction { + public: + int invoke(const std::vector& args, ShellProcessContext& ctx) override; +}; + +// Implementation for 'pwd' +class PwdBuiltin : public BuiltinFunction { + public: + int invoke(const std::vector& args, ShellProcessContext& ctx) override; +}; + +// Implementation for 'exit' +class ExitBuiltin : public BuiltinFunction { + public: + int invoke(const std::vector& args, ShellProcessContext& ctx) override; +}; -constexpr std::array builtinCommands = { - "cd", "exit", "export", "history", "pwd", "set", "unset" +// Implementation for 'kill' +class KillBuiltin : public BuiltinFunction { + public: + int invoke(const std::vector& args, ShellProcessContext& ctx) override; }; -constexpr std::array, 4> builtinVariables = {{ - {"PS1", "8=> "}, - {"PS2", ": "}, - {"HISTORY_SIZE", "10"}, - {"SHELL", "/bin/wshell"}, -}}; +// Forward declaration for History +class History; +// Implementation for 'history' +class HistoryBuiltin : public BuiltinFunction { + public: + explicit HistoryBuiltin(); + int invoke(const std::vector& args, ShellProcessContext& ctx) override; +}; + +// Default built-in shell variables and their values +static const std::unordered_map builtinVariablesDefault = { + {"PS1", "8=> "}, {"PS2", ": "}, {"HISTORY_SIZE", "100"}, {"SHELL", "/bin/wshell"}}; class BuiltIns { -public: - BuiltIns() { - for ( const auto& [name, value] : builtinVariables ) { - builtInvariables_.emplace(name, value); - } - for ( const auto& cmd : builtinCommands ) { - builtinsCommands_.emplace(cmd); - } - //add vars for args - ShellProcessContext ctx = ShellProcessContext(); - for ( int i = 0; i < ctx.argc; i++) { - auto currentKey = "?" + std::to_string(i); - builtInvariables_.emplace(currentKey, ctx.argv[i]); - unmodifiableBuiltinVariables_[currentKey]= ctx.argv[i]; + public: + BuiltIns(History* history_ptr = nullptr) { + // Initialize built-in variable map with defaults + builtinVariables_ = builtinVariablesDefault; + // Register builtin function implementations + builtinFunctionMap_["cd"] = std::make_unique(); + builtinFunctionMap_["pwd"] = std::make_unique(); + builtinFunctionMap_["exit"] = std::make_unique(); + builtinFunctionMap_["kill"] = std::make_unique(); + builtinFunctionMap_["history"] = std::make_unique(); - } } [[nodiscard]] bool is_builtin_command(const std::string& cmd) const noexcept { - return builtinsCommands_.find(cmd) != builtinsCommands_.end(); + return builtinFunctionMap_.find(cmd) != builtinFunctionMap_.end(); } - [[nodiscard]] std::optional get_builtin_variable(const std::string& var) const noexcept { - auto it = builtInvariables_.find(var); - if ( it != builtInvariables_.end() ) { + [[nodiscard]] std::optional + get_builtin_variable(const std::string& var) const noexcept { + auto it = builtinVariables_.find(var); + if (it != builtinVariables_.end()) { return it->second; } return std::nullopt; } -private: - std::unordered_map builtInvariables_; - std::set builtinsCommands_; - std::map unmodifiableBuiltinVariables_ {}; + void set_builtin_variable(const std::string& var, const std::string& value) { + builtinVariables_[var] = value; + } + + BuiltinFunction* get_builtin_function(const std::string& cmd) const { + auto it = builtinFunctionMap_.find(cmd); + if (it != builtinFunctionMap_.end()) { + return it->second.get(); + } + return nullptr; + } + + private: + std::unordered_map builtinVariables_; + std::unordered_map> builtinFunctionMap_; }; -} \ No newline at end of file +} // namespace wshell \ No newline at end of file diff --git a/include/shell/history.hpp b/include/shell/history.hpp index b2a75c8..9ee0a7c 100644 --- a/include/shell/history.hpp +++ b/include/shell/history.hpp @@ -1,13 +1,17 @@ #pragma once + #include #include +#include +#include #include #include #include #include #include #include +#include #include "execution_policy.hpp" // for get_home_directory @@ -17,7 +21,7 @@ namespace wshell { class History { using value_type = std::string; public: - //TODO: find portable way to lookup home directory + explicit History(std::size_t max_items = HISTORY_DEFAULT_SIZE) : max_items_(max_items) { history_.reserve(max_items); diff --git a/include/shell/parser.hpp b/include/shell/parser.hpp index c4f215f..e042d56 100644 --- a/include/shell/parser.hpp +++ b/include/shell/parser.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "ast.hpp" #include "ast_printer.hpp" @@ -26,17 +27,20 @@ struct ParseError { std::string message_; std::size_t line_{0}; std::size_t column_{0}; + std::source_location location_ = std::source_location::current(); ParseError(ParseErrorKind theKind = ParseErrorKind::SyntaxError, std::string msg = "Unknown Error.", std::size_t ln = 0, - std::size_t col = 0) - : kind_{theKind}, message_(std::move(msg)), line_(ln), column_(col) {} + std::size_t col = 0, + std::source_location loc = std::source_location::current()) + : kind_{theKind}, message_(std::move(msg)), line_(ln), column_(col), location_(loc) {} [[nodiscard]] std::string to_string() const { return "Parse error at line " + std::to_string(line_) + ", column " + std::to_string(column_) - + ": " + message_; + + ": " + message_ + + " [at " + location_.file_name() + ":" + std::to_string(location_.line()) + "]"; } }; diff --git a/include/shell/shell_interpreter.hpp b/include/shell/shell_interpreter.hpp index 0940301..7769e6b 100644 --- a/include/shell/shell_interpreter.hpp +++ b/include/shell/shell_interpreter.hpp @@ -39,12 +39,14 @@ template class ShellInterpreter { public: /// Construct with output destination for messages - explicit ShellInterpreter(wshell::IOutputDestination& output, - wshell::IOutputDestination& error_output) - : executor_{}, - variables_{}, - output_(output), - error_output_(error_output), builtins_{}, history_{} {} + explicit ShellInterpreter(wshell::IOutputDestination& output, + wshell::IOutputDestination& error_output) + : executor_{}, + variables_{}, + output_(output), + error_output_(error_output), + history_{}, + builtins_{&history_} {} /// Execute a parsed program (AST) [[nodiscard]] int execute_program(const ProgramNode& program) { @@ -140,17 +142,11 @@ class ShellInterpreter { std::map variables_; wshell::IOutputDestination& output_; wshell::IOutputDestination& error_output_; - wshell::BuiltIns builtins_; wshell::History history_; + wshell::BuiltIns builtins_; + ShellProcessContext process_context_; - [[nodiscard]] std::string replaceVariables(const std::string& line) { - //Find occurences of ${} in line and replace with the - //varable if possible. if not found replace with "" - - //TODO replace variables - return line; - } /// Execute a single statement [[nodiscard]] std::expected @@ -197,16 +193,39 @@ class ShellInterpreter { /// Execute a command [[nodiscard]] std::expected execute_command(const CommandNode& node) { + std::string cmd_name = expand_variables(node.command_name.text); + std::vector args; + args.push_back(cmd_name); + for (const auto& arg : node.arguments) { + std::string expanded_arg; + if (arg.quoted) { + expanded_arg = expand_variables(arg.text); + } else { + expanded_arg = expand_variables(arg.text); + } + args.push_back(expanded_arg); + } + + // Check for built-in + if (builtins_.is_builtin_command(cmd_name)) { + auto* fn = builtins_.get_builtin_function(cmd_name); + if (fn) { + int code = fn->invoke(args, process_context_); + return code; + } else { + return std::unexpected("Builtin not implemented: " + cmd_name); + } + } + + // External command execution (as before) Command cmd; - cmd.executable = expand_variables(node.command_name.text); + cmd.executable = cmd_name; cmd.args.reserve(node.arguments.size()); for (const auto& arg : node.arguments) { std::string expanded_arg; if (arg.quoted) { - // Only expand variables, do not split words expanded_arg = expand_variables(arg.text); } else { - // Expand variables and allow word splitting (future) expanded_arg = expand_variables(arg.text); } cmd.args.emplace_back(expanded_arg, arg.quoted, arg.needs_expansion); diff --git a/out.txt b/out.txt new file mode 100644 index 0000000..bf0eb9c --- /dev/null +++ b/out.txt @@ -0,0 +1,24 @@ +bench +benchmark_results +build +cmake +cmake-build-debug +CMakeLists.txt +CMakePresets.json +compile_commands.json +COPILOT_INSTRUCTIONS.md +docs +Doxyfile +flamegraphs +fuzz +fuzz_corpus +include +LICENSE +out.txt +PROJECT_SUMMARY.md +QUICKSTART.md +README.md +scripts +src +test +third_party diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 9a8e11e..4612e37 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -7,9 +7,15 @@ file(GLOB_RECURSE WSHELL_SOURCES_EXECUTOR ${CMAKE_CURRENT_SOURCE_DIR}/executor/*.cpp ) +file(GLOB_RECURSE WSHELL_SOURCES_BUILTINS + ${CMAKE_CURRENT_SOURCE_DIR}/builtins/*.cpp +) + + add_library(wshell_lib STATIC ${WSHELL_SOURCES_PARSER} ${WSHELL_SOURCES_EXECUTOR} + ${WSHELL_SOURCES_BUILTINS} ../../include/shell/ast_printer.hpp ast/ast_printer.cpp executor/shell_process_context.cpp diff --git a/src/lib/builtins/built_ins.cpp b/src/lib/builtins/built_ins.cpp new file mode 100644 index 0000000..7df81ac --- /dev/null +++ b/src/lib/builtins/built_ins.cpp @@ -0,0 +1 @@ +// This file is intentionally left blank. All built-in implementations are now in their own *_builtin.cpp files in this directory. diff --git a/src/lib/builtins/cd_builtin.cpp b/src/lib/builtins/cd_builtin.cpp new file mode 100644 index 0000000..4974c72 --- /dev/null +++ b/src/lib/builtins/cd_builtin.cpp @@ -0,0 +1,25 @@ +#include +#include +#include + +#include "shell/built_ins.hpp" + + +namespace wshell { + +int CdBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + const char* dir = nullptr; + if (args.size() < 2) { + dir = std::getenv("HOME"); + if (!dir) dir = "."; + } else { + dir = args[1].c_str(); + } + if (chdir(dir) != 0) { + std::perror("cd"); + return 1; + } + return 0; +} + +} // namespace wshell diff --git a/src/lib/builtins/exit_builtin.cpp b/src/lib/builtins/exit_builtin.cpp new file mode 100644 index 0000000..ebafb22 --- /dev/null +++ b/src/lib/builtins/exit_builtin.cpp @@ -0,0 +1,15 @@ +#include + +#include "shell/built_ins.hpp" + +namespace wshell { + +int ExitBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + int code = 0; + if (args.size() > 1) { + code = std::atoi(args[1].c_str()); + } + std::exit(code); +} + +} // namespace wshell diff --git a/src/lib/builtins/history_builtin.cpp b/src/lib/builtins/history_builtin.cpp new file mode 100644 index 0000000..4de2ebf --- /dev/null +++ b/src/lib/builtins/history_builtin.cpp @@ -0,0 +1,16 @@ +#include +#include + +#include "shell/built_ins.hpp" +#include "shell/history.hpp" + + +namespace wshell { + +HistoryBuiltin::HistoryBuiltin() {} + +int HistoryBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + return 1; //placeholder +} + +} // namespace wshell diff --git a/src/lib/builtins/kill_builtin.cpp b/src/lib/builtins/kill_builtin.cpp new file mode 100644 index 0000000..4233db1 --- /dev/null +++ b/src/lib/builtins/kill_builtin.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +#include "shell/built_ins.hpp" + +namespace wshell { + +int KillBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + if (args.size() < 2) { + std::cerr << "kill: missing pid" << std::endl; + return 1; + } + int sig = SIGTERM; + size_t argi = 1; + if (args[1].rfind("-", 0) == 0) { + sig = std::atoi(args[1].c_str() + 1); + argi = 2; + } + if (args.size() <= argi) { + std::cerr << "kill: missing pid" << std::endl; + return 1; + } + //TODO make this work for Windows + pid_t pid = std::atoi(args[argi].c_str()); + if (kill(pid, sig) != 0) { + std::perror("kill"); + return 1; + } + return 0; +} + +} // namespace wshell diff --git a/src/lib/builtins/pwd_builtin.cpp b/src/lib/builtins/pwd_builtin.cpp new file mode 100644 index 0000000..aeda9b1 --- /dev/null +++ b/src/lib/builtins/pwd_builtin.cpp @@ -0,0 +1,19 @@ +#include +#include +#include + +#include "shell/built_ins.hpp" +namespace wshell { + +int PwdBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf))) { + std::cout << buf << std::endl; + return 0; + } else { + std::perror("pwd"); + return 1; + } +} + +} // namespace wshell diff --git a/src/lib/parser/built_ins.cpp b/src/lib/parser/built_ins.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/parser/parser.cpp b/src/lib/parser/parser.cpp index 4f4102c..4282b31 100644 --- a/src/lib/parser/parser.cpp +++ b/src/lib/parser/parser.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-2-Clause #include "shell/parser.hpp" +#include #include "shell/ast.hpp" #include "shell/ast_printer.hpp" @@ -142,7 +143,7 @@ std::expected Parser::parse_redirection() { // Detect incomplete input: "cmd >" or "cmd <" if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { return std::unexpected(ParseError{ParseErrorKind::SyntaxError, - "Expected redirection target", op.line, op.column}); + "Expected redirection target", op.line, op.column, std::source_location::current()}); } Token target = current_token(); @@ -266,7 +267,6 @@ std::expected Parser::parse_pipeline() { while (check(TokenType::Pipe)) { Token pipe_tok = peek_token(); - // Save lexer state auto lexer_state = lexer_; (void)match(TokenType::Pipe); // consume '|' @@ -279,32 +279,29 @@ std::expected Parser::parse_pipeline() { if (check(TokenType::Pipe)) { return std::unexpected(ParseError{ParseErrorKind::SyntaxError, "Unexpected '|' after '|'", peek_token().line, - peek_token().column}); + peek_token().column, std::source_location::current()}); } - // Pipe followed by semicolon: SyntaxError + // Pipe followed by semicolon: SyntaxError (matches Bash) if (check(TokenType::Semicolon)) { return std::unexpected(ParseError{ParseErrorKind::SyntaxError, - "Unexpected ';' after '|'", peek_token().line, - peek_token().column}); + "Syntax error near unexpected token ';' after '|'", + peek_token().line, peek_token().column, std::source_location::current()}); } - // Detect incomplete input: "cmd |", "cmd | # comment" + // Trailing pipe: Syntax error (matches Bash) if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { - return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, - "Expected command after '|'", pipe_tok.line, - pipe_tok.column}); + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Syntax error: unexpected end of input after '|'", + pipe_tok.line, pipe_tok.column, std::source_location::current()}); } auto next = parse_command(); if (!next) { - // If the error is IncompleteInput, propagate it upward (dangling pipe) if (next.error().kind_ == ParseErrorKind::IncompleteInput) { return std::unexpected(next.error()); } - // Restore lexer state so parse_list can see the pipe lexer_ = lexer_state; break; } - cmds.push_back(std::move(*next)); } @@ -336,35 +333,33 @@ std::expected Parser::parse_list() { stmts.push_back(std::move(*first_pipe)); while (match(TokenType::Semicolon)) { - // Accept trailing semicolon at end of input (REPL or script) + // Bash: Trailing semicolons are allowed (no syntax error) if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { break; } - // If the next token is a pipe or redirect, treat as incomplete input (continuation) - if (check(TokenType::Pipe) || check(TokenType::Redirect)) { + // If the next token is a pipe, treat as syntax error (matches Bash) + if (check(TokenType::Pipe)) { Token op_tok = peek_token(); - return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, - "Incomplete command at end of line", - op_tok.line, op_tok.column}); + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Syntax error: unexpected '" + op_tok.value + "' after ';'", + op_tok.line, op_tok.column, std::source_location::current()}); } auto next_pipe = parse_pipeline(); if (!next_pipe.has_value()) { - // Always propagate IncompleteInput upward for continuation if (next_pipe.error().kind_ == ParseErrorKind::IncompleteInput) { return std::unexpected(next_pipe.error()); } - // Otherwise propagate any error return std::unexpected(next_pipe.error()); } - // After parsing a pipeline inside a list, check for dangling operators (pipe/redirect) - if (check(TokenType::Pipe) || check(TokenType::Redirect)) { + // After parsing a pipeline inside a list, check for dangling operators (pipe only) + if (check(TokenType::Pipe)) { Token op_tok = peek_token(); - return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, - "Incomplete command at end of line", - op_tok.line, op_tok.column}); + return std::unexpected(ParseError{ParseErrorKind::SyntaxError, + "Syntax error: unexpected '" + op_tok.value + "' after statement", + op_tok.line, op_tok.column, std::source_location::current()}); } stmts.push_back(std::move(*next_pipe)); @@ -394,13 +389,37 @@ std::expected Parser::parse_statement() { return StatementNode{std::move(*c)}; } - // Assignment + // Assignment (let x = ...) if (check(TokenType::Let)) { auto a = parse_assignment(); if (!a) { return std::unexpected(a.error()); } - return StatementNode{std::move(*a)}; + // If next token is semicolon or command, parse as sequence (let assignment; ...) + if (check(TokenType::Semicolon)) { + // Parse the rest of the sequence + std::vector stmts; + stmts.push_back(StatementNode{std::move(*a)}); + while (match(TokenType::Semicolon)) { + // Allow trailing semicolon (Bash: ok) + if (check(TokenType::EndOfFile) || check(TokenType::Newline)) { + break; + } + auto next = parse_list(); + if (!next) { + return std::unexpected(next.error()); + } + stmts.push_back(std::move(*next)); + } + if (stmts.size() == 1) { + return stmts.front(); + } + SequenceNode seq = make_sequence(std::move(stmts)); + return StatementNode{std::move(seq)}; + } else { + // Single assignment only + return StatementNode{std::move(*a)}; + } } // Command / Pipeline / Sequence @@ -458,45 +477,23 @@ std::expected, ParseError> Parser::parse_line() { program->add_statement(std::move(*stmt)); - // Allow a single trailing newline or semicolon + // Allow a single trailing newline if (check(TokenType::Newline)) { (void)lexer_.next_token(); } + + // Allow a single trailing semicolon (with or without whitespace) if (check(TokenType::Semicolon)) { (void)lexer_.next_token(); + // Allow trailing whitespace (newlines) after semicolon + while (check(TokenType::Newline)) { + (void)lexer_.next_token(); + } } - // If leftover tokens exist, check if they indicate continuation + // If leftover tokens exist, treat as syntax error if (!check(TokenType::EndOfFile)) { Token last = peek_token(); - - // Dangling operator at end of line → continuation - if (last.type == TokenType::Pipe || last.type == TokenType::Redirect) { - return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, - "Incomplete command at end of line", last.line, - last.column}); - } - // If a semicolon is present, allow a single trailing semicolon, but check for pipe after - if (last.type == TokenType::Semicolon) { - (void)lexer_.next_token(); - if (!check(TokenType::EndOfFile)) { - Token next = peek_token(); - if (next.type == TokenType::Pipe || next.type == TokenType::Redirect) { - // Always treat as IncompleteInput - return std::unexpected(ParseError{ParseErrorKind::IncompleteInput, - "Incomplete command at end of line", next.line, - next.column}); - } - // Any other leftover tokens are a syntax error - return std::unexpected(ParseError{ParseErrorKind::SyntaxError, - "Unexpected tokens after statement", next.line, - next.column}); - } - // If only a trailing semicolon, accept - return program; - } - - // Otherwise it's a real syntax error return std::unexpected(ParseError{ParseErrorKind::SyntaxError, "Unexpected tokens after statement", last.line, last.column}); diff --git a/test/line_continuation_tests.cpp b/test/line_continuation_tests.cpp index bbbeef2..db3d7f3 100644 --- a/test/line_continuation_tests.cpp +++ b/test/line_continuation_tests.cpp @@ -1,7 +1,6 @@ #include "shell/ast.hpp" #include "shell/ast_printer.hpp" #include "shell/parser.hpp" - #include using namespace wshell; @@ -13,15 +12,11 @@ std::string parse_error_toString(ParseErrorKind err) { return "Syntax Error\n"; } -static void expect_incomplete(const std::string& input) { - auto result = wshell::parse_line(input); - ASSERT_FALSE(result.has_value()); - ASSERT_EQ(result.error().kind_, wshell::ParseErrorKind::IncompleteInput); -} static void expect_syntax_error(const std::string& input) { auto result = wshell::parse_line(input); - ASSERT_FALSE(result.has_value()); + ASSERT_FALSE(result.has_value()) << "Expected parse error, got success"; + std::cerr << "Parse error: " << result.error().to_string() << std::endl; ASSERT_EQ(result.error().kind_, wshell::ParseErrorKind::SyntaxError); } @@ -29,21 +24,164 @@ static void expect_ok(const std::string& input) { auto result = wshell::parse_line(input); ASSERT_TRUE(result.has_value()); } +// ----------------------------------------------------------------------------- +// Focused investigation: bracket the error for 'let x = 42 ; echo hi |' +// ----------------------------------------------------------------------------- +// (Moved after helper functions and includes) +// ----------------------------------------------------------------------------- +// Expanded diagnostic tests for assignment, semicolon, and pipe edge cases +// ----------------------------------------------------------------------------- + +// Assignment with multiple semicolons and pipes +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonPipeCommand) { + expect_syntax_error("FOO=bar; | grep foo"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonPipeSemicolon) { + expect_syntax_error("FOO=bar; |;"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonPipePipe) { + expect_syntax_error("FOO=bar; ||"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonCommandPipe) { + expect_syntax_error("FOO=bar; echo hi |"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonCommandPipeCommand) { + expect_ok("FOO=bar; echo hi | grep foo"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonCommandPipeSemicolon) { + expect_syntax_error("FOO=bar; echo hi |;"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonCommandPipePipe) { + expect_syntax_error("FOO=bar; echo hi ||"); +} + +// Chained assignments and pipes +TEST(ParserContinuation_Diagnostics, MultipleAssignmentsSemicolonPipe) { + expect_syntax_error("FOO=bar; BAZ=qux |"); +} + +TEST(ParserContinuation_Diagnostics, MultipleAssignmentsSemicolonPipeCommand) { + expect_ok("FOO=bar; BAZ=qux | grep foo"); +} + +TEST(ParserContinuation_Diagnostics, MultipleAssignmentsSemicolonCommandPipe) { + expect_syntax_error("FOO=bar; BAZ=qux; echo hi |"); +} + +TEST(ParserContinuation_Diagnostics, MultipleAssignmentsSemicolonCommandPipeCommand) { + expect_ok("FOO=bar; BAZ=qux; echo hi | grep foo"); +} + +// Nested semicolons and pipes +TEST(ParserContinuation_Diagnostics, SemicolonPipeSemicolon) { + expect_syntax_error("; | ;"); +} + +TEST(ParserContinuation_Diagnostics, PipeSemicolonPipe) { + expect_syntax_error("| ; |"); +} + +TEST(ParserContinuation_Diagnostics, SemicolonPipeCommand) { + expect_syntax_error("; | grep foo"); +} + +TEST(ParserContinuation_Diagnostics, PipeSemicolonCommand) { + expect_syntax_error("| ; grep foo"); +} + +// Assignment with whitespace and comments +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonPipeComment) { + expect_syntax_error("FOO=bar; | # comment"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonCommandPipeComment) { + expect_syntax_error("FOO=bar; echo hi | # comment"); +} +// End of file + +// ----------------------------------------------------------------------------- +// Focused investigation: bracket the error for 'let x = 42 ; echo hi |' +// ----------------------------------------------------------------------------- +// Control: just a pipeline after let assignment (should be ok) +TEST(ParserContinuation_Focused, LetAssignmentThenCommand) { + expect_ok("let x = 42 ; echo hi"); +} + +// Control: just a pipeline after let assignment, no semicolon (should be incomplete) +TEST(ParserContinuation_Focused, LetAssignmentCommandPipe) { + expect_ok("let x = 42 echo hi |"); +} + +// Control: just a pipeline after command (should be incomplete) +TEST(ParserContinuation_Focused, CommandPipe) { + expect_syntax_error("echo hi |"); +} + +// Control: let assignment only (should be ok) +TEST(ParserContinuation_Focused, LetAssignmentOnly) { + expect_ok("let x = 42"); +} + +// Control: let assignment with semicolon only (should be ok) +TEST(ParserContinuation_Focused, LetAssignmentSemicolon) { + expect_ok("let x = 42 ;"); +} + +// Variant: let assignment, semicolon, command, no pipe (should be ok) +TEST(ParserContinuation_Focused, LetAssignmentSemicolonCommand) { + expect_ok("let x = 42 ; echo hi"); +} + +// Variant: let assignment, semicolon, command, pipe (should be incomplete) +TEST(ParserContinuation_Focused, LetAssignmentSemicolonCommandPipe) { + expect_syntax_error("let x = 42 ; echo hi |"); +} + +// Variant: let assignment, semicolon, pipe only (should be incomplete) +TEST(ParserContinuation_Focused, LetAssignmentSemicolonPipe) { + expect_syntax_error("let x = 42 ; |"); +} + +// Variant: let assignment, command, pipe (should be incomplete) +TEST(ParserContinuation_Focused, LetAssignmentCommandPipeNoSemicolon) { + expect_ok("let x = 42 echo hi |"); +} + +// Variant: assignment (no let), semicolon, command, pipe (should be incomplete) +TEST(ParserContinuation_Focused, AssignmentSemicolonCommandPipe) { + expect_syntax_error("FOO=bar ; echo hi |"); +} + +// Variant: assignment (no let), semicolon, pipe (should be incomplete) +TEST(ParserContinuation_Focused, AssignmentSemicolonPipe) { + expect_syntax_error("FOO=bar ; |"); +} + +// Variant: assignment (no let), command, pipe (should be incomplete) +TEST(ParserContinuation_Focused, AssignmentCommandPipeNoSemicolon) { + expect_syntax_error("FOO=bar echo hi |"); +} // ----------------------------------------------------------------------------- // PipelineNeedsMoreInput // ----------------------------------------------------------------------------- TEST(ParserContinuation_PipelineNeedsMoreInput, EchoFooPipe) { - expect_incomplete("echo foo |"); + expect_syntax_error("echo foo |"); } TEST(ParserContinuation_PipelineNeedsMoreInput, LsDashLPipeSpaces) { - expect_incomplete("ls -l | "); + expect_syntax_error("ls -l | "); } TEST(ParserContinuation_PipelineNeedsMoreInput, CatFilePipeComment) { - expect_incomplete("cat file | # comment"); + expect_syntax_error("cat file | # comment"); } // ----------------------------------------------------------------------------- @@ -147,7 +285,7 @@ TEST(ParserContinuation_MixedContinuationCases, PipeThenSemicolon) { } TEST(ParserContinuation_MixedContinuationCases, AssignmentThenPipe) { - expect_incomplete("let x = 42 ; echo hi |"); + expect_syntax_error("let x = 42 ; echo hi |"); } TEST(ParserContinuation_MixedContinuationCases, RedirectThenPipe) { @@ -203,7 +341,7 @@ TEST(ParserContinuation_CompleteStatements, TwoCommandsSpacedSemicolon) { // ----------------------------------------------------------------------------- TEST(ParserContinuation_EdgeCases, PipeComment) { - expect_incomplete("echo hi | # comment"); + expect_syntax_error("echo hi | # comment"); } TEST(ParserContinuation_EdgeCases, LetComment) { @@ -224,4 +362,52 @@ TEST(ParserContinuation_EdgeCases, CommentOnlyOk) { TEST(ParserContinuation_EdgeCases, RedirectMissingTarget) { expect_syntax_error("ls >"); -} \ No newline at end of file +} + +// ----------------------------------------------------------------------------- +// Diagnostic tests for assignment and pipe edge cases +// ----------------------------------------------------------------------------- + +TEST(ParserContinuation_Diagnostics, MinimalAssignmentThenPipe) { + expect_syntax_error("FOO=bar |"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentThenPipeThenCommand) { + expect_ok("FOO=bar | echo hi"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonPipe) { + expect_syntax_error("FOO=bar; |"); +} + +TEST(ParserContinuation_Diagnostics, PipeAtStart) { + expect_syntax_error("| echo hi"); +} + +TEST(ParserContinuation_Diagnostics, PipeAtEnd) { + expect_syntax_error("echo hi |"); +} + +TEST(ParserContinuation_Diagnostics, MultipleAssignmentsThenPipe) { + expect_syntax_error("FOO=bar BAZ=qux |"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentSemicolonCommand) { + // Should parse as complete, not error + auto result = wshell::parse_line("FOO=bar; echo hi"); + ASSERT_TRUE(result.has_value()) << "Expected success, got error: " << result.error().to_string(); +} + +TEST(ParserContinuation_Diagnostics, AssignmentOnly) { + auto result = wshell::parse_line("FOO=bar"); + ASSERT_TRUE(result.has_value()) << "Expected success, got error: " << result.error().to_string(); +} + +TEST(ParserContinuation_Diagnostics, SemicolonOnly) { + expect_syntax_error(";"); +} + +TEST(ParserContinuation_Diagnostics, AssignmentWhitespaceSemicolonPipe) { + expect_syntax_error("FOO=bar ; |"); + expect_syntax_error("FOO=bar;|"); +} From f6a04ecea5cef2346cfa3c0b2f13d7720ab0215b Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:05:10 -0500 Subject: [PATCH 05/14] modified: src/lib/executor/executor_win32.cpp --- src/lib/executor/executor_win32.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib/executor/executor_win32.cpp b/src/lib/executor/executor_win32.cpp index dcb96ac..88e3132 100644 --- a/src/lib/executor/executor_win32.cpp +++ b/src/lib/executor/executor_win32.cpp @@ -5,17 +5,24 @@ #if defined(_WIN32) + + #define _CRT_SECURE_NO_WARNINGS #include "shell/execution_policy.hpp" #include - #include + #include namespace wshell { + std::optional get_home_directory() { - if (const char* home = getenv("USERPROFILE")) { - return home; + char* home = nullptr; + size_t len = 0; + if (_dupenv_s(&home, &len, "USERPROFILE") == 0 && home != nullptr) { + std::filesystem::path result(home); + free(home); + return result; } std::cerr << "Unable to find HOME directory\n"; return std::nullopt; From 4db28148f6f49d073e052817a8cd8fb613fe6888 Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:18:37 -0500 Subject: [PATCH 06/14] platform specific things --- include/shell/platform.h | 20 ++++++++++++++ src/lib/CMakeLists.txt | 4 +++ src/lib/builtins/cd_builtin.cpp | 17 ++++++------ src/lib/builtins/kill_builtin.cpp | 22 ++++----------- src/lib/builtins/pwd_builtin.cpp | 13 +++++---- src/lib/platform/platform_posix.cpp | 37 +++++++++++++++++++++++++ src/lib/platform/platform_win32.cpp | 42 +++++++++++++++++++++++++++++ 7 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 include/shell/platform.h create mode 100644 src/lib/platform/platform_posix.cpp create mode 100644 src/lib/platform/platform_win32.cpp diff --git a/include/shell/platform.h b/include/shell/platform.h new file mode 100644 index 0000000..b85b28d --- /dev/null +++ b/include/shell/platform.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace wshell { + +// Change current working directory +bool set_current_directory(const std::string& path); + +// Get current working directory +std::optional get_current_directory(); + +// Terminate a process by ID +bool terminate_process(int pid); + +// Get home directory +std::optional get_home_directory(); + +} // namespace wshell diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 4612e37..efba994 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -11,11 +11,15 @@ file(GLOB_RECURSE WSHELL_SOURCES_BUILTINS ${CMAKE_CURRENT_SOURCE_DIR}/builtins/*.cpp ) +file(GLOB_RECURSE WSHELL_SOURCES_PLATFORM + ${CMAKE_CURRENT_SOURCE_DIR}/platform/*.cpp +) add_library(wshell_lib STATIC ${WSHELL_SOURCES_PARSER} ${WSHELL_SOURCES_EXECUTOR} ${WSHELL_SOURCES_BUILTINS} + ${WSHELL_SOURCES_PLATFORM} ../../include/shell/ast_printer.hpp ast/ast_printer.cpp executor/shell_process_context.cpp diff --git a/src/lib/builtins/cd_builtin.cpp b/src/lib/builtins/cd_builtin.cpp index 4974c72..8cf43b8 100644 --- a/src/lib/builtins/cd_builtin.cpp +++ b/src/lib/builtins/cd_builtin.cpp @@ -1,22 +1,21 @@ -#include -#include -#include +#include #include "shell/built_ins.hpp" +#include "shell/platform.h" namespace wshell { int CdBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { - const char* dir = nullptr; + std::string dir; if (args.size() < 2) { - dir = std::getenv("HOME"); - if (!dir) dir = "."; + auto home = wshell::get_home_directory(); + dir = home.value_or("."); } else { - dir = args[1].c_str(); + dir = args[1]; } - if (chdir(dir) != 0) { - std::perror("cd"); + if (!wshell::set_current_directory(dir)) { + std::cerr << "cd: failed to change directory to '" << dir << "'\n"; return 1; } return 0; diff --git a/src/lib/builtins/kill_builtin.cpp b/src/lib/builtins/kill_builtin.cpp index 4233db1..dd05127 100644 --- a/src/lib/builtins/kill_builtin.cpp +++ b/src/lib/builtins/kill_builtin.cpp @@ -1,7 +1,6 @@ +// ...existing code... #include -#include -#include - +#include "shell/platform.h" #include "shell/built_ins.hpp" namespace wshell { @@ -11,20 +10,9 @@ int KillBuiltin::invoke(const std::vector& args, ShellProcessContex std::cerr << "kill: missing pid" << std::endl; return 1; } - int sig = SIGTERM; - size_t argi = 1; - if (args[1].rfind("-", 0) == 0) { - sig = std::atoi(args[1].c_str() + 1); - argi = 2; - } - if (args.size() <= argi) { - std::cerr << "kill: missing pid" << std::endl; - return 1; - } - //TODO make this work for Windows - pid_t pid = std::atoi(args[argi].c_str()); - if (kill(pid, sig) != 0) { - std::perror("kill"); + int pid = std::atoi(args[1].c_str()); + if (!wshell::terminate_process(pid)) { + std::cerr << "kill: failed to terminate process " << pid << std::endl; return 1; } return 0; diff --git a/src/lib/builtins/pwd_builtin.cpp b/src/lib/builtins/pwd_builtin.cpp index aeda9b1..640f195 100644 --- a/src/lib/builtins/pwd_builtin.cpp +++ b/src/lib/builtins/pwd_builtin.cpp @@ -1,17 +1,16 @@ +// ...existing code... #include -#include -#include - +#include "shell/platform.h" #include "shell/built_ins.hpp" namespace wshell { int PwdBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { - char buf[PATH_MAX]; - if (getcwd(buf, sizeof(buf))) { - std::cout << buf << std::endl; + auto cwd = wshell::get_current_directory(); + if (cwd) { + std::cout << *cwd << std::endl; return 0; } else { - std::perror("pwd"); + std::cerr << "pwd: failed to get current directory\n"; return 1; } } diff --git a/src/lib/platform/platform_posix.cpp b/src/lib/platform/platform_posix.cpp new file mode 100644 index 0000000..f533064 --- /dev/null +++ b/src/lib/platform/platform_posix.cpp @@ -0,0 +1,37 @@ +#if defined(__unix__) || defined(__APPLE__) +#include "shell/platform.h" +#include +#include +#include +#include +#include +#include + +namespace wshell { + +bool set_current_directory(const std::string& path) { + return chdir(path.c_str()) == 0; +} + +std::optional get_current_directory() { + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf))) { + return std::string(buf); + } + return std::nullopt; +} + +bool terminate_process(int pid) { + return kill(pid, SIGTERM) == 0; +} + +std::optional get_home_directory() { + const char* home = getenv("HOME"); + if (home) return std::string(home); + struct passwd* pw = getpwuid(getuid()); + if (pw && pw->pw_dir) return std::string(pw->pw_dir); + return std::nullopt; +} + +} // namespace wshell +#endif diff --git a/src/lib/platform/platform_win32.cpp b/src/lib/platform/platform_win32.cpp new file mode 100644 index 0000000..9ec84d9 --- /dev/null +++ b/src/lib/platform/platform_win32.cpp @@ -0,0 +1,42 @@ +#if defined(_WIN32) +#include "shell/platform.h" +#include +#include +#include +#include + +namespace wshell { + +bool set_current_directory(const std::string& path) { + return SetCurrentDirectoryA(path.c_str()) != 0; +} + +std::optional get_current_directory() { + char buf[MAX_PATH]; + if (GetCurrentDirectoryA(MAX_PATH, buf)) { + return std::string(buf); + } + return std::nullopt; +} + +bool terminate_process(int pid) { + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, static_cast(pid)); + if (!hProcess) return false; + BOOL result = TerminateProcess(hProcess, 1); + CloseHandle(hProcess); + return result != 0; +} + +std::optional get_home_directory() { + char* home = nullptr; + size_t len = 0; + if (_dupenv_s(&home, &len, "USERPROFILE") == 0 && home != nullptr) { + std::string result(home); + free(home); + return result; + } + return std::nullopt; +} + +} // namespace wshell +#endif From 47144ec06d2ff6d51f46c0ac37ca4801745f3c60 Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:21:46 -0500 Subject: [PATCH 07/14] modified: src/lib/executor/executor_posix.cpp modified: src/lib/executor/executor_win32.cpp --- src/lib/executor/executor_posix.cpp | 10 ---------- src/lib/executor/executor_win32.cpp | 11 ----------- 2 files changed, 21 deletions(-) diff --git a/src/lib/executor/executor_posix.cpp b/src/lib/executor/executor_posix.cpp index 4684d1a..38a9c76 100644 --- a/src/lib/executor/executor_posix.cpp +++ b/src/lib/executor/executor_posix.cpp @@ -26,16 +26,6 @@ extern char** environ; namespace wshell { -std::optional get_home_directory() { - if (const char* home = getenv("HOME")) { - return home; - } - if (auto* pw = getpwuid(getuid())) { - return pw->pw_dir; - } - std::cerr << "Unable to find HOME directory\n"; - return std::nullopt; -} namespace fs = std::filesystem; diff --git a/src/lib/executor/executor_win32.cpp b/src/lib/executor/executor_win32.cpp index 88e3132..e5aabeb 100644 --- a/src/lib/executor/executor_win32.cpp +++ b/src/lib/executor/executor_win32.cpp @@ -16,17 +16,6 @@ namespace wshell { -std::optional get_home_directory() { - char* home = nullptr; - size_t len = 0; - if (_dupenv_s(&home, &len, "USERPROFILE") == 0 && home != nullptr) { - std::filesystem::path result(home); - free(home); - return result; - } - std::cerr << "Unable to find HOME directory\n"; - return std::nullopt; -} void printWindowsErrMsg(DWORD& error) { error = GetLastError(); From f572f7f9dccab15c3e008763749a13665c18e339 Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:28:56 -0500 Subject: [PATCH 08/14] fixing windows issue --- src/lib/builtins/cd_builtin.cpp | 2 ++ src/lib/builtins/exit_builtin.cpp | 2 ++ src/lib/builtins/history_builtin.cpp | 2 ++ src/lib/builtins/kill_builtin.cpp | 2 ++ src/lib/builtins/pwd_builtin.cpp | 2 ++ src/lib/executor/executor_win32.cpp | 2 +- 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/builtins/cd_builtin.cpp b/src/lib/builtins/cd_builtin.cpp index 8cf43b8..6f66223 100644 --- a/src/lib/builtins/cd_builtin.cpp +++ b/src/lib/builtins/cd_builtin.cpp @@ -7,6 +7,8 @@ namespace wshell { int CdBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + (void)args; + (void)ctx; std::string dir; if (args.size() < 2) { auto home = wshell::get_home_directory(); diff --git a/src/lib/builtins/exit_builtin.cpp b/src/lib/builtins/exit_builtin.cpp index ebafb22..78f8eb3 100644 --- a/src/lib/builtins/exit_builtin.cpp +++ b/src/lib/builtins/exit_builtin.cpp @@ -5,6 +5,8 @@ namespace wshell { int ExitBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + (void)args; + (void)ctx; int code = 0; if (args.size() > 1) { code = std::atoi(args[1].c_str()); diff --git a/src/lib/builtins/history_builtin.cpp b/src/lib/builtins/history_builtin.cpp index 4de2ebf..3a73e8f 100644 --- a/src/lib/builtins/history_builtin.cpp +++ b/src/lib/builtins/history_builtin.cpp @@ -10,6 +10,8 @@ namespace wshell { HistoryBuiltin::HistoryBuiltin() {} int HistoryBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + (void)args; + (void)ctx; return 1; //placeholder } diff --git a/src/lib/builtins/kill_builtin.cpp b/src/lib/builtins/kill_builtin.cpp index dd05127..8e9a621 100644 --- a/src/lib/builtins/kill_builtin.cpp +++ b/src/lib/builtins/kill_builtin.cpp @@ -6,6 +6,8 @@ namespace wshell { int KillBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + (void)args; + (void)ctx; if (args.size() < 2) { std::cerr << "kill: missing pid" << std::endl; return 1; diff --git a/src/lib/builtins/pwd_builtin.cpp b/src/lib/builtins/pwd_builtin.cpp index 640f195..4c9e113 100644 --- a/src/lib/builtins/pwd_builtin.cpp +++ b/src/lib/builtins/pwd_builtin.cpp @@ -5,6 +5,8 @@ namespace wshell { int PwdBuiltin::invoke(const std::vector& args, ShellProcessContext& ctx) { + (void)args; + (void)ctx; auto cwd = wshell::get_current_directory(); if (cwd) { std::cout << *cwd << std::endl; diff --git a/src/lib/executor/executor_win32.cpp b/src/lib/executor/executor_win32.cpp index e5aabeb..4613cb1 100644 --- a/src/lib/executor/executor_win32.cpp +++ b/src/lib/executor/executor_win32.cpp @@ -36,7 +36,7 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { std::ostringstream cmdline; cmdline << cmd.executable.string(); for (const auto& arg : cmd.args) { - cmdline << " " << arg; + cmdline << " " << std::string(arg); } std::string cmdline_str = cmdline.str(); From 7f74238867e43732dbe78ccb805b13773b91828a Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:37:06 -0500 Subject: [PATCH 09/14] fix up some warnings --- include/shell/built_ins.hpp | 1 + src/lib/executor/executor_win32.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/include/shell/built_ins.hpp b/include/shell/built_ins.hpp index 17a93fa..178b72d 100644 --- a/include/shell/built_ins.hpp +++ b/include/shell/built_ins.hpp @@ -69,6 +69,7 @@ static const std::unordered_map builtinVariablesDefaul class BuiltIns { public: BuiltIns(History* history_ptr = nullptr) { + [[maybeunused]]history_ptr; // Suppress unused parameter warning (MSVC) // Initialize built-in variable map with defaults builtinVariables_ = builtinVariablesDefault; // Register builtin function implementations diff --git a/src/lib/executor/executor_win32.cpp b/src/lib/executor/executor_win32.cpp index 4613cb1..427a722 100644 --- a/src/lib/executor/executor_win32.cpp +++ b/src/lib/executor/executor_win32.cpp @@ -36,7 +36,7 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { std::ostringstream cmdline; cmdline << cmd.executable.string(); for (const auto& arg : cmd.args) { - cmdline << " " << std::string(arg); + cmdline << " " << arg.str(); } std::string cmdline_str = cmdline.str(); From 43d3939f2d30a388c695edad8512ac40663e75df Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:39:55 -0500 Subject: [PATCH 10/14] fix --- include/shell/built_ins.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/shell/built_ins.hpp b/include/shell/built_ins.hpp index 178b72d..6d5e0f6 100644 --- a/include/shell/built_ins.hpp +++ b/include/shell/built_ins.hpp @@ -69,7 +69,7 @@ static const std::unordered_map builtinVariablesDefaul class BuiltIns { public: BuiltIns(History* history_ptr = nullptr) { - [[maybeunused]]history_ptr; // Suppress unused parameter warning (MSVC) + (void)history_ptr; // Suppress unused parameter warning (MSVC) // Initialize built-in variable map with defaults builtinVariables_ = builtinVariablesDefault; // Register builtin function implementations From 386b8ab4fd25f72e5244e8ef70cba3c9fb06b983 Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:43:38 -0500 Subject: [PATCH 11/14] modified: src/lib/executor/executor_win32.cpp --- src/lib/executor/executor_win32.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/executor/executor_win32.cpp b/src/lib/executor/executor_win32.cpp index 427a722..f4f3fae 100644 --- a/src/lib/executor/executor_win32.cpp +++ b/src/lib/executor/executor_win32.cpp @@ -36,7 +36,7 @@ ExecutionResult PlatformExecutionPolicy::execute(const Command& cmd) const { std::ostringstream cmdline; cmdline << cmd.executable.string(); for (const auto& arg : cmd.args) { - cmdline << " " << arg.str(); + cmdline << " " << arg.value; } std::string cmdline_str = cmdline.str(); From daec3126d7d3a2dcf654c58f59707dbe0bec63de Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 22:56:42 -0500 Subject: [PATCH 12/14] last fix up to get win32 building? --- include/shell/execution_policy.hpp | 2 +- include/shell/history.hpp | 4 ++-- include/shell/platform.h | 6 +++++- src/lib/CMakeLists.txt | 1 + src/lib/builtins/cd_builtin.cpp | 2 +- src/lib/execution_policy.cpp | 8 ++++++++ src/lib/platform/platform_posix.cpp | 9 +++++++++ src/lib/platform/platform_win32.cpp | 9 +++++++++ 8 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/lib/execution_policy.cpp diff --git a/include/shell/execution_policy.hpp b/include/shell/execution_policy.hpp index 1efd533..9df2438 100644 --- a/include/shell/execution_policy.hpp +++ b/include/shell/execution_policy.hpp @@ -16,7 +16,7 @@ namespace wshell { // Execution Result // ============================================================================ -std::optional get_home_directory(); +std::optional get_home_directory_path(); /// Result of command execution struct ExecutionResult { diff --git a/include/shell/history.hpp b/include/shell/history.hpp index 9ee0a7c..49e3509 100644 --- a/include/shell/history.hpp +++ b/include/shell/history.hpp @@ -27,7 +27,7 @@ class History { history_.reserve(max_items); //open and read from history std::filesystem::path historyFile; - auto path = get_home_directory(); + auto path = get_home_directory_path(); if ( path.has_value()) { historyFile = std::filesystem::path(path.value()) / HISTORY_FILE; } else { @@ -50,7 +50,7 @@ class History { ~History() { std::filesystem::path historyFile; - auto path = get_home_directory(); + auto path = get_home_directory_path(); if ( path.has_value()) { historyFile = std::filesystem::path(path.value()) / HISTORY_FILE; } else { diff --git a/include/shell/platform.h b/include/shell/platform.h index b85b28d..db3b5c4 100644 --- a/include/shell/platform.h +++ b/include/shell/platform.h @@ -14,7 +14,11 @@ std::optional get_current_directory(); // Terminate a process by ID bool terminate_process(int pid); -// Get home directory + +// Get home directory as string (platform-specific) std::optional get_home_directory(); +// Get home directory as filesystem::path (platform-agnostic) +std::optional get_home_directory_path(); + } // namespace wshell diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index efba994..74d0f85 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(wshell_lib STATIC ${WSHELL_SOURCES_EXECUTOR} ${WSHELL_SOURCES_BUILTINS} ${WSHELL_SOURCES_PLATFORM} + execution_policy.cpp ../../include/shell/ast_printer.hpp ast/ast_printer.cpp executor/shell_process_context.cpp diff --git a/src/lib/builtins/cd_builtin.cpp b/src/lib/builtins/cd_builtin.cpp index 6f66223..c7e0a8d 100644 --- a/src/lib/builtins/cd_builtin.cpp +++ b/src/lib/builtins/cd_builtin.cpp @@ -11,7 +11,7 @@ int CdBuiltin::invoke(const std::vector& args, ShellProcessContext& (void)ctx; std::string dir; if (args.size() < 2) { - auto home = wshell::get_home_directory(); + auto home = wshell::get_home_directory_path(); dir = home.value_or("."); } else { dir = args[1]; diff --git a/src/lib/execution_policy.cpp b/src/lib/execution_policy.cpp new file mode 100644 index 0000000..92a8a6e --- /dev/null +++ b/src/lib/execution_policy.cpp @@ -0,0 +1,8 @@ +#include "shell/execution_policy.hpp" +#include "shell/platform.h" +#include + +namespace wshell { + + +} // namespace wshell diff --git a/src/lib/platform/platform_posix.cpp b/src/lib/platform/platform_posix.cpp index f533064..81acbb7 100644 --- a/src/lib/platform/platform_posix.cpp +++ b/src/lib/platform/platform_posix.cpp @@ -25,6 +25,7 @@ bool terminate_process(int pid) { return kill(pid, SIGTERM) == 0; } + std::optional get_home_directory() { const char* home = getenv("HOME"); if (home) return std::string(home); @@ -33,5 +34,13 @@ std::optional get_home_directory() { return std::nullopt; } +std::optional get_home_directory_path() { + auto home = get_home_directory(); + if (home) { + return std::filesystem::path(*home); + } + return std::nullopt; +} + } // namespace wshell #endif diff --git a/src/lib/platform/platform_win32.cpp b/src/lib/platform/platform_win32.cpp index 9ec84d9..e7d1f83 100644 --- a/src/lib/platform/platform_win32.cpp +++ b/src/lib/platform/platform_win32.cpp @@ -27,6 +27,7 @@ bool terminate_process(int pid) { return result != 0; } + std::optional get_home_directory() { char* home = nullptr; size_t len = 0; @@ -38,5 +39,13 @@ std::optional get_home_directory() { return std::nullopt; } +std::optional get_home_directory_path() { + auto home = get_home_directory(); + if (home) { + return std::filesystem::path(*home); + } + return std::nullopt; +} + } // namespace wshell #endif From cffb1ac4287a2edd80134d9ad8c80464b93f054e Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 23:03:43 -0500 Subject: [PATCH 13/14] fixes to get builds working --- include/shell/platform.h | 1 + src/lib/platform/platform_posix.cpp | 1 + src/lib/platform/platform_win32.cpp | 2 ++ 3 files changed, 4 insertions(+) diff --git a/include/shell/platform.h b/include/shell/platform.h index db3b5c4..7d4d39f 100644 --- a/include/shell/platform.h +++ b/include/shell/platform.h @@ -2,6 +2,7 @@ #include #include +#include namespace wshell { diff --git a/src/lib/platform/platform_posix.cpp b/src/lib/platform/platform_posix.cpp index 81acbb7..579b543 100644 --- a/src/lib/platform/platform_posix.cpp +++ b/src/lib/platform/platform_posix.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace wshell { diff --git a/src/lib/platform/platform_win32.cpp b/src/lib/platform/platform_win32.cpp index e7d1f83..73b38a9 100644 --- a/src/lib/platform/platform_win32.cpp +++ b/src/lib/platform/platform_win32.cpp @@ -3,7 +3,9 @@ #include #include #include + #include +#include namespace wshell { From c604a39d4837d6d1de11ac1fa4b37947fab2d97d Mon Sep 17 00:00:00 2001 From: BoondockTaints Date: Fri, 26 Dec 2025 23:07:19 -0500 Subject: [PATCH 14/14] fixes for win32 --- src/lib/builtins/cd_builtin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/builtins/cd_builtin.cpp b/src/lib/builtins/cd_builtin.cpp index c7e0a8d..61256be 100644 --- a/src/lib/builtins/cd_builtin.cpp +++ b/src/lib/builtins/cd_builtin.cpp @@ -12,7 +12,7 @@ int CdBuiltin::invoke(const std::vector& args, ShellProcessContext& std::string dir; if (args.size() < 2) { auto home = wshell::get_home_directory_path(); - dir = home.value_or("."); + dir = home ? home->string() : "."; } else { dir = args[1]; }