diff options
-rw-r--r-- | src/csl.rs | 132 | ||||
-rw-r--r-- | src/fexpr/mod.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 124 |
3 files changed, 32 insertions, 232 deletions
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<Item = Self>; - 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<T: Eq + Hash + fmt::Debug> { - values: HashSet<T>, -} - -#[derive(Debug, Clone)] -pub enum ParseError<E> { - Underlying(E), -} - -impl<E: fmt::Display> fmt::Display for ParseError<E> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ParseError::Underlying(ref e) => e.fmt(f), - } - } -} - -impl<T> FromStr for CommaSeparatedList<T> -where - T: FromStr + Variants + Hash + Eq + fmt::Debug, -{ - type Err = ParseError<T::Err>; - - fn from_str(input: &str) -> Result<Self, Self::Err> { - 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::<HashSet<_>>(); - 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::<Result<HashSet<_>, _>>() - .map_err(ParseError::Underlying)?; - Ok(CommaSeparatedList { values }) - } - } -} - -impl<T> CommaSeparatedList<T> -where - T: Hash + Eq + fmt::Debug, -{ - pub fn contains(&self, value: &T) -> bool { - self.values.contains(value) - } - - pub fn values(&self) -> &HashSet<T> { - &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<T> From<CommaSeparatedList<T>> for HashSet<T> -where - T: Hash + Eq + fmt::Debug, -{ - fn from(csl: CommaSeparatedList<T>) -> 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<Box<dyn filters::log::LogFilter>, ParseError<usize, Token, FError>> { + 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<SearchField>, - - /// Only display fights with the given outcome. - #[structopt(short = "o", long = "outcome", default_value = "*")] - outcome: CommaSeparatedList<FightOutcome>, - - /// 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<NaiveDateTime>, - - /// Only show logs that are older than the given time. - #[structopt( - short = "b", - long = "older", - parse(try_from_str = parse_time_arg) - )] - before: Option<NaiveDateTime>, - - /// 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<Weekday>, - - /// Only show logs from the given encounters. - #[structopt(short = "e", long = "bosses", default_value = "*")] - bosses: CommaSeparatedList<evtclib::statistics::gamedata::Boss>, - /// 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<String>, } /// A flag indicating which fields should be searched. @@ -180,26 +135,6 @@ impl FromStr for FightOutcome { } } -fn parse_time_arg(input: &str) -> Result<NaiveDateTime> { - 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<T: FromStr>(input: &str) -> Result<T, String> { - T::from_str(input).map_err(|_| format!("'{}' is an invalid value", input)) -} - enum ZipWrapper<R: Read + Seek> { Raw(Option<R>), Zipped(zip::ZipArchive<R>), @@ -223,6 +158,13 @@ impl<R: Read + Seek> ZipWrapper<R> { } 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<dyn LogFilter> { - 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<Box<dyn LogFilter>> { + // 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 { |