feat: directly execute shell commands from within the REPL
This commit is contained in:
+81
-4
@@ -15,7 +15,8 @@ use crate::config::{AssetCategory, paths};
|
|||||||
use crate::function::supervisor::{GuardrailAction, check_pending_agents_guardrail};
|
use crate::function::supervisor::{GuardrailAction, check_pending_agents_guardrail};
|
||||||
use crate::render::render_error;
|
use crate::render::render_error;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
AbortSignal, abortable_run_with_spinner, create_abort_signal, dimmed_text, set_text, temp_file,
|
AbortSignal, SHELL, abortable_run_with_spinner, create_abort_signal, dimmed_text, run_command,
|
||||||
|
set_text, temp_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::sandbox::SANDBOX_ENV_FLAG;
|
use crate::sandbox::SANDBOX_ENV_FLAG;
|
||||||
@@ -961,9 +962,13 @@ pub async fn run_repl_command(
|
|||||||
_ => unknown_command()?,
|
_ => unknown_command()?,
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
reset_continuation(ctx);
|
if let Some(cmd) = try_extract_shell_command(line) {
|
||||||
let input = Input::from_str(ctx, line, None)?;
|
handle_shell_passthrough(cmd)?;
|
||||||
ask(ctx, abort_signal.clone(), input, true).await?;
|
} else {
|
||||||
|
reset_continuation(ctx);
|
||||||
|
let input = Input::from_str(ctx, line, None)?;
|
||||||
|
ask(ctx, abort_signal.clone(), input, true).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,10 +1184,12 @@ fn dump_repl_help() {
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
println!(
|
println!(
|
||||||
r###"{head}
|
r###"{head}
|
||||||
|
{:<24} Run an arbitrary shell command (stdout/stderr stream to your terminal; Ctrl+C interrupts)
|
||||||
|
|
||||||
Type ::: to start multi-line editing, type ::: to finish it.
|
Type ::: to start multi-line editing, type ::: to finish it.
|
||||||
Press Ctrl+O to open an editor for editing the input buffer.
|
Press Ctrl+O to open an editor for editing the input buffer.
|
||||||
Press Ctrl+C to cancel the response, Ctrl+D to exit the REPL."###,
|
Press Ctrl+C to cancel the response, Ctrl+D to exit the REPL."###,
|
||||||
|
"!<command>",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,6 +1205,25 @@ fn parse_command(line: &str) -> Option<(&str, Option<&str>)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_extract_shell_command(line: &str) -> Option<&str> {
|
||||||
|
let rest = line.strip_prefix('!')?;
|
||||||
|
Some(rest.trim_start())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_shell_passthrough(cmd: &str) -> Result<()> {
|
||||||
|
if cmd.is_empty() {
|
||||||
|
eprintln!("Usage: !<command>");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = run_command(&SHELL.cmd, &[&SHELL.arg, cmd], None)?;
|
||||||
|
if status != 0 {
|
||||||
|
eprintln!("[exit {status}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn split_first_arg(args: Option<&str>) -> Option<(&str, Option<&str>)> {
|
fn split_first_arg(args: Option<&str>) -> Option<(&str, Option<&str>)> {
|
||||||
args.map(|v| match v.split_once(' ') {
|
args.map(|v| match v.split_once(' ') {
|
||||||
Some((subcmd, args)) => (subcmd, Some(args.trim())),
|
Some((subcmd, args)) => (subcmd, Some(args.trim())),
|
||||||
@@ -1532,6 +1558,57 @@ mod tests {
|
|||||||
assert_eq!(parse_command("."), Some((".", None)));
|
assert_eq!(parse_command("."), Some((".", None)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_strips_bang() {
|
||||||
|
assert_eq!(try_extract_shell_command("!ls"), Some("ls"));
|
||||||
|
assert_eq!(try_extract_shell_command("!ls -la"), Some("ls -la"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_trims_inner_whitespace() {
|
||||||
|
assert_eq!(try_extract_shell_command("! echo hi"), Some("echo hi"));
|
||||||
|
assert_eq!(try_extract_shell_command("! ls"), Some("ls"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_only_bang_yields_empty() {
|
||||||
|
assert_eq!(try_extract_shell_command("!"), Some(""));
|
||||||
|
assert_eq!(try_extract_shell_command("! "), Some(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_rejects_leading_whitespace() {
|
||||||
|
assert!(try_extract_shell_command(" !ls").is_none());
|
||||||
|
assert!(try_extract_shell_command("\t!ls").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_rejects_inline_bang() {
|
||||||
|
assert!(try_extract_shell_command("echo !foo").is_none());
|
||||||
|
assert!(try_extract_shell_command("hello world").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_strips_one_leading_bang() {
|
||||||
|
assert_eq!(try_extract_shell_command("!!ls"), Some("!ls"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_extract_shell_command_preserves_pipes_and_redirects() {
|
||||||
|
assert_eq!(
|
||||||
|
try_extract_shell_command("!ls -la | grep yaml"),
|
||||||
|
Some("ls -la | grep yaml")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
try_extract_shell_command("!cat foo.txt > /tmp/out"),
|
||||||
|
Some("cat foo.txt > /tmp/out")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
try_extract_shell_command(r#"!echo "$HOME""#),
|
||||||
|
Some(r#"echo "$HOME""#)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn split_first_arg_none_input() {
|
fn split_first_arg_none_input() {
|
||||||
assert!(split_first_arg(None).is_none());
|
assert!(split_first_arg(None).is_none());
|
||||||
|
|||||||
Reference in New Issue
Block a user