const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const cli = @import("cli.zig"); const fs = @import("fs.zig"); const conf = @import("conf.zig"); const shell = @import("shell.zig"); const Env = @import("env.zig"); const fatal = cli.fatal; pub const Config = struct { raw: conf.Config, prepare_dist_target: ?[]const u8, fn from_file(env: Env, tmpl: Template) !Config { const config_file = tmpl.config_file(); const conf_res = try conf.Config.from_file(env.allocator, config_file.name()); switch (conf_res) { .conf => |c| return .init(env, c), .err => |e| { env.err("{s} (line {d})", .{ e.msg, e.line }); tmpl_corrupted(tmpl.name, "invalid configuration"); }, } } fn init(env: Env, raw: conf.Config) Config { var prepare_dist_target: ?[]const u8 = null; 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; } else { env.warn("unknown template configuration `{s}`", .{v.key}); } } return .{ .raw = raw, .prepare_dist_target = prepare_dist_target, }; } pub fn deinit(self: Config) void { self.raw.deinit(); } }; pub const Template = struct { const config_filename = ".config"; name: []const u8, path: fs.Path, pub fn init(env: Env, template_name: []const u8) Template { var template_path = etc_dir(env); template_path.append(.{template_name}); return .{ .name = template_name, .path = template_path, }; } 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; } const entry_path = try replace_vars_in_path(target, entry.path, tmpl_conf); switch (entry.kind) { .directory => { try fs.mkdir_all(entry_path); }, .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 })); var output_file = try std.fs.createFileAbsoluteZ(entry_path.name(), .{ .mode = 0o644 }); defer output_file.close(); 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 output.flush(); }, else => {}, } } } /// Does not flush the writer. 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 { try w.writeAll(rest); return; }; if (pos > 0) { try w.writeAll(rest[0..pos]); rest = rest[pos..]; } assert(rest.len > 0 and rest[0] == '{'); if (rest.len == 1) { try w.writeByte(rest[0]); return; } if (rest[1] != '{') { try w.writeAll(rest[0..2]); rest = rest[2..]; continue; } // Consume all opening braces to take the innermost pair. var begin: usize = 2; while (begin < rest.len and rest[begin] == '{') { try w.writeByte('{'); begin += 1; } const end = std.mem.indexOfScalarPos(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]; if (var_name.len == 0) return error.missing_variable_name; var found_var = false; for (tmpl_confs) |config| { if (config.find_var(var_name)) |v| { if (v.val.len > 0) { try w.writeAll(v.val); } found_var = true; break; } } if (!found_var) { fatal("variable `{s}` not defined", .{var_name}); } rest = rest[end + 2 ..]; } } fn replace_vars_in_path(root: 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; res.buf[res.len] = slash; res.len += 1; 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 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); }, else => return err, }; name = name[idx + 1 ..]; } try res_writer.flush(); res.len += res_writer.end; res.buf[res.len] = 0; return res; } fn config_file(self: Template) fs.Path { return .join(.{ self.path.name(), config_filename }); } }; pub const Iterator = struct { path: fs.Path, dir: ?std.fs.Dir = null, iter: ?std.fs.Dir.Iterator = null, pub fn deinit(self: *Iterator) void { if (self.dir) |*dir| dir.close(); } 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}); return .{ .name = entry.name, .path = path, }; } } } return null; } }; 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 }; } return err; }; return .{ .path = path, .dir = dir, .iter = dir.iterate(), }; } pub fn exists(env: 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 { const tmpl: Template = .init(env, template_name); 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 { return try get(env, template_name) orelse { fatal("template not found: {s}", .{template_name}); }; } pub fn read_config(env: Env, tmpl: Template) !Config { return Config.from_file(env, tmpl); } pub fn edit_config(env: 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); } pub fn import_config(env: Env, template_name: []const u8, new_config_file: 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 config = switch (parsed) { .conf => |c| c, .err => |e| { env.err("{s} (line {d})", .{ e.msg, e.line }); fatal("failed to import {s}", .{new_config_file.name()}); }, }; defer config.deinit(); if (!verify_config(env, config)) { std.process.exit(1); } try fs.mv_file(new_config_file, tmpl.config_file()); } pub fn etc_dir(env: Env) fs.Path { return env.etc_path(.{"tmpl"}); } fn verify_config(env: 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`", .{}); valid = false; } } return valid; } fn tmpl_corrupted(tmpl_name: []const u8, comptime reason: []const u8) noreturn { fatal("template {s} corrupted (" ++ reason ++ ")", .{tmpl_name}); } test "render simple template - variable at end" { const tmpl = "This is a simple {{test}} with multiple {{vars}} in different {{places}}"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); try config.add_var("test", "template"); try config.add_var("not", "used"); 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); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectEqualStrings("This is a simple template with multiple variables in different positions", rendered.written()); } test "render simple template - variable at beginning" { const tmpl = "{{this}} is a simple {{test}} with multiple {{vars}} in different places"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); try config.add_var("this", "This"); try config.add_var("test", "template"); try config.add_var("not", "used"); 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); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectEqualStrings("This is a simple template with multiple variables in different places", rendered.written()); } test "render empty template" { const tmpl = ""; const config: conf.Config = .init(std.testing.allocator); defer config.deinit(); var rendered: std.io.Writer.Allocating = .init(std.testing.allocator); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectEqualStrings("", rendered.written()); } test "render template without braces" { const tmpl = "No braces at all"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); 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); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectEqualStrings("No braces at all", rendered.written()); } test "render template without variable and brace" { const tmpl = "{No variable seen here {"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); 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); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectEqualStrings("{No variable seen here {", rendered.written()); } test "render template with multiple opening braces" { const tmpl = "Use ${{{foo}}} in your env."; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); try config.add_var("foo", "FOOBAR"); var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectEqualStrings("Use ${FOOBAR} in your env.", rendered.written()); } test "render template with missing variable closing" { const tmpl = "Invalid template {{foo"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); 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); defer rendered.deinit(); const res = Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectError(error.missing_variable_closing, res); } test "render template with missing variable closing at the end" { const tmpl = "Invalid template {{"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); 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); defer rendered.deinit(); const res = Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectError(error.missing_variable_closing, res); } test "render template with incomplete variable closing" { const tmpl = "Invalid {{foo} template"; var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); 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); defer rendered.deinit(); const res = Template.replace_vars(&rendered.writer, tmpl, &.{config}); try std.testing.expectError(error.missing_variable_closing, res); } test "replace variables in path" { var config: conf.Config = .init(std.testing.allocator); defer config.deinit(); try config.add_var("a", "foo"); try config.add_var("b", "bar"); 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()); replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc", &.{config}); try std.testing.expectEqualStrings("/usr/local/etc", replaced.name()); 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()); replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{/foo", &.{config}); try std.testing.expectEqualStrings("/usr/local/etc/{{/foo", replaced.name()); replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{}}/foo", &.{config}); try std.testing.expectEqualStrings("/usr/local/etc/{{}}/foo", replaced.name()); }