diff options
author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2025-08-14 15:14:14 +0200 |
---|---|---|
committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2025-08-14 15:14:14 +0200 |
commit | b74311035a816bbdf9ea69f172d7f6538d8ae9f8 (patch) | |
tree | a30b200b7379a0a0a4cfcda5c2bb382ab3464350 | |
parent | 74862bd729ef5aacce0efbe8c05394ef82454f01 (diff) |
Refactor command structure, extract COPY into a separate file.
-rw-r--r-- | src/cmd.zig | 250 | ||||
-rw-r--r-- | src/cmd/copy.zig | 208 | ||||
-rw-r--r-- | src/cmd/types.zig | 63 | ||||
-rw-r--r-- | src/eval.zig | 259 | ||||
-rw-r--r-- | src/main.zig | 2 | ||||
-rw-r--r-- | src/parser.zig | 416 | ||||
-rw-r--r-- | src/syntax.zig | 653 |
7 files changed, 949 insertions, 902 deletions
diff --git a/src/cmd.zig b/src/cmd.zig new file mode 100644 index 0000000..665b2bf --- /dev/null +++ b/src/cmd.zig @@ -0,0 +1,250 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; + +const syntax = @import("syntax.zig"); +const FileSpec = syntax.FileSpec; +const Redirect = syntax.Redirect; + +const Copy = @import("cmd/copy.zig").Copy; + +pub const BuiltinCommand = union(enum) { + // File-oriented + Copy: Copy, + 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, +}; + +pub 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); + for (redirect.redirects.items) |redir| { + switch (redir.target) { + .Path => |path| allocator.free(path), + else => {}, + } + } + redirect.redirects.deinit(); + }, + .External => |*external| { + allocator.free(external.program); + for (external.args.items) |arg| { + allocator.free(arg); + } + external.args.deinit(); + }, + .Builtin => |builtin| { + switch (builtin) { + .Dir => |dir| allocator.free(dir.path), + .Chdir => |chdir| allocator.free(chdir.path), + .EchoText => |echo| allocator.free(echo.message), + .Type => |type_cmd| { + switch (type_cmd.file) { + .Path => |path| allocator.free(path), + else => {}, + } + }, + .Copy => |copy| { + switch (copy.from) { + .Path => |path| allocator.free(path), + else => {}, + } + switch (copy.to) { + .Path => |path| allocator.free(path), + else => {}, + } + }, + .Remove => |remove| allocator.free(remove.path), + .Mkdir => |mkdir| allocator.free(mkdir.path), + .Deltree => |deltree| allocator.free(deltree.path), + .Tree => |tree| allocator.free(tree.path), + .Rmdir => |rmdir| allocator.free(rmdir.path), + .PathSet => |pathset| allocator.free(pathset.value), + .PromptSet => |promptset| allocator.free(promptset.message), + .Set => |set| { + allocator.free(set.name); + allocator.free(set.value); + }, + .Rem => |rem| allocator.free(rem.message), + .Rename => |rename| { + switch (rename.from) { + .Path => |path| allocator.free(path), + else => {}, + } + switch (rename.to) { + .Path => |path| allocator.free(path), + else => {}, + } + }, + .Xcopy => |xcopy| { + switch (xcopy.from) { + .Path => |path| allocator.free(path), + else => {}, + } + switch (xcopy.to) { + .Path => |path| allocator.free(path), + else => {}, + } + }, + else => {}, + } + }, + else => {}, + } + } +}; diff --git a/src/cmd/copy.zig b/src/cmd/copy.zig new file mode 100644 index 0000000..44eeb87 --- /dev/null +++ b/src/cmd/copy.zig @@ -0,0 +1,208 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +const syntax = @import("../syntax.zig"); +const FileSpec = syntax.FileSpec; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +pub const Copy = struct { + from: FileSpec, + to: FileSpec, + + pub fn eval(copy: Copy, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = input_source; + + // Handle source file + const source_path = switch (copy.from) { + .Con => { + // COPY CON <file> - copy from console to file + const dest_path = switch (copy.to) { + .Con => { + const error_msg = "Cannot copy from CON to CON\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Lpt1, .Lpt2, .Lpt3, .Prn => { + const error_msg = "Cannot copy to device\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + // Create the destination file + const dest_file = std.fs.cwd().createFile(dest_path, .{}) catch |err| { + const error_msg = switch (err) { + error.AccessDenied => "Access denied\n", + error.PathAlreadyExists => "File already exists - use different name\n", + else => "Cannot create file\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + defer dest_file.close(); + + // Read from stdin and write to file until EOF (Ctrl+Z on DOS) + const stdin = std.io.getStdIn().reader(); + var line_count: u32 = 0; + + // Skip output redirection since we're doing interactive input + if (output_capture == null) { + // In interactive mode, show no prompt (DOS behavior) + } + + while (true) { + if (stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |maybe_line| { + if (maybe_line) |line| { + defer allocator.free(line); + + // Check for Ctrl+Z (EOF marker) + if (line.len == 1 and line[0] == 26) { // ASCII 26 = Ctrl+Z + break; + } + + // Remove trailing \r if present (Windows line endings) + const clean_line = if (line.len > 0 and line[line.len - 1] == '\r') + line[0 .. line.len - 1] + else + line; + + // Write line to file with DOS line ending + try dest_file.writeAll(clean_line); + try dest_file.writeAll("\r\n"); + line_count += 1; + } else { + // EOF reached + break; + } + } else |_| { + // Error reading input + break; + } + } + + const msg = try std.fmt.allocPrint(allocator, " 1 File(s) copied\n", .{}); + defer allocator.free(msg); + if (output_capture) |capture| { + try capture.write(msg); + } else { + print("{s}", .{msg}); + } + return CommandStatus{ .Code = 0 }; + }, + .Lpt1, .Lpt2, .Lpt3, .Prn => { + const error_msg = "Cannot copy from device\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + // Handle destination + const dest_path = switch (copy.to) { + .Con => { + // Copy to console (display file contents) + const source_file = std.fs.cwd().openFile(source_path, .{}) catch |err| { + const error_msg = switch (err) { + error.FileNotFound => "File not found\n", + error.AccessDenied => "Access denied\n", + else => "Cannot access source file\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + defer source_file.close(); + + // Read and display file contents + var buffer: [4096]u8 = undefined; + while (true) { + const bytes_read = source_file.readAll(&buffer) catch { + const error_msg = "Error reading file\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + if (bytes_read == 0) break; + + if (output_capture) |capture| { + try capture.write(buffer[0..bytes_read]); + } else { + print("{s}", .{buffer[0..bytes_read]}); + } + + if (bytes_read < buffer.len) break; + } + + const msg = " 1 File(s) copied\n"; + if (output_capture) |capture| { + try capture.write(msg); + } else { + print("{s}", .{msg}); + } + return CommandStatus{ .Code = 0 }; + }, + .Lpt1, .Lpt2, .Lpt3, .Prn => { + const error_msg = "Cannot copy to device\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + // Regular file-to-file copy + std.fs.cwd().copyFile(source_path, std.fs.cwd(), dest_path, .{}) catch |err| { + const error_msg = switch (err) { + error.FileNotFound => "File not found\n", + error.AccessDenied => "Access denied\n", + error.PathAlreadyExists => "File already exists\n", + else => "Cannot copy file\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + + const msg = " 1 File(s) copied\n"; + if (output_capture) |capture| { + try capture.write(msg); + } else { + print("{s}", .{msg}); + } + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/types.zig b/src/cmd/types.zig new file mode 100644 index 0000000..1f0e162 --- /dev/null +++ b/src/cmd/types.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; + +pub const CommandStatus = union(enum) { + Code: u16, + ExitShell, +}; + +pub const OutputCapture = struct { + buffer: ArrayList(u8), + + pub fn init(allocator: Allocator) OutputCapture { + return OutputCapture{ + .buffer = ArrayList(u8).init(allocator), + }; + } + + pub fn deinit(self: *OutputCapture) void { + self.buffer.deinit(); + } + + pub fn write(self: *OutputCapture, data: []const u8) !void { + try self.buffer.appendSlice(data); + } + + pub fn getContents(self: *const OutputCapture) []const u8 { + return self.buffer.items; + } +}; + +pub const InputSource = struct { + data: []const u8, + position: usize, + + pub fn init(data: []const u8) InputSource { + return InputSource{ + .data = data, + .position = 0, + }; + } + + pub fn readLine(self: *InputSource, allocator: Allocator) !?[]const u8 { + if (self.position >= self.data.len) { + return null; // EOF + } + + var line_end = self.position; + while (line_end < self.data.len and self.data[line_end] != '\n') { + line_end += 1; + } + + const line = self.data[self.position..line_end]; + self.position = if (line_end < self.data.len) line_end + 1 else self.data.len; + + // Remove trailing \r if present (DOS line endings) + if (line.len > 0 and line[line.len - 1] == '\r') { + return try allocator.dupe(u8, line[0 .. line.len - 1]); + } else { + return try allocator.dupe(u8, line); + } + } +}; diff --git a/src/eval.zig b/src/eval.zig index 0fe0fde..dcf34f4 100644 --- a/src/eval.zig +++ b/src/eval.zig @@ -13,9 +13,11 @@ const c = @cImport({ } }); +const cmd = @import("cmd.zig"); +const Command = cmd.Command; +const BuiltinCommand = cmd.BuiltinCommand; + const syntax = @import("syntax.zig"); -const Command = syntax.Command; -const BuiltinCommand = syntax.BuiltinCommand; const FileSpec = syntax.FileSpec; const RedirectType = syntax.RedirectType; const Redirect = syntax.Redirect; @@ -24,69 +26,14 @@ const paths = @import("paths.zig"); const formatDosPath = paths.formatDosPath; const convertTo83 = paths.convertTo83; +const cmdTypes = @import("cmd/types.zig"); +pub const CommandStatus = cmdTypes.CommandStatus; +const OutputCapture = cmdTypes.OutputCapture; +const InputSource = cmdTypes.InputSource; + const STDOUT_BUFFER_SIZE: usize = 1024; const STDERR_BUFFER_SIZE: usize = 1024; -pub const CommandStatus = union(enum) { - Code: u16, - ExitShell, -}; - -const OutputCapture = struct { - buffer: ArrayList(u8), - - pub fn init(allocator: Allocator) OutputCapture { - return OutputCapture{ - .buffer = ArrayList(u8).init(allocator), - }; - } - - pub fn deinit(self: *OutputCapture) void { - self.buffer.deinit(); - } - - pub fn write(self: *OutputCapture, data: []const u8) !void { - try self.buffer.appendSlice(data); - } - - pub fn getContents(self: *const OutputCapture) []const u8 { - return self.buffer.items; - } -}; - -const InputSource = struct { - data: []const u8, - position: usize, - - pub fn init(data: []const u8) InputSource { - return InputSource{ - .data = data, - .position = 0, - }; - } - - pub fn readLine(self: *InputSource, allocator: Allocator) !?[]const u8 { - if (self.position >= self.data.len) { - return null; // EOF - } - - var line_end = self.position; - while (line_end < self.data.len and self.data[line_end] != '\n') { - line_end += 1; - } - - const line = self.data[self.position..line_end]; - self.position = if (line_end < self.data.len) line_end + 1 else self.data.len; - - // Remove trailing \r if present (DOS line endings) - if (line.len > 0 and line[line.len - 1] == '\r') { - return try allocator.dupe(u8, line[0 .. line.len - 1]); - } else { - return try allocator.dupe(u8, line); - } - } -}; - fn formatDosDateTime(allocator: Allocator, timestamp_secs: i64) ![]const u8 { const epoch_seconds = @as(u64, @intCast(@max(timestamp_secs, 0))); const epoch_day = @divFloor(epoch_seconds, std.time.s_per_day); @@ -544,193 +491,7 @@ fn executeCommandWithOutput(command: Command, allocator: Allocator, output_captu } }, .Copy => |copy| { - // Handle source file - const source_path = switch (copy.from) { - .Con => { - // COPY CON <file> - copy from console to file - const dest_path = switch (copy.to) { - .Con => { - const error_msg = "Cannot copy from CON to CON\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Lpt1, .Lpt2, .Lpt3, .Prn => { - const error_msg = "Cannot copy to device\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Path => |path| path, - }; - - // Create the destination file - const dest_file = std.fs.cwd().createFile(dest_path, .{}) catch |err| { - const error_msg = switch (err) { - error.AccessDenied => "Access denied\n", - error.PathAlreadyExists => "File already exists - use different name\n", - else => "Cannot create file\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - defer dest_file.close(); - - // Read from stdin and write to file until EOF (Ctrl+Z on DOS) - const stdin = std.io.getStdIn().reader(); - var line_count: u32 = 0; - - // Skip output redirection since we're doing interactive input - if (output_capture == null) { - // In interactive mode, show no prompt (DOS behavior) - } - - while (true) { - if (stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |maybe_line| { - if (maybe_line) |line| { - defer allocator.free(line); - - // Check for Ctrl+Z (EOF marker) - if (line.len == 1 and line[0] == 26) { // ASCII 26 = Ctrl+Z - break; - } - - // Remove trailing \r if present (Windows line endings) - const clean_line = if (line.len > 0 and line[line.len - 1] == '\r') - line[0 .. line.len - 1] - else - line; - - // Write line to file with DOS line ending - try dest_file.writeAll(clean_line); - try dest_file.writeAll("\r\n"); - line_count += 1; - } else { - // EOF reached - break; - } - } else |_| { - // Error reading input - break; - } - } - - const msg = try std.fmt.allocPrint(allocator, " 1 File(s) copied\n", .{}); - defer allocator.free(msg); - if (output_capture) |capture| { - try capture.write(msg); - } else { - print("{s}", .{msg}); - } - return CommandStatus{ .Code = 0 }; - }, - .Lpt1, .Lpt2, .Lpt3, .Prn => { - const error_msg = "Cannot copy from device\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Path => |path| path, - }; - - // Handle destination - const dest_path = switch (copy.to) { - .Con => { - // Copy to console (display file contents) - const source_file = std.fs.cwd().openFile(source_path, .{}) catch |err| { - const error_msg = switch (err) { - error.FileNotFound => "File not found\n", - error.AccessDenied => "Access denied\n", - else => "Cannot access source file\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - defer source_file.close(); - - // Read and display file contents - var buffer: [4096]u8 = undefined; - while (true) { - const bytes_read = source_file.readAll(&buffer) catch { - const error_msg = "Error reading file\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - if (bytes_read == 0) break; - - if (output_capture) |capture| { - try capture.write(buffer[0..bytes_read]); - } else { - print("{s}", .{buffer[0..bytes_read]}); - } - - if (bytes_read < buffer.len) break; - } - - const msg = " 1 File(s) copied\n"; - if (output_capture) |capture| { - try capture.write(msg); - } else { - print("{s}", .{msg}); - } - return CommandStatus{ .Code = 0 }; - }, - .Lpt1, .Lpt2, .Lpt3, .Prn => { - const error_msg = "Cannot copy to device\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Path => |path| path, - }; - - // Regular file-to-file copy - std.fs.cwd().copyFile(source_path, std.fs.cwd(), dest_path, .{}) catch |err| { - const error_msg = switch (err) { - error.FileNotFound => "File not found\n", - error.AccessDenied => "Access denied\n", - error.PathAlreadyExists => "File already exists\n", - else => "Cannot copy file\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - - const msg = " 1 File(s) copied\n"; - if (output_capture) |capture| { - try capture.write(msg); - } else { - print("{s}", .{msg}); - } - return CommandStatus{ .Code = 0 }; + return copy.eval(allocator, output_capture, input_source); }, .Remove => |remove| { const file_path = remove.path; diff --git a/src/main.zig b/src/main.zig index c3e83b5..7b4b43e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator; const Thread = std.Thread; const Mutex = std.Thread.Mutex; -const parser = @import("syntax.zig"); +const parser = @import("parser.zig"); const Command = parser.Command; const eval = @import("eval.zig"); diff --git a/src/parser.zig b/src/parser.zig new file mode 100644 index 0000000..b07f31c --- /dev/null +++ b/src/parser.zig @@ -0,0 +1,416 @@ +const std = @import("std"); +const ArrayList = std.ArrayList; +const Allocator = std.mem.Allocator; + +const syntax = @import("syntax.zig"); +const FileSpec = syntax.FileSpec; +const RedirectType = syntax.RedirectType; +const Redirect = syntax.Redirect; +const Token = syntax.Token; + +const cmd = @import("cmd.zig"); +const BuiltinCommand = cmd.BuiltinCommand; +const Command = cmd.Command; + +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 = try parseFilespec(self.allocator, 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, + } + } + + const result = try self.parseBuiltinCommand(command_name, args); + // For builtin commands, free the args ArrayList (the strings inside belong to tokens and will be freed later) + if (result == .Builtin) { + args.deinit(); + } + return result; + }, + 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) + try self.allocator.dupe(u8, ".") + else + try self.allocator.dupe(u8, 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 = try parseFilespec(self.allocator, args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .Type = .{ .file = file_spec } } }; + } else if (std.mem.eql(u8, cmd_upper, "SORT")) { + return Command{ .Builtin = BuiltinCommand.Sort }; + } else if (std.mem.eql(u8, cmd_upper, "CD") or std.mem.eql(u8, cmd_upper, "CHDIR")) { + const path = if (args.items.len == 0) + try self.allocator.dupe(u8, "") + else + try self.allocator.dupe(u8, args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .Chdir = .{ .path = path } } }; + } else if (std.mem.eql(u8, cmd_upper, "COPY")) { + if (args.items.len < 2) { + return error.ExpectedWord; // Will show "Bad command or file name" + } + const from_spec = try parseFilespec(self.allocator, args.items[0]); + const to_spec = try parseFilespec(self.allocator, args.items[1]); + return Command{ .Builtin = BuiltinCommand{ .Copy = .{ .from = from_spec, .to = to_spec } } }; + } else if (std.mem.eql(u8, cmd_upper, "DEL") or std.mem.eql(u8, cmd_upper, "ERASE")) { + if (args.items.len == 0) { + return error.ExpectedWord; // Will show "Bad command or file name" + } + const path = try self.allocator.dupe(u8, args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .Remove = .{ .path = path } } }; + } else if (std.mem.eql(u8, cmd_upper, "MD") or std.mem.eql(u8, cmd_upper, "MKDIR")) { + if (args.items.len == 0) { + return error.ExpectedWord; // Will show "Bad command or file name" + } + const path = try self.allocator.dupe(u8, args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .Mkdir = .{ .path = path } } }; + } else if (std.mem.eql(u8, cmd_upper, "RD") or std.mem.eql(u8, cmd_upper, "RMDIR")) { + if (args.items.len == 0) { + return error.ExpectedWord; // Will show "Bad command or file name" + } + const path = try self.allocator.dupe(u8, args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .Rmdir = .{ .path = path } } }; + } else if (std.mem.eql(u8, cmd_upper, "REN") or std.mem.eql(u8, cmd_upper, "RENAME")) { + if (args.items.len < 2) { + return error.ExpectedWord; // Will show "Bad command or file name" + } + const from_spec = try parseFilespec(self.allocator, args.items[0]); + const to_spec = try parseFilespec(self.allocator, args.items[1]); + return Command{ .Builtin = BuiltinCommand{ .Rename = .{ .from = from_spec, .to = to_spec } } }; + } else if (std.mem.eql(u8, cmd_upper, "MOVE")) { + // MOVE command is more complex - for now just show not implemented + return Command{ .Builtin = BuiltinCommand.Move }; + } else if (std.mem.eql(u8, cmd_upper, "PATH")) { + if (args.items.len == 0) { + return Command{ .Builtin = BuiltinCommand.PathGet }; + } else { + // PATH=value or PATH value + const value = if (std.mem.startsWith(u8, args.items[0], "=")) + try self.allocator.dupe(u8, args.items[0][1..]) // Skip the '=' + else + try self.allocator.dupe(u8, args.items[0]); + return Command{ .Builtin = BuiltinCommand{ .PathSet = .{ .value = value } } }; + } + } else { + // External command - need to duplicate all strings + const program_copy = try self.allocator.dupe(u8, command_name); + var args_copy = ArrayList([]const u8).init(self.allocator); + for (args.items) |arg| { + const arg_copy = try self.allocator.dupe(u8, arg); + try args_copy.append(arg_copy); + } + args.deinit(); // Free the original args list (but not the strings, as they belong to tokens) + return Command{ .External = .{ .program = program_copy, .args = args_copy } }; + } + } +}; + +fn parseFilespec(allocator: Allocator, path_str: []const u8) !FileSpec { + var upper_buf: [256]u8 = undefined; + if (path_str.len >= upper_buf.len) return FileSpec{ .Path = try allocator.dupe(u8, 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 = try allocator.dupe(u8, path_str) }; +} + +pub fn parse(input: []const u8, allocator: Allocator) !Command { + const trimmed = std.mem.trim(u8, input, " \t\r\n"); + if (trimmed.len == 0) { + return Command.Empty; + } + + 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); + return parser.parseCommand(); +} diff --git a/src/syntax.zig b/src/syntax.zig index 0725859..44aee06 100644 --- a/src/syntax.zig +++ b/src/syntax.zig @@ -1,7 +1,3 @@ -const std = @import("std"); -const ArrayList = std.ArrayList; -const Allocator = std.mem.Allocator; - pub const FileSpec = union(enum) { Con, Lpt1, @@ -22,251 +18,7 @@ pub const Redirect = struct { target: FileSpec, }; -pub 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, -}; - -pub 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); - for (redirect.redirects.items) |redir| { - switch (redir.target) { - .Path => |path| allocator.free(path), - else => {}, - } - } - redirect.redirects.deinit(); - }, - .External => |*external| { - allocator.free(external.program); - for (external.args.items) |arg| { - allocator.free(arg); - } - external.args.deinit(); - }, - .Builtin => |builtin| { - switch (builtin) { - .Dir => |dir| allocator.free(dir.path), - .Chdir => |chdir| allocator.free(chdir.path), - .EchoText => |echo| allocator.free(echo.message), - .Type => |type_cmd| { - switch (type_cmd.file) { - .Path => |path| allocator.free(path), - else => {}, - } - }, - .Copy => |copy| { - switch (copy.from) { - .Path => |path| allocator.free(path), - else => {}, - } - switch (copy.to) { - .Path => |path| allocator.free(path), - else => {}, - } - }, - .Remove => |remove| allocator.free(remove.path), - .Mkdir => |mkdir| allocator.free(mkdir.path), - .Deltree => |deltree| allocator.free(deltree.path), - .Tree => |tree| allocator.free(tree.path), - .Rmdir => |rmdir| allocator.free(rmdir.path), - .PathSet => |pathset| allocator.free(pathset.value), - .PromptSet => |promptset| allocator.free(promptset.message), - .Set => |set| { - allocator.free(set.name); - allocator.free(set.value); - }, - .Rem => |rem| allocator.free(rem.message), - .Rename => |rename| { - switch (rename.from) { - .Path => |path| allocator.free(path), - else => {}, - } - switch (rename.to) { - .Path => |path| allocator.free(path), - else => {}, - } - }, - .Xcopy => |xcopy| { - switch (xcopy.from) { - .Path => |path| allocator.free(path), - else => {}, - } - switch (xcopy.to) { - .Path => |path| allocator.free(path), - else => {}, - } - }, - else => {}, - } - }, - else => {}, - } - } -}; - -const Token = union(enum) { +pub const Token = union(enum) { Word: []const u8, Pipe, RedirectOut, // > @@ -275,406 +27,3 @@ const Token = union(enum) { 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 = try parseFilespec(self.allocator, 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, - } - } - - const result = try self.parseBuiltinCommand(command_name, args); - // For builtin commands, free the args ArrayList (the strings inside belong to tokens and will be freed later) - if (result == .Builtin) { - args.deinit(); - } - return result; - }, - 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) - try self.allocator.dupe(u8, ".") - else - try self.allocator.dupe(u8, 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 = try parseFilespec(self.allocator, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .Type = .{ .file = file_spec } } }; - } else if (std.mem.eql(u8, cmd_upper, "SORT")) { - return Command{ .Builtin = BuiltinCommand.Sort }; - } else if (std.mem.eql(u8, cmd_upper, "CD") or std.mem.eql(u8, cmd_upper, "CHDIR")) { - const path = if (args.items.len == 0) - try self.allocator.dupe(u8, "") - else - try self.allocator.dupe(u8, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .Chdir = .{ .path = path } } }; - } else if (std.mem.eql(u8, cmd_upper, "COPY")) { - if (args.items.len < 2) { - return error.ExpectedWord; // Will show "Bad command or file name" - } - const from_spec = try parseFilespec(self.allocator, args.items[0]); - const to_spec = try parseFilespec(self.allocator, args.items[1]); - return Command{ .Builtin = BuiltinCommand{ .Copy = .{ .from = from_spec, .to = to_spec } } }; - } else if (std.mem.eql(u8, cmd_upper, "DEL") or std.mem.eql(u8, cmd_upper, "ERASE")) { - if (args.items.len == 0) { - return error.ExpectedWord; // Will show "Bad command or file name" - } - const path = try self.allocator.dupe(u8, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .Remove = .{ .path = path } } }; - } else if (std.mem.eql(u8, cmd_upper, "MD") or std.mem.eql(u8, cmd_upper, "MKDIR")) { - if (args.items.len == 0) { - return error.ExpectedWord; // Will show "Bad command or file name" - } - const path = try self.allocator.dupe(u8, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .Mkdir = .{ .path = path } } }; - } else if (std.mem.eql(u8, cmd_upper, "RD") or std.mem.eql(u8, cmd_upper, "RMDIR")) { - if (args.items.len == 0) { - return error.ExpectedWord; // Will show "Bad command or file name" - } - const path = try self.allocator.dupe(u8, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .Rmdir = .{ .path = path } } }; - } else if (std.mem.eql(u8, cmd_upper, "REN") or std.mem.eql(u8, cmd_upper, "RENAME")) { - if (args.items.len < 2) { - return error.ExpectedWord; // Will show "Bad command or file name" - } - const from_spec = try parseFilespec(self.allocator, args.items[0]); - const to_spec = try parseFilespec(self.allocator, args.items[1]); - return Command{ .Builtin = BuiltinCommand{ .Rename = .{ .from = from_spec, .to = to_spec } } }; - } else if (std.mem.eql(u8, cmd_upper, "MOVE")) { - // MOVE command is more complex - for now just show not implemented - return Command{ .Builtin = BuiltinCommand.Move }; - } else if (std.mem.eql(u8, cmd_upper, "PATH")) { - if (args.items.len == 0) { - return Command{ .Builtin = BuiltinCommand.PathGet }; - } else { - // PATH=value or PATH value - const value = if (std.mem.startsWith(u8, args.items[0], "=")) - try self.allocator.dupe(u8, args.items[0][1..]) // Skip the '=' - else - try self.allocator.dupe(u8, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .PathSet = .{ .value = value } } }; - } - } else { - // External command - need to duplicate all strings - const program_copy = try self.allocator.dupe(u8, command_name); - var args_copy = ArrayList([]const u8).init(self.allocator); - for (args.items) |arg| { - const arg_copy = try self.allocator.dupe(u8, arg); - try args_copy.append(arg_copy); - } - args.deinit(); // Free the original args list (but not the strings, as they belong to tokens) - return Command{ .External = .{ .program = program_copy, .args = args_copy } }; - } - } -}; - -fn parseFilespec(allocator: Allocator, path_str: []const u8) !FileSpec { - var upper_buf: [256]u8 = undefined; - if (path_str.len >= upper_buf.len) return FileSpec{ .Path = try allocator.dupe(u8, 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 = try allocator.dupe(u8, path_str) }; -} - -pub fn parse(input: []const u8, allocator: Allocator) !Command { - const trimmed = std.mem.trim(u8, input, " \t\r\n"); - if (trimmed.len == 0) { - return Command.Empty; - } - - 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); - return parser.parseCommand(); -} |