Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
13 changes: 6 additions & 7 deletions examples/colors.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 allocator = init.arena.allocator();
const io = init.io;

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

_ = 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`.
// Each field is a list of type `std.Io.Terminal.Color`.
.colors = &flags.ColorScheme{
.error_label = &.{ .bright_red, .bold },
.command_name = &.{.bright_green},
Expand Down
18 changes: 8 additions & 10 deletions examples/overview.zig
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
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 allocator = init.arena.allocator();
const io = init.io;

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

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

var stdout = std.Io.File.stdout().writerStreaming(io, &.{});

var buffer: [1024]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&buffer);
try std.json.Stringify.value(
options,
.{ .whitespace = .indent_2 },
&file_writer.interface,
&stdout.interface,
);
try file_writer.interface.flush();
}

const Flags = struct {
Expand Down
15 changes: 7 additions & 8 deletions examples/trailing.zig
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
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 allocator = init.arena.allocator();
const io = init.io;

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

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 = std.Io.File.stdout().writerStreaming(io, &.{});
try std.json.Stringify.value(
options,
.{ .whitespace = .indent_2 },
&stdout_writer.interface,
&stdout.interface,
);
}

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
14 changes: 7 additions & 7 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 @@ -100,9 +100,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
56 changes: 30 additions & 26 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,7 +200,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});
},
},

Expand All @@ -207,7 +211,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)}),
Expand Down
22 changes: 12 additions & 10 deletions src/Terminal.zig
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
//! wrapper around std.Io.Terminal that gives us a convenient
//! init and print method.

const Terminal = @This();

const std = @import("std");
const ColorScheme = @import("ColorScheme.zig");

const tty = std.Io.tty;
const File = std.fs.File;
const tty = std.Io.Terminal;
const File = std.Io.File;

writer: *std.Io.Writer,
config: tty.Config,
instance: tty,

pub fn init(file: File, writer: *std.Io.Writer) Terminal {
pub fn init(io: std.Io, file: File, writer: *std.Io.Writer) !Terminal {
const terminal_mode: std.Io.Terminal.Mode = try .detect(io, file, false, false);
return .{
.writer = writer,
.config = tty.detectConfig(file),
.instance = .{ .writer = writer, .mode = terminal_mode },
};
}

Expand All @@ -23,12 +25,12 @@ pub fn print(
args: anytype,
) void {
for (style) |color| {
terminal.config.setColor(terminal.writer, color) catch {};
terminal.instance.setColor(color) catch {};
}

terminal.writer.print(format, args) catch {};
terminal.instance.writer.print(format, args) catch {};

if (style.len > 0) {
terminal.config.setColor(terminal.writer, .reset) catch {};
terminal.instance.setColor(.reset) catch {};
}
}
Loading