From 7881ba85ff40f3d22237ef903c7241b56aa9c185 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2020 15:01:10 +0200 Subject: new filter pipeline This is the groundwork for introducing more complex filter queries like `find` has. Filters can be arbitrarily combined with and/or/not and support an "early filter" mode. So far, the filters have been translated pretty mechanically to match the current command line arguments, so now new syntax has been introduced. The NameFilter is not yet in its final version. The goal is to support something like PlayerAll/PlayerExists and have a PlayerFilter that works on single players instead of the complete log, but that might introduce some code duplication as we then need a PlayerFilterAnd, PlayerFilterOr, ... Some digging has to be done into whether we can reduce that duplication without devolving into madness or resorting to macros. Maybe some type-level generic hackery could be done? Maybe an enum instead of dynamic traits should be used, at least for the base functions? --- src/csl.rs | 34 ++++- src/filters.rs | 396 ++++++++++++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 43 +++++-- 3 files changed, 397 insertions(+), 76 deletions(-) diff --git a/src/csl.rs b/src/csl.rs index e7d84f3..ac20ada 100644 --- a/src/csl.rs +++ b/src/csl.rs @@ -1,14 +1,14 @@ use std::collections::HashSet; +use std::fmt; use std::hash::Hash; use std::str::FromStr; -use std::fmt; -use super::{SearchField, FightOutcome}; +use super::{FightOutcome, SearchField}; use chrono::Weekday; use evtclib::statistics::gamedata::Boss; pub trait Variants: Copy { - type Output: Iterator; + type Output: Iterator; fn variants() -> Self::Output; } @@ -79,18 +79,21 @@ impl fmt::Display for ParseError { } impl FromStr for CommaSeparatedList - where T: FromStr + Variants + Hash + Eq + fmt::Debug +where + T: FromStr + Variants + Hash + Eq + fmt::Debug, { type Err = ParseError; fn from_str(input: &str) -> Result { if input == "*" { - Ok(CommaSeparatedList { values: T::variants().collect() }) + Ok(CommaSeparatedList { + values: T::variants().collect(), + }) } else if input.starts_with(NEGATOR) { let no_csl = CommaSeparatedList::from_str(&input[1..])?; let all_values = T::variants().collect::>(); Ok(CommaSeparatedList { - values: all_values.difference(&no_csl.values).cloned().collect() + values: all_values.difference(&no_csl.values).cloned().collect(), }) } else { let parts = input.split(DELIMITER); @@ -104,9 +107,26 @@ impl FromStr for CommaSeparatedList } impl CommaSeparatedList - where T: Hash + Eq + fmt::Debug +where + T: Hash + Eq + fmt::Debug, { pub fn contains(&self, value: &T) -> bool { self.values.contains(value) } + + pub fn values(&self) -> &HashSet { + &self.values + } +} + +// We allow implicit hasher because then it's a zero-cost conversion, as we're just unwrapping the +// values. +#[allow(clippy::implicit_hasher)] +impl From> for HashSet +where + T: Hash + Eq + fmt::Debug, +{ + fn from(csl: CommaSeparatedList) -> Self { + csl.values + } } diff --git a/src/filters.rs b/src/filters.rs index cdd8f36..7da19ec 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,79 +1,361 @@ +#![allow(clippy::new_ret_no_self)] +use std::collections::HashSet; +use std::ops; + use evtclib::raw::parser::PartialEvtc; use evtclib::statistics::gamedata::Boss; use evtclib::{Agent, AgentName}; -use num_traits::FromPrimitive; - -use super::{guilds, LogResult, Opt, SearchField}; - -use chrono::Datelike; - -/// Do filtering based on the character or account name. -pub fn filter_name(evtc: &PartialEvtc, opt: &Opt) -> bool { - for player in &evtc.agents { - let fancy = Agent::from_raw(player); - if let Ok(AgentName::Player { - ref account_name, - ref character_name, - .. - }) = fancy.as_ref().map(Agent::name) - { - if (opt.field.contains(&SearchField::Account) && opt.expression.is_match(account_name)) - || (opt.field.contains(&SearchField::Character) - && opt.expression.is_match(character_name)) +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +use regex::Regex; + +use super::{guilds, FightOutcome, LogResult, SearchField, Weekday}; + +use chrono::{Datelike, NaiveDateTime}; + +/// Early filtering result. +/// +/// This implements a [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), +/// similar to SQL's `NULL`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(i8)] +pub enum Inclusion { + /// The log should be included. + Include = 1, + /// The state is yet unknown. + Unknown = 0, + /// The log should be excluded. + Exclude = -1, +} + +impl ops::Not for Inclusion { + type Output = Self; + + fn not(self) -> Self::Output { + Inclusion::from_i8(-(self as i8)).unwrap() + } +} + +impl ops::BitAnd for Inclusion { + type Output = Self; + + fn bitand(self, rhs: Inclusion) -> Self::Output { + Inclusion::from_i8((self as i8).min(rhs as i8)).unwrap() + } +} + +impl ops::BitOr for Inclusion { + type Output = Self; + + fn bitor(self, rhs: Inclusion) -> Self::Output { + Inclusion::from_i8((self as i8).max(rhs as i8)).unwrap() + } +} + +impl From for Inclusion { + fn from(data: bool) -> Self { + if data { + Inclusion::Include + } else { + Inclusion::Exclude + } + } +} + +/// The main filter trait. +/// +/// Filters are usually handled as a `Box`. +pub trait Filter: Send + Sync { + /// Determine early (before processing all events) whether the log stands a chance to be + /// included. + /// + /// Note that you can return [Inclusion::Unkown] if this filter cannot determine yet a definite + /// answer. + fn filter_early(&self, _: &PartialEvtc) -> Inclusion { + Inclusion::Unknown + } + + /// Return whether the log should be included, according to this filter. + fn filter(&self, log: &LogResult) -> bool; +} + +#[derive(Debug, Clone, Copy)] +pub struct Const(pub bool); + +impl Const { + pub fn new(output: bool) -> Box { + Box::new(Const(output)) + } +} + +impl Filter for Const { + fn filter_early(&self, _: &PartialEvtc) -> Inclusion { + self.0.into() + } + + fn filter(&self, _: &LogResult) -> bool { + self.0 + } +} + +struct AndFilter(Box, Box); + +impl Filter for AndFilter { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + let lhs = self.0.filter_early(partial_evtc); + // Short circuit behaviour + if lhs == Inclusion::Exclude { + Inclusion::Exclude + } else { + lhs & self.1.filter_early(partial_evtc) + } + } + + fn filter(&self, log: &LogResult) -> bool { + self.0.filter(log) && self.1.filter(log) + } +} + +impl ops::BitAnd> for Box { + type Output = Box; + + fn bitand(self, rhs: Box) -> Self::Output { + Box::new(AndFilter(self, rhs)) + } +} + +struct OrFilter(Box, Box); + +impl Filter for OrFilter { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + let lhs = self.0.filter_early(partial_evtc); + // Short circuit behaviour + if lhs == Inclusion::Include { + Inclusion::Include + } else { + lhs | self.1.filter_early(partial_evtc) + } + } + + fn filter(&self, log: &LogResult) -> bool { + self.0.filter(log) || self.1.filter(log) + } +} + +impl ops::BitOr> for Box { + type Output = Box; + + fn bitor(self, rhs: Box) -> Self::Output { + Box::new(OrFilter(self, rhs)) + } +} + +struct NotFilter(Box); + +impl Filter for NotFilter { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + !self.0.filter_early(partial_evtc) + } + + fn filter(&self, log: &LogResult) -> bool { + !self.0.filter(log) + } +} + +impl ops::Not for Box { + type Output = Box; + + fn not(self) -> Self::Output { + Box::new(NotFilter(self)) + } +} + +// From here onwards, we have the specific filters + +/// Filter that filters according to the name. +/// +/// The given SearchField determines in which field something should be searched. +#[derive(Debug, Clone)] +pub struct NameFilter(SearchField, Regex); + +impl NameFilter { + pub fn new(field: SearchField, regex: Regex) -> Box { + Box::new(NameFilter(field, regex)) + } +} + +impl Filter for NameFilter { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + if self.0 == SearchField::Guild { + return Inclusion::Unknown; + } + + for player in &partial_evtc.agents { + let fancy = Agent::from_raw(player); + if let Ok(AgentName::Player { + ref account_name, + ref character_name, + .. + }) = fancy.as_ref().map(Agent::name) { - return true; + let field = match self.0 { + SearchField::Account => account_name, + SearchField::Character => character_name, + _ => unreachable!("We already checked for Guild earlier"), + }; + if self.1.is_match(field) { + return Inclusion::Include; + } + } + } + Inclusion::Exclude + } + + fn filter(&self, log: &LogResult) -> bool { + for player in &log.players { + match self.0 { + SearchField::Account if self.1.is_match(&player.account_name) => return true, + SearchField::Character if self.1.is_match(&player.character_name) => return true, + SearchField::Guild => { + let guild_ok = player + .guild_id + .as_ref() + .and_then(|id| guilds::lookup(id)) + .map(|guild| self.1.is_match(guild.tag()) || self.1.is_match(guild.name())) + .unwrap_or(false); + if guild_ok { + return true; + } + } + _ => (), } } + false + } +} + +#[derive(Debug, Clone)] +pub struct BossFilter(HashSet); + +impl BossFilter { + pub fn new(bosses: HashSet) -> Box { + Box::new(BossFilter(bosses)) + } +} + +impl Filter for BossFilter { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + let boss = Boss::from_u16(partial_evtc.header.combat_id); + boss.map(|b| self.0.contains(&b).into()) + .unwrap_or(Inclusion::Include) + } + + fn filter(&self, log: &LogResult) -> bool { + let boss = Boss::from_u16(log.boss_id); + boss.map(|b| self.0.contains(&b)).unwrap_or(false) } - // Don't throw away the log yet if we are searching for guilds - opt.field.contains(&SearchField::Guild) } -/// Do filtering based on the boss ID. -pub fn filter_boss(evtc: &PartialEvtc, opt: &Opt) -> bool { - let boss = Boss::from_u16(evtc.header.combat_id); - boss.map(|b| opt.bosses.contains(&b)).unwrap_or(true) +#[derive(Debug, Clone)] +pub struct OutcomeFilter(HashSet); + +impl OutcomeFilter { + pub fn new(outcomes: HashSet) -> Box { + Box::new(OutcomeFilter(outcomes)) + } } -/// Do filtering based on the fight outcome. -pub fn filter_outcome(result: &LogResult, opt: &Opt) -> bool { - opt.outcome.contains(&result.outcome) +impl Filter for OutcomeFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.outcome) + } } -/// Do filtering based on the weekday of the fight. -pub fn filter_weekday(result: &LogResult, opt: &Opt) -> bool { - opt.weekdays.contains(&result.time.weekday()) +#[derive(Debug, Clone)] +pub struct WeekdayFilter(HashSet); + +impl WeekdayFilter { + pub fn new(weekdays: HashSet) -> Box { + Box::new(WeekdayFilter(weekdays)) + } } -/// Do filtering based on encounter time. -pub fn filter_time(result: &LogResult, opt: &Opt) -> bool { - let after_ok = match opt.after { - Some(time) => time <= result.time, - None => true, - }; - let before_ok = match opt.before { - Some(time) => time >= result.time, - None => true, - }; +impl Filter for WeekdayFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.time.weekday()) + } +} - after_ok && before_ok +#[derive(Debug, Clone)] +pub struct TimeFilter(Option, Option); + +impl TimeFilter { + pub fn new(after: Option, before: Option) -> Box { + Box::new(TimeFilter(after, before)) + } } -/// Do filtering based on the guilds. -pub fn filter_guilds(result: &LogResult, opt: &Opt) -> bool { - if !opt.guilds { - return true; +impl Filter for TimeFilter { + fn filter(&self, log: &LogResult) -> bool { + let after_ok = match self.0 { + Some(time) => time <= log.time, + None => true, + }; + let before_ok = match self.1 { + Some(time) => time >= log.time, + None => true, + }; + + after_ok && before_ok } - if !opt.field.contains(&SearchField::Guild) { - return true; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inclusion_not() { + use Inclusion::*; + + assert_eq!(!Exclude, Include); + assert_eq!(!Include, Exclude); + assert_eq!(!Unknown, Unknown); + } + + #[test] + fn test_inclusion_and() { + use Inclusion::*; + + assert_eq!(Exclude & Exclude, Exclude); + assert_eq!(Exclude & Unknown, Exclude); + assert_eq!(Exclude & Include, Exclude); + + assert_eq!(Unknown & Exclude, Exclude); + assert_eq!(Unknown & Unknown, Unknown); + assert_eq!(Unknown & Include, Unknown); + + assert_eq!(Include & Exclude, Exclude); + assert_eq!(Include & Unknown, Unknown); + assert_eq!(Include & Include, Include); + } + + #[test] + fn test_inclusion_or() { + use Inclusion::*; + + assert_eq!(Exclude | Exclude, Exclude); + assert_eq!(Exclude | Unknown, Unknown); + assert_eq!(Exclude | Include, Include); + + assert_eq!(Unknown | Exclude, Unknown); + assert_eq!(Unknown | Unknown, Unknown); + assert_eq!(Unknown | Include, Include); + + assert_eq!(Include | Exclude, Include); + assert_eq!(Include | Unknown, Include); + assert_eq!(Include | Include, Include); } - result.players.iter().any(|player| { - let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id)); - if let Some(guild) = guild { - opt.expression.is_match(guild.tag()) || opt.expression.is_match(guild.name()) - } else { - false - } - }) } diff --git a/src/main.rs b/src/main.rs index 6b67875..41df732 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,15 +6,16 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use chrono::{Duration, NaiveDateTime, Weekday}; +use log::debug; use num_traits::cast::FromPrimitive; use regex::Regex; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; -use log::debug; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod filters; +use filters::{Filter, Inclusion}; mod guilds; mod logger; mod output; @@ -32,7 +33,6 @@ macro_rules! unwrap { }; } - /// A program that allows you to search through all your evtc logs for specific /// people. #[derive(StructOpt, Debug)] @@ -106,7 +106,7 @@ pub struct Opt { /// A flag indicating which fields should be searched. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -enum SearchField { +pub enum SearchField { /// Only search the account name. Account, /// Only search the character name. @@ -135,6 +135,8 @@ pub struct LogResult { log_file: PathBuf, /// The time of the recording. time: NaiveDateTime, + /// The numeric ID of the boss. + boss_id: u16, /// The name of the boss. boss_name: String, /// A vector of all participating players. @@ -258,16 +260,36 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } +fn build_filter(opt: &Opt) -> Box { + let mut filter = filters::Const::new(false); + for field in opt.field.values() { + filter = filter | filters::NameFilter::new(*field, opt.expression.clone()); + } + + if opt.invert { + filter = !filter; + } + + filter = filter + & filters::BossFilter::new(opt.bosses.values().clone()) + & filters::OutcomeFilter::new(opt.outcome.values().clone()) + & filters::WeekdayFilter::new(opt.weekdays.values().clone()) + & filters::TimeFilter::new(opt.after, opt.before); + + filter +} + /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<()> { let pipeline = &output::build_pipeline(opt); + let filter: &dyn Filter = &*build_filter(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { let entry = entry?; s.spawn(move |_| { if is_log_file(&entry) { - let search = search_log(&entry, opt); + let search = search_log(&entry, filter); match search { Ok(None) => (), Ok(Some(result)) => pipeline.push_item(&result), @@ -287,7 +309,7 @@ fn grep(opt: &Opt) -> Result<()> { /// If the log matches, returns `Ok(Some(..))`. /// If the log doesn't match, returns `Ok(None)`. /// If there was a fatal error, returns `Err(..)`. -fn search_log(entry: &DirEntry, opt: &Opt) -> Result> { +fn search_log(entry: &DirEntry, filter: &dyn Filter) -> Result> { let file_stream = BufReader::new(File::open(entry.path())?); let is_zip = entry .file_name() @@ -302,10 +324,9 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result> { let mut stream = wrapper.get_stream(); let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?; - let early_ok = - filters::filter_name(&partial, opt) != opt.invert && filters::filter_boss(&partial, opt); + let early_ok = filter.filter_early(&partial); - if !early_ok { + if early_ok == Inclusion::Exclude { return Ok(None); } @@ -320,10 +341,7 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result> { let info = extract_info(entry, &log); - let take_log = filters::filter_outcome(&info, opt) - && filters::filter_weekday(&info, opt) - && filters::filter_time(&info, opt) - && filters::filter_guilds(&info, opt); + let take_log = filter.filter(&info); if take_log { Ok(Some(info)) @@ -372,6 +390,7 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult { LogResult { log_file: entry.path().to_path_buf(), time: NaiveDateTime::from_timestamp(i64::from(get_start_timestamp(log)), 0), + boss_id: log.boss_id(), boss_name, players, outcome: get_fight_outcome(log), -- cgit v1.2.3 From c381bfd0972d8f8e080c74d63df66486e514f35f Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2020 15:06:28 +0200 Subject: formatting fixes --- src/output/aggregators.rs | 2 -- src/output/formats.rs | 15 +++++++-------- src/output/mod.rs | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/output/aggregators.rs b/src/output/aggregators.rs index 5d0429c..1b04af3 100644 --- a/src/output/aggregators.rs +++ b/src/output/aggregators.rs @@ -15,12 +15,10 @@ pub trait Aggregator: Sync { fn finish(self, format: &dyn Format, stream: &mut dyn Write); } - /// An aggregator that just pushes through each item to the output stream without any sorting or /// whatsoever. pub struct WriteThrough; - impl Aggregator for WriteThrough { fn push_item(&self, item: &LogResult, format: &dyn Format, stream: &mut dyn Write) { let text = format.format_result(item); diff --git a/src/output/formats.rs b/src/output/formats.rs index b697401..a608eab 100644 --- a/src/output/formats.rs +++ b/src/output/formats.rs @@ -1,8 +1,8 @@ //! A crate defining different output formats for search results. use std::fmt::Write; -use super::{LogResult, FightOutcome}; use super::super::guilds; +use super::{FightOutcome, LogResult}; /// An output format pub trait Format: Sync + Send { @@ -10,14 +10,12 @@ pub trait Format: Sync + Send { fn format_result(&self, item: &LogResult) -> String; } - /// The human readable, colored format. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub struct HumanReadable { pub show_guilds: bool, } - impl Format for HumanReadable { fn format_result(&self, item: &LogResult) -> String { use colored::Colorize; @@ -36,7 +34,8 @@ impl Format for HumanReadable { "Boss".green(), item.boss_name, outcome, - ).unwrap(); + ) + .unwrap(); for player in &item.players { write!( result, @@ -45,7 +44,8 @@ impl Format for HumanReadable { player.account_name.yellow(), player.character_name.cyan(), player.profession, - ).unwrap(); + ) + .unwrap(); if self.show_guilds { let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id)); if let Some(guild) = guild { @@ -54,7 +54,8 @@ impl Format for HumanReadable { " [{}] {}", guild.tag().magenta(), guild.name().magenta(), - ).unwrap(); + ) + .unwrap(); } } writeln!(result).unwrap(); @@ -63,12 +64,10 @@ impl Format for HumanReadable { } } - /// A format which outputs only the file-name #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub struct FileOnly; - impl Format for FileOnly { fn format_result(&self, item: &LogResult) -> String { let filename = item.log_file.to_string_lossy(); diff --git a/src/output/mod.rs b/src/output/mod.rs index aadcbf9..0fd92d9 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -2,8 +2,8 @@ use super::{FightOutcome, LogResult, Opt}; use std::io; -pub mod formats; pub mod aggregators; +pub mod formats; pub mod pipeline; pub use self::pipeline::Pipeline; -- cgit v1.2.3 From ba491c8a5f6c8c2fa86b12dacf9d80f92da9168a Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 15:18:20 +0200 Subject: split off player filters and log filters As it turns out, we can easily re-use the existing Filter machinery to generalize over LogFilters (which operate on LogResults) and PlayerFilters (which operate on Players). The feature trait_aliases is not strictly needed but makes the function signatures a bit nicer and easier to read, and it reduces the chances of an error (e.g. by using Filter<&PartialEvtc, ...>). --- src/filters.rs | 361 -------------------------------------------------- src/filters/log.rs | 95 +++++++++++++ src/filters/mod.rs | 213 +++++++++++++++++++++++++++++ src/filters/player.rs | 109 +++++++++++++++ src/main.rs | 28 ++-- 5 files changed, 433 insertions(+), 373 deletions(-) delete mode 100644 src/filters.rs create mode 100644 src/filters/log.rs create mode 100644 src/filters/mod.rs create mode 100644 src/filters/player.rs diff --git a/src/filters.rs b/src/filters.rs deleted file mode 100644 index 7da19ec..0000000 --- a/src/filters.rs +++ /dev/null @@ -1,361 +0,0 @@ -#![allow(clippy::new_ret_no_self)] -use std::collections::HashSet; -use std::ops; - -use evtclib::raw::parser::PartialEvtc; -use evtclib::statistics::gamedata::Boss; -use evtclib::{Agent, AgentName}; - -use num_derive::FromPrimitive; -use num_traits::FromPrimitive as _; - -use regex::Regex; - -use super::{guilds, FightOutcome, LogResult, SearchField, Weekday}; - -use chrono::{Datelike, NaiveDateTime}; - -/// Early filtering result. -/// -/// This implements a [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), -/// similar to SQL's `NULL`. -#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] -#[repr(i8)] -pub enum Inclusion { - /// The log should be included. - Include = 1, - /// The state is yet unknown. - Unknown = 0, - /// The log should be excluded. - Exclude = -1, -} - -impl ops::Not for Inclusion { - type Output = Self; - - fn not(self) -> Self::Output { - Inclusion::from_i8(-(self as i8)).unwrap() - } -} - -impl ops::BitAnd for Inclusion { - type Output = Self; - - fn bitand(self, rhs: Inclusion) -> Self::Output { - Inclusion::from_i8((self as i8).min(rhs as i8)).unwrap() - } -} - -impl ops::BitOr for Inclusion { - type Output = Self; - - fn bitor(self, rhs: Inclusion) -> Self::Output { - Inclusion::from_i8((self as i8).max(rhs as i8)).unwrap() - } -} - -impl From for Inclusion { - fn from(data: bool) -> Self { - if data { - Inclusion::Include - } else { - Inclusion::Exclude - } - } -} - -/// The main filter trait. -/// -/// Filters are usually handled as a `Box`. -pub trait Filter: Send + Sync { - /// Determine early (before processing all events) whether the log stands a chance to be - /// included. - /// - /// Note that you can return [Inclusion::Unkown] if this filter cannot determine yet a definite - /// answer. - fn filter_early(&self, _: &PartialEvtc) -> Inclusion { - Inclusion::Unknown - } - - /// Return whether the log should be included, according to this filter. - fn filter(&self, log: &LogResult) -> bool; -} - -#[derive(Debug, Clone, Copy)] -pub struct Const(pub bool); - -impl Const { - pub fn new(output: bool) -> Box { - Box::new(Const(output)) - } -} - -impl Filter for Const { - fn filter_early(&self, _: &PartialEvtc) -> Inclusion { - self.0.into() - } - - fn filter(&self, _: &LogResult) -> bool { - self.0 - } -} - -struct AndFilter(Box, Box); - -impl Filter for AndFilter { - fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { - let lhs = self.0.filter_early(partial_evtc); - // Short circuit behaviour - if lhs == Inclusion::Exclude { - Inclusion::Exclude - } else { - lhs & self.1.filter_early(partial_evtc) - } - } - - fn filter(&self, log: &LogResult) -> bool { - self.0.filter(log) && self.1.filter(log) - } -} - -impl ops::BitAnd> for Box { - type Output = Box; - - fn bitand(self, rhs: Box) -> Self::Output { - Box::new(AndFilter(self, rhs)) - } -} - -struct OrFilter(Box, Box); - -impl Filter for OrFilter { - fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { - let lhs = self.0.filter_early(partial_evtc); - // Short circuit behaviour - if lhs == Inclusion::Include { - Inclusion::Include - } else { - lhs | self.1.filter_early(partial_evtc) - } - } - - fn filter(&self, log: &LogResult) -> bool { - self.0.filter(log) || self.1.filter(log) - } -} - -impl ops::BitOr> for Box { - type Output = Box; - - fn bitor(self, rhs: Box) -> Self::Output { - Box::new(OrFilter(self, rhs)) - } -} - -struct NotFilter(Box); - -impl Filter for NotFilter { - fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { - !self.0.filter_early(partial_evtc) - } - - fn filter(&self, log: &LogResult) -> bool { - !self.0.filter(log) - } -} - -impl ops::Not for Box { - type Output = Box; - - fn not(self) -> Self::Output { - Box::new(NotFilter(self)) - } -} - -// From here onwards, we have the specific filters - -/// Filter that filters according to the name. -/// -/// The given SearchField determines in which field something should be searched. -#[derive(Debug, Clone)] -pub struct NameFilter(SearchField, Regex); - -impl NameFilter { - pub fn new(field: SearchField, regex: Regex) -> Box { - Box::new(NameFilter(field, regex)) - } -} - -impl Filter for NameFilter { - fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { - if self.0 == SearchField::Guild { - return Inclusion::Unknown; - } - - for player in &partial_evtc.agents { - let fancy = Agent::from_raw(player); - if let Ok(AgentName::Player { - ref account_name, - ref character_name, - .. - }) = fancy.as_ref().map(Agent::name) - { - let field = match self.0 { - SearchField::Account => account_name, - SearchField::Character => character_name, - _ => unreachable!("We already checked for Guild earlier"), - }; - if self.1.is_match(field) { - return Inclusion::Include; - } - } - } - Inclusion::Exclude - } - - fn filter(&self, log: &LogResult) -> bool { - for player in &log.players { - match self.0 { - SearchField::Account if self.1.is_match(&player.account_name) => return true, - SearchField::Character if self.1.is_match(&player.character_name) => return true, - SearchField::Guild => { - let guild_ok = player - .guild_id - .as_ref() - .and_then(|id| guilds::lookup(id)) - .map(|guild| self.1.is_match(guild.tag()) || self.1.is_match(guild.name())) - .unwrap_or(false); - if guild_ok { - return true; - } - } - _ => (), - } - } - false - } -} - -#[derive(Debug, Clone)] -pub struct BossFilter(HashSet); - -impl BossFilter { - pub fn new(bosses: HashSet) -> Box { - Box::new(BossFilter(bosses)) - } -} - -impl Filter for BossFilter { - fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { - let boss = Boss::from_u16(partial_evtc.header.combat_id); - boss.map(|b| self.0.contains(&b).into()) - .unwrap_or(Inclusion::Include) - } - - fn filter(&self, log: &LogResult) -> bool { - let boss = Boss::from_u16(log.boss_id); - boss.map(|b| self.0.contains(&b)).unwrap_or(false) - } -} - -#[derive(Debug, Clone)] -pub struct OutcomeFilter(HashSet); - -impl OutcomeFilter { - pub fn new(outcomes: HashSet) -> Box { - Box::new(OutcomeFilter(outcomes)) - } -} - -impl Filter for OutcomeFilter { - fn filter(&self, log: &LogResult) -> bool { - self.0.contains(&log.outcome) - } -} - -#[derive(Debug, Clone)] -pub struct WeekdayFilter(HashSet); - -impl WeekdayFilter { - pub fn new(weekdays: HashSet) -> Box { - Box::new(WeekdayFilter(weekdays)) - } -} - -impl Filter for WeekdayFilter { - fn filter(&self, log: &LogResult) -> bool { - self.0.contains(&log.time.weekday()) - } -} - -#[derive(Debug, Clone)] -pub struct TimeFilter(Option, Option); - -impl TimeFilter { - pub fn new(after: Option, before: Option) -> Box { - Box::new(TimeFilter(after, before)) - } -} - -impl Filter for TimeFilter { - fn filter(&self, log: &LogResult) -> bool { - let after_ok = match self.0 { - Some(time) => time <= log.time, - None => true, - }; - let before_ok = match self.1 { - Some(time) => time >= log.time, - None => true, - }; - - after_ok && before_ok - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_inclusion_not() { - use Inclusion::*; - - assert_eq!(!Exclude, Include); - assert_eq!(!Include, Exclude); - assert_eq!(!Unknown, Unknown); - } - - #[test] - fn test_inclusion_and() { - use Inclusion::*; - - assert_eq!(Exclude & Exclude, Exclude); - assert_eq!(Exclude & Unknown, Exclude); - assert_eq!(Exclude & Include, Exclude); - - assert_eq!(Unknown & Exclude, Exclude); - assert_eq!(Unknown & Unknown, Unknown); - assert_eq!(Unknown & Include, Unknown); - - assert_eq!(Include & Exclude, Exclude); - assert_eq!(Include & Unknown, Unknown); - assert_eq!(Include & Include, Include); - } - - #[test] - fn test_inclusion_or() { - use Inclusion::*; - - assert_eq!(Exclude | Exclude, Exclude); - assert_eq!(Exclude | Unknown, Unknown); - assert_eq!(Exclude | Include, Include); - - assert_eq!(Unknown | Exclude, Unknown); - assert_eq!(Unknown | Unknown, Unknown); - assert_eq!(Unknown | Include, Include); - - assert_eq!(Include | Exclude, Include); - assert_eq!(Include | Unknown, Include); - assert_eq!(Include | Include, Include); - } -} diff --git a/src/filters/log.rs b/src/filters/log.rs new file mode 100644 index 0000000..ded4c44 --- /dev/null +++ b/src/filters/log.rs @@ -0,0 +1,95 @@ +//! This module contains specific filters that operate on log files. +//! +//! This is the "base unit", as each file corresponds to one log. Filters on other items (such as +//! players) have to be lifted into log filters first. +use super::{ + super::{FightOutcome, LogResult, Weekday}, + Filter, Inclusion, +}; + +use std::collections::HashSet; + +use evtclib::raw::parser::PartialEvtc; +use evtclib::statistics::gamedata::Boss; + +use chrono::{Datelike, NaiveDateTime}; +use num_traits::FromPrimitive as _; + +/// Filter trait used for filters that operate on complete logs. +pub trait LogFilter = Filter; + +#[derive(Debug, Clone)] +pub struct BossFilter(HashSet); + +impl BossFilter { + pub fn new(bosses: HashSet) -> Box { + Box::new(BossFilter(bosses)) + } +} + +impl Filter for BossFilter { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + let boss = Boss::from_u16(partial_evtc.header.combat_id); + boss.map(|b| self.0.contains(&b).into()) + .unwrap_or(Inclusion::Include) + } + + fn filter(&self, log: &LogResult) -> bool { + let boss = Boss::from_u16(log.boss_id); + boss.map(|b| self.0.contains(&b)).unwrap_or(false) + } +} + +#[derive(Debug, Clone)] +pub struct OutcomeFilter(HashSet); + +impl OutcomeFilter { + pub fn new(outcomes: HashSet) -> Box { + Box::new(OutcomeFilter(outcomes)) + } +} + +impl Filter for OutcomeFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.outcome) + } +} + +#[derive(Debug, Clone)] +pub struct WeekdayFilter(HashSet); + +impl WeekdayFilter { + pub fn new(weekdays: HashSet) -> Box { + Box::new(WeekdayFilter(weekdays)) + } +} + +impl Filter for WeekdayFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.time.weekday()) + } +} + +#[derive(Debug, Clone)] +pub struct TimeFilter(Option, Option); + +impl TimeFilter { + pub fn new(after: Option, before: Option) -> Box { + Box::new(TimeFilter(after, before)) + } +} + +impl Filter for TimeFilter { + fn filter(&self, log: &LogResult) -> bool { + let after_ok = match self.0 { + Some(time) => time <= log.time, + None => true, + }; + let before_ok = match self.1 { + Some(time) => time >= log.time, + None => true, + }; + + after_ok && before_ok + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs new file mode 100644 index 0000000..62dc04b --- /dev/null +++ b/src/filters/mod.rs @@ -0,0 +1,213 @@ +#![allow(clippy::new_ret_no_self)] +use std::ops; + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +pub mod log; +pub mod player; + +/// Early filtering result. +/// +/// This implements a [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), +/// similar to SQL's `NULL`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(i8)] +pub enum Inclusion { + /// The log should be included. + Include = 1, + /// The state is yet unknown. + Unknown = 0, + /// The log should be excluded. + Exclude = -1, +} + +impl ops::Not for Inclusion { + type Output = Self; + + fn not(self) -> Self::Output { + Inclusion::from_i8(-(self as i8)).unwrap() + } +} + +impl ops::BitAnd for Inclusion { + type Output = Self; + + fn bitand(self, rhs: Inclusion) -> Self::Output { + Inclusion::from_i8((self as i8).min(rhs as i8)).unwrap() + } +} + +impl ops::BitOr for Inclusion { + type Output = Self; + + fn bitor(self, rhs: Inclusion) -> Self::Output { + Inclusion::from_i8((self as i8).max(rhs as i8)).unwrap() + } +} + +impl From for Inclusion { + fn from(data: bool) -> Self { + if data { + Inclusion::Include + } else { + Inclusion::Exclude + } + } +} + +/// The main filter trait. +/// +/// Filters are usually handled as a `Box`. +pub trait Filter: Send + Sync { + /// Determine early (before processing all events) whether the log stands a chance to be + /// included. + /// + /// Note that you can return [Inclusion::Unkown] if this filter cannot determine yet a definite + /// answer. + fn filter_early(&self, _: &Early) -> Inclusion { + Inclusion::Unknown + } + + /// Return whether the log should be included, according to this filter. + fn filter(&self, _: &Late) -> bool; +} + +#[derive(Debug, Clone, Copy)] +pub struct Const(pub bool); + +impl Const { + pub fn new(output: bool) -> Box> { + Box::new(Const(output)) + } +} + +impl Filter for Const { + fn filter_early(&self, _: &E) -> Inclusion { + self.0.into() + } + + fn filter(&self, _: &L) -> bool { + self.0 + } +} + +struct AndFilter(Box>, Box>); + +impl Filter for AndFilter { + fn filter_early(&self, partial_evtc: &E) -> Inclusion { + let lhs = self.0.filter_early(partial_evtc); + // Short circuit behaviour + if lhs == Inclusion::Exclude { + Inclusion::Exclude + } else { + lhs & self.1.filter_early(partial_evtc) + } + } + + fn filter(&self, log: &L) -> bool { + self.0.filter(log) && self.1.filter(log) + } +} + +impl ops::BitAnd>> for Box> { + type Output = Box>; + + fn bitand(self, rhs: Box>) -> Self::Output { + Box::new(AndFilter(self, rhs)) + } +} + +struct OrFilter(Box>, Box>); + +impl Filter for OrFilter { + fn filter_early(&self, partial_evtc: &E) -> Inclusion { + let lhs = self.0.filter_early(partial_evtc); + // Short circuit behaviour + if lhs == Inclusion::Include { + Inclusion::Include + } else { + lhs | self.1.filter_early(partial_evtc) + } + } + + fn filter(&self, log: &L) -> bool { + self.0.filter(log) || self.1.filter(log) + } +} + +impl ops::BitOr>> for Box> { + type Output = Box>; + + fn bitor(self, rhs: Box>) -> Self::Output { + Box::new(OrFilter(self, rhs)) + } +} + +struct NotFilter(Box>); + +impl Filter for NotFilter { + fn filter_early(&self, partial_evtc: &E) -> Inclusion { + !self.0.filter_early(partial_evtc) + } + + fn filter(&self, log: &L) -> bool { + !self.0.filter(log) + } +} + +impl ops::Not for Box> { + type Output = Box>; + + fn not(self) -> Self::Output { + Box::new(NotFilter(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inclusion_not() { + use Inclusion::*; + + assert_eq!(!Exclude, Include); + assert_eq!(!Include, Exclude); + assert_eq!(!Unknown, Unknown); + } + + #[test] + fn test_inclusion_and() { + use Inclusion::*; + + assert_eq!(Exclude & Exclude, Exclude); + assert_eq!(Exclude & Unknown, Exclude); + assert_eq!(Exclude & Include, Exclude); + + assert_eq!(Unknown & Exclude, Exclude); + assert_eq!(Unknown & Unknown, Unknown); + assert_eq!(Unknown & Include, Unknown); + + assert_eq!(Include & Exclude, Exclude); + assert_eq!(Include & Unknown, Unknown); + assert_eq!(Include & Include, Include); + } + + #[test] + fn test_inclusion_or() { + use Inclusion::*; + + assert_eq!(Exclude | Exclude, Exclude); + assert_eq!(Exclude | Unknown, Unknown); + assert_eq!(Exclude | Include, Include); + + assert_eq!(Unknown | Exclude, Unknown); + assert_eq!(Unknown | Unknown, Unknown); + assert_eq!(Unknown | Include, Include); + + assert_eq!(Include | Exclude, Include); + assert_eq!(Include | Unknown, Include); + assert_eq!(Include | Include, Include); + } +} diff --git a/src/filters/player.rs b/src/filters/player.rs new file mode 100644 index 0000000..6cc7713 --- /dev/null +++ b/src/filters/player.rs @@ -0,0 +1,109 @@ +//! This module contains filters pertaining to a single player. +//! +//! Additionally, it provides methods to lift a player filter to a log filter with [`any`][any] and +//! [`all`][all]. +use super::{ + super::{guilds, LogResult, Player, SearchField}, + log::LogFilter, + Filter, Inclusion, +}; + +use evtclib::raw::parser::PartialEvtc; +use evtclib::{Agent, AgentName}; + +use regex::Regex; + +/// Filter type for filters that operate on single players. +pub trait PlayerFilter = Filter; + +/// Struct that lifts a [`PlayerFilter`](traitalias.PlayerFilter.html) to a +/// [`LogFilter`](../log/traitalias.LogFilter.html) by requiring all players to match. +/// +/// This struct will short-circuit once the result is known. +struct AllPlayers(Box); + +impl Filter for AllPlayers { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + let mut result = Inclusion::Include; + for agent in &partial_evtc.agents { + if !agent.is_player() { + continue; + } + + let agent = Agent::from_raw(agent); + if let Ok(agent) = agent { + result = result & self.0.filter_early(&agent); + } else { + result = result & Inclusion::Unknown; + } + // Short circuit + if result == Inclusion::Exclude { + return result; + } + } + result + } + + fn filter(&self, log: &LogResult) -> bool { + log.players.iter().all(|p| self.0.filter(p)) + } +} + +/// Construct a filter that requires the given `player_filter` to match for all players in a log. +pub fn all(player_filter: Box) -> Box { + Box::new(AllPlayers(player_filter)) +} + +/// Construct a filter that requires the given `player_filter` to match for any player in a log. +pub fn any(player_filter: Box) -> Box { + !all(!player_filter) +} + +/// Filter that filters players according to their name. +/// +/// The given SearchField determines in which field something should be searched. +#[derive(Debug, Clone)] +pub struct NameFilter(SearchField, Regex); + +impl NameFilter { + pub fn new(field: SearchField, regex: Regex) -> Box { + Box::new(NameFilter(field, regex)) + } +} + +impl Filter for NameFilter { + fn filter_early(&self, agent: &Agent) -> Inclusion { + if self.0 == SearchField::Guild { + return Inclusion::Unknown; + } + + if let AgentName::Player { + ref account_name, + ref character_name, + .. + } = agent.name() + { + let field = match self.0 { + SearchField::Account => account_name, + SearchField::Character => character_name, + _ => unreachable!("We already checked for Guild earlier"), + }; + self.1.is_match(field).into() + } else { + Inclusion::Unknown + } + } + + fn filter(&self, player: &Player) -> bool { + match self.0 { + SearchField::Account => self.1.is_match(&player.account_name), + SearchField::Character => self.1.is_match(&player.character_name), + SearchField::Guild => player + .guild_id + .as_ref() + .and_then(|id| guilds::lookup(id)) + .map(|guild| self.1.is_match(guild.tag()) || self.1.is_match(guild.name())) + .unwrap_or(false), + } + } +} diff --git a/src/main.rs b/src/main.rs index 41df732..bea03f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![feature(trait_alias)] use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Read, Seek}; @@ -15,7 +16,7 @@ use walkdir::{DirEntry, WalkDir}; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod filters; -use filters::{Filter, Inclusion}; +use filters::{log::LogFilter, Inclusion}; mod guilds; mod logger; mod output; @@ -260,21 +261,24 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } -fn build_filter(opt: &Opt) -> Box { - let mut filter = filters::Const::new(false); - for field in opt.field.values() { - filter = filter | filters::NameFilter::new(*field, opt.expression.clone()); - } +fn build_filter(opt: &Opt) -> Box { + let player_filter = opt + .field + .values() + .iter() + .map(|field| filters::player::NameFilter::new(*field, opt.expression.clone())) + .fold(filters::Const::new(false), |a, f| a | f); + let mut filter = filters::player::any(player_filter); if opt.invert { filter = !filter; } filter = filter - & filters::BossFilter::new(opt.bosses.values().clone()) - & filters::OutcomeFilter::new(opt.outcome.values().clone()) - & filters::WeekdayFilter::new(opt.weekdays.values().clone()) - & filters::TimeFilter::new(opt.after, opt.before); + & filters::log::BossFilter::new(opt.bosses.values().clone()) + & filters::log::OutcomeFilter::new(opt.outcome.values().clone()) + & filters::log::WeekdayFilter::new(opt.weekdays.values().clone()) + & filters::log::TimeFilter::new(opt.after, opt.before); filter } @@ -282,7 +286,7 @@ fn build_filter(opt: &Opt) -> Box { /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<()> { let pipeline = &output::build_pipeline(opt); - let filter: &dyn Filter = &*build_filter(opt); + let filter: &dyn LogFilter = &*build_filter(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { @@ -309,7 +313,7 @@ fn grep(opt: &Opt) -> Result<()> { /// If the log matches, returns `Ok(Some(..))`. /// If the log doesn't match, returns `Ok(None)`. /// If there was a fatal error, returns `Err(..)`. -fn search_log(entry: &DirEntry, filter: &dyn Filter) -> Result> { +fn search_log(entry: &DirEntry, filter: &dyn LogFilter) -> Result> { let file_stream = BufReader::new(File::open(entry.path())?); let is_zip = entry .file_name() -- cgit v1.2.3 From 79752779036d31ee6427c731cd7c058eccf1034a Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 16:23:39 +0200 Subject: add missing num-derive dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index b1b9a12..db1e9c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ colored = "1" chrono = "0.4" rayon = "1" num-traits = "0.2" +num-derive = "0.3" humantime = "2.0" zip = "0.5" anyhow = "1.0" -- cgit v1.2.3 From 7030224fd2a97b3551fdd47c43249e3a42341238 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 18 Apr 2020 15:10:59 +0200 Subject: make filters Debug It's nice if you can print out the filter tree for debugging, so we're requireing filters to be Debug now. --- src/filters/log.rs | 12 ++++++++++++ src/filters/mod.rs | 22 ++++++++++++++++++++-- src/filters/player.rs | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/filters/log.rs b/src/filters/log.rs index ded4c44..8b84bf3 100644 --- a/src/filters/log.rs +++ b/src/filters/log.rs @@ -47,6 +47,18 @@ impl OutcomeFilter { pub fn new(outcomes: HashSet) -> Box { Box::new(OutcomeFilter(outcomes)) } + + pub fn success() -> Box { + let mut outcomes = HashSet::new(); + outcomes.insert(FightOutcome::Success); + Self::new(outcomes) + } + + pub fn wipe() -> Box { + let mut outcomes = HashSet::new(); + outcomes.insert(FightOutcome::Wipe); + Self::new(outcomes) + } } impl Filter for OutcomeFilter { diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 62dc04b..525ff27 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -1,5 +1,5 @@ #![allow(clippy::new_ret_no_self)] -use std::ops; +use std::{ops, fmt}; use num_derive::FromPrimitive; use num_traits::FromPrimitive as _; @@ -59,7 +59,7 @@ impl From for Inclusion { /// The main filter trait. /// /// Filters are usually handled as a `Box`. -pub trait Filter: Send + Sync { +pub trait Filter: Send + Sync + fmt::Debug { /// Determine early (before processing all events) whether the log stands a chance to be /// included. /// @@ -118,6 +118,12 @@ impl ops::BitAnd>> for Box fmt::Debug for AndFilter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({:?}) and ({:?})", self.0, self.1) + } +} + struct OrFilter(Box>, Box>); impl Filter for OrFilter { @@ -144,6 +150,12 @@ impl ops::BitOr>> for Box fmt::Debug for OrFilter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({:?}) or ({:?})", self.0, self.1) + } +} + struct NotFilter(Box>); impl Filter for NotFilter { @@ -164,6 +176,12 @@ impl ops::Not for Box> { } } +impl fmt::Debug for NotFilter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "not ({:?})", self.0) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/filters/player.rs b/src/filters/player.rs index 6cc7713..8f9196a 100644 --- a/src/filters/player.rs +++ b/src/filters/player.rs @@ -20,6 +20,7 @@ pub trait PlayerFilter = Filter; /// [`LogFilter`](../log/traitalias.LogFilter.html) by requiring all players to match. /// /// This struct will short-circuit once the result is known. +#[derive(Debug)] struct AllPlayers(Box); impl Filter for AllPlayers { -- cgit v1.2.3 From e19519e155af95698807f377a5f6b525e255c4e5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 18 Apr 2020 15:12:21 +0200 Subject: first version of the new filter pipeline --- Cargo.toml | 5 ++ build.rs | 5 ++ src/fexpr/grammar.lalrpop | 131 ++++++++++++++++++++++++++++++++++++++++++++++ src/fexpr/mod.rs | 25 +++++++++ 4 files changed, 166 insertions(+) create mode 100644 build.rs create mode 100644 src/fexpr/grammar.lalrpop create mode 100644 src/fexpr/mod.rs diff --git a/Cargo.toml b/Cargo.toml index db1e9c5..2d64d51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dirs = "2.0" log = { version = "0.4", features = ["std"] } +thiserror = "1.0" +lalrpop-util = "0.18" + +[build-dependencies] +lalrpop = { version = "0.18", features = ["lexer"] } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..23c7d3f --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +extern crate lalrpop; + +fn main() { + lalrpop::process_root().unwrap(); +} diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop new file mode 100644 index 0000000..cb16153 --- /dev/null +++ b/src/fexpr/grammar.lalrpop @@ -0,0 +1,131 @@ +use super::{ + FError, + FightOutcome, + filters, + SearchField, + Weekday, +}; +use evtclib::statistics::gamedata::Boss; +use std::collections::HashSet; +use lalrpop_util::ParseError; + +use chrono::NaiveDateTime; +use regex::Regex; + +grammar; + +extern { + type Error = FError; +} + +pub LogFilter: Box = { + Disjunction, +} + +PlayerFilter: Box = { + Disjunction, +} + +Disjunction: T = { + > "or" > => a | b, + Conjunction, +} + +Conjunction: T = { + > "and"? > => a & b, + Negation, +} + +Negation: T = { + "not" => ! <>, + "!" => ! <>, + T, +} + +LogPredicate: Box = { + "-success" => filters::log::OutcomeFilter::success(), + "-wipe" => filters::log::OutcomeFilter::wipe(), + "-outcome" > => filters::log::OutcomeFilter::new(<>), + + "-weekday" > => filters::log::WeekdayFilter::new(<>), + "-before" => filters::log::TimeFilter::new(None, Some(<>)), + "-after" => filters::log::TimeFilter::new(Some(<>), None), + + "-boss" > => filters::log::BossFilter::new(<>), + + "all" "(" "player" ":" ")" => filters::player::all(<>), + "any" "(" "player" ":" ")" => filters::player::any(<>), + "exists" "(" "player" ":" ")" => filters::player::any(<>), + + "(" ")", +} + +PlayerPredicate: Box = { + "-character" => filters::player::NameFilter::new(SearchField::Character, <>), + "-account" => filters::player::NameFilter::new(SearchField::Account, <>), + "-name" => + filters::player::NameFilter::new(SearchField::Account, <>.clone()) + | filters::player::NameFilter::new(SearchField::Character, <>), + + "(" ")", +} + +Regex: Regex = { + =>? Regex::new(&s[1..s.len() - 1]).map_err(|_| ParseError::User { + error: FError::InvalidRegex(s.into()), + }), + =>? Regex::new(s).map_err(|e| ParseError::User { + error: FError::InvalidRegex(s.into()), + }), +} + +FightOutcome: FightOutcome = { + =>? <>.parse().map_err(|_| ParseError::User { + error: FError::InvalidFightOutcome(<>.into()), + }), +} + +Weekday: Weekday = { + =>? <>.parse().map_err(|_| ParseError::User { + error: FError::InvalidWeekday(<>.into()), + }), +} + +Boss: Boss = { + =>? <>.parse().map_err(|_| ParseError::User { + error: FError::InvalidBoss(<>.into()), + }), +} + +Date: NaiveDateTime = { + =>? NaiveDateTime::parse_from_str(<>, "%Y-%m-%d %H:%M:%S") + .map_err(|_| ParseError::User { + error: FError::InvalidTimestamp(<>.into()), + }), + =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", <>), "%Y-%m-%d %H:%M:%S") + .map_err(|_| ParseError::User { + error: FError::InvalidTimestamp(<>.into()), + }), +} + +Comma: HashSet = { + ",")*> => { + let mut result = v.into_iter().collect::>(); + result.insert(e); + result + }, +} + +match { + "player" => "player", + "not" => "not", + "any" => "any", + "all" => "all", + "exists" => "exists", + + r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime, + r"\d\d\d\d-\d\d-\d\d" => date, + r"\w+" => word, + + _ +} diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs new file mode 100644 index 0000000..f2b1090 --- /dev/null +++ b/src/fexpr/mod.rs @@ -0,0 +1,25 @@ +//! Filter expression language. +//! +//! This module contains methods to parse a given string into an abstract filter tree, check its +//! type and convert it to a [`Filter`][super::filters::Filter]. +// Make it available in the grammar mod. +use super::{filters, FightOutcome, SearchField, Weekday}; +use lalrpop_util::lalrpop_mod; + +use thiserror::Error; + +lalrpop_mod!(pub grammar, "/fexpr/grammar.rs"); + +#[derive(Debug, Error)] +pub enum FError { + #[error("invalid regular expression: {0}")] + InvalidRegex(String), + #[error("invalid fight outcome: {0}")] + InvalidFightOutcome(String), + #[error("invalid weekday: {0}")] + InvalidWeekday(String), + #[error("invalid timestamp: {0}")] + InvalidTimestamp(String), + #[error("invalid boss name: {0}")] + InvalidBoss(String), +} -- cgit v1.2.3 From 569c17607297dbbd57462a9603861d9fe619fd2d Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2020 13:47:00 +0200 Subject: Add -player as a shortcut to search player names --- src/fexpr/grammar.lalrpop | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index cb16153..48349a1 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -53,6 +53,11 @@ LogPredicate: Box = { "-boss" > => filters::log::BossFilter::new(<>), + "-player" => filters::player::any( + filters::player::NameFilter::new(SearchField::Character, <>.clone()) + | filters::player::NameFilter::new(SearchField::Account, <>) + ), + "all" "(" "player" ":" ")" => filters::player::all(<>), "any" "(" "player" ":" ")" => filters::player::any(<>), "exists" "(" "player" ":" ")" => filters::player::any(<>), -- cgit v1.2.3 From 0e4e148a0890ba206df40cffe5a5f1cc47c8079e Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Apr 2020 14:27:42 +0200 Subject: hook up new expression parser to command line args This method is not perfect yet, because 1. The items are not documented as they were before 2. You need to separate the args with --, otherwise Clap tries to parse them as optional flags This should be fixed (especially the documentation part) before merging into master. --- src/csl.rs | 132 ------------------------------------------------------- src/fexpr/mod.rs | 8 +++- src/main.rs | 124 +++++++++++---------------------------------------- 3 files changed, 32 insertions(+), 232 deletions(-) delete mode 100644 src/csl.rs diff --git a/src/csl.rs b/src/csl.rs deleted file mode 100644 index ac20ada..0000000 --- a/src/csl.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::collections::HashSet; -use std::fmt; -use std::hash::Hash; -use std::str::FromStr; - -use super::{FightOutcome, SearchField}; -use chrono::Weekday; -use evtclib::statistics::gamedata::Boss; - -pub trait Variants: Copy { - type Output: Iterator; - fn variants() -> Self::Output; -} - -macro_rules! variants { - ($target:ident => $($var:ident),+) => { - impl Variants for $target { - type Output = ::std::iter::Cloned<::std::slice::Iter<'static, Self>>; - fn variants() -> Self::Output { - // Exhaustiveness check - #[allow(dead_code)] - fn exhaustiveness_check(value: $target) { - match value { - $($target :: $var => ()),+ - } - } - // Actual result - [ - $($target :: $var),+ - ].iter().cloned() - } - } - }; - ($target:ident => $($var:ident,)+) => { - variants!($target => $($var),+); - }; -} - -variants! { SearchField => Account, Character, Guild } -variants! { FightOutcome => Success, Wipe } -variants! { Weekday => Mon, Tue, Wed, Thu, Fri, Sat, Sun } -variants! { Boss => - ValeGuardian, Gorseval, Sabetha, - Slothasor, Matthias, - KeepConstruct, Xera, - Cairn, MursaatOverseer, Samarog, Deimos, - SoullessHorror, Dhuum, - ConjuredAmalgamate, LargosTwins, Qadim, - CardinalAdina, CardinalSabir, QadimThePeerless, - - IcebroodConstruct, VoiceOfTheFallen, FraenirOfJormag, Boneskinner, WhisperOfJormag, - - Skorvald, Artsariiv, Arkk, - MAMA, Siax, Ensolyss, -} - -/// The character that delimits items from each other. -const DELIMITER: char = ','; -/// The character that negates the result. -const NEGATOR: char = '!'; - -/// A list that is given as comma-separated values. -#[derive(Debug, Clone)] -pub struct CommaSeparatedList { - values: HashSet, -} - -#[derive(Debug, Clone)] -pub enum ParseError { - Underlying(E), -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ParseError::Underlying(ref e) => e.fmt(f), - } - } -} - -impl FromStr for CommaSeparatedList -where - T: FromStr + Variants + Hash + Eq + fmt::Debug, -{ - type Err = ParseError; - - fn from_str(input: &str) -> Result { - if input == "*" { - Ok(CommaSeparatedList { - values: T::variants().collect(), - }) - } else if input.starts_with(NEGATOR) { - let no_csl = CommaSeparatedList::from_str(&input[1..])?; - let all_values = T::variants().collect::>(); - Ok(CommaSeparatedList { - values: all_values.difference(&no_csl.values).cloned().collect(), - }) - } else { - let parts = input.split(DELIMITER); - let values = parts - .map(FromStr::from_str) - .collect::, _>>() - .map_err(ParseError::Underlying)?; - Ok(CommaSeparatedList { values }) - } - } -} - -impl CommaSeparatedList -where - T: Hash + Eq + fmt::Debug, -{ - pub fn contains(&self, value: &T) -> bool { - self.values.contains(value) - } - - pub fn values(&self) -> &HashSet { - &self.values - } -} - -// We allow implicit hasher because then it's a zero-cost conversion, as we're just unwrapping the -// values. -#[allow(clippy::implicit_hasher)] -impl From> for HashSet -where - T: Hash + Eq + fmt::Debug, -{ - fn from(csl: CommaSeparatedList) -> Self { - csl.values - } -} diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index f2b1090..aafdea7 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -4,7 +4,7 @@ //! type and convert it to a [`Filter`][super::filters::Filter]. // Make it available in the grammar mod. use super::{filters, FightOutcome, SearchField, Weekday}; -use lalrpop_util::lalrpop_mod; +use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; @@ -23,3 +23,9 @@ pub enum FError { #[error("invalid boss name: {0}")] InvalidBoss(String), } + +pub fn parse_logfilter( + input: &str, +) -> Result, ParseError> { + grammar::LogFilterParser::new().parse(input) +} diff --git a/src/main.rs b/src/main.rs index bea03f6..8edd75a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,25 +5,22 @@ use std::io::{BufReader, Read, Seek}; use std::path::PathBuf; use std::str::FromStr; -use anyhow::{anyhow, Result}; -use chrono::{Duration, NaiveDateTime, Weekday}; +use anyhow::Result; +use chrono::{NaiveDateTime, Weekday}; use log::debug; use num_traits::cast::FromPrimitive; -use regex::Regex; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; use evtclib::{AgentKind, AgentName, EventKind, Log}; +mod fexpr; mod filters; use filters::{log::LogFilter, Inclusion}; mod guilds; mod logger; mod output; -mod csl; -use csl::CommaSeparatedList; - macro_rules! unwrap { ($p:pat = $e:expr => { $r:expr} ) => { if let $p = $e { @@ -43,18 +40,6 @@ pub struct Opt { #[structopt(short = "d", long = "dir", default_value = ".", parse(from_os_str))] path: PathBuf, - /// The fields which should be searched. - #[structopt(short = "f", long = "fields", default_value = "account,character")] - field: CommaSeparatedList, - - /// Only display fights with the given outcome. - #[structopt(short = "o", long = "outcome", default_value = "*")] - outcome: CommaSeparatedList, - - /// Invert the regular expression (show fights that do not match) - #[structopt(short = "v", long = "invert-match")] - invert: bool, - /// Only show the name of matching files. #[structopt(short = "l", long = "files-with-matches")] file_name_only: bool, @@ -63,35 +48,6 @@ pub struct Opt { #[structopt(long = "no-color")] no_color: bool, - /// Only show logs that are younger than the given time. - #[structopt( - short = "a", - long = "younger", - parse(try_from_str = parse_time_arg) - )] - after: Option, - - /// Only show logs that are older than the given time. - #[structopt( - short = "b", - long = "older", - parse(try_from_str = parse_time_arg) - )] - before: Option, - - /// Only show logs from the given weekdays. - #[structopt( - short = "w", - long = "weekdays", - default_value = "*", - parse(try_from_str = try_from_str_simple_error) - )] - weekdays: CommaSeparatedList, - - /// Only show logs from the given encounters. - #[structopt(short = "e", long = "bosses", default_value = "*")] - bosses: CommaSeparatedList, - /// Print more debugging information to stderr. #[structopt(long = "debug")] debug: bool, @@ -100,9 +56,8 @@ pub struct Opt { #[structopt(long = "guilds")] guilds: bool, - /// The regular expression to search for. - #[structopt(name = "EXPR")] - expression: Regex, + /// The filter expression. + expression: Vec, } /// A flag indicating which fields should be searched. @@ -180,26 +135,6 @@ impl FromStr for FightOutcome { } } -fn parse_time_arg(input: &str) -> Result { - if let Ok(duration) = humantime::parse_duration(input) { - let now = chrono::Local::now().naive_local(); - let chrono_dur = Duration::from_std(duration).expect("Duration out of range!"); - return Ok(now - chrono_dur); - } - if let Ok(time) = humantime::parse_rfc3339_weak(input) { - let timestamp = time - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - return Ok(NaiveDateTime::from_timestamp(timestamp as i64, 0)); - } - Err(anyhow!("unknown time format")) -} - -fn try_from_str_simple_error(input: &str) -> Result { - T::from_str(input).map_err(|_| format!("'{}' is an invalid value", input)) -} - enum ZipWrapper { Raw(Option), Zipped(zip::ZipArchive), @@ -223,6 +158,13 @@ impl ZipWrapper { } fn main() { + let result = run(); + if let Err(err) = result { + eprintln!("Error: {}", err); + } +} + +fn run() -> Result<()> { let opt = Opt::from_args(); if opt.no_color { @@ -239,17 +181,15 @@ fn main() { guilds::prepare_cache(); } - let result = grep(&opt); - match result { - Ok(_) => {} - Err(e) => { - eprintln!("Error: {}", e); - } - } + let filter = build_filter(&opt)?; + + grep(&opt, &*filter)?; if opt.guilds { guilds::save_cache(); } + + Ok(()) } /// Check if the given entry represents a log file, based on the file name. @@ -261,32 +201,18 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } -fn build_filter(opt: &Opt) -> Box { - let player_filter = opt - .field - .values() - .iter() - .map(|field| filters::player::NameFilter::new(*field, opt.expression.clone())) - .fold(filters::Const::new(false), |a, f| a | f); - - let mut filter = filters::player::any(player_filter); - if opt.invert { - filter = !filter; - } - - filter = filter - & filters::log::BossFilter::new(opt.bosses.values().clone()) - & filters::log::OutcomeFilter::new(opt.outcome.values().clone()) - & filters::log::WeekdayFilter::new(opt.weekdays.values().clone()) - & filters::log::TimeFilter::new(opt.after, opt.before); - - filter +fn build_filter(opt: &Opt) -> Result> { + // Our error needs access to the string, so we make our lives easier by just leaking it into a + // 'static lifetime. Otherwise we'd need to build this string in main() and pass it in. + // We're fine with the small memory leak, as we're only dealing with a small string in a + // short-lived program. + let expr_string = Box::leak(Box::new(opt.expression.join(" "))); + Ok(fexpr::parse_logfilter(expr_string)?) } /// Run the grep search with the given options. -fn grep(opt: &Opt) -> Result<()> { +fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { let pipeline = &output::build_pipeline(opt); - let filter: &dyn LogFilter = &*build_filter(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { -- cgit v1.2.3 From 185a5b2f802f9d05c3eb40f807c0488f168c6661 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 13:59:28 +0200 Subject: better error outputs --- src/fexpr/grammar.lalrpop | 62 +++++++++++++++++++++++++++++++++++------------ src/fexpr/mod.rs | 57 ++++++++++++++++++++++++++++++++++--------- src/filters/mod.rs | 2 +- src/main.rs | 45 +++++++++++++++++++++++++++++++--- 4 files changed, 135 insertions(+), 31 deletions(-) diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index 48349a1..4e6ac89 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -1,5 +1,6 @@ use super::{ FError, + FErrorKind, FightOutcome, filters, SearchField, @@ -76,40 +77,68 @@ PlayerPredicate: Box = { } Regex: Regex = { - =>? Regex::new(&s[1..s.len() - 1]).map_err(|_| ParseError::User { - error: FError::InvalidRegex(s.into()), + =>? Regex::new(&s[1..s.len() - 1]).map_err(|error| ParseError::User { + error: FError { + location: l, + data: s.to_string(), + kind: error.into(), + } }), - =>? Regex::new(s).map_err(|e| ParseError::User { - error: FError::InvalidRegex(s.into()), + =>? Regex::new(s).map_err(|error| ParseError::User { + error: FError { + location: l, + data: s.to_string(), + kind: error.into(), + } }), } FightOutcome: FightOutcome = { - =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidFightOutcome(<>.into()), + =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidFightOutcome, + } }), } Weekday: Weekday = { - =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidWeekday(<>.into()), + =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidWeekday, + } }), } Boss: Boss = { - =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidBoss(<>.into()), + =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidBoss, + } }), } Date: NaiveDateTime = { - =>? NaiveDateTime::parse_from_str(<>, "%Y-%m-%d %H:%M:%S") - .map_err(|_| ParseError::User { - error: FError::InvalidTimestamp(<>.into()), + =>? NaiveDateTime::parse_from_str(d, "%Y-%m-%d %H:%M:%S") + .map_err(|error| ParseError::User { + error: FError { + location: l, + data: d.into(), + kind: error.into(), + } }), - =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", <>), "%Y-%m-%d %H:%M:%S") - .map_err(|_| ParseError::User { - error: FError::InvalidTimestamp(<>.into()), + =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", d), "%Y-%m-%d %H:%M:%S") + .map_err(|error| ParseError::User { + error: FError { + location: l, + data: d.into(), + kind: error.into(), + } }), } @@ -131,6 +160,7 @@ match { r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime, r"\d\d\d\d-\d\d-\d\d" => date, r"\w+" => word, + r#""[^"]*""# => regex, _ } diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index aafdea7..5754d94 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -4,28 +4,63 @@ //! type and convert it to a [`Filter`][super::filters::Filter]. // Make it available in the grammar mod. use super::{filters, FightOutcome, SearchField, Weekday}; -use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; +use std::{error, fmt}; + +use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; -lalrpop_mod!(pub grammar, "/fexpr/grammar.rs"); +lalrpop_mod!(#[allow(clippy::all)] pub grammar, "/fexpr/grammar.rs"); + +#[derive(Debug)] +pub struct FError { + location: usize, + data: String, + kind: FErrorKind, +} + +impl fmt::Display for FError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} (at {})", self.kind, self.location) + } +} + +impl error::Error for FError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(&self.kind) + } +} #[derive(Debug, Error)] -pub enum FError { +pub enum FErrorKind { #[error("invalid regular expression: {0}")] - InvalidRegex(String), - #[error("invalid fight outcome: {0}")] - InvalidFightOutcome(String), - #[error("invalid weekday: {0}")] - InvalidWeekday(String), + InvalidRegex(#[from] regex::Error), + #[error("invalid fight outcome")] + InvalidFightOutcome, + #[error("invalid weekday")] + InvalidWeekday, #[error("invalid timestamp: {0}")] - InvalidTimestamp(String), - #[error("invalid boss name: {0}")] - InvalidBoss(String), + InvalidTimestamp(#[from] chrono::format::ParseError), + #[error("invalid boss name")] + InvalidBoss, } +/// Shortcut to create a new parser and parse the given input. pub fn parse_logfilter( input: &str, ) -> Result, ParseError> { grammar::LogFilterParser::new().parse(input) } + +/// Extract the location from the given error. +pub fn location(err: &ParseError) -> usize { + match *err { + ParseError::InvalidToken { location } => location, + ParseError::UnrecognizedEOF { location, .. } => location, + ParseError::UnrecognizedToken { + token: (l, _, _), .. + } => l, + ParseError::ExtraToken { token: (l, _, _) } => l, + ParseError::User { ref error } => error.location, + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 525ff27..3d0868b 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -1,5 +1,5 @@ #![allow(clippy::new_ret_no_self)] -use std::{ops, fmt}; +use std::{fmt, ops}; use num_derive::FromPrimitive; use num_traits::FromPrimitive as _; diff --git a/src/main.rs b/src/main.rs index 8edd75a..2e0c82f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ #![feature(trait_alias)] use std::collections::HashMap; +use std::fmt; use std::fs::File; use std::io::{BufReader, Read, Seek}; use std::path::PathBuf; use std::str::FromStr; -use anyhow::Result; +use anyhow::{anyhow, Error, Result}; use chrono::{NaiveDateTime, Weekday}; +use colored::Colorize; use log::debug; use num_traits::cast::FromPrimitive; use structopt::StructOpt; @@ -157,10 +159,38 @@ impl ZipWrapper { } } +#[derive(Clone, Debug)] +struct InputError { + line: String, + location: usize, + msg: String, +} + +impl fmt::Display for InputError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let prefix = "Input:"; + writeln!(f, "{} {}", prefix.yellow(), self.line)?; + let prefix_len = prefix.len() + self.location; + writeln!(f, "{}{}", " ".repeat(prefix_len), " ^-".red())?; + write!(f, "{}: {}", "Error".red(), self.msg)?; + Ok(()) + } +} + +impl std::error::Error for InputError {} + fn main() { let result = run(); if let Err(err) = result { - eprintln!("Error: {}", err); + display_error(&err); + } +} + +fn display_error(err: &Error) { + if let Some(err) = err.downcast_ref::() { + eprintln!("{}", err); + } else { + eprintln!("{}: {}", "Error".red(), err); } } @@ -207,7 +237,16 @@ fn build_filter(opt: &Opt) -> Result> { // We're fine with the small memory leak, as we're only dealing with a small string in a // short-lived program. let expr_string = Box::leak(Box::new(opt.expression.join(" "))); - Ok(fexpr::parse_logfilter(expr_string)?) + if expr_string.trim().is_empty() { + return Err(anyhow!("Expected a filter to be given")); + } + Ok( + fexpr::parse_logfilter(expr_string).map_err(|error| InputError { + line: expr_string.to_string(), + location: fexpr::location(&error), + msg: error.to_string(), + })?, + ) } /// Run the grep search with the given options. -- cgit v1.2.3 From d1f277892ec127b1fb83ad56de59b29c32695661 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:03:13 +0200 Subject: add "or" and "and" to the list of tokens Otherwise they'd get tokenized as word and we couldn't build conjunctions/disjunctions. --- src/fexpr/grammar.lalrpop | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index 4e6ac89..f559ff1 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -153,6 +153,8 @@ Comma: HashSet = { match { "player" => "player", "not" => "not", + "or" => "or", + "and" => "and", "any" => "any", "all" => "all", "exists" => "exists", -- cgit v1.2.3 From 0ad7a333dc2b45f0ba658ea455284d086294a088 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:06:15 +0200 Subject: grammar: fix precendence rules If we don't allow the higher-tier on the left side, we cannot chain multiple or/and on the same level. Since or is associative, we shouldn't expect the user to write (... or (... or ...)) and instead provide the flattened version as well. --- src/fexpr/grammar.lalrpop | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index f559ff1..caaaf7f 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -28,18 +28,18 @@ PlayerFilter: Box = { } Disjunction: T = { - > "or" > => a | b, + > "or" > => a | b, Conjunction, } Conjunction: T = { - > "and"? > => a & b, + > "and"? > => a & b, Negation, } Negation: T = { - "not" => ! <>, - "!" => ! <>, + "not" > => ! <>, + "!" > => ! <>, T, } -- cgit v1.2.3 From 0a27adbc0bf3bbbf87fea9e55c00c38f61d55058 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:23:50 +0200 Subject: add a small repl --- src/fexpr/mod.rs | 6 +++--- src/main.rs | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index 5754d94..5610aba 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -46,9 +46,9 @@ pub enum FErrorKind { } /// Shortcut to create a new parser and parse the given input. -pub fn parse_logfilter( - input: &str, -) -> Result, ParseError> { +pub fn parse_logfilter<'a>( + input: &'a str, +) -> Result, ParseError, FError>> { grammar::LogFilterParser::new().parse(input) } diff --git a/src/main.rs b/src/main.rs index 2e0c82f..ff882e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::fs::File; -use std::io::{BufReader, Read, Seek}; +use std::io::{BufReader, Read, Seek, Write}; use std::path::PathBuf; use std::str::FromStr; @@ -58,6 +58,10 @@ pub struct Opt { #[structopt(long = "guilds")] guilds: bool, + /// Run the REPL. + #[structopt(long)] + repl: bool, + /// The filter expression. expression: Vec, } @@ -211,9 +215,13 @@ fn run() -> Result<()> { guilds::prepare_cache(); } - let filter = build_filter(&opt)?; - - grep(&opt, &*filter)?; + if !opt.repl { + let expr_string = opt.expression.join(" "); + let filter = build_filter(&expr_string)?; + grep(&opt, &*filter)?; + } else { + repl(&opt)?; + } if opt.guilds { guilds::save_cache(); @@ -222,6 +230,22 @@ fn run() -> Result<()> { Ok(()) } +fn repl(opt: &Opt) -> Result<()> { + let stdin = std::io::stdin(); + loop { + print!("Query> "); + std::io::stdout().flush()?; + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + let parsed = build_filter(&line); + match parsed { + Ok(filter) => grep(&opt, &*filter)?, + Err(err) => display_error(&err.into()), + } + } +} + /// Check if the given entry represents a log file, based on the file name. fn is_log_file(entry: &DirEntry) -> bool { entry @@ -231,12 +255,8 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } -fn build_filter(opt: &Opt) -> Result> { - // Our error needs access to the string, so we make our lives easier by just leaking it into a - // 'static lifetime. Otherwise we'd need to build this string in main() and pass it in. - // We're fine with the small memory leak, as we're only dealing with a small string in a - // short-lived program. - let expr_string = Box::leak(Box::new(opt.expression.join(" "))); +/// Small wrapper around `fexpr::parse_logfilter` to convert the returned `Err` to be `'static'. +fn build_filter(expr_string: &str) -> Result> { if expr_string.trim().is_empty() { return Err(anyhow!("Expected a filter to be given")); } -- cgit v1.2.3 From ab909d2f3a0a59ae1b9a169ec79d4e9ffeddd1e1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:34:08 +0200 Subject: use readline/rustyline instead of stdin.read_line This gives us a history, nicer editing capabilities and the possibility to add completion in the future. --- Cargo.toml | 1 + src/main.rs | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d64d51..2825776 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ dirs = "2.0" log = { version = "0.4", features = ["std"] } thiserror = "1.0" lalrpop-util = "0.18" +rustyline = "6.1" [build-dependencies] lalrpop = { version = "0.18", features = ["lexer"] } diff --git a/src/main.rs b/src/main.rs index ff882e5..b6633ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use chrono::{NaiveDateTime, Weekday}; use colored::Colorize; use log::debug; use num_traits::cast::FromPrimitive; +use rustyline::{error::ReadlineError, Editor}; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; @@ -231,13 +232,10 @@ fn run() -> Result<()> { } fn repl(opt: &Opt) -> Result<()> { - let stdin = std::io::stdin(); + let mut rl = Editor::<()>::new(); loop { - print!("Query> "); - std::io::stdout().flush()?; - let mut line = String::new(); - stdin.read_line(&mut line)?; - let line = line.trim(); + let line = rl.readline("Query> ")?; + rl.add_history_entry(&line); let parsed = build_filter(&line); match parsed { Ok(filter) => grep(&opt, &*filter)?, -- cgit v1.2.3 From efbc3fb2131b8c69a88e0622ad4b120c2c48ed85 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 15:04:53 +0200 Subject: add predicate documentation to the help text Sadly, structopt always displays this, despite the documentation stating that it should be hidden when the user uses -h (instead of --help). It seems like this is a bug in clap, which might get fixed with clap 3.0. --- src/main.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index b6633ac..ab66848 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,10 +34,29 @@ macro_rules! unwrap { }; } -/// A program that allows you to search through all your evtc logs for specific -/// people. +/// A program that allows you to search through all your evtc logs for specific people. +/// +/// raidgrep supports different predicates that determine whether a log is included or not. +/// Predicates start with a - and optionally take an argument. Predicates can be combined with +/// "and", "or" and "not", and predicates that operate on single players (instead of logs) have to +/// be within an "all(player: ...)" or "any(player: ...)" construct. +/// +/// PREDICATES: +/// +/// -character REGEX True if the character name matches the regex. +/// -account REGEX True if the account name matches the regex. +/// -name REGEX True if either character or account name match. +/// +/// -success Only include successful logs. +/// -wipe Only include failed logs. +/// -outcome OUTCOMES Only include logs with the given outcomes. +/// -weekday WEEKDAYS Only include logs from the given weekdays. +/// -before DATE Only include logs from before the given date. +/// -after DATE Only include logs from after the given date. +/// -boss BOSSES Only include logs from the given bosses. +/// -player REGEX Shorthand to check if any player in the log has the given name. #[derive(StructOpt, Debug)] -#[structopt(name = "raidgrep")] +#[structopt(verbatim_doc_comment)] pub struct Opt { /// Path to the folder with logs. #[structopt(short = "d", long = "dir", default_value = ".", parse(from_os_str))] @@ -56,14 +75,20 @@ pub struct Opt { debug: bool, /// Load guild information from the API. + /// + /// Loading guild information requires network access and slows down the program considerably, + /// so this is disabled by default. #[structopt(long = "guilds")] guilds: bool, /// Run the REPL. + /// + /// The REPL will allow you to keep entering queries which are being searched by raidgrep, + /// until you manually exit with Crtl+C. #[structopt(long)] repl: bool, - /// The filter expression. + /// The filter expression, see PREDICATES for more information. expression: Vec, } -- cgit v1.2.3 From fe88f503676091c53d31db99ca4af36fe08dcdc8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 15:07:41 +0200 Subject: remove unused imports --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index ab66848..c89614b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::fs::File; -use std::io::{BufReader, Read, Seek, Write}; +use std::io::{BufReader, Read, Seek}; use std::path::PathBuf; use std::str::FromStr; @@ -11,7 +11,7 @@ use chrono::{NaiveDateTime, Weekday}; use colored::Colorize; use log::debug; use num_traits::cast::FromPrimitive; -use rustyline::{error::ReadlineError, Editor}; +use rustyline::Editor; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; -- cgit v1.2.3 From 675d0aadf1495a7ac752789c125e124db5152b43 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 12:49:27 +0200 Subject: better CLI args/parser integration First, this commit adds a shortcut if only a single argument is given that can be parsed as a regex. This is to retain the old behaviour of "raidgrep NAME" just working, without needing to specify -player or anything. Secondly, this also re-wraps arguments with spaces (unless there's only one argument in total). This means that the following should work: raidgrep -- -player "Godric Gobbledygook" instead of either raidgrep -- '-player "Godric Gobbledygook"' raidgrep -- -player '"Godric Gobbledygook"' (notice the extra quotes). --- src/main.rs | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index c89614b..e18a109 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,10 @@ use std::str::FromStr; use anyhow::{anyhow, Error, Result}; use chrono::{NaiveDateTime, Weekday}; use colored::Colorize; +use itertools::Itertools; use log::debug; use num_traits::cast::FromPrimitive; +use regex::Regex; use rustyline::Editor; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; @@ -242,9 +244,7 @@ fn run() -> Result<()> { } if !opt.repl { - let expr_string = opt.expression.join(" "); - let filter = build_filter(&expr_string)?; - grep(&opt, &*filter)?; + single(&opt)?; } else { repl(&opt)?; } @@ -256,6 +256,41 @@ fn run() -> Result<()> { Ok(()) } +fn single(opt: &Opt) -> Result<()> { + // As a shortcut, we allow only the regular expression to be given, to retain the behaviour + // before the filter changes. + if opt.expression.len() == 1 { + let line = &opt.expression[0]; + let maybe_filter = build_filter(line); + if let Err(_) = maybe_filter { + let maybe_regex = Regex::new(line); + if let Ok(rgx) = maybe_regex { + let filter = filters::player::any( + filters::player::NameFilter::new(SearchField::Account, rgx.clone()) + | filters::player::NameFilter::new(SearchField::Character, rgx), + ); + return grep(opt, &*filter); + } + } + return grep(opt, &*maybe_filter?); + } + + let expr_string = opt + .expression + .iter() + .map(|part| { + if part.contains(' ') { + format!(r#""{}""#, part) + } else { + part.into() + } + }) + .join(" "); + let filter = build_filter(&expr_string)?; + grep(&opt, &*filter)?; + Ok(()) +} + fn repl(opt: &Opt) -> Result<()> { let mut rl = Editor::<()>::new(); loop { -- cgit v1.2.3 From d124265bee159193090f085343b8523bc6387620 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 12:53:54 +0200 Subject: cosmetic fixes --- src/main.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index e18a109..66b2880 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,18 +45,18 @@ macro_rules! unwrap { /// /// PREDICATES: /// -/// -character REGEX True if the character name matches the regex. -/// -account REGEX True if the account name matches the regex. -/// -name REGEX True if either character or account name match. +/// -character REGEX True if the character name matches the regex. +/// -account REGEX True if the account name matches the regex. +/// -name REGEX True if either character or account name match. /// -/// -success Only include successful logs. -/// -wipe Only include failed logs. -/// -outcome OUTCOMES Only include logs with the given outcomes. -/// -weekday WEEKDAYS Only include logs from the given weekdays. -/// -before DATE Only include logs from before the given date. -/// -after DATE Only include logs from after the given date. -/// -boss BOSSES Only include logs from the given bosses. -/// -player REGEX Shorthand to check if any player in the log has the given name. +/// -success Only include successful logs. +/// -wipe Only include failed logs. +/// -outcome OUTCOMES Only include logs with the given outcomes. +/// -weekday WEEKDAYS Only include logs from the given weekdays. +/// -before DATE Only include logs from before the given date. +/// -after DATE Only include logs from after the given date. +/// -boss BOSSES Only include logs from the given bosses. +/// -player REGEX Shorthand to check if any player in the log has the given name. #[derive(StructOpt, Debug)] #[structopt(verbatim_doc_comment)] pub struct Opt { @@ -262,7 +262,7 @@ fn single(opt: &Opt) -> Result<()> { if opt.expression.len() == 1 { let line = &opt.expression[0]; let maybe_filter = build_filter(line); - if let Err(_) = maybe_filter { + if maybe_filter.is_err() { let maybe_regex = Regex::new(line); if let Ok(rgx) = maybe_regex { let filter = filters::player::any( @@ -299,7 +299,7 @@ fn repl(opt: &Opt) -> Result<()> { let parsed = build_filter(&line); match parsed { Ok(filter) => grep(&opt, &*filter)?, - Err(err) => display_error(&err.into()), + Err(err) => display_error(&err), } } } -- cgit v1.2.3 From 509e5817e6e035e762840c00fb95b18253b1d269 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 12:56:47 +0200 Subject: only try regex if word doesn't start with - Since our predicates start with -, this sounds like a good heuristic to prevent something like "raidgrep -- -player" from silently succeeding but not doing what the user had intended. In this case, we want the parse error to show and not treat "-player" as a regex. --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 66b2880..3164bd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -262,7 +262,7 @@ fn single(opt: &Opt) -> Result<()> { if opt.expression.len() == 1 { let line = &opt.expression[0]; let maybe_filter = build_filter(line); - if maybe_filter.is_err() { + if maybe_filter.is_err() && !line.starts_with('-') { let maybe_regex = Regex::new(line); if let Ok(rgx) = maybe_regex { let filter = filters::player::any( -- cgit v1.2.3 From 5dbea93266c3a30dac5ec6f5a7915d73a440f573 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 13:14:30 +0200 Subject: use free functions instead of Filter::new Having a ::new on each of the filter types was a bit weird, especially because we returned Box instead of Self (and clippy rightfully complained). With this patch, we now have a bunch of normal functions, and we don't show to the outside how a filter is actually implemented (or what struct is behind it). --- src/fexpr/grammar.lalrpop | 26 ++++++------- src/filters/log.rs | 99 +++++++++++++++++++++++++++++------------------ src/filters/mod.rs | 14 +++---- src/filters/player.rs | 23 +++++++---- src/main.rs | 3 +- 5 files changed, 97 insertions(+), 68 deletions(-) diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index caaaf7f..d8f64fa 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -44,19 +44,19 @@ Negation: T = { } LogPredicate: Box = { - "-success" => filters::log::OutcomeFilter::success(), - "-wipe" => filters::log::OutcomeFilter::wipe(), - "-outcome" > => filters::log::OutcomeFilter::new(<>), + "-success" => filters::log::success(), + "-wipe" => filters::log::wipe(), + "-outcome" > => filters::log::outcome(<>), - "-weekday" > => filters::log::WeekdayFilter::new(<>), - "-before" => filters::log::TimeFilter::new(None, Some(<>)), - "-after" => filters::log::TimeFilter::new(Some(<>), None), + "-weekday" > => filters::log::weekday(<>), + "-before" => filters::log::before(<>), + "-after" => filters::log::after(<>), - "-boss" > => filters::log::BossFilter::new(<>), + "-boss" > => filters::log::boss(<>), "-player" => filters::player::any( - filters::player::NameFilter::new(SearchField::Character, <>.clone()) - | filters::player::NameFilter::new(SearchField::Account, <>) + filters::player::character(<>.clone()) + | filters::player::account(<>) ), "all" "(" "player" ":" ")" => filters::player::all(<>), @@ -67,11 +67,11 @@ LogPredicate: Box = { } PlayerPredicate: Box = { - "-character" => filters::player::NameFilter::new(SearchField::Character, <>), - "-account" => filters::player::NameFilter::new(SearchField::Account, <>), + "-character" => filters::player::character(<>), + "-account" => filters::player::account(<>), "-name" => - filters::player::NameFilter::new(SearchField::Account, <>.clone()) - | filters::player::NameFilter::new(SearchField::Character, <>), + filters::player::account(<>.clone()) + | filters::player::character(<>), "(" ")", } diff --git a/src/filters/log.rs b/src/filters/log.rs index 8b84bf3..8d4e0b5 100644 --- a/src/filters/log.rs +++ b/src/filters/log.rs @@ -19,13 +19,7 @@ use num_traits::FromPrimitive as _; pub trait LogFilter = Filter; #[derive(Debug, Clone)] -pub struct BossFilter(HashSet); - -impl BossFilter { - pub fn new(bosses: HashSet) -> Box { - Box::new(BossFilter(bosses)) - } -} +struct BossFilter(HashSet); impl Filter for BossFilter { fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { @@ -40,26 +34,14 @@ impl Filter for BossFilter { } } -#[derive(Debug, Clone)] -pub struct OutcomeFilter(HashSet); - -impl OutcomeFilter { - pub fn new(outcomes: HashSet) -> Box { - Box::new(OutcomeFilter(outcomes)) - } +/// A `LogFilter` that only accepts logs with one of the given bosses. +pub fn boss(bosses: HashSet) -> Box { + Box::new(BossFilter(bosses)) +} - pub fn success() -> Box { - let mut outcomes = HashSet::new(); - outcomes.insert(FightOutcome::Success); - Self::new(outcomes) - } - pub fn wipe() -> Box { - let mut outcomes = HashSet::new(); - outcomes.insert(FightOutcome::Wipe); - Self::new(outcomes) - } -} +#[derive(Debug, Clone)] +struct OutcomeFilter(HashSet); impl Filter for OutcomeFilter { fn filter(&self, log: &LogResult) -> bool { @@ -67,30 +49,49 @@ impl Filter for OutcomeFilter { } } -#[derive(Debug, Clone)] -pub struct WeekdayFilter(HashSet); +/// A `LogFilter` that only accepts logs with one of the given outcomes. +/// +/// See also [`success`][success] and [`wipe`][wipe]. +pub fn outcome(outcomes: HashSet) -> Box { + Box::new(OutcomeFilter(outcomes)) +} -impl WeekdayFilter { - pub fn new(weekdays: HashSet) -> Box { - Box::new(WeekdayFilter(weekdays)) - } +/// A `LogFilter` that only accepts successful logs. +/// +/// See also [`outcome`][outcome] and [`wipe`][wipe]. +pub fn success() -> Box { + let mut outcomes = HashSet::new(); + outcomes.insert(FightOutcome::Success); + outcome(outcomes) +} + +/// A `LogFilter` that only accepts failed logs. +/// +/// See also [`outcome`][outcome] and [`success`][wipe]. +pub fn wipe() -> Box { + let mut outcomes = HashSet::new(); + outcomes.insert(FightOutcome::Wipe); + outcome(outcomes) } +#[derive(Debug, Clone)] +struct WeekdayFilter(HashSet); + impl Filter for WeekdayFilter { fn filter(&self, log: &LogResult) -> bool { self.0.contains(&log.time.weekday()) } } -#[derive(Debug, Clone)] -pub struct TimeFilter(Option, Option); - -impl TimeFilter { - pub fn new(after: Option, before: Option) -> Box { - Box::new(TimeFilter(after, before)) - } +/// A `LogFilter` that only accepts logs if they were done on one of the given weekdays. +pub fn weekday(weekdays: HashSet) -> Box { + Box::new(WeekdayFilter(weekdays)) } + +#[derive(Debug, Clone)] +struct TimeFilter(Option, Option); + impl Filter for TimeFilter { fn filter(&self, log: &LogResult) -> bool { let after_ok = match self.0 { @@ -105,3 +106,25 @@ impl Filter for TimeFilter { after_ok && before_ok } } + +/// A `LogFilter` that only accepts logs in the given time frame. +/// +/// If a bound is not given, -Infinity is assumed for the lower bound, and Infinity for the upper +/// bound. +pub fn time(lower: Option, upper: Option) -> Box { + Box::new(TimeFilter(lower, upper)) +} + +/// A `LogFilter` that only accepts logs after the given date. +/// +/// Also see [`time`][time] and [`before`][before]. +pub fn after(when: NaiveDateTime) -> Box { + time(Some(when), None) +} + +/// A `LogFilter` that only accepts logs before the given date. +/// +/// Also see [`time`][time] and [`after`][after]. +pub fn before(when: NaiveDateTime) -> Box { + time(None, Some(when)) +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 3d0868b..162b6f8 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -1,4 +1,3 @@ -#![allow(clippy::new_ret_no_self)] use std::{fmt, ops}; use num_derive::FromPrimitive; @@ -74,13 +73,7 @@ pub trait Filter: Send + Sync + fmt::Debug { } #[derive(Debug, Clone, Copy)] -pub struct Const(pub bool); - -impl Const { - pub fn new(output: bool) -> Box> { - Box::new(Const(output)) - } -} +struct Const(pub bool); impl Filter for Const { fn filter_early(&self, _: &E) -> Inclusion { @@ -92,6 +85,11 @@ impl Filter for Const { } } +/// Construct a `Filter` that always returns a fixed value. +pub fn constant(output: bool) -> Box> { + Box::new(Const(output)) +} + struct AndFilter(Box>, Box>); impl Filter for AndFilter { diff --git a/src/filters/player.rs b/src/filters/player.rs index 8f9196a..4daeb22 100644 --- a/src/filters/player.rs +++ b/src/filters/player.rs @@ -64,13 +64,7 @@ pub fn any(player_filter: Box) -> Box { /// /// The given SearchField determines in which field something should be searched. #[derive(Debug, Clone)] -pub struct NameFilter(SearchField, Regex); - -impl NameFilter { - pub fn new(field: SearchField, regex: Regex) -> Box { - Box::new(NameFilter(field, regex)) - } -} +struct NameFilter(SearchField, Regex); impl Filter for NameFilter { fn filter_early(&self, agent: &Agent) -> Inclusion { @@ -108,3 +102,18 @@ impl Filter for NameFilter { } } } + +/// Construct a `PlayerFilter` that searches the given `field` with the given `regex`. +pub fn name(field: SearchField, regex: Regex) -> Box { + Box::new(NameFilter(field, regex)) +} + +/// Construct a `PlayerFilter` that searches the character name with the given `regex`. +pub fn character(regex: Regex) -> Box { + name(SearchField::Character, regex) +} + +/// Construct a `PlayerFilter` that searches the account name with the given `regex`. +pub fn account(regex: Regex) -> Box { + name(SearchField::Account, regex) +} diff --git a/src/main.rs b/src/main.rs index 3164bd4..50dcf8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -266,8 +266,7 @@ fn single(opt: &Opt) -> Result<()> { let maybe_regex = Regex::new(line); if let Ok(rgx) = maybe_regex { let filter = filters::player::any( - filters::player::NameFilter::new(SearchField::Account, rgx.clone()) - | filters::player::NameFilter::new(SearchField::Character, rgx), + filters::player::account(rgx.clone()) | filters::player::character(rgx), ); return grep(opt, &*filter); } -- cgit v1.2.3 From 9bbd5db2a6caae10f0ab2cf2625fbc34485a4ce9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 13:22:00 +0200 Subject: add -include and -exclude --- src/fexpr/grammar.lalrpop | 3 +++ src/main.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index d8f64fa..58ec052 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -54,6 +54,9 @@ LogPredicate: Box = { "-boss" > => filters::log::boss(<>), + "-include" => filters::constant(true), + "-exclude" => filters::constant(false), + "-player" => filters::player::any( filters::player::character(<>.clone()) | filters::player::account(<>) diff --git a/src/main.rs b/src/main.rs index 50dcf8d..231fbdc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,8 @@ macro_rules! unwrap { /// -after DATE Only include logs from after the given date. /// -boss BOSSES Only include logs from the given bosses. /// -player REGEX Shorthand to check if any player in the log has the given name. +/// -include Always evaluates to including the log. +/// -exclude Always evaluates to excluding the log. #[derive(StructOpt, Debug)] #[structopt(verbatim_doc_comment)] pub struct Opt { -- cgit v1.2.3