use crate::entries::Entry;
use anyhow::{Context, Result};
use clap::Args;
use clap::builder::NonEmptyStringValueParser;
use regex::Regex;
#[derive(Debug, Args)]
#[command(next_help_heading = Some("Fetch"))]
pub struct FilterArgs {
#[arg(short = 'F', long, conflicts_with = "only_dirs")]
only_files: bool,
#[arg(short = 'D', long, conflicts_with = "only_files")]
only_dirs: bool,
#[arg(short = 'i', long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
inc: Vec<String>,
#[arg(short = 'x', long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
exc: Vec<String>,
#[arg(short = 'I', long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
dir: Vec<String>,
#[arg(short = 'X', long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
dir_ex: Vec<String>,
#[arg(long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
path: Vec<String>,
#[arg(long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
path_ex: Vec<String>,
#[arg(long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
file: Vec<String>,
#[arg(long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
file_ex: Vec<String>,
#[arg(long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
ext: Vec<String>,
#[arg(long, value_name = "REGEX", allow_hyphen_values = true, value_parser = NonEmptyStringValueParser::new(), value_delimiter = ',')]
ext_ex: Vec<String>,
}
#[derive(Debug)]
enum Constraint {
Include(Regex),
Exclude(Regex),
Both(Regex, Regex),
}
#[derive(Debug, Default)]
pub struct Filter {
only_files: bool,
only_dirs: bool,
all: Option<Constraint>,
dir: Option<Constraint>,
path: Option<Constraint>,
file: Option<Constraint>,
ext: Option<Constraint>,
}
impl Filter {
pub fn is_in(&self, entry: &Entry) -> bool {
self.is_included(entry).unwrap_or_default()
}
fn is_included(&self, entry: &Entry) -> Option<bool> {
let (stem, ext) = entry.filename_parts();
(!stem.starts_with('.')).then_some(())?;
let ret = if entry.is_dir() {
self.dir.as_ref().is_none_or(|r| r.is_match(entry.file_name()))
&& self.path.as_ref().is_none_or(|r| r.is_match(entry.to_str()))
&& !self.only_files
&& match &self.all {
None => true,
Some(r) => {
let parent = entry.parent()?;
r.is_match(&format!("{}{stem}", parent.to_str()))
}
}
} else {
self.file.as_ref().is_none_or(|r| r.is_match(stem))
&& self.ext.as_ref().is_none_or(|r| r.is_match(ext))
&& !self.only_dirs
&& (self.all.is_none() && self.dir.is_none() && self.path.is_none()
|| {
let parent = entry.parent()?;
self.all.as_ref().is_none_or(|r| r.is_match(&format!("{}{stem}", parent.to_str())))
&& self.dir.as_ref().is_none_or(|r| r.is_match(parent.file_name()))
&& self.path.as_ref().is_none_or(|r| r.is_match(parent.to_str()))
})
};
Some(ret)
}
}
impl Constraint {
fn is_match(&self, s: &str) -> bool {
match self {
Constraint::Include(re) => re.is_match(s),
Constraint::Exclude(re) => !re.is_match(s),
Constraint::Both(re_in, re_ex) => re_in.is_match(s) && !re_ex.is_match(s),
}
}
}
impl TryFrom<FilterArgs> for Filter {
type Error = anyhow::Error;
fn try_from(s: FilterArgs) -> Result<Self> {
Ok(Filter {
only_files: s.only_files,
only_dirs: s.only_dirs,
all: build((s.inc, "inc"), (s.exc, "all-ex"))?,
dir: build((s.dir, "dir"), (s.dir_ex, "dir-ex"))?,
path: build((s.path, "path"), (s.path_ex, "path-ex"))?,
file: build((s.file, "file"), (s.file_ex, "file-ex"))?,
ext: build((s.ext, "ext"), (s.ext_ex, "ext-ex"))?,
})
}
}
type Rule<'a> = (Vec<String>, &'a str);
fn build((_in, p_in): Rule, (_ex, p_ex): Rule) -> Result<Option<Constraint>> {
Ok(match (compile(_in, p_in)?, compile(_ex, p_ex)?) {
(Some(re_in), None) => Some(Constraint::Include(re_in)),
(None, Some(re_ex)) => Some(Constraint::Exclude(re_ex)),
(Some(re_in), Some(re_ex)) => Some(Constraint::Both(re_in, re_ex)),
(None, None) => None,
})
}
fn compile(value: Vec<String>, param: &str) -> Result<Option<Regex>> {
(!value.is_empty())
.then(|| {
let r = value
.iter()
.map(|v| format!("(?:{v})")) .collect::<Vec<_>>()
.join("|");
Regex::new(&format!("(?i){r}")).with_context(|| format!("invalid --{param}"))
})
.transpose()
}