Resolve model-provided shells by type (#39607)

## Why

A model-provided shell path should select the requested shell type without
allowing that path to determine which executable Codex runs.

## What changed

- Resolve model-provided shells through Codex's normal shell discovery and
  fallback logic after detecting their type.
- Keep the configured packaged zsh executable when the zsh-fork feature is
  enabled and the file exists.
- Update shell, unified exec, and network approval expectations to use the
  resolved local executable and arguments.

GitOrigin-RevId: ebe6f7eec2cfd1c0548d5bf1a26b7a30dba02cc2
This commit is contained in:
pakrym-oai
2026-08-19 20:14:46 +00:00
committed by copyberry
parent 6869d17cc2
commit 186b449bc2
9 changed files with 73 additions and 422 deletions

View File

@@ -1130,13 +1130,19 @@ impl Session {
"zsh fork feature enabled, but no packaged zsh fork is available for this install"
)
})?;
let zsh_path = zsh_path.to_path_buf();
shell::get_shell(shell::ShellType::Zsh, Some(&zsh_path)).ok_or_else(|| {
anyhow::anyhow!(
"zsh fork feature enabled, but packaged zsh fork `{}` is not usable",
zsh_path.display()
)
})?
if zsh_path.is_file() {
shell::Shell {
shell_type: shell::ShellType::Zsh,
shell_path: zsh_path.clone(),
}
} else {
shell::get_shell(shell::ShellType::Zsh).ok_or_else(|| {
anyhow::anyhow!(
"zsh fork feature enabled, but packaged zsh fork `{}` is not usable",
zsh_path.display()
)
})?
}
} else {
shell::default_user_shell()
};

View File

