const std = @import("std"); const assert = std.debug.assert; const cli = @import("cli.zig"); const fs = @import("fs.zig"); const conf = @import("conf.zig"); const Env = @import("env.zig"); const fatal = @import("cli.zig").fatal; pub const Exec_Args = struct { env: Env, argv: []const []const u8, cwd: ?fs.Path = null, }; pub const Exec_Result = union(enum) { success: []const u8, failure: []const u8, }; pub fn exec(args: Exec_Args) Exec_Result { assert(args.argv.len > 0); const res = std.process.Child.run(.{ .allocator = args.env.allocator, .argv = args.argv, .cwd = if (args.cwd) |cwd| cwd.name() else null, .cwd_dir = if (args.cwd == null) std.fs.cwd() else null, }) catch |err| { fatal("error executing {s}: {s}", .{ args.argv[0], @errorName(err) }); }; switch (res.term) { .Exited => |code| { if (code == 0) { args.env.dealloc(res.stderr); return .{ .success = res.stdout }; } else if (res.stderr.len > 0) { args.env.dealloc(res.stdout); return .{ .failure = res.stderr }; } else { args.env.dealloc(res.stderr); return .{ .failure = res.stdout }; } }, .Signal => fatal("signal received executing {s}", .{args.argv[0]}), .Stopped => fatal("stopped executing {s}", .{args.argv[0]}), .Unknown => fatal("unknown error executing {s}", .{args.argv[0]}), } } pub fn run(args: Exec_Args) void { assert(args.argv.len > 0); var child = std.process.Child.init(args.argv, args.env.allocator); if (args.cwd) |cwd| child.cwd = cwd.name(); child.cwd_dir = if (args.cwd == null) std.fs.cwd() else null; child.spawn() catch |err| { fatal("error executing {s}: {s}", .{ args.argv[0], @errorName(err) }); }; errdefer _ = child.kill() catch {}; const term = child.wait() catch |err| { fatal("error waiting for {s}: {s}", .{ args.argv[0], @errorName(err) }); }; switch (term) { .Exited => |code| if (code != 0) std.process.exit(code), .Signal => fatal("signal received executing {s}", .{args.argv[0]}), .Stopped => fatal("stopped executing {s}", .{args.argv[0]}), .Unknown => fatal("unknown error executing {s}", .{args.argv[0]}), } } pub fn edit_config_file(env: Env, file: fs.Path, comptime verify: fn (env: Env, vars: conf.Config) bool) !void { const tmp_file = tmp_edit_file(env, file); fs.cp_file(file, tmp_file) catch |err| { switch (err) { error.FileNotFound => try fs.touch(tmp_file), else => return err, } }; edit: while (true) { const vars: ?conf.Config = parse: while (true) { try exec_editor(env, tmp_file); const parsed = try conf.Config.from_file(env.allocator, file.name()); switch (parsed) { .conf => |c| break :parse c, .err => |e| { env.err("{s} (line {d})", .{ e.msg, e.line }); switch (ask_edit_again()) { .again => continue, .cancel => break :parse null, } }, } }; if (vars) |v| { defer v.deinit(); if (verify(env, v)) { try fs.mv_file(tmp_file, file); return; } switch (ask_edit_again()) { .again => continue, .cancel => break :edit, } } else { break :edit; } } try fs.rm(tmp_file); } pub fn edit_config_file_noverify(env: Env, file: fs.Path) !void { try edit_config_file(env, file, struct { fn noverify(_: Env, _: conf.Config) bool { return true; } }.noverify); } fn ask_edit_again() enum { again, cancel } { while (true) { var again_buf: [16]u8 = undefined; const again = cli.ask("Edit again? (yes/no) [yes] ", .{}, &again_buf) orelse "yes"; for (again, 0..) |c, i| again_buf[i] = std.ascii.toLower(c); if (std.mem.eql(u8, again, "y") or std.mem.eql(u8, again, "yes")) { return .again; } if (std.mem.eql(u8, again, "n") or std.mem.eql(u8, again, "no")) { cli.stderr.print_line("cancelled (changes discarded)", .{}); cli.stderr.flush(); return .cancel; } } } fn exec_editor(env: Env, file: fs.Path) !void { const pid = std.posix.fork() catch |err| { fatal("cannot fork editor: {s}", .{@errorName(err)}); }; if (pid == 0) { // editor process var editor_env: [512:null]?[*:0]const u8 = undefined; var editor_envlen: usize = 0; const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511]; for (environ) |e| { editor_env[editor_envlen] = e; editor_envlen += 1; } editor_env[editor_envlen] = null; var configured_editor_buf: [std.fs.max_path_bytes]u8 = undefined; var configured_editor: ?[:0]u8 = null; if (env.editor_cmd) |cmd| { @memcpy(configured_editor_buf[0..cmd.len], cmd); configured_editor_buf[cmd.len] = 0; configured_editor = configured_editor_buf[0..cmd.len :0]; } const editor = configured_editor orelse std.posix.getenv("VISUAL") orelse std.posix.getenv("EDITOR") orelse "vi"; const editor_argv: [*:null]const ?[*:0]const u8 = &.{ editor, file.name(), null }; std.posix.execvpeZ(editor_argv[0].?, editor_argv, &editor_env) catch unreachable; std.process.exit(1); } _ = std.posix.waitpid(pid, 0); } /// Derive the filename for the temporary file of `file`. /// The filename is "{relname}.{timestamp}", where `relname` is the relative /// part of file regarding to etc. fn tmp_edit_file(env: Env, file: fs.Path) fs.Path { const relname = file.relname(env.etc_path(.{})).?; var tmp_filename_buf: [std.fs.max_name_bytes]u8 = undefined; var tmp_filename_len: usize = 0; tmp_filename_len += std.fmt.printInt(tmp_filename_buf[tmp_filename_len..], std.time.timestamp(), 16, .lower, .{}); tmp_filename_buf[tmp_filename_len] = '.'; tmp_filename_len += 1; for (relname) |c| { tmp_filename_buf[tmp_filename_len] = if (c == std.fs.path.sep) '_' else c; tmp_filename_len += 1; } return env.work_path(.{ "edit", tmp_filename_buf[0..tmp_filename_len] }); }