aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs249
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),