Skip to content

Commit

Permalink
feat(HelpMessageWriter): Improve/polish help message
Browse files Browse the repository at this point in the history
Signed-off-by: prajwalch <[email protected]>
  • Loading branch information
prajwalch committed Sep 6, 2024
1 parent 6aa05c9 commit 3c55e16
Showing 1 changed file with 122 additions and 83 deletions.
205 changes: 122 additions & 83 deletions src/HelpMessageWriter.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const HelpMessageWriter = @This();

const std = @import("std");
const Arg = @import("Arg.zig");
const BufferedWriter = std.io.BufferedWriter(4096, std.fs.File.Writer);
const ParsedCommand = @import("./parser/ParseResult.zig").ParsedCommand;

Expand All @@ -19,141 +20,179 @@ pub fn init(command: *const ParsedCommand) HelpMessageWriter {
}

pub fn write(self: *HelpMessageWriter) !void {
const writer = self.buffer.writer();

try self.writeDescription(writer);
try self.writeHeader(writer);
try self.writeCommands(writer);
try self.writeOptions(writer);
try self.writeFooter(writer);
try self.writeDescription();
try self.writeHeader();
try self.writeCommands();
try self.writeOptions();
try self.writeFooter();

try self.buffer.flush();
}

fn writeDescription(self: *HelpMessageWriter, writer: anytype) !void {
fn writeDescription(self: *HelpMessageWriter) !void {
const writer = self.buffer.writer();

if (self.command.deref().description) |d| {
try writer.print("{s}", .{d});
try writeNewLine(writer);
try writeNewLine(writer);
try writer.print("{s}\n\n", .{d});
}
}

fn writeHeader(self: *HelpMessageWriter, writer: anytype) !void {
try writer.print("Usage: {s}", .{self.command.name()});
try writer.writeByte(' ');
fn writeHeader(self: *HelpMessageWriter) !void {
const writer = self.buffer.writer();

try writer.print("Usage: {s}", .{self.command.name()});
const command = self.command.deref();

if (command.countPositionalArgs() >= 1) {
const braces = getBraces(command.hasProperty(.positional_arg_required));

for (command.positional_args.items) |arg| {
try writer.print("{c}{s}", .{ braces[0], arg.name });
try writer.writeByte(' ');
try writer.writeByte(braces[0]);

try writer.print("{s}", .{arg.name});
if (arg.hasProperty(.takes_multiple_values)) {
try writer.writeAll("...");
}

try writer.print("{c} ", .{braces[1]});
try writer.writeByte(braces[1]);
}
}

if (command.countOptions() >= 1) {
try writer.writeAll("[OPTIONS] ");
try writer.writeAll(" [OPTIONS]");
}

if (command.countSubcommands() >= 1) {
const braces = getBraces(command.hasProperty(.subcommand_required));
try writer.print("{c}COMMAND{c}", .{ braces[0], braces[1] });
try writer.print(
" {c}COMMAND{c}",
getBraces(command.hasProperty(.subcommand_required)),
);
}

try writeNewLine(writer);
try writeNewLine(writer);
// End of line.
try writer.writeByte('\n');
}

fn writeCommands(self: *HelpMessageWriter, writer: anytype) !void {
fn writeCommands(self: *HelpMessageWriter) !void {
const writer = self.buffer.writer();
const command = self.command.deref();

if (!(command.countSubcommands() >= 1)) return;
if (command.countSubcommands() == 0) {
return;
}

try writer.writeAll("Commands:");
try writeNewLine(writer);
try writer.writeAll("\nCommands:\n");
const right_padding: usize = if (command.countOptions() == 0) 10 else 30;

for (command.subcommands.items) |subcmd| {
try writer.print(" {s:<20} ", .{subcmd.name});
if (subcmd.description) |d| try writer.print("{s}", .{d});
try writeNewLine(writer);
try writer.print(" {[0]s:<[1]}", .{ subcmd.name, right_padding });

if (subcmd.description) |d| {
try writer.print("\t\t{s}", .{d});
}
try writer.writeByte('\n');
}
try writeNewLine(writer);
}

fn writeOptions(self: *HelpMessageWriter, writer: anytype) !void {
fn writeOptions(self: *HelpMessageWriter) !void {
const writer = self.buffer.writer();
const command = self.command.deref();
if (!(command.countOptions() >= 1)) return;

try writer.writeAll("Options:");
try writeNewLine(writer);

for (command.options.items) |option| {
if (option.short_name) |short_name|
try writer.print(" -{c},", .{short_name});

const long_name = option.long_name orelse option.name;
// When short name is null, add left padding in-order to
// align all long names in the same line
//
// 6 comes by counting (` `) + (`-`) + (`x`) + (`,`)
// where x is some short name
const padding: usize = if (option.short_name == null) 6 else 0;
try writer.print(" {[1]s:>[0]}{[2]s} ", .{ padding, "--", long_name });

if (option.hasProperty(.takes_value)) {
// TODO: Add new `Arg.placeholderName()` to display proper placeholder
if (option.valid_values) |values| {
try writer.writeByte('{');

for (values, 0..) |value, idx| {
try writer.print("{s}", .{value});

// Only print '|' till second last option
if (idx < (values.len - 1)) {
try writer.writeAll("|");
}

if (command.countOptions() == 0) {
return;
}

try writer.writeAll("\nOptions:\n");
for (command.options.items) |*option| {
try self.writeOption(option);
}

const help = Arg.booleanOption("help", 'h', "Print this help and exit");
try self.writeOption(&help);
}

fn writeOption(self: *HelpMessageWriter, option: *const Arg) !void {
const writer = self.buffer.writer();

// Signature refers to the option name (short and long) and its value name
// if available.
//
// For e.x.: -h, --help <description>
// -f, --file <FILE> <description>
// ^---signature---^
//
// Signature pattern makes so much easy to add padding between it and the
// description since we will only have two components to handle.
var signature_buffer = try std.BoundedArray(u8, 100).init(50);
var signature_writer = signature_buffer.writer();

// Option name.
if (option.short_name != null and option.long_name != null) {
try signature_writer.print(
"-{c}, --{s}",
.{ option.short_name.?, option.long_name.? },
);
} else if (option.short_name) |short_name| {
try signature_writer.print("-{c}", .{short_name});
} else if (option.long_name) |long_name| {
try signature_writer.print(" --{s}", .{long_name});
}

// Value name.
if (option.hasProperty(.takes_value)) {
try signature_writer.writeByte('=');

// If the option has set acceptable values, print that.
if (option.valid_values) |valid_values| {
try signature_writer.writeByte('<');

for (valid_values, 0..) |value, idx| {
try signature_writer.print("{s}", .{value});

// Don't print `|` at first and last.
//
// For e.x.: --format=<json|toml|yaml>
if (idx < (valid_values.len - 1)) {
try signature_writer.writeByte('|');
}
try writer.writeByte('}');
} else {
try writer.print("<{s}>", .{option.name});
}
}

if (option.description) |des_txt| {
try writeNewLine(writer);
try writer.print("\t{s}", .{des_txt});
try writeNewLine(writer);
try signature_writer.writeByte('>');
} else {
// Otherwise print the option name.
//
// TODO: Add new `Arg.placeholderName()` to display correct value
// or placeholder name. For e.x.: --time=SECS.
try signature_writer.print("<{s}>", .{option.name});

if (option.hasProperty(.takes_multiple_values)) {
try signature_writer.writeAll("...");
}
}
try writer.writeAll("\n");
}
try writer.writeAll(" -h, --help\n\tPrint help and exit");
try writeNewLine(writer);
}

fn writeFooter(self: *HelpMessageWriter, writer: anytype) !void {
const command = self.command.deref();
// First write the signature into the buffer with some padding.
try writer.print(" {s:<80}", .{signature_buffer.constSlice()});

if (command.countSubcommands() >= 1) {
try writeNewLine(writer);
try writer.print(
"Run '{s} <command> -h' or '{s} <command> --help' to get help for specific command",
.{ self.command.name(), self.command.name() },
);
// Then write the description.
if (option.description) |description| {
try writer.print("\t\t{s}", .{description});
}
try writeNewLine(writer);
}

fn writeNewLine(writer: anytype) !void {
// End of line.
try writer.writeByte('\n');
}

fn writeFooter(self: *HelpMessageWriter) !void {
if (self.command.deref().countSubcommands() >= 1) {
try self.buffer.writer().print(
"\nRun '{s} <command>` with `-h/--h' flag to get help of any command.\n",
.{self.command.name()},
);
}
}

fn getBraces(required: bool) struct { u8, u8 } {
return if (required) .{ '<', '>' } else .{ '[', ']' };
}

0 comments on commit 3c55e16

Please sign in to comment.