diff options
author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2025-08-13 19:05:32 +0200 |
---|---|---|
committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2025-08-13 19:05:32 +0200 |
commit | ab36c1b5178ee0e7934b1c5cdcf2ccfccc3514bb (patch) | |
tree | 1d02229f3e165ec375d10301820ae71c16fe6a16 | |
parent | 1a795d8eb7a9e7475414fa537810726d2be127cb (diff) |
Add a Zig port.
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README-ZIG.md | 57 | ||||
-rw-r--r-- | build.zig | 39 | ||||
-rw-r--r-- | src/main.zig | 865 |
4 files changed, 963 insertions, 0 deletions
@@ -1,3 +1,5 @@ /.idea +/.zig-cache /target +/zig-out *~ diff --git a/README-ZIG.md b/README-ZIG.md new file mode 100644 index 0000000..9aa043d --- /dev/null +++ b/README-ZIG.md @@ -0,0 +1,57 @@ +# Dosage - Zig Port + +This is a Zig port of the original Rust-based DOS command shell implementation. + +## Dependency Replacements + +The original Rust version used several external crates that have been replaced with Zig standard library functionality: + +### Rust → Zig Replacements +- **`rustyline`** (command-line editing) → Simple `stdin.readUntilDelimiterOrEofAlloc()` +- **`crossterm`** (terminal manipulation) → ANSI escape sequences for screen clearing +- **`prettytable-rs`** (table formatting) → Custom formatting with `print()` +- **`eyre`/`color-eyre`** (error handling) → Zig's built-in error handling +- **`thiserror`** (error derive macros) → Zig error unions +- **`regex`** (regular expressions) → Not needed in current implementation + +### Key Architectural Changes + +1. **Error Handling**: Replaced Rust's `Result<T, E>` with Zig's error unions (`!T`) +2. **Memory Management**: Manual allocation/deallocation using Zig's allocators instead of Rust's ownership system +3. **String Handling**: Explicit memory management for strings vs Rust's `String`/`&str` +4. **Concurrency**: Removed complex threading from original Rust version for simplicity +5. **Command Line Editing**: Simplified to basic line reading (no history or editing features) + +### Missing Features (compared to Rust version) +- Command-line history and editing (rustyline features) +- Colored error output +- Advanced terminal manipulation +- Complex pipe/redirection handling +- Multi-threaded command execution + +## Build Instructions + +```bash +# Build and run in debug mode +zig build run + +# Build optimized release version +zig build -Doptimize=ReleaseFast + +# Run tests +zig build test +``` + +## Implementation Notes + +The Zig version focuses on core DOS command functionality while maintaining the same architectural patterns as the Rust original. The enum-based command system has been preserved using Zig's union types. + +Key DOS commands implemented: +- `ECHO` (with ON/OFF variants) +- `CLS` (clear screen) +- `EXIT` (exit shell) +- `VER` (version info) +- `DATE` and `TIME` (system date/time) +- `DIR` (directory listing) + +The implementation uses Zig's standard library exclusively, avoiding external dependencies for better portability and simplicity.
\ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..6810bfc --- /dev/null +++ b/build.zig @@ -0,0 +1,39 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "dosage", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // Add cross-platform terminal support + exe.linkLibC(); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +}
\ No newline at end of file diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..947ed23 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,865 @@ +const std = @import("std"); +const print = std.debug.print; +const ArrayList = std.ArrayList; +const Allocator = std.mem.Allocator; +const Thread = std.Thread; +const Mutex = std.Thread.Mutex; + +const STDOUT_BUFFER_SIZE: usize = 1024; +const STDERR_BUFFER_SIZE: usize = 1024; + +const FileSpec = union(enum) { + Con, + Lpt1, + Lpt2, + Lpt3, + Prn, + Path: []const u8, +}; + +const RedirectType = enum { + OutputOverwrite, // > + OutputAppend, // >> + InputFrom, // < +}; + +const Redirect = struct { + redirect_type: RedirectType, + target: FileSpec, +}; + +const BuiltinCommand = union(enum) { + // File-oriented + Copy: struct { + from: FileSpec, + to: FileSpec, + }, + Deltree: struct { + path: []const u8, + }, + Dir: struct { + path: []const u8, + }, + Fc, + Find, + Mkdir: struct { + path: []const u8, + }, + Move, + Remove: struct { + path: []const u8, + }, + Rename: struct { + from: FileSpec, + to: FileSpec, + }, + Replace, + Rmdir: struct { + path: []const u8, + }, + Sort, + Tree: struct { + path: []const u8, + }, + Type: struct { + file: FileSpec, + }, + Xcopy: struct { + from: FileSpec, + to: FileSpec, + recursive: bool, + }, + + // Shell-oriented + Append, + Chdir: struct { + path: []const u8, + }, + EchoOff, + EchoOn, + EchoPlain, + EchoText: struct { + message: []const u8, + }, + Exit, + PathGet, + PathSet: struct { + value: []const u8, + }, + PromptGet, + PromptSet: struct { + message: []const u8, + }, + Set: struct { + name: []const u8, + value: []const u8, + }, + Setver, + Ver, + + // Utilities + Date, + Time, + + // Screen-oriented + Cls, + More, + + // Dummies + Verify, + Fastopen, + Smartdrv, + Sizer, + + // For later + Assign, + Attrib, + Chkdsk, + Doskey, + Dosshell, + Edit, + Fasthelp, + Help, + Join, + Mem, + Power, + Subst, + Truename, + + // For much later, if ever + Break, + Chcp, + Ctty, + Defrag, + Diskcopy, + Emm386, + Fdisk, + Format, + Interlnk, + Keyb, + Label, + Mode, + Msav, + Msbackup, + Mscdex, + Msd, + Print_: void, // 'print' is reserved in Zig + Qbasic, + Restore, + Scandisk, + Share, + Sys, + Undelete, + Unformat, + Vol, + Vsafe, + + // Scripting + Call, + Choice, + Echo, + For, + Goto, + If, + Pause, + Prompt, + Rem: struct { + message: []const u8, + }, + Shift, +}; + +const Command = union(enum) { + Pipe: struct { + left: *Command, + right: *Command, + }, + Redirect: struct { + command: *Command, + redirects: ArrayList(Redirect), + }, + External: struct { + program: []const u8, + args: ArrayList([]const u8), + }, + Builtin: BuiltinCommand, + Empty, + + pub fn deinit(self: *Command, allocator: Allocator) void { + switch (self.*) { + .Pipe => |*pipe| { + pipe.left.deinit(allocator); + pipe.right.deinit(allocator); + allocator.destroy(pipe.left); + allocator.destroy(pipe.right); + }, + .Redirect => |*redirect| { + redirect.command.deinit(allocator); + allocator.destroy(redirect.command); + redirect.redirects.deinit(); + }, + .External => |*external| { + external.args.deinit(); + }, + else => {}, + } + } +}; + +const CommandStatus = union(enum) { + Code: u16, + ExitShell, +}; + +const Token = union(enum) { + Word: []const u8, + Pipe, + RedirectOut, // > + RedirectAppend, // >> + RedirectIn, // < + Newline, + Eof, +}; + +const Lexer = struct { + input: []const u8, + position: usize, + current_char: ?u8, + + pub fn init(input: []const u8) Lexer { + return Lexer{ + .input = input, + .position = 0, + .current_char = if (input.len > 0) input[0] else null, + }; + } + + fn advance(self: *Lexer) void { + self.position += 1; + self.current_char = if (self.position < self.input.len) self.input[self.position] else null; + } + + fn peek(self: *const Lexer) ?u8 { + const next_pos = self.position + 1; + return if (next_pos < self.input.len) self.input[next_pos] else null; + } + + fn skipWhitespace(self: *Lexer) void { + while (self.current_char) |ch| { + if (std.ascii.isWhitespace(ch) and ch != '\n') { + self.advance(); + } else { + break; + } + } + } + + fn readWord(self: *Lexer, allocator: Allocator) ![]const u8 { + var word = ArrayList(u8).init(allocator); + defer word.deinit(); + + var in_quotes = false; + var quote_char: u8 = '"'; + + while (self.current_char) |ch| { + switch (ch) { + '"', '\'' => { + if (!in_quotes) { + in_quotes = true; + quote_char = ch; + self.advance(); + } else if (ch == quote_char) { + in_quotes = false; + self.advance(); + } else { + try word.append(ch); + self.advance(); + } + }, + '|', '>', '<', '\n' => { + if (!in_quotes) break; + try word.append(ch); + self.advance(); + }, + else => { + if (!in_quotes and std.ascii.isWhitespace(ch)) break; + try word.append(ch); + self.advance(); + }, + } + } + + return allocator.dupe(u8, word.items); + } + + fn nextToken(self: *Lexer, allocator: Allocator) !Token { + while (true) { + if (self.current_char) |ch| { + switch (ch) { + '\n' => { + self.advance(); + return Token.Newline; + }, + '|' => { + self.advance(); + return Token.Pipe; + }, + '>' => { + self.advance(); + if (self.current_char == '>') { + self.advance(); + return Token.RedirectAppend; + } + return Token.RedirectOut; + }, + '<' => { + self.advance(); + return Token.RedirectIn; + }, + else => { + if (std.ascii.isWhitespace(ch)) { + self.skipWhitespace(); + continue; + } + const word = try self.readWord(allocator); + if (word.len == 0) { + self.advance(); + continue; + } + return Token{ .Word = word }; + }, + } + } else { + return Token.Eof; + } + } + } + + pub fn tokenize(self: *Lexer, allocator: Allocator) !ArrayList(Token) { + var tokens = ArrayList(Token).init(allocator); + + while (true) { + const token = try self.nextToken(allocator); + const is_eof = switch (token) { + .Eof => true, + else => false, + }; + try tokens.append(token); + if (is_eof) break; + } + + return tokens; + } +}; + +const Parser = struct { + tokens: ArrayList(Token), + position: usize, + allocator: Allocator, + + pub fn init(tokens: ArrayList(Token), allocator: Allocator) Parser { + return Parser{ + .tokens = tokens, + .position = 0, + .allocator = allocator, + }; + } + + fn currentToken(self: *const Parser) Token { + if (self.position < self.tokens.items.len) { + return self.tokens.items[self.position]; + } + return Token.Eof; + } + + fn advance(self: *Parser) void { + if (self.position < self.tokens.items.len) { + self.position += 1; + } + } + + fn expectWord(self: *Parser) ![]const u8 { + switch (self.currentToken()) { + .Word => |word| { + self.advance(); + return word; + }, + else => return error.ExpectedWord, + } + } + + pub fn parseCommand(self: *Parser) !Command { + return self.parsePipeline(); + } + + fn parsePipeline(self: *Parser) !Command { + var left = try self.parseRedirectedCommand(); + + while (true) { + switch (self.currentToken()) { + .Pipe => { + self.advance(); // consume | + const right = try self.parseRedirectedCommand(); + const left_ptr = try self.allocator.create(Command); + const right_ptr = try self.allocator.create(Command); + left_ptr.* = left; + right_ptr.* = right; + left = Command{ .Pipe = .{ .left = left_ptr, .right = right_ptr } }; + }, + else => break, + } + } + + return left; + } + + fn parseRedirectedCommand(self: *Parser) !Command { + const command = try self.parseSimpleCommand(); + var redirects = ArrayList(Redirect).init(self.allocator); + + while (true) { + const redirect_type = switch (self.currentToken()) { + .RedirectOut => RedirectType.OutputOverwrite, + .RedirectAppend => RedirectType.OutputAppend, + .RedirectIn => RedirectType.InputFrom, + else => break, + }; + + self.advance(); // consume redirect token + + const target_str = try self.expectWord(); + const target = parseFilespec(target_str); + + try redirects.append(Redirect{ + .redirect_type = redirect_type, + .target = target, + }); + } + + if (redirects.items.len == 0) { + redirects.deinit(); + return command; + } else { + const command_ptr = try self.allocator.create(Command); + command_ptr.* = command; + return Command{ .Redirect = .{ .command = command_ptr, .redirects = redirects } }; + } + } + + fn parseSimpleCommand(self: *Parser) !Command { + switch (self.currentToken()) { + .Eof, .Newline => return Command.Empty, + .Word => |command_name| { + self.advance(); + var args = ArrayList([]const u8).init(self.allocator); + + // Collect arguments + while (true) { + switch (self.currentToken()) { + .Word => |arg| { + try args.append(arg); + self.advance(); + }, + else => break, + } + } + + return try self.parseBuiltinCommand(command_name, args); + }, + else => return error.UnexpectedToken, + } + } + + fn parseBuiltinCommand(self: *Parser, command_name: []const u8, args: ArrayList([]const u8)) !Command { + const cmd_upper = try std.ascii.allocUpperString(self.allocator, command_name); + defer self.allocator.free(cmd_upper); + + if (std.mem.eql(u8, cmd_upper, "ECHO")) { + if (args.items.len == 0) { + return Command{ .Builtin = BuiltinCommand.EchoPlain }; + } else { + const first_arg_upper = try std.ascii.allocUpperString(self.allocator, args.items[0]); + defer self.allocator.free(first_arg_upper); + + if (std.mem.eql(u8, first_arg_upper, "ON") and args.items.len == 1) { + return Command{ .Builtin = BuiltinCommand.EchoOn }; + } else if (std.mem.eql(u8, first_arg_upper, "OFF") and args.items.len == 1) { + return Command{ .Builtin = BuiltinCommand.EchoOff }; + } else { + const message = try std.mem.join(self.allocator, " ", args.items); + return Command{ .Builtin = BuiltinCommand{ .EchoText = .{ .message = message } } }; + } + } + } else if (std.mem.eql(u8, cmd_upper, "CLS")) { + return Command{ .Builtin = BuiltinCommand.Cls }; + } else if (std.mem.eql(u8, cmd_upper, "EXIT")) { + return Command{ .Builtin = BuiltinCommand.Exit }; + } else if (std.mem.eql(u8, cmd_upper, "MORE")) { + return Command{ .Builtin = BuiltinCommand.More }; + } else if (std.mem.eql(u8, cmd_upper, "VERIFY")) { + return Command{ .Builtin = BuiltinCommand.Verify }; + } else if (std.mem.eql(u8, cmd_upper, "DIR")) { + const path = if (args.items.len == 0) "." else args.items[0]; + return Command{ .Builtin = BuiltinCommand{ .Dir = .{ .path = path } } }; + } else if (std.mem.eql(u8, cmd_upper, "VER")) { + return Command{ .Builtin = BuiltinCommand.Ver }; + } else if (std.mem.eql(u8, cmd_upper, "DATE")) { + return Command{ .Builtin = BuiltinCommand.Date }; + } else if (std.mem.eql(u8, cmd_upper, "TIME")) { + return Command{ .Builtin = BuiltinCommand.Time }; + } else if (std.mem.eql(u8, cmd_upper, "TYPE")) { + if (args.items.len == 0) { + return error.ExpectedWord; // Will be caught and show "Bad command or file name" + } + const file_spec = parseFilespec(args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .Type = .{ .file = file_spec } } }; + } else { + // External command + return Command{ .External = .{ .program = command_name, .args = args } }; + } + } +}; + +fn isLeapYear(year: u32) bool { + return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); +} + +fn parseFilespec(path_str: []const u8) FileSpec { + var upper_buf: [256]u8 = undefined; + if (path_str.len >= upper_buf.len) return FileSpec{ .Path = path_str }; + const upper_str = std.ascii.upperString(upper_buf[0..path_str.len], path_str); + + if (std.mem.eql(u8, upper_str, "CON")) return FileSpec.Con; + if (std.mem.eql(u8, upper_str, "LPT1")) return FileSpec.Lpt1; + if (std.mem.eql(u8, upper_str, "LPT2")) return FileSpec.Lpt2; + if (std.mem.eql(u8, upper_str, "LPT3")) return FileSpec.Lpt3; + if (std.mem.eql(u8, upper_str, "PRN")) return FileSpec.Prn; + return FileSpec{ .Path = path_str }; +} + +fn formatPath(allocator: Allocator, path: []const u8) ![]const u8 { + var result = ArrayList(u8).init(allocator); + defer result.deinit(); + + // Simple DOS-style path formatting + // Convert to uppercase and replace / with \ + for (path) |ch| { + if (ch == '/') { + try result.append('\\'); + } else { + try result.append(std.ascii.toUpper(ch)); + } + } + + // Add C: prefix if no drive letter + if (result.items.len == 0 or result.items[1] != ':') { + var prefixed = ArrayList(u8).init(allocator); + defer prefixed.deinit(); + try prefixed.appendSlice("C:"); + try prefixed.appendSlice(result.items); + return allocator.dupe(u8, prefixed.items); + } + + return allocator.dupe(u8, result.items); +} + +fn parseAndExecute(input: []const u8, allocator: Allocator) !CommandStatus { + const trimmed = std.mem.trim(u8, input, " \t\r\n"); + if (trimmed.len == 0) { + return CommandStatus{ .Code = 0 }; + } + + var lexer = Lexer.init(trimmed); + var tokens = try lexer.tokenize(allocator); + defer { + for (tokens.items) |token| { + switch (token) { + .Word => |word| allocator.free(word), + else => {}, + } + } + tokens.deinit(); + } + + var parser = Parser.init(tokens, allocator); + var command = try parser.parseCommand(); + defer command.deinit(allocator); + + return try executeCommand(command, allocator); +} + +fn executeCommand(command: Command, _: Allocator) !CommandStatus { + switch (command) { + .Empty => return CommandStatus{ .Code = 0 }, + + .Builtin => |builtin_cmd| { + switch (builtin_cmd) { + .EchoText => |echo| { + print("{s}\n", .{echo.message}); + return CommandStatus{ .Code = 0 }; + }, + .Cls => { + // Clear screen - simplified version + print("\x1B[2J\x1B[H", .{}); + return CommandStatus{ .Code = 0 }; + }, + .Exit => { + return CommandStatus.ExitShell; + }, + .EchoPlain => { + print("ECHO is on\n", .{}); + return CommandStatus{ .Code = 0 }; + }, + .EchoOn => { + print("ECHO is on\n", .{}); + return CommandStatus{ .Code = 0 }; + }, + .EchoOff => { + return CommandStatus{ .Code = 0 }; + }, + .Ver => { + print("MS-DOS Version 6.22 (Zig Implementation)\n", .{}); + return CommandStatus{ .Code = 0 }; + }, + .Date => { + const timestamp = std.time.timestamp(); + const epoch_seconds = @as(u64, @intCast(timestamp)); + const epoch_day = @divFloor(epoch_seconds, std.time.s_per_day); + + // Calculate days since Unix epoch (1970-01-01) + // Unix epoch is 719163 days since year 1 AD + const days_since_year_1 = epoch_day + 719163; + + // Simple algorithm to convert days to year/month/day + var year: u32 = 1; + var remaining_days = days_since_year_1; + + // Find the year + while (true) { + const days_in_year: u64 = if (isLeapYear(year)) 366 else 365; + if (remaining_days < days_in_year) break; + remaining_days -= days_in_year; + year += 1; + } + + // Days in each month (non-leap year) + const days_in_month = [_]u32{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + + var month: u32 = 1; + for (days_in_month, 1..) |days, m| { + var month_days = days; + // Adjust February for leap years + if (m == 2 and isLeapYear(year)) { + month_days = 29; + } + + if (remaining_days < month_days) { + month = @intCast(m); + break; + } + remaining_days -= month_days; + } + + const day = remaining_days + 1; // Days are 1-indexed + + print("Current date is {d:0>2}/{d:0>2}/{d}\n", .{ month, day, year }); + return CommandStatus{ .Code = 0 }; + }, + .Time => { + const timestamp = std.time.timestamp(); + const epoch_seconds = @as(u64, @intCast(timestamp)); + const day_seconds = epoch_seconds % std.time.s_per_day; + const hours = day_seconds / std.time.s_per_hour; + const minutes = (day_seconds % std.time.s_per_hour) / std.time.s_per_min; + const seconds = day_seconds % std.time.s_per_min; + print("Current time is {d:0>2}:{d:0>2}:{d:0>2}\n", .{ hours, minutes, seconds }); + return CommandStatus{ .Code = 0 }; + }, + .Dir => |dir| { + print("Directory of {s}\n\n", .{dir.path}); + + var dir_iterator = std.fs.cwd().openDir(dir.path, .{ .iterate = true }) catch { + print("File not found\n", .{}); + return CommandStatus{ .Code = 1 }; + }; + defer dir_iterator.close(); + + var iterator = dir_iterator.iterate(); + var file_count: u32 = 0; + var dir_count: u32 = 0; + + while (try iterator.next()) |entry| { + switch (entry.kind) { + .directory => { + print("<DIR> {s}\n", .{entry.name}); + dir_count += 1; + }, + .file => { + const stat = dir_iterator.statFile(entry.name) catch continue; + print("{d:>14} {s}\n", .{ stat.size, entry.name }); + file_count += 1; + }, + else => {}, + } + } + + print("\n{d} File(s)\n", .{file_count}); + print("{d} Dir(s)\n", .{dir_count}); + return CommandStatus{ .Code = 0 }; + }, + .Type => |type_cmd| { + const file_path = switch (type_cmd.file) { + .Con => { + print("Cannot TYPE from CON\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + .Lpt1, .Lpt2, .Lpt3, .Prn => { + print("Cannot TYPE from device\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { + switch (err) { + error.FileNotFound => { + print("The system cannot find the file specified.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + error.IsDir => { + print("Access is denied.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + error.AccessDenied => { + print("Access is denied.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + else => { + print("Cannot access file.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + } + }; + defer file.close(); + + // Read and display file contents + var buffer: [4096]u8 = undefined; + while (true) { + const bytes_read = file.readAll(&buffer) catch |err| { + switch (err) { + error.AccessDenied => { + print("Access is denied.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + else => { + print("Error reading file.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + } + }; + + if (bytes_read == 0) break; + + // Print the buffer contents, handling both text and binary files + for (buffer[0..bytes_read]) |byte| { + // Convert to printable characters, similar to DOS TYPE behavior + if (byte >= 32 and byte <= 126) { + print("{c}", .{byte}); + } else if (byte == '\n') { + print("\n", .{}); + } else if (byte == '\r') { + // Skip carriage return in DOS-style line endings + continue; + } else if (byte == '\t') { + print("\t", .{}); + } else { + // Replace non-printable characters with '?' + print("?", .{}); + } + } + + // If we read less than the buffer size, we're done + if (bytes_read < buffer.len) break; + } + + return CommandStatus{ .Code = 0 }; + }, + else => { + print("Command not implemented: {any}\n", .{builtin_cmd}); + return CommandStatus{ .Code = 1 }; + }, + } + }, + + .External => |external| { + print("'{s}' is not recognized as an internal or external command,\n", .{external.program}); + print("operable program or batch file.\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + + else => { + print("Command type not implemented\n", .{}); + return CommandStatus{ .Code = 1 }; + }, + } +} + +fn readLine(allocator: Allocator, prompt_text: []const u8) !?[]const u8 { + const stdin = std.io.getStdIn().reader(); + print("{s}", .{prompt_text}); + + if (try stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |input| { + // Remove trailing \r on Windows + if (input.len > 0 and input[input.len - 1] == '\r') { + return input[0..input.len - 1]; + } + return input; + } + return null; +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const prompt_spec = "$p$g "; + + while (true) { + const cwd = std.fs.cwd().realpathAlloc(allocator, ".") catch |err| switch (err) { + error.FileNotFound => "C:\\", + else => return err, + }; + defer allocator.free(cwd); + + const full_cwd = try formatPath(allocator, cwd); + defer allocator.free(full_cwd); + + const interpolated_prompt = try std.mem.replaceOwned(u8, allocator, prompt_spec, "$p", full_cwd); + defer allocator.free(interpolated_prompt); + + const final_prompt = try std.mem.replaceOwned(u8, allocator, interpolated_prompt, "$g", ">"); + defer allocator.free(final_prompt); + + if (try readLine(allocator, final_prompt)) |line| { + defer allocator.free(line); + + const command_result = parseAndExecute(line, allocator) catch |err| { + switch (err) { + error.ExpectedWord, error.UnexpectedToken => { + print("Bad command or file name\n", .{}); + continue; + }, + else => return err, + } + }; + + switch (command_result) { + .ExitShell => break, + .Code => |_| {}, + } + } else { + break; // EOF + } + } +}
\ No newline at end of file |