From 103babecc2525d128869d1def9ad88a50ed7ef90 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 23 Apr 2026 00:35:36 -0500 Subject: [PATCH] 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=` the `bash` and `nushell` integrations would break commands that were specified with `+new-window -e ` (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 --- src/config/Config.zig | 5 +- src/config/command.zig | 83 ++++++++++++++++++++++++++++++++ src/termio/shell_integration.zig | 74 ++++++---------------------- 3 files changed, 101 insertions(+), 61 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 13f78eea6..0cadcd782 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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, diff --git a/src/config/command.zig b/src/config/command.zig index 7cd70acb3..87f3da6aa 100644 --- a/src/config/command.zig +++ b/src/config/command.zig @@ -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); diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 2dd09ee29..2d74db6f9 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -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=` 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,