diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 249 |
1 files changed, 158 insertions, 91 deletions
diff --git a/src/main.rs b/src/main.rs index 6b67875..231fbdc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,31 @@ +#![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::{anyhow, Result}; -use chrono::{Duration, NaiveDateTime, Weekday}; +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}; -use log::debug; 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 { @@ -32,28 +36,36 @@ 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. +/// -include Always evaluates to including the log. +/// -exclude Always evaluates to excluding the log. #[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))] 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, @@ -62,51 +74,31 @@ 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, /// 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, - /// The regular expression to search for. - #[structopt(name = "EXPR")] - expression: Regex, + /// 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, see PREDICATES for more information. + expression: Vec<String>, } /// 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 +127,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. @@ -177,26 +171,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>), @@ -219,7 +193,42 @@ impl<R: Read + Seek> ZipWrapper<R> { } } +#[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 { + display_error(&err); + } +} + +fn display_error(err: &Error) { + if let Some(err) = err.downcast_ref::<InputError>() { + eprintln!("{}", err); + } else { + eprintln!("{}: {}", "Error".red(), err); + } +} + +fn run() -> Result<()> { let opt = Opt::from_args(); if opt.no_color { @@ -236,17 +245,64 @@ fn main() { guilds::prepare_cache(); } - let result = grep(&opt); - match result { - Ok(_) => {} - Err(e) => { - eprintln!("Error: {}", e); - } + if !opt.repl { + single(&opt)?; + } else { + repl(&opt)?; } if opt.guilds { guilds::save_cache(); } + + 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 maybe_filter.is_err() && !line.starts_with('-') { + let maybe_regex = Regex::new(line); + if let Ok(rgx) = maybe_regex { + let filter = filters::player::any( + filters::player::account(rgx.clone()) | filters::player::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 { + let line = rl.readline("Query> ")?; + rl.add_history_entry(&line); + let parsed = build_filter(&line); + match parsed { + Ok(filter) => grep(&opt, &*filter)?, + Err(err) => display_error(&err), + } + } } /// Check if the given entry represents a log file, based on the file name. @@ -258,8 +314,22 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } +/// Small wrapper around `fexpr::parse_logfilter` to convert the returned `Err` to be `'static'. +fn build_filter(expr_string: &str) -> Result<Box<dyn LogFilter>> { + 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. -fn grep(opt: &Opt) -> Result<()> { +fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { let pipeline = &output::build_pipeline(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); @@ -267,7 +337,7 @@ fn grep(opt: &Opt) -> Result<()> { 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 +357,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<Option<LogResult>> { +fn search_log(entry: &DirEntry, filter: &dyn LogFilter) -> Result<Option<LogResult>> { let file_stream = BufReader::new(File::open(entry.path())?); let is_zip = entry .file_name() @@ -302,10 +372,9 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> { 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 +389,7 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> { 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 +438,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), |