Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn main() !void {
.{},
);

try std.io.getStdOut().writer().print("Hello, {s}!\n", .{cli.username});
try std.debug.print("Hello, {s}!\n", .{cli.username});
}

const std = @import("std");
Expand Down
2 changes: 1 addition & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ pub fn build(b: *std.Build) void {
});
example.root_module.addImport("flags", mod);
const run_example = b.addRunArtifact(example);
if (b.args) |args| run_example.addArgs(args);
run_example.addPassthruArgs(); // Zig 0.17+
example_step.dependOn(&run_example.step);
}
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .flags,
.version = "0.10.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 = .{
Expand Down
12 changes: 6 additions & 6 deletions examples/colors.zig
Original file line number Diff line number Diff line change
@@ -1,14 +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 gpa = init.gpa;

const args = try std.process.argsAlloc(gpa.allocator());
defer std.process.argsFree(gpa.allocator(), args);
const args = try init.minimal.args.toSlice(gpa);
defer gpa.free(args);

_ = flags.parse(args, "colors", Flags, .{
_ = flags.parse(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`.
Expand Down
15 changes: 7 additions & 8 deletions examples/overview.zig
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
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 gpa = init.gpa;
const args = try init.minimal.args.toSlice(gpa);
defer gpa.free(args);

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 buffer: [1024]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&buffer);
var file_writer = std.Io.File.stdout().writer(io, &buffer);
try std.json.Stringify.value(
options,
.{ .whitespace = .indent_2 },
Expand Down
14 changes: 7 additions & 7 deletions examples/trailing.zig
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
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 gpa = init.gpa;

const args = try std.process.argsAlloc(gpa.allocator());
defer std.process.argsFree(gpa.allocator(), args);
const args = try init.minimal.args.toSlice(gpa);
defer gpa.free(args);

const options = flags.parse(args, "trailing", Flags, .{});
const options = flags.parse(io, args, "trailing", Flags, .{});

var stdout_writer = std.fs.File.stdout().writer(&.{});
var stdout_writer = std.Io.File.stdout().writer(io, &.{});
try std.json.Stringify.value(
options,
.{ .whitespace = .indent_2 },
Expand Down
2 changes: 1 addition & 1 deletion src/ColorScheme.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
34 changes: 20 additions & 14 deletions src/Help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -17,9 +17,9 @@ pub const Usage = struct {
command: []const u8,
body: []const u8,

pub fn render(usage: Usage, stdout: File, colors: *const ColorScheme) void {
var stdout_writer = stdout.writer(&.{});
const term = Terminal.init(stdout, &stdout_writer.interface);
pub fn render(io: std.Io, usage: Usage, stdout: File, colors: *const ColorScheme) void {
var stdout_writer = stdout.writerStreaming(io, &.{});
const term = Terminal.init(io, stdout, &stdout_writer.interface) catch unreachable;
usage.renderToTerminal(term, colors);
}

Expand Down Expand Up @@ -75,7 +75,11 @@ pub const Usage = struct {
fn add(usage: *Usage, item: []const u8, line_len: *usize) void {
if (line_len.* + " ".len + item.len > max_line_len) {
const indent_len = "Usage: ".len + usage.command.len;
usage.body = usage.body ++ "\n" ++ " " ** indent_len;
usage.body = usage.body ++ "\n" ++ comptime blk: {
var indent: [indent_len]u8 = undefined;
@memset(&indent, ' ');
break :blk &indent;
};
line_len.* = indent_len;
}

Expand All @@ -100,9 +104,9 @@ const Section = struct {
}
};

pub fn render(help: *const Help, stdout: File, colors: *const ColorScheme) void {
var stdout_writer = stdout.writer(&.{});
const term = Terminal.init(stdout, &stdout_writer.interface);
pub fn render(help: *const Help, io: std.Io, stdout: File, colors: *const ColorScheme) void {
var stdout_writer = stdout.writerStreaming(io, &.{});
const term = Terminal.init(io, stdout, &stdout_writer.interface) catch unreachable;
help.usage.renderToTerminal(term, colors);

if (help.description) |description| {
Expand Down Expand Up @@ -166,10 +170,11 @@ pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Help {
const T = meta.unwrapOptional(flag.type);
if (@typeInfo(T) == .@"enum") {
const variant_descriptions = meta.getDescriptions(T);
for (@typeInfo(T).@"enum".fields) |variant| {
const enum_info = @typeInfo(T).@"enum";
for (enum_info.field_names) |variant_name| {
options.add(.{
.name = " " ++ meta.toKebab(variant.name),
.desc = @field(variant_descriptions, variant.name),
.name = " " ++ meta.toKebab(variant_name),
.desc = @field(variant_descriptions, variant_name),
});
}
}
Expand All @@ -194,10 +199,11 @@ pub fn generate(Flags: type, info: meta.FlagsInfo, command: []const u8) Help {
const T = meta.unwrapOptional(arg.type);
if (@typeInfo(T) == .@"enum") {
const variant_descriptions = meta.getDescriptions(T);
for (@typeInfo(T).@"enum".fields) |variant| {
const enum_info = @typeInfo(T).@"enum";
for (enum_info.field_names) |variant_name| {
arguments.add(.{
.name = " " ++ meta.toKebab(variant.name),
.desc = @field(variant_descriptions, variant.name),
.name = " " ++ meta.toKebab(variant_name),
.desc = @field(variant_descriptions, variant_name),
});
}
}
Expand Down
62 changes: 33 additions & 29 deletions src/Parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ args: []const [:0]const u8,
current_arg: usize,
colors: *const ColorScheme,

fn fatal(parser: *const Parser, comptime fmt: []const u8, args: anytype) noreturn {
const stderr_file = std.fs.File.stderr();
var stderr_file_writer = stderr_file.writer(&.{});
const stderr = Terminal.init(stderr_file, &stderr_file_writer.interface);
fn fatal(parser: *const Parser, io: std.Io, comptime fmt: []const u8, args: anytype) noreturn {
var stderr_file = std.Io.File.stderr();
var stderr_writer = stderr_file.writerStreaming(io, &.{});
const stderr = Terminal.init(io, stderr_file, &stderr_writer.interface) catch unreachable;
stderr.print(parser.colors.error_label, "Error: ", .{});
stderr.print(parser.colors.error_message, fmt ++ "\n", args);
std.process.exit(1);
}

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);
const help = comptime Help.generate(Flags, info, command_name);

Expand All @@ -39,18 +39,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")) {
help.render(std.fs.File.stdout(), parser.colors);
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;
Expand All @@ -59,17 +59,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..];
Expand All @@ -79,31 +79,32 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl
// Removing this check would allow formats like:
// `$ <cmd> -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 },
);
@field(passed, flag.field_name) = true;
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(@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;
Expand All @@ -115,7 +116,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});
},
};
};
Expand All @@ -126,21 +127,22 @@ 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});
},
};
}
}

if (info.subcommands.len > 0 and !passed.command) {
parser.fatal("missing subcommand", .{});
parser.fatal(io, "missing subcommand", .{});
}

return flags;
}

fn parsePositional(
parser: *Parser,
io: std.Io,
arg: [:0]const u8,
index: usize,
comptime positionals: []const meta.Positional,
Expand All @@ -152,42 +154,44 @@ 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;
},

else => unreachable,
}
}

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},
),
Expand All @@ -196,18 +200,18 @@ 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});
},
},

.@"enum" => |info| {
inline for (info.fields) |field| {
if (std.mem.eql(u8, arg, meta.toKebab(field.name))) {
return @enumFromInt(field.value);
inline for (info.field_names, info.field_values) |field_name, field_value| {
if (std.mem.eql(u8, arg, meta.toKebab(field_name))) {
return @fromBackingInt(@intCast(field_value));
}
}

parser.fatal("unrecognized option: '{s}'", .{arg});
parser.fatal(io, "unrecognized option: '{s}'", .{arg});
},

else => comptime meta.compileError("invalid flag type: {s}", .{@typeName(T)}),
Expand Down
Loading