From c204f99444d70d74969ac081fe6b36830b62e21a Mon Sep 17 00:00:00 2001 From: Ryan Geary Date: Thu, 10 Oct 2019 21:24:07 -0400 Subject: Reorganize massively --- src/choice.rs | 332 ++++++++++++++++++---------------------------------------- src/config.rs | 177 +++++++++++++++++++++++++++++++ src/main.rs | 23 ++-- 3 files changed, 286 insertions(+), 246 deletions(-) create mode 100644 src/config.rs diff --git a/src/choice.rs b/src/choice.rs index 65e1acd..d3485a4 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -1,169 +1,180 @@ -#[cfg(test)] -mod tests { - use super::*; +use crate::io::{BufWriter, Write}; +use std::convert::TryInto; - mod parse_choice_tests { - use super::*; +use crate::config::Config; - #[test] - fn parse_single_choice() { - let result = Choice::parse_choice("6").unwrap(); - assert_eq!( - 6, - match result { - Choice::Field(x) => x, - _ => panic!(), - } - ) - } +pub type Range = (Option, Option); - #[test] - fn parse_none_started_range() { - let result = Choice::parse_choice(":5").unwrap(); - assert_eq!( - (None, Some(5)), - match result { - Choice::FieldRange(x) => x, - _ => panic!(), - } - ) - } +#[derive(Debug)] +pub enum Choice { + Field(u32), + FieldRange(Range), +} - #[test] - fn parse_none_terminated_range() { - let result = Choice::parse_choice("5:").unwrap(); - assert_eq!( - (Some(5), None), - match result { - Choice::FieldRange(x) => x, - _ => panic!(), - } - ) - } +impl Choice { + pub fn print_choice( + &self, + line: &String, + config: &Config, + handle: &mut BufWriter, + ) { + write!(handle, "{}", self.get_choice_slice(line, config).join(" ")); + } - #[test] - fn parse_full_range() { - let result = Choice::parse_choice("5:7").unwrap(); - assert_eq!( - (Some(5), Some(7)), - match result { - Choice::FieldRange(x) => x, - _ => panic!(), - } - ) - } + fn get_choice_slice<'a>(&self, line: &'a String, config: &Config) -> Vec<&'a str> { + let words = config + .separator + .split(line) + .into_iter() + .filter(|s| !s.is_empty()) + .enumerate(); - #[test] - fn parse_beginning_to_end_range() { - let result = Choice::parse_choice(":").unwrap(); - assert_eq!( - (None, None), - match result { - Choice::FieldRange(x) => x, - _ => panic!(), + match self { + Choice::Field(i) => words + .filter(|x| x.0 == *i as usize) + .map(|x| x.1) + .collect::>(), + Choice::FieldRange(r) => match r { + (None, None) => words.map(|x| x.1).collect::>(), + (Some(start), None) => words + .filter(|x| x.0 >= (*start).try_into().unwrap()) + .map(|x| x.1) + .collect::>(), + (None, Some(end)) => { + let e: usize = if config.opt.inclusive { + (end + 1).try_into().unwrap() + } else { + (*end).try_into().unwrap() + }; + words + .filter(|x| x.0 < e) + .map(|x| x.1) + .collect::>() } - ) + (Some(start), Some(end)) => { + let e: usize = if config.opt.inclusive { + (end + 1).try_into().unwrap() + } else { + (*end).try_into().unwrap() + }; + words + .filter(|x| x.0 < e && x.0 >= (*start).try_into().unwrap()) + .map(|x| x.1) + .collect::>() + } + }, } - - // These tests should pass once parse_choice return errors properly, but until that time makes - // running other tests impossible. - //#[test] - //fn parse_bad_choice() { - //assert!(Choice::parse_choice("d").is_err()); - //} - // - //#[test] - //fn parse_bad_range() { - //assert!(Choice::parse_choice("d:i").is_err()); - //} } +} + +#[cfg(test)] +mod tests { mod get_choice_slice_tests { - use super::*; + use crate::config::{Config, Opt}; + use std::ffi::OsString; + use structopt::StructOpt; + + impl Config { + pub fn from_iter(iter: I) -> Self + where + I: IntoIterator, + I::Item: Into + Clone, + { + return Config::new(Opt::from_iter(iter)); + } + } #[test] fn print_0() { - let opt = Opt::from_iter(vec!["choose", "0"]); + let config = Config::from_iter(vec!["choose", "0"]); assert_eq!( vec!["rust"], - opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust is pretty cool"), &config) ); } #[test] fn print_after_end() { - let opt = Opt::from_iter(vec!["choose", "10"]); + let config = Config::from_iter(vec!["choose", "10"]); assert_eq!( Vec::<&str>::new(), - opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust is pretty cool"), &config) ); } #[test] fn print_1_to_3() { - let opt = Opt::from_iter(vec!["choose", "1:3"]); + let config = Config::from_iter(vec!["choose", "1:3"]); assert_eq!( vec!["is", "pretty"], - opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust is pretty cool"), &config) ); } #[test] fn print_1_to_3_inclusive() { - let opt = Opt::from_iter(vec!["choose", "1:3", "-n"]); + let config = Config::from_iter(vec!["choose", "1:3", "-n"]); assert_eq!( vec!["is", "pretty", "cool"], - opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust is pretty cool"), &config) ); } #[test] fn print_1_to_3_separated_by_hashtag() { - let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "#"]); + let config = Config::from_iter(vec!["choose", "1:3", "-f", "#"]); assert_eq!( vec!["is", "pretty"], - opt.choice[0].get_choice_slice(&String::from("rust#is#pretty#cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust#is#pretty#cool"), &config) ); } #[test] fn print_1_to_3_separated_by_varying_multiple_hashtag() { - let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "#"]); + let config = Config::from_iter(vec!["choose", "1:3", "-f", "#"]); assert_eq!( vec!["is", "pretty"], - opt.choice[0].get_choice_slice(&String::from("rust##is###pretty####cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust##is###pretty####cool"), &config) ); } #[test] fn print_1_to_3_separated_by_varying_multiple_hashtag_inclusive() { - let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "#", "-n"]); + let config = Config::from_iter(vec!["choose", "1:3", "-f", "#", "-n"]); assert_eq!( vec!["is", "pretty", "cool"], - opt.choice[0].get_choice_slice(&String::from("rust##is###pretty####cool"), &opt) + config.opt.choice[0] + .get_choice_slice(&String::from("rust##is###pretty####cool"), &config) ); } #[test] fn print_1_to_3_separated_by_regex_group_vowels() { - let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "[aeiou]"]); + let config = Config::from_iter(vec!["choose", "1:3", "-f", "[aeiou]"]); assert_eq!( vec![" q", "ck br"], - opt.choice[0].get_choice_slice( + config.opt.choice[0].get_choice_slice( &String::from("the quick brown fox jumped over the lazy dog"), - &opt + &config ) ); } #[test] fn print_1_to_3_separated_by_regex_group_vowels_inclusive() { - let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "[aeiou]", "-n"]); + let config = Config::from_iter(vec!["choose", "1:3", "-f", "[aeiou]", "-n"]); assert_eq!( vec![" q", "ck br", "wn f"], - opt.choice[0].get_choice_slice( + config.opt.choice[0].get_choice_slice( &String::from("the quick brown fox jumped over the lazy dog"), - &opt + &config ) ); } @@ -172,142 +183,3 @@ mod tests { } -use crate::io::{BufWriter, Write}; -use regex::Regex; -use std::convert::TryInto; -use std::num::ParseIntError; -use std::path::PathBuf; -use std::process; -use structopt::StructOpt; - -#[derive(Debug, StructOpt)] -#[structopt(name = "choose", about = "`choose` sections from each line of files")] -pub struct Opt { - /// Specify field separator other than whitespace - #[structopt(short, long)] - pub field_separator: Option, - - /// Use inclusive ranges - #[structopt(short = "n", long)] - pub inclusive: bool, - - /// Activate debug mode - #[structopt(short, long)] - pub debug: bool, - - /// Input file - #[structopt(short, long, parse(from_os_str))] - pub input: Option, - - /// Fields to print. Either x, x:, :y, or x:y, where x and y are integers, colons indicate a - /// range, and an empty field on either side of the colon continues to the beginning or end of - /// the line. - #[structopt(required = true, min_values = 1, parse(try_from_str = Choice::parse_choice))] - pub choice: Vec, -} - -pub type Range = (Option, Option); - -#[derive(Debug)] -pub enum Choice { - Field(u32), - FieldRange(Range), -} - -impl Choice { - pub fn print_choice( - &self, - line: &String, - opt: &Opt, - re: &Regex, - handle: &mut BufWriter, - ) { - write!(handle, "{}", self.get_choice_slice(line, opt, re).join(" ")); - } - - fn get_choice_slice<'a>(&self, line: &'a String, opt: &Opt, re: &Regex) -> Vec<&'a str> { - let words = re - .split(line) - .into_iter() - .filter(|s| !s.is_empty()) - .enumerate(); - - match self { - Choice::Field(i) => words - .filter(|x| x.0 == *i as usize) - .map(|x| x.1) - .collect::>(), - Choice::FieldRange(r) => match r { - (None, None) => words.map(|x| x.1).collect::>(), - (Some(start), None) => words - .filter(|x| x.0 >= (*start).try_into().unwrap()) - .map(|x| x.1) - .collect::>(), - (None, Some(end)) => { - let e: usize = if opt.inclusive { - (end + 1).try_into().unwrap() - } else { - (*end).try_into().unwrap() - }; - words - .filter(|x| x.0 < e) - .map(|x| x.1) - .collect::>() - } - (Some(start), Some(end)) => { - let e: usize = if opt.inclusive { - (end + 1).try_into().unwrap() - } else { - (*end).try_into().unwrap() - }; - words - .filter(|x| x.0 < e && x.0 >= (*start).try_into().unwrap()) - .map(|x| x.1) - .collect::>() - } - }, - } - } - - pub fn parse_choice(src: &str) -> Result { - let re = Regex::new(r"^(\d*):(\d*)$").unwrap(); - - let cap = match re.captures_iter(src).next() { - Some(v) => v, - None => match src.parse() { - Ok(x) => return Ok(Choice::Field(x)), - Err(_) => { - eprintln!("failed to parse choice argument: {}", src); - // Exit code of 2 means failed to parse choice argument - process::exit(2); - } - }, - }; - - let start = if cap[1].is_empty() { - None - } else { - match cap[1].parse() { - Ok(x) => Some(x), - Err(_) => { - eprintln!("failed to parse range start: {}", &cap[1]); - process::exit(2); - } - } - }; - - let end = if cap[2].is_empty() { - None - } else { - match cap[2].parse() { - Ok(x) => Some(x), - Err(_) => { - eprintln!("failed to parse range end: {}", &cap[2]); - process::exit(2); - } - } - }; - - return Ok(Choice::FieldRange((start, end))); - } -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5ee68b6 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,177 @@ +use regex::Regex; +use std::num::ParseIntError; +use std::path::PathBuf; +use std::process; +use structopt::StructOpt; + +use crate::choice::Choice; + +#[derive(Debug, StructOpt)] +#[structopt(name = "choose", about = "`choose` sections from each line of files")] +pub struct Opt { + /// Specify field separator other than whitespace + #[structopt(short, long)] + pub field_separator: Option, + + /// Use inclusive ranges + #[structopt(short = "n", long)] + pub inclusive: bool, + + /// Activate debug mode + #[structopt(short, long)] + pub debug: bool, + + /// Input file + #[structopt(short, long, parse(from_os_str))] + pub input: Option, + + /// Fields to print. Either x, x:, :y, or x:y, where x and y are integers, colons indicate a + /// range, and an empty field on either side of the colon continues to the beginning or end of + /// the line. + #[structopt(required = true, min_values = 1, parse(try_from_str = Config::parse_choice))] + pub choice: Vec, +} + +pub struct Config { + pub opt: Opt, + pub separator: Regex, +} + +impl Config { + pub fn new(opt: Opt) -> Self { + let separator = Regex::new(match &opt.field_separator { + Some(s) => s, + None => "[[:space:]]", + }) + .unwrap_or_else(|e| { + eprintln!("Failed to compile regular expression: {}", e); + // Exit code of 1 means failed to compile field_separator regex + process::exit(1); + }); + Config { opt, separator } + } + + pub fn parse_choice(src: &str) -> Result { + let re = Regex::new(r"^(\d*):(\d*)$").unwrap(); + + let cap = match re.captures_iter(src).next() { + Some(v) => v, + None => match src.parse() { + Ok(x) => return Ok(Choice::Field(x)), + Err(_) => { + eprintln!("failed to parse choice argument: {}", src); + // Exit code of 2 means failed to parse choice argument + process::exit(2); + } + }, + }; + + let start = if cap[1].is_empty() { + None + } else { + match cap[1].parse() { + Ok(x) => Some(x), + Err(_) => { + eprintln!("failed to parse range start: {}", &cap[1]); + process::exit(2); + } + } + }; + + let end = if cap[2].is_empty() { + None + } else { + match cap[2].parse() { + Ok(x) => Some(x), + Err(_) => { + eprintln!("failed to parse range end: {}", &cap[2]); + process::exit(2); + } + } + }; + + return Ok(Choice::FieldRange((start, end))); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod parse_choice_tests { + use super::*; + + #[test] + fn parse_single_choice() { + let result = Config::parse_choice("6").unwrap(); + assert_eq!( + 6, + match result { + Choice::Field(x) => x, + _ => panic!(), + } + ) + } + + #[test] + fn parse_none_started_range() { + let result = Config::parse_choice(":5").unwrap(); + assert_eq!( + (None, Some(5)), + match result { + Choice::FieldRange(x) => x, + _ => panic!(), + } + ) + } + + #[test] + fn parse_none_terminated_range() { + let result = Config::parse_choice("5:").unwrap(); + assert_eq!( + (Some(5), None), + match result { + Choice::FieldRange(x) => x, + _ => panic!(), + } + ) + } + + #[test] + fn parse_full_range() { + let result = Config::parse_choice("5:7").unwrap(); + assert_eq!( + (Some(5), Some(7)), + match result { + Choice::FieldRange(x) => x, + _ => panic!(), + } + ) + } + + #[test] + fn parse_beginning_to_end_range() { + let result = Config::parse_choice(":").unwrap(); + assert_eq!( + (None, None), + match result { + Choice::FieldRange(x) => x, + _ => panic!(), + } + ) + } + + // These tests should pass once parse_choice return errors properly, but until that time + // makes running other tests impossible. + //#[test] + //fn parse_bad_choice() { + //assert!(Config::parse_choice("d").is_err()); + //} + + //#[test] + //fn parse_bad_range() { + //assert!(Config::parse_choice("d:i").is_err()); + //} + } + +} diff --git a/src/main.rs b/src/main.rs index d6c7c50..1050950 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ -use regex::Regex; use std::fs::File; use std::io::{self, BufRead, BufReader, Read, Write}; -use std::process; use structopt::StructOpt; mod choice; +mod config; +use config::Config; fn main() { - let opt = choice::Opt::from_args(); + let opt = config::Opt::from_args(); + let config = Config::new(opt); - let read = match &opt.input { + let read = match &config.opt.input { Some(f) => Box::new(File::open(f).expect("Could not open file")) as Box, None => Box::new(io::stdin()) as Box, }; @@ -20,22 +21,12 @@ fn main() { let lock = stdout.lock(); let mut handle = io::BufWriter::new(lock); - let re = Regex::new(match &opt.field_separator { - Some(s) => s, - None => "[[:space:]]", - }) - .unwrap_or_else(|e| { - eprintln!("Failed to compile regular expression: {}", e); - // Exit code of 1 means failed to compile field_separator regex - process::exit(1); - }); - let lines = buf.lines(); for line in lines { match line { Ok(l) => { - for choice in &opt.choice { - choice.print_choice(&l, &opt, &re, &mut handle); + for choice in &config.opt.choice { + choice.print_choice(&l, &config, &mut handle); } writeln!(handle, ""); } -- cgit v1.2.3