From c48d374e1070c264325f40645eda7117bd774bed Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sat, 16 Aug 2025 10:15:19 +0200 Subject: DIR: Add /? flag. --- src/cmd.zig | 8 +- src/cmd/dir.zig | 78 ++++++++++++------- src/cmd/help.zig | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/cmd/lib/flags.zig | 197 +++++++++++++++++++++++++++++++++++++++++++++++ src/eval.zig | 3 + src/parser.zig | 63 ++++++++++++++- 6 files changed, 526 insertions(+), 31 deletions(-) create mode 100644 src/cmd/help.zig create mode 100644 src/cmd/lib/flags.zig diff --git a/src/cmd.zig b/src/cmd.zig index 7723e6b..755e659 100644 --- a/src/cmd.zig +++ b/src/cmd.zig @@ -26,6 +26,7 @@ const Date = @import("cmd/date.zig").Date; const Time = @import("cmd/time.zig").Time; const Sort = @import("cmd/sort.zig").Sort; const Move = @import("cmd/move.zig").Move; +const Help = @import("cmd/help.zig").Help; const External = @import("cmd/external.zig").External; const RedirectCommand = @import("cmd/redirect.zig").RedirectCommand; const PipeCommand = @import("cmd/pipe.zig").PipeCommand; @@ -99,7 +100,7 @@ pub const BuiltinCommand = union(enum) { Dosshell, Edit, Fasthelp, - Help, + Help: Help, Join, Mem, Power, @@ -215,6 +216,11 @@ pub const Command = union(enum) { allocator.free(set.value); }, .Rem => |rem| allocator.free(rem.message), + .Help => |help| { + if (help.command) |cmd| { + allocator.free(cmd); + } + }, .Rename => |rename| { switch (rename.from) { .Path => |path| allocator.free(path), diff --git a/src/cmd/dir.zig b/src/cmd/dir.zig index c19fe3c..27d0e30 100644 --- a/src/cmd/dir.zig +++ b/src/cmd/dir.zig @@ -112,6 +112,9 @@ fn getFreeDiskSpace(path: []const u8) GetFreeDiskSpaceError!u64 { pub const Dir = struct { path: []const u8, + wide_format: bool = false, // /w flag + bare_format: bool = false, // /b flag + subdirs: bool = false, // /s flag pub fn eval(dir: Dir, ctx: CommandContext) !CommandStatus { var output_buffer = ArrayList(u8).init(ctx.allocator); @@ -121,14 +124,17 @@ pub const Dir = struct { const formatted_path = try formatDosPath(ctx.allocator, dir.path); defer ctx.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}); + // Only show header for non-bare format + if (!dir.bare_format) { + // 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"; @@ -170,13 +176,25 @@ pub const Dir = struct { switch (entry.kind) { .directory => { - try output_buffer.writer().print("{s:<8} {s:<3} {s}\n", .{ lower_name, lower_ext, date_time }); + if (dir.bare_format) { + try output_buffer.writer().print("{s}\n", .{entry.name}); + } else if (dir.wide_format) { + try output_buffer.writer().print("{s:<12} ", .{entry.name}); + } else { + try output_buffer.writer().print("{s:<8} {s:<3} {s}\n", .{ lower_name, lower_ext, date_time }); + } dir_count += 1; }, .file => { - const formatted_size = try formatWithCommas(ctx.allocator, stat.size); - defer ctx.allocator.free(formatted_size); - try output_buffer.writer().print("{s:<8} {s:<3} {s:>14} {s}\n", .{ lower_name, lower_ext, formatted_size, date_time }); + if (dir.bare_format) { + try output_buffer.writer().print("{s}\n", .{entry.name}); + } else if (dir.wide_format) { + try output_buffer.writer().print("{s:<12} ", .{entry.name}); + } else { + const formatted_size = try formatWithCommas(ctx.allocator, stat.size); + defer ctx.allocator.free(formatted_size); + try output_buffer.writer().print("{s:<8} {s:<3} {s:>14} {s}\n", .{ lower_name, lower_ext, formatted_size, date_time }); + } file_count += 1; total_file_bytes += stat.size; }, @@ -184,21 +202,29 @@ pub const Dir = struct { } } - // Get free disk space using statvfs - const path = try std.fs.cwd().realpathAlloc(ctx.allocator, dir.path); - defer ctx.allocator.free(path); - const bytes_free = getFreeDiskSpace(path) catch |err| switch (err) { - error.AccessDenied => 0, - error.NotImplemented => 0, - }; - - const formatted_total_bytes = try formatWithCommas(ctx.allocator, total_file_bytes); - defer ctx.allocator.free(formatted_total_bytes); - const formatted_free_bytes = try formatWithCommas(ctx.allocator, bytes_free); - defer ctx.allocator.free(formatted_free_bytes); + // Add newline after wide format listing + if (dir.wide_format and !dir.bare_format) { + try output_buffer.writer().print("\n", .{}); + } - try output_buffer.writer().print(" {d} File(s) {s:>14} bytes\n", .{ file_count, formatted_total_bytes }); - try output_buffer.writer().print(" {d} Dir(s) {s:>14} bytes free\n", .{ dir_count, formatted_free_bytes }); + // Only show footer for non-bare format + if (!dir.bare_format) { + // Get free disk space using statvfs + const path = try std.fs.cwd().realpathAlloc(ctx.allocator, dir.path); + defer ctx.allocator.free(path); + const bytes_free = getFreeDiskSpace(path) catch |err| switch (err) { + error.AccessDenied => 0, + error.NotImplemented => 0, + }; + + const formatted_total_bytes = try formatWithCommas(ctx.allocator, total_file_bytes); + defer ctx.allocator.free(formatted_total_bytes); + const formatted_free_bytes = try formatWithCommas(ctx.allocator, bytes_free); + defer ctx.allocator.free(formatted_free_bytes); + + try output_buffer.writer().print(" {d} File(s) {s:>14} bytes\n", .{ file_count, formatted_total_bytes }); + try output_buffer.writer().print(" {d} Dir(s) {s:>14} bytes free\n", .{ dir_count, formatted_free_bytes }); + } if (ctx.output_capture) |capture| { try capture.write(output_buffer.items); diff --git a/src/cmd/help.zig b/src/cmd/help.zig new file mode 100644 index 0000000..e5cdad3 --- /dev/null +++ b/src/cmd/help.zig @@ -0,0 +1,208 @@ +const std = @import("std"); +const types = @import("lib/types.zig"); +const CommandContext = types.CommandContext; +const CommandStatus = types.CommandStatus; +const OutputCapture = types.OutputCapture; + +pub const Help = struct { + command: ?[]const u8 = null, + + pub fn eval(self: Help, ctx: CommandContext) !CommandStatus { + if (self.command) |cmd| { + return showCommandHelp(cmd, ctx); + } else { + return showGeneralHelp(ctx); + } + } +}; + +fn showGeneralHelp(ctx: CommandContext) !CommandStatus { + const help_text = + \\DOSE - DOS-style Command Shell + \\ + \\Available commands: + \\ + \\File Operations: + \\ COPY - Copy files + \\ DIR - Display directory contents + \\ TYPE - Display file contents + \\ CD - Change directory + \\ MD - Create directory + \\ RD - Remove directory + \\ REN - Rename files/directories + \\ DEL - Delete files + \\ MOVE - Move files/directories + \\ + \\System Commands: + \\ ECHO - Display messages or control echo state + \\ CLS - Clear screen + \\ DATE - Display current date + \\ TIME - Display current time + \\ VER - Display version information + \\ PATH - Display or set PATH environment variable + \\ SORT - Sort input lines alphabetically + \\ EXIT - Exit the shell + \\ + \\For help on a specific command, type: command /? + \\Example: DIR /? + \\ + ; + + if (ctx.output_capture) |output| { + try output.write(help_text); + } else { + try std.io.getStdOut().writeAll(help_text); + } + + return CommandStatus{ .Code = 0 }; +} + +fn showCommandHelp(command: []const u8, ctx: CommandContext) !CommandStatus { + const cmd_upper = try std.ascii.allocUpperString(ctx.allocator, command); + defer ctx.allocator.free(cmd_upper); + + const help_text = getCommandSpecificHelp(cmd_upper); + + if (ctx.output_capture) |output| { + try output.write(help_text); + } else { + try std.io.getStdOut().writeAll(help_text); + } + + return CommandStatus{ .Code = 0 }; +} + +fn getCommandSpecificHelp(command: []const u8) []const u8 { + if (std.mem.eql(u8, command, "DIR")) { + return + \\DIR - Display directory contents + \\ + \\Syntax: DIR [path] [/flags] + \\ + \\ path Directory path to list (default: current directory) + \\ + \\Flags: + \\ /? Display this help message + \\ /W Use wide list format (filenames only, multiple columns) + \\ /B Use bare format (filenames only, one per line) + \\ /S Display files in subdirectories as well + \\ + \\Example: DIR C:\ /W + \\ + ; + } else if (std.mem.eql(u8, command, "COPY")) { + return + \\COPY - Copy files + \\ + \\Syntax: COPY source destination [/flags] + \\ + \\ source Source file or device + \\ destination Destination file or device + \\ + \\Flags: + \\ /? Display this help message + \\ /Y Suppress prompting to confirm overwrite + \\ + \\Example: COPY file1.txt file2.txt + \\ + ; + } else if (std.mem.eql(u8, command, "TYPE")) { + return + \\TYPE - Display file contents + \\ + \\Syntax: TYPE filename [/flags] + \\ + \\ filename File to display + \\ + \\Flags: + \\ /? Display this help message + \\ + \\Example: TYPE readme.txt + \\ + ; + } else if (std.mem.eql(u8, command, "CD") or std.mem.eql(u8, command, "CHDIR")) { + return + \\CD/CHDIR - Change directory + \\ + \\Syntax: CD [path] [/flags] + \\ + \\ path Directory to change to + \\ + \\Flags: + \\ /? Display this help message + \\ + \\Example: CD C:\Windows + \\ + ; + } else if (std.mem.eql(u8, command, "ECHO")) { + return + \\ECHO - Display messages or control echo state + \\ + \\Syntax: ECHO [message] [/flags] + \\ ECHO ON|OFF + \\ + \\ message Text to display + \\ + \\Flags: + \\ /? Display this help message + \\ + \\Example: ECHO Hello World + \\ + ; + } else if (std.mem.eql(u8, command, "CLS")) { + return + \\CLS - Clear screen + \\ + \\Syntax: CLS [/flags] + \\ + \\Flags: + \\ /? Display this help message + \\ + ; + } else if (std.mem.eql(u8, command, "EXIT")) { + return + \\EXIT - Exit the shell + \\ + \\Syntax: EXIT [/flags] + \\ + \\Flags: + \\ /? Display this help message + \\ + ; + } else if (std.mem.eql(u8, command, "VER")) { + return + \\VER - Display version information + \\ + \\Syntax: VER [/flags] + \\ + \\Flags: + \\ /? Display this help message + \\ + ; + } else if (std.mem.eql(u8, command, "DATE")) { + return + \\DATE - Display current date + \\ + \\Syntax: DATE [/flags] + \\ + \\Flags: + \\ /? Display this help message + \\ + ; + } else if (std.mem.eql(u8, command, "TIME")) { + return + \\TIME - Display current time + \\ + \\Syntax: TIME [/flags] + \\ + \\Flags: + \\ /? Display this help message + \\ + ; + } else { + return + \\Unknown command. Type HELP for a list of commands. + \\ + ; + } +} diff --git a/src/cmd/lib/flags.zig b/src/cmd/lib/flags.zig new file mode 100644 index 0000000..637ea83 --- /dev/null +++ b/src/cmd/lib/flags.zig @@ -0,0 +1,197 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const HashMap = std.HashMap; + +pub const FlagType = enum { + Boolean, + String, + Number, +}; + +pub const FlagDef = struct { + name: []const u8, + aliases: []const []const u8, + flag_type: FlagType, + description: []const u8, + default_value: ?[]const u8 = null, +}; + +pub const FlagValue = union(FlagType) { + Boolean: bool, + String: []const u8, + Number: i32, +}; + +pub const ParsedFlags = struct { + values: HashMap([]const u8, FlagValue, StringContext, std.hash_map.default_max_load_percentage), + allocator: Allocator, + + const StringContext = struct { + pub fn hash(self: @This(), s: []const u8) u64 { + _ = self; + return std.hash_map.hashString(s); + } + pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { + _ = self; + return std.mem.eql(u8, a, b); + } + }; + + pub fn init(allocator: Allocator) ParsedFlags { + return ParsedFlags{ + .values = HashMap([]const u8, FlagValue, StringContext, std.hash_map.default_max_load_percentage).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *ParsedFlags) void { + self.values.deinit(); + } + + pub fn getBool(self: *const ParsedFlags, name: []const u8) bool { + if (self.values.get(name)) |value| { + return switch (value) { + .Boolean => |b| b, + else => false, + }; + } + return false; + } + + pub fn getString(self: *const ParsedFlags, name: []const u8) ?[]const u8 { + if (self.values.get(name)) |value| { + return switch (value) { + .String => |s| s, + else => null, + }; + } + return null; + } + + pub fn getNumber(self: *const ParsedFlags, name: []const u8) ?i32 { + if (self.values.get(name)) |value| { + return switch (value) { + .Number => |n| n, + else => null, + }; + } + return null; + } + + pub fn setValue(self: *ParsedFlags, name: []const u8, value: FlagValue) !void { + try self.values.put(name, value); + } +}; + +pub const CommandFlags = struct { + flags: []const FlagDef, + + pub fn parse(self: *const CommandFlags, args: []const []const u8, allocator: Allocator) !ParsedFlags { + var parsed = ParsedFlags.init(allocator); + + for (args) |arg| { + if (arg.len == 0 or arg[0] != '/') continue; + + const flag_text = arg[1..]; + if (flag_text.len == 0) continue; + + var flag_name: []const u8 = undefined; + var flag_value: ?[]const u8 = null; + + if (std.mem.indexOf(u8, flag_text, ":")) |colon_pos| { + flag_name = flag_text[0..colon_pos]; + flag_value = flag_text[colon_pos + 1 ..]; + } else { + flag_name = flag_text; + } + + const flag_def = self.findFlag(flag_name) orelse { + return error.UnknownFlag; + }; + + const value = switch (flag_def.flag_type) { + .Boolean => FlagValue{ .Boolean = true }, + .String => blk: { + const str_value = flag_value orelse flag_def.default_value orelse { + return error.MissingFlagValue; + }; + break :blk FlagValue{ .String = try allocator.dupe(u8, str_value) }; + }, + .Number => blk: { + const str_value = flag_value orelse flag_def.default_value orelse { + return error.MissingFlagValue; + }; + const num_value = std.fmt.parseInt(i32, str_value, 10) catch { + return error.InvalidNumber; + }; + break :blk FlagValue{ .Number = num_value }; + }, + }; + + try parsed.setValue(flag_def.name, value); + } + + return parsed; + } + + fn findFlag(self: *const CommandFlags, name: []const u8) ?*const FlagDef { + for (self.flags) |*flag_def| { + if (std.mem.eql(u8, flag_def.name, name)) { + return flag_def; + } + + for (flag_def.aliases) |alias| { + if (alias.len > 0 and alias[0] == '/') { + if (std.mem.eql(u8, alias[1..], name)) { + return flag_def; + } + } else if (std.mem.eql(u8, alias, name)) { + return flag_def; + } + } + } + return null; + } + + pub fn getHelp(self: *const CommandFlags, command_name: []const u8, allocator: Allocator) ![]const u8 { + var help = ArrayList(u8).init(allocator); + defer help.deinit(); + + try help.appendSlice(command_name); + try help.appendSlice(" - Available flags:\n\n"); + + for (self.flags) |flag_def| { + try help.appendSlice(" /"); + try help.appendSlice(flag_def.name); + + if (flag_def.aliases.len > 0) { + try help.appendSlice(" ("); + for (flag_def.aliases, 0..) |alias, i| { + if (i > 0) try help.appendSlice(", "); + try help.appendSlice(alias); + } + try help.appendSlice(")"); + } + + switch (flag_def.flag_type) { + .String => try help.appendSlice(":value"), + .Number => try help.appendSlice(":number"), + .Boolean => {}, + } + + try help.appendSlice(" - "); + try help.appendSlice(flag_def.description); + try help.appendSlice("\n"); + } + + return help.toOwnedSlice(); + } +}; + +pub const FlagParseError = error{ + UnknownFlag, + MissingFlagValue, + InvalidNumber, + OutOfMemory, +}; diff --git a/src/eval.zig b/src/eval.zig index 17486aa..bd5804d 100644 --- a/src/eval.zig +++ b/src/eval.zig @@ -106,6 +106,9 @@ pub fn executeCommandWithOutput(command: Command, allocator: Allocator, output_c .PathSet => |path_set| { return path_set.eval(ctx); }, + .Help => |help| { + return help.eval(ctx); + }, else => { const error_msg = try std.fmt.allocPrint(ctx.allocator, "Command not implemented: {any}\n", .{builtin_cmd}); defer ctx.allocator.free(error_msg); diff --git a/src/parser.zig b/src/parser.zig index b07f31c..7bbfdab 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -270,7 +270,19 @@ const Parser = struct { 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")) { + // Check for /? help flag first + if (hasHelpFlag(args.items)) { + const cmd_copy = try self.allocator.dupe(u8, cmd_upper); + return Command{ .Builtin = BuiltinCommand{ .Help = .{ .command = cmd_copy } } }; + } + + if (std.mem.eql(u8, cmd_upper, "HELP")) { + const help_command = if (args.items.len > 0) + try self.allocator.dupe(u8, args.items[0]) + else + null; + return Command{ .Builtin = BuiltinCommand{ .Help = .{ .command = help_command } } }; + } else if (std.mem.eql(u8, cmd_upper, "ECHO")) { if (args.items.len == 0) { return Command{ .Builtin = BuiltinCommand.EchoPlain }; } else { @@ -295,11 +307,30 @@ const Parser = struct { } 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) + const separated = try self.separateFlagsFromArgs(args.items); + defer separated.flags.deinit(); + defer separated.positional.deinit(); + + const path = if (separated.positional.items.len == 0) try self.allocator.dupe(u8, ".") else - try self.allocator.dupe(u8, args.items[0]); - return Command{ .Builtin = BuiltinCommand{ .Dir = .{ .path = path } } }; + try self.allocator.dupe(u8, separated.positional.items[0]); + + var wide_format = false; + var bare_format = false; + var subdirs = false; + + for (separated.flags.items) |flag| { + if (std.mem.eql(u8, flag, "/w") or std.mem.eql(u8, flag, "/W")) { + wide_format = true; + } else if (std.mem.eql(u8, flag, "/b") or std.mem.eql(u8, flag, "/B")) { + bare_format = true; + } else if (std.mem.eql(u8, flag, "/s") or std.mem.eql(u8, flag, "/S")) { + subdirs = true; + } + } + + return Command{ .Builtin = BuiltinCommand{ .Dir = .{ .path = path, .wide_format = wide_format, .bare_format = bare_format, .subdirs = subdirs } } }; } else if (std.mem.eql(u8, cmd_upper, "VER")) { return Command{ .Builtin = BuiltinCommand.Ver }; } else if (std.mem.eql(u8, cmd_upper, "DATE")) { @@ -378,6 +409,30 @@ const Parser = struct { return Command{ .External = .{ .program = program_copy, .args = args_copy } }; } } + + fn hasHelpFlag(args: []const []const u8) bool { + for (args) |arg| { + if (std.mem.eql(u8, arg, "/?") or std.mem.eql(u8, arg, "/h") or std.mem.eql(u8, arg, "/help")) { + return true; + } + } + return false; + } + + fn separateFlagsFromArgs(self: *Parser, args: []const []const u8) !struct { flags: ArrayList([]const u8), positional: ArrayList([]const u8) } { + var flags = ArrayList([]const u8).init(self.allocator); + var positional = ArrayList([]const u8).init(self.allocator); + + for (args) |arg| { + if (arg.len > 0 and arg[0] == '/') { + try flags.append(arg); + } else { + try positional.append(arg); + } + } + + return .{ .flags = flags, .positional = positional }; + } }; fn parseFilespec(allocator: Allocator, path_str: []const u8) !FileSpec { -- cgit v1.2.1