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 { | 
