Skip to content

Commit b1ad1bd

Browse files
committed
Add liquibase TOML filter
1 parent 45d8dca commit b1ad1bd

5 files changed

Lines changed: 134 additions & 14 deletions

File tree

src/core/toml_filter.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ struct TomlFilterDef {
102102
tail_lines: Option<usize>,
103103
max_lines: Option<usize>,
104104
on_empty: Option<String>,
105+
/// When true, stderr is captured and merged with stdout before filtering.
106+
/// Use for tools like liquibase that emit banners/logs to stderr.
107+
#[serde(default)]
108+
filter_stderr: bool,
105109
}
106110

107111
// ---------------------------------------------------------------------------
@@ -145,6 +149,8 @@ pub struct CompiledFilter {
145149
tail_lines: Option<usize>,
146150
pub max_lines: Option<usize>,
147151
on_empty: Option<String>,
152+
/// When true, the runner should capture stderr and merge it with stdout.
153+
pub filter_stderr: bool,
148154
}
149155

150156
// ---------------------------------------------------------------------------
@@ -391,6 +397,7 @@ fn compile_filter(name: String, def: TomlFilterDef) -> Result<CompiledFilter, St
391397
tail_lines: def.tail_lines,
392398
max_lines: def.max_lines,
393399
on_empty: def.on_empty,
400+
filter_stderr: def.filter_stderr,
394401
})
395402
}
396403

@@ -1570,6 +1577,7 @@ match_command = "^make\\b"
15701577
"hadolint",
15711578
"helm",
15721579
"iptables",
1580+
"liquibase",
15731581
"make",
15741582
"markdownlint",
15751583
"mix-compile",
@@ -1613,8 +1621,8 @@ match_command = "^make\\b"
16131621
let filters = make_filters(BUILTIN_TOML);
16141622
assert_eq!(
16151623
filters.len(),
1616-
58,
1617-
"Expected exactly 58 built-in filters, got {}. \
1624+
59,
1625+
"Expected exactly 59 built-in filters, got {}. \
16181626
Update this count when adding/removing filters in src/filters/.",
16191627
filters.len()
16201628
);
@@ -1671,11 +1679,11 @@ expected = "output line 1\noutput line 2"
16711679
let combined = format!("{}\n\n{}", BUILTIN_TOML, new_filter);
16721680
let filters = make_filters(&combined);
16731681

1674-
// All 58 existing filters still present + 1 new = 59
1682+
// All 59 existing filters still present + 1 new = 60
16751683
assert_eq!(
16761684
filters.len(),
1677-
59,
1678-
"Expected 59 filters after concat (58 built-in + 1 new)"
1685+
60,
1686+
"Expected 60 filters after concat (59 built-in + 1 new)"
16791687
);
16801688

16811689
// New filter is discoverable

src/discover/rules.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,15 @@ pub const RULES: &[RtkRule] = &[
641641
subcmd_savings: &[],
642642
subcmd_status: &[],
643643
},
644+
RtkRule {
645+
pattern: r"^liquibase(?:\s|$)",
646+
rtk_cmd: "rtk liquibase",
647+
rewrite_prefixes: &["liquibase"],
648+
category: "Infra",
649+
savings_pct: 65.0,
650+
subcmd_savings: &[],
651+
subcmd_status: &[],
652+
},
644653
];
645654

