fix(core/history): classify sh -lc reads for explore cells

This commit is contained in:
James Peter
2025-10-10 10:57:29 +10:00
parent 4fae3cb247
commit 32faff91d0
3 changed files with 50 additions and 25 deletions
+17 -14
View File
@@ -1,11 +1,10 @@
use crate::bash::try_parse_bash;
use crate::bash::try_parse_word_only_commands_sequence;
use crate::util::is_shell_like_executable;
use serde::Deserialize;
use serde::Serialize;
use shlex::split as shlex_split;
use shlex::try_join as shlex_try_join;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum ParsedCommand {
Read {
@@ -49,16 +48,8 @@ fn shlex_join(tokens: &[String]) -> String {
.unwrap_or_else(|_| "<command included NUL byte>".to_string())
}
fn is_bash_executable(cmd: &str) -> bool {
let trimmed = cmd.trim_matches('"').trim_matches('\'');
if trimmed.eq_ignore_ascii_case("bash") || trimmed.eq_ignore_ascii_case("bash.exe") {
return true;
}
Path::new(trimmed)
.file_name()
.and_then(|s| s.to_str())
.map(|name| name.eq_ignore_ascii_case("bash") || name.eq_ignore_ascii_case("bash.exe"))
.unwrap_or(false)
fn is_shell_executable(cmd: &str) -> bool {
is_shell_like_executable(cmd)
}
/// DO NOT REVIEW THIS CODE BY HAND
@@ -124,6 +115,18 @@ mod tests {
);
}
#[test]
fn sh_lc_sed_is_read_command() {
let inner = "sed -n '10,20p' code-rs/tui/src/chatwidget.rs";
assert_parsed(
&vec_str(&["sh", "-lc", inner]),
vec![ParsedCommand::Read {
cmd: inner.to_string(),
name: "chatwidget.rs".to_string(),
}],
);
}
#[test]
fn git_diff_staged_is_read_command() {
assert_parsed(
@@ -1119,7 +1122,7 @@ fn normalize_tokens(cmd: &[String]) -> Vec<String> {
// Do not re-shlex already-tokenized input; just drop the prefix.
rest.to_vec()
}
[bash, flag, script] if is_bash_executable(bash) && (flag == "-c" || flag == "-lc") => {
[bash, flag, script] if is_shell_executable(bash) && (flag == "-c" || flag == "-lc") => {
shlex_split(script)
.unwrap_or_else(|| vec!["bash".to_string(), flag.clone(), script.clone()])
}
@@ -1400,7 +1403,7 @@ fn parse_bash_lc_commands(original: &[String]) -> Option<Vec<ParsedCommand>> {
let [bash, flag, script] = original else {
return None;
};
if !is_bash_executable(bash) || flag != "-lc" {
if !is_shell_executable(bash) || flag != "-lc" {
return None;
}
if let Some(tree) = try_parse_bash(script) {
+3 -10
View File
@@ -1,9 +1,10 @@
use serde::Deserialize;
use serde::Serialize;
use shlex;
use std::path::Path;
use std::path::PathBuf;
use crate::util::is_shell_like_executable;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct ZshShell {
pub(crate) shell_path: String,
@@ -137,15 +138,7 @@ fn strip_bash_lc(command: &[String]) -> Option<String> {
}
fn is_bash_like(cmd: &str) -> bool {
let trimmed = cmd.trim_matches('"').trim_matches('\'');
if trimmed.eq_ignore_ascii_case("bash") || trimmed.eq_ignore_ascii_case("bash.exe") {
return true;
}
Path::new(trimmed)
.file_name()
.and_then(|s| s.to_str())
.map(|name| name.eq_ignore_ascii_case("bash") || name.eq_ignore_ascii_case("bash.exe"))
.unwrap_or(false)
is_shell_like_executable(cmd)
}
#[cfg(unix)]
+30 -1
View File
@@ -1,3 +1,4 @@
use std::path::Path;
use std::time::Duration;
use std::sync::Arc;
@@ -25,11 +26,39 @@ pub fn escape_command(command: &[String]) -> String {
pub fn strip_bash_lc_and_escape(command: &[String]) -> String {
match command {
[first, second, third] if first == "bash" && second == "-lc" => third.clone(),
[first, second, third]
if is_shell_like_executable(first)
&& (second == "-lc" || second == "-c") =>
{
third.clone()
}
_ => escape_command(command),
}
}
pub(crate) fn is_shell_like_executable(token: &str) -> bool {
let trimmed = token.trim_matches('"').trim_matches('\'');
let name = Path::new(trimmed)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(trimmed)
.to_ascii_lowercase();
matches!(
name.as_str(),
"bash"
| "bash.exe"
| "sh"
| "sh.exe"
| "zsh"
| "zsh.exe"
| "dash"
| "dash.exe"
| "ksh"
| "ksh.exe"
| "busybox"
)
}
#[allow(dead_code)]
pub fn notify_on_sigint() -> Arc<Notify> {
let notify = Arc::new(Notify::new());