summaryrefslogtreecommitdiff
path: root/clap
diff options
context:
space:
mode:
authorGravatar Jimmi Holst Christensen2022-01-31 17:11:15 +0100
committerGravatar Jimmi Holst Christensen2022-01-31 17:11:15 +0100
commit7188a9fc85f6aa0f71a4cb7966f8b0a044f29e02 (patch)
tree0363b15fb5393c7ca46f60325eee93c13f9c76f7 /clap
parentRelicense to MIT (diff)
downloadzig-clap-7188a9fc85f6aa0f71a4cb7966f8b0a044f29e02.tar.gz
zig-clap-7188a9fc85f6aa0f71a4cb7966f8b0a044f29e02.tar.xz
zig-clap-7188a9fc85f6aa0f71a4cb7966f8b0a044f29e02.zip
Refactor the ArgIterator interface
They now follow the interface provided by the standard library. This now means that we no longer needs `args.OsIterator` as that the one from `std` can now be used directly. Also remove `args.ShellIterator` as a simular iterator exists in `std` called `ArgIteratorGeneral`.
Diffstat (limited to 'clap')
-rw-r--r--clap/args.zig316
-rw-r--r--clap/streaming.zig10
2 files changed, 10 insertions, 316 deletions
diff --git a/clap/args.zig b/clap/args.zig
index 90c50fa..d0aaee3 100644
--- a/clap/args.zig
+++ b/clap/args.zig
@@ -9,9 +9,7 @@ const testing = std.testing;
9 9
10/// An example of what methods should be implemented on an arg iterator. 10/// An example of what methods should be implemented on an arg iterator.
11pub const ExampleArgIterator = struct { 11pub const ExampleArgIterator = struct {
12 const Error = error{}; 12 pub fn next(iter: *ExampleArgIterator) ?[]const u8 {
13
14 pub fn next(iter: *ExampleArgIterator) Error!?[]const u8 {
15 _ = iter; 13 _ = iter;
16 return "2"; 14 return "2";
17 } 15 }
@@ -20,12 +18,10 @@ pub const ExampleArgIterator = struct {
20/// An argument iterator which iterates over a slice of arguments. 18/// An argument iterator which iterates over a slice of arguments.
21/// This implementation does not allocate. 19/// This implementation does not allocate.
22pub const SliceIterator = struct { 20pub const SliceIterator = struct {
23 const Error = error{};
24
25 args: []const []const u8, 21 args: []const []const u8,
26 index: usize = 0, 22 index: usize = 0,
27 23
28 pub fn next(iter: *SliceIterator) Error!?[]const u8 { 24 pub fn next(iter: *SliceIterator) ?[]const u8 {
29 if (iter.args.len <= iter.index) 25 if (iter.args.len <= iter.index)
30 return null; 26 return null;
31 27
@@ -38,310 +34,8 @@ test "SliceIterator" {
38 const args = [_][]const u8{ "A", "BB", "CCC" }; 34 const args = [_][]const u8{ "A", "BB", "CCC" };
39 var iter = SliceIterator{ .args = &args }; 35 var iter = SliceIterator{ .args = &args };
40 36
41 for (args) |a| { 37 for (args) |a|
42 const b = try iter.next(); 38 try testing.expectEqualStrings(a, iter.next().?);
43 debug.assert(mem.eql(u8, a, b.?));
44 }
45}
46
47/// An argument iterator which wraps the ArgIterator in ::std.
48/// On windows, this iterator allocates.
49pub const OsIterator = struct {
50 const Error = process.ArgIterator.NextError;
51
52 arena: heap.ArenaAllocator,
53 args: process.ArgIterator,
54
55 /// The executable path (this is the first argument passed to the program)
56 /// TODO: Is it the right choice for this to be null? Maybe `init` should
57 /// return an error when we have no exe.
58 exe_arg: ?[:0]const u8,
59
60 pub fn init(allocator: mem.Allocator) Error!OsIterator {
61 var res = OsIterator{
62 .arena = heap.ArenaAllocator.init(allocator),
63 .args = process.args(),
64 .exe_arg = undefined,
65 };
66 res.exe_arg = try res.next();
67 return res;
68 }
69
70 pub fn deinit(iter: *OsIterator) void {
71 iter.arena.deinit();
72 }
73
74 pub fn next(iter: *OsIterator) Error!?[:0]const u8 {
75 if (builtin.os.tag == .windows) {
76 return (try iter.args.next(iter.arena.allocator())) orelse return null;
77 } else {
78 return iter.args.nextPosix();
79 }
80 }
81};
82
83/// An argument iterator that takes a string and parses it into arguments, simulating
84/// how shells split arguments.
85pub const ShellIterator = struct {
86 const Error = error{
87 DanglingEscape,
88 QuoteNotClosed,
89 } || mem.Allocator.Error;
90
91 arena: heap.ArenaAllocator,
92 str: []const u8,
93
94 pub fn init(allocator: mem.Allocator, str: []const u8) ShellIterator {
95 return .{
96 .arena = heap.ArenaAllocator.init(allocator),
97 .str = str,
98 };
99 }
100
101 pub fn deinit(iter: *ShellIterator) void {
102 iter.arena.deinit();
103 }
104
105 pub fn next(iter: *ShellIterator) Error!?[]const u8 {
106 // Whenever possible, this iterator will return slices into `str` instead of
107 // allocating. Sometimes this is not possible, for example, escaped characters
108 // have be be unescape, so we need to allocate in this case.
109 var list = std.ArrayList(u8).init(iter.arena.allocator());
110 var start: usize = 0;
111 var state: enum {
112 skip_whitespace,
113 no_quote,
114 no_quote_escape,
115 single_quote,
116 double_quote,
117 double_quote_escape,
118 after_quote,
119 } = .skip_whitespace;
120
121 for (iter.str) |c, i| {
122 switch (state) {
123 // The state that skips the initial whitespace.
124 .skip_whitespace => switch (c) {
125 ' ', '\t', '\n' => {},
126 '\'' => {
127 start = i + 1;
128 state = .single_quote;
129 },
130 '"' => {
131 start = i + 1;
132 state = .double_quote;
133 },
134 '\\' => {
135 start = i + 1;
136 state = .no_quote_escape;
137 },
138 else => {
139 start = i;
140 state = .no_quote;
141 },
142 },
143
144 // The state that parses the none quoted part of a argument.
145 .no_quote => switch (c) {
146 // We're done parsing a none quoted argument when we hit a
147 // whitespace.
148 ' ', '\t', '\n' => {
149 defer iter.str = iter.str[i..];
150 return iter.result(start, i, &list);
151 },
152
153 // Slicing is not possible if a quote starts while parsing none
154 // quoted args.
155 // Example:
156 // ab'cd' -> abcd
157 '\'' => {
158 try list.appendSlice(iter.str[start..i]);
159 start = i + 1;
160 state = .single_quote;
161 },
162 '"' => {
163 try list.appendSlice(iter.str[start..i]);
164 start = i + 1;
165 state = .double_quote;
166 },
167
168 // Slicing is not possible if we need to escape a character.
169 // Example:
170 // ab\"d -> ab"d
171 '\\' => {
172 try list.appendSlice(iter.str[start..i]);
173 start = i + 1;
174 state = .no_quote_escape;
175 },
176 else => {},
177 },
178
179 // We're in this state after having parsed the quoted part of an
180 // argument. This state works mostly the same as .no_quote, but
181 // is aware, that the last character seen was a quote, which should
182 // not be part of the argument. This is why you will see `i - 1` here
183 // instead of just `i` when `iter.str` is sliced.
184 .after_quote => switch (c) {
185 ' ', '\t', '\n' => {
186 defer iter.str = iter.str[i..];
187 return iter.result(start, i - 1, &list);
188 },
189 '\'' => {
190 try list.appendSlice(iter.str[start .. i - 1]);
191 start = i + 1;
192 state = .single_quote;
193 },
194 '"' => {
195 try list.appendSlice(iter.str[start .. i - 1]);
196 start = i + 1;
197 state = .double_quote;
198 },
199 '\\' => {
200 try list.appendSlice(iter.str[start .. i - 1]);
201 start = i + 1;
202 state = .no_quote_escape;
203 },
204 else => {
205 try list.appendSlice(iter.str[start .. i - 1]);
206 start = i;
207 state = .no_quote;
208 },
209 },
210
211 // The states that parse the quoted part of arguments. The only differnece
212 // between single and double quoted arguments is that single quoted
213 // arguments ignore escape sequences, while double quoted arguments
214 // does escaping.
215 .single_quote => switch (c) {
216 '\'' => state = .after_quote,
217 else => {},
218 },
219 .double_quote => switch (c) {
220 '"' => state = .after_quote,
221 '\\' => {
222 try list.appendSlice(iter.str[start..i]);
223 start = i + 1;
224 state = .double_quote_escape;
225 },
226 else => {},
227 },
228
229 // The state we end up when after the escape character (`\`). All these
230 // states do is transition back into the previous state.
231 // TODO: Are there any escape sequences that does transform the second
232 // character into something else? For example, in Zig, `\n` is
233 // transformed into the line feed ascii character.
234 .no_quote_escape => switch (c) {
235 else => state = .no_quote,
236 },
237 .double_quote_escape => switch (c) {
238 else => state = .double_quote,
239 },
240 }
241 }
242
243 defer iter.str = iter.str[iter.str.len..];
244 switch (state) {
245 .skip_whitespace => return null,
246 .no_quote => return iter.result(start, iter.str.len, &list),
247 .after_quote => return iter.result(start, iter.str.len - 1, &list),
248 .no_quote_escape => return Error.DanglingEscape,
249 .single_quote,
250 .double_quote,
251 .double_quote_escape,
252 => return Error.QuoteNotClosed,
253 }
254 }
255
256 fn result(
257 iter: *ShellIterator,
258 start: usize,
259 end: usize,
260 list: *std.ArrayList(u8),
261 ) Error!?[]const u8 {
262 const res = iter.str[start..end];
263
264 // If we already have something in `list` that means that we could not
265 // parse the argument without allocation. We therefor need to just append
266 // the rest we have to the list and return that.
267 if (list.items.len != 0) {
268 try list.appendSlice(res);
269 return list.toOwnedSlice();
270 }
271 return res;
272 }
273};
274
275fn testShellIteratorOk(str: []const u8, allocations: usize, expect: []const []const u8) !void {
276 var allocator = testing.FailingAllocator.init(testing.allocator, allocations);
277 var it = ShellIterator.init(allocator.allocator(), str);
278 defer it.deinit();
279
280 for (expect) |e| {
281 if (it.next()) |actual| {
282 try testing.expect(actual != null);
283 try testing.expectEqualStrings(e, actual.?);
284 } else |err| try testing.expectEqual(@as(anyerror![]const u8, e), err);
285 }
286
287 if (it.next()) |actual| {
288 try testing.expectEqual(@as(?[]const u8, null), actual);
289 try testing.expectEqual(allocations, allocator.allocations);
290 } else |err| try testing.expectEqual(@as(anyerror!void, {}), err);
291}
292
293fn testShellIteratorErr(str: []const u8, expect: anyerror) !void {
294 var it = ShellIterator.init(testing.allocator, str);
295 defer it.deinit();
296
297 while (it.next() catch |err| {
298 try testing.expectError(expect, @as(anyerror!void, err));
299 return;
300 }) |_| {}
301
302 try testing.expectError(expect, @as(anyerror!void, {}));
303}
304
305test "ShellIterator" {
306 try testShellIteratorOk("a", 0, &.{"a"});
307 try testShellIteratorOk("'a'", 0, &.{"a"});
308 try testShellIteratorOk("\"a\"", 0, &.{"a"});
309 try testShellIteratorOk("a b", 0, &.{ "a", "b" });
310 try testShellIteratorOk("'a' b", 0, &.{ "a", "b" });
311 try testShellIteratorOk("\"a\" b", 0, &.{ "a", "b" });
312 try testShellIteratorOk("a 'b'", 0, &.{ "a", "b" });
313 try testShellIteratorOk("a \"b\"", 0, &.{ "a", "b" });
314 try testShellIteratorOk("'a b'", 0, &.{"a b"});
315 try testShellIteratorOk("\"a b\"", 0, &.{"a b"});
316 try testShellIteratorOk("\"a\"\"b\"", 1, &.{"ab"});
317 try testShellIteratorOk("'a''b'", 1, &.{"ab"});
318 try testShellIteratorOk("'a'b", 1, &.{"ab"});
319 try testShellIteratorOk("a'b'", 1, &.{"ab"});
320 try testShellIteratorOk("a\\ b", 1, &.{"a b"});
321 try testShellIteratorOk("\"a\\ b\"", 1, &.{"a b"});
322 try testShellIteratorOk("'a\\ b'", 0, &.{"a\\ b"});
323 try testShellIteratorOk(" a b ", 0, &.{ "a", "b" });
324 try testShellIteratorOk("\\ \\ ", 0, &.{ " ", " " });
325
326 try testShellIteratorOk(
327 \\printf 'run\nuninstall\n'
328 , 0, &.{ "printf", "run\\nuninstall\\n" });
329 try testShellIteratorOk(
330 \\setsid -f steam "steam://$action/$id"
331 , 0, &.{ "setsid", "-f", "steam", "steam://$action/$id" });
332 try testShellIteratorOk(
333 \\xargs -I% rg --no-heading --no-line-number --only-matching
334 \\ --case-sensitive --multiline --text --byte-offset '(?-u)%' $@
335 \\
336 , 0, &.{
337 "xargs", "-I%", "rg", "--no-heading",
338 "--no-line-number", "--only-matching", "--case-sensitive", "--multiline",
339 "--text", "--byte-offset", "(?-u)%", "$@",
340 });
341 39
342 try testShellIteratorErr("'a", error.QuoteNotClosed); 40 try testing.expectEqual(@as(?[]const u8, null), iter.next());
343 try testShellIteratorErr("'a\\", error.QuoteNotClosed);
344 try testShellIteratorErr("\"a", error.QuoteNotClosed);
345 try testShellIteratorErr("\"a\\", error.QuoteNotClosed);
346 try testShellIteratorErr("a\\", error.DanglingEscape);
347} 41}
diff --git a/clap/streaming.zig b/clap/streaming.zig
index 3f24aaa..8eca51a 100644
--- a/clap/streaming.zig
+++ b/clap/streaming.zig
@@ -49,7 +49,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type {
49 .chaining => |state| return try parser.chaining(state), 49 .chaining => |state| return try parser.chaining(state),
50 .rest_are_positional => { 50 .rest_are_positional => {
51 const param = parser.positionalParam() orelse unreachable; 51 const param = parser.positionalParam() orelse unreachable;
52 const value = (try parser.iter.next()) orelse return null; 52 const value = parser.iter.next() orelse return null;
53 return Arg(Id){ .param = param, .value = value }; 53 return Arg(Id){ .param = param, .value = value };
54 }, 54 },
55 } 55 }
@@ -80,7 +80,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type {
80 if (maybe_value) |v| 80 if (maybe_value) |v|
81 break :blk v; 81 break :blk v;
82 82
83 break :blk (try parser.iter.next()) orelse 83 break :blk parser.iter.next() orelse
84 return parser.err(arg, .{ .long = name }, error.MissingValue); 84 return parser.err(arg, .{ .long = name }, error.MissingValue);
85 }; 85 };
86 86
@@ -99,7 +99,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type {
99 // arguments. 99 // arguments.
100 if (mem.eql(u8, arg, "--")) { 100 if (mem.eql(u8, arg, "--")) {
101 parser.state = .rest_are_positional; 101 parser.state = .rest_are_positional;
102 const value = (try parser.iter.next()) orelse return null; 102 const value = parser.iter.next() orelse return null;
103 return Arg(Id){ .param = param, .value = value }; 103 return Arg(Id){ .param = param, .value = value };
104 } 104 }
105 105
@@ -142,7 +142,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type {
142 } 142 }
143 143
144 if (arg.len <= next_index) { 144 if (arg.len <= next_index) {
145 const value = (try parser.iter.next()) orelse 145 const value = parser.iter.next() orelse
146 return parser.err(arg, .{ .short = short }, error.MissingValue); 146 return parser.err(arg, .{ .short = short }, error.MissingValue);
147 147
148 return Arg(Id){ .param = param, .value = value }; 148 return Arg(Id){ .param = param, .value = value };
@@ -184,7 +184,7 @@ pub fn StreamingClap(comptime Id: type, comptime ArgIterator: type) type {
184 }; 184 };
185 185
186 fn parseNextArg(parser: *@This()) !?ArgInfo { 186 fn parseNextArg(parser: *@This()) !?ArgInfo {
187 const full_arg = (try parser.iter.next()) orelse return null; 187 const full_arg = parser.iter.next() orelse return null;
188 if (mem.eql(u8, full_arg, "--") or mem.eql(u8, full_arg, "-")) 188 if (mem.eql(u8, full_arg, "--") or mem.eql(u8, full_arg, "-"))
189 return ArgInfo{ .arg = full_arg, .kind = .positional }; 189 return ArgInfo{ .arg = full_arg, .kind = .positional };
190 if (mem.startsWith(u8, full_arg, "--")) 190 if (mem.startsWith(u8, full_arg, "--"))