646655
pub const IGNORED_PREFIXES: &[&str] = &[

src/filters/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ expected = "expected filtered output"
5151
| `description` | string | Human-readable description |
5252
| `match_command` | regex | Matches the command string (e.g. `"^docker\\s+inspect"`) |
5353
| `strip_ansi` | bool | Strip ANSI escape codes before processing |
54+
| `filter_stderr` | bool | Capture and merge stderr into stdout before filtering (use for tools like liquibase that emit banners to stderr) |
5455
| `strip_lines_matching` | regex[] | Drop lines matching any regex |
5556
| `keep_lines_matching` | regex[] | Keep only lines matching at least one regex |
5657
| `replace` | array | Regex substitutions (`{ pattern, replacement }`) |

src/filters/liquibase.toml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
[filters.liquibase]
2+
description = "Compact liquibase output — strip headers and generic info"
3+
match_command = "(?:^|/)liquibase(?:\\s|$)"
4+
strip_ansi = true
5+
filter_stderr = true
6+
strip_lines_matching = [
7+
"^\\s*$",
8+
"^Starting Liquibase at",
9+
"^Liquibase (?:Community|Open Source)",
10+
"^Liquibase Home:",
11+
"^Java Home",
12+
"^Libraries:",
13+
"^\\s*-\\s+\\S+\\.jar",
14+
"^INFO \\[liquibase\\.integration\\]",
15+
"^INFO \\[liquibase\\.core\\] Reading resource",
16+
"^INFO \\[liquibase\\.core\\] Parsing",
17+
"^(?:\\[?INFO\\]?\\s*)?#+$",
18+
"^\\s*##"
19+
]
20+
on_empty = "liquibase: ok"
21+
max_lines = 200
22+
23+
[[tests.liquibase]]
24+
name = "strip ascii banner and info logs from subcommand"
25+
input = '''
26+
####################################################
27+
## _ _ _ _ ##
28+
## | | (_) (_) | ##
29+
####################################################
30+
Starting Liquibase at 10:12:11 (version 4.29.1)
31+
Liquibase Version: 4.29.1
32+
Liquibase Open Source 4.29.1 by Liquibase
33+
INFO [liquibase.integration] Starting command
34+
INFO [liquibase.core] Reading resource db/changelog.xml
35+
INFO [liquibase.core] Parsing db/changelog.xml
36+
Running Changeset: filepath::id::author
37+
Changeset filepath::id::author ran successfully
38+
'''
39+
expected = '''
40+
Liquibase Version: 4.29.1
41+
Running Changeset: filepath::id::author
42+
Changeset filepath::id::author ran successfully'''
43+
44+
[[tests.liquibase]]
45+
name = "strip --version noise, keep only version line"
46+
input = '''
47+
####################################################
48+
## _ _ _ _ ##
49+
####################################################
50+
Starting Liquibase at 13:45:24 using Java 17.0.15 (version 4.30.0 #4943 built at 2024-10-31 17:00+0000)
51+
Liquibase Home: D:\mcp\bash\lbr\third-party
52+
Java Home C:\Program Files\Java\jdk-17.0.15 (Version 17.0.15)
53+
Libraries:
54+
- internal\lib\commons-io.jar: Apache Commons IO 2.17.0 By The Apache Software Foundation
55+
- internal\lib\picocli.jar: picocli 4.7.6 By Remko Popma
56+
- lib\ojdbc10-19.30.0.0.jar: JDBC 19.30.0.0.0 By Oracle Corporation
57+
58+
Liquibase Version: 4.30.0
59+
Liquibase Open Source 4.30.0 by Liquibase
60+
'''
61+
expected = '''
62+
Liquibase Version: 4.30.0'''
63+
64+
[[tests.liquibase]]
65+
name = "keep status and error lines"
66+
input = '''
67+
####################################################
68+
## _ _ _ _ ##
69+
####################################################
70+
Starting Liquibase at 10:00:00 (version 4.30.0)
71+
Liquibase Version: 4.30.0
72+
Liquibase Open Source 4.30.0 by Liquibase
73+
HR@jdbc:oracle:thin:@localhost:1523:XE is up to date
74+
Liquibase command 'status' was executed successfully.
75+
'''
76+
expected = '''
77+
Liquibase Version: 4.30.0
78+
HR@jdbc:oracle:thin:@localhost:1523:XE is up to date
79+
Liquibase command 'status' was executed successfully.'''
80+
81+
[[tests.liquibase]]
82+
name = "empty input"
83+
input = ""
84+
expected = "liquibase: ok"

src/main.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,26 +1090,44 @@ fn run_fallback(parse_error: clap::Error) -> Result<i32> {
10901090

10911091
if let Some(filter) = toml_match {
10921092
// TOML match: capture stdout for filtering
1093-
let result = core::utils::resolved_command(&args[0])
1094-
.args(&args[1..])
1095-
.stdin(std::process::Stdio::inherit())
1096-
.stdout(std::process::Stdio::piped()) // capture
1097-
.stderr(std::process::Stdio::inherit()) // stderr always direct
1098-
.output();
1093+
let result = if filter.filter_stderr {
1094+
// Merge stderr into stdout so the filter can strip banners emitted by tools like liquibase
1095+
core::utils::resolved_command(&args[0])
1096+
.args(&args[1..])
1097+
.stdin(std::process::Stdio::inherit())
1098+
.stdout(std::process::Stdio::piped())
1099+
.stderr(std::process::Stdio::piped()) // captured for merging
1100+
.output()
1101+
} else {
1102+
core::utils::resolved_command(&args[0])
1103+
.args(&args[1..])
1104+
.stdin(std::process::Stdio::inherit())
1105+
.stdout(std::process::Stdio::piped()) // capture
1106+
.stderr(std::process::Stdio::inherit()) // stderr always direct
1107+
.output()
1108+
};
10991109

11001110
match result {
11011111
Ok(output) => {
11021112
let exit_code = core::utils::exit_code_from_output(&output, &raw_command);
11031113
let stdout_raw = String::from_utf8_lossy(&output.stdout);
1114+
let stderr_raw = String::from_utf8_lossy(&output.stderr);
11041115

1116+
// Merge stderr into the text to filter when filter_stderr is enabled;
1117+
// otherwise emit stderr directly so it is always visible.
1118+
let combined_raw = if filter.filter_stderr {
1119+
format!("{}{}", stdout_raw, stderr_raw)
1120+
} else {
1121+
stdout_raw.to_string()
1122+
};
11051123
// Tee raw output BEFORE filtering on failure — lets LLM re-read if needed
11061124
let tee_hint = if !output.status.success() {
1107-
core::tee::tee_and_hint(&stdout_raw, &raw_command, exit_code)
1125+
core::tee::tee_and_hint(&combined_raw, &raw_command, exit_code)
11081126
} else {
11091127
None
11101128
};
11111129

1112-
let filtered = core::toml_filter::apply_filter(filter, &stdout_raw);
1130+
let filtered = core::toml_filter::apply_filter(filter, &combined_raw);
11131131
println!("{}", filtered);
11141132
if let Some(hint) = tee_hint {
11151133
println!("{}", hint);
@@ -1118,7 +1136,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<i32> {
11181136
timer.track(
11191137
&raw_command,
11201138
&format!("rtk:toml {}", raw_command),
1121-
&stdout_raw,
1139+
&combined_raw,
11221140
&filtered,
11231141
);
11241142
core::tracking::record_parse_failure_silent(&raw_command, &error_message, true);

0 commit comments

Comments
 (0)