Merge pull request #749 from mitchellh/command-args

Command args, `-e` flag
This commit is contained in:
Mitchell Hashimoto
2023-10-27 18:29:18 -07:00
committed by GitHub
3 changed files with 125 additions and 15 deletions

View File

@ -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: {

View File

@ -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.
@ -1135,6 +1145,37 @@ 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
self.@"command-arg".list.clearRetainingCapacity();
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`
@ -1268,6 +1309,69 @@ 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 "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;

View File

@ -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();
};