summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cmd.zig250
-rw-r--r--src/cmd/copy.zig208
-rw-r--r--src/cmd/types.zig63
-rw-r--r--src/eval.zig259
-rw-r--r--src/main.zig2
-rw-r--r--src/parser.zig416
-rw-r--r--src/syntax.zig653
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();
-}