├── .gitignore ├── .vscode └── tasks.json ├── src ├── main.zig ├── utils.zig ├── tests.zig └── argparse.zig └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "run zig", 8 | "type": "shell", 9 | "command": "/usr/local/bin/zig build run", 10 | "group": "build", 11 | "presentation": { 12 | "clear": true 13 | } 14 | }, 15 | { 16 | "label": "test zig", 17 | "type": "shell", 18 | "command": "/usr/local/bin/zig test ${workspaceRoot}/src/tests.zig", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "presentation": { 24 | "clear": true 25 | } 26 | }, 27 | ] 28 | } -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | usingnamespace @import("argparse.zig"); 3 | 4 | const MyArgs = struct { 5 | foo: ?bool, 6 | 7 | weight: ?u32, 8 | height: f32, 9 | 10 | depth: f32, 11 | const depth__SHORT = "d"; 12 | const depth__DEFAULT = "100"; 13 | const depth__DOC = "Depth of the thing, in meters."; 14 | 15 | /// Age of the user in years. 16 | age: u32 = 100, 17 | // a: Alias("age"), 18 | 19 | name: []const u8 = "evan", 20 | 21 | values: []u32, // --values 1 2 3 22 | 23 | dim: [2]u32, 24 | }; 25 | 26 | pub fn main() anyerror!void { 27 | const parsed = parseArgs(MyArgs) catch { 28 | std.debug.warn("\nCould not parse args!!!\n"); 29 | return; 30 | }; 31 | 32 | std.debug.warn("\n-------------\n"); 33 | std.debug.warn("{}\n", parsed); 34 | for (parsed.values) |value| { 35 | std.debug.warn("{} ", value); 36 | } 37 | std.debug.warn("\n"); 38 | } 39 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const mem = std.mem; 4 | 5 | pub fn startsWith(s: []const u8, prefix: []const u8) bool { 6 | return s.len >= prefix.len and (mem.eql(u8, s[0..prefix.len], prefix)); 7 | } 8 | 9 | test "startsWith" { 10 | std.debug.assert(startsWith("hello", "he")); 11 | std.debug.assert(startsWith("hello", "hello")); 12 | std.debug.assert(!startsWith("hello", "nope")); 13 | std.debug.assert(startsWith("hello", "")); 14 | std.debug.assert(!startsWith("", "hi")); 15 | std.debug.assert(!startsWith("h", "hi")); 16 | std.debug.assert(!startsWith("ho", "hi")); 17 | } 18 | 19 | /// ("key=value", '=') -> {"key", "value"} 20 | pub fn splitAtFirst(s: []const u8, sep: u8) ?[2][]const u8 { 21 | for (s) |c, i| { 22 | if (c == sep) { 23 | return [2][]const u8{ s[0..i], s[i + 1 ..] }; 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | test "splitAtFirst" { 30 | { 31 | const vals = splitAtFirst("key=value", '='); 32 | std.debug.assert(vals != null); 33 | std.testing.expectEqualSlices(u8, vals.?[0], "key"); 34 | std.testing.expectEqualSlices(u8, vals.?[1], "value"); 35 | } 36 | 37 | { 38 | const vals = splitAtFirst("=value", '='); 39 | std.debug.assert(vals != null); 40 | std.testing.expectEqualSlices(u8, vals.?[0], ""); 41 | std.testing.expectEqualSlices(u8, vals.?[1], "value"); 42 | } 43 | { 44 | const vals = splitAtFirst("key=", '='); 45 | std.debug.assert(vals != null); 46 | std.testing.expectEqualSlices(u8, vals.?[0], "key"); 47 | std.testing.expectEqualSlices(u8, vals.?[1], ""); 48 | } 49 | } 50 | 51 | pub fn isArray(comptime T: type) ?builtin.TypeInfo.Array { 52 | const info = @typeInfo(T); 53 | return switch (info) { 54 | .Array => info.Array, 55 | else => null, 56 | }; 57 | } 58 | 59 | pub fn NonOptional(comptime T: type) type { 60 | comptime var info = @typeInfo(T); 61 | return switch (info) { 62 | .Optional => info.Optional.child, 63 | else => T, 64 | }; 65 | } 66 | 67 | pub fn isOptional(comptime T: type) bool { 68 | comptime var info = @typeInfo(T); 69 | return switch (info) { 70 | .Optional => true, 71 | else => false, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig argparse 2 | ## Easy declarative argument parsing in zig 3 | 4 | ```zig 5 | const parseArgs = @import("argparse.zig").parseArgs; 6 | 7 | // All you need to provide is a type defining what arguments you expect / support. 8 | // This struct is the result of parsing, so you can just use these values in your code 9 | const MyArgs = struct { 10 | // Any field of this struct is an argument. Optional args will be `null` if not provided by the user. 11 | name: ?[]const u8, 12 | 13 | // `--foo` sets this to `true`, otherwise it's set to `false` 14 | foo: ?bool, 15 | 16 | // required because type is not optional, parsing will fail if not provided. 17 | // This gets parsed as a number, with bounds checks. 18 | age: u32, 19 | }; 20 | 21 | pub fn main() void { 22 | const parsed = parseArgs(MyArgs) catch return; 23 | 24 | std.debug.warn("{}\n", parsed.age); 25 | if (parsed.name) |name| { 26 | std.debug.warn("{}\n", name); 27 | } 28 | } 29 | ``` 30 | 31 | Note that you can catch errors instead of just returning. You could `return -1` for example. 32 | 33 | If you want to support subcommands or otherwise preprocess arguments, you could use `parseArgsList` and provide a slice of strings. 34 | 35 | Use `parseArgsOpt` / `parseArgsListOpt` if you want to provide options like a custom allocator. 36 | 37 | 38 | # Features 39 | 40 | ## Parse with = or additional arguments 41 | `./a --foo 123` is the same as `./a --foo=123` 42 | 43 | ## Number bounds checks 44 | Since numbers are parsed with zig's std lib, they get bounds checked 45 | ```zig 46 | const MyArgs = struct { 47 | age: ?u32, 48 | small: ?i8, 49 | }; 50 | // ./a --number -42 51 | // Expected u32 for 'age', found '-42' 52 | 53 | // ./a --small 10000 54 | // Expected i8 for 'small', found '10000' 55 | ``` 56 | 57 | ## Float parsing when expected 58 | If you declare your arg as an int, it won't validate floats. 59 | If you declare your arg as a float, it will allow floats or integers. 60 | ```zig 61 | const MyArgs = struct { 62 | height: ?f32, 63 | age: ?u32, 64 | }; 65 | // ./a --height 1.85 66 | // ./a --age 1.85 Expected u32 for 'age', found '1.85' 67 | ``` 68 | 69 | ## Array types 70 | ```zig 71 | const MyArgs = struct { 72 | resolution: [2]u32, 73 | }; 74 | // ./a --resolution 1920 1080 75 | ``` 76 | Passing more or fewer values is an error, and values are typechecked. 77 | 78 | ## Slice types 79 | Slices are allocated for you and consume any number of arguments until an invalid one is found. Allocated using `options.allocator`. 80 | 81 | ```zig 82 | const MyArgs = struct { 83 | names: [][]const u8, 84 | values: []u32, 85 | }; 86 | // ./a --names Alice Bob Carol --values 1 100 87 | ``` 88 | 89 | 90 | # Future... 91 | 92 | ## Default values 93 | If [zig issue #2937](https://github.com/ziglang/zig/issues/2937) is done, 94 | arguments can provide default values as part of the struct! 95 | ```zig 96 | const MyArgs = struct { 97 | foo: i32 = 20, 98 | }; 99 | // foo is 20 if not provided, instead of null. Type doesn't need to be optional if a default is given. 100 | ``` 101 | 102 | Unimplemented Workaround idea: provide a constant declaration value 103 | that matches argument name 104 | ```zig 105 | const MyArgs = struct { 106 | foo: i32, 107 | const foo__DEFAULT = 33; 108 | }; 109 | ``` 110 | 111 | 112 | ## Documentation 113 | If [zig issue #2573](https://github.com/ziglang/zig/issues/2573) is done, 114 | doc strings can be shown when `--help` is used! 115 | ```zig 116 | const MyArgs = struct { 117 | /// This comment would show up if on `--help` or when used incorrectly 118 | foo: i32 = 20, 119 | }; 120 | ``` 121 | 122 | Unimplemented Workaround idea: provide a constant declaration string 123 | that matches argument name 124 | ```zig 125 | const MyArgs = struct { 126 | foo: i32, 127 | const foo__DOC = "This string would show in `--help`"; 128 | }; 129 | ``` 130 | 131 | ## Collect extra arguments 132 | (TODO) 133 | Any arguments not starting with `--` are errors right now, but these could be accumulated and put somewhere. There could be a special signal value: 134 | 135 | ```zig 136 | const MyArgs = struct { 137 | foo: bool, 138 | 139 | files: ExtraArguments, 140 | }; 141 | // ./a file1.txt file2.txt 142 | ``` 143 | Where `ExtraArguments` would be a type provided by the library, which is basically just an array of strings in some form. 144 | 145 | 146 | ## Positional arguments 147 | (TODO) 148 | Add a syntax to say that an argument is not specified by name, but is instead positional. A type wrapper seems like a nice way to do this. 149 | 150 | ```zig 151 | const MyArgs = struct { 152 | age: Positional(u32), 153 | height: Positional(f32), 154 | 155 | foo: bool, 156 | }; 157 | // ./a file1.txt file2.txt 158 | ``` 159 | 160 | ## Aliases 161 | (TODO) 162 | Add a syntax to say that an argument is an alias for another one, so you can provide duplicate functionality without thinking about it in your usage code. This would also let you define "short" versions of commands. 163 | 164 | This could be done as a zero-bit type wrapper or a static constant so that there is a canonical value to use in the usage code. 165 | 166 | ```zig 167 | const MyArgs = struct { 168 | file: []const u8, 169 | const fileName = Alias("file"); 170 | const f = Alias("file"); 171 | }; 172 | // All these are equivalent: 173 | // ./a --file foo.txt 174 | // ./a --fileName foo.txt 175 | // ./a -f foo.txt 176 | ``` 177 | 178 | ## Add more options 179 | Add some user parameters. 180 | - Should `--` be allowed at the start of value names? 181 | - Should `-` argument prefixes be treated like `--` (Windows often uses `-`, Linux/macOS `--`) 182 | - What function should be used for outputing information? (default: `std.debug.warn`) 183 | - What function/extra context should be used when printing `usage`? 184 | 185 | ## Repeatable arguments 186 | (TODO) 187 | Add a syntax to say that an argument can be repeated multiple or unlimited times 188 | 189 | ```zig 190 | const MyArgs = struct { 191 | file: []const u8, 192 | repeatedFile: RepeatedOnce("file"), 193 | }; 194 | // All these are equivalent: 195 | // ./a --file 1.txt 196 | // ./a --file 1.txt --file 2.txt 197 | // ./a --file 1.txt --file 2.txt --file 3.txt 'file' argument can only be provided twice 198 | ``` 199 | 200 | ```zig 201 | const MyArgs = struct { 202 | // Can be repeated any number of times, 203 | // ends up being accessible as a slice. 204 | id: Repeatable(u32), 205 | }; 206 | ``` -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | usingnamespace @import("argparse.zig"); 4 | usingnamespace std.testing; 5 | 6 | /// function that replaces std.debug.warn when testing 7 | pub fn logInsideTest(comptime fmt: []const u8, args: ...) void { 8 | const allocator = std.heap.c_allocator; 9 | var size: usize = 0; 10 | std.fmt.format(&size, error{}, countSize, fmt, args) catch |err| switch (err) {}; 11 | const buf = allocator.alloc(u8, size) catch return; 12 | const printed = std.fmt.bufPrint(buf, fmt, args) catch |err| switch (err) { 13 | error.BufferTooSmall => unreachable, // we just counted the size above 14 | }; 15 | 16 | std.debug.warn("\x1B[32m{}\x1B[0m", printed); 17 | } 18 | 19 | fn countSize(size: *usize, bytes: []const u8) (error{}!void) { 20 | size.* += bytes.len; 21 | } 22 | 23 | ///////////////////////////////////////////////////////////// 24 | 25 | test "argparse.optionals" { 26 | const result = try parseArgsList(struct { 27 | foo: ?u32, 28 | }, [_][]const u8{"./a"}); 29 | expectEqual(result.foo, null); 30 | } 31 | test "argparse.specifyOptional" { 32 | const result = try parseArgsList(struct { 33 | foo: ?u32, 34 | }, [_][]const u8{ "./a", "--foo", "42" }); 35 | std.debug.warn("{}\n", result); 36 | expectEqual(result.foo, 42); 37 | } 38 | 39 | test "argparse.missingRequired" { 40 | expectError(error.MissingRequiredArgument, parseArgsList(struct { 41 | required: u32, 42 | }, [_][]const u8{"./a"})); 43 | } 44 | 45 | test "argparse.string" { 46 | const result = try parseArgsList(struct { 47 | str: []const u8, 48 | }, [_][]const u8{ "./a", "--str", "Hello" }); 49 | expectEqualSlices(u8, result.str, "Hello"); 50 | } 51 | 52 | test "argparse.boolGiven" { 53 | const result = try parseArgsList(struct { 54 | foo: ?bool, 55 | }, [_][]const u8{ "./a", "--foo" }); 56 | expectEqual(result.foo, true); 57 | } 58 | 59 | test "argparse.boolDefault" { 60 | const result = try parseArgsList(struct { 61 | foo: ?bool, 62 | }, [_][]const u8{"./a"}); 63 | expectEqual(result.foo, false); 64 | } 65 | 66 | test "argparse.uintMin" { 67 | expectError(error.CouldNotParseInteger, parseArgsList(struct { 68 | foo: u8, 69 | }, [_][]const u8{ "./a", "--foo", "-42" })); 70 | } 71 | 72 | test "argparse.signed" { 73 | const result = try parseArgsList(struct { 74 | foo: i8, 75 | }, [_][]const u8{ "./a", "--foo", "-42" }); 76 | expectEqual(result.foo, -42); 77 | } 78 | 79 | test "argparse.uintMax" { 80 | expectError(error.CouldNotParseInteger, parseArgsList(struct { 81 | foo: u8, 82 | }, [_][]const u8{ "./a", "--foo", "256" })); 83 | } 84 | 85 | test "argparse.notInt" { 86 | expectError(error.CouldNotParseInteger, parseArgsList(struct { 87 | foo: u8, 88 | }, [_][]const u8{ "./a", "--foo", "hi" })); 89 | } 90 | 91 | test "argparse.floatInInt" { 92 | expectError(error.CouldNotParseInteger, parseArgsList(struct { 93 | foo: u32, 94 | }, [_][]const u8{ "./a", "--foo", "123.45" })); 95 | } 96 | 97 | test "argparse.nonFloat" { 98 | expectError(error.CouldNotParseFloat, parseArgsList(struct { 99 | foo: f32, 100 | }, [_][]const u8{ "./a", "--foo", "hi" })); 101 | } 102 | 103 | test "argparse.float" { 104 | const result = try parseArgsList(struct { 105 | foo: f64, 106 | }, [_][]const u8{ "./a", "--foo", "123.456" }); 107 | expectEqual(result.foo, 123.456); 108 | } 109 | 110 | test "argparse.key=value" { 111 | const result = try parseArgsList(struct { 112 | foo: u32, 113 | }, [_][]const u8{ "./a", "--foo=123" }); 114 | expectEqual(result.foo, 123); 115 | } 116 | 117 | test "argparse.expectedValueFoundArgument" { 118 | expectError(error.ExpectedArgument, parseArgsList(struct { 119 | foo: ?u32, 120 | bar: ?bool, 121 | }, [_][]const u8{ "./a", "--foo", "--bar" })); 122 | } 123 | 124 | test "argparse.array.u32" { 125 | const result = try parseArgsList(struct { 126 | dim: [2]u32, 127 | }, [_][]const u8{ "./a", "--dim", "2", "3" }); 128 | expectEqual(result.dim[0], 2); 129 | expectEqual(result.dim[1], 3); 130 | } 131 | 132 | test "argparse.array.string" { 133 | const result = try parseArgsList(struct { 134 | names: [3][]const u8, 135 | }, [_][]const u8{ "./a", "--names", "Alice", "Bob", "Carol" }); 136 | expectEqualSlices(u8, result.names[0], "Alice"); 137 | expectEqualSlices(u8, result.names[1], "Bob"); 138 | expectEqualSlices(u8, result.names[2], "Carol"); 139 | } 140 | 141 | test "argparse.array.notEnough" { 142 | expectError(error.NotEnoughArrayArguments, parseArgsList(struct { 143 | dim: [2]u32, 144 | }, [_][]const u8{ "./a", "--dim", "2" })); 145 | } 146 | 147 | test "argparse.array.tooMany" { 148 | expectError(error.UnexpectedArgument, parseArgsList(struct { 149 | dim: [2]u32, 150 | }, [_][]const u8{ "./a", "--dim", "2", "3", "4" })); 151 | } 152 | 153 | test "argparse.array.typeSafe" { 154 | expectError(error.CouldNotParseInteger, parseArgsList(struct { 155 | dim: [2]u32, 156 | }, [_][]const u8{ "./a", "--dim", "2", "blah" })); 157 | } 158 | 159 | test "argparse.slice.u32" { 160 | const result = try parseArgsList(struct { 161 | dim: []u32, 162 | }, [_][]const u8{ "./a", "--dim", "2", "3", "4" }); 163 | expectEqual(result.dim[0], 2); 164 | expectEqual(result.dim[1], 3); 165 | expectEqual(result.dim[2], 4); 166 | } 167 | 168 | test "argparse.slice.empty" { 169 | const result = try parseArgsList(struct { 170 | dim: []u32, 171 | }, [_][]const u8{ "./a", "--dim" }); 172 | expectEqual(result.dim.len, 0); 173 | } 174 | 175 | test "argparse.slice.untilNextArg" { 176 | const result = try parseArgsList(struct { 177 | dim: []u32, 178 | foo: u32, 179 | }, [_][]const u8{ "./a", "--dim", "2", "3", "--foo", "4" }); 180 | expectEqual(result.dim[0], 2); 181 | expectEqual(result.dim[1], 3); 182 | expectEqual(result.dim.len, 2); 183 | expectEqual(result.foo, 4); 184 | } 185 | 186 | test "argparse.slice.typeSafe" { 187 | expectError(error.UnexpectedArgument, parseArgsList(struct { 188 | dim: []u32, 189 | foo: u32, 190 | }, [_][]const u8{ "./a", "--dim", "2", "blah", "--foo", "4" })); 191 | expectError(error.UnexpectedArgument, parseArgsList(struct { 192 | dim: []u8, 193 | foo: u32, 194 | }, [_][]const u8{ "./a", "--dim", "2", "1000", "--foo", "4" })); 195 | } 196 | 197 | test "argparse.slice.optional" { 198 | const result = try parseArgsList(struct { 199 | dim: ?[]u32, 200 | }, [_][]const u8{ "./a", "--dim", "2", "3" }); 201 | expectEqual(result.dim.?[0], 2); 202 | expectEqual(result.dim.?[1], 3); 203 | expectEqual(result.dim.?.len, 2); 204 | } 205 | 206 | test "argparse.slice.strings" { 207 | const result = try parseArgsList(struct { 208 | words: [][]const u8, 209 | }, [_][]const u8{ "./a", "--words", "yo", "there" }); 210 | expectEqualSlices(u8, result.words[0], "yo"); 211 | expectEqualSlices(u8, result.words[1], "there"); 212 | expectEqual(result.words.len, 2); 213 | } 214 | -------------------------------------------------------------------------------- /src/argparse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const mem = std.mem; 4 | const logInsideTest = @import("tests.zig").logInsideTest; 5 | usingnamespace @import("utils.zig"); 6 | 7 | // getting default values depends on https://github.com/ziglang/zig/issues/2937 8 | // parsing doc strings depends on https://github.com/ziglang/zig/issues/2573 9 | 10 | // TODOs: 11 | // disallow passing the same arg twice 12 | // add a way to allow `--` at the start of values? 13 | // support positional arguments 14 | // support comma-separate array values? --arrayArg=1,2,3 or --arrayArg 1,2,3 15 | 16 | const log = if (builtin.is_test) logInsideTest else std.debug.warn; 17 | 18 | // Use this in the struct as a value that's specifically ignored by the parser 19 | // and gets filled with all positional arguments 20 | pub const PositionalArguments = [][]const u8; 21 | 22 | const AliasEntry = struct { 23 | fieldName: []const u8, 24 | }; 25 | pub fn Alias(comptime fieldName: []const u8) AliasEntry { 26 | return AliasEntry{ .fieldName = fieldName }; 27 | } 28 | const ArgParseError = error{ 29 | CalledWithoutAnyArguments, 30 | InvalidArgs, 31 | MissingRequiredArgument, 32 | ExpectedArgument, 33 | CouldNotParseInteger, 34 | CouldNotParseFloat, 35 | UnexpectedArgument, 36 | NotEnoughArrayArguments, 37 | OutOfMemory, 38 | }; 39 | 40 | const ArgParseOptions = struct { 41 | allocator: *std.mem.Allocator = std.heap.c_allocator, 42 | }; 43 | 44 | /// Parse process's command line arguments subject to the passed struct's format. 45 | pub fn parseArgs(comptime T: type) !T { 46 | return parseArgsOpt(T, ArgParseOptions{}); 47 | } 48 | 49 | /// Parse process's command line arguments subject to the passed struct's format and parsing options 50 | pub fn parseArgsOpt(comptime T: type, options: ArgParseOptions) !T { 51 | const args = try std.process.argsAlloc(options.allocator); 52 | return parseArgsListOpt(T, args, options); 53 | } 54 | 55 | /// Parse arbitrary string arguments subject to the passed struct's format. 56 | pub fn parseArgsList(comptime T: type, args: []const []const u8) ArgParseError!T { 57 | return parseArgsListOpt(T, args, ArgParseOptions{}); 58 | } 59 | 60 | fn Context(comptime T: type) type { 61 | return struct { 62 | args: []const []const u8, 63 | arg_i: usize, 64 | result: T, 65 | silent: bool = false, 66 | }; 67 | } 68 | 69 | /// Parse arbitrary string arguments subject to the passed struct's format and parsing options. 70 | pub fn parseArgsListOpt(comptime T: type, args: []const []const u8, options: ArgParseOptions) ArgParseError!T { 71 | if (args.len < 1) { 72 | log("\nFirst argument should be the program name\n"); 73 | return error.CalledWithoutAnyArguments; 74 | } 75 | const info = @typeInfo(T).Struct; 76 | 77 | var ctx = Context(T){ 78 | .args = args, 79 | .arg_i = 1, // skip program name 80 | .result = undefined, 81 | }; 82 | 83 | // set all optional fields to null 84 | inline for (info.fields) |field| { 85 | const fieldInfo = @typeInfo(field.field_type); 86 | switch (fieldInfo) { 87 | .Optional => { 88 | if (fieldInfo.Optional.child == bool) { 89 | // optional bools are just false 90 | @field(ctx.result, field.name) = false; 91 | } else { 92 | @field(ctx.result, field.name) = null; 93 | } 94 | break; 95 | }, 96 | else => continue, 97 | } 98 | } 99 | 100 | // collect required arguments 101 | var requiredArgs: [info.fields.len](?[]const u8) = undefined; 102 | inline for (info.fields) |field, field_i| { 103 | switch (@typeInfo(field.field_type)) { 104 | .Optional => continue, 105 | else => { 106 | requiredArgs[field_i] = field.name; 107 | }, 108 | } 109 | } 110 | 111 | while (ctx.arg_i < args.len) : (ctx.arg_i += 1) { 112 | const arg = args[ctx.arg_i]; 113 | if (mem.eql(u8, arg, "--help")) { 114 | usage(T, args); 115 | return error.InvalidArgs; 116 | } 117 | 118 | if (startsWith(arg, "--")) { 119 | inline for (info.fields) |field| { 120 | const name = field.name; 121 | var argName = arg[2..]; 122 | var next: ?[]const u8 = null; 123 | if (splitAtFirst(argName, '=')) |parts| { 124 | argName = parts[0]; 125 | next = parts[1]; 126 | } 127 | if (mem.eql(u8, argName, name)) { 128 | const typeInfo = @typeInfo(NonOptional(field.field_type)); 129 | switch (typeInfo) { 130 | .Bool => { 131 | @field(ctx.result, name) = true; 132 | }, 133 | .Array => |arrType| { 134 | // TODO: split next arg on ','? 135 | const len = arrType.len; 136 | 137 | if (ctx.arg_i + len >= ctx.args.len) { 138 | usage(T, ctx.args); 139 | log("\nMust provide {} values for array argument '{}'\n", usize(len), name); 140 | return error.NotEnoughArrayArguments; 141 | } 142 | 143 | var array_i: usize = 0; 144 | while (array_i < len) : (array_i += 1) { 145 | ctx.arg_i += 1; 146 | const value = ctx.args[ctx.arg_i]; 147 | @field(ctx.result, field.name)[array_i] = try parseValueForField(T, &ctx, field.name, arrType.child, value); 148 | } 149 | }, 150 | .Pointer => |pointerType| { 151 | // TODO: split next arg on ','? 152 | if (builtin.TypeInfo.Pointer.Size(pointerType.size) == .One) { 153 | @compileError("Pointers are not supported as argument types."); 154 | } 155 | var gotString = false; 156 | if (pointerType.is_const) { 157 | if (pointerType.child == u8) { 158 | var value = if (next) |nonNullNext| nonNullNext else blk: { 159 | ctx.arg_i += 1; 160 | if (ctx.arg_i >= ctx.args.len) { 161 | usage(T, ctx.args); 162 | return error.ExpectedArgument; 163 | } 164 | break :blk ctx.args[ctx.arg_i]; 165 | }; 166 | 167 | @field(ctx.result, field.name) = try parseValueForField(T, &ctx, field.name, field.field_type, value); 168 | gotString = true; 169 | } else { 170 | @compileError("non-string slices must not be const: " ++ @typeName(T) ++ "." ++ field.name ++ " is " ++ @typeName(field.field_type)); 171 | } 172 | } else { 173 | // count number of valid next args 174 | var countingCtx: Context(T) = ctx; // copy context to count in 175 | countingCtx.silent = true; 176 | const starting_arg_i = ctx.arg_i + 1; 177 | var sliceCount: usize = 0; 178 | while (true) : (sliceCount += 1) { 179 | if (starting_arg_i + sliceCount >= ctx.args.len) { 180 | break; 181 | } 182 | const value = ctx.args[starting_arg_i + sliceCount]; 183 | _ = parseValueForField(T, &countingCtx, field.name, pointerType.child, value) catch { 184 | break; 185 | }; 186 | } 187 | 188 | @field(ctx.result, field.name) = try options.allocator.alloc(pointerType.child, sliceCount); 189 | 190 | var slice = comptime if (isOptional(field.field_type)) 191 | @field(ctx.result, field.name).? 192 | else 193 | @field(ctx.result, field.name); 194 | 195 | var array_i: usize = 0; 196 | while (array_i < sliceCount) : (array_i += 1) { 197 | ctx.arg_i += 1; 198 | const value = ctx.args[ctx.arg_i]; 199 | slice[array_i] = try parseValueForField(T, &ctx, field.name, pointerType.child, value); 200 | } 201 | } 202 | }, 203 | else => { 204 | var value = if (next) |nonNullNext| nonNullNext else blk: { 205 | ctx.arg_i += 1; 206 | if (ctx.arg_i >= ctx.args.len) { 207 | usage(T, ctx.args); 208 | return error.ExpectedArgument; 209 | } 210 | break :blk ctx.args[ctx.arg_i]; 211 | }; 212 | 213 | @field(ctx.result, field.name) = try parseValueForField(T, &ctx, field.name, field.field_type, value); 214 | }, 215 | } 216 | 217 | markArgAsFound(requiredArgs.len, &requiredArgs, name); 218 | break; 219 | } 220 | } 221 | } else { 222 | // TODO: Support positional args 223 | usage(T, args); 224 | log("\nUnexpected argument '{}'\n", arg); 225 | return error.UnexpectedArgument; 226 | } 227 | } 228 | 229 | for (requiredArgs) |req_arg| { 230 | if (req_arg) |rarg| { 231 | usage(T, args); 232 | log("\nMissing required argument '{}'\n", rarg); 233 | return error.MissingRequiredArgument; 234 | } 235 | } 236 | 237 | return ctx.result; 238 | } 239 | 240 | fn usage(comptime T: type, args: []const []const u8) void { 241 | const info = @typeInfo(T).Struct; 242 | log("Usage: {}\n", args[0]); 243 | inline for (info.fields) |field| { 244 | const name = field.name; 245 | log("--{}", name); 246 | if (field.field_type != ?bool) { 247 | log("=({})", @typeName(field.field_type)); 248 | } 249 | log("\n"); 250 | } 251 | } 252 | 253 | fn markArgAsFound(comptime n: usize, requiredArgs: *[n](?[]const u8), name: []const u8) void { 254 | for (requiredArgs) |*reqArg| { 255 | if (reqArg.*) |reqArgName| { 256 | if (mem.eql(u8, reqArgName, name)) { 257 | reqArg.* = null; 258 | } 259 | } 260 | } 261 | } 262 | 263 | fn parseValueForField( 264 | comptime T: type, 265 | ctx: *Context(T), 266 | comptime name: []const u8, 267 | comptime FieldType: type, 268 | value: []const u8, 269 | ) ArgParseError!NonOptional(FieldType) { 270 | comptime const FT = NonOptional(FieldType); 271 | if (startsWith(value, "--")) { 272 | if (!ctx.silent) { 273 | usage(T, ctx.args); 274 | log("\nExpected value for argument '{}', found argument '{}'\n", name, value); 275 | } 276 | return error.ExpectedArgument; 277 | } 278 | switch (FT) { 279 | u8, u16, u32, u64, i8, i16, i32, i64, usize => { 280 | return std.fmt.parseInt(FT, value, 10) catch |e| { 281 | if (!ctx.silent) { 282 | usage(T, ctx.args); 283 | log("\nExpected {} for '{}', found '{}'\n", @typeName(FT), name, value); 284 | } 285 | return error.CouldNotParseInteger; 286 | }; 287 | }, 288 | f32, f64 => { 289 | return std.fmt.parseFloat(FT, value) catch |e| { 290 | if (!ctx.silent) { 291 | usage(T, ctx.args); 292 | log("\nExpected {} for '{}', found '{}'\n", @typeName(FT), name, value); 293 | } 294 | return error.CouldNotParseFloat; 295 | }; 296 | }, 297 | []const u8 => { 298 | return value; 299 | }, 300 | else => unreachable, 301 | } 302 | } 303 | --------------------------------------------------------------------------------