Add parser for filters

This commit is contained in:
Félix Saparelli 2021-09-14 20:09:57 +12:00
parent 6a55f5cc6d
commit 84dc77f787
4 changed files with 183 additions and 12 deletions

34
Cargo.lock generated
View File

@ -732,6 +732,19 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
[[package]]
name = "globset"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
"fnv",
"log",
"regex",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
@ -954,6 +967,12 @@ dependencies = [
"syn 1.0.73",
]
[[package]]
name = "minimal-lexical"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@ -1032,6 +1051,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "nom"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1"
dependencies = [
"memchr",
"minimal-lexical",
"version_check",
]
[[package]]
name = "notify"
version = "5.0.0-pre.12"
@ -1691,7 +1721,7 @@ checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
dependencies = [
"dirs 2.0.2",
"fnv",
"nom",
"nom 5.1.2",
"phf",
"phf_codegen",
]
@ -1982,7 +2012,9 @@ dependencies = [
"derive_builder",
"dunce",
"futures",
"globset",
"miette",
"nom 7.0.0",
"notify",
"once_cell",
"regex",

View File

@ -21,7 +21,9 @@ clearscreen = "1.0.6"
derive_builder = "0.10.2"
dunce = "1.0.2"
futures = "0.3.16"
globset = "0.4.8"
miette = "1.0.0-beta.1"
nom = "7.0.0"
notify = "5.0.0-pre.12"
once_cell = "1.8.0"
regex = "1.5.4"

View File

@ -157,6 +157,15 @@ pub enum RuntimeError {
#[error("clear screen: {0}")]
#[diagnostic(code(watchexec::runtime::clearscreen))]
Clearscreen(#[from] clearscreen::Error),
/// Error received when a filter cannot be parsed.
#[error("cannot parse filter `{src}`: {err:?}")]
#[diagnostic(code(watchexec::runtime::filter_parse))]
FilterParse {
src: String,
err: nom::error::ErrorKind,
// TODO: use miette's source snippet feature
},
}
/// Errors occurring from reconfigs.

View File

@ -1,33 +1,160 @@
use std::{collections::HashSet, path::PathBuf};
use std::{collections::HashSet, path::PathBuf, str::FromStr};
use globset::Glob;
use nom::{
branch::alt,
bytes::complete::{is_not, tag, tag_no_case, take_while1},
character::complete::char,
combinator::map_res,
sequence::{delimited, tuple},
Finish, IResult,
};
use regex::Regex;
use crate::event::Tag;
use crate::error::RuntimeError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter {
pub in_path: Option<PathBuf>,
pub on: Tag,
pub on: Matcher,
pub op: Op,
pub pat: Pattern,
}
impl FromStr for Filter {
type Err = RuntimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn matcher(i: &str) -> IResult<&str, Matcher> {
map_res(
alt((
tag_no_case("tag"),
tag_no_case("path"),
tag_no_case("kind"),
tag_no_case("source"),
tag_no_case("src"),
tag_no_case("process"),
tag_no_case("signal"),
tag_no_case("exit"),
)),
|m: &str| match m.to_ascii_lowercase().as_str() {
"tag" => Ok(Matcher::Tag),
"path" => Ok(Matcher::Path),
"kind" => Ok(Matcher::FileEventKind),
"source" => Ok(Matcher::Source),
"src" => Ok(Matcher::Source),
"process" => Ok(Matcher::Process),
"signal" => Ok(Matcher::Signal),
"exit" => Ok(Matcher::ProcessCompletion),
m => Err(format!("unknown matcher: {}", m)),
},
)(i)
}
fn op(i: &str) -> IResult<&str, Op> {
map_res(
alt((
tag("=="),
tag("!="),
tag("~="),
tag("*="),
tag(":="),
tag(":!"),
tag("="),
)),
|o: &str| match o {
"==" => Ok(Op::Equal),
"!=" => Ok(Op::NotEqual),
"~=" => Ok(Op::Regex),
"*=" => Ok(Op::Glob),
":=" => Ok(Op::InSet),
":!" => Ok(Op::NotInSet),
"=" => Ok(Op::Auto),
o => Err(format!("unknown op: `{}`", o)),
},
)(i)
}
fn pattern(i: &str) -> IResult<&str, &str> {
alt((
// TODO: escapes
delimited(char('"'), is_not("\""), char('"')),
delimited(char('\''), is_not("'"), char('\'')),
take_while1(|_| true),
))(i)
}
fn filter(i: &str) -> IResult<&str, Filter> {
map_res(
tuple((matcher, op, pattern)),
|(m, o, p)| -> Result<_, ()> {
Ok(Filter {
in_path: None,
on: m,
op: match o {
Op::Auto => match m {
Matcher::Path => Op::Glob,
_ => Op::InSet,
},
o => o,
},
pat: match (o, m) {
// TODO: carry regex/glob errors through
(Op::Auto | Op::Glob, Matcher::Path) => {
Pattern::Glob(Glob::new(p).map_err(drop)?)
}
(Op::Equal | Op::NotEqual, _) => Pattern::Exact(p.to_string()),
(Op::Glob, _) => Pattern::Glob(Glob::new(p).map_err(drop)?),
(Op::Regex, _) => Pattern::Regex(Regex::new(p).map_err(drop)?),
(Op::Auto | Op::InSet | Op::NotInSet, _) => {
Pattern::Set(p.split(',').map(|s| s.trim().to_string()).collect())
}
},
})
},
)(i)
}
filter(s)
.finish()
.map(|(_, f)| f)
.map_err(|e| RuntimeError::FilterParse {
src: s.to_string(),
err: e.code,
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Matcher {
Tag,
Path,
FileEventKind,
Source,
Process,
Signal,
ProcessCompletion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Op {
Equal,
NotEqual,
Regex,
Glob,
Includes,
Excludes,
InSet,
OutSet,
Auto, // =
Equal, // ==
NotEqual, // !=
Regex, // ~=
Glob, // *=
InSet, // :=
NotInSet, // :!
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Pattern {
Exact(String),
Regex(Regex),
Glob(Glob),
Set(HashSet<String>),
}
@ -36,6 +163,7 @@ impl PartialEq<Self> for Pattern {
match (self, other) {
(Self::Exact(l), Self::Exact(r)) => l == r,
(Self::Regex(l), Self::Regex(r)) => l.as_str() == r.as_str(),
(Self::Glob(l), Self::Glob(r)) => l == r,
(Self::Set(l), Self::Set(r)) => l == r,
_ => false,
}