diff options
-rw-r--r-- | src/fexpr/grammar.lalrpop | 62 | ||||
-rw-r--r-- | src/fexpr/mod.rs | 57 | ||||
-rw-r--r-- | src/filters/mod.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 45 |
4 files changed, 135 insertions, 31 deletions
diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index 48349a1..4e6ac89 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -1,5 +1,6 @@ use super::{ FError, + FErrorKind, FightOutcome, filters, SearchField, @@ -76,40 +77,68 @@ PlayerPredicate: Box<dyn filters::player::PlayerFilter> = { } Regex: Regex = { - <s:r#""[^"]*""#> =>? Regex::new(&s[1..s.len() - 1]).map_err(|_| ParseError::User { - error: FError::InvalidRegex(s.into()), + <l:@L> <s:regex> =>? Regex::new(&s[1..s.len() - 1]).map_err(|error| ParseError::User { + error: FError { + location: l, + data: s.to_string(), + kind: error.into(), + } }), - <s:word> =>? Regex::new(s).map_err(|e| ParseError::User { - error: FError::InvalidRegex(s.into()), + <l:@L> <s:word> =>? Regex::new(s).map_err(|error| ParseError::User { + error: FError { + location: l, + data: s.to_string(), + kind: error.into(), + } }), } FightOutcome: FightOutcome = { - <word> =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidFightOutcome(<>.into()), + <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidFightOutcome, + } }), } Weekday: Weekday = { - <word> =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidWeekday(<>.into()), + <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidWeekday, + } }), } Boss: Boss = { - <word> =>? <>.parse().map_err(|_| ParseError::User { - error: FError::InvalidBoss(<>.into()), + <l:@L> <w:word> =>? w.parse().map_err(|_| ParseError::User { + error: FError { + location: l, + data: w.into(), + kind: FErrorKind::InvalidBoss, + } }), } Date: NaiveDateTime = { - <datetime> =>? NaiveDateTime::parse_from_str(<>, "%Y-%m-%d %H:%M:%S") - .map_err(|_| ParseError::User { - error: FError::InvalidTimestamp(<>.into()), + <l:@L> <d:datetime> =>? NaiveDateTime::parse_from_str(d, "%Y-%m-%d %H:%M:%S") + .map_err(|error| ParseError::User { + error: FError { + location: l, + data: d.into(), + kind: error.into(), + } }), - <date> =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", <>), "%Y-%m-%d %H:%M:%S") - .map_err(|_| ParseError::User { - error: FError::InvalidTimestamp(<>.into()), + <l:@L> <d:date> =>? NaiveDateTime::parse_from_str(&format!("{} 00:00:00", d), "%Y-%m-%d %H:%M:%S") + .map_err(|error| ParseError::User { + error: FError { + location: l, + data: d.into(), + kind: error.into(), + } }), } @@ -131,6 +160,7 @@ match { r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime, r"\d\d\d\d-\d\d-\d\d" => date, r"\w+" => word, + r#""[^"]*""# => regex, _ } diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index aafdea7..5754d94 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -4,28 +4,63 @@ //! 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, lexer::Token, ParseError}; +use std::{error, fmt}; + +use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; -lalrpop_mod!(pub grammar, "/fexpr/grammar.rs"); +lalrpop_mod!(#[allow(clippy::all)] pub grammar, "/fexpr/grammar.rs"); + +#[derive(Debug)] +pub struct FError { + location: usize, + data: String, + kind: FErrorKind, +} + +impl fmt::Display for FError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} (at {})", self.kind, self.location) + } +} + +impl error::Error for FError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(&self.kind) + } +} #[derive(Debug, Error)] -pub enum FError { +pub enum FErrorKind { #[error("invalid regular expression: {0}")] - InvalidRegex(String), - #[error("invalid fight outcome: {0}")] - InvalidFightOutcome(String), - #[error("invalid weekday: {0}")] - InvalidWeekday(String), + InvalidRegex(#[from] regex::Error), + #[error("invalid fight outcome")] + InvalidFightOutcome, + #[error("invalid weekday")] + InvalidWeekday, #[error("invalid timestamp: {0}")] - InvalidTimestamp(String), - #[error("invalid boss name: {0}")] - InvalidBoss(String), + InvalidTimestamp(#[from] chrono::format::ParseError), + #[error("invalid boss name")] + InvalidBoss, } +/// Shortcut to create a new parser and parse the given input. pub fn parse_logfilter( input: &str, ) -> Result<Box<dyn filters::log::LogFilter>, ParseError<usize, Token, FError>> { grammar::LogFilterParser::new().parse(input) } + +/// Extract the location from the given error. +pub fn location<T>(err: &ParseError<usize, T, FError>) -> usize { + match *err { + ParseError::InvalidToken { location } => location, + ParseError::UnrecognizedEOF { location, .. } => location, + ParseError::UnrecognizedToken { + token: (l, _, _), .. + } => l, + ParseError::ExtraToken { token: (l, _, _) } => l, + ParseError::User { ref error } => error.location, + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 525ff27..3d0868b 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -1,5 +1,5 @@ #![allow(clippy::new_ret_no_self)] -use std::{ops, fmt}; +use std::{fmt, ops}; use num_derive::FromPrimitive; use num_traits::FromPrimitive as _; 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<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 { - eprintln!("Error: {}", err); + display_error(&err); + } +} + +fn display_error(err: &Error) { + if let Some(err) = err.downcast_ref::<InputError>() { + eprintln!("{}", err); + } else { + eprintln!("{}: {}", "Error".red(), err); } } @@ -207,7 +237,16 @@ fn build_filter(opt: &Opt) -> Result<Box<dyn LogFilter>> { // 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. |