aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/csl.rs132
-rw-r--r--src/fexpr/mod.rs8
-rw-r--r--src/main.rs124
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 {