287 lines
7.3 KiB
Rust
287 lines
7.3 KiB
Rust
//! 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<dyn Fn(&str) -> String + 'a>;
|
|
pub type ExitAwareCaptureFilter<'a> = Box<dyn Fn(&str, i32) -> String + 'a>;
|
|
|
|
pub enum RunMode<'a> {
|
|
Filtered(CaptureFilter<'a>),
|
|
FilteredWithExit(ExitAwareCaptureFilter<'a>),
|
|
Streamed(Box<dyn StreamFilter + 'a>),
|
|
Passthrough,
|
|
}
|
|
|
|
fn run_captured_filter<F>(
|
|
mut cmd: Command,
|
|
tool_name: &str,
|
|
cmd_label: &str,
|
|
filter_fn: F,
|
|
opts: RunOptions<'_>,
|
|
timer: tracking::TimedExecution,
|
|
) -> Result<i32>
|
|
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<i32> {
|
|
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<F>(
|
|
cmd: Command,
|
|
tool_name: &str,
|
|
args_display: &str,
|
|
filter_fn: F,
|
|
opts: RunOptions<'_>,
|
|
) -> Result<i32>
|
|
where
|
|
F: Fn(&str) -> String,
|
|
{
|
|
run(
|
|
cmd,
|
|
tool_name,
|
|
args_display,
|
|
RunMode::Filtered(Box::new(filter_fn)),
|
|
opts,
|
|
)
|
|
}
|
|
|
|
pub fn run_filtered_with_exit<F>(
|
|
cmd: Command,
|
|
tool_name: &str,
|
|
args_display: &str,
|
|
filter_fn: F,
|
|
opts: RunOptions<'_>,
|
|
) -> Result<i32>
|
|
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<i32> {
|
|
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<dyn StreamFilter + '_>,
|
|
opts: RunOptions<'_>,
|
|
) -> Result<i32> {
|
|
run(
|
|
cmd,
|
|
tool_name,
|
|
args_display,
|
|
RunMode::Streamed(filter),
|
|
opts,
|
|
)
|
|
}
|