const std = @import("std");
const print = std.debug.print;
const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator;
const Thread = std.Thread;
const Mutex = std.Thread.Mutex;
const c = @cImport({
@cInclude("errno.h");
@cInclude("stdio.h");
if (@import("builtin").os.tag != .windows) {
@cInclude("sys/statvfs.h");
}
});
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;
const paths = @import("paths.zig");
const formatDosPath = paths.formatDosPath;
const convertTo83 = paths.convertTo83;
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);
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 });
}
pub fn executeCommand(command: Command, allocator: Allocator) !CommandStatus {
return executeCommandWithOutput(command, allocator, null, null);
}
fn executeCommandWithOutput(command: Command, allocator: Allocator, output_capture: ?*OutputCapture, input_source: ?*InputSource) !CommandStatus {
switch (command) {
.Empty => return CommandStatus{ .Code = 0 },
.Builtin => |builtin_cmd| {
switch (builtin_cmd) {
.EchoText => |echo| {
const output = try std.fmt.allocPrint(allocator, "{s}\n", .{echo.message});
defer allocator.free(output);
if (output_capture) |capture| {
try capture.write(output);
} else {
print("{s}", .{output});
}
return CommandStatus{ .Code = 0 };
},
.Cls => {
if (output_capture == null) {
// Clear screen - only works when not redirected
print("\x1B[2J\x1B[H", .{});
}
return CommandStatus{ .Code = 0 };
},
.Exit => {
return CommandStatus.ExitShell;
},
.EchoPlain => {
const output = "ECHO is on\n";
if (output_capture) |capture| {
try capture.write(output);
} else {
print("{s}", .{output});
}
return CommandStatus{ .Code = 0 };
},
.EchoOn => {
const output = "ECHO is on\n";
if (output_capture) |capture| {
try capture.write(output);
} else {
print("{s}", .{output});
}
return CommandStatus{ .Code = 0 };
},
.EchoOff => {
return CommandStatus{ .Code = 0 };
},
.Ver => {
const output = "MB-DOSE Version 6.22\n";
if (output_capture) |capture| {
try capture.write(output);
} else {
print("{s}", .{output});
}
return CommandStatus{ .Code = 0 };
},
.Date => {
const timestamp = std.time.timestamp();
const epoch_seconds = @as(u64, @intCast(timestamp));
const epoch_day = @divFloor(epoch_seconds, std.time.s_per_day);
// Calculate days since Unix epoch (1970-01-01)
// Unix epoch is 719163 days since year 1 AD
const days_since_year_1 = epoch_day + 719163;
// Simple algorithm to convert days to year/month/day
var year: u32 = 1;
var remaining_days = days_since_year_1;
// Find the year
while (true) {
const days_in_year: u64 = if (isLeapYear(year)) 366 else 365;
if (remaining_days < days_in_year) break;
remaining_days -= days_in_year;
year += 1;
}
// Days in each month (non-leap year)
const days_in_month = [_]u32{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
var month: u32 = 1;
for (days_in_month, 1..) |days, m| {
var month_days = days;
// Adjust February for leap years
if (m == 2 and isLeapYear(year)) {
month_days = 29;
}
if (remaining_days < month_days) {
month = @intCast(m);
break;
}
remaining_days -= month_days;
}
const day = remaining_days + 1; // Days are 1-indexed
const output = try std.fmt.allocPrint(allocator, "Current date is {d:0>2}/{d:0>2}/{d}\n", .{ month, day, year });
defer allocator.free(output);
if (output_capture) |capture| {
try capture.write(output);
} else {
print("{s}", .{output});
}
return CommandStatus{ .Code = 0 };
},
.Time => {
const timestamp = std.time.timestamp();
const epoch_seconds = @as(u64, @intCast(timestamp));
const day_seconds = epoch_seconds % std.time.s_per_day;
const hours = day_seconds / std.time.s_per_hour;
const minutes = (day_seconds % std.time.s_per_hour) / std.time.s_per_min;
const seconds = day_seconds % std.time.s_per_min;
const output = try std.fmt.allocPrint(allocator, "Current time is {d:0>2}:{d:0>2}:{d:0>2}\n", .{ hours, minutes, seconds });
defer allocator.free(output);
if (output_capture) |capture| {
try capture.write(output);
} else {
print("{s}", .{output});
}
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 };
},
.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 };
},
.Sort => {
var lines = ArrayList([]const u8).init(allocator);
defer {
for (lines.items) |line| {
allocator.free(line);
}
lines.deinit();
}
// Read input lines
if (input_source) |source| {
// Read from input redirection
while (try source.readLine(allocator)) |line| {
try lines.append(line);
}
} else {
// Read from stdin (simplified - just show message)
const msg = "SORT: Use input redirection (< file.txt) to sort file contents\n";
if (output_capture) |capture| {
try capture.write(msg);
} else {
print("{s}", .{msg});
}
return CommandStatus{ .Code = 0 };
}
// Sort the lines
std.mem.sort([]const u8, lines.items, {}, struct {
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.order(u8, lhs, rhs) == .lt;
}
}.lessThan);
// Output sorted lines
var output_buffer = ArrayList(u8).init(allocator);
defer output_buffer.deinit();
for (lines.items) |line| {
try output_buffer.writer().print("{s}\n", .{line});
}
if (output_capture) |capture| {
try capture.write(output_buffer.items);
} else {
print("{s}", .{output_buffer.items});
}
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 };
}
},
.Copy => |copy| {
// Handle source file
const source_path = switch (copy.from) {
.Con => {
// COPY CON - 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 };
},
.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 };
},
.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 };
},
else => {
const error_msg = try std.fmt.allocPrint(allocator, "Command not implemented: {any}\n", .{builtin_cmd});
defer allocator.free(error_msg);
if (output_capture) |capture| {
try capture.write(error_msg);
} else {
print("{s}", .{error_msg});
}
return CommandStatus{ .Code = 1 };
},
}
},
.External => |external| {
// Try to execute external command
var child_args = ArrayList([]const u8).init(allocator);
defer child_args.deinit();
try child_args.append(external.program);
for (external.args.items) |arg| {
try child_args.append(arg);
}
var child = std.process.Child.init(child_args.items, allocator);
// Set up pipes for capturing output
child.stdin_behavior = if (input_source != null) .Pipe else .Inherit;
child.stdout_behavior = if (output_capture != null) .Pipe else .Inherit;
child.stderr_behavior = if (output_capture != null) .Pipe else .Inherit;
const spawn_result = child.spawn();
if (spawn_result) |_| {
// Spawn succeeded, continue with execution
} else |err| switch (err) {
error.FileNotFound => {
const error_msg = try std.fmt.allocPrint(allocator, "'{s}' is not recognized as an internal or external command,\noperable program or batch file.\n", .{external.program});
defer allocator.free(error_msg);
if (output_capture) |capture| {
try capture.write(error_msg);
} else {
print("{s}", .{error_msg});
}
return CommandStatus{ .Code = 1 };
},
error.AccessDenied => {
const error_msg = "Access is denied.\n";
if (output_capture) |capture| {
try capture.write(error_msg);
} else {
print("{s}", .{error_msg});
}
return CommandStatus{ .Code = 1 };
},
else => {
const error_msg = try std.fmt.allocPrint(allocator, "Cannot execute '{s}': {}\n", .{ external.program, err });
defer allocator.free(error_msg);
if (output_capture) |capture| {
try capture.write(error_msg);
} else {
print("{s}", .{error_msg});
}
return CommandStatus{ .Code = 1 };
},
}
// Handle input redirection
if (input_source) |source| {
if (child.stdin) |stdin| {
const writer = stdin.writer();
// Reset source position for reading
var temp_source = source.*;
temp_source.position = 0;
while (try temp_source.readLine(allocator)) |line| {
defer allocator.free(line);
try writer.print("{s}\n", .{line});
}
child.stdin.?.close();
child.stdin = null;
}
}
// Handle output capture
if (output_capture) |capture| {
// Read stdout
if (child.stdout) |stdout| {
var buffer: [4096]u8 = undefined;
while (true) {
const bytes_read = stdout.read(&buffer) catch break;
if (bytes_read == 0) break;
try capture.write(buffer[0..bytes_read]);
}
}
// Read stderr
if (child.stderr) |stderr| {
var buffer: [4096]u8 = undefined;
while (true) {
const bytes_read = stderr.read(&buffer) catch break;
if (bytes_read == 0) break;
try capture.write(buffer[0..bytes_read]);
}
}
}
// Wait for process to complete
const term = child.wait() catch |err| {
const error_msg = switch (err) {
error.FileNotFound => try std.fmt.allocPrint(allocator, "'{s}' is not recognized as an internal or external command,\noperable program or batch file.\n", .{external.program}),
else => try std.fmt.allocPrint(allocator, "Error waiting for command: {}\n", .{err}),
};
defer allocator.free(error_msg);
if (output_capture) |capture| {
try capture.write(error_msg);
} else {
print("{s}", .{error_msg});
}
return CommandStatus{ .Code = 1 };
};
// Return exit code
switch (term) {
.Exited => |code| return CommandStatus{ .Code = @intCast(code) },
.Signal => |_| return CommandStatus{ .Code = 1 },
.Stopped => |_| return CommandStatus{ .Code = 1 },
.Unknown => |_| return CommandStatus{ .Code = 1 },
}
},
.Redirect => |redirect| {
// Check if we have any output redirections
var has_output_redirect = false;
for (redirect.redirects.items) |redir| {
if (redir.redirect_type == .OutputOverwrite or redir.redirect_type == .OutputAppend) {
has_output_redirect = true;
break;
}
}
var captured_output = OutputCapture.init(allocator);
defer captured_output.deinit();
// Prepare input redirection if needed
var input_data: ?[]const u8 = null;
var redirect_input_source: ?InputSource = null;
defer if (input_data) |data| allocator.free(data);
// Process input redirections first
for (redirect.redirects.items) |redir| {
if (redir.redirect_type == .InputFrom) {
const file_path = switch (redir.target) {
.Con => {
print("Input redirection from CON not supported\n", .{});
return CommandStatus{ .Code = 1 };
},
.Lpt1, .Lpt2, .Lpt3, .Prn => {
print("Cannot redirect input from device\n", .{});
return CommandStatus{ .Code = 1 };
},
.Path => |path| path,
};
// Read input file
const file = std.fs.cwd().openFile(file_path, .{}) catch |err| {
switch (err) {
error.FileNotFound => print("The system cannot find the file specified.\n", .{}),
error.AccessDenied => print("Access is denied.\n", .{}),
else => print("Cannot open input file.\n", .{}),
}
return CommandStatus{ .Code = 1 };
};
defer file.close();
input_data = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| {
switch (err) {
error.AccessDenied => print("Access is denied.\n", .{}),
else => print("Cannot read input file.\n", .{}),
}
return CommandStatus{ .Code = 1 };
};
redirect_input_source = InputSource.init(input_data.?);
break; // Only handle first input redirection
}
}
// Execute the command with input and output capture (only capture output if needed)
const status = try executeCommandWithOutput(redirect.command.*, allocator, if (has_output_redirect) &captured_output else null, if (redirect_input_source) |*source| source else null);
// Handle output redirections
for (redirect.redirects.items) |redir| {
if (redir.redirect_type == .InputFrom) continue; // Already handled
const file_path = switch (redir.target) {
.Con => {
// Redirect to console - just print normally
print("{s}", .{captured_output.getContents()});
continue;
},
.Lpt1, .Lpt2, .Lpt3, .Prn => {
print("Cannot redirect to device\n", .{});
return CommandStatus{ .Code = 1 };
},
.Path => |path| path,
};
// Handle different redirect types
switch (redir.redirect_type) {
.OutputOverwrite => {
// Write to file, overwriting existing content
const file = std.fs.cwd().createFile(file_path, .{}) catch |err| {
switch (err) {
error.AccessDenied => print("Access is denied.\n", .{}),
else => print("Cannot create file.\n", .{}),
}
return CommandStatus{ .Code = 1 };
};
defer file.close();
file.writeAll(captured_output.getContents()) catch |err| {
switch (err) {
error.AccessDenied => print("Access is denied.\n", .{}),
else => print("Cannot write to file.\n", .{}),
}
return CommandStatus{ .Code = 1 };
};
},
.OutputAppend => {
// Append to file
const file = std.fs.cwd().openFile(file_path, .{ .mode = .write_only }) catch |err| {
switch (err) {
error.FileNotFound => {
// Create new file if it doesn't exist
const new_file = std.fs.cwd().createFile(file_path, .{}) catch |create_err| {
switch (create_err) {
error.AccessDenied => print("Access is denied.\n", .{}),
else => print("Cannot create file.\n", .{}),
}
return CommandStatus{ .Code = 1 };
};
defer new_file.close();
new_file.writeAll(captured_output.getContents()) catch {
print("Cannot write to file.\n", .{});
return CommandStatus{ .Code = 1 };
};
continue;
},
error.AccessDenied => {
print("Access is denied.\n", .{});
return CommandStatus{ .Code = 1 };
},
else => {
print("Cannot open file.\n", .{});
return CommandStatus{ .Code = 1 };
},
}
};
defer file.close();
// Seek to end for append
file.seekFromEnd(0) catch {
print("Cannot seek to end of file.\n", .{});
return CommandStatus{ .Code = 1 };
};
file.writeAll(captured_output.getContents()) catch |err| {
switch (err) {
error.AccessDenied => print("Access is denied.\n", .{}),
else => print("Cannot write to file.\n", .{}),
}
return CommandStatus{ .Code = 1 };
};
},
.InputFrom => {
// Input redirection already handled above
continue;
},
}
}
return status;
},
else => {
const error_msg = "Command type not implemented\n";
if (output_capture) |capture| {
try capture.write(error_msg);
} else {
print("{s}", .{error_msg});
}
return CommandStatus{ .Code = 1 };
},
}
}
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;
}