diff options
Diffstat (limited to 'src/platform/mod.rs')
| -rw-r--r-- | src/platform/mod.rs | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 82a6704d..1415181e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -157,6 +157,90 @@ mod fallback; #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] pub use fallback::*; +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn available_pane_shell_from_job(child_pid: u32, job: ForegroundJob) -> Option<String> { + if job.process_group_id != child_pid + || job.processes.iter().any(|process| process.pid != child_pid) + { + return None; + } + job.processes + .into_iter() + .find(|process| process.pid == child_pid) + .map(|process| process.name) + .filter(|name| is_pane_shell_process_name(name)) +} + +fn normalized_process_name(name: &str) -> String { + name.rsplit(['/', '\\']) + .next() + .unwrap_or(name) + .trim_start_matches('-') + .trim_end_matches(".exe") + .to_ascii_lowercase() +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn is_powershell_process_name(name: &str) -> bool { + matches!( + normalized_process_name(name).as_str(), + "pwsh" | "powershell" + ) +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn interactive_unix_shell_command( + argv: &[String], + shell_name: &str, + quote_posix_arg: fn(&str) -> String, +) -> Option<String> { + let quote = if is_powershell_process_name(shell_name) { + quote_powershell_arg + } else { + quote_posix_arg + }; + let mut parts = argv.iter(); + let mut command = quote(parts.next()?); + for part in parts { + command.push(' '); + command.push_str("e(part)); + } + Some(command) +} + +pub(crate) fn quote_powershell_arg(value: &str) -> String { + if !value.is_empty() + && value.bytes().all(|byte| { + byte.is_ascii_alphanumeric() + || matches!(byte, b'_' | b'-' | b'.' | b'/' | b':' | b'+' | b'=') + }) + { + return value.to_string(); + } + format!("'{}'", value.replace('\'', "''")) +} + +pub(crate) fn is_pane_shell_process_name(name: &str) -> bool { + let normalized = normalized_process_name(name); + matches!( + normalized.as_str(), + "sh" | "bash" + | "dash" + | "zsh" + | "fish" + | "ksh" + | "mksh" + | "csh" + | "tcsh" + | "elvish" + | "xonsh" + | "nu" + | "pwsh" + | "powershell" + | "cmd" + ) +} + #[cfg(not(any(target_os = "linux", target_os = "macos")))] pub fn process_agent_hint(_pid: u32) -> Option<crate::detect::Agent> { None @@ -227,6 +311,16 @@ mod tests { use super::*; #[test] + fn pane_shell_process_names_reject_exec_replacement_programs() { + for shell in ["bash", "-zsh", "/bin/fish", "pwsh", "powershell.exe"] { + assert!(is_pane_shell_process_name(shell), "{shell}"); + } + for program in ["vim", "nvim", "cargo", "test-runner", "opencode"] { + assert!(!is_pane_shell_process_name(program), "{program}"); + } + } + + #[test] fn detached_custom_command_preserves_unix_login_shell_flag() { let cmd = detached_custom_command_process("echo hello"); assert_eq!(cmd.get_program(), std::ffi::OsStr::new("/bin/sh")); @@ -269,6 +363,28 @@ mod tests { assert_eq!(parse_agent_env_hint(b"HERDR_AGENT=not-an-agent\0"), None); } + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn interactive_shell_command_quotes_for_posix_and_powershell() { + let argv = vec![ + "pi".into(), + String::new(), + "two words".into(), + "a'b".into(), + "$HOME".into(), + "semi;colon".into(), + "@options".into(), + ]; + assert_eq!( + interactive_shell_command(&argv, "bash").as_deref(), + Some("pi '' 'two words' 'a'\\''b' '$HOME' 'semi;colon' @options") + ); + assert_eq!( + interactive_shell_command(&argv, "pwsh").as_deref(), + Some("pi '' 'two words' 'a''b' '$HOME' 'semi;colon' '@options'") + ); + } + #[test] fn read_limited_reader_returns_complete_data_under_limit() { let input = std::io::Cursor::new(b"image".to_vec()); |
