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
18 changes: 13 additions & 5 deletions src/core/toml_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ struct TomlFilterDef {
tail_lines: Option<usize>,
max_lines: Option<usize>,
on_empty: Option<String>,
/// When true, stderr is captured and merged with stdout before filtering.
/// Use for tools like liquibase that emit banners/logs to stderr.
#[serde(default)]
filter_stderr: bool,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -145,6 +149,8 @@ pub struct CompiledFilter {
tail_lines: Option<usize>,
pub max_lines: Option<usize>,
on_empty: Option<String>,
/// When true, the runner should capture stderr and merge it with stdout.
pub filter_stderr: bool,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -391,6 +397,7 @@ fn compile_filter(name: String, def: TomlFilterDef) -> Result<CompiledFilter, St
tail_lines: def.tail_lines,
max_lines: def.max_lines,
on_empty: def.on_empty,
filter_stderr: def.filter_stderr,
})
}

Expand Down Expand Up @@ -1570,6 +1577,7 @@ match_command = "^make\\b"
"hadolint",
"helm",
"iptables",
"liquibase",
"make",
"markdownlint",
"mix-compile",
Expand Down Expand Up @@ -1613,8 +1621,8 @@ match_command = "^make\\b"
let filters = make_filters(BUILTIN_TOML);
assert_eq!(
filters.len(),
58,
"Expected exactly 58 built-in filters, got {}. \
59,
"Expected exactly 59 built-in filters, got {}. \
Update this count when adding/removing filters in src/filters/.",
filters.len()
);
Expand Down Expand Up @@ -1671,11 +1679,11 @@ expected = "output line 1\noutput line 2"
let combined = format!("{}\n\n{}", BUILTIN_TOML, new_filter);
let filters = make_filters(&combined);

// All 58 existing filters still present + 1 new = 59
// All 59 existing filters still present + 1 new = 60
assert_eq!(
filters.len(),
59,
"Expected 59 filters after concat (58 built-in + 1 new)"
60,
"Expected 60 filters after concat (59 built-in + 1 new)"
);

// New filter is discoverable
Expand Down
9 changes: 9 additions & 0 deletions src/discover/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,15 @@ pub const RULES: &[RtkRule] = &[
subcmd_savings: &[],
subcmd_status: &[],
},
RtkRule {
pattern: r"^liquibase(?:\s|$)",
rtk_cmd: "rtk liquibase",
rewrite_prefixes: &["liquibase"],
category: "Infra",
savings_pct: 65.0,
subcmd_savings: &[],
subcmd_status: &[],
},
];

