Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Required arguments #30

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from 2 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
282 changes: 208 additions & 74 deletions args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ pub fn parseWithVerb(comptime Generic: type, comptime Verb: type, args_iterator:
fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterator: anytype, allocator: std.mem.Allocator, error_handling: ErrorHandling) !ParseArgsResult(Generic, MaybeVerb) {
var result = ParseArgsResult(Generic, MaybeVerb){
.arena = std.heap.ArenaAllocator.init(allocator),
.options = Generic{},
.verb = if (MaybeVerb != null) null else {}, // no verb by default
.positionals = undefined,
.executable_name = null,
Expand All @@ -98,7 +97,22 @@ fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterato

var last_error: ?anyerror = null;

while (args_iterator.next()) |item| {
// Create map for required arguments
var required_map = std.StringHashMap(bool).init(allocator);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you replace the StringHashMap with a std.EnumSet? As we know all possible fields, we can use std.meta.FieldEnum instead of runtime allocation

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having some trouble using the EnumSet. How do you get a proper enum to insert in this set from a field? Also, how would this work with the required items for the tagged enum of the Verb set, do we need a set per verb?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you get a proper enum to insert in this set from a field?

const Fields = std.meta.FieldEnum(Type);

const field_member = @field(Fields, field.name);

set.insert(field_member);

Also, how would this work with the required items for the tagged enum of the Verb set, do we need a set per verb?

Yes, but that would not hurt much, as we have to process the verb separately anyways. As we only process more errors when mandatory verb parameters are missing, this should not be much of a problem

defer required_map.deinit();

// Add the generic arguments
// and init defaults
inline for (std.meta.fields(Generic)) |field| {
if (field.default_value) |default_value_ptr| {
const default_value = @ptrCast(*const field.field_type, default_value_ptr).*;
@field(result.options, field.name) = default_value;
} else {
try required_map.put(field.name, true);
}
}

args_loop: while (args_iterator.next()) |item| {
if (std.mem.startsWith(u8, item, "--")) {
if (std.mem.eql(u8, item, "--")) {
// double hyphen is considered 'everything from here now is positional'
Expand All @@ -122,48 +136,45 @@ fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterato
.value = null,
};

var found = false;
inline for (std.meta.fields(Generic)) |fld| {
if (std.mem.eql(u8, pair.name, fld.name)) {
try parseOption(Generic, result_arena_allocator, &result.options, args_iterator, error_handling, &last_error, fld.name, pair.value);
found = true;
_ = required_map.remove(fld.name);
continue :args_loop;
}
}

if (MaybeVerb) |Verb| {
if (result.verb) |*verb| {
if (!found) {
const Tag = std.meta.Tag(Verb);
inline for (std.meta.fields(Verb)) |verb_info| {
if (verb.* == @field(Tag, verb_info.name)) {
inline for (std.meta.fields(verb_info.field_type)) |fld| {
if (std.mem.eql(u8, pair.name, fld.name)) {
try parseOption(
verb_info.field_type,
result_arena_allocator,
&@field(verb.*, verb_info.name),
args_iterator,
error_handling,
&last_error,
fld.name,
pair.value,
);
found = true;
}
const Tag = std.meta.Tag(Verb);
inline for (std.meta.fields(Verb)) |verb_info| {
if (verb.* == @field(Tag, verb_info.name)) {
inline for (std.meta.fields(verb_info.field_type)) |fld| {
if (std.mem.eql(u8, pair.name, fld.name)) {
try parseOption(
verb_info.field_type,
result_arena_allocator,
&@field(verb.*, verb_info.name),
args_iterator,
error_handling,
&last_error,
fld.name,
pair.value,
);
_ = required_map.remove(fld.name);
continue :args_loop;
}
}
}
}
}
}

if (!found) {
last_error = error.EncounteredUnknownArgument;
try error_handling.process(error.EncounteredUnknownArgument, Error{
.option = pair.name,
.kind = .unknown,
});
}
last_error = error.EncounteredUnknownArgument;
try error_handling.process(error.EncounteredUnknownArgument, Error{
.option = pair.name,
.kind = .unknown,
});
} else if (std.mem.startsWith(u8, item, "-")) {
if (std.mem.eql(u8, item, "-")) {
// single hyphen is considered a positional argument
Expand All @@ -172,7 +183,6 @@ fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterato
var any_shorthands = false;
for (item[1..]) |char, index| {
var option_name = [2]u8{ '-', char };
var found = false;
if (@hasDecl(Generic, "shorthands")) {
any_shorthands = true;
inline for (std.meta.fields(@TypeOf(Generic.shorthands))) |fld| {
Expand All @@ -191,57 +201,55 @@ fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterato
});
} else {
try parseOption(Generic, result_arena_allocator, &result.options, args_iterator, error_handling, &last_error, real_name, null);
_ = required_map.remove(real_name);
}

found = true;
continue :args_loop;
}
}
}

if (MaybeVerb) |Verb| {
if (result.verb) |*verb| {
if (!found) {
const Tag = std.meta.Tag(Verb);
inline for (std.meta.fields(Verb)) |verb_info| {
const VerbType = verb_info.field_type;
if (verb.* == @field(Tag, verb_info.name)) {
const target_value = &@field(verb.*, verb_info.name);
if (@hasDecl(VerbType, "shorthands")) {
any_shorthands = true;
inline for (std.meta.fields(@TypeOf(VerbType.shorthands))) |fld| {
if (fld.name.len != 1)
@compileError("All shorthand fields must be exactly one character long!");
if (fld.name[0] == char) {
const real_name = @field(VerbType.shorthands, fld.name);
const real_fld_type = @TypeOf(@field(target_value.*, real_name));

// -2 because we stripped of the "-" at the beginning
if (requiresArg(real_fld_type) and index != item.len - 2) {
last_error = error.EncounteredUnexpectedArgument;
try error_handling.process(error.EncounteredUnexpectedArgument, Error{
.option = &option_name,
.kind = .invalid_placement,
});
} else {
try parseOption(VerbType, result_arena_allocator, target_value, args_iterator, error_handling, &last_error, real_name, null);
}
last_error = null; // we need to reset that error here, as it was set previously
found = true;
const Tag = std.meta.Tag(Verb);
inline for (std.meta.fields(Verb)) |verb_info| {
const VerbType = verb_info.field_type;
if (verb.* == @field(Tag, verb_info.name)) {
const target_value = &@field(verb.*, verb_info.name);
if (@hasDecl(VerbType, "shorthands")) {
any_shorthands = true;
inline for (std.meta.fields(@TypeOf(VerbType.shorthands))) |fld| {
if (fld.name.len != 1)
@compileError("All shorthand fields must be exactly one character long!");
if (fld.name[0] == char) {
const real_name = @field(VerbType.shorthands, fld.name);
const real_fld_type = @TypeOf(@field(target_value.*, real_name));

// -2 because we stripped of the "-" at the beginning
if (requiresArg(real_fld_type) and index != item.len - 2) {
last_error = error.EncounteredUnexpectedArgument;
try error_handling.process(error.EncounteredUnexpectedArgument, Error{
.option = &option_name,
.kind = .invalid_placement,
});
} else {
try parseOption(VerbType, result_arena_allocator, target_value, args_iterator, error_handling, &last_error, real_name, null);
_ = required_map.remove(real_name);
}
last_error = null; // we need to reset that error here, as it was set previously
continue :args_loop;
}
}
}
}
}
}
}
if (!found) {
last_error = error.EncounteredUnknownArgument;
try error_handling.process(error.EncounteredUnknownArgument, Error{
.option = &option_name,
.kind = .unknown,
});
}
last_error = error.EncounteredUnknownArgument;
try error_handling.process(error.EncounteredUnknownArgument, Error{
.option = &option_name,
.kind = .unknown,
});
}
if (!any_shorthands) {
try error_handling.process(error.EncounteredUnsupportedArgument, Error{
Expand All @@ -256,25 +264,46 @@ fn parseInternal(comptime Generic: type, comptime MaybeVerb: ?type, args_iterato
inline for (std.meta.fields(Verb)) |fld| {
if (std.mem.eql(u8, item, fld.name)) {
// found active verb, default-initialize it
result.verb = @unionInit(Verb, fld.name, fld.field_type{});
result.verb = @unionInit(Verb, fld.name, undefined);
const target_value = &@field(result.verb.?, fld.name);

const VerbType = fld.field_type;
inline for (std.meta.fields(VerbType)) |field| {
if (field.default_value) |default_value_ptr| {
const default_value = @ptrCast(*const field.field_type, default_value_ptr).*;
@field(target_value, field.name) = default_value;
} else {
try required_map.put(field.name, true);
}
}

continue :args_loop;
}
}

if (result.verb == null) {
try error_handling.process(error.EncounteredUnknownVerb, Error{
.option = "verb",
.kind = .unsupported,
});
}
try error_handling.process(error.EncounteredUnknownVerb, Error{
.option = "verb",
.kind = .unsupported,
});

continue;
}
}

// Argument doesn't match anything, so should be a argument to the program itself
try arglist.append(try result_arena_allocator.dupeZ(u8, item));
}
}

if (required_map.count() > 0) {
last_error = error.MissingRequiredArgument;
try error_handling.process(error.MissingRequiredArgument, Error{
.option = "",
.kind = .unknown,
});
return error.MissingRequiredArgument;
}

if (last_error != null)
return error.InvalidArguments;

Expand Down Expand Up @@ -309,7 +338,7 @@ pub fn ParseArgsResult(comptime Generic: type, comptime MaybeVerb: ?type) type {
arena: std.heap.ArenaAllocator,

/// The options with either default or set values.
options: Generic,
options: Generic = undefined,

/// The verb that was parsed or `null` if no first positional was provided.
/// Is `void` when verb parsing is disabled
Expand Down Expand Up @@ -727,6 +756,22 @@ const TestVerb = union(enum) {
};
};

const TestVerbRequired = union(enum) {
magic: MagicOptions,
booze: BoozeOptions,

const MagicOptions = struct { invoke: bool = false };
const BoozeOptions = struct {
cocktail: bool = false,
longdrink: bool,

pub const shorthands = .{
.c = "cocktail",
.l = "longdrink",
};
};
};

test "basic parsing (no verbs)" {
var titerator = TestIterator.init(&[_][:0]const u8{
"--output",
Expand Down Expand Up @@ -801,7 +846,7 @@ test "shorthand parsing (no verbs)" {

test "basic parsing (with verbs)" {
var titerator = TestIterator.init(&[_][:0]const u8{
"--output", // non-verb options can come before or after verb
"--output", // non-verb options can come before or after verb
"foobar",
"booze", // verb
"--with-offset",
Expand Down Expand Up @@ -889,6 +934,58 @@ test "shorthand parsing (with verbs)" {
try std.testing.expectEqual(false, booze.longdrink);
}

test "shorthand parsing (with verbs and required)" {
var titerator_magic = TestIterator.init(&[_][:0]const u8{
"magic", // verb
});

var titerator_short = TestIterator.init(&[_][:0]const u8{
"booze", // verb
"-c", // --cocktail
"-l",
});
var titerator_long = TestIterator.init(&[_][:0]const u8{
"booze", // verb
"-l", // --longdring
});

var titerator_missing = TestIterator.init(&[_][:0]const u8{
"booze", // verb
"-c", // --cocktail
});

{
var args = try parseInternal(TestGenericOptions, TestVerbRequired, &titerator_magic, std.testing.allocator, .print);
defer args.deinit();
try std.testing.expect(?TestVerbRequired == @TypeOf(args.verb));
try std.testing.expect(args.verb.? == .magic);
}

{
var args = try parseInternal(TestGenericOptions, TestVerbRequired, &titerator_short, std.testing.allocator, .print);
defer args.deinit();
try std.testing.expect(?TestVerbRequired == @TypeOf(args.verb));
try std.testing.expect(args.verb.? == .booze);
const booze = args.verb.?.booze;
try std.testing.expectEqual(true, booze.cocktail);
try std.testing.expectEqual(true, booze.longdrink);
}

{
var args = try parseInternal(TestGenericOptions, TestVerbRequired, &titerator_long, std.testing.allocator, .print);
defer args.deinit();
try std.testing.expect(?TestVerbRequired == @TypeOf(args.verb));
try std.testing.expect(args.verb.? == .booze);
const booze = args.verb.?.booze;
try std.testing.expectEqual(false, booze.cocktail);
try std.testing.expectEqual(true, booze.longdrink);
}

{
try std.testing.expectError(error.MissingRequiredArgument, parseInternal(TestGenericOptions, TestVerbRequired, &titerator_missing, std.testing.allocator, .print));
}
}

test "strings with sentinel" {
var titerator = TestIterator.init(&[_][:0]const u8{
"--output",
Expand Down Expand Up @@ -944,3 +1041,40 @@ test "index of raw indicator --" {
try std.testing.expectEqual(args.raw_start_index, 2);
try std.testing.expectEqual(args.positionals.len, 5);
}

test "required argument --" {
var titerator = TestIterator.init(&[_][:0]const u8{
"--output",
"foobar",
});
{
var args = try parseInternal(
struct {
output: ?[:0]const u8 = "test",
},
null,
&titerator,
std.testing.allocator,
.print,
);
defer args.deinit();

try std.testing.expectEqualStrings("foobar", args.options.output.?);
}

{
var titerator_none = TestIterator.init(&[_][:0]const u8{});
var args = try parseInternal(
struct {
output: ?[:0]const u8 = "test",
},
null,
&titerator_none,
std.testing.allocator,
.print,
);
defer args.deinit();

try std.testing.expectEqualStrings("test", args.options.output.?);
}
}