diff --git a/examples/git.zig b/examples/git.zig index ac06bea..93b722a 100644 --- a/examples/git.zig +++ b/examples/git.zig @@ -6,7 +6,7 @@ pub fn main() void { var args = std.process.args(); const command = flags.parse(&args, Git); - prettyPrint(command.config, command.args); + prettyPrint(command.flags, command.args); } /// A very stripped-down model of the git CLI. diff --git a/examples/overview.zig b/examples/overview.zig index c0dbddf..0c2795c 100644 --- a/examples/overview.zig +++ b/examples/overview.zig @@ -2,7 +2,21 @@ const std = @import("std"); const flags = @import("flags"); const prettyPrint = @import("prettyprint.zig").prettyPrint; -const Config = struct { +// Optionally, you can specify the size of the buffer for positional arguments if you wish to +// impose a specific limit or you expect more than the default (32). +pub const max_positional_flags = 3; + +pub fn main() !void { + var args = std.process.args(); + const result = flags.parse(&args, Command); + + prettyPrint( + result.flags, // result, has type of `Command` + result.args, // extra positional arguments + ); +} + +const Command = struct { // This field is required for your top-level command, and is used in help messages. pub const name = "example"; @@ -52,18 +66,3 @@ const Config = struct { .size = "How big?", }; }; - -// Optionally, you can specify the size of the buffer for positional flags if you wish to -// impose a specific limit or you expect more flags than the default (32). -pub const max_positional_flags = 3; - -pub fn main() !void { - var args = std.process.args(); - - const result = flags.parse(&args, Config); - - prettyPrint( - result.config, // result, of passed `Config` type - result.args, // extra positional flags - ); -} diff --git a/examples/prettyprint.zig b/examples/prettyprint.zig index 08a85d7..6136890 100644 --- a/examples/prettyprint.zig +++ b/examples/prettyprint.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const flags = @import("flags"); fn print(comptime fmt: []const u8, args: anytype) void { const stdout = std.io.getStdOut().writer(); @@ -7,10 +6,10 @@ fn print(comptime fmt: []const u8, args: anytype) void { } /// Utility for pretty printing a parse result. -pub fn prettyPrint(config: anytype, args: []const []const u8) void { - switch (@typeInfo(@TypeOf(config))) { - .Struct => printOptions(config), - .Union => printCommand(config), +pub fn prettyPrint(command: anytype, args: []const []const u8) void { + switch (@typeInfo(@TypeOf(command))) { + .Struct => printFlags(command), + .Union => printCommand(command), else => comptime unreachable, } @@ -28,7 +27,7 @@ fn printCommand(command: anytype) void { switch (@typeInfo(field.type)) { .Struct => { print("\n", .{}); - printOptions(@field(command, field.name)); + printFlags(@field(command, field.name)); }, .Union => printCommand(@field(command, field.name)), else => unreachable, @@ -37,10 +36,10 @@ fn printCommand(command: anytype) void { } } -fn printOptions(options: anytype) void { - inline for (std.meta.fields(@TypeOf(options))) |field| { +fn printFlags(flags: anytype) void { + inline for (std.meta.fields(@TypeOf(flags))) |field| { print("{s}: ", .{field.name}); - const value = @field(options, field.name); + const value = @field(flags, field.name); switch (field.type) { []const u8 => print("{s}", .{value}), ?[]const u8 => print("{?s}", .{value}), diff --git a/examples/subcommands.zig b/examples/subcommands.zig index 70728fc..4a51774 100644 --- a/examples/subcommands.zig +++ b/examples/subcommands.zig @@ -2,13 +2,26 @@ const std = @import("std"); const flags = @import("flags"); const prettyPrint = @import("prettyprint.zig").prettyPrint; +pub fn main() !void { + var args = std.process.args(); + const result = flags.parse(&args, Command); + + prettyPrint(result.flags, result.args); +} + const Command = union(enum) { pub const name = "subcommands"; + pub const descriptions = .{ + .add = "Create a new item", + .remove = "Delete an item", + .change = "Edit or move an existing item", + }; + add: struct { name: []const u8, pub const switches = .{ - .name = "n", + .name = 'n', }; }, @@ -17,8 +30,8 @@ const Command = union(enum) { all: bool, pub const switches = .{ - .name = "n", - .all = "a", + .name = 'n', + .all = 'a', }; pub const descriptions = .{ @@ -26,7 +39,7 @@ const Command = union(enum) { }; }, - nested: union(enum) { + change: union(enum) { edit: struct { title: ?[]const u8, content: ?[]const u8, @@ -48,11 +61,3 @@ const Command = union(enum) { }, }, }; - -pub fn main() !void { - var args = std.process.args(); - - const result = flags.parse(&args, Command); - - prettyPrint(result.config, result.args); -} diff --git a/src/help.zig b/src/help.zig index 2e43fa4..bda9c7e 100644 --- a/src/help.zig +++ b/src/help.zig @@ -4,7 +4,7 @@ const format = @import("format.zig"); pub fn helpMessage(comptime Command: type, comptime name: []const u8) []const u8 { if (std.meta.fields(Command).len == 0) return "Usage: " ++ name ++ "\n"; return switch (@typeInfo(Command)) { - .Struct => helpOptions(Command, name), + .Struct => helpFlags(Command, name), .Union => helpCommands(Command, name), else => comptime unreachable, }; @@ -12,11 +12,11 @@ pub fn helpMessage(comptime Command: type, comptime name: []const u8) []const u8 const indent = " "; -/// This is used during comptime to collect info about options/commands. +/// This is used during comptime to collect info about flags/commands. /// The `name` and `description` are stored separately then the `render` method is used /// to concatenate them with the correct spacing. const Description = struct { - /// For an option, this is the flag name and the switch, if present. + /// For a flag, this is the flag name and the switch, if present. /// For a command, this is the command name. /// For an enum variant, this is the variant name. /// This includes the preceding indentation. @@ -68,7 +68,7 @@ fn helpCommands(comptime Commands: type, comptime command_name: []const u8) []co return help; } -fn helpOptions(comptime Options: type, comptime command_name: []const u8) []const u8 { +fn helpFlags(comptime Flags: type, comptime command_name: []const u8) []const u8 { comptime var help: []const u8 = std.fmt.comptimePrint( // TODO: More specific usage expression. \\Usage: {s} [options] @@ -80,10 +80,10 @@ fn helpOptions(comptime Options: type, comptime command_name: []const u8) []cons comptime var descriptions: []const Description = &.{}; comptime var max_name_len = Description.help.name.len; - for (std.meta.fields(Options)) |field| { + for (std.meta.fields(Flags)) |field| { comptime var name: []const u8 = format.flagName(field); - if (comptime getSwitchFor(Options, field.name)) |swtch| { + if (comptime getSwitchFor(Flags, field.name)) |swtch| { name = std.fmt.comptimePrint( "-{c}, {s}", .{ swtch, name }, @@ -96,19 +96,19 @@ fn helpOptions(comptime Options: type, comptime command_name: []const u8) []cons descriptions = descriptions ++ [_]Description{.{ .name = name, - .description = getDescriptionFor(Options, field.name), + .description = getDescriptionFor(Flags, field.name), }}; - const OptionType = switch (@typeInfo(field.type)) { + const T = switch (@typeInfo(field.type)) { .Optional => |optional| optional.child, else => field.type, }; - if (@typeInfo(OptionType) == .Enum) { - for (std.meta.fields(OptionType)) |enum_field| { + if (@typeInfo(T) == .Enum) { + for (std.meta.fields(T)) |enum_field| { const variant = .{ .name = indent ** 2 ++ format.toKebab(enum_field.name), - .description = getDescriptionFor(OptionType, enum_field.name), + .description = getDescriptionFor(T, enum_field.name), }; if (variant.name.len > max_name_len) max_name_len = variant.name.len; descriptions = descriptions ++ [_]Description{variant}; @@ -125,11 +125,11 @@ fn helpOptions(comptime Options: type, comptime command_name: []const u8) []cons return help; } -fn getSwitchFor(comptime Options: type, comptime name: []const u8) ?u8 { - if (@hasDecl(Options, "switches") and - @hasField(@TypeOf(Options.switches), name)) +fn getSwitchFor(comptime Flags: type, comptime name: []const u8) ?u8 { + if (@hasDecl(Flags, "switches") and + @hasField(@TypeOf(Flags.switches), name)) { - return @field(Options.switches, name); + return @field(Flags.switches, name); } return null; } diff --git a/src/parser.zig b/src/parser.zig index df970f1..d257728 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -6,11 +6,11 @@ const format = @import("format.zig"); const ArgIterator = std.process.ArgIterator; -/// Combines `Config` with an `args` field which stores positional arguments. -pub fn Result(comptime Config: type) type { +/// Combines `Command` with an `args` field which stores positional arguments. +pub fn Result(comptime Command: type) type { return struct { - config: Config, - /// Stores extra positional arguments not linked to any flag or option. + flags: Command, + /// Stores extra positional arguments not linked to any flag. args: []const []const u8, }; } @@ -22,7 +22,7 @@ pub fn fatal(comptime message: []const u8, args: anytype) noreturn { std.process.exit(1); } -pub fn printHelp(comptime Command: type, comptime command_name: []const u8) void { +fn printHelp(comptime Command: type, comptime command_name: []const u8) void { const stdout = std.io.getStdOut().writer(); stdout.writeAll(comptime help.helpMessage(Command, command_name)) catch |err| { fatal("could not write help to stdout: {!}", .{err}); @@ -49,8 +49,8 @@ pub fn parse(args: *ArgIterator, comptime Command: type) Result(Command) { fn parseGeneric(args: *ArgIterator, comptime Command: type, comptime name: []const u8) Result(Command) { return switch (@typeInfo(Command)) { .Union => parseCommands(args, Command, name), - .Struct => parseOptions(args, Command, name), - else => @compileError("Command must be a struct or union type."), + .Struct => parseFlags(args, Command, name), + else => comptime unreachable, }; } @@ -65,7 +65,7 @@ fn parseCommands(args: *ArgIterator, comptime Commands: type, comptime command_n if (std.mem.eql(u8, command.name, arg)) { const sub_result = parseGeneric(args, command.type, command_name ++ " " ++ command.name); return .{ - .config = @unionInit(Commands, command.name, sub_result.config), + .flags = @unionInit(Commands, command.name, sub_result.flags), .args = sub_result.args, }; } @@ -74,9 +74,9 @@ fn parseCommands(args: *ArgIterator, comptime Commands: type, comptime command_n fatal("unrecognized subcommand: '{s}'", .{arg}); } -fn parseOptions(args: *ArgIterator, comptime Options: type, comptime command_name: []const u8) Result(Options) { - var options: Options = undefined; - var passed: std.enums.EnumFieldStruct(std.meta.FieldEnum(Options), bool, false) = .{}; +fn parseFlags(args: *ArgIterator, comptime Flags: type, comptime command_name: []const u8) Result(Flags) { + var flags: Flags = undefined; + var passed: std.enums.EnumFieldStruct(std.meta.FieldEnum(Flags), bool, false) = .{}; var positional_count: u32 = 0; @@ -92,16 +92,16 @@ fn parseOptions(args: *ArgIterator, comptime Options: type, comptime command_nam } if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - printHelp(Options, command_name); + printHelp(Flags, command_name); } if (arg.len == 1) fatal("unrecognized argument: '-'", .{}); if (arg[1] == '-') { - inline for (std.meta.fields(Options)) |field| { + inline for (std.meta.fields(Flags)) |field| { if (std.mem.eql(u8, arg, format.flagName(field))) { @field(passed, field.name) = true; - @field(options, field.name) = parseArg(field.type, args, format.flagName(field)); + @field(flags, field.name) = parseArg(field.type, args, format.flagName(field)); continue :next_arg; } @@ -109,19 +109,19 @@ fn parseOptions(args: *ArgIterator, comptime Options: type, comptime command_nam fatal("unrecognized flag: {s}", .{arg}); } - if (@hasDecl(Options, "switches")) { + if (@hasDecl(Flags, "switches")) { next_switch: for (arg[1..], 1..) |char, i| { - inline for (std.meta.fields(@TypeOf(Options.switches))) |switch_field| { - if (char == @field(Options.switches, switch_field.name)) { + inline for (std.meta.fields(@TypeOf(Flags.switches))) |switch_field| { + if (char == @field(Flags.switches, switch_field.name)) { @field(passed, switch_field.name) = true; - const FieldType = @TypeOf(@field(options, switch_field.name)); + const FieldType = @TypeOf(@field(flags, switch_field.name)); // Removing this check would allow formats like "-abc value-for-a value-for-b value-for-c" if (FieldType != bool and i != arg.len - 1) { fatal("expected argument after switch '{c}'", .{char}); } const value = parseArg(FieldType, args, &.{ '-', char }); - @field(options, switch_field.name) = value; + @field(flags, switch_field.name) = value; continue :next_switch; } @@ -133,13 +133,13 @@ fn parseOptions(args: *ArgIterator, comptime Options: type, comptime command_nam } } - inline for (std.meta.fields(Options)) |field| { + inline for (std.meta.fields(Flags)) |field| { if (@field(passed, field.name) == false) { if (field.default_value) |default_opaque| { const default = @as(*const field.type, @ptrCast(@alignCast(default_opaque))).*; - @field(options, field.name) = default; + @field(flags, field.name) = default; } else { - @field(options, field.name) = switch (@typeInfo(field.type)) { + @field(flags, field.name) = switch (@typeInfo(field.type)) { .Optional => null, .Bool => false, else => fatal("missing required flag: '{s}'", .{format.flagName(field)}), @@ -148,8 +148,8 @@ fn parseOptions(args: *ArgIterator, comptime Options: type, comptime command_nam } } - return Result(Options){ - .config = options, + return Result(Flags){ + .flags = flags, .args = positionals[0..positional_count], }; } diff --git a/src/root.zig b/src/root.zig index 57424ad..a4bd628 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,6 +1,7 @@ const std = @import("std"); const parser = @import("parser.zig"); const help = @import("help.zig"); + pub const parse = parser.parse; pub const fatal = parser.fatal; pub const helpMessage = help.helpMessage; diff --git a/src/validate.zig b/src/validate.zig index b2c4f32..4c09d8d 100644 --- a/src/validate.zig +++ b/src/validate.zig @@ -26,7 +26,7 @@ pub fn assertValid(comptime Command: type) void { fn assertValidGeneric(comptime Command: type) void { switch (@typeInfo(Command)) { - .Struct => assertValidOptions(Command), + .Struct => assertValidFlags(Command), .Union => assertValidCommands(Command), else => compileError("command must be a struct or union type", .{}), } @@ -41,29 +41,29 @@ fn assertValidCommands(comptime Commands: type) void { } } -fn assertValidOptions(comptime Options: type) void { - inline for (std.meta.fields(Options)) |field| { +fn assertValidFlags(comptime Flags: type) void { + inline for (std.meta.fields(Flags)) |field| { if (comptime std.mem.eql(u8, "help", field.name)) { - compileError("option name 'help' is reserved for showing usage", {}); + compileError("flag name 'help' is reserved for showing usage", {}); } switch (@typeInfo(field.type)) { // Allow bool values only outside of optionals .Bool => {}, - .Optional => |optional| assertValidOption(optional.child, field.name), - else => assertValidOption(field.type, field.name), + .Optional => |optional| assertValidFlag(optional.child, field.name), + else => assertValidFlag(field.type, field.name), } } - if (@hasDecl(Options, "switches")) { - assertValidSwitches(Options, Options.switches); + if (@hasDecl(Flags, "switches")) { + assertValidSwitches(Flags, Flags.switches); } - if (@hasDecl(Options, "descriptions")) { - assertValidDescriptions(Options, Options.descriptions); + if (@hasDecl(Flags, "descriptions")) { + assertValidDescriptions(Flags, Flags.descriptions); } } -fn assertValidSwitches(comptime Config: type, switches: anytype) void { +fn assertValidSwitches(comptime Flags: type, switches: anytype) void { const Switches = @TypeOf(switches); if (@typeInfo(Switches) != .Struct) { compileError("'switches' is not a struct declaration", .{}); @@ -71,12 +71,12 @@ fn assertValidSwitches(comptime Config: type, switches: anytype) void { const fields = std.meta.fields(Switches); inline for (fields, 0..) |field, i| { - if (!@hasField(Config, field.name)) compileError( - "switch name not defined in Config: '{s}'", + if (!@hasField(Flags, field.name)) compileError( + "switch name does not match any field: '{s}'", .{field.name}, ); - const swtch = @field(Config.switches, field.name); + const swtch = @field(Flags.switches, field.name); if (@TypeOf(swtch) != comptime_int) compileError( "switch is not a character: '{s}'", .{field.name}, @@ -105,25 +105,25 @@ fn assertValidSwitches(comptime Config: type, switches: anytype) void { } } -fn assertValidDescriptions(comptime Config: type, descriptions: anytype) void { +fn assertValidDescriptions(comptime Flags: type, descriptions: anytype) void { const Descriptions = @TypeOf(descriptions); if (@typeInfo(Descriptions) != .Struct) { compileError("'descriptions' is not a struct declaration", {}); } inline for (std.meta.fields(Descriptions)) |field| { - if (!@hasField(Config, field.name)) compileError( + if (!@hasField(Flags, field.name)) compileError( "description name does not match any field: '{s}'", .{field.name}, ); - const desc = @field(Config.descriptions, field.name); + const desc = @field(Flags.descriptions, field.name); if (comptime !isString(@TypeOf(desc))) { compileError("description is not a string: '{s}'", .{field.name}); } } } -fn assertValidOption(comptime T: type, comptime field_name: []const u8) void { +fn assertValidFlag(comptime T: type, comptime field_name: []const u8) void { if (T == []const u8) return; switch (@typeInfo(T)) { @@ -139,7 +139,7 @@ fn assertValidOption(comptime T: type, comptime field_name: []const u8) void { } compileError( - "bad Config field type '{s}': {s}", + "bad flag type '{s}': {s}", .{ field_name, @typeName(T) }, ); }