From f40779cf8ebe81ffa802967587a83d71c74c1c7e Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Thu, 14 Aug 2025 16:31:23 +0200 Subject: Factor out CD, DEL, MD, RD, PATH, DIR, TYPE. --- src/cmd.zig | 44 +++-- src/cmd/chdir.zig | 99 ++++++++++++ src/cmd/dir.zig | 157 ++++++++++++++++++ src/cmd/mkdir.zig | 38 +++++ src/cmd/path.zig | 67 ++++++++ src/cmd/remove.zig | 50 ++++++ src/cmd/rename.zig | 66 ++++++++ src/cmd/rmdir.zig | 39 +++++ src/cmd/type.zig | 109 +++++++++++++ src/eval.zig | 466 ++--------------------------------------------------- 10 files changed, 656 insertions(+), 479 deletions(-) create mode 100644 src/cmd/chdir.zig create mode 100644 src/cmd/dir.zig create mode 100644 src/cmd/mkdir.zig create mode 100644 src/cmd/path.zig create mode 100644 src/cmd/remove.zig create mode 100644 src/cmd/rename.zig create mode 100644 src/cmd/rmdir.zig create mode 100644 src/cmd/type.zig diff --git a/src/cmd.zig b/src/cmd.zig index 665b2bf..c24cea5 100644 --- a/src/cmd.zig +++ b/src/cmd.zig @@ -7,6 +7,15 @@ const FileSpec = syntax.FileSpec; const Redirect = syntax.Redirect; const Copy = @import("cmd/copy.zig").Copy; +const Chdir = @import("cmd/chdir.zig").Chdir; +const Remove = @import("cmd/remove.zig").Remove; +const Mkdir = @import("cmd/mkdir.zig").Mkdir; +const Rmdir = @import("cmd/rmdir.zig").Rmdir; +const Rename = @import("cmd/rename.zig").Rename; +const PathGet = @import("cmd/path.zig").PathGet; +const PathSet = @import("cmd/path.zig").PathSet; +const Dir = @import("cmd/dir.zig").Dir; +const Type = @import("cmd/type.zig").Type; pub const BuiltinCommand = union(enum) { // File-oriented @@ -14,33 +23,20 @@ pub const BuiltinCommand = union(enum) { Deltree: struct { path: []const u8, }, - Dir: struct { - path: []const u8, - }, + Dir: Dir, Fc, Find, - Mkdir: struct { - path: []const u8, - }, + Mkdir: Mkdir, Move, - Remove: struct { - path: []const u8, - }, - Rename: struct { - from: FileSpec, - to: FileSpec, - }, + Remove: Remove, + Rename: Rename, Replace, - Rmdir: struct { - path: []const u8, - }, + Rmdir: Rmdir, Sort, Tree: struct { path: []const u8, }, - Type: struct { - file: FileSpec, - }, + Type: Type, Xcopy: struct { from: FileSpec, to: FileSpec, @@ -49,9 +45,7 @@ pub const BuiltinCommand = union(enum) { // Shell-oriented Append, - Chdir: struct { - path: []const u8, - }, + Chdir: Chdir, EchoOff, EchoOn, EchoPlain, @@ -59,10 +53,8 @@ pub const BuiltinCommand = union(enum) { message: []const u8, }, Exit, - PathGet, - PathSet: struct { - value: []const u8, - }, + PathGet: PathGet, + PathSet: PathSet, PromptGet, PromptSet: struct { message: []const u8, diff --git a/src/cmd/chdir.zig b/src/cmd/chdir.zig new file mode 100644 index 0000000..fa2da83 --- /dev/null +++ b/src/cmd/chdir.zig @@ -0,0 +1,99 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +const paths = @import("../paths.zig"); +const formatDosPath = paths.formatDosPath; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +pub const Chdir = struct { + path: []const u8, + + pub fn eval(chdir: Chdir, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = input_source; + + if (chdir.path.len == 0) { + // No arguments - display current directory + const cwd = std.fs.cwd().realpathAlloc(allocator, ".") catch { + const error_msg = "Unable to determine current directory\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + defer allocator.free(cwd); + + const formatted_path = try formatDosPath(allocator, cwd); + defer allocator.free(formatted_path); + + const output = try std.fmt.allocPrint(allocator, "{s}\n", .{formatted_path}); + defer allocator.free(output); + + if (output_capture) |capture| { + try capture.write(output); + } else { + print("{s}", .{output}); + } + return CommandStatus{ .Code = 0 }; + } else { + // Change directory + const target_path = chdir.path; + + // Handle special cases + if (std.mem.eql(u8, target_path, "..")) { + // Go to parent directory + std.process.changeCurDir("..") catch { + const error_msg = "The system cannot find the path specified.\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + } else if (std.mem.eql(u8, target_path, "\\") or std.mem.eql(u8, target_path, "/")) { + // Go to root directory - simplified to just go to "/" + std.process.changeCurDir("/") catch { + const error_msg = "The system cannot find the path specified.\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + } else { + // Regular directory change + // Make sure the path doesn't contain null bytes + for (target_path) |ch| { + if (ch == 0) { + const error_msg = "Invalid path: contains null character\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + } + } + + std.process.changeCurDir(target_path) catch { + const error_msg = "The system cannot find the path specified.\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + } + return CommandStatus{ .Code = 0 }; + } + } +}; diff --git a/src/cmd/dir.zig b/src/cmd/dir.zig new file mode 100644 index 0000000..0c57e6f --- /dev/null +++ b/src/cmd/dir.zig @@ -0,0 +1,157 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const print = std.debug.print; + +const c = @cImport({ + @cInclude("errno.h"); + @cInclude("stdio.h"); + if (@import("builtin").os.tag != .windows) { + @cInclude("sys/statvfs.h"); + } +}); + +const paths = @import("../paths.zig"); +const formatDosPath = paths.formatDosPath; +const convertTo83 = paths.convertTo83; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +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); + const day_seconds = epoch_seconds % std.time.s_per_day; + + // Calculate date (simplified) + var year: u32 = 1970; // Start from Unix epoch year + var remaining_days = epoch_day; + + // Simple year calculation + while (remaining_days >= 365) { + const days_in_year: u64 = if (isLeapYear(year)) 366 else 365; + if (remaining_days < days_in_year) break; + remaining_days -= days_in_year; + year += 1; + } + + // Simple month/day calculation (approximate) + const month = @min(@divFloor(remaining_days, 30) + 1, 12); + const day = @min(remaining_days % 30 + 1, 31); + + // Calculate time + const hours = day_seconds / std.time.s_per_hour; + const minutes = (day_seconds % std.time.s_per_hour) / std.time.s_per_min; + + // Format as MM-DD-YY HH:MMa (DOS style) + const am_pm = if (hours < 12) "a" else "p"; + const display_hour = if (hours == 0) 12 else if (hours > 12) hours - 12 else hours; + + return try std.fmt.allocPrint(allocator, "{d:0>2}-{d:0>2}-{d:0>2} {d:>2}:{d:0>2}{s}", .{ @as(u32, @intCast(month)), @as(u32, @intCast(day)), @as(u32, @intCast(year % 100)), @as(u32, @intCast(display_hour)), @as(u32, @intCast(minutes)), am_pm }); +} + +fn isLeapYear(year: u32) bool { + return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); +} + +const GetFreeDiskSpaceError = error{ NotImplemented, AccessDenied }; + +fn getFreeDiskSpace(path: []const u8) GetFreeDiskSpaceError!u64 { + if (@import("builtin").os.tag == .windows) { + return error.NotImplemented; + } + + var stat: c.struct_statvfs = undefined; + if (c.statvfs(path.ptr, &stat) != 0) { + _ = c.perror("statvfs"); + return error.AccessDenied; + } + + return stat.f_bsize * stat.f_bfree; +} + +pub const Dir = struct { + path: []const u8, + + pub fn eval(dir: Dir, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = input_source; + + var output_buffer = ArrayList(u8).init(allocator); + defer output_buffer.deinit(); + + // Format path in DOS style with backslashes and uppercase drive letter + const formatted_path = try formatDosPath(allocator, dir.path); + defer allocator.free(formatted_path); + + // Get volume label (simplified - just show drive) + const drive_letter = if (formatted_path.len >= 2 and formatted_path[1] == ':') + formatted_path[0] + else + 'C'; + try output_buffer.writer().print(" Volume in drive {c} has no label\n", .{drive_letter}); + try output_buffer.writer().print(" Volume Serial Number is 1234-5678\n", .{}); + try output_buffer.writer().print("\n Directory of {s}\n\n", .{formatted_path}); + + var dir_iterator = std.fs.cwd().openDir(dir.path, .{ .iterate = true }) catch { + const error_msg = "File not found\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + defer dir_iterator.close(); + + var iterator = dir_iterator.iterate(); + var file_count: u32 = 0; + var dir_count: u32 = 0; + var total_file_bytes: u64 = 0; + + while (try iterator.next()) |entry| { + const stat = dir_iterator.statFile(entry.name) catch continue; + + // Convert timestamp to DOS date/time format + const mtime_secs = @divFloor(stat.mtime, std.time.ns_per_s); + const date_time = try formatDosDateTime(allocator, @intCast(mtime_secs)); + defer allocator.free(date_time); + + // Convert filename to 8.3 format + const short_name = try convertTo83(allocator, entry.name); + defer allocator.free(short_name); + + switch (entry.kind) { + .directory => { + try output_buffer.writer().print("{s} {s}\n", .{ date_time, short_name }); + dir_count += 1; + }, + .file => { + try output_buffer.writer().print("{s} {d:>14} {s}\n", .{ date_time, stat.size, short_name }); + file_count += 1; + total_file_bytes += stat.size; + }, + else => {}, + } + } + + // Get free disk space using statvfs + const path = try std.fs.cwd().realpathAlloc(allocator, dir.path); + defer allocator.free(path); + const bytes_free = getFreeDiskSpace(path) catch |err| switch (err) { + error.AccessDenied => 0, + error.NotImplemented => 0, + }; + + try output_buffer.writer().print(" {d} File(s) {d:>14} bytes\n", .{ file_count, total_file_bytes }); + try output_buffer.writer().print(" {d} Dir(s) {d:>14} bytes free\n", .{ dir_count, bytes_free }); + + if (output_capture) |capture| { + try capture.write(output_buffer.items); + } else { + print("{s}", .{output_buffer.items}); + } + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/mkdir.zig b/src/cmd/mkdir.zig new file mode 100644 index 0000000..55d2ab6 --- /dev/null +++ b/src/cmd/mkdir.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +pub const Mkdir = struct { + path: []const u8, + + pub fn eval(mkdir: Mkdir, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = allocator; + _ = input_source; + + const dir_path = mkdir.path; + + std.fs.cwd().makeDir(dir_path) catch |err| { + const error_msg = switch (err) { + error.PathAlreadyExists => "A subdirectory or file already exists\n", + error.AccessDenied => "Access denied\n", + error.FileNotFound => "The system cannot find the path specified\n", + error.NotDir => "The system cannot find the path specified\n", + else => "Unable to create directory\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + + // No output for successful creation (DOS style) + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/path.zig b/src/cmd/path.zig new file mode 100644 index 0000000..734350a --- /dev/null +++ b/src/cmd/path.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +pub const PathGet = struct { + pub fn eval(path_get: PathGet, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = path_get; + _ = input_source; + + const current_path = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { + error.EnvironmentVariableNotFound => { + // PATH not set, show empty + const output = "PATH=(not set)\n"; + if (output_capture) |capture| { + try capture.write(output); + } else { + print("{s}", .{output}); + } + return CommandStatus{ .Code = 0 }; + }, + else => { + const error_msg = "Cannot access PATH environment variable\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + }; + defer allocator.free(current_path); + + const output = try std.fmt.allocPrint(allocator, "PATH={s}\n", .{current_path}); + defer allocator.free(output); + if (output_capture) |capture| { + try capture.write(output); + } else { + print("{s}", .{output}); + } + return CommandStatus{ .Code = 0 }; + } +}; + +pub const PathSet = struct { + value: []const u8, + + pub fn eval(path_set: PathSet, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = input_source; + + // Note: In a real DOS system, this would persist for the session + // Here we just show what would be set but don't actually set it + // since Zig's std.process doesn't provide a simple way to set env vars + const output = try std.fmt.allocPrint(allocator, "PATH would be set to: {s}\n(Note: Environment variable setting not implemented in this shell)\n", .{path_set.value}); + defer allocator.free(output); + if (output_capture) |capture| { + try capture.write(output); + } else { + print("{s}", .{output}); + } + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/remove.zig b/src/cmd/remove.zig new file mode 100644 index 0000000..91acf31 --- /dev/null +++ b/src/cmd/remove.zig @@ -0,0 +1,50 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +pub const Remove = struct { + path: []const u8, + + pub fn eval(remove: Remove, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = allocator; + _ = input_source; + + const file_path = remove.path; + + // Check for wildcards (basic support) + if (std.mem.indexOf(u8, file_path, "*") != null or std.mem.indexOf(u8, file_path, "?") != null) { + // Simple wildcard deletion - just show error for now + const error_msg = "Wildcard deletion not yet implemented\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + } + + // Delete single file + std.fs.cwd().deleteFile(file_path) catch |err| { + const error_msg = switch (err) { + error.FileNotFound => "File not found\n", + error.AccessDenied => "Access denied\n", + error.IsDir => "Access denied - cannot delete directory\n", + else => "Cannot delete file\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + + // No output for successful deletion (DOS style) + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/rename.zig b/src/cmd/rename.zig new file mode 100644 index 0000000..e3401d2 --- /dev/null +++ b/src/cmd/rename.zig @@ -0,0 +1,66 @@ +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 Rename = struct { + from: FileSpec, + to: FileSpec, + + pub fn eval(rename: Rename, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = allocator; + _ = input_source; + + const from_path = switch (rename.from) { + .Con, .Lpt1, .Lpt2, .Lpt3, .Prn => { + const error_msg = "Cannot rename device\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + const to_path = switch (rename.to) { + .Con, .Lpt1, .Lpt2, .Lpt3, .Prn => { + const error_msg = "Cannot rename to device\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + std.fs.cwd().rename(from_path, to_path) catch |err| { + const error_msg = switch (err) { + error.FileNotFound => "The system cannot find the file specified\n", + error.AccessDenied => "Access denied\n", + error.PathAlreadyExists => "A duplicate file name exists, or the file cannot be found\n", + error.RenameAcrossMountPoints => "Cannot rename across different drives\n", + else => "Cannot rename file\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + + // No output for successful rename (DOS style) + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/rmdir.zig b/src/cmd/rmdir.zig new file mode 100644 index 0000000..03be046 --- /dev/null +++ b/src/cmd/rmdir.zig @@ -0,0 +1,39 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +const types = @import("./types.zig"); +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; +const InputSource = types.InputSource; + +pub const Rmdir = struct { + path: []const u8, + + pub fn eval(rmdir: Rmdir, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = allocator; + _ = input_source; + + const dir_path = rmdir.path; + + std.fs.cwd().deleteDir(dir_path) catch |err| { + const error_msg = switch (err) { + error.FileNotFound => "The system cannot find the path specified\n", + error.AccessDenied => "Access denied\n", + error.DirNotEmpty => "The directory is not empty\n", + error.FileBusy => "The directory is in use\n", + error.NotDir => "The system cannot find the path specified\n", + else => "Unable to remove directory\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + + // No output for successful removal (DOS style) + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/cmd/type.zig b/src/cmd/type.zig new file mode 100644 index 0000000..b908da9 --- /dev/null +++ b/src/cmd/type.zig @@ -0,0 +1,109 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +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 Type = struct { + file: FileSpec, + + pub fn eval(type_cmd: Type, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus { + _ = input_source; + + const file_path = switch (type_cmd.file) { + .Con => { + const error_msg = "Cannot TYPE from 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 TYPE from device\n"; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }, + .Path => |path| path, + }; + + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { + const error_msg = switch (err) { + error.FileNotFound => "The system cannot find the file specified.\n", + error.IsDir => "Access is denied.\n", + error.AccessDenied => "Access is denied.\n", + else => "Cannot access file.\n", + }; + if (output_capture) |capture| { + try capture.write(error_msg); + } else { + print("{s}", .{error_msg}); + } + return CommandStatus{ .Code = 1 }; + }; + defer file.close(); + + // Read and display file contents + var buffer: [4096]u8 = undefined; + while (true) { + const bytes_read = file.readAll(&buffer) catch |err| { + const error_msg = switch (err) { + error.AccessDenied => "Access is denied.\n", + else => "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; + + // Process buffer contents for output + var processed_output = ArrayList(u8).init(allocator); + defer processed_output.deinit(); + + for (buffer[0..bytes_read]) |byte| { + // Convert to printable characters, similar to DOS TYPE behavior + if (byte >= 32 and byte <= 126) { + try processed_output.append(byte); + } else if (byte == '\n') { + try processed_output.append('\n'); + } else if (byte == '\r') { + // Skip carriage return in DOS-style line endings + continue; + } else if (byte == '\t') { + try processed_output.append('\t'); + } else { + // Replace non-printable characters with '?' + try processed_output.append('?'); + } + } + + if (output_capture) |capture| { + try capture.write(processed_output.items); + } else { + print("{s}", .{processed_output.items}); + } + + // If we read less than the buffer size, we're done + if (bytes_read < buffer.len) break; + } + + return CommandStatus{ .Code = 0 }; + } +}; diff --git a/src/eval.zig b/src/eval.zig index dcf34f4..9b00462 100644 --- a/src/eval.zig +++ b/src/eval.zig @@ -34,36 +34,8 @@ const InputSource = cmdTypes.InputSource; const STDOUT_BUFFER_SIZE: usize = 1024; const STDERR_BUFFER_SIZE: usize = 1024; -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); - const day_seconds = epoch_seconds % std.time.s_per_day; - - // Calculate date (simplified) - var year: u32 = 1970; // Start from Unix epoch year - var remaining_days = epoch_day; - - // Simple year calculation - while (remaining_days >= 365) { - const days_in_year: u64 = if (isLeapYear(year)) 366 else 365; - if (remaining_days < days_in_year) break; - remaining_days -= days_in_year; - year += 1; - } - - // Simple month/day calculation (approximate) - const month = @min(@divFloor(remaining_days, 30) + 1, 12); - const day = @min(remaining_days % 30 + 1, 31); - - // Calculate time - const hours = day_seconds / std.time.s_per_hour; - const minutes = (day_seconds % std.time.s_per_hour) / std.time.s_per_min; - - // Format as MM-DD-YY HH:MMa (DOS style) - const am_pm = if (hours < 12) "a" else "p"; - const display_hour = if (hours == 0) 12 else if (hours > 12) hours - 12 else hours; - - return try std.fmt.allocPrint(allocator, "{d:0>2}-{d:0>2}-{d:0>2} {d:>2}:{d:0>2}{s}", .{ @as(u32, @intCast(month)), @as(u32, @intCast(day)), @as(u32, @intCast(year % 100)), @as(u32, @intCast(display_hour)), @as(u32, @intCast(minutes)), am_pm }); +fn isLeapYear(year: u32) bool { + return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); } pub fn executeCommand(command: Command, allocator: Allocator) !CommandStatus { @@ -194,171 +166,10 @@ fn executeCommandWithOutput(command: Command, allocator: Allocator, output_captu return CommandStatus{ .Code = 0 }; }, .Dir => |dir| { - var output_buffer = ArrayList(u8).init(allocator); - defer output_buffer.deinit(); - - // Format path in DOS style with backslashes and uppercase drive letter - const formatted_path = try formatDosPath(allocator, dir.path); - defer allocator.free(formatted_path); - - // Get volume label (simplified - just show drive) - const drive_letter = if (formatted_path.len >= 2 and formatted_path[1] == ':') - formatted_path[0] - else - 'C'; - try output_buffer.writer().print(" Volume in drive {c} has no label\n", .{drive_letter}); - try output_buffer.writer().print(" Volume Serial Number is 1234-5678\n", .{}); - try output_buffer.writer().print("\n Directory of {s}\n\n", .{formatted_path}); - - var dir_iterator = std.fs.cwd().openDir(dir.path, .{ .iterate = true }) catch { - const error_msg = "File not found\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - defer dir_iterator.close(); - - var iterator = dir_iterator.iterate(); - var file_count: u32 = 0; - var dir_count: u32 = 0; - var total_file_bytes: u64 = 0; - - while (try iterator.next()) |entry| { - const stat = dir_iterator.statFile(entry.name) catch continue; - - // Convert timestamp to DOS date/time format - const mtime_secs = @divFloor(stat.mtime, std.time.ns_per_s); - const date_time = try formatDosDateTime(allocator, @intCast(mtime_secs)); - defer allocator.free(date_time); - - // Convert filename to 8.3 format - const short_name = try convertTo83(allocator, entry.name); - defer allocator.free(short_name); - - switch (entry.kind) { - .directory => { - try output_buffer.writer().print("{s} {s}\n", .{ date_time, short_name }); - dir_count += 1; - }, - .file => { - try output_buffer.writer().print("{s} {d:>14} {s}\n", .{ date_time, stat.size, short_name }); - file_count += 1; - total_file_bytes += stat.size; - }, - else => {}, - } - } - - // Get free disk space using statvfs - const path = try std.fs.cwd().realpathAlloc(allocator, dir.path); - defer allocator.free(path); - const bytes_free = getFreeDiskSpace(path) catch |err| switch (err) { - error.AccessDenied => 0, - error.NotImplemented => 0, - }; - - try output_buffer.writer().print(" {d} File(s) {d:>14} bytes\n", .{ file_count, total_file_bytes }); - try output_buffer.writer().print(" {d} Dir(s) {d:>14} bytes free\n", .{ dir_count, bytes_free }); - - if (output_capture) |capture| { - try capture.write(output_buffer.items); - } else { - print("{s}", .{output_buffer.items}); - } - return CommandStatus{ .Code = 0 }; + return dir.eval(allocator, output_capture, input_source); }, .Type => |type_cmd| { - const file_path = switch (type_cmd.file) { - .Con => { - const error_msg = "Cannot TYPE from 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 TYPE from device\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Path => |path| path, - }; - - const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { - const error_msg = switch (err) { - error.FileNotFound => "The system cannot find the file specified.\n", - error.IsDir => "Access is denied.\n", - error.AccessDenied => "Access is denied.\n", - else => "Cannot access file.\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - defer file.close(); - - // Read and display file contents - var buffer: [4096]u8 = undefined; - while (true) { - const bytes_read = file.readAll(&buffer) catch |err| { - const error_msg = switch (err) { - error.AccessDenied => "Access is denied.\n", - else => "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; - - // Process buffer contents for output - var processed_output = ArrayList(u8).init(allocator); - defer processed_output.deinit(); - - for (buffer[0..bytes_read]) |byte| { - // Convert to printable characters, similar to DOS TYPE behavior - if (byte >= 32 and byte <= 126) { - try processed_output.append(byte); - } else if (byte == '\n') { - try processed_output.append('\n'); - } else if (byte == '\r') { - // Skip carriage return in DOS-style line endings - continue; - } else if (byte == '\t') { - try processed_output.append('\t'); - } else { - // Replace non-printable characters with '?' - try processed_output.append('?'); - } - } - - if (output_capture) |capture| { - try capture.write(processed_output.items); - } else { - print("{s}", .{processed_output.items}); - } - - // If we read less than the buffer size, we're done - if (bytes_read < buffer.len) break; - } - - return CommandStatus{ .Code = 0 }; + return type_cmd.eval(allocator, output_capture, input_source); }, .Sort => { var lines = ArrayList([]const u8).init(allocator); @@ -410,213 +221,22 @@ fn executeCommandWithOutput(command: Command, allocator: Allocator, output_captu return CommandStatus{ .Code = 0 }; }, .Chdir => |chdir| { - if (chdir.path.len == 0) { - // No arguments - display current directory - const cwd = std.fs.cwd().realpathAlloc(allocator, ".") catch { - const error_msg = "Unable to determine current directory\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - defer allocator.free(cwd); - - const formatted_path = try formatDosPath(allocator, cwd); - defer allocator.free(formatted_path); - - const output = try std.fmt.allocPrint(allocator, "{s}\n", .{formatted_path}); - defer allocator.free(output); - - if (output_capture) |capture| { - try capture.write(output); - } else { - print("{s}", .{output}); - } - return CommandStatus{ .Code = 0 }; - } else { - // Change directory - const target_path = chdir.path; - - // Handle special cases - if (std.mem.eql(u8, target_path, "..")) { - // Go to parent directory - std.process.changeCurDir("..") catch { - const error_msg = "The system cannot find the path specified.\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - } else if (std.mem.eql(u8, target_path, "\\") or std.mem.eql(u8, target_path, "/")) { - // Go to root directory - simplified to just go to "/" - std.process.changeCurDir("/") catch { - const error_msg = "The system cannot find the path specified.\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - } else { - // Regular directory change - // Make sure the path doesn't contain null bytes - for (target_path) |ch| { - if (ch == 0) { - const error_msg = "Invalid path: contains null character\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - } - } - - std.process.changeCurDir(target_path) catch { - const error_msg = "The system cannot find the path specified.\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - } - return CommandStatus{ .Code = 0 }; - } + return chdir.eval(allocator, output_capture, input_source); }, .Copy => |copy| { return copy.eval(allocator, output_capture, input_source); }, .Remove => |remove| { - const file_path = remove.path; - - // Check for wildcards (basic support) - if (std.mem.indexOf(u8, file_path, "*") != null or std.mem.indexOf(u8, file_path, "?") != null) { - // Simple wildcard deletion - just show error for now - const error_msg = "Wildcard deletion not yet implemented\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - } - - // Delete single file - std.fs.cwd().deleteFile(file_path) catch |err| { - const error_msg = switch (err) { - error.FileNotFound => "File not found\n", - error.AccessDenied => "Access denied\n", - error.IsDir => "Access denied - cannot delete directory\n", - else => "Cannot delete file\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - - // No output for successful deletion (DOS style) - return CommandStatus{ .Code = 0 }; + return remove.eval(allocator, output_capture, input_source); }, .Mkdir => |mkdir| { - const dir_path = mkdir.path; - - std.fs.cwd().makeDir(dir_path) catch |err| { - const error_msg = switch (err) { - error.PathAlreadyExists => "A subdirectory or file already exists\n", - error.AccessDenied => "Access denied\n", - error.FileNotFound => "The system cannot find the path specified\n", - error.NotDir => "The system cannot find the path specified\n", - else => "Unable to create directory\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - - // No output for successful creation (DOS style) - return CommandStatus{ .Code = 0 }; + return mkdir.eval(allocator, output_capture, input_source); }, .Rmdir => |rmdir| { - const dir_path = rmdir.path; - - std.fs.cwd().deleteDir(dir_path) catch |err| { - const error_msg = switch (err) { - error.FileNotFound => "The system cannot find the path specified\n", - error.AccessDenied => "Access denied\n", - error.DirNotEmpty => "The directory is not empty\n", - error.FileBusy => "The directory is in use\n", - error.NotDir => "The system cannot find the path specified\n", - else => "Unable to remove directory\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - - // No output for successful removal (DOS style) - return CommandStatus{ .Code = 0 }; + return rmdir.eval(allocator, output_capture, input_source); }, .Rename => |rename| { - const from_path = switch (rename.from) { - .Con, .Lpt1, .Lpt2, .Lpt3, .Prn => { - const error_msg = "Cannot rename device\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Path => |path| path, - }; - - const to_path = switch (rename.to) { - .Con, .Lpt1, .Lpt2, .Lpt3, .Prn => { - const error_msg = "Cannot rename to device\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - .Path => |path| path, - }; - - std.fs.cwd().rename(from_path, to_path) catch |err| { - const error_msg = switch (err) { - error.FileNotFound => "The system cannot find the file specified\n", - error.AccessDenied => "Access denied\n", - error.PathAlreadyExists => "A duplicate file name exists, or the file cannot be found\n", - error.RenameAcrossMountPoints => "Cannot rename across different drives\n", - else => "Cannot rename file\n", - }; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }; - - // No output for successful rename (DOS style) - return CommandStatus{ .Code = 0 }; + return rename.eval(allocator, output_capture, input_source); }, .Move => { const error_msg = "MOVE command not yet implemented\n"; @@ -627,51 +247,11 @@ fn executeCommandWithOutput(command: Command, allocator: Allocator, output_captu } return CommandStatus{ .Code = 1 }; }, - .PathGet => { - const current_path = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { - error.EnvironmentVariableNotFound => { - // PATH not set, show empty - const output = "PATH=(not set)\n"; - if (output_capture) |capture| { - try capture.write(output); - } else { - print("{s}", .{output}); - } - return CommandStatus{ .Code = 0 }; - }, - else => { - const error_msg = "Cannot access PATH environment variable\n"; - if (output_capture) |capture| { - try capture.write(error_msg); - } else { - print("{s}", .{error_msg}); - } - return CommandStatus{ .Code = 1 }; - }, - }; - defer allocator.free(current_path); - - const output = try std.fmt.allocPrint(allocator, "PATH={s}\n", .{current_path}); - defer allocator.free(output); - if (output_capture) |capture| { - try capture.write(output); - } else { - print("{s}", .{output}); - } - return CommandStatus{ .Code = 0 }; + .PathGet => |path_get| { + return path_get.eval(allocator, output_capture, input_source); }, - .PathSet => |pathset| { - // Note: In a real DOS system, this would persist for the session - // Here we just show what would be set but don't actually set it - // since Zig's std.process doesn't provide a simple way to set env vars - const output = try std.fmt.allocPrint(allocator, "PATH would be set to: {s}\n(Note: Environment variable setting not implemented in this shell)\n", .{pathset.value}); - defer allocator.free(output); - if (output_capture) |capture| { - try capture.write(output); - } else { - print("{s}", .{output}); - } - return CommandStatus{ .Code = 0 }; + .PathSet => |path_set| { + return path_set.eval(allocator, output_capture, input_source); }, else => { const error_msg = try std.fmt.allocPrint(allocator, "Command not implemented: {any}\n", .{builtin_cmd}); @@ -971,23 +551,3 @@ fn executeCommandWithOutput(command: Command, allocator: Allocator, output_captu }, } } - -fn isLeapYear(year: u32) bool { - return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); -} - -const GetFreeDiskSpaceError = error{ NotImplemented, AccessDenied }; - -fn getFreeDiskSpace(path: []const u8) GetFreeDiskSpaceError!u64 { - if (@import("builtin").os.tag == .windows) { - return error.NotImplemented; - } - - var stat: c.struct_statvfs = undefined; - if (c.statvfs(path.ptr, &stat) != 0) { - _ = c.perror("statvfs"); - return error.AccessDenied; - } - - return stat.f_bsize * stat.f_bfree; -} -- cgit v1.2.1