Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ rtk smart file.rs # 2-line heuristic code summary
rtk find "*.rs" . # Compact find results
rtk grep "pattern" . # Grouped search results
rtk diff file1 file2 # Condensed diff
rtk sed -n '1,200p' file.log # Compact large sed output
```

### Git
Expand Down
1 change: 1 addition & 0 deletions docs/guide/resources/what-rtk-covers.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Typical savings: 60-99%.
| `grep` | 70% | Truncated lines, grouped by file |
| `diff` | 65% | Context reduced |
| `wc` | 60% | Compact counts |
| `sed` | 60% | Large output shortened with head/tail and line truncation |
| `cat` / `head` / `tail <file>` | 60-80% | Smart file reading via `rtk read` |
| `rtk smart <file>` | 85% | 2-line heuristic code summary (signatures only) |

Expand Down
1 change: 1 addition & 0 deletions src/cmds/system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- `read.rs` uses `core/filter` for language-aware code stripping (FilterLevel: none/minimal/aggressive)
- `grep_cmd.rs` reads `core/config` for `limits.grep_max_results` and `limits.grep_max_per_file`
- `sed_cmd.rs` runs native `sed` unchanged and compacts large stdout with head/tail preservation and line truncation
- `local_llm.rs` (`rtk smart`) uses `core/filter` for heuristic file summarization
- `format_cmd.rs` is a cross-ecosystem dispatcher: auto-detects and routes to `prettier_cmd` or `ruff_cmd` (black is handled inline, not as a separate module)

Expand Down
144 changes: 144 additions & 0 deletions src/cmds/system/sed_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//! Runs sed and compacts large stdout while preserving native behavior.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module-level doc comment says sed runs “while preserving native behavior”, but the implementation compacts/truncates stdout and (as written) disables stdin. Please reword to reflect what’s actually preserved (exit code, stderr) vs what changes (stdout compaction / potential stdin behavior).

Suggested change
//! Runs sed and compacts large stdout while preserving native behavior.
//! Runs sed, preserving the underlying exit code and stderr while compacting
//! and truncating large stdout output.

Copilot uses AI. Check for mistakes.

use crate::core::stream::exec_capture;
use crate::core::tracking;
use crate::core::utils::{resolved_command, truncate};
use anyhow::Result;

const MAX_UNFILTERED_LINES: usize = 120;
const HEAD_LINES: usize = 60;
const TAIL_LINES: usize = 40;
const MAX_LINE_CHARS: usize = 200;

pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let timer = tracking::TimedExecution::start();

if verbose > 0 {
eprintln!("Running: sed {}", args.join(" "));
}

let mut cmd = resolved_command("sed");
for arg in args {
cmd.arg(arg);
}

let result = exec_capture(&mut cmd)?;
Comment on lines +20 to +25
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec_capture forces stdin to Stdio::null(), so rtk sed will not work for common usages like echo foo | rtk sed 's/o/a/g' or rtk sed '...' (reading from stdin). Consider running sed via core::stream::run_streaming with StdinMode::Inherit (and ideally a streaming head/tail filter) so stdin is preserved and you don’t have to buffer unbounded stdout into memory for large outputs.

Copilot uses AI. Check for mistakes.
let filtered_stdout = compact_sed_output(&result.stdout);
Comment on lines +25 to +26
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec_capture(&mut cmd)? returns a fairly generic error; please add anyhow::Context here (similar to other command modules) so failures are attributable to sed execution (e.g., missing binary, spawn failure).

Copilot uses AI. Check for mistakes.

print!("{}", filtered_stdout);
eprint!("{}", result.stderr);

let filtered_combined = format!("{}{}", filtered_stdout, result.stderr);
timer.track(
&format!("sed {}", args.join(" ")),
"rtk sed",
&result.combined(),
&filtered_combined,
);

Ok(result.exit_code)
}

fn compact_sed_output(output: &str) -> String {
if output.is_empty() {
return String::new();
}

let lines: Vec<&str> = output.lines().collect();
let has_trailing_newline = output.ends_with('\n');
let has_long_lines = lines
.iter()
.any(|line| line.chars().count() > MAX_LINE_CHARS);

if lines.len() <= MAX_UNFILTERED_LINES && !has_long_lines {
return output.to_string();
}

let mut compacted = Vec::new();
let head_count = HEAD_LINES.min(lines.len());
let tail_count = TAIL_LINES.min(lines.len().saturating_sub(head_count));
let omitted = lines.len().saturating_sub(head_count + tail_count);

for line in lines.iter().take(head_count) {
compacted.push(truncate(line, MAX_LINE_CHARS));
}

if omitted > 0 {
compacted.push(format!("... {} lines omitted ...", omitted));
}

if tail_count > 0 {
let tail_start = lines.len() - tail_count;
for line in &lines[tail_start..] {
compacted.push(truncate(line, MAX_LINE_CHARS));
}
}

let mut result = compacted.join("\n");
if has_trailing_newline {
result.push('\n');
}
result
}

#[cfg(test)]
mod tests {
use super::*;
use crate::core::utils::count_tokens;

#[test]
fn small_output_is_unchanged() {
let input = "one\ntwo\nthree\n";
assert_eq!(compact_sed_output(input), input);
}

#[test]
fn large_output_keeps_head_tail_and_marker() {
let input = numbered_lines(150);
let output = compact_sed_output(&input);

assert!(output.starts_with("line 1\nline 2\n"));
assert!(output.contains("... 50 lines omitted ..."));
assert!(output.contains("line 111\n"));
assert!(output.ends_with("line 150\n"));
assert!(!output.contains("line 80\n"));
}

#[test]
fn long_lines_are_truncated() {
let input = format!("{}\nshort\n", "x".repeat(260));
let output = compact_sed_output(&input);
let first_line = output.lines().next().unwrap();

assert_eq!(first_line.chars().count(), MAX_LINE_CHARS);
assert!(first_line.ends_with("..."));
assert!(output.ends_with("short\n"));
}

#[test]
fn empty_output_stays_empty() {
assert_eq!(compact_sed_output(""), "");
}

#[test]
fn large_output_reaches_token_savings_target() {
let input = numbered_lines(500);
let output = compact_sed_output(&input);
let savings =
100.0 - (count_tokens(&output) as f64 / count_tokens(&input) as f64 * 100.0);

assert!(
savings >= 60.0,
"Expected >=60% savings, got {:.1}%",
savings
);
}

fn numbered_lines(count: usize) -> String {
(1..=count)
.map(|n| format!("line {}", n))
.collect::<Vec<_>>()
.join("\n")
+ "\n"
}
}
24 changes: 23 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use cmds::ruby::{rake_cmd, rspec_cmd, rubocop_cmd};
use cmds::rust::{cargo_cmd, runner};
use cmds::system::{
deps, env_cmd, find_cmd, format_cmd, grep_cmd, json_cmd, local_llm, log_cmd, ls, pipe_cmd,
read, summary, tree, wc_cmd,
read, sed_cmd, summary, tree, wc_cmd,
};

use anyhow::{Context, Result};
Expand Down Expand Up @@ -377,6 +377,14 @@ enum Commands {
args: Vec<String>,
},

/// Stream editor with compact output for large substitutions/prints
#[command(disable_help_flag = true)]
Sed {
/// Arguments passed to sed
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},

/// Show token savings summary and history
Gain {
/// Filter statistics to current project (current working directory) // added
Expand Down Expand Up @@ -1793,6 +1801,8 @@ fn run_cli() -> Result<i32> {

Commands::Wc { args } => wc_cmd::run(&args, cli.verbose)?,

Commands::Sed { args } => sed_cmd::run(&args, cli.verbose)?,

Commands::Gain {
project, // added
graph,
Expand Down Expand Up @@ -2365,6 +2375,7 @@ fn is_operational_command(cmd: &Commands) -> bool {
| Commands::Summary { .. }
| Commands::Grep { .. }
| Commands::Wget { .. }
| Commands::Sed { .. }
| Commands::Vitest { .. }
| Commands::Prisma { .. }
| Commands::Tsc { .. }
Expand Down Expand Up @@ -2553,6 +2564,17 @@ mod tests {
}
}

#[test]
fn test_sed_args_pass_through() {
let cli = Cli::try_parse_from(["rtk", "sed", "-n", "1,20p", "--help"]).unwrap();
match cli.command {
Commands::Sed { args } => {
assert_eq!(args, vec!["-n", "1,20p", "--help"]);
}
_ => panic!("Expected Sed command"),
}
}

#[test]
fn test_try_parse_git_with_dash_c_succeeds() {
let result = Cli::try_parse_from(["rtk", "git", "-C", "/path", "status"]);
Expand Down
Loading