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(" {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 } } }