aboutsummaryrefslogtreecommitdiff
path: root/src/platform/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform/mod.rs')
-rw-r--r--src/platform/mod.rs116
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(&quote(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());