summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-08-13 19:05:32 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-08-13 19:05:32 +0200
commitab36c1b5178ee0e7934b1c5cdcf2ccfccc3514bb (patch)
tree1d02229f3e165ec375d10301820ae71c16fe6a16
parent1a795d8eb7a9e7475414fa537810726d2be127cb (diff)
Add a Zig port.
-rw-r--r--.gitignore2
-rw-r--r--README-ZIG.md57
-rw-r--r--build.zig39
-rw-r--r--src/main.zig865
4 files changed, 963 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 2d9abde..df15dc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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