diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 6937f02ab..3fb20c3e7 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -287,6 +287,7 @@ fn search_node_for_text_with_argument<'a>( "dupe" => parse_dupes(val)?, "prop" => parse_prop(val)?, "re" => SearchNode::Regex(unescape_quotes(val)), + "r" => SearchNode::UnqualifiedText(unescape_raw(val)), "nc" => SearchNode::NoCombining(unescape(val)?), "w" => SearchNode::WordBoundary(unescape(val)?), // anything else is a field search @@ -419,6 +420,12 @@ fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult Cow { } } +/// Unescape quotes but escape wildcards and \s. +fn unescape_raw(s: &str) -> Cow { + lazy_static! { + static ref RE: Regex = Regex::new(r#"\\"?|\*|_"#).unwrap(); + } + RE.replace_all(&s, |caps: &Captures| match &caps[0] { + r"\" => r"\\", + "\\\"" => "\"", + r"*" => r"\*", + r"_" => r"\_", + _ => unreachable!(), + }) +} + +/// Unescape chars with special meaning to the parser. fn unescape(txt: &str) -> ParseResult> { if is_invalid_escape(txt) { Err(ParseError {}) @@ -611,12 +633,21 @@ mod test { vec![Search(Regex(r"\btest\%".into()))] ); + // treat all chars as literals in raw searches + assert_eq!(parse(r"r:\*_"), parse(r"\\\*\_")); + assert_eq!(parse(r"field:r:\*_"), parse(r"field:\\\*\_")); + // no exceptions for escaping " assert_eq!( parse(r#"re:te\"st"#)?, vec![Search(Regex(r#"te"st"#.into()))] ); assert!(parse(r#"re:te"st"#).is_err()); + assert_eq!( + parse(r#"r:te\"st"#)?, + vec![Search(UnqualifiedText(r#"te"st"#.into()))] + ); + assert!(parse(r#"r:te"st"#).is_err()); // spaces are optional if node separation is clear assert_eq!(parse(r#"a"b"(c)"#)?, parse("a b (c)")?);