From 6aa675822c110059f6589cbc21d4f30bd19ed69c Mon Sep 17 00:00:00 2001 From: JacobCrabill Date: Sat, 2 Aug 2025 20:12:23 -0700 Subject: [PATCH 1/7] Enable optional commands The 'command' field of a Flags struct can now be a '?union' in addition to a plain 'union'. --- build.zig | 1 + examples/optional_command.zig | 63 +++++++++++++++++++++++++++++++++++ examples/overview.zig | 1 + src/Help.zig | 5 +-- src/Parser.zig | 4 +-- src/meta.zig | 25 ++++++++++---- 6 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 examples/optional_command.zig diff --git a/build.zig b/build.zig index 2d77d5d..0809f1b 100644 --- a/build.zig +++ b/build.zig @@ -29,6 +29,7 @@ pub fn build(b: *std.Build) void { overview, colors, trailing, + optional_command, }, "example", "Example to run for example step (default = overview)", diff --git a/examples/optional_command.zig b/examples/optional_command.zig new file mode 100644 index 0000000..43290f4 --- /dev/null +++ b/examples/optional_command.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const flags = @import("flags"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}).init; + defer _ = gpa.deinit(); + + const args = try std.process.argsAlloc(gpa.allocator()); + defer std.process.argsFree(gpa.allocator(), args); + + const options = flags.parse(args, "overview", Flags, .{}); + + try std.json.stringify( + options, + .{ .whitespace = .indent_2 }, + std.io.getStdOut().writer(), + ); +} + +const Flags = struct { + // Optional description of the program. + pub const description = + \\This is a dummy command for testing purposes. + \\There are a bunch of options for demonstration purposes. + ; + + // Optional description of some or all of the flags (must match field names in the struct). + pub const descriptions = .{ + .force = "Use the force", + }; + + force: bool, // Set to `true` only if '--force' is passed. + + // Subcommands can be defined through the `command` field, which should be a union with struct + // fields which are defined the same way this struct is. Subcommands may be nested. + // Subcommands (this union) can be made optional. + command: ?union(enum) { + frobnicate: struct { + pub const descriptions = .{ + .level = "Frobnication level", + }; + + level: u8, + + positional: struct { + trailing: []const []const u8, + }, + }, + defrabulise: struct { + supercharge: bool, + }, + + pub const descriptions = .{ + .frobnicate = "Frobnicate everywhere", + .defrabulise = "Defrabulise everyone", + }; + }, + + // Optional declaration to define shorthands. These can be chained e.g '-fs large'. + pub const switches = .{ + .force = 'f', + }; +}; diff --git a/examples/overview.zig b/examples/overview.zig index 16be5b6..744ce34 100644 --- a/examples/overview.zig +++ b/examples/overview.zig @@ -76,6 +76,7 @@ const Flags = struct { // Subcommands can be defined through the `command` field, which should be a union with struct // fields which are defined the same way this struct is. Subcommands may be nested. + // Subcommands (this union) can be made optional. command: union(enum) { frobnicate: struct { pub const descriptions = .{ diff --git a/src/Help.zig b/src/Help.zig index 75011db..31cdcc8 100644 --- a/src/Help.zig +++ b/src/Help.zig @@ -203,8 +203,9 @@ pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Help { help.sections = help.sections ++ .{arguments}; } if (info.subcommands.len > 0) { - const cmd_descriptions = meta.getDescriptions(std.meta.FieldType(Flags, .command)); - var commands = Section{ .header = "Commands:" }; + const T = meta.unwrapOptional(@FieldType(Flags, "command")); + const cmd_descriptions = meta.getDescriptions(T); + var commands = Section{ .header = if (info.optional_commands) "Commands: [Optional]" else "Commands:" }; for (info.subcommands) |cmd| commands.add(.{ .name = cmd.command_name, .desc = @field(cmd_descriptions, cmd.field_name), diff --git a/src/Parser.zig b/src/Parser.zig index 453ac65..2bc4119 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -95,7 +95,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl inline for (info.subcommands) |cmd| { if (std.mem.eql(u8, arg, cmd.command_name)) { const cmd_flags = parser.parse(cmd.type, command_name ++ " " ++ cmd.command_name); - flags.command = @unionInit(@TypeOf(flags.command), cmd.field_name, cmd_flags); + flags.command = @unionInit(meta.unwrapOptional(@TypeOf(flags.command)), cmd.field_name, cmd_flags); passed.command = true; continue :next_arg; } @@ -130,7 +130,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl } } - if (info.subcommands.len > 0 and !passed.command) { + if (!info.optional_commands and info.subcommands.len > 0 and !passed.command) { parser.fatal("missing subcommand", .{}); } diff --git a/src/meta.zig b/src/meta.zig index 7f9eda7..6e3509f 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -4,6 +4,7 @@ pub const FlagsInfo = struct { flags: []const Flag = &.{}, positionals: []const Positional = &.{}, subcommands: []const SubCommand = &.{}, + optional_commands: bool = false, }; const SubCommand = struct { @@ -79,12 +80,24 @@ pub fn info(comptime Flags: type) FlagsInfo { }}; } } else if (std.mem.eql(u8, field.name, "command")) { - if (@typeInfo(field.type) != .@"union") compileError( - "command field type is not a union: {s}", - .{@typeName(field.type)}, - ); - - for (@typeInfo(field.type).@"union".fields) |cmd| { + const cmd_type = @typeInfo(field.type); + switch (cmd_type) { + .@"union" => {}, + .optional => |o| { + const opt_cmd_type = @typeInfo(o.child); + if (opt_cmd_type != .@"union") compileError( + "command field type is not a union: {s}", + .{@typeName(field.type)}, + ); + command.optional_commands = true; + }, + else => compileError( + "command field type is not a union: {s}", + .{@typeName(field.type)}, + ), + } + const u = @typeInfo(unwrapOptional(field.type)).@"union"; + for (u.fields) |cmd| { command.subcommands = command.subcommands ++ .{SubCommand{ .type = cmd.type, .field_name = cmd.name, From fb4e65266b9c3a48fc5eb4e71de616e919783ca0 Mon Sep 17 00:00:00 2001 From: JacobCrabill Date: Sat, 2 Aug 2025 20:29:26 -0700 Subject: [PATCH 2/7] Add 'printHelp()' function --- src/flags.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/flags.zig b/src/flags.zig index eeb74c9..333ad6e 100644 --- a/src/flags.zig +++ b/src/flags.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub const ColorScheme = @import("ColorScheme.zig"); const Parser = @import("Parser.zig"); const Help = @import("Help.zig"); +const meta = @import("meta.zig"); pub const Options = struct { skip_first_arg: bool = true, @@ -26,3 +27,12 @@ pub fn parse( return parser.parse(Flags, exe_name); } + +pub fn printHelp( + comptime exe_name: []const u8, + Flags: type, + options: Options, +) void { + const help = comptime Help.generate(Flags, meta.info(Flags), exe_name); + help.render(std.io.getStdOut(), options.colors); +} From c0e9215d6405fe4fa534f2ccd1b94d5e07c097da Mon Sep 17 00:00:00 2001 From: JacobCrabill Date: Tue, 5 Aug 2025 20:32:59 -0700 Subject: [PATCH 3/7] Parser: Bug fix optional command --- src/Parser.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Parser.zig b/src/Parser.zig index 2bc4119..33091ad 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -130,8 +130,12 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl } } - if (!info.optional_commands and info.subcommands.len > 0 and !passed.command) { - parser.fatal("missing subcommand", .{}); + if (info.subcommands.len > 0 and !passed.command) { + if (info.optional_commands) { + flags.command = null; + } else { + parser.fatal("missing subcommand", .{}); + } } return flags; From 2839f30a02485f863f5780170eef290c8ccf8d53 Mon Sep 17 00:00:00 2001 From: JacobCrabill Date: Tue, 19 Aug 2025 07:48:25 -0700 Subject: [PATCH 4/7] parser: Print help on error Since 'parse' no longer returns an error, and instead exits directly, be helpful by printing the active Help menu after the error message, for context on where the error came from and what options exist. --- src/Parser.zig | 11 ++++++++--- src/flags.zig | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Parser.zig b/src/Parser.zig index 33091ad..e806620 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -13,17 +13,22 @@ pub const Terminal = @import("Terminal.zig"); args: []const [:0]const u8, current_arg: usize, colors: *const ColorScheme, +/// The current Help of the command being parsed +help: Help, fn fatal(parser: *const Parser, comptime fmt: []const u8, args: anytype) noreturn { const stderr = Terminal.init(std.io.getStdErr()); stderr.print(parser.colors.error_label, "Error: ", .{}); - stderr.print(parser.colors.error_message, fmt ++ "\n", args); + stderr.print(parser.colors.error_message, fmt ++ "\n\n", args); + parser.help.render(std.io.getStdErr(), parser.colors); std.process.exit(1); } +/// Parse the Flags struct and return the parsed result. +/// If an error is encounterd, the error is displayed, followed by the help menu. pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Flags { const info = comptime meta.info(Flags); - const help = comptime Help.generate(Flags, info, command_name); + parser.help = comptime Help.generate(Flags, info, command_name); var flags: Flags = undefined; var passed: std.enums.EnumFieldStruct(std.meta.FieldEnum(Flags), bool, false) = .{}; @@ -41,7 +46,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl } if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - help.render(std.io.getStdOut(), parser.colors); + parser.help.render(std.io.getStdOut(), parser.colors); std.process.exit(0); } diff --git a/src/flags.zig b/src/flags.zig index 333ad6e..9a4f5ef 100644 --- a/src/flags.zig +++ b/src/flags.zig @@ -23,6 +23,7 @@ pub fn parse( .args = args, .current_arg = if (options.skip_first_arg) 1 else 0, .colors = options.colors, + .help = comptime Help.generate(Flags, meta.info(Flags), exe_name), }; return parser.parse(Flags, exe_name); From 1a5e7569bda623129da212d9fb1215994245fce4 Mon Sep 17 00:00:00 2001 From: JacobCrabill Date: Wed, 20 Aug 2025 20:10:02 -0700 Subject: [PATCH 5/7] Update for Zig 0.15 --- build.zig | 11 ++++++----- build.zig.zon | 4 ++-- examples/optional_command.zig | 9 +++++++-- examples/overview.zig | 11 ++++++++--- examples/trailing.zig | 9 +++++++-- src/Help.zig | 16 +++++++++------- src/Parser.zig | 11 ++++++----- src/Terminal.zig | 25 +++++++++++++++++-------- src/flags.zig | 2 +- 9 files changed, 63 insertions(+), 35 deletions(-) diff --git a/build.zig b/build.zig index 0809f1b..b7dc76e 100644 --- a/build.zig +++ b/build.zig @@ -15,8 +15,7 @@ pub fn build(b: *std.Build) void { const tests_step = b.step("test", "Run tests"); const tests = b.addTest(.{ - .root_source_file = root, - .target = target, + .root_module = mod, }); const tests_run = b.addRunArtifact(tests); @@ -37,9 +36,11 @@ pub fn build(b: *std.Build) void { const example = b.addExecutable(.{ .name = "example", - .root_source_file = b.path(b.fmt("examples/{s}.zig", .{@tagName(example_option)})), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path(b.fmt("examples/{s}.zig", .{@tagName(example_option)})), + .target = target, + .optimize = optimize, + }), }); example.root_module.addImport("flags", mod); const run_example = b.addRunArtifact(example); diff --git a/build.zig.zon b/build.zig.zon index 4580318..2956b79 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,8 +1,8 @@ .{ .name = .flags, - .version = "0.10.0", + .version = "0.11.0", .fingerprint = 0xb0541bade61ff6b, - .minimum_zig_version = "0.14.0-dev.2802+257054a14", + .minimum_zig_version = "0.15.1", // Want to limit these to only the things that really constitute "the library". // The hash needn't be updated by some small change to the README. .paths = .{ diff --git a/examples/optional_command.zig b/examples/optional_command.zig index 43290f4..b6be20e 100644 --- a/examples/optional_command.zig +++ b/examples/optional_command.zig @@ -10,10 +10,15 @@ pub fn main() !void { const options = flags.parse(args, "overview", Flags, .{}); - try std.json.stringify( + var stdout_buf: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + try std.json.Stringify.value( options, .{ .whitespace = .indent_2 }, - std.io.getStdOut().writer(), + stdout, ); } diff --git a/examples/overview.zig b/examples/overview.zig index 744ce34..0b3987a 100644 --- a/examples/overview.zig +++ b/examples/overview.zig @@ -8,12 +8,17 @@ pub fn main() !void { const args = try std.process.argsAlloc(gpa.allocator()); defer std.process.argsFree(gpa.allocator(), args); - const options = flags.parse(args, "overview", Flags, .{}); + const options: Flags = flags.parse(args, "overview", Flags, .{}); - try std.json.stringify( + var stdout_buf: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + try std.json.Stringify.value( options, .{ .whitespace = .indent_2 }, - std.io.getStdOut().writer(), + stdout, ); } diff --git a/examples/trailing.zig b/examples/trailing.zig index c76b0c3..29795f9 100644 --- a/examples/trailing.zig +++ b/examples/trailing.zig @@ -10,10 +10,15 @@ pub fn main() !void { const options = flags.parse(args, "trailing", Flags, .{}); - try std.json.stringify( + var stdout_buf: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + try std.json.Stringify.value( options, .{ .whitespace = .indent_2 }, - std.io.getStdOut().writer(), + stdout, ); } diff --git a/src/Help.zig b/src/Help.zig index 31cdcc8..c766bde 100644 --- a/src/Help.zig +++ b/src/Help.zig @@ -18,11 +18,12 @@ pub const Usage = struct { body: []const u8, pub fn render(usage: Usage, stdout: File, colors: *const ColorScheme) void { - const term = Terminal.init(stdout); - usage.renderToTerminal(term, colors); + var term = Terminal.init(stdout); + defer term.flush(); + usage.renderToTerminal(&term, colors); } - pub fn renderToTerminal(usage: Usage, term: Terminal, colors: *const ColorScheme) void { + pub fn renderToTerminal(usage: Usage, term: *Terminal, colors: *const ColorScheme) void { term.print(colors.header, "Usage: ", .{}); term.print(colors.command_name, "{s}", .{usage.command}); term.print(colors.usage, "{s}\n", .{usage.body}); @@ -99,9 +100,10 @@ const Section = struct { } }; -pub fn render(help: *const Help, stdout: File, colors: *const ColorScheme) void { - const term = Terminal.init(stdout); - help.usage.renderToTerminal(term, colors); +pub fn render(help: *const Help, writer: File, colors: *const ColorScheme) void { + var term = Terminal.init(writer); + defer term.flush(); + help.usage.renderToTerminal(&term, colors); if (help.description) |description| { term.print(colors.command_description, "\n{s}\n", .{description}); @@ -181,7 +183,7 @@ pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Help { help.sections = help.sections ++ .{options}; if (info.positionals.len > 0) { - const pos_descriptions = meta.getDescriptions(std.meta.FieldType(Flags, .positional)); + const pos_descriptions = meta.getDescriptions(@FieldType(Flags, "positional")); var arguments = Section{ .header = "Arguments:" }; for (info.positionals) |arg| { arguments.add(.{ diff --git a/src/Parser.zig b/src/Parser.zig index e806620..adbd880 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -17,10 +17,11 @@ colors: *const ColorScheme, help: Help, fn fatal(parser: *const Parser, comptime fmt: []const u8, args: anytype) noreturn { - const stderr = Terminal.init(std.io.getStdErr()); - stderr.print(parser.colors.error_label, "Error: ", .{}); - stderr.print(parser.colors.error_message, fmt ++ "\n\n", args); - parser.help.render(std.io.getStdErr(), parser.colors); + var term = Terminal.init(std.fs.File.stderr()); + term.print(parser.colors.error_label, "Error: ", .{}); + term.print(parser.colors.error_message, fmt ++ "\n\n", args); + term.flush(); + parser.help.render(std.fs.File.stderr(), parser.colors); std.process.exit(1); } @@ -46,7 +47,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl } if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - parser.help.render(std.io.getStdOut(), parser.colors); + parser.help.render(std.fs.File.stdout(), parser.colors); std.process.exit(0); } diff --git a/src/Terminal.zig b/src/Terminal.zig index 442e0f8..a4f3af8 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -6,29 +6,38 @@ const ColorScheme = @import("ColorScheme.zig"); const tty = std.io.tty; const File = std.fs.File; -writer: File.Writer, -config: tty.Config, +write_buffer: [1024]u8 = undefined, +file: File, +writer: std.fs.File.Writer = undefined, +config: tty.Config = undefined, pub fn init(file: File) Terminal { - return .{ - .writer = file.writer(), + var term = Terminal{ + .file = file, .config = tty.detectConfig(file), }; + term.writer = term.file.writer(&term.write_buffer); + return term; } pub fn print( - terminal: Terminal, + term: *Terminal, style: ColorScheme.Style, comptime format: []const u8, args: anytype, ) void { + const writer = &term.writer.interface; for (style) |color| { - terminal.config.setColor(terminal.writer, color) catch {}; + term.config.setColor(writer, color) catch @panic("Can't set color!"); } - terminal.writer.print(format, args) catch {}; + writer.print(format, args) catch @panic("Print failed!"); if (style.len > 0) { - terminal.config.setColor(terminal.writer, .reset) catch {}; + term.config.setColor(writer, .reset) catch @panic("Can't set color!"); } } + +pub fn flush(term: *Terminal) void { + term.writer.interface.flush() catch @panic("Flush failed!"); +} diff --git a/src/flags.zig b/src/flags.zig index 9a4f5ef..bb06202 100644 --- a/src/flags.zig +++ b/src/flags.zig @@ -35,5 +35,5 @@ pub fn printHelp( options: Options, ) void { const help = comptime Help.generate(Flags, meta.info(Flags), exe_name); - help.render(std.io.getStdOut(), options.colors); + help.render(std.fs.File.stdout(), options.colors); } From 8f65f0d5bf0d92b950ddf16afb34533491831c7d Mon Sep 17 00:00:00 2001 From: JacobCrabill Date: Sun, 24 Aug 2025 09:50:30 -0700 Subject: [PATCH 6/7] flush more often I think I was overflowing the write buffer w/o more frequent flushing --- src/Help.zig | 3 +++ src/Terminal.zig | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Help.zig b/src/Help.zig index c766bde..c08938c 100644 --- a/src/Help.zig +++ b/src/Help.zig @@ -25,8 +25,11 @@ pub const Usage = struct { pub fn renderToTerminal(usage: Usage, term: *Terminal, colors: *const ColorScheme) void { term.print(colors.header, "Usage: ", .{}); + term.flush(); term.print(colors.command_name, "{s}", .{usage.command}); + term.flush(); term.print(colors.usage, "{s}\n", .{usage.body}); + term.flush(); } pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Usage { diff --git a/src/Terminal.zig b/src/Terminal.zig index a4f3af8..6bb197e 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -26,7 +26,7 @@ pub fn print( comptime format: []const u8, args: anytype, ) void { - const writer = &term.writer.interface; + const writer: *std.Io.Writer = &term.writer.interface; for (style) |color| { term.config.setColor(writer, color) catch @panic("Can't set color!"); } @@ -36,6 +36,8 @@ pub fn print( if (style.len > 0) { term.config.setColor(writer, .reset) catch @panic("Can't set color!"); } + + writer.flush() catch @panic("Flush failed!"); } pub fn flush(term: *Terminal) void { From de90d081cd4ef7907fbdd8e1e3853fcee9bcf1b9 Mon Sep 17 00:00:00 2001 From: Jacob Crabill Date: Wed, 10 Jun 2026 16:00:21 -0700 Subject: [PATCH 7/7] Upgrade to zig 0.16 --- build.zig.zon | 4 +-- examples/colors.zig | 12 +++----- examples/optional_command.zig | 13 ++++----- examples/overview.zig | 13 ++++----- examples/trailing.zig | 13 ++++----- src/ColorScheme.zig | 2 +- src/Help.zig | 12 ++++---- src/Parser.zig | 55 +++++++++++++++++++---------------- src/Terminal.zig | 30 ++++++++++++------- src/flags.zig | 6 ++-- 10 files changed, 82 insertions(+), 78 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 2956b79..f9a6de1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,8 +1,8 @@ .{ .name = .flags, - .version = "0.11.0", + .version = "0.12.0", .fingerprint = 0xb0541bade61ff6b, - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0", // Want to limit these to only the things that really constitute "the library". // The hash needn't be updated by some small change to the README. .paths = .{ diff --git a/examples/colors.zig b/examples/colors.zig index 00239a3..36f1724 100644 --- a/examples/colors.zig +++ b/examples/colors.zig @@ -1,17 +1,13 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const args = try init.minimal.args.toSlice(init.arena.allocator()); - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); - - _ = flags.parse(args, "colors", Flags, .{ + _ = flags.parse(init.io, args, "colors", Flags, .{ // Use the `colors` option to provide a colorscheme for the error/help messages. // Specifying this as empty: `.colors = &.{}` will disable colors. - // Each field is a list of type `std.io.tty.Color`. + // Each field is a list of type `std.Io.Terminal.Color`. .colors = &flags.ColorScheme{ .error_label = &.{ .bright_red, .bold }, .command_name = &.{.bright_green}, diff --git a/examples/optional_command.zig b/examples/optional_command.zig index b6be20e..8dbc45d 100644 --- a/examples/optional_command.zig +++ b/examples/optional_command.zig @@ -1,17 +1,14 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const args = try init.minimal.args.toSlice(init.arena.allocator()); - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); - - const options = flags.parse(args, "overview", Flags, .{}); + const options = flags.parse(io, args, "overview", Flags, .{}); var stdout_buf: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buf); const stdout = &stdout_writer.interface; defer stdout.flush() catch {}; diff --git a/examples/overview.zig b/examples/overview.zig index 0b3987a..d2b5ee3 100644 --- a/examples/overview.zig +++ b/examples/overview.zig @@ -1,17 +1,14 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const args = try init.minimal.args.toSlice(init.arena.allocator()); - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); - - const options: Flags = flags.parse(args, "overview", Flags, .{}); + const options: Flags = flags.parse(io, args, "overview", Flags, .{}); var stdout_buf: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buf); const stdout = &stdout_writer.interface; defer stdout.flush() catch {}; diff --git a/examples/trailing.zig b/examples/trailing.zig index 29795f9..68fefe5 100644 --- a/examples/trailing.zig +++ b/examples/trailing.zig @@ -1,17 +1,14 @@ const std = @import("std"); const flags = @import("flags"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}).init; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const args = try init.minimal.args.toSlice(init.arena.allocator()); - const args = try std.process.argsAlloc(gpa.allocator()); - defer std.process.argsFree(gpa.allocator(), args); - - const options = flags.parse(args, "trailing", Flags, .{}); + const options = flags.parse(io, args, "trailing", Flags, .{}); var stdout_buf: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buf); const stdout = &stdout_writer.interface; defer stdout.flush() catch {}; diff --git a/src/ColorScheme.zig b/src/ColorScheme.zig index 8b95bf7..59755ce 100644 --- a/src/ColorScheme.zig +++ b/src/ColorScheme.zig @@ -2,7 +2,7 @@ const ColorScheme = @This(); const std = @import("std"); -const Color = std.io.tty.Color; +const Color = std.Io.Terminal.Color; pub const Style = []const Color; diff --git a/src/Help.zig b/src/Help.zig index c08938c..1a7a7d5 100644 --- a/src/Help.zig +++ b/src/Help.zig @@ -3,7 +3,7 @@ const Help = @This(); const std = @import("std"); const meta = @import("meta.zig"); -const File = std.fs.File; +const File = std.Io.File; const ColorScheme = @import("ColorScheme.zig"); const Terminal = @import("Terminal.zig"); @@ -17,8 +17,9 @@ pub const Usage = struct { command: []const u8, body: []const u8, - pub fn render(usage: Usage, stdout: File, colors: *const ColorScheme) void { - var term = Terminal.init(stdout); + pub fn render(usage: Usage, io: std.Io, stdout: File, colors: *const ColorScheme) void { + var term = Terminal.init(io, stdout); + term.fixSelfRef(); defer term.flush(); usage.renderToTerminal(&term, colors); } @@ -103,8 +104,9 @@ const Section = struct { } }; -pub fn render(help: *const Help, writer: File, colors: *const ColorScheme) void { - var term = Terminal.init(writer); +pub fn render(help: *const Help, io: std.Io, writer: File, colors: *const ColorScheme) void { + var term = Terminal.init(io, writer); + term.fixSelfRef(); defer term.flush(); help.usage.renderToTerminal(&term, colors); diff --git a/src/Parser.zig b/src/Parser.zig index adbd880..1c2009b 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -16,18 +16,19 @@ colors: *const ColorScheme, /// The current Help of the command being parsed help: Help, -fn fatal(parser: *const Parser, comptime fmt: []const u8, args: anytype) noreturn { - var term = Terminal.init(std.fs.File.stderr()); +fn fatal(parser: *const Parser, io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { + var term = Terminal.init(io, std.Io.File.stderr()); + term.fixSelfRef(); term.print(parser.colors.error_label, "Error: ", .{}); term.print(parser.colors.error_message, fmt ++ "\n\n", args); term.flush(); - parser.help.render(std.fs.File.stderr(), parser.colors); + parser.help.render(io, std.Io.File.stderr(), parser.colors); std.process.exit(1); } /// Parse the Flags struct and return the parsed result. /// If an error is encounterd, the error is displayed, followed by the help menu. -pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Flags { +pub fn parse(parser: *Parser, io: std.Io, Flags: type, comptime command_name: []const u8) Flags { const info = comptime meta.info(Flags); parser.help = comptime Help.generate(Flags, info, command_name); @@ -43,18 +44,18 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl next_arg: while (parser.nextArg()) |arg| { if (arg.len == 0) { - parser.fatal("empty argument", .{}); + parser.fatal(io, "empty argument", .{}); } if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - parser.help.render(std.fs.File.stdout(), parser.colors); + parser.help.render(io, std.Io.File.stdout(), parser.colors); std.process.exit(0); } if (std.mem.eql(u8, arg, "--")) { // Blindly treat remaining arguments as positional. while (parser.nextArg()) |positional| { - if (parser.parsePositional(positional, positional_index, info.positionals, &flags) == .consumed_all) { + if (parser.parsePositional(io, positional, positional_index, info.positionals, &flags) == .consumed_all) { break :next_arg; } positional_index += 1; @@ -63,17 +64,17 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl if (std.mem.startsWith(u8, arg, "--")) { inline for (info.flags) |flag| if (std.mem.eql(u8, arg, flag.flag_name)) { - @field(flags, flag.field_name) = parser.parseOption(flag.type, flag.flag_name); + @field(flags, flag.field_name) = parser.parseOption(io, flag.type, flag.flag_name); @field(passed, flag.field_name) = true; continue :next_arg; }; - parser.fatal("unrecognized flag: {s}", .{arg}); + parser.fatal(io, "unrecognized flag: {s}", .{arg}); } if (std.mem.startsWith(u8, arg, "-")) { if (arg.len == 1) { - parser.fatal("unrecognized argument: '-'", .{}); + parser.fatal(io, "unrecognized argument: '-'", .{}); } const switch_set = arg[1..]; @@ -83,9 +84,10 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl // Removing this check would allow formats like: // `$ -abc value-for-a value-for-b value-for-c` if (flag.type != bool and i < switch_set.len - 1) { - parser.fatal("missing value after switch: {c}", .{switch_char}); + parser.fatal(io, "missing value after switch: {c}", .{switch_char}); } @field(flags, flag.field_name) = parser.parseOption( + io, flag.type, &.{ '-', switch_char }, ); @@ -93,21 +95,21 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl continue :next_switch; } }; - parser.fatal("unrecognized switch: {c}", .{ch}); + parser.fatal(io, "unrecognized switch: {c}", .{ch}); } continue :next_arg; } inline for (info.subcommands) |cmd| { if (std.mem.eql(u8, arg, cmd.command_name)) { - const cmd_flags = parser.parse(cmd.type, command_name ++ " " ++ cmd.command_name); + const cmd_flags = parser.parse(io, cmd.type, command_name ++ " " ++ cmd.command_name); flags.command = @unionInit(meta.unwrapOptional(@TypeOf(flags.command)), cmd.field_name, cmd_flags); passed.command = true; continue :next_arg; } } - if (parser.parsePositional(arg, positional_index, info.positionals, &flags) == .consumed_all) { + if (parser.parsePositional(io, arg, positional_index, info.positionals, &flags) == .consumed_all) { break :next_arg; } positional_index += 1; @@ -119,7 +121,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl .bool => false, .optional => null, else => { - parser.fatal("missing required flag: {s}", .{flag.flag_name}); + parser.fatal(io, "missing required flag: {s}", .{flag.flag_name}); }, }; }; @@ -130,7 +132,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl switch (@typeInfo(pos.type)) { .optional => null, else => { - parser.fatal("missing required argument: {s}", .{pos.arg_name}); + parser.fatal(io, "missing required argument: {s}", .{pos.arg_name}); }, }; } @@ -140,7 +142,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl if (info.optional_commands) { flags.command = null; } else { - parser.fatal("missing subcommand", .{}); + parser.fatal(io, "missing subcommand", .{}); } } @@ -149,6 +151,7 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl fn parsePositional( parser: *Parser, + io: std.Io, arg: [:0]const u8, index: usize, comptime positionals: []const meta.Positional, @@ -160,14 +163,14 @@ fn parsePositional( parser.current_arg = parser.args.len; return .consumed_all; } - parser.fatal("unexpected argument: {s}", .{arg}); + parser.fatal(io, "unexpected argument: {s}", .{arg}); } switch (index) { inline 0...positionals.len - 1 => |i| { const positional = positionals[i]; const T = meta.unwrapOptional(positional.type); - @field(flags.positional, positional.field_name) = parser.parseValue(T, arg); + @field(flags.positional, positional.field_name) = parser.parseValue(io, T, arg); return .consumed_one; }, @@ -175,27 +178,29 @@ fn parsePositional( } } -fn parseOption(parser: *Parser, T: type, option_name: []const u8) T { +fn parseOption(parser: *Parser, io: std.Io, T: type, option_name: []const u8) T { if (T == bool) return true; const value = parser.nextArg() orelse { - parser.fatal("missing value for '{s}'", .{option_name}); + parser.fatal(io, "missing value for '{s}'", .{option_name}); }; - return parser.parseValue(meta.unwrapOptional(T), value); + return parser.parseValue(io, meta.unwrapOptional(T), value); } -fn parseValue(parser: *const Parser, T: type, arg: [:0]const u8) T { +fn parseValue(parser: *const Parser, io: std.Io, T: type, arg: [:0]const u8) T { if (T == []const u8 or T == [:0]const u8) return arg; switch (@typeInfo(T)) { .int => |info| return std.fmt.parseInt(T, arg, 10) catch |err| { switch (err) { error.Overflow => parser.fatal( + io, "value out of bounds for {d}-bit {s} integer: {s}", .{ info.bits, @tagName(info.signedness), arg }, ), error.InvalidCharacter => parser.fatal( + io, "expected integer number, found '{s}'", .{arg}, ), @@ -204,7 +209,7 @@ fn parseValue(parser: *const Parser, T: type, arg: [:0]const u8) T { .float => return std.fmt.parseFloat(T, arg) catch |err| switch (err) { error.InvalidCharacter => { - parser.fatal("expected numerical value, found '{s}'", .{arg}); + parser.fatal(io, "expected numerical value, found '{s}'", .{arg}); }, }, @@ -215,7 +220,7 @@ fn parseValue(parser: *const Parser, T: type, arg: [:0]const u8) T { } } - parser.fatal("unrecognized option: '{s}'", .{arg}); + parser.fatal(io, "unrecognized option: '{s}'", .{arg}); }, else => comptime meta.compileError("invalid flag type: {s}", .{@typeName(T)}), diff --git a/src/Terminal.zig b/src/Terminal.zig index 6bb197e..025f02d 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -3,43 +3,51 @@ const Terminal = @This(); const std = @import("std"); const ColorScheme = @import("ColorScheme.zig"); -const tty = std.io.tty; -const File = std.fs.File; +const File = std.Io.File; write_buffer: [1024]u8 = undefined, +file_writer: std.Io.File.Writer = undefined, file: File, -writer: std.fs.File.Writer = undefined, -config: tty.Config = undefined, +tty: std.Io.Terminal = undefined, -pub fn init(file: File) Terminal { +pub fn init(io: std.Io, file: File) Terminal { var term = Terminal{ .file = file, - .config = tty.detectConfig(file), + .tty = .{ + .writer = undefined, + .mode = std.Io.Terminal.Mode.detect(io, file, false, false) catch .escape_codes, + }, }; - term.writer = term.file.writer(&term.write_buffer); + term.file_writer = term.file.writer(io, &term.write_buffer); + term.tty.writer = &term.file_writer.interface; return term; } +pub fn fixSelfRef(self: *Terminal) void { + self.file_writer.interface.buffer = &self.write_buffer; + self.tty.writer = &self.file_writer.interface; +} + pub fn print( term: *Terminal, style: ColorScheme.Style, comptime format: []const u8, args: anytype, ) void { - const writer: *std.Io.Writer = &term.writer.interface; + const writer: *std.Io.Writer = term.tty.writer; for (style) |color| { - term.config.setColor(writer, color) catch @panic("Can't set color!"); + term.tty.setColor(color) catch @panic("Can't set color!"); } writer.print(format, args) catch @panic("Print failed!"); if (style.len > 0) { - term.config.setColor(writer, .reset) catch @panic("Can't set color!"); + term.tty.setColor(.reset) catch @panic("Can't set color!"); } writer.flush() catch @panic("Flush failed!"); } pub fn flush(term: *Terminal) void { - term.writer.interface.flush() catch @panic("Flush failed!"); + term.tty.writer.flush() catch @panic("Flush failed!"); } diff --git a/src/flags.zig b/src/flags.zig index bb06202..ec4fd15 100644 --- a/src/flags.zig +++ b/src/flags.zig @@ -13,6 +13,7 @@ pub const Options = struct { }; pub fn parse( + io: std.Io, args: []const [:0]const u8, /// The name of your program. comptime exe_name: []const u8, @@ -26,14 +27,15 @@ pub fn parse( .help = comptime Help.generate(Flags, meta.info(Flags), exe_name), }; - return parser.parse(Flags, exe_name); + return parser.parse(io, Flags, exe_name); } pub fn printHelp( + io: std.Io, comptime exe_name: []const u8, Flags: type, options: Options, ) void { const help = comptime Help.generate(Flags, meta.info(Flags), exe_name); - help.render(std.fs.File.stdout(), options.colors); + help.render(io, std.Io.File.stdout(), options.colors); }