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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ termbg = "0.6.2"
tokio = { version = "1.50", optional = true, features = ["rt"] }
toml = "1.0"
unicode-width = "0.2"
regex = "1.12"

[build-dependencies]
anyhow = "1.0"
Expand All @@ -61,7 +62,6 @@ clap_complete = "4.4"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
pager = "0.16.1"
procfs = "0.18.0"
regex = "1.12"
uzers = "0.12"
which = "8"

Expand Down
15 changes: 13 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::column::Column;
use crate::columns::*;
use crate::config::*;
use crate::opt::*;
use crate::util::{adjust, get_theme, lap};
use crate::util::{adjust, get_theme, has_regex_syntax, lap};
use crate::view::View;
use crate::watcher::Watcher;
use anyhow::{Context, Error};
Expand Down Expand Up @@ -115,6 +115,7 @@ fn main() {
fn run() -> Result<(), Error> {
let mut opt: Opt = Parser::parse();
opt.watch_mode = opt.watch || opt.watch_interval.is_some();
validate_search_args(&opt)?;

if opt.gen_config {
run_gen_config()
Expand Down Expand Up @@ -183,7 +184,7 @@ fn run_default(opt: &mut Opt, config: &Config) -> Result<(), Error> {
lap(&mut time, "Info: View::new");
}

view.filter(opt, config, 1);
view.filter(opt, config, 1)?;

if opt.debug {
lap(&mut time, "Info: view.filter");
Expand All @@ -204,6 +205,16 @@ fn run_default(opt: &mut Opt, config: &Config) -> Result<(), Error> {
Ok(())
}

fn validate_search_args(opt: &Opt) -> Result<(), Error> {
if opt.regex && opt.keyword.len() > 1 {
anyhow::bail!("--regex accepts a single PATTERN argument");
}
if opt.smart && opt.keyword.len() > 1 && opt.keyword.iter().any(|k| has_regex_syntax(k)) {
anyhow::bail!("--smart supports a single PATTERN when regex syntax is detected");
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
12 changes: 12 additions & 0 deletions src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ pub struct Opt {
#[clap(action, name = "KEYWORD")]
pub keyword: Vec<String>,

/// Use plain text search mode (default)
#[clap(long = "text", conflicts_with_all(&["regex", "smart"]))]
pub text: bool,

/// Use regex search mode (single PATTERN)
#[clap(long = "regex", conflicts_with_all(&["text", "smart"]))]
pub regex: bool,

/// Use smart mode (auto-detect regex syntax)
#[clap(long = "smart", conflicts_with_all(&["text", "regex"]))]
pub smart: bool,

/// AND logic for multi-keyword
#[clap(
short = 'a',
Expand Down
21 changes: 21 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@ pub fn classify(keyword: &str) -> KeywordClass {
}
}

pub fn has_regex_syntax(pattern: &str) -> bool {
let mut escaped = false;
for c in pattern.chars() {
if escaped {
escaped = false;
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if matches!(
c,
'|' | '(' | ')' | '[' | ']' | '{' | '}' | '*' | '+' | '?' | '^' | '$'
) {
return true;
}
}
false
}

pub fn adjust(x: &str, len: usize, align: &ConfigColumnAlign) -> String {
if len < UnicodeWidthStr::width(x) {
String::from(truncate(x, len))
Expand Down
41 changes: 39 additions & 2 deletions src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ use crate::process::collect_proc;
use crate::style::{apply_color, apply_style, color_to_column_style};
use crate::term_info::TermInfo;
use crate::util::{
KeywordClass, ansi_trim_end, classify, find_column_kind, find_exact, find_partial, truncate,
KeywordClass, ansi_trim_end, classify, find_column_kind, find_exact, find_partial,
has_regex_syntax, truncate,
};
use anyhow::{Error, bail};
#[cfg(not(target_os = "windows"))]
use pager::Pager;
use regex::RegexBuilder;
use std::collections::HashMap;
use std::time::Duration;

Expand Down Expand Up @@ -219,15 +221,20 @@ impl View {
})
}

pub fn filter(&mut self, opt: &Opt, config: &Config, header_lines: usize) {
pub fn filter(&mut self, opt: &Opt, config: &Config, header_lines: usize) -> Result<(), Error> {
let mut cols_nonnumeric = Vec::new();
let mut cols_numeric = Vec::new();
let mut cols_searchable = Vec::new();
for c in &self.columns {
if c.nonnumeric_search {
cols_nonnumeric.push(c.column.as_ref());
cols_searchable.push(c.column.as_ref());
}
if c.numeric_search {
cols_numeric.push(c.column.as_ref());
if !c.nonnumeric_search {
cols_searchable.push(c.column.as_ref());
}
}
}

Expand All @@ -241,6 +248,29 @@ impl View {
}
}

let regex_mode = if opt.regex {
true
} else if opt.smart {
opt.keyword.len() == 1 && has_regex_syntax(&opt.keyword[0])
} else {
false
};

let regex = if regex_mode && !opt.keyword.is_empty() {
let pattern = &opt.keyword[0];
let ignore_case = match config.search.case {
ConfigSearchCase::Smart => pattern == &pattern.to_ascii_lowercase(),
ConfigSearchCase::Insensitive => true,
ConfigSearchCase::Sensitive => false,
};
let regex = RegexBuilder::new(pattern)
.case_insensitive(ignore_case)
.build()?;
Some(regex)
} else {
None
};

let pids = self.columns[self.sort_info.idx]
.column
.sorted_pid(&self.sort_info.order);
Expand Down Expand Up @@ -285,6 +315,8 @@ impl View {
false
} else if opt.keyword.is_empty() {
true
} else if let Some(regex) = &regex {
View::search_regex(*pid, cols_searchable.as_slice(), regex)
} else {
View::search(
*pid,
Expand Down Expand Up @@ -338,6 +370,7 @@ impl View {

self.visible_pids = visible_pids;
self.auxiliary_pids = auxiliary_pids;
Ok(())
}

fn get_parent_pids(&self, pid: i32, parent_pids: &mut Vec<i32>) {
Expand Down Expand Up @@ -681,6 +714,10 @@ impl View {
}
}

fn search_regex(pid: i32, cols: &[&dyn Column], regex: &regex::Regex) -> bool {
cols.iter().any(|c| regex.is_match(&c.display_json(pid)))
}

#[cfg(not(any(target_os = "windows", any(target_os = "linux", target_os = "android"))))]
fn pager(config: &Config) {
if let Some(ref pager) = config.pager.command {
Expand Down
Loading