summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md10
-rw-r--r--clap.zig14
-rw-r--r--clap/args.zig316
-rw-r--r--clap/streaming.zig10
-rw-r--r--example/simple-ex.zig8
-rw-r--r--example/streaming-clap.zig10
6 files changed, 37 insertions, 331 deletions
diff --git a/README.md b/README.md
index d872965..80d0e80 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ const std = @import("std");
108 108
109const debug = std.debug; 109const debug = std.debug;
110const io = std.io; 110const io = std.io;
111const process = std.process;
111 112
112pub fn main() !void { 113pub fn main() !void {
113 const allocator = std.heap.page_allocator; 114 const allocator = std.heap.page_allocator;
@@ -126,16 +127,17 @@ pub fn main() !void {
126 .{ .id = 'f', .takes_value = .one }, 127 .{ .id = 'f', .takes_value = .one },
127 }; 128 };
128 129
129 // We then initialize an argument iterator. We will use the OsIterator as it nicely 130 var iter = try process.ArgIterator.initWithAllocator(allocator);
130 // wraps iterating over arguments the most efficient way on each os.
131 var iter = try clap.args.OsIterator.init(allocator);
132 defer iter.deinit(); 131 defer iter.deinit();
133 132
133 // Skip exe argument
134 _ = iter.next();
135
134 // Initalize our diagnostics, which can be used for reporting useful errors. 136 // Initalize our diagnostics, which can be used for reporting useful errors.
135 // This is optional. You can also leave the `diagnostic` field unset if you 137 // This is optional. You can also leave the `diagnostic` field unset if you
136 // don't care about the extra information `Diagnostic` provides. 138 // don't care about the extra information `Diagnostic` provides.
137 var diag = clap.Diagnostic{}; 139 var diag = clap.Diagnostic{};
138 var parser = clap.StreamingClap(u8, clap.args.OsIterator){ 140 var parser = clap.StreamingClap(u8, process.ArgIterator){
139 .params = &params, 141 .params = &params,
140 .iter = &iter, 142 .iter = &iter,
141 .diagnostic = &diag, 143 .diagnostic = &diag,
diff --git a/clap.zig b/clap.zig
index 7234283..39bbef2 100644
--- a/clap.zig
+++ b/clap.zig
@@ -4,6 +4,7 @@ const debug = std.debug;
4const heap = std.heap; 4const heap = std.heap;
5const io = std.io; 5const io = std.io;
6const mem = std.mem; 6const mem = std.mem;
7const process = std.process;
7const testing = std.testing; 8const testing = std.testing;
8 9
9pub const args = @import("clap/args.zig"); 10pub const args = @import("clap/args.zig");
@@ -347,16 +348,21 @@ pub fn parse(
347 comptime params: []const Param(Id), 348 comptime params: []const Param(Id),
348 opt: ParseOptions, 349 opt: ParseOptions,
349) !Args(Id, params) { 350) !Args(Id, params) {
350 var iter = try args.OsIterator.init(opt.allocator); 351 var arena = heap.ArenaAllocator.init(opt.allocator);
352 errdefer arena.deinit();
353
354 var iter = try process.ArgIterator.initWithAllocator(arena.allocator());
355 const exe_arg = iter.next();
356
351 const clap = try parseEx(Id, params, &iter, .{ 357 const clap = try parseEx(Id, params, &iter, .{
352 // Let's reuse the arena from the `OSIterator` since we already have it. 358 // Let's reuse the arena from the `OSIterator` since we already have it.
353 .allocator = iter.arena.allocator(), 359 .allocator = arena.allocator(),
354 .diagnostic = opt.diagnostic, 360 .diagnostic = opt.diagnostic,
355 }); 361 });
356 362
357 return Args(Id, params){ 363 return Args(Id, params){
358 .exe_arg = iter.exe_arg, 364 .exe_arg = exe_arg,
359 .arena = iter.arena, 365 .arena = arena,
360 .clap = clap, 366 .clap = clap,
361 }; 367 };
362} 368}
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, "--"))
diff --git a/example/simple-ex.zig b/example/simple-ex.zig
index 5653fd1..d2dc77e 100644
--- a/example/simple-ex.zig
+++ b/example/simple-ex.zig
@@ -3,6 +3,7 @@ const std = @import("std");
3 3
4const debug = std.debug; 4const debug = std.debug;
5const io = std.io; 5const io = std.io;
6const process = std.process;
6 7
7pub fn main() !void { 8pub fn main() !void {
8 const allocator = std.heap.page_allocator; 9 const allocator = std.heap.page_allocator;
@@ -16,11 +17,12 @@ pub fn main() !void {
16 clap.parseParam("<POS>...") catch unreachable, 17 clap.parseParam("<POS>...") catch unreachable,
17 }; 18 };
18 19
19 // We then initialize an argument iterator. We will use the OsIterator as it nicely 20 var iter = try process.ArgIterator.initWithAllocator(allocator);
20 // wraps iterating over arguments the most efficient way on each os.
21 var iter = try clap.args.OsIterator.init(allocator);
22 defer iter.deinit(); 21 defer iter.deinit();
23 22
23 // Skip exe argument
24 _ = iter.next();
25
24 // Initalize our diagnostics, which can be used for reporting useful errors. 26 // Initalize our diagnostics, which can be used for reporting useful errors.
25 // This is optional. You can also pass `.{}` to `clap.parse` if you don't 27 // This is optional. You can also pass `.{}` to `clap.parse` if you don't
26 // care about the extra information `Diagnostics` provides. 28 // care about the extra information `Diagnostics` provides.
diff --git a/example/streaming-clap.zig b/example/streaming-clap.zig
index 9ed38dd..a7ab7d8 100644
--- a/example/streaming-clap.zig
+++ b/example/streaming-clap.zig
@@ -3,6 +3,7 @@ const std = @import("std");
3 3
4const debug = std.debug; 4const debug = std.debug;
5const io = std.io; 5const io = std.io;
6const process = std.process;
6 7
7pub fn main() !void { 8pub fn main() !void {
8 const allocator = std.heap.page_allocator; 9 const allocator = std.heap.page_allocator;
@@ -21,16 +22,17 @@ pub fn main() !void {
21 .{ .id = 'f', .takes_value = .one }, 22 .{ .id = 'f', .takes_value = .one },
22 }; 23 };
23 24
24 // We then initialize an argument iterator. We will use the OsIterator as it nicely 25 var iter = try process.ArgIterator.initWithAllocator(allocator);
25 // wraps iterating over arguments the most efficient way on each os.
26 var iter = try clap.args.OsIterator.init(allocator);
27 defer iter.deinit(); 26 defer iter.deinit();
28 27
28 // Skip exe argument
29 _ = iter.next();
30
29 // Initalize our diagnostics, which can be used for reporting useful errors. 31 // Initalize our diagnostics, which can be used for reporting useful errors.
30 // This is optional. You can also leave the `diagnostic` field unset if you 32 // This is optional. You can also leave the `diagnostic` field unset if you
31 // don't care about the extra information `Diagnostic` provides. 33 // don't care about the extra information `Diagnostic` provides.
32 var diag = clap.Diagnostic{}; 34 var diag = clap.Diagnostic{};
33 var parser = clap.StreamingClap(u8, clap.args.OsIterator){ 35 var parser = clap.StreamingClap(u8, process.ArgIterator){
34 .params = &params, 36 .params = &params,
35 .iter = &iter, 37 .iter = &iter,
36 .diagnostic = &diag, 38 .diagnostic = &diag,