mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-21 06:45:22 +00:00
config: allow commands to specify whether they shell expand or not
This introduces a syntax for `command` and `initial-command` that allows
the user to specify whether it should be run via `/bin/sh -c` or not.
The syntax is a prefix `direct:` or `shell:` prior to the command,
with no prefix implying a default behavior as documented.
Previously, we unconditionally ran commands via `/bin/sh -c`, primarily
to avoid having to do any shell expansion ourselves. We also leaned on
it as a crutch for PATH-expansion but this is an easy problem compared
to shell expansion.
For the principle of least surprise, this worked well for configurations
specified via the config file, and is still the default. However, these
configurations are also set via the `-e` special flag to the CLI, and it
is very much not the principle of least surprise to have the command run via
`/bin/sh -c` in that scenario since a shell has already expanded all the
arguments and given them to us in a nice separated format. But we had no
way to toggle this behavior.
This commit introduces the ability to do this, and changes the defaults
so that `-e` doesn't shell expand. Further, we also do PATH lookups
ourselves for the non-shell expanded case because thats easy (using
execvpe style extensions but implemented as part of the Zig stdlib). We don't
do path expansion (e.g. `~/`) because thats a shell expansion.
So to be clear, there are no two polar opposite behavioes here with
clear semantics:
1. Direct commands are passed to `execvpe` directly, space separated.
This will not handle quoted strings, environment variables, path
expansion (e.g. `~/`), command expansion (e.g. `$()`), etc.
2. Shell commands are passed to `/bin/sh -c` and will be shell expanded
as per the shell's rules. This will handle everything that `sh`
supports.
In doing this work, I also stumbled upon a variety of smaller
improvements that could be made:
- A number of allocations have been removed from the startup path that
only existed to add a null terminator to various strings. We now
have null terminators from the beginning since we are almost always
on a system that's going to need it anyways.
- For bash shell integration, we no longer wrap the new bash command
in a shell since we've formed a full parsed command line.
- The process of creating the command to execute by termio is now unit
tested, so we can test the various complex cases particularly on
macOS of wrapping commands in the login command.
- `xdg-terminal-exec` on Linux uses the `direct:` method by default
since it is also assumed to be executed via a shell environment.
This commit is contained in:
@@ -24,6 +24,7 @@ const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool;
|
||||
const ptypkg = @import("../pty.zig");
|
||||
const Pty = ptypkg.Pty;
|
||||
const EnvMap = std.process.EnvMap;
|
||||
const PasswdEntry = internal_os.passwd.Entry;
|
||||
const windows = internal_os.windows;
|
||||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
@@ -725,7 +726,7 @@ pub const ThreadData = struct {
|
||||
};
|
||||
|
||||
pub const Config = struct {
|
||||
command: ?[]const u8 = null,
|
||||
command: ?configpkg.Command = null,
|
||||
env: EnvMap,
|
||||
env_override: configpkg.RepeatableStringMap = .{},
|
||||
shell_integration: configpkg.Config.ShellIntegration = .detect,
|
||||
@@ -746,7 +747,7 @@ const Subprocess = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
cwd: ?[]const u8,
|
||||
env: ?EnvMap,
|
||||
args: [][]const u8,
|
||||
args: []const [:0]const u8,
|
||||
grid_size: renderer.GridSize,
|
||||
screen_size: renderer.ScreenSize,
|
||||
pty: ?Pty = null,
|
||||
@@ -892,18 +893,29 @@ const Subprocess = struct {
|
||||
env.remove("VTE_VERSION");
|
||||
|
||||
// Setup our shell integration, if we can.
|
||||
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: {
|
||||
const default_shell_command = cfg.command orelse switch (builtin.os.tag) {
|
||||
.windows => "cmd.exe",
|
||||
else => "sh",
|
||||
};
|
||||
const shell_command: configpkg.Command = shell: {
|
||||
const default_shell_command: configpkg.Command =
|
||||
cfg.command orelse .{ .shell = switch (builtin.os.tag) {
|
||||
.windows => "cmd.exe",
|
||||
else => "sh",
|
||||
} };
|
||||
|
||||
const force: ?shell_integration.Shell = switch (cfg.shell_integration) {
|
||||
.none => {
|
||||
// Even if shell integration is none, we still want to set up the feature env vars
|
||||
try shell_integration.setupFeatures(&env, cfg.shell_integration_features);
|
||||
break :shell .{ null, default_shell_command };
|
||||
// Even if shell integration is none, we still want to
|
||||
// set up the feature env vars
|
||||
try shell_integration.setupFeatures(
|
||||
&env,
|
||||
cfg.shell_integration_features,
|
||||
);
|
||||
|
||||
// This is a source of confusion for users despite being
|
||||
// opt-in since it results in some Ghostty features not
|
||||
// working. We always want to log it.
|
||||
log.info("shell integration disabled by configuration", .{});
|
||||
break :shell default_shell_command;
|
||||
},
|
||||
|
||||
.detect => null,
|
||||
.bash => .bash,
|
||||
.elvish => .elvish,
|
||||
@@ -911,9 +923,9 @@ const Subprocess = struct {
|
||||
.zsh => .zsh,
|
||||
};
|
||||
|
||||
const dir = cfg.resources_dir orelse break :shell .{
|
||||
null,
|
||||
default_shell_command,
|
||||
const dir = cfg.resources_dir orelse {
|
||||
log.warn("no resources dir set, shell integration disabled", .{});
|
||||
break :shell default_shell_command;
|
||||
};
|
||||
|
||||
const integration = try shell_integration.setup(
|
||||
@@ -923,19 +935,18 @@ const Subprocess = struct {
|
||||
&env,
|
||||
force,
|
||||
cfg.shell_integration_features,
|
||||
) orelse break :shell .{ null, default_shell_command };
|
||||
) orelse {
|
||||
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
|
||||
break :shell default_shell_command;
|
||||
};
|
||||
|
||||
break :shell .{ integration.shell, integration.command };
|
||||
};
|
||||
|
||||
if (integrated_shell) |shell| {
|
||||
log.info(
|
||||
"shell integration automatically injected shell={}",
|
||||
.{shell},
|
||||
.{integration.shell},
|
||||
);
|
||||
} else if (cfg.shell_integration != .none) {
|
||||
log.warn("shell could not be detected, no automatic shell integration will be injected", .{});
|
||||
}
|
||||
|
||||
break :shell integration.command;
|
||||
};
|
||||
|
||||
// Add the environment variables that override any others.
|
||||
{
|
||||
@@ -947,134 +958,29 @@ const Subprocess = struct {
|
||||
}
|
||||
|
||||
// Build our args list
|
||||
const args = args: {
|
||||
const cap = 9; // the most we'll ever use
|
||||
var args = try std.ArrayList([]const u8).initCapacity(alloc, cap);
|
||||
defer args.deinit();
|
||||
const args: []const [:0]const u8 = execCommand(
|
||||
alloc,
|
||||
shell_command,
|
||||
internal_os.passwd,
|
||||
) catch |err| switch (err) {
|
||||
// If we fail to allocate space for the command we want to
|
||||
// execute, we'd still like to try to run something so
|
||||
// Ghostty can launch (and maybe the user can debug this further).
|
||||
// Realistically, if you're getting OOM, I think other stuff is
|
||||
// about to crash, but we can try.
|
||||
error.OutOfMemory => oom: {
|
||||
log.warn("failed to allocate space for command args, falling back to basic shell", .{});
|
||||
|
||||
// If we're on macOS, we have to use `login(1)` to get all of
|
||||
// the proper environment variables set, a login shell, and proper
|
||||
// hushlogin behavior.
|
||||
if (comptime builtin.target.os.tag.isDarwin()) darwin: {
|
||||
const passwd = internal_os.passwd.get(alloc) catch |err| {
|
||||
log.warn("failed to read passwd, not using a login shell err={}", .{err});
|
||||
break :darwin;
|
||||
// The comptime here is important to ensure the full slice
|
||||
// is put into the binary data and not the stack.
|
||||
break :oom comptime switch (builtin.os.tag) {
|
||||
.windows => &.{"cmd.exe"},
|
||||
else => &.{"/bin/sh"},
|
||||
};
|
||||
},
|
||||
|
||||
const username = passwd.name orelse {
|
||||
log.warn("failed to get username, not using a login shell", .{});
|
||||
break :darwin;
|
||||
};
|
||||
|
||||
const hush = if (passwd.home) |home| hush: {
|
||||
var dir = std.fs.openDirAbsolute(home, .{}) catch |err| {
|
||||
log.warn(
|
||||
"failed to open home dir, not checking for hushlogin err={}",
|
||||
.{err},
|
||||
);
|
||||
break :hush false;
|
||||
};
|
||||
defer dir.close();
|
||||
|
||||
break :hush if (dir.access(".hushlogin", .{})) true else |_| false;
|
||||
} else false;
|
||||
|
||||
const cmd = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"exec -l {s}",
|
||||
.{shell_command},
|
||||
);
|
||||
|
||||
// The reason for executing login this way is unclear. This
|
||||
// comment will attempt to explain but prepare for a truly
|
||||
// unhinged reality.
|
||||
//
|
||||
// The first major issue is that on macOS, a lot of users
|
||||
// put shell configurations in ~/.bash_profile instead of
|
||||
// ~/.bashrc (or equivalent for another shell). This file is only
|
||||
// loaded for a login shell so macOS users expect all their terminals
|
||||
// to be login shells. No other platform behaves this way and its
|
||||
// totally braindead but somehow the entire dev community on
|
||||
// macOS has cargo culted their way to this reality so we have to
|
||||
// do it...
|
||||
//
|
||||
// To get a login shell, you COULD just prepend argv0 with a `-`
|
||||
// but that doesn't fully work because `getlogin()` C API will
|
||||
// return the wrong value, SHELL won't be set, and various
|
||||
// other login behaviors that macOS users expect.
|
||||
//
|
||||
// The proper way is to use `login(1)`. But login(1) forces
|
||||
// the working directory to change to the home directory,
|
||||
// which we may not want. If we specify "-l" then we can avoid
|
||||
// this behavior but now the shell isn't a login shell.
|
||||
//
|
||||
// There is another issue: `login(1)` on macOS 14.3 and earlier
|
||||
// checked for ".hushlogin" in the working directory. This means
|
||||
// that if we specify "-l" then we won't get hushlogin honored
|
||||
// if its in the home directory (which is standard). To get
|
||||
// around this, we check for hushlogin ourselves and if present
|
||||
// specify the "-q" flag to login(1).
|
||||
//
|
||||
// So to get all the behaviors we want, we specify "-l" but
|
||||
// execute "bash" (which is built-in to macOS). We then use
|
||||
// the bash builtin "exec" to replace the process with a login
|
||||
// shell ("-l" on exec) with the command we really want.
|
||||
//
|
||||
// We use "bash" instead of other shells that ship with macOS
|
||||
// because as of macOS Sonoma, we found with a microbenchmark
|
||||
// that bash can `exec` into the desired command ~2x faster
|
||||
// than zsh.
|
||||
//
|
||||
// To figure out a lot of this logic I read the login.c
|
||||
// source code in the OSS distribution Apple provides for
|
||||
// macOS.
|
||||
//
|
||||
// Awesome.
|
||||
try args.append("/usr/bin/login");
|
||||
if (hush) try args.append("-q");
|
||||
try args.append("-flp");
|
||||
|
||||
// We execute bash with "--noprofile --norc" so that it doesn't
|
||||
// load startup files so that (1) our shell integration doesn't
|
||||
// break and (2) user configuration doesn't mess this process
|
||||
// up.
|
||||
try args.append(username);
|
||||
try args.append("/bin/bash");
|
||||
try args.append("--noprofile");
|
||||
try args.append("--norc");
|
||||
try args.append("-c");
|
||||
try args.append(cmd);
|
||||
break :args try args.toOwnedSlice();
|
||||
}
|
||||
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
// We run our shell wrapped in `cmd.exe` so that we don't have
|
||||
// to parse the command line ourselves if it has arguments.
|
||||
|
||||
// Note we don't free any of the memory below since it is
|
||||
// allocated in the arena.
|
||||
const windir = try std.process.getEnvVarOwned(alloc, "WINDIR");
|
||||
const cmd = try std.fs.path.join(alloc, &[_][]const u8{
|
||||
windir,
|
||||
"System32",
|
||||
"cmd.exe",
|
||||
});
|
||||
|
||||
try args.append(cmd);
|
||||
try args.append("/C");
|
||||
} else {
|
||||
// We run our shell wrapped in `/bin/sh` so that we don't have
|
||||
// to parse the command line ourselves if it has arguments.
|
||||
// Additionally, some environments (NixOS, I found) use /bin/sh
|
||||
// to setup some environment variables that are important to
|
||||
// have set.
|
||||
try args.append("/bin/sh");
|
||||
if (internal_os.isFlatpak()) try args.append("-l");
|
||||
try args.append("-c");
|
||||
}
|
||||
|
||||
try args.append(shell_command);
|
||||
break :args try args.toOwnedSlice();
|
||||
// This logs on its own, this is a bad error.
|
||||
error.SystemError => return err,
|
||||
};
|
||||
|
||||
// We have to copy the cwd because there is no guarantee that
|
||||
@@ -1562,3 +1468,320 @@ pub const ReadThread = struct {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Builds the argv array for the process we should exec for the
|
||||
/// configured command. This isn't as straightforward as it seems since
|
||||
/// we deal with shell-wrapping, macOS login shells, etc.
|
||||
///
|
||||
/// The passwdpkg comptime argument is expected to have a single function
|
||||
/// `get(Allocator)` that returns a passwd entry. This is used by macOS
|
||||
/// to determine the username and home directory for the login shell.
|
||||
/// It is unused on other platforms.
|
||||
///
|
||||
/// Memory ownership:
|
||||
///
|
||||
/// The allocator should be an arena, since the returned value may or
|
||||
/// may not be allocated and args may or may not be allocated (or copied).
|
||||
/// Pointers in the return value may point to pointers in the command
|
||||
/// struct.
|
||||
fn execCommand(
|
||||
alloc: Allocator,
|
||||
command: configpkg.Command,
|
||||
comptime passwdpkg: type,
|
||||
) (Allocator.Error || error{SystemError})![]const [:0]const u8 {
|
||||
// If we're on macOS, we have to use `login(1)` to get all of
|
||||
// the proper environment variables set, a login shell, and proper
|
||||
// hushlogin behavior.
|
||||
if (comptime builtin.target.os.tag.isDarwin()) darwin: {
|
||||
const passwd = passwdpkg.get(alloc) catch |err| {
|
||||
log.warn("failed to read passwd, not using a login shell err={}", .{err});
|
||||
break :darwin;
|
||||
};
|
||||
|
||||
const username = passwd.name orelse {
|
||||
log.warn("failed to get username, not using a login shell", .{});
|
||||
break :darwin;
|
||||
};
|
||||
|
||||
const hush = if (passwd.home) |home| hush: {
|
||||
var dir = std.fs.openDirAbsolute(home, .{}) catch |err| {
|
||||
log.warn(
|
||||
"failed to open home dir, not checking for hushlogin err={}",
|
||||
.{err},
|
||||
);
|
||||
break :hush false;
|
||||
};
|
||||
defer dir.close();
|
||||
|
||||
break :hush if (dir.access(".hushlogin", .{})) true else |_| false;
|
||||
} else false;
|
||||
|
||||
// If we made it this far we're going to start building
|
||||
// the actual command.
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(
|
||||
alloc,
|
||||
|
||||
// This capacity is chosen based on what we'd need to
|
||||
// execute a shell command (very common). We can/will
|
||||
// grow if necessary for a longer command (uncommon).
|
||||
9,
|
||||
);
|
||||
defer args.deinit();
|
||||
|
||||
// The reason for executing login this way is unclear. This
|
||||
// comment will attempt to explain but prepare for a truly
|
||||
// unhinged reality.
|
||||
//
|
||||
// The first major issue is that on macOS, a lot of users
|
||||
// put shell configurations in ~/.bash_profile instead of
|
||||
// ~/.bashrc (or equivalent for another shell). This file is only
|
||||
// loaded for a login shell so macOS users expect all their terminals
|
||||
// to be login shells. No other platform behaves this way and its
|
||||
// totally braindead but somehow the entire dev community on
|
||||
// macOS has cargo culted their way to this reality so we have to
|
||||
// do it...
|
||||
//
|
||||
// To get a login shell, you COULD just prepend argv0 with a `-`
|
||||
// but that doesn't fully work because `getlogin()` C API will
|
||||
// return the wrong value, SHELL won't be set, and various
|
||||
// other login behaviors that macOS users expect.
|
||||
//
|
||||
// The proper way is to use `login(1)`. But login(1) forces
|
||||
// the working directory to change to the home directory,
|
||||
// which we may not want. If we specify "-l" then we can avoid
|
||||
// this behavior but now the shell isn't a login shell.
|
||||
//
|
||||
// There is another issue: `login(1)` on macOS 14.3 and earlier
|
||||
// checked for ".hushlogin" in the working directory. This means
|
||||
// that if we specify "-l" then we won't get hushlogin honored
|
||||
// if its in the home directory (which is standard). To get
|
||||
// around this, we check for hushlogin ourselves and if present
|
||||
// specify the "-q" flag to login(1).
|
||||
//
|
||||
// So to get all the behaviors we want, we specify "-l" but
|
||||
// execute "bash" (which is built-in to macOS). We then use
|
||||
// the bash builtin "exec" to replace the process with a login
|
||||
// shell ("-l" on exec) with the command we really want.
|
||||
//
|
||||
// We use "bash" instead of other shells that ship with macOS
|
||||
// because as of macOS Sonoma, we found with a microbenchmark
|
||||
// that bash can `exec` into the desired command ~2x faster
|
||||
// than zsh.
|
||||
//
|
||||
// To figure out a lot of this logic I read the login.c
|
||||
// source code in the OSS distribution Apple provides for
|
||||
// macOS.
|
||||
//
|
||||
// Awesome.
|
||||
try args.append("/usr/bin/login");
|
||||
if (hush) try args.append("-q");
|
||||
try args.append("-flp");
|
||||
try args.append(username);
|
||||
|
||||
switch (command) {
|
||||
// Direct args can be passed directly to login, since
|
||||
// login uses execvp we don't need to worry about PATH
|
||||
// searching.
|
||||
.direct => |v| try args.appendSlice(v),
|
||||
|
||||
.shell => |v| {
|
||||
// Use "exec" to replace the bash process with
|
||||
// our intended command so we don't have a parent
|
||||
// process hanging around.
|
||||
const cmd = try std.fmt.allocPrintZ(
|
||||
alloc,
|
||||
"exec -l {s}",
|
||||
.{v},
|
||||
);
|
||||
|
||||
// We execute bash with "--noprofile --norc" so that it doesn't
|
||||
// load startup files so that (1) our shell integration doesn't
|
||||
// break and (2) user configuration doesn't mess this process
|
||||
// up.
|
||||
try args.append("/bin/bash");
|
||||
try args.append("--noprofile");
|
||||
try args.append("--norc");
|
||||
try args.append("-c");
|
||||
try args.append(cmd);
|
||||
},
|
||||
}
|
||||
|
||||
return try args.toOwnedSlice();
|
||||
}
|
||||
|
||||
return switch (command) {
|
||||
.direct => |v| v,
|
||||
|
||||
.shell => |v| shell: {
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4);
|
||||
defer args.deinit();
|
||||
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
// We run our shell wrapped in `cmd.exe` so that we don't have
|
||||
// to parse the command line ourselves if it has arguments.
|
||||
|
||||
// Note we don't free any of the memory below since it is
|
||||
// allocated in the arena.
|
||||
const windir = std.process.getEnvVarOwned(
|
||||
alloc,
|
||||
"WINDIR",
|
||||
) catch |err| {
|
||||
log.warn("failed to get WINDIR, cannot run shell command err={}", .{err});
|
||||
return error.SystemError;
|
||||
};
|
||||
const cmd = try std.fs.path.joinZ(alloc, &[_][]const u8{
|
||||
windir,
|
||||
"System32",
|
||||
"cmd.exe",
|
||||
});
|
||||
|
||||
try args.append(cmd);
|
||||
try args.append("/C");
|
||||
} else {
|
||||
// We run our shell wrapped in `/bin/sh` so that we don't have
|
||||
// to parse the command line ourselves if it has arguments.
|
||||
// Additionally, some environments (NixOS, I found) use /bin/sh
|
||||
// to setup some environment variables that are important to
|
||||
// have set.
|
||||
try args.append("/bin/sh");
|
||||
if (internal_os.isFlatpak()) try args.append("-l");
|
||||
try args.append("-c");
|
||||
}
|
||||
|
||||
try args.append(v);
|
||||
break :shell try args.toOwnedSlice();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test "execCommand darwin: shell command" {
|
||||
if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const result = try execCommand(alloc, .{ .shell = "foo bar baz" }, struct {
|
||||
fn get(_: Allocator) !PasswdEntry {
|
||||
return .{
|
||||
.name = "testuser",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
try testing.expectEqual(8, result.len);
|
||||
try testing.expectEqualStrings(result[0], "/usr/bin/login");
|
||||
try testing.expectEqualStrings(result[1], "-flp");
|
||||
try testing.expectEqualStrings(result[2], "testuser");
|
||||
try testing.expectEqualStrings(result[3], "/bin/bash");
|
||||
try testing.expectEqualStrings(result[4], "--noprofile");
|
||||
try testing.expectEqualStrings(result[5], "--norc");
|
||||
try testing.expectEqualStrings(result[6], "-c");
|
||||
try testing.expectEqualStrings(result[7], "exec -l foo bar baz");
|
||||
}
|
||||
|
||||
test "execCommand darwin: direct command" {
|
||||
if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const result = try execCommand(alloc, .{ .direct = &.{
|
||||
"foo",
|
||||
"bar baz",
|
||||
} }, struct {
|
||||
fn get(_: Allocator) !PasswdEntry {
|
||||
return .{
|
||||
.name = "testuser",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
try testing.expectEqual(5, result.len);
|
||||
try testing.expectEqualStrings(result[0], "/usr/bin/login");
|
||||
try testing.expectEqualStrings(result[1], "-flp");
|
||||
try testing.expectEqualStrings(result[2], "testuser");
|
||||
try testing.expectEqualStrings(result[3], "foo");
|
||||
try testing.expectEqualStrings(result[4], "bar baz");
|
||||
}
|
||||
|
||||
test "execCommand: shell command, empty passwd" {
|
||||
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const result = try execCommand(
|
||||
alloc,
|
||||
.{ .shell = "foo bar baz" },
|
||||
struct {
|
||||
fn get(_: Allocator) !PasswdEntry {
|
||||
// Empty passwd entry means we can't construct a macOS
|
||||
// login command and falls back to POSIX behavior.
|
||||
return .{};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
try testing.expectEqual(3, result.len);
|
||||
try testing.expectEqualStrings(result[0], "/bin/sh");
|
||||
try testing.expectEqualStrings(result[1], "-c");
|
||||
try testing.expectEqualStrings(result[2], "foo bar baz");
|
||||
}
|
||||
|
||||
test "execCommand: shell command, error passwd" {
|
||||
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const result = try execCommand(
|
||||
alloc,
|
||||
.{ .shell = "foo bar baz" },
|
||||
struct {
|
||||
fn get(_: Allocator) !PasswdEntry {
|
||||
// Failed passwd entry means we can't construct a macOS
|
||||
// login command and falls back to POSIX behavior.
|
||||
return error.Fail;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
try testing.expectEqual(3, result.len);
|
||||
try testing.expectEqualStrings(result[0], "/bin/sh");
|
||||
try testing.expectEqualStrings(result[1], "-c");
|
||||
try testing.expectEqualStrings(result[2], "foo bar baz");
|
||||
}
|
||||
|
||||
test "execCommand: direct command, error passwd" {
|
||||
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
|
||||
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const result = try execCommand(alloc, .{
|
||||
.direct = &.{
|
||||
"foo",
|
||||
"bar baz",
|
||||
},
|
||||
}, struct {
|
||||
fn get(_: Allocator) !PasswdEntry {
|
||||
// Failed passwd entry means we can't construct a macOS
|
||||
// login command and falls back to POSIX behavior.
|
||||
return error.Fail;
|
||||
}
|
||||
});
|
||||
|
||||
try testing.expectEqual(2, result.len);
|
||||
try testing.expectEqualStrings(result[0], "foo");
|
||||
try testing.expectEqualStrings(result[1], "bar baz");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user