From 6daf7ebd3f88170e553a3220383f50af91b9885d Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Tue, 19 May 2026 05:44:02 -0400 Subject: [PATCH] bug: fix issue with parsing quoted delimiters in process search (#2069) Noticed this when trying to parse something with `<` and `>` in the name, even when quoted. The fix is to just check for quotes when delimiting. --- CHANGELOG.md | 1 + src/widgets/process_table/query.rs | 70 ++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bae00dae..338759d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ That said, these are more guidelines rather than hard rules, though the project - [#2035](https://github.com/ClementTsang/bottom/pull/2035): Fix panic when deleting unicode words in search. - [#2042](https://github.com/ClementTsang/bottom/pull/2042): Address possible memory usage growth for AMD GPU stat gathering on Linux. +- [#2069](https://github.com/ClementTsang/bottom/pull/2069): Fix issue in process search with parsing quoted delimiters. ### Features diff --git a/src/widgets/process_table/query.rs b/src/widgets/process_table/query.rs index 9a239772..460ee770 100644 --- a/src/widgets/process_table/query.rs +++ b/src/widgets/process_table/query.rs @@ -120,16 +120,30 @@ pub(crate) fn parse_query(search_query: &str, options: &QueryOptions) -> QueryRe } let mut split_query = VecDeque::new(); + let mut in_quotes = false; search_query.split_whitespace().for_each(|s| { // From https://stackoverflow.com/a/56923739 get a split but include the parentheses let mut last = 0; for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) { - if last != index { - split_query.push_back(s[last..index].to_owned()); + if matched == "\"" { + // Always split on quote delimiters to open/close quoted sections. + if last != index { + split_query.push_back(s[last..index].to_owned()); + } + split_query.push_back(matched.to_owned()); + last = index + matched.len(); + in_quotes = !in_quotes; + } else if !in_quotes { + // Only split on other delimiters when outside a quoted section, + // so that special chars like `<` and `>` in `"Thread<15>"` are + // preserved as literals. + if last != index { + split_query.push_back(s[last..index].to_owned()); + } + split_query.push_back(matched.to_owned()); + last = index + matched.len(); } - split_query.push_back(matched.to_owned()); - last = index + matched.len(); } if last < s.len() { split_query.push_back(s[last..].to_owned()); @@ -1098,6 +1112,54 @@ mod tests { assert!(!query.check(&without, false)); } + /// Quoted angle brackets should be treated as literal characters in name matches. + #[test] + fn quoted_angle_brackets_match_literal() { + let query = parse_query_no_options("\"Thread<15>\"").unwrap(); + + let matching = simple_process("Thread<15>"); + let non_matching = simple_process("Thread"); + + assert!(query.check(&matching, false)); + assert!(!query.check(&non_matching, false)); + } + + /// A quoted `=` should be treated as a literal character in name matches. + #[test] + fn quoted_equals_matches_literal() { + let query = parse_query_no_options("\"a=b\"").unwrap(); + + let matching = simple_process("a=b"); + let non_matching = simple_process("ab"); + + assert!(query.check(&matching, false)); + assert!(!query.check(&non_matching, false)); + } + + /// A quoted `!=` should be treated as a literal character sequence in name matches. + #[test] + fn quoted_not_equals_matches_literal() { + let query = parse_query_no_options("\"a!=b\"").unwrap(); + + let matching = simple_process("a!=b"); + let non_matching = simple_process("ab"); + + assert!(query.check(&matching, false)); + assert!(!query.check(&non_matching, false)); + } + + /// Quoted parentheses should be treated as literal characters in name matches. + #[test] + fn quoted_parens_match_literal() { + let query = parse_query_no_options("\"a(b)\"").unwrap(); + + let matching = simple_process("a(b)"); + let non_matching = simple_process("ab"); + + assert!(query.check(&matching, false)); + assert!(!query.check(&non_matching, false)); + } + /// Trailing operators with no RHS must error. #[test] fn not_equal_missing_value_is_rejected() {