Skip to content

Commit a63dac0

Browse files
committed
[Lib] Custom escape function to ignore newline and carriage returns
1 parent a573a5f commit a63dac0

File tree

3 files changed

+82
-7
lines changed

3 files changed

+82
-7
lines changed

src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,40 @@ fn namedarg(name: &str) {
602602
assert!(ptr::eq(result.unwrap(), &src_refs[0]));
603603
}
604604

605+
const MULTILINE_SOURCE: &str = r#"
606+
#[macro_use]
607+
extern crate log;
608+
609+
fn main() {
610+
env_logger::init();
611+
debug!("you're only as funky\n as your last cut");
612+
}
613+
"#;
614+
#[test]
615+
fn test_link_multiline() {
616+
let lf = LogFormat::new(
617+
r#"^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z \w+ \w+\]\s+(?<body>.*)"#.to_string(),
618+
);
619+
let log_ref = LogRef::with_format(
620+
"[2024-05-09T19:58:53Z DEBUG main] you're only as funky\n as your last cut",
621+
lf,
622+
);
623+
let code = CodeSource::new(
624+
&PathBuf::from("in-mem.rs"),
625+
Box::new(MULTILINE_SOURCE.as_bytes()),
626+
)
627+
.unwrap();
628+
let src_refs = extract_logging(&[code], &ProgressTracker::new())
629+
.pop()
630+
.unwrap()
631+
.log_statements;
632+
assert_eq!(src_refs.len(), 1);
633+
println!("`{}`", log_ref.body());
634+
println!("`{}`", src_refs[0].matcher);
635+
let result = link_to_source(&log_ref, &src_refs);
636+
assert!(ptr::eq(result.unwrap(), &src_refs[0]));
637+
}
638+
605639
#[test]
606640
fn test_link_to_source_no_matches() {
607641
let log_ref = LogRef::new("nope!");

src/log_format.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use regex::{Captures, Regex};
1+
use regex::{Captures, Regex, RegexBuilder};
22

33
use crate::LogRef;
44

@@ -11,7 +11,13 @@ impl LogFormat {
1111
pub fn new(format: String) -> LogFormat {
1212
LogFormat {
1313
// TODO handle more gracefully if wrong format
14-
regex: Regex::new(&format).unwrap(),
14+
regex: RegexBuilder::new(&format)
15+
// XXX: This is kinda a hack to support multiline matching in lnav, but
16+
// not really useful for log2src atm because its still filtering line-by-line,
17+
// so this case would never come up
18+
.dot_matches_new_line(true)
19+
.build()
20+
.unwrap(),
1521
}
1622
}
1723

src/source_ref.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ fn build_matcher(
116116
let mut exact_len = 0;
117117
for cap in placeholder_regex_for(language).captures_iter(text) {
118118
let placeholder = cap.get(0).unwrap();
119-
let text = regex::escape(&text[last_end..placeholder.start()]);
119+
let text = escape_ignore_newlines(&text[last_end..placeholder.start()]);
120120
exact_len += text.len();
121121
pattern.push_str(text.as_str());
122122
last_end = placeholder.end();
@@ -128,7 +128,7 @@ fn build_matcher(
128128
(None, None) => FormatArgument::Placeholder,
129129
});
130130
}
131-
let text = regex::escape(&text[last_end..]);
131+
let text = escape_ignore_newlines(&text[last_end..]);
132132
exact_len += text.len();
133133
if exact_len == 0 {
134134
None
@@ -139,6 +139,24 @@ fn build_matcher(
139139
}
140140
}
141141

142+
/// Escape special chars except newlines and carriage returns in order to support multiline strings
143+
fn escape_ignore_newlines(segment: &str) -> String {
144+
let mut result = String::with_capacity(segment.len() * 2);
145+
for c in segment.chars() {
146+
match c {
147+
'\n' => result.push_str(r"\n"), // Use actual newline in regex
148+
'\r' => result.push_str(r"\r"), // Handle carriage returns too
149+
// Escape regex special chars
150+
'.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' => {
151+
result.push('\\');
152+
result.push(c);
153+
}
154+
_ => result.push(c),
155+
}
156+
}
157+
result
158+
}
159+
142160
#[cfg(test)]
143161
mod tests {
144162
use super::*;
@@ -176,7 +194,10 @@ mod tests {
176194
#[test]
177195
fn test_build_matcher_positional() {
178196
let (matcher, _pat, args) = build_matcher("second={2}", SourceLanguage::Rust).unwrap();
179-
assert_eq!(Regex::new(r#"^second=(.+)$"#).unwrap().as_str(), matcher.as_str());
197+
assert_eq!(
198+
Regex::new(r#"^second=(.+)$"#).unwrap().as_str(),
199+
matcher.as_str()
200+
);
180201
assert_eq!(args[0], FormatArgument::Positional(2));
181202
}
182203

@@ -193,8 +214,22 @@ mod tests {
193214

194215
#[test]
195216
fn test_build_matcher_none() {
196-
let build_res =
197-
build_matcher("%s", SourceLanguage::Cpp);
217+
let build_res = build_matcher("%s", SourceLanguage::Cpp);
198218
assert!(build_res.is_none());
199219
}
220+
221+
#[test]
222+
fn test_build_matcher_multiline() {
223+
let (matcher, _pat, _args) = build_matcher(
224+
"you're only as funky\n as your last cut",
225+
SourceLanguage::Rust,
226+
)
227+
.unwrap();
228+
assert_eq!(
229+
Regex::new(r#"^you're only as funky\n as your last cut$"#)
230+
.unwrap()
231+
.as_str(),
232+
matcher.as_str()
233+
);
234+
}
200235
}

0 commit comments

Comments
 (0)