pull/12390/merge
Jeffrey C. Ollie 2026-06-03 16:23:06 +08:00 committed by GitHub
commit 80ff2d834a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 101 additions and 61 deletions

View File

@ -2807,7 +2807,10 @@ keybind: Keybinds = .{},
///
/// * `detect` - Detect the shell based on the filename.
///
/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme.
/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell
/// injection scheme if the command is this shell. Has no effect if the
/// command is not a shell, or is a different shell from the one specified
/// here.
///
/// The default value is `detect`.
@"shell-integration": ShellIntegration = .detect,

View File

@ -1,4 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const formatterpkg = @import("formatter.zig");
@ -197,6 +199,87 @@ pub const Command = union(enum) {
}
}
/// Shell types we support
pub const Shell = enum {
bash,
elvish,
fish,
nushell,
zsh,
};
/// Detect if this command is a known shell.
pub fn detectShell(self: Self) ?Shell {
var buf: [std.fs.max_path_bytes]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = .init(&buf);
const arg0 = switch (self) {
.direct => |v| v[0],
.shell => |v| arg: {
var it = std.process.ArgIteratorGeneral(.{}).init(fba.allocator(), v) catch return null;
break :arg it.next() orelse return null;
},
};
const exe = std.fs.path.basename(arg0);
if (std.mem.eql(u8, "bash", exe)) {
// Apple distributes their own patched version of Bash 3.2
// on macOS that disables the ENV-based POSIX startup path.
// This means we're unable to perform our automatic shell
// integration sequence in this specific environment.
//
// If we're running "/bin/bash" on Darwin, we can assume
// we're using Apple's Bash because /bin is non-writable
// on modern macOS due to System Integrity Protection.
if (comptime builtin.target.os.tag.isDarwin()) {
if (std.mem.eql(u8, "/bin/bash", arg0)) {
return null;
}
}
return .bash;
}
if (std.mem.eql(u8, "elvish", exe)) return .elvish;
if (std.mem.eql(u8, "fish", exe)) return .fish;
if (std.mem.eql(u8, "nu", exe)) return .nushell;
if (std.mem.eql(u8, "zsh", exe)) return .zsh;
return null;
}
test detectShell {
const testing = std.testing;
try testing.expect(detectShell(.{ .shell = "sh" }) == null);
try testing.expectEqual(.bash, detectShell(.{ .shell = "bash" }));
try testing.expectEqual(.elvish, detectShell(.{ .shell = "elvish" }));
try testing.expectEqual(.fish, detectShell(.{ .shell = "fish" }));
try testing.expectEqual(.nushell, detectShell(.{ .shell = "nu" }));
try testing.expectEqual(.zsh, detectShell(.{ .shell = "zsh" }));
if (comptime builtin.target.os.tag.isDarwin()) {
try testing.expect(detectShell(.{ .shell = "/bin/bash" }) == null);
}
try testing.expectEqual(.bash, detectShell(.{ .shell = "bash -c 'command'" }));
try testing.expectEqual(.bash, detectShell(.{ .shell = "\"/a b/bash\"" }));
try testing.expect(detectShell(.{ .direct = &.{"sh"} }) == null);
try testing.expectEqual(.bash, detectShell(.{ .direct = &.{"bash"} }));
try testing.expectEqual(.elvish, detectShell(.{ .direct = &.{"elvish"} }));
try testing.expectEqual(.fish, detectShell(.{ .direct = &.{"fish"} }));
try testing.expectEqual(.nushell, detectShell(.{ .direct = &.{"nu"} }));
try testing.expectEqual(.zsh, detectShell(.{ .direct = &.{"zsh"} }));
if (comptime builtin.target.os.tag.isDarwin()) {
try testing.expect(detectShell(.{&.{ .direct = "/bin/bash" }}) == null);
}
try testing.expectEqual(.bash, detectShell(.{ .direct = &.{ "bash", "-c", "command" } }));
try testing.expectEqual(.bash, detectShell(.{ .direct = &.{"/a b/bash"} }));
}
test "Command: parseCLI errors" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);

View File

@ -10,13 +10,7 @@ const internal_os = @import("../os/main.zig");
const log = std.log.scoped(.shell_integration);
/// Shell types we support
pub const Shell = enum {
bash,
elvish,
fish,
nushell,
zsh,
};
pub const Shell = config.Command.Shell;
/// The result of setting up a shell integration.
pub const ShellIntegration = struct {
@ -46,10 +40,21 @@ pub fn setup(
env: *EnvMap,
force_shell: ?Shell,
) !?ShellIntegration {
const actual_shell = command.detectShell();
const shell: Shell = force_shell orelse
try detectShell(alloc_arena, command) orelse
actual_shell orelse
return null;
// Don't do any shell integration if we're not actually running the shell.
// This prevents problems when the command is overridden by `+new-window`
// or some other mechanism and `shell-integration` is forced to a specific
// shell rather than being detected.
//
// This means that `shell-integration=<shell>` has no effect if we detect
// that the command is not a shell, or is not the shell specified by
// `shell-integration`.
if (shell != actual_shell) return null;
const new_command: config.Command = switch (shell) {
.bash => try setupBash(
alloc_arena,
@ -107,7 +112,7 @@ test "force shell" {
&env,
shell,
);
try testing.expectEqual(shell, result.?.shell);
try testing.expect(result == null);
}
}
@ -133,57 +138,6 @@ test "shell integration failure" {
try testing.expectEqual(0, env.count());
}
fn detectShell(alloc: Allocator, command: config.Command) !?Shell {
var arg_iter = try command.argIterator(alloc);
defer arg_iter.deinit();
const arg0 = arg_iter.next() orelse return null;
const exe = std.fs.path.basename(arg0);
if (std.mem.eql(u8, "bash", exe)) {
// Apple distributes their own patched version of Bash 3.2
// on macOS that disables the ENV-based POSIX startup path.
// This means we're unable to perform our automatic shell
// integration sequence in this specific environment.
//
// If we're running "/bin/bash" on Darwin, we can assume
// we're using Apple's Bash because /bin is non-writable
// on modern macOS due to System Integrity Protection.
if (comptime builtin.target.os.tag.isDarwin()) {
if (std.mem.eql(u8, "/bin/bash", arg0)) {
return null;
}
}
return .bash;
}
if (std.mem.eql(u8, "elvish", exe)) return .elvish;
if (std.mem.eql(u8, "fish", exe)) return .fish;
if (std.mem.eql(u8, "nu", exe)) return .nushell;
if (std.mem.eql(u8, "zsh", exe)) return .zsh;
return null;
}
test detectShell {
const testing = std.testing;
const alloc = testing.allocator;
try testing.expect(try detectShell(alloc, .{ .shell = "sh" }) == null);
try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash" }));
try testing.expectEqual(.elvish, try detectShell(alloc, .{ .shell = "elvish" }));
try testing.expectEqual(.fish, try detectShell(alloc, .{ .shell = "fish" }));
try testing.expectEqual(.nushell, try detectShell(alloc, .{ .shell = "nu" }));
try testing.expectEqual(.zsh, try detectShell(alloc, .{ .shell = "zsh" }));
if (comptime builtin.target.os.tag.isDarwin()) {
try testing.expect(try detectShell(alloc, .{ .shell = "/bin/bash" }) == null);
}
try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash -c 'command'" }));
try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "\"/a b/bash\"" }));
}
/// Set up the shell integration features environment variable.
pub fn setupFeatures(
env: *EnvMap,