diff --git a/codex-rs/core/src/bash.rs b/codex-rs/core/src/bash.rs index 07fc125f..e85ce76a 100644 --- a/codex-rs/core/src/bash.rs +++ b/codex-rs/core/src/bash.rs @@ -88,17 +88,33 @@ pub fn try_parse_word_only_commands_sequence(tree: &Tree, src: &str) -> Option bool { + if shell == "bash" || shell == "zsh" { + return true; + } + + let shell_name = std::path::Path::new(shell) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or(shell); + matches!(shell_name, "bash" | "zsh") +} + +pub fn extract_bash_command(command: &[String]) -> Option<(&str, &str)> { + let [shell, flag, script] = command else { + return None; + }; + if flag != "-lc" || !is_well_known_sh_shell(shell) { + return None; + } + Some((shell, script)) +} + /// Returns the sequence of plain commands within a `bash -lc "..."` or /// `zsh -lc "..."` invocation when the script only contains word-only commands /// joined by safe operators. pub fn parse_shell_lc_plain_commands(command: &[String]) -> Option>> { - let [shell, flag, script] = command else { - return None; - }; - - if flag != "-lc" || !(shell == "bash" || shell == "zsh") { - return None; - } + let (_, script) = extract_bash_command(command)?; let tree = try_parse_shell(script)?; try_parse_word_only_commands_sequence(&tree, script) diff --git a/codex-rs/core/src/parse_command.rs b/codex-rs/core/src/parse_command.rs index 53402f88..61454db4 100644 --- a/codex-rs/core/src/parse_command.rs +++ b/codex-rs/core/src/parse_command.rs @@ -1,3 +1,4 @@ +use crate::bash::extract_bash_command; use crate::bash::try_parse_shell; use crate::bash::try_parse_word_only_commands_sequence; use codex_protocol::parse_command::ParsedCommand; @@ -853,6 +854,29 @@ mod tests { }], ); } + + #[test] + fn bin_bash_lc_sed() { + assert_parsed( + &shlex_split_safe("/bin/bash -lc 'sed -n '1,10p' Cargo.toml'"), + vec![ParsedCommand::Read { + cmd: "sed -n '1,10p' Cargo.toml".to_string(), + name: "Cargo.toml".to_string(), + path: PathBuf::from("Cargo.toml"), + }], + ); + } + #[test] + fn bin_zsh_lc_sed() { + assert_parsed( + &shlex_split_safe("/bin/zsh -lc 'sed -n '1,10p' Cargo.toml'"), + vec![ParsedCommand::Read { + cmd: "sed -n '1,10p' Cargo.toml".to_string(), + name: "Cargo.toml".to_string(), + path: PathBuf::from("Cargo.toml"), + }], + ); + } } pub fn parse_command_impl(command: &[String]) -> Vec { @@ -1166,18 +1190,13 @@ fn parse_find_query_and_path(tail: &[String]) -> (Option, Option } fn parse_shell_lc_commands(original: &[String]) -> Option> { - let [shell, flag, script] = original else { - return None; - }; - if flag != "-lc" || !(shell == "bash" || shell == "zsh") { - return None; - } + let (_, script) = extract_bash_command(original)?; + if let Some(tree) = try_parse_shell(script) && let Some(all_commands) = try_parse_word_only_commands_sequence(&tree, script) && !all_commands.is_empty() { - let script_tokens = shlex_split(script) - .unwrap_or_else(|| vec![shell.clone(), flag.clone(), script.clone()]); + let script_tokens = shlex_split(script).unwrap_or_else(|| vec![script.to_string()]); // Strip small formatting helpers (e.g., head/tail/awk/wc/etc) so we // bias toward the primary command when pipelines are present. // First, drop obvious small formatting helpers (e.g., wc/awk/etc). @@ -1186,7 +1205,7 @@ fn parse_shell_lc_commands(original: &[String]) -> Option> { let filtered_commands = drop_small_formatting_commands(all_commands); if filtered_commands.is_empty() { return Some(vec![ParsedCommand::Unknown { - cmd: script.clone(), + cmd: script.to_string(), }]); } // Build parsed commands, tracking `cd` segments to compute effective file paths. @@ -1250,7 +1269,7 @@ fn parse_shell_lc_commands(original: &[String]) -> Option> { }); if has_pipe && has_sed_n { ParsedCommand::Read { - cmd: script.clone(), + cmd: script.to_string(), name, path, } @@ -1295,7 +1314,7 @@ fn parse_shell_lc_commands(original: &[String]) -> Option> { return Some(commands); } Some(vec![ParsedCommand::Unknown { - cmd: script.clone(), + cmd: script.to_string(), }]) } diff --git a/codex-rs/tui/src/exec_command.rs b/codex-rs/tui/src/exec_command.rs index 70ec4e84..6f2212b0 100644 --- a/codex-rs/tui/src/exec_command.rs +++ b/codex-rs/tui/src/exec_command.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::path::PathBuf; +use codex_core::bash::extract_bash_command; use dirs::home_dir; use shlex::try_join; @@ -8,19 +9,11 @@ pub(crate) fn escape_command(command: &[String]) -> String { try_join(command.iter().map(String::as_str)).unwrap_or_else(|_| command.join(" ")) } -fn is_login_shell_with_lc(shell: &str) -> bool { - let shell_name = std::path::Path::new(shell) - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(shell); - matches!(shell_name, "bash" | "zsh") -} - pub(crate) fn strip_bash_lc_and_escape(command: &[String]) -> String { - match command { - [first, second, third] if is_login_shell_with_lc(first) && second == "-lc" => third.clone(), - _ => escape_command(command), + if let Some((_, script)) = extract_bash_command(command) { + return script.to_string(); } + escape_command(command) } /// If `path` is absolute and inside $HOME, return the part *after* the home