//! Shared command execution skeleton for filter modules. use anyhow::{Context, Result}; use std::process::Command; use crate::core::stream::{self, FilterMode, StdinMode, StreamFilter}; use crate::core::tracking; /// Compose `filtered` with an optional recovery `hint`, cap the total at `raw` /// (never emit more tokens than the command), print it, and return what was /// emitted so the caller tracks exactly that. pub fn emit_guarded(filtered: &str, hint: Option<&str>, raw: &str) -> String { let body = match hint { Some(h) => format!("{}\n{}", filtered, h), None => filtered.to_string(), }; let shown = crate::core::guard::never_worse(raw, &body).to_string(); println!("{}", shown); shown } pub fn print_with_hint( filtered: &str, tee_raw: &str, guard_raw: &str, tee_label: &str, exit_code: i32, ) -> String { let hint = crate::core::tee::tee_and_hint(tee_raw, tee_label, exit_code); emit_guarded(filtered, hint.as_deref(), guard_raw) } #[derive(Default)] pub struct RunOptions<'a> { pub tee_label: Option<&'a str>, pub filter_stdout_only: bool, pub skip_filter_on_failure: bool, pub no_trailing_newline: bool, /// Forward rtk's own stdin to the child process. Needed for commands that /// can read from a pipe (e.g. `cat file | rtk wc`); without it the child /// gets an empty stdin and reports zero. pub inherit_stdin: bool, } impl<'a> RunOptions<'a> { pub fn with_tee(label: &'a str) -> Self { Self { tee_label: Some(label), ..Default::default() } } pub fn stdout_only() -> Self { Self { filter_stdout_only: true, ..Default::default() } } pub fn tee(mut self, label: &'a str) -> Self { self.tee_label = Some(label); self } pub fn early_exit_on_failure(mut self) -> Self { self.skip_filter_on_failure = true; self } pub fn no_trailing_newline(mut self) -> Self { self.no_trailing_newline = true; self } pub fn inherit_stdin(mut self) -> Self { self.inherit_stdin = true; self } } pub type CaptureFilter<'a> = Box String + 'a>; pub type ExitAwareCaptureFilter<'a> = Box String + 'a>; pub enum RunMode<'a> { Filtered(CaptureFilter<'a>), FilteredWithExit(ExitAwareCaptureFilter<'a>), Streamed(Box), Passthrough, } fn run_captured_filter( mut cmd: Command, tool_name: &str, cmd_label: &str, filter_fn: F, opts: RunOptions<'_>, timer: tracking::TimedExecution, ) -> Result where F: Fn(&str, i32) -> String, { let stdin_mode = if opts.inherit_stdin { StdinMode::Inherit } else { StdinMode::Null }; let result = stream::run_streaming(&mut cmd, stdin_mode, FilterMode::CaptureOnly) .with_context(|| format!("Failed to run {}", tool_name))?; let exit_code = result.exit_code; let raw = &result.raw; let raw_stdout = &result.raw_stdout; if opts.skip_filter_on_failure && exit_code != 0 { if !result.raw_stdout.trim().is_empty() { print!("{}", result.raw_stdout); } if !result.raw_stderr.trim().is_empty() { eprint!("{}", result.raw_stderr); } timer.track(cmd_label, &format!("rtk {}", cmd_label), raw, raw); return Ok(exit_code); } let text_to_filter = if opts.filter_stdout_only { raw_stdout } else { raw }; let filtered = filter_fn(text_to_filter, exit_code); let raw_for_tracking = if opts.filter_stdout_only { raw_stdout } else { raw }; let shown = if let Some(label) = opts.tee_label { print_with_hint(&filtered, raw, raw_for_tracking, label, exit_code) } else { let guarded = crate::core::guard::never_worse(raw_for_tracking, &filtered).to_string(); if opts.no_trailing_newline { print!("{}", guarded); } else { println!("{}", guarded); } guarded }; timer.track( cmd_label, &format!("rtk {}", cmd_label), raw_for_tracking, &shown, ); Ok(exit_code) } pub fn run( mut cmd: Command, tool_name: &str, args_display: &str, mode: RunMode<'_>, opts: RunOptions<'_>, ) -> Result { let timer = tracking::TimedExecution::start(); let cmd_label = format!("{} {}", tool_name, args_display); match mode { RunMode::Filtered(filter_fn) => run_captured_filter( cmd, tool_name, &cmd_label, move |text, _| filter_fn(text), opts, timer, ), RunMode::FilteredWithExit(filter_fn) => run_captured_filter( cmd, tool_name, &cmd_label, move |text, exit_code| filter_fn(text, exit_code), opts, timer, ), RunMode::Streamed(filter) => { let result = stream::run_streaming(&mut cmd, StdinMode::Null, FilterMode::Streaming(filter)) .with_context(|| format!("Failed to run {}", tool_name))?; if let Some(label) = opts.tee_label { if let Some(hint) = crate::core::tee::tee_and_hint(&result.raw, label, result.exit_code) { println!("{}", hint); } } timer.track( &cmd_label, &format!("rtk {}", cmd_label), &result.raw, &result.filtered, ); Ok(result.exit_code) } RunMode::Passthrough => { let result = stream::run_streaming(&mut cmd, StdinMode::Inherit, FilterMode::Passthrough) .with_context(|| format!("Failed to run {}", tool_name))?; timer.track_passthrough(&cmd_label, &format!("rtk {} (passthrough)", cmd_label)); Ok(result.exit_code) } } } pub fn run_filtered( cmd: Command, tool_name: &str, args_display: &str, filter_fn: F, opts: RunOptions<'_>, ) -> Result where F: Fn(&str) -> String, { run( cmd, tool_name, args_display, RunMode::Filtered(Box::new(filter_fn)), opts, ) } pub fn run_filtered_with_exit( cmd: Command, tool_name: &str, args_display: &str, filter_fn: F, opts: RunOptions<'_>, ) -> Result where F: Fn(&str, i32) -> String, { run( cmd, tool_name, args_display, RunMode::FilteredWithExit(Box::new(filter_fn)), opts, ) } pub fn run_passthrough(tool: &str, args: &[std::ffi::OsString], verbose: u8) -> Result { if verbose > 0 { eprintln!("{} passthrough: {:?}", tool, args); } let mut cmd = crate::core::utils::resolved_command(tool); cmd.args(args); let args_str = tracking::args_display(args); run( cmd, tool, &args_str, RunMode::Passthrough, RunOptions::default(), ) } pub fn run_streamed( cmd: Command, tool_name: &str, args_display: &str, filter: Box, opts: RunOptions<'_>, ) -> Result { run( cmd, tool_name, args_display, RunMode::Streamed(filter), opts, ) }