mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
feat: introduce prettify_command_for_display()
This commit is contained in:
@@ -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::*;
|
||||
|
||||
@@ -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(¶ms.command)
|
||||
.unwrap_or_else(|| params.command.clone());
|
||||
(params, safety, command_for_display)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
169
codex-rs/core/src/prettify_command_for_display.rs
Normal file
169
codex-rs/core/src/prettify_command_for_display.rs
Normal 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()])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user