aboutsummaryrefslogtreecommitdiff
path: root/src/cli.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli.zig')
-rw-r--r--src/cli.zig543
1 files changed, 313 insertions, 230 deletions
diff --git a/src/cli.zig b/src/cli.zig
index 8c5e37a..5b43e96 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -26,16 +26,150 @@
//! during compile time. There are two versions for a help text: a short
//! and a long version. The short version displays the usage pattern of
//! the command whereas the long version displays a detailed help text.
+//!
+//! The CLI contains global variables for argument parsing. These variables
+//! need to be initialized with the `init` function before the package can be
+//! used.
const std = @import("std");
const assert = std.debug.assert;
-var stdout_buf: [1024]u8 = undefined;
-var stdout_printer: Printer = .init(std.fs.File.stdout().writer(&stdout_buf));
-var stderr_printer: Printer = .init(std.fs.File.stderr().writer(&.{}));
+var stdout_buf: [256]u8 = undefined;
+var stderr_buf: [0]u8 = undefined;
+var stdout_printer: Printer = .init(std.posix.STDOUT_FILENO, &stdout_buf);
+var stderr_printer: Printer = .init(std.posix.STDERR_FILENO, &stderr_buf);
+
+pub const stdout = &stdout_printer;
+pub const stderr = &stderr_printer;
+
+/// The command we are currently in or null if we reached the end or positional arguments.
+var current_cmd: ?[]const u8 = null;
+var arg_iter: std.process.Args.Iterator = undefined;
+var exe_name: []const u8 = "";
+
+/// Initialize the global CLI variables and return the program (i.e. the main
+/// entry command).
+pub fn init(comptime Program: type, args: std.process.Args) Program {
+ const cmd_info = comptime Command_Info.from_command(Program);
+ var program: Program = undefined;
+
+ arg_iter = args.iterate();
+ _ = arg_iter.skip(); // skip executable name
+ current_cmd = parse_opts(cmd_info, &program);
+ exe_name = cmd_info.name;
+ return program;
+}
+
+/// Return the next subcommand of the given command type. The next subcommand
+/// is the name of the next argument. It also parses all options and prefills
+/// them in the returned subcommand.
+pub fn next_subcommand(comptime Command: type) ?Command.Subcommands {
+ const subcommands = comptime Command_Info.from_subcommand(Command);
+ comptime assert(subcommands.len > 0);
+
+ const arg = current_cmd orelse return null;
+ inline for (subcommands) |subcmd| {
+ if (std.mem.eql(u8, arg, subcmd.name)) {
+ var cmd: subcmd.type = undefined;
+ current_cmd = parse_opts(subcmd, &cmd);
+ return @unionInit(Command.Subcommands, subcmd.tag.?, cmd);
+ }
+ }
+ fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, exe_name });
+}
+
+/// Return the next positional argument or `null` if there are no more arguments.
+pub fn next_positional() ?[]const u8 {
+ if (current_cmd) |cmd| {
+ current_cmd = null;
+ return cmd;
+ }
+ return arg_iter.next();
+}
+
+/// Parse and fill all command options and return the current command.
+fn parse_opts(comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 {
+ inline for (cmd_info.options) |opt| {
+ @field(cmd.options, opt.field).init();
+ }
+
+ args_loop: while (arg_iter.next()) |arg| {
+ assert(arg.len > 0);
+ if (arg.len < 2 or arg[0] != '-') {
+ // We have either a single dash or a non-option here. Treat these as
+ // a positional or subcommand (however the executor chooses to use it).
+ return arg;
+ }
-pub var stdout = &stdout_printer;
-pub var stderr = &stderr_printer;
+ var opt_name: []const u8 = undefined;
+ if (arg[1] != '-') {
+ // We check for a short option name.
+ // If we have a boolean flag or an option name longer than a
+ // single character, we have both the name and the value in
+ // a single argument. Otherwise we fetch the next argument.
+ opt_name = arg[1..2];
+ if (opt_name[0] == 'h') {
+ if (arg.len > 2) {
+ fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg});
+ }
+ cmd_info.print_usage(stderr);
+ std.process.exit(0);
+ }
+
+ inline for (cmd_info.options) |opt| {
+ if (opt.short_name) |short_name| {
+ if (short_name == opt_name[0]) {
+ const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: {
+ const next_arg = arg_iter.next();
+ if (next_arg == null or next_arg.?[0] == '-') {
+ fatal("missing value for option '{c}'", .{opt_name[0]});
+ }
+ break :next next_arg.?;
+ };
+ @field(cmd.options, opt.field).parse(opt_name, value);
+ continue :args_loop;
+ }
+ }
+ }
+ } else if (arg.len > 2) {
+ // We check for a long option name.
+ // If we don't have the format `name=value`, we get the
+ // next argument lazily, because for boolean flags we
+ // don't need any extra argument.
+ opt_name = arg[2..];
+ var value: ?[:0]const u8 = null;
+ if (std.mem.findScalar(u8, arg, '=')) |eq| {
+ opt_name = arg[2..eq];
+ value = arg[eq + 1 ..];
+ }
+
+ if (std.mem.eql(u8, opt_name, "help")) {
+ cmd_info.print_help();
+ }
+
+ inline for (cmd_info.options) |opt| {
+ if (std.mem.eql(u8, opt_name, opt.long_name)) {
+ value = value orelse if (opt.arg_name.len == 0) "" else next_arg: {
+ const next = arg_iter.next();
+ if (next == null or next.?[0] == '-') {
+ fatal("missing value for option '{s}'", .{opt_name});
+ }
+ break :next_arg next.?;
+ };
+ @field(cmd.options, opt.field).parse(opt_name, value.?);
+ continue :args_loop;
+ }
+ }
+ } else {
+ // A single "--" was detected which means the rest of the arguments
+ // are interpreted as positional. We are done parsing.
+ return null;
+ }
+
+ fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name});
+ }
+ return null;
+}
pub const Command_Help = struct {
pub const Section = struct {
@@ -215,145 +349,6 @@ pub fn Option(comptime T: type, comptime opt_help: Option_Help(T)) type {
};
}
-pub const Args = struct {
- /// The command we are currently in or null if we reached the end or positional arguments.
- current_cmd: ?[]const u8,
- iter: std.process.ArgIterator,
- exe_name: []const u8,
-
- pub fn init(allocator: std.mem.Allocator, comptime exe_name: []const u8, program: anytype) !Args {
- comptime assert(exe_name.len > 0);
-
- var iter = try std.process.ArgIterator.initWithAllocator(allocator);
- errdefer iter.deinit();
- _ = iter.skip(); // skip executable name
- const cmd_info = Command_Info.from_command(switch (@typeInfo(@TypeOf(program))) {
- .pointer => |info| info.child,
- else => @compileError("expected pointer, got " ++ @typeName(@TypeOf(program))),
- });
- const current_cmd = parse_opts(&iter, cmd_info, program);
- return Args{
- .current_cmd = current_cmd,
- .iter = iter,
- .exe_name = exe_name,
- };
- }
-
- pub fn deinit(self: *Args, allocator: std.mem.Allocator) void {
- _ = allocator;
- self.iter.deinit();
- }
-
- pub fn next_subcommand(self: *Args, comptime Command: type) ?Command.Subcommands {
- const subcommands = comptime Command_Info.from_subcommand(Command);
- comptime assert(subcommands.len > 0);
-
- const arg = self.current_cmd orelse return null;
- inline for (subcommands) |subcmd| {
- if (std.mem.eql(u8, arg, subcmd.name)) {
- var cmd: subcmd.type = undefined;
- self.current_cmd = parse_opts(&self.iter, subcmd, &cmd);
- return @unionInit(Command.Subcommands, subcmd.tag.?, cmd);
- }
- }
- fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, self.exe_name });
- }
-
- pub fn next_positional(self: *Args) ?[]const u8 {
- if (self.current_cmd) |cmd| {
- self.current_cmd = null;
- return cmd;
- }
- self.current_cmd = null;
- return self.iter.next();
- }
-
- /// Parse and fill all command options and return the current command.
- fn parse_opts(iter: *std.process.ArgIterator, comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 {
- inline for (cmd_info.options) |opt| {
- @field(cmd.options, opt.field).init();
- }
-
- args_loop: while (iter.next()) |arg| {
- assert(arg.len > 0);
- if (arg.len < 2 or arg[0] != '-') {
- // We have either a single dash or a non-option here. Treat these as
- // a positional or subcommand (however the executor chooses to use it).
- return arg;
- }
-
- var opt_name: []const u8 = undefined;
- if (arg[1] != '-') {
- // We check for a short option name.
- // If we have a boolean flag or an option name longer than a
- // single character, we have both the name and the value in
- // a single argument. Otherwise we fetch the next argument.
- opt_name = arg[1..2];
- if (opt_name[0] == 'h') {
- if (arg.len > 2) {
- fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg});
- }
- cmd_info.print_usage(stderr);
- std.process.exit(0);
- }
-
- inline for (cmd_info.options) |opt| {
- if (opt.short_name) |short_name| {
- if (short_name == opt_name[0]) {
- const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: {
- const next_arg = iter.next();
- if (next_arg == null or next_arg.?[0] == '-') {
- fatal("missing value for option '{c}'", .{opt_name[0]});
- }
- break :next next_arg.?;
- };
- @field(cmd.options, opt.field).parse(opt_name, value);
- continue :args_loop;
- }
- }
- }
- } else if (arg.len > 2) {
- // We check for a long option name.
- // If we don't have the format `name=value`, we get the
- // next argument lazily, because for boolean flags we
- // don't need any extra argument.
- opt_name = arg[2..];
- var value: ?[:0]const u8 = null;
- if (std.mem.indexOfScalar(u8, arg, '=')) |eq| {
- opt_name = arg[2..eq];
- value = arg[eq + 1 ..];
- }
-
- if (std.mem.eql(u8, opt_name, "help")) {
- cmd_info.print_help();
- }
-
- inline for (cmd_info.options) |opt| {
- if (std.mem.eql(u8, opt_name, opt.long_name)) {
- value = value orelse if (opt.arg_name.len == 0) "" else next_arg: {
- const next = iter.next();
- if (next == null or next.?[0] == '-') {
- fatal("missing value for option '{s}'", .{opt_name});
- }
- break :next_arg next.?;
- };
- @field(cmd.options, opt.field).parse(opt_name, value.?);
- continue :args_loop;
- }
- }
- } else {
- // A single "--" was detected which means the rest of the arguments
- // are interpreted as positional. We are done parsing.
- return null;
- }
-
- fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name});
- }
-
- return null;
- }
-};
-
pub const Print_Options = struct {
/// Indentation of the first line.
margin: u16 = 0,
@@ -369,25 +364,30 @@ const Usage_Column = struct {
};
pub const Printer = struct {
- writer: std.fs.File.Writer,
+ fd: std.posix.fd_t,
max_width: u16,
+ writer: std.Io.Writer,
- fn init(w: std.fs.File.Writer) Printer {
+ fn init(fd: std.posix.fd_t, buf: []u8) Printer {
return Printer{
- .writer = w,
+ .fd = fd,
.max_width = 80,
+ .writer = .{
+ .vtable = &.{ .drain = drain },
+ .buffer = buf,
+ },
};
}
- /// Drain the remaining buffered data and puts it to the sink.
+ /// Drain the remaining buffered data and put it to the sink.
pub fn flush(self: *Printer) void {
- self.writer.interface.flush() catch {};
+ self.writer.flush() catch {};
}
/// Write a value directly without splitting. This uses the formatting capabilities
/// of `std.fmt`.
pub fn write(self: *Printer, comptime fmt: []const u8, value: anytype) void {
- self.writer.interface.print(fmt, value) catch {};
+ self.writer.print(fmt, value) catch {};
}
/// Write a value directly without splitting and append a newline. This uses the
@@ -420,7 +420,7 @@ pub const Printer = struct {
self.put('\n');
}
- /// Print a formatted text and split it into multiple lines if if exceeds
+ /// Print a formatted text and split it into multiple lines if it exceeds
/// a certain width. The splitting behaviour can be configured using the
/// given print options.
pub fn printf(self: *Printer, comptime format: []const u8, args: anytype, opts: Print_Options) void {
@@ -435,7 +435,7 @@ pub const Printer = struct {
buf[buf.len - 3] = '.';
buf[buf.len - 2] = '.';
buf[buf.len - 1] = '.';
- break :dots buf[0..];
+ break :dots &buf;
},
};
self.print(text, opts);
@@ -452,7 +452,7 @@ pub const Printer = struct {
/// given line of text.
pub fn print_lines(self: *Printer, text: []const u8, opts: Print_Options) void {
var rest = text;
- while (std.mem.indexOfScalar(u8, rest, '\n')) |eol| {
+ while (std.mem.findScalar(u8, rest, '\n')) |eol| {
self.print_line(rest[0..eol], opts);
rest = rest[eol + 1 ..];
}
@@ -463,11 +463,11 @@ pub const Printer = struct {
}
fn put(self: *Printer, c: u8) void {
- self.writer.interface.writeByte(c) catch {};
+ self.writer.writeByte(c) catch {};
}
fn put_str(self: *Printer, text: []const u8) void {
- self.writer.interface.writeAll(text) catch {};
+ self.writer.writeAll(text) catch {};
}
fn print_usage_table(self: *Printer, comptime left_column: Usage_Column, comptime right_column: Usage_Column, opts: Print_Options) void {
@@ -521,12 +521,12 @@ pub const Printer = struct {
if (std.mem.allEqual(u8, text[0..last_split], line_split)) {
// There are leading line splits here, so we consider an indentation.
// We want to keep indentation intact and print to the next split.
- eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len;
+ eol = std.mem.findScalarPos(u8, text, width, line_split) orelse text.len;
}
} else {
// We don't have a split char in the line. So we extend the line
- // to the next space. This hopefully will not happen in the wild.
- eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len;
+ // to the next space. This will hopefully not happen in the wild.
+ eol = std.mem.findScalarPos(u8, text, width, line_split) orelse text.len;
}
self.put_str(text[0..eol]);
@@ -536,11 +536,51 @@ pub const Printer = struct {
fn put_indent(self: *Printer, n: usize) void {
const buf = [_]u8{' '} ** 32;
var remaining = n;
- while (remaining > buf.len) : (remaining -= buf.len) {
+ while (remaining > buf.len) {
self.put_str(&buf);
+ remaining -= buf.len;
}
self.put_str(buf[0..remaining]);
}
+
+ pub fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
+ const self: *Printer = @alignCast(@fieldParentPtr("writer", w));
+ var iovec: [8]std.posix.iovec_const = undefined;
+ var iovlen: usize = 0;
+ if (w.end > 0) {
+ iovec[iovlen] = .{ .base = w.buffer.ptr, .len = w.end };
+ iovlen += 1;
+ }
+ var off: usize = 0;
+ while (iovlen < iovec.len and off < data.len - 1) {
+ if (data[off].len > 0) {
+ iovec[iovlen] = .{ .base = data[off].ptr, .len = data[off].len };
+ iovlen += 1;
+ }
+ off += 1;
+ }
+ if (data[data.len - 1].len > 0) {
+ off = 0;
+ const buf = data[data.len - 1];
+ while (iovlen < iovec.len and off < splat) {
+ iovec[iovlen] = .{ .base = buf.ptr, .len = buf.len };
+ iovlen += 1;
+ off += 1;
+ }
+ }
+
+ if (iovlen == 0) return 0;
+
+ const n: usize = while (true) {
+ const res = std.posix.system.writev(self.fd, &iovec, @intCast(iovlen));
+ if (res < 0) {
+ if (std.posix.errno(res) == .INTR) continue;
+ return error.WriteFailed;
+ }
+ break @intCast(res);
+ };
+ return w.consume(n);
+ }
};
pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
@@ -570,21 +610,32 @@ pub fn print_help(comptime Command: type) noreturn {
}
pub fn ask(comptime prompt: []const u8, args: anytype, buf: []u8) ?[]const u8 {
- var stdin = std.fs.File.stdin().reader(buf);
+ assert(buf.len > 0);
+
+ const stdin: std.posix.fd_t = std.posix.STDIN_FILENO;
+ var off: usize = 0;
while (true) {
stderr.printf(prompt, args, .{});
stderr.flush();
- const line = stdin.interface.takeDelimiterExclusive('\n') catch |err| switch (err) {
- error.StreamTooLong => {
- stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{});
- continue;
- },
- else => unreachable,
- };
- const input = std.mem.trim(u8, line, " \t\r");
- return if (input.len == 0) null else input;
+ const res = std.posix.system.read(stdin, buf.ptr + off, buf.len - off);
+ assert(res >= 0);
+ if (res == 0) break;
+
+ const n: usize = @intCast(res);
+ if (std.mem.findScalar(u8, buf[off .. off + n], '\n')) |idx| {
+ off += idx;
+ break;
+ }
+ off += n;
+ if (off >= buf.len) {
+ stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{});
+ off = 0;
+ }
}
+
+ const input = std.mem.trim(u8, buf[0..off], " \t\r");
+ return if (input.len == 0) null else input;
}
pub fn ask_confirmation(comptime prompt: []const u8, args: anytype, comptime default_answer: enum { yes, no }) bool {
@@ -612,7 +663,7 @@ const Pager = struct {
text: []const u8,
dumb: bool,
- pub fn format(self: Bold, w: *std.io.Writer) std.io.Writer.Error!void {
+ pub fn format(self: Bold, w: *std.Io.Writer) std.Io.Writer.Error!void {
if (!self.dumb) try w.writeAll("\x1b[1m");
try w.writeAll(self.text);
if (!self.dumb) try w.writeAll("\x1b[m");
@@ -624,56 +675,85 @@ const Pager = struct {
dumb: bool,
fn init(out_buf: []u8) Pager {
- if (!stdout.writer.file.isTty()) {
- return .{ .pid = null, .printer = stdout_printer, .dumb = true };
- }
+ if (std.posix.system.isatty(stdout_printer.fd) != 1) return .init_dumb();
- const fds = std.posix.pipe() catch return .{ .pid = null, .printer = stdout_printer, .dumb = true };
- const fdin = fds[0];
- const fdout = fds[1];
+ var stdin_pipe: [2]std.posix.fd_t = undefined;
+ if (std.posix.system.pipe(&stdin_pipe) != 0) return .init_dumb();
- const pid = std.posix.fork() catch {
- std.posix.close(fdin);
- std.posix.close(fdout);
- return .{ .pid = null, .printer = stdout_printer, .dumb = true };
- };
+ const pid = std.posix.system.fork();
+ switch (pid) {
+ -1 => {
+ _ = std.posix.system.close(stdin_pipe[0]);
+ _ = std.posix.system.close(stdin_pipe[1]);
+ return .init_dumb();
+ },
+ 0 => {
+ // pager process
+ if (std.posix.system.dup2(stdin_pipe[0], std.posix.STDIN_FILENO) == -1) unreachable;
+ _ = std.posix.system.close(stdin_pipe[0]);
+ _ = std.posix.system.close(stdin_pipe[1]);
- if (pid == 0) {
- // pager process
- std.posix.close(fdout);
- std.posix.dup2(fdin, std.posix.STDIN_FILENO) catch unreachable;
+ var env: [512:null]?[*:0]const u8 = undefined;
+ var envlen: usize = 0;
+ inline for (&[_][*:0]const u8{
+ "LESSCHARSET=utf-8",
+ }) |predefined_env| {
+ env[envlen] = predefined_env;
+ envlen += 1;
+ }
- var env: [512:null]?[*:0]const u8 = undefined;
- var envlen: usize = 0;
- const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511];
- for (environ) |e| {
- env[envlen] = e;
- envlen += 1;
- }
- env[envlen] = null;
+ while (std.posix.system.environ[envlen]) |e| {
+ env[envlen] = e;
+ envlen += 1;
+ if (envlen >= env.len - 1) break;
+ }
+ env[envlen] = null;
- // TODO: Allow environment PAGER to define custom pagers.
- const pager_argv: [*:null]const ?[*:0]const u8 = &.{ "less", "-R", null };
+ // TODO: Allow environment PAGER to define custom pagers.
+ const Pager_Prog = struct {
+ path: [:0]const u8,
+ argv: [*:null]const ?[*:0]const u8,
+ };
- std.posix.execvpeZ(pager_argv[0].?, pager_argv, &env) catch unreachable;
- std.process.exit(1);
- } else {
- // application process
- std.posix.close(fdin);
- var out: std.fs.File = .{ .handle = fdout };
- return .{
- .pid = pid,
- .printer = Printer.init(out.writer(out_buf)),
- .dumb = if (std.posix.getenv("TERM")) |term| std.mem.eql(u8, term, "dumb") else true,
- };
+ for ([_]Pager_Prog{
+ .{ .path = "/usr/bin/less", .argv = &.{ "less", "-R", null } },
+ .{ .path = "/bin/cat", .argv = &.{ "cat", null } },
+ }) |pager| {
+ const res = std.posix.system.execve(pager.path, pager.argv, &env);
+ switch (std.posix.errno(res)) {
+ .NOENT => continue,
+ else => |err| fatal("error executing pager: {}", .{err}),
+ }
+ }
+ fatal("no pager found", .{});
+ },
+ else => {
+ // application process
+ _ = std.posix.system.close(stdin_pipe[0]);
+ var dumb = true;
+ if (std.posix.system.getenv("TERM")) |term| {
+ dumb = term[0] == 'd' and term[1] == 'u' and term[2] == 'm' and term[3] == 'b';
+ }
+
+ return .{
+ .pid = pid,
+ .printer = .init(stdin_pipe[1], out_buf),
+ .dumb = dumb,
+ };
+ },
}
}
+ fn init_dumb() Pager {
+ return .{ .pid = null, .printer = stdout_printer, .dumb = true };
+ }
+
fn deinit(self: *Pager) noreturn {
self.printer.flush();
if (self.pid) |pid| {
- self.printer.writer.file.close();
- _ = std.posix.waitpid(pid, 0);
+ var status: c_int = undefined;
+ _ = std.posix.system.close(self.printer.fd);
+ _ = std.posix.system.waitpid(pid, &status, 0);
}
std.process.exit(0);
}
@@ -808,7 +888,7 @@ const Command_Info = struct {
};
}
- fn from_subcommand(comptime Command: type) []Command_Info {
+ fn from_subcommand(comptime Command: type) []const Command_Info {
if (!@hasDecl(Command, "Subcommands")) {
return &.{};
}
@@ -819,12 +899,15 @@ const Command_Info = struct {
};
assert(union_info.tag_type != null);
- var subcmds: [union_info.fields.len]Command_Info = undefined;
- for (union_info.fields, 0..) |field, i| {
- subcmds[i] = Command_Info.from_command(field.type);
- subcmds[i].tag = field.name;
- }
- return &subcmds;
+ const infos = comptime sub: {
+ var subcmds: [union_info.fields.len]Command_Info = undefined;
+ for (union_info.fields, 0..) |field, i| {
+ subcmds[i] = Command_Info.from_command(field.type);
+ subcmds[i].tag = field.name;
+ }
+ break :sub subcmds;
+ };
+ return &infos;
}
/// Flushes the printer
@@ -863,7 +946,7 @@ const Command_Info = struct {
p.put('\n');
// subcommands
- const subcmds = Command_Info.from_subcommand(cmd.type);
+ const subcmds = comptime Command_Info.from_subcommand(cmd.type);
if (subcmds.len > 0) {
const name_col = comptime Usage_Table.column(Command_Info, subcmds, struct {
fn extract(comptime subcmd: Command_Info) []const u8 {
@@ -886,7 +969,7 @@ const Command_Info = struct {
if (cmd.options.len > 0) {
const title_col = comptime Usage_Table.column(Option_Info, cmd.options, struct {
fn extract(comptime opt: Option_Info) []const u8 {
- const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">";
+ const argname = if (opt.arg_name.len == 0) "" else " ⟨" ++ opt.arg_name ++ "⟩";
return if (opt.short_name) |short|
std.fmt.comptimePrint("-{c}, --{s}{s}", .{ short, opt.long_name, argname })
else
@@ -940,7 +1023,7 @@ const Command_Info = struct {
if (cmd.options.len > 0) {
p.printf_line("{f}", .{pager.bold("OPTIONS")}, .{ .margin = margin0 });
inline for (cmd.options) |opt| {
- const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">";
+ const argname = if (opt.arg_name.len == 0) "" else " ⟨" ++ opt.arg_name ++ "⟩";
if (opt.short_name) |short| {
p.printf_line("-{f}, --{f}{s}", .{ pager.bold(&[1]u8{short}), pager.bold(opt.long_name), argname }, .{ .margin = margin1 });
} else {
@@ -952,7 +1035,7 @@ const Command_Info = struct {
}
// subcommands
- const subcmds = Command_Info.from_subcommand(cmd.type);
+ const subcmds = comptime Command_Info.from_subcommand(cmd.type);
if (subcmds.len > 0) {
p.printf_line("{f}", .{pager.bold("COMMANDS")}, .{ .margin = margin0 });
inline for (subcmds) |subcmd| {
@@ -1065,16 +1148,6 @@ const Option_Info = struct {
};
return &options;
}
-
- fn contains_whitespace(comptime text: []const u8) bool {
- for (text) |c| {
- switch (c) {
- ' ', '\t', '\r', '\n' => return true,
- else => {},
- }
- }
- return false;
- }
};
fn sanitize_name(comptime name: []const u8) []const u8 {
@@ -1138,6 +1211,16 @@ fn contains_eol(comptime text: []const u8) bool {
return false;
}
+fn contains_whitespace(comptime text: []const u8) bool {
+ for (text) |c| {
+ switch (c) {
+ ' ', '\t', '\r', '\n' => return true,
+ else => {},
+ }
+ }
+ return false;
+}
+
fn is_uppercase(comptime text: []const u8) bool {
for (text) |c| {
if ('a' <= c and c <= 'z') return false;
@@ -1151,7 +1234,7 @@ fn join_enum_values(comptime E: type) []const u8 {
0 => unreachable,
1 => "'" ++ fields[0].name ++ "'",
2 => "'" ++ fields[0].name ++ "' or '" ++ fields[1].name ++ "'",
- 3 => str: {
+ else => str: {
var text: []const u8 = "";
for (fields[0 .. fields.len - 1]) |field| {
text = text ++ "'" ++ field.name ++ "', ";