aboutsummaryrefslogtreecommitdiff
path: root/src/template.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/template.zig')
-rw-r--r--src/template.zig359
1 files changed, 255 insertions, 104 deletions
diff --git a/src/template.zig b/src/template.zig
index 210e77d..89521e3 100644
--- a/src/template.zig
+++ b/src/template.zig
@@ -10,13 +10,30 @@ const Env = @import("env.zig");
const fatal = cli.fatal;
pub const Config = struct {
+ const max_archives = 16;
+
+ pub const Archive = struct {
+ /// The id of the distribution file.
+ /// Default: empty (no distname suffix)
+ id: []const u8,
+ /// The path that should be the root of the archive (relative to the repository's root).
+ root: ?[]const u8 = null,
+ /// A list of paths that should be included in the archive.
+ /// All paths are relative to the root's root.
+ /// Default: empty (everything is included)
+ paths: [64][]const u8 = undefined,
+ paths_len: usize = 0,
+ };
+
raw: conf.Config,
- prepare_dist_target: ?[]const u8,
+ prepare_target: ?[]const u8,
+ archives: [max_archives]Archive,
+ archives_len: usize,
- fn from_file(env: Env, tmpl: Template) !Config {
+ fn from_file(env: *const Env, tmpl: Template) !Config {
const config_file = tmpl.config_file();
- const conf_res = try conf.Config.from_file(env.allocator, config_file.name());
+ const conf_res = try conf.Config.from_file(env.allocator, &config_file);
switch (conf_res) {
.conf => |c| return .init(env, c),
.err => |e| {
@@ -26,21 +43,69 @@ pub const Config = struct {
}
}
- fn init(env: Env, raw: conf.Config) Config {
- var prepare_dist_target: ?[]const u8 = null;
+ fn init(env: *const Env, raw: conf.Config) Config {
+ var dist_prepare_target: ?[]const u8 = null;
+ var archives: [max_archives]Archive = undefined;
+ var n_archives: usize = 0;
var iter = raw.iterate();
while (iter.next()) |v| {
- if (std.mem.eql(u8, v.key, "prepare-dist-target") and v.val.len > 0) {
- prepare_dist_target = v.val;
+ if (std.mem.eql(u8, v.key, "dist.prepare-target")) {
+ dist_prepare_target = if (v.val.len > 0) v.val else null;
+ } else if (std.mem.startsWith(u8, v.key, "dist.archive")) {
+ var archive_key = v.key[12..];
+ if (std.mem.findScalar(u8, archive_key, '.')) |dot| {
+ var archive_id: []const u8 = undefined;
+ if (dot == 0) {
+ archive_id = "";
+ } else if (archive_key[0] != '[' or archive_key[dot - 1] != ']') {
+ env.warn("unknown template configuration `{s}`", .{v.key});
+ continue;
+ } else {
+ archive_id = archive_key[1 .. dot - 1];
+ }
+ archive_key = archive_key[dot + 1 ..];
+
+ var archive: ?*Archive = null;
+ for (0..n_archives) |i| {
+ if (std.mem.eql(u8, archives[i].id, archive_id)) {
+ archive = &archives[i];
+ break;
+ }
+ }
+ if (archive == null) {
+ if (n_archives == max_archives) {
+ fatal("maximum number of distribution files exceeded", .{});
+ }
+ archive = &archives[n_archives];
+ archives[n_archives] = .{ .id = archive_id };
+ n_archives += 1;
+ }
+ if (std.mem.eql(u8, archive_key, "root")) {
+ archive.?.root = if (v.val.len > 0) v.val else null;
+ } else if (std.mem.eql(u8, archive_key, "paths")) {
+ archive.?.paths_len = split_paths(&archive.?.paths, v.val);
+ } else {
+ env.warn("unknown template configuration `{s}`", .{v.key});
+ }
+ } else {
+ env.warn("unknown template configuration `{s}`", .{v.key});
+ }
} else {
env.warn("unknown template configuration `{s}`", .{v.key});
}
}
+ if (n_archives == 0) {
+ archives[0] = .{ .id = "" };
+ n_archives = 1;
+ }
+
return .{
.raw = raw,
- .prepare_dist_target = prepare_dist_target,
+ .prepare_target = dist_prepare_target,
+ .archives = archives,
+ .archives_len = n_archives,
};
}
@@ -55,7 +120,7 @@ pub const Template = struct {
name: []const u8,
path: fs.Path,
- pub fn init(env: Env, template_name: []const u8) Template {
+ pub fn init(env: *const Env, template_name: []const u8) Template {
var template_path = etc_dir(env);
template_path.append(.{template_name});
return .{
@@ -64,47 +129,45 @@ pub const Template = struct {
};
}
- pub fn render(self: *const Template, env: Env, tmpl_conf: []const conf.Config, target: fs.Path) !void {
- var dir = try std.fs.openDirAbsoluteZ(self.path.name(), .{ .iterate = true });
- defer dir.close();
-
- var walker = try dir.walk(env.allocator);
- defer walker.deinit();
-
- while (try walker.next()) |entry| {
- if (std.mem.eql(u8, entry.path, config_filename)) {
- continue;
- }
+ pub fn render(self: *const Template, env: *const Env, tmpl_confs: []const conf.Config, target: *const fs.Path) !void {
+ try render_files_recursively(env, target, &self.path, "", tmpl_confs);
+ }
- const entry_path = try replace_vars_in_path(target, entry.path, tmpl_conf);
- switch (entry.kind) {
- .directory => {
- try fs.mkdir_all(entry_path);
+ fn render_files_recursively(env: *const Env, dest_root: *const fs.Path, src_root: *const fs.Path, src_subpath: []const u8, tmpl_confs: []const conf.Config) !void {
+ const path: fs.Path = .join(.{ src_root.name(), src_subpath });
+ var iter = try fs.iterate(&path);
+ defer iter.deinit();
+ while (try iter.next()) |entry| {
+ const entry_subpath: fs.Path = .join(.{ src_subpath, entry.name });
+ const src_path: fs.Path = .join(.{ src_root.name(), entry_subpath.name() });
+ const dest_path = try replace_vars_in_path(dest_root, entry_subpath.name(), tmpl_confs);
+ switch (entry.type) {
+ .dir => {
+ try fs.mkdir(&dest_path);
+ try render_files_recursively(env, dest_root, src_root, entry_subpath.name(), tmpl_confs);
},
.file => {
- env.info("writing template file {s}", .{entry_path.name()[target.len + 1 ..]});
- const input = try fs.read_file(env.allocator, .join(.{ self.path.name(), entry.path }));
+ env.info("writing template file {s}", .{dest_path.name()[dest_root.len + 1 ..]});
+ const input = try fs.read_file(env.allocator, &src_path);
- var output_file = try std.fs.createFileAbsoluteZ(entry_path.name(), .{ .mode = 0o644 });
- defer output_file.close();
+ var output_buf: [512]u8 = undefined;
+ var output_writer = try fs.file_writer(&dest_path, &output_buf, .{ .create = true });
+ defer output_writer.deinit();
- var write_buf: [1024]u8 = undefined;
- var output_writer = output_file.writer(&write_buf);
const output = &output_writer.interface;
-
- try replace_vars(output, input.?, tmpl_conf);
+ try replace_vars(output, input, tmpl_confs);
try output.flush();
},
- else => {},
+ else => {}, // skip
}
}
}
/// Does not flush the writer.
- fn replace_vars(w: *std.io.Writer, text: []const u8, tmpl_confs: []const conf.Config) !void {
+ fn replace_vars(w: *std.Io.Writer, text: []const u8, tmpl_confs: []const conf.Config) !void {
var rest = text;
while (rest.len > 0) {
- const pos = std.mem.indexOfScalar(u8, rest, '{') orelse {
+ const pos = std.mem.findScalar(u8, rest, '{') orelse {
try w.writeAll(rest);
return;
};
@@ -132,7 +195,7 @@ pub const Template = struct {
begin += 1;
}
- const end = std.mem.indexOfScalarPos(u8, rest, begin, '}') orelse return error.missing_variable_closing;
+ const end = std.mem.findScalarPos(u8, rest, begin, '}') orelse return error.missing_variable_closing;
if (end == rest.len - 1 or rest[end + 1] != '}') return error.missing_variable_closing;
const var_name = rest[begin..end];
@@ -155,31 +218,32 @@ pub const Template = struct {
}
}
- fn replace_vars_in_path(root: fs.Path, subpath: []const u8, tmpl_confs: []const conf.Config) !fs.Path {
+ fn replace_vars_in_path(root: *const fs.Path, subpath: []const u8, tmpl_confs: []const conf.Config) !fs.Path {
assert(root.len > 0);
assert(subpath.len > 0);
const slash = std.fs.path.sep;
- var res: fs.Path = root;
+ var res: fs.Path = root.*;
res.buf[res.len] = slash;
res.len += 1;
- var res_writer = std.io.Writer.fixed(res.buf[res.len..]);
+ var res_writer = std.Io.Writer.fixed(res.buf[res.len..]);
var name = subpath;
while (name.len > 0) {
- const idx = std.mem.indexOfScalar(u8, name, slash) orelse name.len - 1;
+ const idx = std.mem.findScalar(u8, name, slash) orelse name.len - 1;
const part = name[0 .. idx + 1];
const pos = res_writer.end;
replace_vars(&res_writer, part, tmpl_confs) catch |err| switch (err) {
error.missing_variable_closing, error.missing_variable_name => {
res_writer.undo(res_writer.end - pos);
- try res_writer.writeAll(part);
+ res_writer.writeAll(part) catch unreachable;
},
- else => return err,
+ error.WriteFailed => return error.PathTooLong,
+ else => unreachable,
};
name = name[idx + 1 ..];
}
- try res_writer.flush();
+ res_writer.flush() catch unreachable;
res.len += res_writer.end;
res.buf[res.len] = 0;
@@ -193,22 +257,19 @@ pub const Template = struct {
pub const Iterator = struct {
path: fs.Path,
- dir: ?std.fs.Dir = null,
- iter: ?std.fs.Dir.Iterator = null,
+ iter: ?fs.Iterator,
pub fn deinit(self: *Iterator) void {
- if (self.dir) |*dir| dir.close();
+ if (self.iter) |*iter| iter.deinit();
}
pub fn next(self: *Iterator) !?Template {
if (self.iter) |*iter| {
while (try iter.next()) |entry| {
- if (entry.kind == .directory) {
- var path = self.path;
- path.append(.{entry.name});
+ if (entry.type == .dir) {
return .{
.name = entry.name,
- .path = path,
+ .path = .join(.{ self.path.name(), entry.name }),
};
}
}
@@ -217,59 +278,45 @@ pub const Iterator = struct {
}
};
-pub inline fn iterate(env: Env) !Iterator {
- const path = etc_dir(env);
- const dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) catch |err| {
- if (err == error.FileNotFound) {
- return .{ .path = path };
- }
+pub inline fn iterate(env: *const Env) !Iterator {
+ var tmpl_iter: Iterator = .{ .path = etc_dir(env), .iter = undefined };
+ tmpl_iter.iter = fs.iterate(&tmpl_iter.path) catch |err| it: {
+ if (err == error.FileNotFound) break :it null;
return err;
};
- return .{
- .path = path,
- .dir = dir,
- .iter = dir.iterate(),
- };
+ return tmpl_iter;
}
-pub fn exists(env: Env, template_name: []const u8) !bool {
+pub fn exists(env: *const Env, template_name: []const u8) !bool {
return try get(env, template_name) != null;
}
-pub fn exists_any(env: Env) !bool {
- var iter = try iterate(env);
- defer iter.deinit();
- if (try iter.next()) |_| {
- return true;
- }
- return false;
-}
-
-pub fn get(env: Env, template_name: []const u8) !?Template {
+pub fn get(env: *const Env, template_name: []const u8) !?Template {
const tmpl: Template = .init(env, template_name);
- const info = try fs.dir_info(tmpl.path);
+ const info = try fs.dir_info(&tmpl.path);
return if (info.exists) tmpl else null;
}
-pub fn must_get(env: Env, template_name: []const u8) !Template {
+pub fn must_get(env: *const Env, template_name: []const u8) !Template {
return try get(env, template_name) orelse {
fatal("template not found: {s}", .{template_name});
};
}
-pub fn read_config(env: Env, tmpl: Template) !Config {
+pub fn read_config(env: *const Env, tmpl: Template) !Config {
return Config.from_file(env, tmpl);
}
-pub fn edit_config(env: Env, template_name: []const u8) !void {
+pub fn edit_config(env: *const Env, template_name: []const u8) !void {
const tmpl = try must_get(env, template_name);
- try shell.edit_config_file(env, tmpl.config_file(), verify_config);
+ const config_file = tmpl.config_file();
+ try shell.edit_config_file(env, &config_file, verify_config);
}
-pub fn import_config(env: Env, template_name: []const u8, new_config_file: fs.Path) !void {
+pub fn import_config(env: *const Env, template_name: []const u8, new_config_file: *const fs.Path) !void {
const tmpl = try must_get(env, template_name);
- const parsed = try conf.Config.from_file(env.allocator, new_config_file.name());
+ const parsed = try conf.Config.from_file(env.allocator, new_config_file);
const config = switch (parsed) {
.conf => |c| c,
.err => |e| {
@@ -282,29 +329,101 @@ pub fn import_config(env: Env, template_name: []const u8, new_config_file: fs.Pa
if (!verify_config(env, config)) {
std.process.exit(1);
}
- try fs.mv_file(new_config_file, tmpl.config_file());
+
+ const config_file = tmpl.config_file();
+ try fs.mv_file(new_config_file, &config_file);
}
-pub fn etc_dir(env: Env) fs.Path {
+pub fn etc_dir(env: *const Env) fs.Path {
return env.etc_path(.{"tmpl"});
}
-fn verify_config(env: Env, raw: conf.Config) bool {
+fn verify_config(env: *const Env, raw: conf.Config) bool {
const tmpl_conf: Config = .init(env, raw);
var valid = true;
- if (tmpl_conf.prepare_dist_target) |prepare_dist_target| {
- if (std.mem.indexOfAny(u8, prepare_dist_target, " \n\t\r")) |_| {
- cli.stderr.write_line("Error: invalid value `prepare-dist-target`", .{});
+ if (tmpl_conf.prepare_target) |prepare_target| {
+ if (std.mem.findAny(u8, prepare_target, " \n\t\r")) |_| {
+ cli.stderr.write_line("Error: invalid value `dist.prepare-target`", .{});
valid = false;
}
}
+ for (tmpl_conf.archives[0..tmpl_conf.archives_len]) |archive| {
+ if (archive.root) |root| {
+ if (!fs.is_relative(root)) {
+ if (archive.id.len > 0) {
+ cli.stderr.write_line("Error: value `dist.archive[{s}].root` is not a relative path", .{archive.id});
+ } else {
+ cli.stderr.write_line("Error: value `dist.archive.root` is not a relative path", .{});
+ }
+ valid = false;
+ }
+ }
+ for (archive.paths[0..archive.paths_len]) |path| {
+ if (!fs.is_relative(path)) {
+ if (archive.id.len > 0) {
+ cli.stderr.write_line("Error: value `dist.archive[{s}].paths` contains a non-relative path", .{archive.id});
+ } else {
+ cli.stderr.write_line("Error: value `dist.archive.paths` contains a non-relative path", .{});
+ }
+ valid = false;
+ }
+ }
+ }
return valid;
}
+fn split_paths(buf: [][]const u8, str: []const u8) usize {
+ var path_count: usize = 0;
+ var pos: usize = 0;
+ while (pos < str.len) {
+ pos = std.mem.findNonePos(u8, str, pos, " \t\r\n") orelse break;
+ var space = pos;
+ while (space < str.len) : (space += 1) {
+ switch (str[space]) {
+ ' ' => if (str[space - 1] != '\\') break,
+ '\t', '\r', '\n' => break,
+ else => {},
+ }
+ }
+
+ buf[path_count] = str[pos..space];
+ path_count += 1;
+ pos = space + 1;
+ }
+ return path_count;
+}
+
fn tmpl_corrupted(tmpl_name: []const u8, comptime reason: []const u8) noreturn {
fatal("template {s} corrupted (" ++ reason ++ ")", .{tmpl_name});
}
+test "parse config" {
+ var raw: conf.Config = .init(std.testing.allocator);
+ try raw.add_var("dist.prepare-target", "prepare-everything");
+ try raw.add_var("dist.archive.root", "foo");
+ try raw.add_var("dist.archive.paths", "foo");
+ try raw.add_var("dist.archive[bar].root", "foo");
+ try raw.add_var("dist.archive[bar].paths", "bar\nbaz");
+
+ const env: Env = undefined;
+ const config: Config = .init(&env, raw);
+ defer config.deinit();
+
+ try std.testing.expectEqualStrings("prepare-everything", config.prepare_target.?);
+ try std.testing.expectEqual(2, config.archives_len);
+
+ try std.testing.expectEqualStrings("", config.archives[0].id);
+ try std.testing.expectEqualStrings("foo", config.archives[0].root.?);
+ try std.testing.expectEqual(1, config.archives[0].paths_len);
+ try std.testing.expectEqualStrings("foo", config.archives[0].paths[0]);
+
+ try std.testing.expectEqualStrings("bar", config.archives[1].id);
+ try std.testing.expectEqualStrings("foo", config.archives[1].root.?);
+ try std.testing.expectEqual(2, config.archives[1].paths_len);
+ try std.testing.expectEqualStrings("bar", config.archives[1].paths[0]);
+ try std.testing.expectEqualStrings("baz", config.archives[1].paths[1]);
+}
+
test "render simple template - variable at end" {
const tmpl = "This is a simple {{test}} with multiple {{vars}} in different {{places}}";
@@ -316,7 +435,7 @@ test "render simple template - variable at end" {
try config.add_var("vars", "variables");
try config.add_var("places", "positions");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len);
defer rendered.deinit();
try Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -335,7 +454,7 @@ test "render simple template - variable at beginning" {
try config.add_var("vars", "variables");
try config.add_var("places", "positions");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len);
defer rendered.deinit();
try Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -348,7 +467,7 @@ test "render empty template" {
const config: conf.Config = .init(std.testing.allocator);
defer config.deinit();
- var rendered: std.io.Writer.Allocating = .init(std.testing.allocator);
+ var rendered: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer rendered.deinit();
try Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -364,7 +483,7 @@ test "render template without braces" {
try config.add_var("a", "foo");
try config.add_var("b", "bar");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
defer rendered.deinit();
try Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -380,7 +499,7 @@ test "render template without variable and brace" {
try config.add_var("a", "foo");
try config.add_var("b", "bar");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
defer rendered.deinit();
try Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -395,7 +514,7 @@ test "render template with multiple opening braces" {
try config.add_var("foo", "FOOBAR");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
defer rendered.deinit();
try Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -411,7 +530,7 @@ test "render template with missing variable closing" {
try config.add_var("a", "foo");
try config.add_var("b", "bar");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
defer rendered.deinit();
const res = Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -427,7 +546,7 @@ test "render template with missing variable closing at the end" {
try config.add_var("a", "foo");
try config.add_var("b", "bar");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
defer rendered.deinit();
const res = Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -443,7 +562,7 @@ test "render template with incomplete variable closing" {
try config.add_var("a", "foo");
try config.add_var("b", "bar");
- var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
+ var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len);
defer rendered.deinit();
const res = Template.replace_vars(&rendered.writer, tmpl, &.{config});
@@ -459,18 +578,50 @@ test "replace variables in path" {
var replaced: fs.Path = undefined;
- replaced = try Template.replace_vars_in_path(.init("root"), ".config/foo", &.{config});
- try std.testing.expectEqualStrings("root/.config/foo", replaced.name());
+ inline for ([_]struct {
+ root: []const u8,
+ subpath: []const u8,
+ expected: []const u8,
+ }{
+ .{ .root = "root", .subpath = ".config/foo", .expected = "root/.config/foo" },
+ .{ .root = "/usr/local", .subpath = "etc", .expected = "/usr/local/etc" },
+ .{ .root = "/usr/local", .subpath = "etc/{{a}}.{{b}}", .expected = "/usr/local/etc/foo.bar" },
+ .{ .root = "/usr/local", .subpath = "etc/{{/foo", .expected = "/usr/local/etc/{{/foo" },
+ .{ .root = "/usr/local", .subpath = "etc/{{}}/foo", .expected = "/usr/local/etc/{{}}/foo" },
+ }) |t| {
+ const root: fs.Path = .init(t.root);
+ replaced = try Template.replace_vars_in_path(&root, t.subpath, &.{config});
+ try std.testing.expectEqualStrings(t.expected, replaced.name());
+ }
+}
+
+test "split paths" {
+ var paths: [8][]const u8 = undefined;
+ var n: usize = undefined;
+
+ n = split_paths(&paths, "");
+ try std.testing.expectEqual(0, n);
+
+ n = split_paths(&paths, "dir");
+ try std.testing.expectEqual(1, n);
+ try std.testing.expectEqualStrings("dir", paths[0]);
- replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc", &.{config});
- try std.testing.expectEqualStrings("/usr/local/etc", replaced.name());
+ n = split_paths(&paths, "dir/subdir");
+ try std.testing.expectEqual(1, n);
+ try std.testing.expectEqualStrings("dir/subdir", paths[0]);
- replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{a}}.{{b}}", &.{config});
- try std.testing.expectEqualStrings("/usr/local/etc/foo.bar", replaced.name());
+ n = split_paths(&paths, "dir/sub\\ dir");
+ try std.testing.expectEqual(1, n);
+ try std.testing.expectEqualStrings("dir/sub\\ dir", paths[0]);
- replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{/foo", &.{config});
- try std.testing.expectEqualStrings("/usr/local/etc/{{/foo", replaced.name());
+ n = split_paths(&paths, "dir_one/subdir dir_two");
+ try std.testing.expectEqual(2, n);
+ try std.testing.expectEqualStrings("dir_one/subdir", paths[0]);
+ try std.testing.expectEqualStrings("dir_two", paths[1]);
- replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{}}/foo", &.{config});
- try std.testing.expectEqualStrings("/usr/local/etc/{{}}/foo", replaced.name());
+ n = split_paths(&paths, "dir_one/subdir dir_two\ndir_three/foo\\ bar");
+ try std.testing.expectEqual(3, n);
+ try std.testing.expectEqualStrings("dir_one/subdir", paths[0]);
+ try std.testing.expectEqualStrings("dir_two", paths[1]);
+ try std.testing.expectEqualStrings("dir_three/foo\\ bar", paths[2]);
}