@@ -85,8 +85,8 @@ pub fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> Shell {
codex_shell_command::shell_detect::get_shell_by_model_provided_path(shell_path).into()
}
pub fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<Shell> {
codex_shell_command::shell_detect::get_shell(shell_type, path).map(Into::into)
pub fn get_shell(shell_type: ShellType) -> Option<Shell> {
codex_shell_command::shell_detect::get_shell(shell_type).map(Into::into)
}
pub fn default_user_shell() -> Shell {

View File

@@ -206,8 +206,8 @@ async fn write_shell_snapshot(
if shell_type == ShellType::PowerShell || shell_type == ShellType::Cmd {
bail!("Shell snapshot not supported yet for {shell_type:?}");
}
let shell = get_shell(shell_type, /*path*/ None)
.with_context(|| format!("No available shell for {shell_type:?}"))?;
let shell =
get_shell(shell_type).with_context(|| format!("No available shell for {shell_type:?}"))?;
let raw_snapshot = capture_snapshot(&shell, cwd).await?;
let snapshot = strip_snapshot_preamble(&raw_snapshot)?;

View File

@@ -5,7 +5,7 @@ use std::process::Command;
#[test]
#[cfg(target_os = "macos")]
fn detects_zsh() {
let zsh_shell = get_shell(ShellType::Zsh, /*path*/ None).unwrap();
let zsh_shell = get_shell(ShellType::Zsh).unwrap();
let shell_path = zsh_shell.shell_path;
@@ -24,7 +24,7 @@ fn fish_fallback_to_zsh() {
#[test]
fn detects_bash() {
let bash_shell = get_shell(ShellType::Bash, /*path*/ None).unwrap();
let bash_shell = get_shell(ShellType::Bash).unwrap();
let shell_path = bash_shell.shell_path;
assert!(
@@ -35,7 +35,7 @@ fn detects_bash() {
#[test]
fn detects_sh() {
let sh_shell = get_shell(ShellType::Sh, /*path*/ None).unwrap();
let sh_shell = get_shell(ShellType::Sh).unwrap();
let shell_path = sh_shell.shell_path;
assert!(
shell_path.file_name().and_then(|name| name.to_str()) == Some("sh"),
@@ -48,12 +48,12 @@ fn can_run_on_shell_test() {
let cmd = "echo \"Works\"";
if cfg!(windows) {
assert!(shell_works(
get_shell(ShellType::PowerShell, /*path*/ None),
get_shell(ShellType::PowerShell),
"Out-String 'Works'",
/*required*/ true,
));
assert!(shell_works(
get_shell(ShellType::Cmd, /*path*/ None),
get_shell(ShellType::Cmd),
cmd,
/*required*/ true,
));
@@ -69,17 +69,17 @@ fn can_run_on_shell_test() {
/*required*/ true
));
assert!(shell_works(
get_shell(ShellType::Zsh, /*path*/ None),
get_shell(ShellType::Zsh),
cmd,
/*required*/ false
));
assert!(shell_works(
get_shell(ShellType::Bash, /*path*/ None),
get_shell(ShellType::Bash),
cmd,
/*required*/ true
));
assert!(shell_works(
get_shell(ShellType::Sh, /*path*/ None),
get_shell(ShellType::Sh),
cmd,
/*required*/ true
));
@@ -181,7 +181,7 @@ fn finds_powershell() {
return;
}
let powershell_shell = get_shell(ShellType::PowerShell, /*path*/ None).unwrap();
let powershell_shell = get_shell(ShellType::PowerShell).unwrap();
let shell_path = powershell_shell.shell_path;
assert!(shell_path.ends_with("pwsh.exe") || shell_path.ends_with("powershell.exe"));

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::shell::ShellType;
use crate::shell::default_user_shell;
use crate::shell::get_shell;
use codex_exec_server::Environment;
use codex_tools::UnifiedExecShellMode;
use codex_tools::ZshForkConfig;
@@ -95,7 +96,7 @@ fn test_get_command_respects_explicit_bash_shell() -> anyhow::Result<()> {
}
#[test]
fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> {
fn test_get_command_resolves_powershell_by_type() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let powershell_path = temp_dir.path().join(if cfg!(windows) {
"powershell.exe"
@@ -123,10 +124,13 @@ fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> {
/*allow_login_shell*/ true,
)
.map_err(anyhow::Error::msg)?;
let command = resolved.command;
assert_eq!(command[2], "echo hello");
assert_eq!(resolved.shell_type, ShellType::PowerShell);
let expected_shell = get_shell(ShellType::PowerShell)
.unwrap_or_else(|| codex_shell_command::shell_detect::ultimate_fallback_shell().into());
assert_eq!(
resolved.command,
expected_shell.derive_exec_args("echo hello", /*use_login_shell*/ true)
);
assert_eq!(resolved.shell_type, expected_shell.shell_type);
Ok(())
}

View File

@@ -1,349 +0,0 @@
use anyhow::Result;
use codex_config::Constrained;
use codex_core::TurnInputRequest;
use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_remote;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use test_case::test_case;
#[derive(Clone, Copy)]
enum ShellAttack {
ExactShellName,
ExactShellNameWithAllowedInnerCommand,
ExactShellNameWithForbiddenInnerCommand,
DangerousCommandOnRequest,
DangerousCommandNever,
ApprovedCustomShell,
SessionApprovalDoesNotTrustDifferentShell,
SpoofedShellExtension,
}
#[test_case(ShellAttack::ExactShellName; "workspace shell requires approval")]
#[test_case(ShellAttack::ExactShellNameWithAllowedInnerCommand; "inner allow does not trust workspace shell")]
#[test_case(ShellAttack::ExactShellNameWithForbiddenInnerCommand; "inner forbidden rule still rejects workspace shell")]
#[test_case(ShellAttack::DangerousCommandOnRequest; "dangerous inner command requires approval")]
#[test_case(ShellAttack::DangerousCommandNever; "dangerous inner command is forbidden without approval")]
#[test_case(ShellAttack::ApprovedCustomShell; "approved custom shell still runs")]
#[test_case(ShellAttack::SessionApprovalDoesNotTrustDifferentShell; "session approval does not trust a different shell")]
#[test_case(ShellAttack::SpoofedShellExtension; "workspace shell with an extra extension requires approval")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_provided_shell_cannot_inherit_inner_command_trust(
attack: ShellAttack,
) -> Result<()> {
skip_if_remote!(
Ok(()),
"remote executors already replace requested shell paths with their reported shell"
);
let approval_policy = match attack {
ShellAttack::DangerousCommandOnRequest => AskForApproval::OnRequest,
ShellAttack::DangerousCommandNever => AskForApproval::Never,
ShellAttack::ExactShellName
| ShellAttack::ExactShellNameWithAllowedInnerCommand
| ShellAttack::ExactShellNameWithForbiddenInnerCommand
| ShellAttack::ApprovedCustomShell
| ShellAttack::SessionApprovalDoesNotTrustDifferentShell
| ShellAttack::SpoofedShellExtension => AskForApproval::UnlessTrusted,
};
let server = start_mock_server().await;
let mut builder = test_codex().with_config(move |config| {
config.use_experimental_unified_exec_tool = true;
config
.features
.enable(Feature::UnifiedExec)
.expect("enable unified exec");
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config.approvals_reviewer = ApprovalsReviewer::User;
let inner_command_rule = match attack {
ShellAttack::ExactShellNameWithAllowedInnerCommand => {
Some("prefix_rule(pattern=[\"echo\"], decision=\"allow\")\n")
}
ShellAttack::ExactShellNameWithForbiddenInnerCommand => {
if cfg!(windows) {
Some(
r#"prefix_rule(pattern=["Remove-Item", "C:\\important"], decision="forbidden")"#,
)
} else {
Some("prefix_rule(pattern=[\"rm\"], decision=\"forbidden\")\n")
}
}
ShellAttack::ExactShellName
| ShellAttack::DangerousCommandOnRequest
| ShellAttack::DangerousCommandNever
| ShellAttack::ApprovedCustomShell
| ShellAttack::SessionApprovalDoesNotTrustDifferentShell
| ShellAttack::SpoofedShellExtension => None,
};
if let Some(inner_command_rule) = inner_command_rule {
let policy_path = config.codex_home.join("rules/default.rules");
fs::create_dir_all(policy_path.parent().expect("rules directory"))
.expect("create rules directory");
fs::write(policy_path, inner_command_rule).expect("write execution policy rule");
}
});
#[cfg(windows)]
if matches!(
attack,
ShellAttack::ExactShellNameWithForbiddenInnerCommand | ShellAttack::ApprovedCustomShell
) {
let system_root = std::env::var_os("SystemRoot").expect("Windows SystemRoot");
let configured_shell = std::path::Path::new(&system_root)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0")
.join("powershell.exe");
builder = builder.with_user_shell(codex_core::shell::get_shell_by_model_provided_path(
&configured_shell,
));
}
let test = builder.build_with_auto_env(&server).await?;
let shell_name = match attack {
ShellAttack::ExactShellNameWithForbiddenInnerCommand | ShellAttack::ApprovedCustomShell
if cfg!(windows) =>
{
"pwsh.exe"
}
ShellAttack::ExactShellName
| ShellAttack::ExactShellNameWithAllowedInnerCommand
| ShellAttack::ExactShellNameWithForbiddenInnerCommand
| ShellAttack::DangerousCommandOnRequest
| ShellAttack::DangerousCommandNever
| ShellAttack::ApprovedCustomShell
| ShellAttack::SessionApprovalDoesNotTrustDifferentShell => {
if cfg!(windows) {
"powershell.exe"
} else {
"bash"
}
}
ShellAttack::SpoofedShellExtension => {
if cfg!(windows) {
"powershell.evil"
} else {
"bash.evil"
}
}
};
let shell = test.workspace_path(shell_name);
let marker = test.workspace_path("attacker-executed");
#[cfg(unix)]
{
fs::write(&shell, "#!/bin/sh\nprintf ran > attacker-executed\n")?;
fs::set_permissions(&shell, fs::Permissions::from_mode(0o755))?;
}
#[cfg(windows)]
{
let test_executable = std::env::current_exe()?;
fs::hard_link(&test_executable, &shell)
.or_else(|_| fs::copy(&test_executable, &shell).map(|_| ()))?;
fs::write(
shell.with_file_name(".codex-executable-identity-fixture"),
b"fake shell",
)?;
}
let other_shell = if matches!(
attack,
ShellAttack::SessionApprovalDoesNotTrustDifferentShell
) {
let other_shell = test.workspace_path("another").join(shell_name);
fs::create_dir_all(other_shell.parent().expect("alternate shell directory"))?;
fs::copy(&shell, &other_shell)?;
#[cfg(windows)]
fs::write(
other_shell.with_file_name(".codex-executable-identity-fixture"),
b"fake shell",
)?;
Some(other_shell)
} else {
None
};
let call_id = "untrusted-shell-path";
let other_call_id = "different-untrusted-shell-path";
let command = match attack {
ShellAttack::ExactShellName if cfg!(windows) => "Write-Output $env:USERNAME",
ShellAttack::DangerousCommandOnRequest | ShellAttack::DangerousCommandNever => {
if cfg!(windows) {
"Remove-Item important -Force"
} else {
"rm -rf important"
}
}
ShellAttack::ExactShellNameWithForbiddenInnerCommand => {
if cfg!(windows) {
r"echo shell-safe && Remove-Item C:\important"
} else {
"echo shell-safe; rm important"
}
}
ShellAttack::ExactShellName
| ShellAttack::ExactShellNameWithAllowedInnerCommand
| ShellAttack::ApprovedCustomShell
| ShellAttack::SessionApprovalDoesNotTrustDifferentShell
| ShellAttack::SpoofedShellExtension => "echo shell-safe",
};
mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-untrusted-shell-1"),
ev_function_call(
call_id,
"exec_command",
&json!({ "cmd": command, "shell": shell }).to_string(),
),
ev_completed("resp-untrusted-shell-1"),
]),
)
.await;
if let Some(other_shell) = other_shell.as_ref() {
mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-different-untrusted-shell"),
ev_function_call(
other_call_id,
"exec_command",
&json!({ "cmd": command, "shell": other_shell }).to_string(),
),
ev_completed("resp-different-untrusted-shell"),
]),
)
.await;
}
let completed = mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-untrusted-shell", "done"),
ev_completed("resp-untrusted-shell-2"),
]),
)
.await;
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex
.start_or_steer_turn(
TurnInputRequest::user_input(vec![UserInput::Text {
text: "inspect the repository".to_string(),
text_elements: Vec::new(),
}])
.with_thread_settings(ThreadSettingsOverrides {
approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy),
permission_profile,
..Default::default()
}),
)
.await?;
let event = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
)
})
.await;
if matches!(
attack,
ShellAttack::ExactShellNameWithForbiddenInnerCommand | ShellAttack::DangerousCommandNever
) {
assert!(matches!(event, EventMsg::TurnComplete(_)));
let output = completed
.single_request()
.function_call_output_text(call_id)
.expect("forbidden command output");
assert!(
output.contains("rejected"),
"the forbidden command should be rejected: {output}"
);
#[cfg(windows)]
if matches!(attack, ShellAttack::ExactShellNameWithForbiddenInnerCommand) {
assert!(
output.contains("Remove-Item"),
"the forbidden PowerShell command should remain visible to policy: {output}"
);
}
} else {
let EventMsg::ExecApprovalRequest(approval) = event else {
panic!("workspace shell bypassed approval");
};
assert_eq!(approval.call_id, call_id);
assert!(!marker.exists(), "the shell ran before approval");
test.codex
.submit(Op::ExecApproval {
id: approval.effective_approval_id(),
turn_id: None,
decision: match attack {
ShellAttack::ApprovedCustomShell => ReviewDecision::Approved,
ShellAttack::SessionApprovalDoesNotTrustDifferentShell => {
ReviewDecision::ApprovedForSession
}
_ => ReviewDecision::denied("untrusted shell"),
},
})
.await?;
if other_shell.is_some() {
let event = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
)
})
.await;
let EventMsg::ExecApprovalRequest(approval) = event else {
panic!("a different workspace shell reused the first shell's session approval");
};
assert_eq!(approval.call_id, other_call_id);
assert!(
marker.exists(),
"the session-approved shell should have run"
);
test.codex
.submit(Op::ExecApproval {
id: approval.effective_approval_id(),
turn_id: None,
decision: ReviewDecision::denied("different untrusted shell"),
})
.await?;
}
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
}
assert_eq!(
marker.exists(),
matches!(
attack,
ShellAttack::ApprovedCustomShell
| ShellAttack::SessionApprovalDoesNotTrustDifferentShell
),
"only an explicitly approved custom shell should run"
);
Ok(())
}

