diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/config.rs | 168 | ||||
-rw-r--r-- | src/filter.rs | 50 | ||||
-rw-r--r-- | src/main.rs | 44 |
4 files changed, 219 insertions, 45 deletions
@@ -1,5 +1,5 @@ [package] -name = "rust-httplog" +name = "ncsautil" version = "0.0.1" authors = ["Yorhel <git@yorhel.nl>"] diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6ab2118 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,168 @@ +use std::collections::HashMap; +use std::fs::{File,metadata}; +use std::io::{stderr,Write,BufReader,BufRead}; +use std::env::{home_dir,args}; +use std::process::exit; + +use super::filter::Filter; +use getopts::{Matches,Options}; + +#[derive(Clone,Debug)] +pub struct Config { + pub quiet: bool, + pub vhost2ident: bool, + pub aliases: HashMap<String, Filter>, +} + + +impl Config { + pub fn new() -> Config { + Config { + quiet: false, + vhost2ident: false, + aliases: HashMap::new() + } + } + + fn add_alias(&mut self, file: &str, i: usize, l: &str) { + let mut a = l.splitn(2, ' '); + if a.clone().count() != 2 { + writeln!(&mut stderr(), "{}:{}: alias needs two arguments", file, i+1).unwrap(); + return; + } + let n = a.next().unwrap(); + // TODO: Verify that the alias only consists of allowed chars + + let f = match Filter::new(a.next().unwrap()) { + Err(()) => { + writeln!(&mut stderr(), "{}:{}: invalid filter string", file, i+1).unwrap(); + return + }, + Ok(x) => x + }; + + self.aliases.insert(String::from_str(n), f); + } + + /// Dumps any errors/warnings to stderr, returns false on fatal error. + fn load_file(&mut self, file: &str) -> bool { + let f = match File::open(file) { + Err(x) => { + writeln!(&mut stderr(), "Unable to open '{}': {}", file, x).unwrap(); + return false; + }, + Ok(x) => x + }; + let b = BufReader::new(f); + for (i, l) in b.lines().enumerate() { + let l = match l { + Err(x) => { + writeln!(&mut stderr(), "Unable to read from '{}': {}", file, x).unwrap(); + return false; + }, + Ok(x) => x + }; + if l == "quiet" { + self.quiet = true; + } else if l == "no-quiet" { + self.quiet = false; + } else if l == "vhost2ident" { + self.vhost2ident = true; + } else if l == "no-vhost2ident" { + self.vhost2ident = false; + } else if l.starts_with("alias ") { + self.add_alias(file, i, &l[6..]); + } else if !(l.starts_with("#") || l.len() == 0) { + writeln!(&mut stderr(), "{}:{}: Unrecognized line: {}", file, i+1, l).unwrap(); + } + } + true + } + + fn load_config(&mut self, m: &Matches) { + if m.opt_present("config") { + if !self.load_file(&m.opt_str("config").unwrap()) { + exit(1); + } + } else if !m.opt_present("no-config") { + if metadata("/etc/ncsautil").is_ok() { + self.load_file("/etc/ncsautil"); + } + let h = home_dir(); + match h { + None => (), + Some(mut x) => { + x.push(".ncsautil"); + if metadata(&x).is_ok() { + self.load_file(x.to_str().unwrap()); + } + } + }; + } + } + + fn parse_opts(&mut self, m: &Matches) { + if m.opt_present("vhost2ident") { + self.vhost2ident = true; + } + if m.opt_present("no-vhost2ident") { + self.vhost2ident = false; + } + + if m.opt_present("quiet") { + self.quiet = true; + } + if m.opt_present("no-quiet") { + self.quiet = false; + } + } + + /// Parses the filter string from the command line + fn parse_filter(&self, m: &Matches) -> Filter { + if m.free.is_empty() { + println!("No filter string given. See --help for options."); + exit(1); + } + + let filter = &m.free[0]; + let f = Filter::new(filter); + if f.is_err() { + println!("Invalid filter '{}'", filter); + exit(1); + } + let mut f = f.unwrap(); + match f.expand(&self.aliases) { + Ok(_) => (), + Err(x) => { + println!("Invalid filter '{}': {}", filter, x); + exit(1); + } + } + f + } + + /// Parses command-line arguments and loads any config files. Exits the program on error. + pub fn parse_args(&mut self) -> Filter { + let mut opts = Options::new(); + opts.optflag("h", "help", "print this help"); + opts.optopt( "c", "config", "load alternative config file", "FILE"); + opts.optflag("", "no-config", "don't load any config file at all"); + opts.optflag("", "vhost2ident", "normalize hostname into ident field"); + opts.optflag("", "no-vhost2ident", ""); + opts.optflag("q", "quiet", "suppress warnings about unrecognized lines"); + opts.optflag("", "no-quiet", ""); + + let mut args = args(); + let prog = args.next().unwrap(); + let m = opts.parse(args).unwrap(); + + if m.opt_present("h") { + print!("{}", opts.usage(format!("Usage: {} [options] <filter>", prog).as_ref())); + exit(1); + } + + self.load_config(&m); + self.parse_opts(&m); + self.parse_filter(&m) + } +} diff --git a/src/filter.rs b/src/filter.rs index d4ce4bd..4b91e09 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use regex::Regex; use super::Line; @@ -23,7 +24,7 @@ pub enum FilterStrField { Ua, } -#[derive(Debug)] +#[derive(Clone,Debug,PartialEq,Eq)] pub enum Filter { And(Box<Filter>, Box<Filter>), Or(Box<Filter>, Box<Filter>), @@ -31,6 +32,7 @@ pub enum Filter { SizeCmp(FilterCmp, u64), StatusCmp(FilterCmp, u16), Match(FilterStrField, Regex), + FilterRef(String), } @@ -74,9 +76,8 @@ size -> u64 = u:unsigned m:unit_mult { u * m } string_part -> &'input str - = [^"\\]+ { match_str } - / "\\\"" { "\"" } - / "\\\\" { "\\" } + = "\\\"" { "\"" } + / [^"]+ { match_str } string -> String = "\"" s:string_part* "\"" { s.concat() } @@ -84,6 +85,9 @@ string -> String regex -> super::super::regex::Regex = s:string {? match super::super::regex::Regex::new(&s) { Ok(x) => Ok(x), Err(_) => Err("Invalid regular expression") } } +ident -> &'input str + = [a-zA-Z0-9_]+ { match_str } + ws = [ ]* @@ -97,6 +101,7 @@ filter -> super::Filter / "size" ws c:cmp ws n:size ws { super::Filter::SizeCmp(c, n) } / "status" ws c:cmp ws n:unsigned ws { super::Filter::StatusCmp(c, n as u16) } / f:strfield ws "~" ws r:regex ws { super::Filter::Match(f, r) } + / ":" f:ident ws { super::Filter::FilterRef(String::from_str(f)) } "#); @@ -144,11 +149,42 @@ impl Filter { Filter::And(ref a, ref b) => a.apply(line) && b.apply(line), Filter::Or(ref a, ref b) => a.apply(line) || b.apply(line), Filter::Not(ref a) => !a.apply(line), - Filter::SizeCmp(c, x) => c.apply(x, line.size), - Filter::StatusCmp(c, x) => c.apply(x, line.status), - Filter::Match(f, ref r) => r.is_match(f.getfield(&line)) + Filter::SizeCmp(c, x) => c.apply(line.size, x), + Filter::StatusCmp(c, x) => c.apply(line.status, x), + Filter::Match(f, ref r) => r.is_match(f.getfield(&line)), + // Can't match without expansion. + Filter::FilterRef(_) => unreachable!(), } } + + /// Recursively expands any FilterRefs. + pub fn expand(&mut self, lst: &HashMap<String, Filter>) -> Result<(), String> { + // TODO: Detect and halt on recursion in filters. + let mut rep = match *self { + Filter::And(ref mut a, ref mut b) => { + try!(a.expand(lst)); + try!(b.expand(lst)); + return Ok(()) + }, + Filter::Or(ref mut a, ref mut b) => { + try!(a.expand(lst)); + try!(b.expand(lst)); + return Ok(()) + }, + Filter::Not(ref mut a) => { + try!(a.expand(lst)); + return Ok(()) + }, + Filter::FilterRef(ref n) => match lst.get(n) { + None => return Err(format!("unresolved alias: {}", n)), + Some(x) => x.clone() + }, + _ => return Ok(()), + }; + try!(rep.expand(lst)); + *self = rep; + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index f94107b..50bba93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,63 +9,33 @@ extern crate regex; mod line; mod filter; - -pub use line::Line; +mod config; use std::io::{stdin,stdout,stderr,Write,BufRead}; - -fn print_usage(prog: &str, opts: getopts::Options) { - print!("{}", opts.usage(format!("Usage: {} [options] <filter>", prog).as_ref())); - std::process::exit(1); -} +pub use line::Line; +use config::Config; fn main() { - let mut opts = getopts::Options::new(); - opts.optflag("", "vhost2ident", "normalize hostname into ident field"); - opts.optflag("h", "help", "print this help"); - opts.optflag("q", "quiet", "suppress warnings about unrecognized lines"); - let mut args = std::env::args(); - let prog = args.next().unwrap(); - let m = opts.parse(args).unwrap(); - - if m.opt_present("h") { - return print_usage(prog.as_ref(), opts); - } - - let vhost2ident = m.opt_present("vhost2ident"); - let quiet = m.opt_present("q"); - let filter = if m.free.is_empty() { - return print_usage(prog.as_ref(), opts); - } else { - m.free[0].clone() - }; - - let filter = match filter::Filter::new(filter.as_ref()) { - Ok(x) => x, - Err(_) => { - println!("Invalid filter '{}'", filter); - return; - } - }; + let mut c = Config::new(); + let filter = c.parse_args(); let s = stdin(); let o = stdout(); let mut o = o.lock(); for l in s.lock().lines().map(|x| x.unwrap()) { - let mut line = match Line::new(l.as_ref()) { Ok(x) => x, Err(_) => { - if !quiet { + if !c.quiet { writeln!(&mut stderr(), "Unrecognized line: {}", l).unwrap(); } continue; } }; - if vhost2ident { + if c.vhost2ident { line.vhost2ident(); } |