core: don't force shell integration unless we're running that shell

If you bypassed shell detection and forced a specific shell integration
with `shell-integration=<shell>` the `bash` and `nushell` integrations
would break commands that were specified with `+new-window -e
<command>` (or a similar mechanism) because they modify the command
in shell-specific ways and the new command would fail with an "unknown
flag" error or similar.

This PR fixes that by only permitting shell integration if the
command matches the type of shell integration. That means that
`shell-integration=nushell` only works if `argv[0]` ends with `nu`, etc.
Generally this should not matter unless the shell executable was renamed
for some reason and the shell detection failed.

Fixes #12378
pull/12390/head
Jeffrey C. Ollie 2026-04-23 00:35:36 -05:00
parent 2a3d93f77b
commit 103babecc2
No known key found for this signature in database
GPG Key ID: 1BB9EB7EA602265B
3 changed files with 101 additions and 61 deletions

View File

@ -2792,7 +2792,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,