feat: introduce prettify_command_for_display()

This commit is contained in:
Michael Bolin
2025-09-14 16:50:15 -07:00
parent 916fdc2a37
commit cfff3af45c
4 changed files with 216 additions and 3 deletions

View File

@@ -1,3 +1,4 @@
use tree_sitter::Node;
use tree_sitter::Parser;
use tree_sitter::Tree;
use tree_sitter_bash::LANGUAGE as BASH;
@@ -75,7 +76,7 @@ pub fn try_parse_word_only_commands_sequence(tree: &Tree, src: &str) -> Option<V
let mut commands = Vec::new();
for node in command_nodes {
if let Some(words) = parse_plain_command_from_node(node, src) {
if let Some(words) = extract_words_from_command_node(node, src) {
commands.push(words);
} else {
return None;
@@ -84,7 +85,10 @@ pub fn try_parse_word_only_commands_sequence(tree: &Tree, src: &str) -> Option<V
Some(commands)
}
fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Vec<String>> {
/// Extract the plain words of a simple command node, normalizing quoted
/// strings into their contents. Returns None if the node contains unsupported
/// constructs for a word-only command.
pub(crate) fn extract_words_from_command_node(cmd: Node, src: &str) -> Option<Vec<String>> {
if cmd.kind() != "command" {
return None;
}
@@ -130,6 +134,43 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Ve
Some(words)
}
/// Find the earliest `command` node in source order within the parse tree.
pub(crate) fn find_first_command_node(tree: &Tree) -> Option<Node<'_>> {
let root = tree.root_node();
let mut cursor = root.walk();
let mut stack = vec![root];
let mut best: Option<Node> = None;
while let Some(node) = stack.pop() {
if node.is_named() && node.kind() == "command" {
if best.map_or(true, |b| node.start_byte() < b.start_byte()) {
best = Some(node);
}
}
for child in node.children(&mut cursor) {
stack.push(child);
}
}
best
}
/// Given the first command node, return the byte index in `src` at which the
/// remainder script starts, only if the next non-whitespace token is `&&`.
pub(crate) fn remainder_start_after_and(first_cmd: Node, src: &str) -> Option<usize> {
let mut sib = first_cmd.next_sibling()?;
while !sib.is_named() && sib.kind().trim().is_empty() {
sib = sib.next_sibling()?;
}
if sib.is_named() || sib.kind() != "&&" {
return None;
}
let mut idx = sib.end_byte() as usize;
let bytes = src.as_bytes();
while idx < bytes.len() && bytes[idx].is_ascii_whitespace() {
idx += 1;
}
if idx >= bytes.len() { None } else { Some(idx) }
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -11,6 +11,7 @@ use std::time::Duration;
use crate::AuthManager;
use crate::client_common::REVIEW_PROMPT;
use crate::event_mapping::map_response_item_to_event_messages;
use crate::prettify_command_for_display::prettify_command_for_display;
use async_channel::Receiver;
use async_channel::Sender;
use codex_apply_patch::ApplyPatchAction;
@@ -2783,7 +2784,8 @@ async fn handle_container_exec_with_params(
params.with_escalated_permissions.unwrap_or(false),
)
};
let command_for_display = params.command.clone();
let command_for_display = prettify_command_for_display(&params.command)
.unwrap_or_else(|| params.command.clone());
(params, safety, command_for_display)
}
};

View File

@@ -36,6 +36,7 @@ mod mcp_tool_call;
mod message_history;
mod model_provider_info;
pub mod parse_command;
mod prettify_command_for_display;
mod truncate;
mod unified_exec;
mod user_instructions;

View File

@@ -0,0 +1,169 @@
use crate::bash::extract_words_from_command_node;
use crate::bash::find_first_command_node;
use crate::bash::remainder_start_after_and;
use crate::bash::try_parse_bash;
/// If one exists, returns a copy of `command` that reads more naturally in logs
/// and error messages.
///
/// When the command is a classic shell wrapper such as `bash -lc "cd repo &&
/// git status"`, the returned value contains only the prettified script (with
/// the leading `cd`/`pushd` removed) and excludes the `bash -lc` wrapper.
/// Any other command returns `None`.
pub fn prettify_command_for_display(command: &[String]) -> Option<Vec<String>> {
let shell_script = parse_shell_script_from_shell_invocation(command)?;
let tree = try_parse_bash(&shell_script)?;
// Find the earliest command node in source order.
let first_cmd = find_first_command_node(&tree)?;
// Verify the first command is `cd <dir>` or `pushd <dir>` (exactly 2 words).
let words = extract_words_from_command_node(first_cmd, &shell_script)?;
if !is_command_cd_to_directory(&words) {
return None;
}
// Determine textual remainder using sibling tokens in the parse tree.
let idx = remainder_start_after_and(first_cmd, &shell_script)?;
let remainder = shell_script[idx..].to_string();
Some(vec![remainder])
}
/// This is similar to [`crate::shell::strip_bash_lc`] and should potentially
/// be unified with it.
fn parse_shell_script_from_shell_invocation(command: &[String]) -> Option<String> {
match command {
// exactly three items
[first, second, third]
// first two must be "bash", "-lc"
if first == "bash" && second == "-lc" =>
{
Some(third.clone())
}
_ => None,
}
}
fn is_command_cd_to_directory(command: &[String]) -> bool {
matches!(command, [first, _dir] if first == "cd" || first == "pushd")
}
// Helper moved to crate::bash
#[cfg(test)]
mod tests {
use super::prettify_command_for_display;
use pretty_assertions::assert_eq;
#[test]
fn cd_prefix_in_bash_script_is_hidden() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd foo && echo hi".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["echo hi".to_string()]));
}
#[test]
fn cd_prefix_with_quoted_path_is_hidden() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
" cd 'foo bar' && ls".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["ls".to_string()]));
}
#[test]
fn cd_prefix_with_additional_cd_is_preserved() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd foo && cd bar && ls".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["cd bar && ls".to_string()]));
}
#[test]
fn cd_prefix_with_or_connector_is_preserved() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd and_and || echo \"couldn't find the dir for &&\"".to_string(),
];
let display = prettify_command_for_display(&command);
// Not a classic wrapper (uses ||), so no prettified form.
assert_eq!(display, None);
}
#[test]
fn cd_prefix_preserves_operators_between_remaining_commands() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd foo && ls && git status".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["ls && git status".to_string()]));
}
#[test]
fn cd_prefix_preserves_pipelines() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd foo && ls | rg foo".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["ls | rg foo".to_string()]));
}
#[test]
fn cd_prefix_preserves_sequence_operator() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd foo && ls; git status".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["ls; git status".to_string()]));
}
#[test]
fn non_shell_command_is_returned_unmodified() {
let command = vec!["rg".to_string(), "--files".to_string()];
let display = prettify_command_for_display(&command);
// Not a shell wrapper, so no prettified form.
assert_eq!(display, None);
}
#[test]
fn cd_prefix_with_or_operator_is_preserved() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd missing && ls || echo 'fallback'".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(display, Some(vec!["ls || echo 'fallback'".to_string()]));
}
#[test]
fn cd_prefix_with_ampersands_in_string_is_hidden() {
let command = vec![
"bash".to_string(),
"-lc".to_string(),
"cd foo && echo \"checking && markers\"".to_string(),
];
let display = prettify_command_for_display(&command);
assert_eq!(
display,
Some(vec!["echo \"checking && markers\"".to_string()])
);
}
}