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