pub const IGNORED_PREFIXES: &[&str] = &[
Expand Down
1 change: 1 addition & 0 deletions src/filters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ expected = "expected filtered output"
| `description` | string | Human-readable description |
| `match_command` | regex | Matches the command string (e.g. `"^docker\\s+inspect"`) |
| `strip_ansi` | bool | Strip ANSI escape codes before processing |
| `filter_stderr` | bool | Capture and merge stderr into stdout before filtering (use for tools like liquibase that emit banners to stderr) |
| `strip_lines_matching` | regex[] | Drop lines matching any regex |
| `keep_lines_matching` | regex[] | Keep only lines matching at least one regex |
| `replace` | array | Regex substitutions (`{ pattern, replacement }`) |
Expand Down
84 changes: 84 additions & 0 deletions src/filters/liquibase.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
[filters.liquibase]
description = "Compact liquibase output — strip headers and generic info"
match_command = "(?:^|/)liquibase(?:\\s|$)"
strip_ansi = true
filter_stderr = true
strip_lines_matching = [
"^\\s*$",
"^Starting Liquibase at",
"^Liquibase (?:Community|Open Source)",
"^Liquibase Home:",
"^Java Home",
"^Libraries:",
"^\\s*-\\s+\\S+\\.jar",
"^INFO \\[liquibase\\.integration\\]",
"^INFO \\[liquibase\\.core\\] Reading resource",
"^INFO \\[liquibase\\.core\\] Parsing",
"^(?:\\[?INFO\\]?\\s*)?#+$",
"^\\s*##"
]
on_empty = "liquibase: ok"
max_lines = 200

[[tests.liquibase]]
name = "strip ascii banner and info logs from subcommand"
input = '''
####################################################
## _ _ _ _ ##
## | | (_) (_) | ##
####################################################
Starting Liquibase at 10:12:11 (version 4.29.1)
Liquibase Version: 4.29.1
Liquibase Open Source 4.29.1 by Liquibase
INFO [liquibase.integration] Starting command
INFO [liquibase.core] Reading resource db/changelog.xml
INFO [liquibase.core] Parsing db/changelog.xml
Running Changeset: filepath::id::author
Changeset filepath::id::author ran successfully
'''
expected = '''
Liquibase Version: 4.29.1
Running Changeset: filepath::id::author
Changeset filepath::id::author ran successfully'''

[[tests.liquibase]]
name = "strip --version noise, keep only version line"
input = '''
####################################################
## _ _ _ _ ##
####################################################
Starting Liquibase at 13:45:24 using Java 17.0.15 (version 4.30.0 #4943 built at 2024-10-31 17:00+0000)
Liquibase Home: D:\mcp\bash\lbr\third-party
Java Home C:\Program Files\Java\jdk-17.0.15 (Version 17.0.15)
Libraries:
- internal\lib\commons-io.jar: Apache Commons IO 2.17.0 By The Apache Software Foundation
- internal\lib\picocli.jar: picocli 4.7.6 By Remko Popma
- lib\ojdbc10-19.30.0.0.jar: JDBC 19.30.0.0.0 By Oracle Corporation

Liquibase Version: 4.30.0
Liquibase Open Source 4.30.0 by Liquibase
'''
expected = '''
Liquibase Version: 4.30.0'''

[[tests.liquibase]]
name = "keep status and error lines"
input = '''
####################################################
## _ _ _ _ ##
####################################################
Starting Liquibase at 10:00:00 (version 4.30.0)
Liquibase Version: 4.30.0
Liquibase Open Source 4.30.0 by Liquibase
HR@jdbc:oracle:thin:@localhost:1523:XE is up to date
Liquibase command 'status' was executed successfully.
'''
expected = '''
Liquibase Version: 4.30.0
HR@jdbc:oracle:thin:@localhost:1523:XE is up to date
Liquibase command 'status' was executed successfully.'''

[[tests.liquibase]]
name = "empty input"
input = ""
expected = "liquibase: ok"
36 changes: 27 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,26 +1091,44 @@ fn run_fallback(parse_error: clap::Error) -> Result<i32> {

if let Some(filter) = toml_match {
// TOML match: capture stdout for filtering
let result = core::utils::resolved_command(&args[0])
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped()) // capture
.stderr(std::process::Stdio::inherit()) // stderr always direct
.output();
let result = if filter.filter_stderr {
// Merge stderr into stdout so the filter can strip banners emitted by tools like liquibase
core::utils::resolved_command(&args[0])
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped()) // captured for merging
.output()
} else {
core::utils::resolved_command(&args[0])
.args(&args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped()) // capture
.stderr(std::process::Stdio::inherit()) // stderr always direct
.output()
};

match result {
Ok(output) => {
let exit_code = core::utils::exit_code_from_output(&output, &raw_command);
let stdout_raw = String::from_utf8_lossy(&output.stdout);
let stderr_raw = String::from_utf8_lossy(&output.stderr);

// Merge stderr into the text to filter when filter_stderr is enabled;
// otherwise emit stderr directly so it is always visible.
let combined_raw = if filter.filter_stderr {
format!("{}{}", stdout_raw, stderr_raw)
} else {
stdout_raw.to_string()
};
// Tee raw output BEFORE filtering on failure — lets LLM re-read if needed
let tee_hint = if !output.status.success() {
core::tee::tee_and_hint(&stdout_raw, &raw_command, exit_code)
core::tee::tee_and_hint(&combined_raw, &raw_command, exit_code)
} else {
None
};

let filtered = core::toml_filter::apply_filter(filter, &stdout_raw);
let filtered = core::toml_filter::apply_filter(filter, &combined_raw);
println!("{}", filtered);
if let Some(hint) = tee_hint {
println!("{}", hint);
Expand All @@ -1119,7 +1137,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<i32> {
timer.track(
&raw_command,
&format!("rtk:toml {}", raw_command),
&stdout_raw,
&combined_raw,
&filtered,
);
core::tracking::record_parse_failure_silent(&raw_command, &error_message, true);
Expand Down