From 7881ba85ff40f3d22237ef903c7241b56aa9c185 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2020 15:01:10 +0200 Subject: new filter pipeline This is the groundwork for introducing more complex filter queries like `find` has. Filters can be arbitrarily combined with and/or/not and support an "early filter" mode. So far, the filters have been translated pretty mechanically to match the current command line arguments, so now new syntax has been introduced. The NameFilter is not yet in its final version. The goal is to support something like PlayerAll/PlayerExists and have a PlayerFilter that works on single players instead of the complete log, but that might introduce some code duplication as we then need a PlayerFilterAnd, PlayerFilterOr, ... Some digging has to be done into whether we can reduce that duplication without devolving into madness or resorting to macros. Maybe some type-level generic hackery could be done? Maybe an enum instead of dynamic traits should be used, at least for the base functions? --- src/main.rs | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 6b67875..41df732 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,15 +6,16 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use chrono::{Duration, NaiveDateTime, Weekday}; +use log::debug; use num_traits::cast::FromPrimitive; use regex::Regex; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; -use log::debug; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod filters; +use filters::{Filter, Inclusion}; mod guilds; mod logger; mod output; @@ -32,7 +33,6 @@ macro_rules! unwrap { }; } - /// A program that allows you to search through all your evtc logs for specific /// people. #[derive(StructOpt, Debug)] @@ -106,7 +106,7 @@ pub struct Opt { /// 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 +135,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. @@ -258,16 +260,36 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } +fn build_filter(opt: &Opt) -> Box { + let mut filter = filters::Const::new(false); + for field in opt.field.values() { + filter = filter | filters::NameFilter::new(*field, opt.expression.clone()); + } + + if opt.invert { + filter = !filter; + } + + filter = filter + & filters::BossFilter::new(opt.bosses.values().clone()) + & filters::OutcomeFilter::new(opt.outcome.values().clone()) + & filters::WeekdayFilter::new(opt.weekdays.values().clone()) + & filters::TimeFilter::new(opt.after, opt.before); + + filter +} + /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<()> { let pipeline = &output::build_pipeline(opt); + let filter: &dyn Filter = &*build_filter(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { 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 +309,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> { +fn search_log(entry: &DirEntry, filter: &dyn Filter) -> Result> { let file_stream = BufReader::new(File::open(entry.path())?); let is_zip = entry .file_name() @@ -302,10 +324,9 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result> { 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 +341,7 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result> { 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 +390,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), -- cgit v1.2.3 From ba491c8a5f6c8c2fa86b12dacf9d80f92da9168a Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 15:18:20 +0200 Subject: split off player filters and log filters As it turns out, we can easily re-use the existing Filter machinery to generalize over LogFilters (which operate on LogResults) and PlayerFilters (which operate on Players). The feature trait_aliases is not strictly needed but makes the function signatures a bit nicer and easier to read, and it reduces the chances of an error (e.g. by using Filter<&PartialEvtc, ...>). --- src/main.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 41df732..bea03f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![feature(trait_alias)] use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Read, Seek}; @@ -15,7 +16,7 @@ use walkdir::{DirEntry, WalkDir}; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod filters; -use filters::{Filter, Inclusion}; +use filters::{log::LogFilter, Inclusion}; mod guilds; mod logger; mod output; @@ -260,21 +261,24 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } -fn build_filter(opt: &Opt) -> Box { - let mut filter = filters::Const::new(false); - for field in opt.field.values() { - filter = filter | filters::NameFilter::new(*field, opt.expression.clone()); - } +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::BossFilter::new(opt.bosses.values().clone()) - & filters::OutcomeFilter::new(opt.outcome.values().clone()) - & filters::WeekdayFilter::new(opt.weekdays.values().clone()) - & filters::TimeFilter::new(opt.after, opt.before); + & 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 } @@ -282,7 +286,7 @@ fn build_filter(opt: &Opt) -> Box { /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<()> { let pipeline = &output::build_pipeline(opt); - let filter: &dyn Filter = &*build_filter(opt); + let filter: &dyn LogFilter = &*build_filter(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { @@ -309,7 +313,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, filter: &dyn Filter) -> Result> { +fn search_log(entry: &DirEntry, filter: &dyn LogFilter) -> Result> { let file_stream = BufReader::new(File::open(entry.path())?); let is_zip = entry .file_name() -- cgit v1.2.3 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/main.rs | 124 ++++++++++++------------------------------------------------ 1 file changed, 25 insertions(+), 99 deletions(-) (limited to 'src/main.rs') 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 From 185a5b2f802f9d05c3eb40f807c0488f168c6661 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 13:59:28 +0200 Subject: better error outputs --- src/main.rs | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 8edd75a..2e0c82f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ #![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::Result; +use anyhow::{anyhow, Error, Result}; use chrono::{NaiveDateTime, Weekday}; +use colored::Colorize; use log::debug; use num_traits::cast::FromPrimitive; use structopt::StructOpt; @@ -157,10 +159,38 @@ impl ZipWrapper { } } +#[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 { - eprintln!("Error: {}", err); + display_error(&err); + } +} + +fn display_error(err: &Error) { + if let Some(err) = err.downcast_ref::() { + eprintln!("{}", err); + } else { + eprintln!("{}: {}", "Error".red(), err); } } @@ -207,7 +237,16 @@ fn build_filter(opt: &Opt) -> Result> { // 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)?) + 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. -- cgit v1.2.3 From 0a27adbc0bf3bbbf87fea9e55c00c38f61d55058 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:23:50 +0200 Subject: add a small repl --- src/main.rs | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 2e0c82f..ff882e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::fs::File; -use std::io::{BufReader, Read, Seek}; +use std::io::{BufReader, Read, Seek, Write}; use std::path::PathBuf; use std::str::FromStr; @@ -58,6 +58,10 @@ pub struct Opt { #[structopt(long = "guilds")] guilds: bool, + /// Run the REPL. + #[structopt(long)] + repl: bool, + /// The filter expression. expression: Vec, } @@ -211,9 +215,13 @@ fn run() -> Result<()> { guilds::prepare_cache(); } - let filter = build_filter(&opt)?; - - grep(&opt, &*filter)?; + if !opt.repl { + let expr_string = opt.expression.join(" "); + let filter = build_filter(&expr_string)?; + grep(&opt, &*filter)?; + } else { + repl(&opt)?; + } if opt.guilds { guilds::save_cache(); @@ -222,6 +230,22 @@ fn run() -> Result<()> { Ok(()) } +fn repl(opt: &Opt) -> Result<()> { + let stdin = std::io::stdin(); + loop { + print!("Query> "); + std::io::stdout().flush()?; + let mut line = String::new(); + stdin.read_line(&mut line)?; + let line = line.trim(); + let parsed = build_filter(&line); + match parsed { + Ok(filter) => grep(&opt, &*filter)?, + Err(err) => display_error(&err.into()), + } + } +} + /// Check if the given entry represents a log file, based on the file name. fn is_log_file(entry: &DirEntry) -> bool { entry @@ -231,12 +255,8 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } -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(" "))); +/// Small wrapper around `fexpr::parse_logfilter` to convert the returned `Err` to be `'static'. +fn build_filter(expr_string: &str) -> Result> { if expr_string.trim().is_empty() { return Err(anyhow!("Expected a filter to be given")); } -- cgit v1.2.3 From ab909d2f3a0a59ae1b9a169ec79d4e9ffeddd1e1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 14:34:08 +0200 Subject: use readline/rustyline instead of stdin.read_line This gives us a history, nicer editing capabilities and the possibility to add completion in the future. --- src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index ff882e5..b6633ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use chrono::{NaiveDateTime, Weekday}; use colored::Colorize; use log::debug; use num_traits::cast::FromPrimitive; +use rustyline::{error::ReadlineError, Editor}; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; @@ -231,13 +232,10 @@ fn run() -> Result<()> { } fn repl(opt: &Opt) -> Result<()> { - let stdin = std::io::stdin(); + let mut rl = Editor::<()>::new(); loop { - print!("Query> "); - std::io::stdout().flush()?; - let mut line = String::new(); - stdin.read_line(&mut line)?; - let line = line.trim(); + let line = rl.readline("Query> ")?; + rl.add_history_entry(&line); let parsed = build_filter(&line); match parsed { Ok(filter) => grep(&opt, &*filter)?, -- cgit v1.2.3 From efbc3fb2131b8c69a88e0622ad4b120c2c48ed85 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 15:04:53 +0200 Subject: add predicate documentation to the help text Sadly, structopt always displays this, despite the documentation stating that it should be hidden when the user uses -h (instead of --help). It seems like this is a bug in clap, which might get fixed with clap 3.0. --- src/main.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index b6633ac..ab66848 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,10 +34,29 @@ 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. #[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))] @@ -56,14 +75,20 @@ pub struct Opt { 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, /// 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. + /// The filter expression, see PREDICATES for more information. expression: Vec, } -- cgit v1.2.3 From fe88f503676091c53d31db99ca4af36fe08dcdc8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Apr 2020 15:07:41 +0200 Subject: remove unused imports --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index ab66848..c89614b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::fs::File; -use std::io::{BufReader, Read, Seek, Write}; +use std::io::{BufReader, Read, Seek}; use std::path::PathBuf; use std::str::FromStr; @@ -11,7 +11,7 @@ use chrono::{NaiveDateTime, Weekday}; use colored::Colorize; use log::debug; use num_traits::cast::FromPrimitive; -use rustyline::{error::ReadlineError, Editor}; +use rustyline::Editor; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; -- cgit v1.2.3 From 675d0aadf1495a7ac752789c125e124db5152b43 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 12:49:27 +0200 Subject: better CLI args/parser integration First, this commit adds a shortcut if only a single argument is given that can be parsed as a regex. This is to retain the old behaviour of "raidgrep NAME" just working, without needing to specify -player or anything. Secondly, this also re-wraps arguments with spaces (unless there's only one argument in total). This means that the following should work: raidgrep -- -player "Godric Gobbledygook" instead of either raidgrep -- '-player "Godric Gobbledygook"' raidgrep -- -player '"Godric Gobbledygook"' (notice the extra quotes). --- src/main.rs | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index c89614b..e18a109 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,10 @@ use std::str::FromStr; 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}; @@ -242,9 +244,7 @@ fn run() -> Result<()> { } if !opt.repl { - let expr_string = opt.expression.join(" "); - let filter = build_filter(&expr_string)?; - grep(&opt, &*filter)?; + single(&opt)?; } else { repl(&opt)?; } @@ -256,6 +256,41 @@ fn run() -> Result<()> { 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 let Err(_) = maybe_filter { + let maybe_regex = Regex::new(line); + if let Ok(rgx) = maybe_regex { + let filter = filters::player::any( + filters::player::NameFilter::new(SearchField::Account, rgx.clone()) + | filters::player::NameFilter::new(SearchField::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 { -- cgit v1.2.3 From d124265bee159193090f085343b8523bc6387620 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 12:53:54 +0200 Subject: cosmetic fixes --- src/main.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index e18a109..66b2880 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,18 +45,18 @@ macro_rules! unwrap { /// /// 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. +/// -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. +/// -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. #[derive(StructOpt, Debug)] #[structopt(verbatim_doc_comment)] pub struct Opt { @@ -262,7 +262,7 @@ fn single(opt: &Opt) -> Result<()> { if opt.expression.len() == 1 { let line = &opt.expression[0]; let maybe_filter = build_filter(line); - if let Err(_) = maybe_filter { + if maybe_filter.is_err() { let maybe_regex = Regex::new(line); if let Ok(rgx) = maybe_regex { let filter = filters::player::any( @@ -299,7 +299,7 @@ fn repl(opt: &Opt) -> Result<()> { let parsed = build_filter(&line); match parsed { Ok(filter) => grep(&opt, &*filter)?, - Err(err) => display_error(&err.into()), + Err(err) => display_error(&err), } } } -- cgit v1.2.3 From 509e5817e6e035e762840c00fb95b18253b1d269 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 12:56:47 +0200 Subject: only try regex if word doesn't start with - Since our predicates start with -, this sounds like a good heuristic to prevent something like "raidgrep -- -player" from silently succeeding but not doing what the user had intended. In this case, we want the parse error to show and not treat "-player" as a regex. --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 66b2880..3164bd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -262,7 +262,7 @@ fn single(opt: &Opt) -> Result<()> { if opt.expression.len() == 1 { let line = &opt.expression[0]; let maybe_filter = build_filter(line); - if maybe_filter.is_err() { + 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( -- cgit v1.2.3 From 5dbea93266c3a30dac5ec6f5a7915d73a440f573 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 13:14:30 +0200 Subject: use free functions instead of Filter::new Having a ::new on each of the filter types was a bit weird, especially because we returned Box instead of Self (and clippy rightfully complained). With this patch, we now have a bunch of normal functions, and we don't show to the outside how a filter is actually implemented (or what struct is behind it). --- src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 3164bd4..50dcf8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -266,8 +266,7 @@ fn single(opt: &Opt) -> Result<()> { let maybe_regex = Regex::new(line); if let Ok(rgx) = maybe_regex { let filter = filters::player::any( - filters::player::NameFilter::new(SearchField::Account, rgx.clone()) - | filters::player::NameFilter::new(SearchField::Character, rgx), + filters::player::account(rgx.clone()) | filters::player::character(rgx), ); return grep(opt, &*filter); } -- cgit v1.2.3 From 9bbd5db2a6caae10f0ab2cf2625fbc34485a4ce9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 25 Apr 2020 13:22:00 +0200 Subject: add -include and -exclude --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 50dcf8d..231fbdc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,8 @@ macro_rules! unwrap { /// -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(verbatim_doc_comment)] pub struct Opt { -- cgit v1.2.3