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
12 changes: 7 additions & 5 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -29,16 +28,19 @@ pub fn build(b: *std.Build) void {
overview,
colors,
trailing,
optional_command,
},
"example",
"Example to run for example step (default = overview)",
) orelse .overview;

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);
Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -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 = .{
Expand Down
68 changes: 68 additions & 0 deletions examples/optional_command.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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, .{});

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 },
stdout,
);
}

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',
};
};
12 changes: 9 additions & 3 deletions examples/overview.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}

Expand Down Expand Up @@ -76,6 +81,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 = .{
Expand Down
9 changes: 7 additions & 2 deletions examples/trailing.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}

Expand Down
24 changes: 15 additions & 9 deletions src/Help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ 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.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 {
Expand Down Expand Up @@ -99,9 +103,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});
Expand Down Expand Up @@ -181,7 +186,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(.{
Expand All @@ -203,8 +208,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),
Expand Down
24 changes: 17 additions & 7 deletions src/Parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@ 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);
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);
}

/// 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) = .{};
Expand All @@ -41,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")) {
help.render(std.io.getStdOut(), parser.colors);
parser.help.render(std.fs.File.stdout(), parser.colors);
std.process.exit(0);
}

Expand Down Expand Up @@ -95,7 +101,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;
}
Expand Down Expand Up @@ -131,7 +137,11 @@ pub fn parse(parser: *Parser, Flags: type, comptime command_name: []const u8) Fl
}

if (info.subcommands.len > 0 and !passed.command) {
parser.fatal("missing subcommand", .{});
if (info.optional_commands) {
flags.command = null;
} else {
parser.fatal("missing subcommand", .{});
}
}

return flags;
Expand Down
27 changes: 19 additions & 8 deletions src/Terminal.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,40 @@ 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: *std.Io.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!");
}

writer.flush() catch @panic("Flush failed!");
}

pub fn flush(term: *Terminal) void {
term.writer.interface.flush() catch @panic("Flush failed!");
}
11 changes: 11 additions & 0 deletions src/flags.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +23,17 @@ 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);
}

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.fs.File.stdout(), options.colors);
}
25 changes: 19 additions & 6 deletions src/meta.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub const FlagsInfo = struct {
flags: []const Flag = &.{},
positionals: []const Positional = &.{},
subcommands: []const SubCommand = &.{},
optional_commands: bool = false,
};

const SubCommand = struct {
Expand Down Expand Up @@ -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,
Expand Down