summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jimmi Holst Christensen2018-03-14 14:15:24 +0100
committerGravatar Jimmi Holst Christensen2018-03-14 14:15:24 +0100
commit33014cb6d8f35771ea9d2c7ab605452ee1c5997f (patch)
treeb0fffe88f396a65872e1facef9c9ec73347f66d2
parentInitial commit (diff)
downloadzig-clap-33014cb6d8f35771ea9d2c7ab605452ee1c5997f.tar.gz
zig-clap-33014cb6d8f35771ea9d2c7ab605452ee1c5997f.tar.xz
zig-clap-33014cb6d8f35771ea9d2c7ab605452ee1c5997f.zip
Added command line argument parser.
* Zig crashes when trying to compile this code
-rw-r--r--.gitignore1
-rw-r--r--bits.zig9
-rw-r--r--clap.zig332
3 files changed, 342 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2040c29
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
zig-cache
diff --git a/bits.zig b/bits.zig
new file mode 100644
index 0000000..64f7d8b
--- /dev/null
+++ b/bits.zig
@@ -0,0 +1,9 @@
1const math = @import("std").math;
2
3pub fn set(comptime Int: type, num: Int, bit: math.Log2Int(Int), value: bool) Int {
4 return (num & ~(Int(1) << bit)) | (Int(value) << bit);
5}
6
7pub fn get(comptime Int: type, num: Int, bit: math.Log2Int(Int)) bool {
8 return ((num >> bit) & 1) != 0;
9}
diff --git a/clap.zig b/clap.zig
new file mode 100644
index 0000000..c9a45d3
--- /dev/null
+++ b/clap.zig
@@ -0,0 +1,332 @@
1const std = @import("std");
2const bits = @import("bits.zig");
3
4const mem = std.mem;
5const fmt = std.fmt;
6const debug = std.debug;
7const io = std.io;
8
9const assert = debug.assert;
10
11// TODO: Missing a few convinient features
12// * Short arguments that doesn't take values should probably be able to be
13// chain like many linux programs: "rm -rf"
14// * Handle "--something=VALUE"
15pub fn Option(comptime Result: type, comptime ParseError: type) type {
16 return struct {
17 const Self = this;
18
19 pub const Kind = enum {
20 Optional,
21 Required,
22 IgnoresRequired
23 };
24
25 parser: fn(&Result, []const u8) ParseError!void,
26 help: []const u8,
27 kind: Kind,
28 takes_value: bool,
29 short: ?u8,
30 long: ?[]const u8,
31
32 pub fn init(parser: fn(&Result, []const u8) ParseError!void) Self {
33 return Self {
34 .parser = parser,
35 .help = "",
36 .kind = Kind.Optional,
37 .takes_value = false,
38 .short = null,
39 .long = null,
40 };
41 }
42
43 pub fn setHelp(option: &const Self, help_str: []const u8) Self {
44 var res = *option; res.help = help_str;
45 return res;
46 }
47
48 pub fn setKind(option: &const Self, kind: Kind) Self {
49 var res = *option; res.kind = kind;
50 return res;
51 }
52
53 pub fn takesValue(option: &const Self, takes_value: bool) Self {
54 var res = *option; res.takes_value = takes_value;
55 return res;
56 }
57
58 pub fn setShort(option: &const Self, short: u8) Self {
59 var res = *option; res.short = short;
60 return res;
61 }
62
63 pub fn setLong(option: &const Self, long: []const u8) Self {
64 var res = *option; res.long = long;
65 return res;
66 }
67 };
68}
69
70pub fn Parser(comptime Result: type, comptime ParseError: type, comptime defaults: &const Result,
71 comptime options: []const Option(Result, ParseError)) type {
72
73 const OptionT = Option(Result, ParseError);
74 const Arg = struct {
75 const Kind = enum { Long, Short, None };
76
77 arg: []const u8,
78 kind: Kind
79 };
80
81 // NOTE: For now, a bitfield is used to keep track of the required arguments.
82 // This limits the user to 128 required arguments, which is more than
83 // enough.
84 const required_mask = comptime blk: {
85 var required_res : u128 = 0;
86 for (options) |option, i| {
87 if (option.kind == OptionT.Kind.Required) {
88 required_res = (required_res << 1) | 0x1;
89 }
90 }
91
92 break :blk required_res;
93 };
94
95 return struct {
96 fn parse(args: []const []const u8) !Result {
97 var result = *defaults;
98 var required = required_mask;
99
100 var arg_i = usize(0);
101 loop: while (arg_i < args.len) : (arg_i += 1) {
102 const pair = blk: {
103 const tmp = args[arg_i];
104 if (mem.startsWith(u8, tmp, "--"))
105 break :blk Arg { .arg = tmp[2..], .kind = Arg.Kind.Long };
106 if (mem.startsWith(u8, tmp, "-"))
107 break :blk Arg { .arg = tmp[1..], .kind = Arg.Kind.Short };
108
109 break :blk Arg { .arg = tmp, .kind = Arg.Kind.None };
110 };
111 const arg = pair.arg;
112 const kind = pair.kind;
113
114 comptime var required_index : usize = 0;
115 inline_loop: inline for (options) |option, op_i| {
116
117 switch (kind) {
118 Arg.Kind.None => {
119 if (option.short != null) continue :inline_loop;
120 if (option.long != null) continue :inline_loop;
121
122 try option.parser(&result, arg);
123
124 switch (option.kind) {
125 OptionT.Kind.Required => {
126 required = bits.set(u128, required, u7(required_index), false);
127 required_index += 1;
128 },
129 OptionT.Kind.IgnoresRequired => {
130 required = 0;
131 required_index += 1;
132 },
133 else => {}
134 }
135
136 continue :loop;
137 },
138 Arg.Kind.Short => {
139 const short = option.short ?? continue :inline_loop;
140 if (arg.len != 1 or arg[0] != short) continue :inline_loop;
141 },
142 Arg.Kind.Long => {
143 const long = option.long ?? continue :inline_loop;
144 if (!mem.eql(u8, long, arg)) continue :inline_loop;
145 }
146 }
147
148 if (option.takes_value) arg_i += 1;
149 if (args.len <= arg_i) return error.MissingValueToArgument;
150 try option.parser(&result, args[arg_i]);
151
152 switch (option.kind) {
153 OptionT.Kind.Required => {
154 required = bits.set(u128, required, u7(required_index), false);
155 required_index += 1;
156 },
157 OptionT.Kind.IgnoresRequired => {
158 required = 0;
159 required_index += 1;
160 },
161 else => {}
162 }
163
164 continue :loop;
165 } else {
166 return error.InvalidArgument;
167 }
168 }
169
170 if (required != 0) {
171 return error.RequiredArgumentWasntHandled;
172 }
173
174 return result;
175 }
176
177 // TODO:
178 // * Usage
179 // * Description
180 pub fn help(out_stream: var) !void {
181 const equal_value : []const u8 = "=OPTION";
182 const longest_long = comptime blk: {
183 var res = usize(0);
184 for (options) |option| {
185 const long = option.long ?? continue;
186 var len = long.len;
187
188 if (option.takes_value)
189 len += equal_value.len;
190
191 if (res < len)
192 res = len;
193 }
194
195 break :blk res;
196 };
197
198 inline for (options) |option| {
199 if (option.short == null and option.long == null) continue;
200
201 try out_stream.print(" ");
202 if (option.short) |short| {
203 try out_stream.print("-{c}", short);
204 } else {
205 try out_stream.print(" ");
206 }
207
208 if (option.short != null and option.long != null) {
209 try out_stream.print(", ");
210 } else {
211 try out_stream.print(" ");
212 }
213
214 // We need to ident by:
215 // "--<longest_long> ".len
216 const missing_spaces = comptime blk: {
217 var res = longest_long + 3;
218 if (option.long) |long| {
219 res -= 2 + long.len;
220
221 if (option.takes_value) {
222 res -= equal_value.len;
223 }
224 }
225
226 break :blk res;
227 };
228
229 if (option.long) |long| {
230 try out_stream.print("--{}", long);
231
232 if (option.takes_value) {
233 try out_stream.print("{}", equal_value);
234 }
235 }
236
237 try out_stream.print(" " ** missing_spaces);
238 try out_stream.print("{}\n", option.help_message);
239 }
240 }
241 };
242}
243
244test "clap.parse.Example" {
245 const Color = struct {
246 const Self = this;
247
248 r: u8, g: u8, b: u8,
249
250 fn rFromStr(color: &Self, str: []const u8) !void {
251 color.r = try fmt.parseInt(u8, str, 10);
252 }
253
254 fn gFromStr(color: &Self, str: []const u8) !void {
255 color.g = try fmt.parseInt(u8, str, 10);
256 }
257
258 fn bFromStr(color: &Self, str: []const u8) !void {
259 color.b = try fmt.parseInt(u8, str, 10);
260 }
261 };
262
263 const COption = Option(Color, @typeOf(Color.rFromStr).ReturnType.ErrorSet);
264 const options = comptime []COption {
265 COption.init(Color.rFromStr)
266 .setHelp("The amount of red in our color")
267 .setShort('r')
268 .setLong("red")
269 .takesValue(true)
270 .setKind(COption.Kind.Required),
271 COption.init(Color.gFromStr)
272 .setHelp("The amount of green in our color")
273 .setShort('g')
274 .setLong("green")
275 .takesValue(true),
276 COption.init(Color.bFromStr)
277 .setHelp("The amount of blue in our color")
278 .setShort('b')
279 .setLong("blue")
280 .takesValue(true),
281 };
282
283 const Case = struct { args: []const []const u8, res: Color, err: ?error };
284 const cases = []Case {
285 Case {
286 .args = [][]const u8 { "-r", "100", "-g", "100", "-b", "100", },
287 .res = Color { .r = 100, .g = 100, .b = 100 },
288 .err = null,
289 },
290 Case {
291 .args = [][]const u8 { "--red", "100", "-g", "100", "--blue", "50", },
292 .res = Color { .r = 100, .g = 100, .b = 50 },
293 .err = null,
294 },
295 Case {
296 .args = [][]const u8 { "-g", "200", "--blue", "100", "--red", "100", },
297 .res = Color { .r = 100, .g = 200, .b = 100 },
298 .err = null,
299 },
300 Case {
301 .args = [][]const u8 { "-r", "200", "-r", "255" },
302 .res = Color { .r = 255, .g = 0, .b = 0 },
303 .err = null,
304 },
305 Case {
306 .args = [][]const u8 { "-g", "200", "-b", "255" },
307 .res = Color { .r = 0, .g = 0, .b = 0 },
308 .err = error.RequiredArgumentWasntHandled,
309 },
310 Case {
311 .args = [][]const u8 { "-p" },
312 .res = Color { .r = 0, .g = 0, .b = 0 },
313 .err = error.InvalidArgument,
314 },
315 Case {
316 .args = [][]const u8 { "-g" },
317 .res = Color { .r = 0, .g = 0, .b = 0 },
318 .err = error.MissingValueToArgument,
319 },
320 };
321
322 const Clap = Parser(Color, @typeOf(Color.rFromStr).ReturnType.ErrorSet, Color { .r = 0, .g = 0, .b = 0 }, options);
323 for (cases) |case, i| {
324 if (Clap.parse(case.args)) |res| {
325 assert(res.r == case.res.r);
326 assert(res.g == case.res.g);
327 assert(res.b == case.res.b);
328 } else |err| {
329 assert(err == (case.err ?? unreachable));
330 }
331 }
332}