From 9c56bd5dba8e7a87ef1578e955c46d0e90c127b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Oct 2023 15:38:50 -0700 Subject: [PATCH 1/4] config: command-arg to specify arguments to the executed command Fixes #744 --- src/config/Config.zig | 10 ++++++++++ src/termio/Exec.zig | 31 ++++++++++++++++--------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e9c1d95af..a527cd70e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -268,6 +268,16 @@ palette: Palette = .{}, /// command: ?[]const u8 = null, +/// A single argument to pass to the command. This can be repeated to +/// pass multiple arguments. This slightly clunky configuration style is +/// so that Ghostty doesn't have to perform any sort of shell parsing +/// to find argument boundaries. +/// +/// This cannot be used to override argv[0]. argv[0] will always be +/// set by Ghostty to be the command (possibly with a hyphen-prefix to +/// indicate that it is a login shell, depending on the OS). +@"command-arg": RepeatableString = .{}, + /// The directory to change to after starting the command. /// /// The default is "inherit" except in special scenarios listed next. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d4d80d696..6db769dc6 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -744,24 +744,25 @@ const Subprocess = struct { env.remove("GHOSTTY_MAC_APP"); } - // If we're NOT in a flatpak (usually!), then we just exec the - // process directly. If we are in a flatpak, we use flatpak-spawn - // to escape the sandbox. - const args = if (!internal_os.isFlatpak()) try alloc.dupe( - []const u8, - &[_][]const u8{argv0_override orelse path}, - ) else args: { - var args = try std.ArrayList([]const u8).initCapacity(alloc, 8); + // Build our args list + const args = args: { + const cap = 1 + opts.full_config.@"command-arg".list.items.len; + var args = try std.ArrayList([]const u8).initCapacity(alloc, cap); defer args.deinit(); - // We run our shell wrapped in a /bin/sh login shell because - // some systems do not properly initialize the env vars unless - // we start this way (NixOS!) - try args.append("/bin/sh"); - try args.append("-l"); - try args.append("-c"); - try args.append(path); + if (!internal_os.isFlatpak()) { + try args.append(argv0_override orelse path); + } else { + // We run our shell wrapped in a /bin/sh login shell because + // some systems do not properly initialize the env vars unless + // we start this way (NixOS!) + try args.append("/bin/sh"); + try args.append("-l"); + try args.append("-c"); + try args.append(path); + } + try args.appendSlice(opts.full_config.@"command-arg".list.items); break :args try args.toOwnedSlice(); }; From 4104f78cba322da7210124aa3b8dc82e1ac5f545 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Oct 2023 15:57:20 -0700 Subject: [PATCH 2/4] cli: handle "-e" as the command to execute --- src/cli/args.zig | 5 +++++ src/config/Config.zig | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/cli/args.zig b/src/cli/args.zig index fcb4c039c..eb91c43a8 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -64,6 +64,11 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void { }; while (iter.next()) |arg| { + // Do manual parsing if we have a hook for it. + if (@hasDecl(T, "parseManuallyHook")) { + if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return; + } + if (mem.startsWith(u8, arg, "--")) { var key: []const u8 = arg[2..]; const value: ?[]const u8 = value: { diff --git a/src/config/Config.zig b/src/config/Config.zig index a527cd70e..0bd1410ef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1145,6 +1145,36 @@ pub fn finalize(self: *Config) !void { if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height"); } +/// Callback for src/cli/args.zig to allow us to handle special cases +/// like `--help` or `-e`. Returns "false" if the CLI parsing should halt. +pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool { + // If it isn't "-e" then we just continue parsing normally. + if (!std.mem.eql(u8, arg, "-e")) return true; + + // The first value is the command to run. + if (iter.next()) |command| { + self.command = try alloc.dupe(u8, command); + } else { + try self._errors.add(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "missing command after -e", + .{}, + ), + }); + + return false; + } + + // All further arguments are parameters + while (iter.next()) |param| { + try self.@"command-arg".parseCLI(alloc, param); + } + + // Do not continue, we consumed everything. + return false; +} + /// Create a shallow copy of this config. This will share all the memory /// allocated with the previous config but will have a new arena for /// any changes or new allocations. The config should have `deinit` From 4f62526782399fc86602f9dd59f5b7f4fe789cf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Oct 2023 18:24:13 -0700 Subject: [PATCH 3/4] config: tests for -e parsing --- src/config/Config.zig | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 0bd1410ef..b391a1076 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1308,6 +1308,54 @@ pub const ChangeIterator = struct { } }; +const TestIterator = struct { + data: []const []const u8, + i: usize = 0, + + pub fn next(self: *TestIterator) ?[]const u8 { + if (self.i >= self.data.len) return null; + const result = self.data[self.i]; + self.i += 1; + return result; + } +}; + +test "parse hook: invalid command" { + const testing = std.testing; + var cfg = try Config.default(testing.allocator); + defer cfg.deinit(); + const alloc = cfg._arena.?.allocator(); + + var it: TestIterator = .{ .data = &.{"foo"} }; + try testing.expect(try cfg.parseManuallyHook(alloc, "--command", &it)); + try testing.expect(cfg.command == null); +} + +test "parse e: command only" { + const testing = std.testing; + var cfg = try Config.default(testing.allocator); + defer cfg.deinit(); + const alloc = cfg._arena.?.allocator(); + + var it: TestIterator = .{ .data = &.{"foo"} }; + try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); + try testing.expectEqualStrings("foo", cfg.command.?); +} + +test "parse e: command and args" { + const testing = std.testing; + var cfg = try Config.default(testing.allocator); + defer cfg.deinit(); + const alloc = cfg._arena.?.allocator(); + + var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; + try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); + try testing.expectEqualStrings("echo", cfg.command.?); + try testing.expectEqual(@as(usize, 2), cfg.@"command-arg".list.items.len); + try testing.expectEqualStrings("foo", cfg.@"command-arg".list.items[0]); + try testing.expectEqualStrings("bar baz", cfg.@"command-arg".list.items[1]); +} + test "clone default" { const testing = std.testing; const alloc = testing.allocator; From 2c541a7e86df205d02374a07f4b10a4c0ca83f14 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Oct 2023 18:26:44 -0700 Subject: [PATCH 4/4] config: -e replaces previous args --- src/config/Config.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index b391a1076..453913937 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1167,6 +1167,7 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: } // All further arguments are parameters + self.@"command-arg".list.clearRetainingCapacity(); while (iter.next()) |param| { try self.@"command-arg".parseCLI(alloc, param); } @@ -1356,6 +1357,21 @@ test "parse e: command and args" { try testing.expectEqualStrings("bar baz", cfg.@"command-arg".list.items[1]); } +test "parse e: command replaces args" { + const testing = std.testing; + var cfg = try Config.default(testing.allocator); + defer cfg.deinit(); + const alloc = cfg._arena.?.allocator(); + + try cfg.@"command-arg".parseCLI(alloc, "foo"); + try testing.expectEqual(@as(usize, 1), cfg.@"command-arg".list.items.len); + + var it: TestIterator = .{ .data = &.{"echo"} }; + try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); + try testing.expectEqualStrings("echo", cfg.command.?); + try testing.expectEqual(@as(usize, 0), cfg.@"command-arg".list.items.len); +} + test "clone default" { const testing = std.testing; const alloc = testing.allocator;