View File

@@ -16,21 +16,6 @@ use ctor::ctor;
#[ctor]
pub static CODEX_ALIASES_TEMP_DIR: Option<TestBinaryDispatchGuard> = {
configure_test_binary_dispatch("codex-core-tests", |exe_name, argv1| {
#[cfg(windows)]
if exe_name.eq_ignore_ascii_case("powershell.exe")
|| exe_name.eq_ignore_ascii_case("powershell.evil")
|| exe_name.eq_ignore_ascii_case("pwsh.exe")
{
let executable = std::env::current_exe().expect("locate fake PowerShell executable");
if executable
.with_file_name(".codex-executable-identity-fixture")
.is_file()
{
let marker = executable.with_file_name("attacker-executed");
std::fs::write(marker, b"ran").expect("record fake PowerShell execution");
std::process::exit(0);
}
}
if argv1 == Some(CODEX_CORE_APPLY_PATCH_ARG1) {
return TestBinaryDispatchMode::DispatchArg0Only;
}
@@ -77,7 +62,6 @@ mod cyber_exec_policy;
mod deprecation_notice;
mod exec;
mod exec_policy;
mod executable_identity;
#[cfg(not(target_os = "windows"))]
mod extension_sandbox;
mod external_auth;

View File

@@ -3,6 +3,8 @@ use anyhow::Result;
use codex_config::types::ApprovalsReviewer;
use codex_core::TurnInputRequest;
use codex_core::config::Constrained;
use codex_core::shell::ShellType;
use codex_core::shell::get_shell;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
@@ -96,6 +98,9 @@ async fn guardian_network_approval_preserves_action_and_outcome_routing() -> Res
.context("expected network command")?
.to_string();
let second_command = first_command.clone();
let expected_command = get_shell(ShellType::Sh)
.context("expected local sh")?
.derive_exec_args(&first_command, /*use_login_shell*/ false);
let denial = "The destination is outside the approved test boundary.";
let responses = mount_sse_sequence(
&server,
@@ -178,7 +183,7 @@ async fn guardian_network_approval_preserves_action_and_outcome_routing() -> Res
"tool": "network_access",
"trigger": {
"callId": first_call_id,
"command": ["/bin/sh", "-c", first_command],
"command": expected_command,
"cwd": test.config.cwd,
"sandboxPermissions": "use_default",
"toolName": "exec_command",
@@ -1869,12 +1874,18 @@ async fn remote_guardian_network_decisions_are_scoped_to_each_request_and_enviro
.await
.context("remote session approval should bypass another network prompt")?;
let local_shell = get_shell(ShellType::Sh).context("expected local sh")?;
let mut expected_actions = Vec::with_capacity(cases.len());
for (call_id, command, environment, _, _) in &cases {
let cwd = environment
.cwd
.to_abs_path()
.with_context(|| format!("resolve the environment cwd for {call_id}"))?;
let command = if environment.environment_id == LOCAL_ENVIRONMENT_ID {
local_shell.derive_exec_args(command, /*use_login_shell*/ false)
} else {
vec!["/bin/sh".to_string(), "-c".to_string(), command.clone()]
};
expected_actions.push(json!({
"host": NETWORK_TEST_HOST,
"port": 80,
@@ -1883,7 +1894,7 @@ async fn remote_guardian_network_decisions_are_scoped_to_each_request_and_enviro
"tool": "network_access",
"trigger": {
"callId": call_id,
"command": ["/bin/sh", "-c", command],
"command": command,
"cwd": cwd,
"sandboxPermissions": "use_default",
"toolName": "exec_command",

View File

@@ -134,14 +134,9 @@ fn file_exists(path: &std::path::Path) -> Option<PathBuf> {
fn get_shell_path(
shell_type: ShellType,
provided_path: Option<&PathBuf>,
binary_name: &str,
fallback_paths: &[&str],
) -> Option<PathBuf> {
if let Some(path) = provided_path.and_then(|path| file_exists(path)) {
return Some(path);
}
let default_shell_path = get_user_shell_path();
if let Some(default_shell_path) = default_shell_path
&& detect_shell_type(&default_shell_path) == Some(shell_type)
@@ -165,8 +160,8 @@ fn get_shell_path(
const ZSH_FALLBACK_PATHS: &[&str] = &["/bin/zsh"];
fn get_zsh_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Zsh, path, "zsh", ZSH_FALLBACK_PATHS);
fn get_zsh_shell() -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Zsh, "zsh", ZSH_FALLBACK_PATHS);
shell_path.map(|shell_path| DetectedShell {
shell_type: ShellType::Zsh,
@@ -176,8 +171,8 @@ fn get_zsh_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
const BASH_FALLBACK_PATHS: &[&str] = &["/bin/bash", "/usr/bin/bash"];
fn get_bash_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Bash, path, "bash", BASH_FALLBACK_PATHS);
fn get_bash_shell() -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Bash, "bash", BASH_FALLBACK_PATHS);
shell_path.map(|shell_path| DetectedShell {
shell_type: ShellType::Bash,
@@ -187,8 +182,8 @@ fn get_bash_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
const SH_FALLBACK_PATHS: &[&str] = &["/bin/sh"];
fn get_sh_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Sh, path, "sh", SH_FALLBACK_PATHS);
fn get_sh_shell() -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Sh, "sh", SH_FALLBACK_PATHS);
shell_path.map(|shell_path| DetectedShell {
shell_type: ShellType::Sh,
@@ -212,12 +207,11 @@ const POWERSHELL_FALLBACK_PATHS: &[&str] =
#[cfg(not(windows))]
const POWERSHELL_FALLBACK_PATHS: &[&str] = &[];
fn get_powershell_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::PowerShell, path, "pwsh", PWSH_FALLBACK_PATHS)
.or_else(|| {
fn get_powershell_shell() -> Option<DetectedShell> {
let shell_path =
get_shell_path(ShellType::PowerShell, "pwsh", PWSH_FALLBACK_PATHS).or_else(|| {
get_shell_path(
ShellType::PowerShell,
path,
"powershell",
POWERSHELL_FALLBACK_PATHS,
)
@@ -229,8 +223,8 @@ fn get_powershell_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
})
}
fn get_cmd_shell(path: Option<&PathBuf>) -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Cmd, path, "cmd", &[]);
fn get_cmd_shell() -> Option<DetectedShell> {
let shell_path = get_shell_path(ShellType::Cmd, "cmd", &[]);
shell_path.map(|shell_path| DetectedShell {
shell_type: ShellType::Cmd,
@@ -252,19 +246,20 @@ pub fn ultimate_fallback_shell() -> DetectedShell {
}
}
/// Uses the model-provided path only to select a shell type, then discovers its executable.
pub fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> DetectedShell {
detect_shell_type(shell_path)
.and_then(|shell_type| get_shell(shell_type, Some(shell_path)))
.and_then(get_shell)
.unwrap_or_else(ultimate_fallback_shell)
}
pub fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<DetectedShell> {
pub fn get_shell(shell_type: ShellType) -> Option<DetectedShell> {
match shell_type {
ShellType::Zsh => get_zsh_shell(path),
ShellType::Bash => get_bash_shell(path),
ShellType::PowerShell => get_powershell_shell(path),
ShellType::Sh => get_sh_shell(path),
ShellType::Cmd => get_cmd_shell(path),
ShellType::Zsh => get_zsh_shell(),
ShellType::Bash => get_bash_shell(),
ShellType::PowerShell => get_powershell_shell(),
ShellType::Sh => get_sh_shell(),
ShellType::Cmd => get_cmd_shell(),
}
}
@@ -274,20 +269,20 @@ pub fn default_user_shell() -> DetectedShell {
pub fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> DetectedShell {
if cfg!(windows) {
get_shell(ShellType::PowerShell, /*path*/ None).unwrap_or_else(ultimate_fallback_shell)
get_shell(ShellType::PowerShell).unwrap_or_else(ultimate_fallback_shell)
} else {
let user_default_shell = user_shell_path
.and_then(|shell| detect_shell_type(&shell))
.and_then(|shell_type| get_shell(shell_type, /*path*/ None));
.and_then(get_shell);
let shell_with_fallback = if cfg!(target_os = "macos") {
user_default_shell
.or_else(|| get_shell(ShellType::Zsh, /*path*/ None))
.or_else(|| get_shell(ShellType::Bash, /*path*/ None))
.or_else(|| get_shell(ShellType::Zsh))
.or_else(|| get_shell(ShellType::Bash))
} else {
user_default_shell
.or_else(|| get_shell(ShellType::Bash, /*path*/ None))
.or_else(|| get_shell(ShellType::Zsh, /*path*/ None))
.or_else(|| get_shell(ShellType::Bash))
.or_else(|| get_shell(ShellType::Zsh))
};
shell_with_fallback.unwrap_or_else(ultimate_fallback_shell)