summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-08-16 10:15:19 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-08-16 10:15:19 +0200
commitc48d374e1070c264325f40645eda7117bd774bed (patch)
treec2209aa777ebbfd2d0b2d9649550f0dea5c5698b /src
parentb740fff1edd8217c54657e4ee33c65b4fe830704 (diff)
DIR: Add /? flag.
Diffstat (limited to 'src')
-rw-r--r--src/cmd.zig8
-rw-r--r--src/cmd/dir.zig78
-rw-r--r--src/cmd/help.zig208
-rw-r--r--src/cmd/lib/flags.zig197
-rw-r--r--src/eval.zig3
-rw-r--r--src/parser.zig63
6 files changed, 526 insertions, 31 deletions
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} <DIR> {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} <DIR> {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 {