summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ivan Stepanov2025-07-20 21:40:56 +0200
committerGravatar Jimmi Holst Christensen2025-07-21 18:52:54 +0200
commit6f103922a8133ba11773e6ad9a52e26e1d99b3e7 (patch)
tree66bc5fa5fc36f20575be1e1a59ff890ab9c8b23e
parentchore: Update setup-zig to v2 (diff)
downloadzig-clap-6f103922a8133ba11773e6ad9a52e26e1d99b3e7.tar.gz
zig-clap-6f103922a8133ba11773e6ad9a52e26e1d99b3e7.tar.xz
zig-clap-6f103922a8133ba11773e6ad9a52e26e1d99b3e7.zip
Update to Zig 0.15.0-dev.1147
-rw-r--r--README.md87
-rw-r--r--build.zig12
-rw-r--r--build.zig.zon2
-rw-r--r--clap.zig128
-rw-r--r--clap/codepoint_counting_writer.zig68
-rw-r--r--clap/streaming.zig6
-rw-r--r--example/help.zig8
-rw-r--r--example/simple-ex.zig6
-rw-r--r--example/simple.zig6
-rw-r--r--example/streaming-clap.zig6
-rw-r--r--example/subcommands.zig13
-rw-r--r--example/usage.zig9
12 files changed, 201 insertions, 150 deletions
diff --git a/README.md b/README.md
index fbc19e4..2e64264 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,9 @@ A simple and easy to use command line argument parser library for Zig.
5## Installation 5## Installation
6 6
7Developers tend to either use 7Developers tend to either use
8* The latest tagged release of Zig 8
9* The latest build of Zigs master branch 9- The latest tagged release of Zig
10- The latest build of Zigs master branch
10 11
11Depending on which developer you are, you need to run different `zig fetch` commands: 12Depending on which developer you are, you need to run different `zig fetch` commands:
12 13
@@ -29,22 +30,22 @@ exe.root_module.addImport("clap", clap.module("clap"));
29 30
30## Features 31## Features
31 32
32* Short arguments `-a` 33- Short arguments `-a`
33 * Chaining `-abc` where `a` and `b` does not take values. 34 - Chaining `-abc` where `a` and `b` does not take values.
34 * Multiple specifications are tallied (e.g. `-v -v`). 35 - Multiple specifications are tallied (e.g. `-v -v`).
35* Long arguments `--long` 36- Long arguments `--long`
36* Supports both passing values using spacing and `=` (`-a 100`, `-a=100`) 37- Supports both passing values using spacing and `=` (`-a 100`, `-a=100`)
37 * Short args also support passing values with no spacing or `=` (`-a100`) 38 - Short args also support passing values with no spacing or `=` (`-a100`)
38 * This all works with chaining (`-ba 100`, `-ba=100`, `-ba100`) 39 - This all works with chaining (`-ba 100`, `-ba=100`, `-ba100`)
39* Supports options that can be specified multiple times (`-e 1 -e 2 -e 3`) 40- Supports options that can be specified multiple times (`-e 1 -e 2 -e 3`)
40* Print help message from parameter specification. 41- Print help message from parameter specification.
41* Parse help message to parameter specification. 42- Parse help message to parameter specification.
42 43
43## API Reference 44## API Reference
44 45
45Automatically generated API Reference for the project can be found at 46Automatically generated API Reference for the project can be found at
46https://Hejsil.github.io/zig-clap. Note that Zig autodoc is in beta; the website 47https://Hejsil.github.io/zig-clap. Note that Zig autodoc is in beta; the website may be broken or
47may be broken or incomplete. 48incomplete.
48 49
49## Examples 50## Examples
50 51
@@ -76,7 +77,10 @@ pub fn main() !void {
76 .allocator = gpa.allocator(), 77 .allocator = gpa.allocator(),
77 }) catch |err| { 78 }) catch |err| {
78 // Report useful error and exit. 79 // Report useful error and exit.
79 diag.report(std.io.getStdErr().writer(), err) catch {}; 80 var buf: [1024]u8 = undefined;
81 var stderr = std.fs.File.stderr().writer(&buf);
82 diag.report(&stderr.interface, err) catch {};
83 try stderr.interface.flush();
80 return err; 84 return err;
81 }; 85 };
82 defer res.deinit(); 86 defer res.deinit();
@@ -93,7 +97,6 @@ pub fn main() !void {
93 97
94const clap = @import("clap"); 98const clap = @import("clap");
95const std = @import("std"); 99const std = @import("std");
96
97``` 100```
98 101
99The result will contain an `args` field and a `positionals` field. `args` will have one field for 102The result will contain an `args` field and a `positionals` field. `args` will have one field for
@@ -141,7 +144,10 @@ pub fn main() !void {
141 // allowed. 144 // allowed.
142 .assignment_separators = "=:", 145 .assignment_separators = "=:",
143 }) catch |err| { 146 }) catch |err| {
144 diag.report(std.io.getStdErr().writer(), err) catch {}; 147 var buf: [1024]u8 = undefined;
148 var stderr = std.fs.File.stderr().writer(&buf);
149 diag.report(&stderr.interface, err) catch {};
150 try stderr.interface.flush();
145 return err; 151 return err;
146 }; 152 };
147 defer res.deinit(); 153 defer res.deinit();
@@ -160,7 +166,6 @@ pub fn main() !void {
160 166
161const clap = @import("clap"); 167const clap = @import("clap");
162const std = @import("std"); 168const std = @import("std");
163
164``` 169```
165 170
166### Subcommands 171### Subcommands
@@ -212,7 +217,10 @@ pub fn main() !void {
212 // not fully consumed. It can then be reused to parse the arguments for subcommands. 217 // not fully consumed. It can then be reused to parse the arguments for subcommands.
213 .terminating_positional = 0, 218 .terminating_positional = 0,
214 }) catch |err| { 219 }) catch |err| {
215 diag.report(std.io.getStdErr().writer(), err) catch {}; 220 var buf: [1024]u8 = undefined;
221 var stderr = std.fs.File.stderr().writer(&buf);
222 diag.report(&stderr.interface, err) catch {};
223 try stderr.interface.flush();
216 return err; 224 return err;
217 }; 225 };
218 defer res.deinit(); 226 defer res.deinit();
@@ -248,8 +256,11 @@ fn mathMain(gpa: std.mem.Allocator, iter: *std.process.ArgIterator, main_args: M
248 .diagnostic = &diag, 256 .diagnostic = &diag,
249 .allocator = gpa, 257 .allocator = gpa,
250 }) catch |err| { 258 }) catch |err| {
251 diag.report(std.io.getStdErr().writer(), err) catch {}; 259 var buf: [1024]u8 = undefined;
252 return err; 260 var stderr = std.fs.File.stderr().writer(&buf);
261 diag.report(&stderr.interface, err) catch {};
262 try stderr.interface.flush();
263 return err; // propagate error
253 }; 264 };
254 defer res.deinit(); 265 defer res.deinit();
255 266
@@ -265,7 +276,6 @@ fn mathMain(gpa: std.mem.Allocator, iter: *std.process.ArgIterator, main_args: M
265 276
266const clap = @import("clap"); 277const clap = @import("clap");
267const std = @import("std"); 278const std = @import("std");
268
269``` 279```
270 280
271### `streaming.Clap` 281### `streaming.Clap`
@@ -310,7 +320,10 @@ pub fn main() !void {
310 // Because we use a streaming parser, we have to consume each argument parsed individually. 320 // Because we use a streaming parser, we have to consume each argument parsed individually.
311 while (parser.next() catch |err| { 321 while (parser.next() catch |err| {
312 // Report useful error and exit. 322 // Report useful error and exit.
313 diag.report(std.io.getStdErr().writer(), err) catch {}; 323 var buf: [1024]u8 = undefined;
324 var stderr = std.fs.File.stderr().writer(&buf);
325 diag.report(&stderr.interface, err) catch {};
326 try stderr.interface.flush();
314 return err; 327 return err;
315 }) |arg| { 328 }) |arg| {
316 // arg.param will point to the parameter which matched the argument. 329 // arg.param will point to the parameter which matched the argument.
@@ -329,10 +342,17 @@ pub fn main() !void {
329 342
330const clap = @import("clap"); 343const clap = @import("clap");
331const std = @import("std"); 344const std = @import("std");
345```
332 346
333``` 347```
348$ zig-out/bin/streaming-clap --help --number=1 f=10
349Help!
350--number = 1
351f=10
352```
334 353
335Currently, this parser is the only parser that allows an array of `Param` that is generated at runtime. 354Currently, this parser is the only parser that allows an array of `Param` that is generated at
355runtime.
336 356
337### `help` 357### `help`
338 358
@@ -360,13 +380,16 @@ pub fn main() !void {
360 // where `Id` has a `description` and `value` method (`Param(Help)` is one such parameter). 380 // where `Id` has a `description` and `value` method (`Param(Help)` is one such parameter).
361 // The last argument contains options as to how `help` should print those parameters. Using 381 // The last argument contains options as to how `help` should print those parameters. Using
362 // `.{}` means the default options. 382 // `.{}` means the default options.
363 if (res.args.help != 0) 383 if (res.args.help != 0) {
364 return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}); 384 var buf: [1024]u8 = undefined;
385 var stderr = std.fs.File.stderr().writer(&buf);
386 try clap.help(&stderr.interface, clap.Help, &params, .{});
387 return stderr.interface.flush();
388 }
365} 389}
366 390
367const clap = @import("clap"); 391const clap = @import("clap");
368const std = @import("std"); 392const std = @import("std");
369
370``` 393```
371 394
372``` 395```
@@ -402,17 +425,19 @@ pub fn main() !void {
402 425
403 // `clap.usage` is a function that can print a simple help message. It can print any `Param` 426 // `clap.usage` is a function that can print a simple help message. It can print any `Param`
404 // where `Id` has a `value` method (`Param(Help)` is one such parameter). 427 // where `Id` has a `value` method (`Param(Help)` is one such parameter).
405 if (res.args.help != 0) 428 if (res.args.help != 0) {
406 return clap.usage(std.io.getStdErr().writer(), clap.Help, &params); 429 var buf: [1024]u8 = undefined;
430 var stderr = std.fs.File.stderr().writer(&buf);
431 try clap.usage(&stderr.interface, clap.Help, &params);
432 return stderr.interface.flush();
433 }
407} 434}
408 435
409const clap = @import("clap"); 436const clap = @import("clap");
410const std = @import("std"); 437const std = @import("std");
411
412``` 438```
413 439
414``` 440```
415$ zig-out/bin/usage --help 441$ zig-out/bin/usage --help
416[-hv] [--value <str>] 442[-hv] [--value <str>]
417``` 443```
418
diff --git a/build.zig b/build.zig
index 7a1fe95..02887e4 100644
--- a/build.zig
+++ b/build.zig
@@ -24,12 +24,16 @@ pub fn build(b: *std.Build) void {
24 }) |example_name| { 24 }) |example_name| {
25 const example = b.addExecutable(.{ 25 const example = b.addExecutable(.{
26 .name = example_name, 26 .name = example_name,
27 .root_source_file = b.path(b.fmt("example/{s}.zig", .{example_name})), 27 .root_module = b.createModule(.{
28 .target = target, 28 .root_source_file = b.path(b.fmt("example/{s}.zig", .{example_name})),
29 .optimize = optimize, 29 .target = target,
30 .optimize = optimize,
31 .imports = &.{
32 .{ .name = "clap", .module = clap_mod },
33 },
34 }),
30 }); 35 });
31 const install_example = b.addInstallArtifact(example, .{}); 36 const install_example = b.addInstallArtifact(example, .{});
32 example.root_module.addImport("clap", clap_mod);
33 example_step.dependOn(&example.step); 37 example_step.dependOn(&example.step);
34 example_step.dependOn(&install_example.step); 38 example_step.dependOn(&install_example.step);
35 } 39 }
diff --git a/build.zig.zon b/build.zig.zon
index 0fc81aa..883ed5e 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,7 +1,7 @@
1.{ 1.{
2 .name = .clap, 2 .name = .clap,
3 .version = "0.10.0", 3 .version = "0.10.0",
4 .minimum_zig_version = "0.14.0", 4 .minimum_zig_version = "0.15.0-dev.1147+69cf40da6",
5 .fingerprint = 0x65f99e6f07a316a0, 5 .fingerprint = 0x65f99e6f07a316a0,
6 .paths = .{ 6 .paths = .{
7 "clap", 7 "clap",
diff --git a/clap.zig b/clap.zig
index b666f56..3863087 100644
--- a/clap.zig
+++ b/clap.zig
@@ -542,7 +542,7 @@ pub const Diagnostic = struct {
542 542
543 /// Default diagnostics reporter when all you want is English with no colors. 543 /// Default diagnostics reporter when all you want is English with no colors.
544 /// Use this as a reference for implementing your own if needed. 544 /// Use this as a reference for implementing your own if needed.
545 pub fn report(diag: Diagnostic, stream: anytype, err: anyerror) !void { 545 pub fn report(diag: Diagnostic, stream: *std.Io.Writer, err: anyerror) !void {
546 var longest = diag.name.longest(); 546 var longest = diag.name.longest();
547 if (longest.kind == .positional) 547 if (longest.kind == .positional)
548 longest.name = diag.arg; 548 longest.name = diag.arg;
@@ -567,9 +567,9 @@ pub const Diagnostic = struct {
567 567
568fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) !void { 568fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) !void {
569 var buf: [1024]u8 = undefined; 569 var buf: [1024]u8 = undefined;
570 var slice_stream = std.io.fixedBufferStream(&buf); 570 var writer = std.Io.Writer.fixed(&buf);
571 diag.report(slice_stream.writer(), err) catch unreachable; 571 diag.report(&writer, err) catch unreachable;
572 try std.testing.expectEqualStrings(expected, slice_stream.getWritten()); 572 try std.testing.expectEqualStrings(expected, writer.buffered());
573} 573}
574 574
575test "Diagnostic.report" { 575test "Diagnostic.report" {
@@ -1263,9 +1263,9 @@ fn testErr(
1263 .diagnostic = &diag, 1263 .diagnostic = &diag,
1264 }) catch |err| { 1264 }) catch |err| {
1265 var buf: [1024]u8 = undefined; 1265 var buf: [1024]u8 = undefined;
1266 var fbs = std.io.fixedBufferStream(&buf); 1266 var writer = std.Io.Writer.fixed(&buf);
1267 diag.report(fbs.writer(), err) catch return error.TestFailed; 1267 diag.report(&writer, err) catch return error.TestFailed;
1268 try std.testing.expectEqualStrings(expected, fbs.getWritten()); 1268 try std.testing.expectEqualStrings(expected, writer.buffered());
1269 return; 1269 return;
1270 }; 1270 };
1271 1271
@@ -1366,7 +1366,7 @@ pub const HelpOptions = struct {
1366/// The output can be constumized with the `opt` parameter. For default formatting `.{}` can 1366/// The output can be constumized with the `opt` parameter. For default formatting `.{}` can
1367/// be passed. 1367/// be passed.
1368pub fn help( 1368pub fn help(
1369 writer: anytype, 1369 writer: *std.Io.Writer,
1370 comptime Id: type, 1370 comptime Id: type,
1371 params: []const Param(Id), 1371 params: []const Param(Id),
1372 opt: HelpOptions, 1372 opt: HelpOptions,
@@ -1374,8 +1374,9 @@ pub fn help(
1374 const max_spacing = blk: { 1374 const max_spacing = blk: {
1375 var res: usize = 0; 1375 var res: usize = 0;
1376 for (params) |param| { 1376 for (params) |param| {
1377 var cs = ccw.codepointCountingWriter(std.io.null_writer); 1377 var discarding = std.Io.Writer.Discarding.init(&.{});
1378 try printParam(cs.writer(), Id, param); 1378 var cs = ccw.CodepointCountingWriter.init(&discarding.writer);
1379 try printParam(&cs.interface, Id, param);
1379 if (res < cs.codepoints_written) 1380 if (res < cs.codepoints_written)
1380 res = @intCast(cs.codepoints_written); 1381 res = @intCast(cs.codepoints_written);
1381 } 1382 }
@@ -1390,16 +1391,15 @@ pub fn help(
1390 var first_parameter: bool = true; 1391 var first_parameter: bool = true;
1391 for (params) |param| { 1392 for (params) |param| {
1392 if (!first_parameter) 1393 if (!first_parameter)
1393 try writer.writeByteNTimes('\n', opt.spacing_between_parameters); 1394 try writer.splatByteAll('\n', opt.spacing_between_parameters);
1394 1395
1395 first_parameter = false; 1396 first_parameter = false;
1396 try writer.writeByteNTimes(' ', opt.indent); 1397 try writer.splatByteAll(' ', opt.indent);
1397 1398
1398 var cw = ccw.codepointCountingWriter(writer); 1399 var cw = ccw.CodepointCountingWriter.init(writer);
1399 try printParam(cw.writer(), Id, param); 1400 try printParam(&cw.interface, Id, param);
1400 1401
1401 const Writer = DescriptionWriter(@TypeOf(writer)); 1402 var description_writer = DescriptionWriter{
1402 var description_writer = Writer{
1403 .underlying_writer = writer, 1403 .underlying_writer = writer,
1404 .indentation = description_indentation, 1404 .indentation = description_indentation,
1405 .printed_chars = @intCast(cw.codepoints_written), 1405 .printed_chars = @intCast(cw.codepoints_written),
@@ -1498,57 +1498,53 @@ pub fn help(
1498 } 1498 }
1499} 1499}
1500 1500
1501fn DescriptionWriter(comptime UnderlyingWriter: type) type { 1501const DescriptionWriter = struct {
1502 return struct { 1502 underlying_writer: *std.Io.Writer,
1503 pub const WriteError = UnderlyingWriter.Error;
1504
1505 underlying_writer: UnderlyingWriter,
1506
1507 indentation: usize,
1508 max_width: usize,
1509 printed_chars: usize,
1510
1511 pub fn writeWord(writer: *@This(), word: []const u8) !void {
1512 std.debug.assert(word.len != 0);
1513 1503
1514 var first_word = writer.printed_chars <= writer.indentation; 1504 indentation: usize,
1515 const chars_to_write = try std.unicode.utf8CountCodepoints(word) + @intFromBool(!first_word); 1505 max_width: usize,
1516 if (chars_to_write + writer.printed_chars > writer.max_width) { 1506 printed_chars: usize,
1517 // If the word does not fit on this line, then we insert a new line and print
1518 // it on that line. The only exception to this is if this was the first word.
1519 // If the first word does not fit on this line, then it will also not fit on the
1520 // next one. In that case, all we can really do is just output the word.
1521 if (!first_word)
1522 try writer.newline();
1523 1507
1524 first_word = true; 1508 pub fn writeWord(writer: *@This(), word: []const u8) !void {
1525 } 1509 std.debug.assert(word.len != 0);
1526 1510
1511 var first_word = writer.printed_chars <= writer.indentation;
1512 const chars_to_write = try std.unicode.utf8CountCodepoints(word) + @intFromBool(!first_word);
1513 if (chars_to_write + writer.printed_chars > writer.max_width) {
1514 // If the word does not fit on this line, then we insert a new line and print
1515 // it on that line. The only exception to this is if this was the first word.
1516 // If the first word does not fit on this line, then it will also not fit on the
1517 // next one. In that case, all we can really do is just output the word.
1527 if (!first_word) 1518 if (!first_word)
1528 try writer.underlying_writer.writeAll(" "); 1519 try writer.newline();
1529 1520
1530 try writer.ensureIndented(); 1521 first_word = true;
1531 try writer.underlying_writer.writeAll(word);
1532 writer.printed_chars += chars_to_write;
1533 } 1522 }
1534 1523
1535 pub fn newline(writer: *@This()) !void { 1524 if (!first_word)
1536 try writer.underlying_writer.writeAll("\n"); 1525 try writer.underlying_writer.writeAll(" ");
1537 writer.printed_chars = 0;
1538 }
1539 1526
1540 fn ensureIndented(writer: *@This()) !void { 1527 try writer.ensureIndented();
1541 if (writer.printed_chars < writer.indentation) { 1528 try writer.underlying_writer.writeAll(word);
1542 const to_indent = writer.indentation - writer.printed_chars; 1529 writer.printed_chars += chars_to_write;
1543 try writer.underlying_writer.writeByteNTimes(' ', to_indent); 1530 }
1544 writer.printed_chars += to_indent; 1531
1545 } 1532 pub fn newline(writer: *@This()) !void {
1533 try writer.underlying_writer.writeAll("\n");
1534 writer.printed_chars = 0;
1535 }
1536
1537 fn ensureIndented(writer: *@This()) !void {
1538 if (writer.printed_chars < writer.indentation) {
1539 const to_indent = writer.indentation - writer.printed_chars;
1540 try writer.underlying_writer.splatByteAll(' ', to_indent);
1541 writer.printed_chars += to_indent;
1546 } 1542 }
1547 }; 1543 }
1548} 1544};
1549 1545
1550fn printParam( 1546fn printParam(
1551 stream: anytype, 1547 stream: *std.Io.Writer,
1552 comptime Id: type, 1548 comptime Id: type,
1553 param: Param(Id), 1549 param: Param(Id),
1554) !void { 1550) !void {
@@ -1583,9 +1579,9 @@ fn testHelp(opt: HelpOptions, str: []const u8) !void {
1583 defer std.testing.allocator.free(params); 1579 defer std.testing.allocator.free(params);
1584 1580
1585 var buf: [2048]u8 = undefined; 1581 var buf: [2048]u8 = undefined;
1586 var fbs = std.io.fixedBufferStream(&buf); 1582 var writer = std.Io.Writer.fixed(&buf);
1587 try help(fbs.writer(), Help, params, opt); 1583 try help(&writer, Help, params, opt);
1588 try std.testing.expectEqualStrings(str, fbs.getWritten()); 1584 try std.testing.expectEqualStrings(str, writer.buffered());
1589} 1585}
1590 1586
1591test "clap.help" { 1587test "clap.help" {
@@ -2015,9 +2011,9 @@ test "clap.help" {
2015/// 2011///
2016/// First all none value taking parameters, which have a short name are printed, then non 2012/// First all none value taking parameters, which have a short name are printed, then non
2017/// positional parameters and finally the positional. 2013/// positional parameters and finally the positional.
2018pub fn usage(stream: anytype, comptime Id: type, params: []const Param(Id)) !void { 2014pub fn usage(stream: *std.Io.Writer, comptime Id: type, params: []const Param(Id)) !void {
2019 var cos = ccw.codepointCountingWriter(stream); 2015 var cos = ccw.CodepointCountingWriter.init(stream);
2020 const cs = cos.writer(); 2016 const cs = &cos.interface;
2021 for (params) |param| { 2017 for (params) |param| {
2022 const name = param.names.short orelse continue; 2018 const name = param.names.short orelse continue;
2023 if (param.takes_value != .none) 2019 if (param.takes_value != .none)
@@ -2060,7 +2056,7 @@ pub fn usage(stream: anytype, comptime Id: type, params: []const Param(Id)) !voi
2060 try cs.writeAll("..."); 2056 try cs.writeAll("...");
2061 } 2057 }
2062 2058
2063 try cs.writeByte(']'); 2059 try cs.writeAll("]");
2064 } 2060 }
2065 2061
2066 if (!has_positionals) 2062 if (!has_positionals)
@@ -2083,9 +2079,9 @@ pub fn usage(stream: anytype, comptime Id: type, params: []const Param(Id)) !voi
2083 2079
2084fn testUsage(expected: []const u8, params: []const Param(Help)) !void { 2080fn testUsage(expected: []const u8, params: []const Param(Help)) !void {
2085 var buf: [1024]u8 = undefined; 2081 var buf: [1024]u8 = undefined;
2086 var fbs = std.io.fixedBufferStream(&buf); 2082 var writer = std.Io.Writer.fixed(&buf);
2087 try usage(fbs.writer(), Help, params); 2083 try usage(&writer, Help, params);
2088 try std.testing.expectEqualStrings(expected, fbs.getWritten()); 2084 try std.testing.expectEqualStrings(expected, writer.buffered());
2089} 2085}
2090 2086
2091test "usage" { 2087test "usage" {
diff --git a/clap/codepoint_counting_writer.zig b/clap/codepoint_counting_writer.zig
index c445c90..3518d48 100644
--- a/clap/codepoint_counting_writer.zig
+++ b/clap/codepoint_counting_writer.zig
@@ -1,30 +1,39 @@
1/// A Writer that counts how many codepoints has been written to it. 1/// A Writer that counts how many codepoints has been written to it.
2/// Expects valid UTF-8 input, and does not validate the input. 2/// Expects valid UTF-8 input, and does not validate the input.
3pub fn CodepointCountingWriter(comptime WriterType: type) type { 3pub const CodepointCountingWriter = struct {
4 return struct { 4 codepoints_written: u64 = 0,
5 codepoints_written: u64, 5 child_stream: *std.Io.Writer,
6 child_stream: WriterType, 6 interface: std.Io.Writer = .{
7 7 .buffer = &.{},
8 pub const Error = WriterType.Error || error{Utf8InvalidStartByte}; 8 .vtable = &.{ .drain = drain },
9 pub const Writer = std.io.Writer(*Self, Error, write); 9 },
10
11 const Self = @This();
12
13 pub fn init(child_stream: *std.Io.Writer) Self {
14 return .{
15 .child_stream = child_stream,
16 };
17 }
10 18
11 const Self = @This(); 19 fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
20 const self: *Self = @fieldParentPtr("interface", w);
21 var n_bytes_written: usize = 0;
22 var i: usize = 0;
12 23
13 pub fn write(self: *Self, bytes: []const u8) Error!usize { 24 while (i < data.len + splat - 1) : (i += 1) {
14 const bytes_and_codepoints = try utf8CountCodepointsAllowTruncate(bytes); 25 const chunk = data[@min(i, data.len)];
26 const bytes_and_codepoints = utf8CountCodepointsAllowTruncate(chunk) catch return std.Io.Writer.Error.WriteFailed;
15 // Might not be the full input, so the leftover bytes are written on the next call. 27 // Might not be the full input, so the leftover bytes are written on the next call.
16 const bytes_to_write = bytes[0..bytes_and_codepoints.bytes]; 28 const bytes_to_write = chunk[0..bytes_and_codepoints.bytes];
17 const amt = try self.child_stream.write(bytes_to_write); 29 const amt = try self.child_stream.write(bytes_to_write);
30 n_bytes_written += amt;
18 const bytes_written = bytes_to_write[0..amt]; 31 const bytes_written = bytes_to_write[0..amt];
19 self.codepoints_written += (try utf8CountCodepointsAllowTruncate(bytes_written)).codepoints; 32 self.codepoints_written += (utf8CountCodepointsAllowTruncate(bytes_written) catch return std.Io.Writer.Error.WriteFailed).codepoints;
20 return amt;
21 }
22
23 pub fn writer(self: *Self) Writer {
24 return .{ .context = self };
25 } 33 }
26 }; 34 return n_bytes_written;
27} 35 }
36};
28 37
29// Like `std.unicode.utf8CountCodepoints`, but on truncated input, it returns 38// Like `std.unicode.utf8CountCodepoints`, but on truncated input, it returns
30// the number of codepoints up to that point. 39// the number of codepoints up to that point.
@@ -58,44 +67,39 @@ fn utf8CountCodepointsAllowTruncate(s: []const u8) !struct { bytes: usize, codep
58 return .{ .bytes = i, .codepoints = len }; 67 return .{ .bytes = i, .codepoints = len };
59} 68}
60 69
61pub fn codepointCountingWriter(child_stream: anytype) CodepointCountingWriter(@TypeOf(child_stream)) {
62 return .{ .codepoints_written = 0, .child_stream = child_stream };
63}
64
65const testing = std.testing; 70const testing = std.testing;
66 71
67test CodepointCountingWriter { 72test CodepointCountingWriter {
68 var counting_stream = codepointCountingWriter(std.io.null_writer); 73 var discarding = std.Io.Writer.Discarding.init(&.{});
69 const stream = counting_stream.writer(); 74 var counting_stream = CodepointCountingWriter.init(&discarding.writer);
70 75
71 const utf8_text = "blåhaj" ** 100; 76 const utf8_text = "blåhaj" ** 100;
72 stream.writeAll(utf8_text) catch unreachable; 77 counting_stream.interface.writeAll(utf8_text) catch unreachable;
73 const expected_count = try std.unicode.utf8CountCodepoints(utf8_text); 78 const expected_count = try std.unicode.utf8CountCodepoints(utf8_text);
74 try testing.expectEqual(expected_count, counting_stream.codepoints_written); 79 try testing.expectEqual(expected_count, counting_stream.codepoints_written);
75} 80}
76 81
77test "handles partial UTF-8 writes" { 82test "handles partial UTF-8 writes" {
78 var buf: [100]u8 = undefined; 83 var buf: [100]u8 = undefined;
79 var fbs = std.io.fixedBufferStream(&buf); 84 var fbs = std.Io.Writer.fixed(&buf);
80 var counting_stream = codepointCountingWriter(fbs.writer()); 85 var counting_stream = CodepointCountingWriter.init(&fbs);
81 const stream = counting_stream.writer();
82 86
83 const utf8_text = "ååå"; 87 const utf8_text = "ååå";
84 // `å` is represented as `\xC5\xA5`, write 1.5 `å`s. 88 // `å` is represented as `\xC5\xA5`, write 1.5 `å`s.
85 var wc = try stream.write(utf8_text[0..3]); 89 var wc = try counting_stream.interface.write(utf8_text[0..3]);
86 // One should have been written fully. 90 // One should have been written fully.
87 try testing.expectEqual("å".len, wc); 91 try testing.expectEqual("å".len, wc);
88 try testing.expectEqual(1, counting_stream.codepoints_written); 92 try testing.expectEqual(1, counting_stream.codepoints_written);
89 93
90 // Write the rest, continuing from the reported number of bytes written. 94 // Write the rest, continuing from the reported number of bytes written.
91 wc = try stream.write(utf8_text[wc..]); 95 wc = try counting_stream.interface.write(utf8_text[wc..]);
92 try testing.expectEqual(4, wc); 96 try testing.expectEqual(4, wc);
93 try testing.expectEqual(3, counting_stream.codepoints_written); 97 try testing.expectEqual(3, counting_stream.codepoints_written);
94 98
95 const expected_count = try std.unicode.utf8CountCodepoints(utf8_text); 99 const expected_count = try std.unicode.utf8CountCodepoints(utf8_text);
96 try testing.expectEqual(expected_count, counting_stream.codepoints_written); 100 try testing.expectEqual(expected_count, counting_stream.codepoints_written);
97 101
98 try testing.expectEqualSlices(u8, utf8_text, fbs.getWritten()); 102 try testing.expectEqualSlices(u8, utf8_text, fbs.buffered());
99} 103}
100 104
101const std = @import("std"); 105const std = @import("std");
diff --git a/clap/streaming.zig b/clap/streaming.zig
index 4a687a2..fa5ec70 100644
--- a/clap/streaming.zig
+++ b/clap/streaming.zig
@@ -234,9 +234,9 @@ fn expectError(
234 234
235 while (parser.next() catch |err| { 235 while (parser.next() catch |err| {
236 var buf: [1024]u8 = undefined; 236 var buf: [1024]u8 = undefined;
237 var fbs = std.io.fixedBufferStream(&buf); 237 var fbs = std.Io.Writer.fixed(&buf);
238 diag.report(fbs.writer(), err) catch return error.TestFailed; 238 diag.report(&fbs, err) catch return error.TestFailed;
239 try std.testing.expectEqualStrings(expected, fbs.getWritten()); 239 try std.testing.expectEqualStrings(expected, fbs.buffered());
240 return; 240 return;
241 }) |_| {} 241 }) |_| {}
242 242
diff --git a/example/help.zig b/example/help.zig
index b80ee35..676a56a 100644
--- a/example/help.zig
+++ b/example/help.zig
@@ -17,8 +17,12 @@ pub fn main() !void {
17 // where `Id` has a `description` and `value` method (`Param(Help)` is one such parameter). 17 // where `Id` has a `description` and `value` method (`Param(Help)` is one such parameter).
18 // The last argument contains options as to how `help` should print those parameters. Using 18 // The last argument contains options as to how `help` should print those parameters. Using
19 // `.{}` means the default options. 19 // `.{}` means the default options.
20 if (res.args.help != 0) 20 if (res.args.help != 0) {
21 return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}); 21 var buf: [1024]u8 = undefined;
22 var stderr = std.fs.File.stderr().writer(&buf);
23 try clap.help(&stderr.interface, clap.Help, &params, .{});
24 return stderr.interface.flush();
25 }
22} 26}
23 27
24const clap = @import("clap"); 28const clap = @import("clap");
diff --git a/example/simple-ex.zig b/example/simple-ex.zig
index 22f657f..a993868 100644
--- a/example/simple-ex.zig
+++ b/example/simple-ex.zig
@@ -31,7 +31,11 @@ pub fn main() !void {
31 // allowed. 31 // allowed.
32 .assignment_separators = "=:", 32 .assignment_separators = "=:",
33 }) catch |err| { 33 }) catch |err| {
34 diag.report(std.io.getStdErr().writer(), err) catch {}; 34 // Report useful error and exit.
35 var buf: [1024]u8 = undefined;
36 var stderr = std.fs.File.stderr().writer(&buf);
37 try diag.report(&stderr.interface, err);
38 try stderr.interface.flush();
35 return err; 39 return err;
36 }; 40 };
37 defer res.deinit(); 41 defer res.deinit();
diff --git a/example/simple.zig b/example/simple.zig
index 2b7bf0a..ca6bd75 100644
--- a/example/simple.zig
+++ b/example/simple.zig
@@ -21,8 +21,10 @@ pub fn main() !void {
21 .allocator = gpa.allocator(), 21 .allocator = gpa.allocator(),
22 }) catch |err| { 22 }) catch |err| {
23 // Report useful error and exit. 23 // Report useful error and exit.
24 diag.report(std.io.getStdErr().writer(), err) catch {}; 24 var buf: [1024]u8 = undefined;
25 return err; 25 var stderr = std.fs.File.stderr().writer(&buf);
26 try diag.report(&stderr.interface, err);
27 return stderr.interface.flush();
26 }; 28 };
27 defer res.deinit(); 29 defer res.deinit();
28 30
diff --git a/example/streaming-clap.zig b/example/streaming-clap.zig
index 054c401..d60167c 100644
--- a/example/streaming-clap.zig
+++ b/example/streaming-clap.zig
@@ -34,8 +34,10 @@ pub fn main() !void {
34 // Because we use a streaming parser, we have to consume each argument parsed individually. 34 // Because we use a streaming parser, we have to consume each argument parsed individually.
35 while (parser.next() catch |err| { 35 while (parser.next() catch |err| {
36 // Report useful error and exit. 36 // Report useful error and exit.
37 diag.report(std.io.getStdErr().writer(), err) catch {}; 37 var buf: [1024]u8 = undefined;
38 return err; 38 var stderr = std.fs.File.stderr().writer(&buf);
39 try diag.report(&stderr.interface, err);
40 return stderr.interface.flush();
39 }) |arg| { 41 }) |arg| {
40 // arg.param will point to the parameter which matched the argument. 42 // arg.param will point to the parameter which matched the argument.
41 switch (arg.param.id) { 43 switch (arg.param.id) {
diff --git a/example/subcommands.zig b/example/subcommands.zig
index 8223f31..644e371 100644
--- a/example/subcommands.zig
+++ b/example/subcommands.zig
@@ -41,8 +41,10 @@ pub fn main() !void {
41 // not fully consumed. It can then be reused to parse the arguments for subcommands. 41 // not fully consumed. It can then be reused to parse the arguments for subcommands.
42 .terminating_positional = 0, 42 .terminating_positional = 0,
43 }) catch |err| { 43 }) catch |err| {
44 diag.report(std.io.getStdErr().writer(), err) catch {}; 44 var buf: [1024]u8 = undefined;
45 return err; 45 var stderr = std.fs.File.stderr().writer(&buf);
46 try diag.report(&stderr.interface, err);
47 return stderr.interface.flush();
46 }; 48 };
47 defer res.deinit(); 49 defer res.deinit();
48 50
@@ -77,8 +79,11 @@ fn mathMain(gpa: std.mem.Allocator, iter: *std.process.ArgIterator, main_args: M
77 .diagnostic = &diag, 79 .diagnostic = &diag,
78 .allocator = gpa, 80 .allocator = gpa,
79 }) catch |err| { 81 }) catch |err| {
80 diag.report(std.io.getStdErr().writer(), err) catch {}; 82 var buf: [1024]u8 = undefined;
81 return err; 83 var stderr = std.fs.File.stderr().writer(&buf);
84 try diag.report(&stderr.interface, err);
85 try stderr.interface.flush();
86 return err; // propagate error
82 }; 87 };
83 defer res.deinit(); 88 defer res.deinit();
84 89
diff --git a/example/usage.zig b/example/usage.zig
index 59ac84a..8bd25b7 100644
--- a/example/usage.zig
+++ b/example/usage.zig
@@ -16,8 +16,13 @@ pub fn main() !void {
16 16
17 // `clap.usage` is a function that can print a simple help message. It can print any `Param` 17 // `clap.usage` is a function that can print a simple help message. It can print any `Param`
18 // where `Id` has a `value` method (`Param(Help)` is one such parameter). 18 // where `Id` has a `value` method (`Param(Help)` is one such parameter).
19 if (res.args.help != 0) 19 if (res.args.help != 0) {
20 return clap.usage(std.io.getStdErr().writer(), clap.Help, &params); 20 var buf: [1024]u8 = undefined;
21 var stderr = std.fs.File.stderr().writer(&buf);
22 clap.usage(&stderr.interface, clap.Help, &params) catch {};
23 try stderr.interface.flush();
24 return;
25 }
21} 26}
22 27
23const clap = @import("clap"); 28const clap = @import("clap");