Reorganize massively

This commit is contained in:
Ryan Geary 2019-10-10 21:24:07 -04:00
parent f722cbcbbb
commit c204f99444
3 changed files with 309 additions and 269 deletions

View file

@ -1,210 +1,7 @@
#[cfg(test)]
mod tests {
use super::*;
mod parse_choice_tests {
use super::*;
#[test]
fn parse_single_choice() {
let result = Choice::parse_choice("6").unwrap();
assert_eq!(
6,
match result {
Choice::Field(x) => x,
_ => panic!(),
}
)
}
#[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!(),
}
)
}
#[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!(),
}
)
}
#[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!(),
}
)
}
#[test]
fn parse_beginning_to_end_range() {
let result = Choice::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!(Choice::parse_choice("d").is_err());
//}
//
//#[test]
//fn parse_bad_range() {
//assert!(Choice::parse_choice("d:i").is_err());
//}
}
mod get_choice_slice_tests {
use super::*;
#[test]
fn print_0() {
let opt = Opt::from_iter(vec!["choose", "0"]);
assert_eq!(
vec!["rust"],
opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt)
);
}
#[test]
fn print_after_end() {
let opt = Opt::from_iter(vec!["choose", "10"]);
assert_eq!(
Vec::<&str>::new(),
opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt)
);
}
#[test]
fn print_1_to_3() {
let opt = Opt::from_iter(vec!["choose", "1:3"]);
assert_eq!(
vec!["is", "pretty"],
opt.choice[0].get_choice_slice(&String::from("rust is pretty cool"), &opt)
);
}
#[test]
fn print_1_to_3_inclusive() {
let opt = Opt::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)
);
}
#[test]
fn print_1_to_3_separated_by_hashtag() {
let opt = Opt::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)
);
}
#[test]
fn print_1_to_3_separated_by_varying_multiple_hashtag() {
let opt = Opt::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)
);
}
#[test]
fn print_1_to_3_separated_by_varying_multiple_hashtag_inclusive() {
let opt = Opt::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)
);
}
#[test]
fn print_1_to_3_separated_by_regex_group_vowels() {
let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "[aeiou]"]);
assert_eq!(
vec![" q", "ck br"],
opt.choice[0].get_choice_slice(
&String::from("the quick brown fox jumped over the lazy dog"),
&opt
)
);
}
#[test]
fn print_1_to_3_separated_by_regex_group_vowels_inclusive() {
let opt = Opt::from_iter(vec!["choose", "1:3", "-f", "[aeiou]", "-n"]);
assert_eq!(
vec![" q", "ck br", "wn f"],
opt.choice[0].get_choice_slice(
&String::from("the quick brown fox jumped over the lazy dog"),
&opt
)
);
}
}
}
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<String>,
/// 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<PathBuf>,
/// 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<Choice>,
}
use crate::config::Config;
pub type Range = (Option<u32>, Option<u32>);
@ -218,15 +15,15 @@ impl Choice {
pub fn print_choice(
&self,
line: &String,
opt: &Opt,
re: &Regex,
config: &Config,
handle: &mut BufWriter<std::io::StdoutLock>,
) {
write!(handle, "{}", self.get_choice_slice(line, opt, re).join(" "));
write!(handle, "{}", self.get_choice_slice(line, config).join(" "));
}
fn get_choice_slice<'a>(&self, line: &'a String, opt: &Opt, re: &Regex) -> Vec<&'a str> {
let words = re
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())
@ -244,7 +41,7 @@ impl Choice {
.map(|x| x.1)
.collect::<Vec<&str>>(),
(None, Some(end)) => {
let e: usize = if opt.inclusive {
let e: usize = if config.opt.inclusive {
(end + 1).try_into().unwrap()
} else {
(*end).try_into().unwrap()
@ -255,7 +52,7 @@ impl Choice {
.collect::<Vec<&str>>()
}
(Some(start), Some(end)) => {
let e: usize = if opt.inclusive {
let e: usize = if config.opt.inclusive {
(end + 1).try_into().unwrap()
} else {
(*end).try_into().unwrap()
@ -268,46 +65,121 @@ impl Choice {
},
}
}
pub fn parse_choice(src: &str) -> Result<Choice, ParseIntError> {
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 {
mod get_choice_slice_tests {
use crate::config::{Config, Opt};
use std::ffi::OsString;
use structopt::StructOpt;
impl Config {
pub fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
return Config::new(Opt::from_iter(iter));
}
}
#[test]
fn print_0() {
let config = Config::from_iter(vec!["choose", "0"]);
assert_eq!(
vec!["rust"],
config.opt.choice[0]
.get_choice_slice(&String::from("rust is pretty cool"), &config)
);
}
#[test]
fn print_after_end() {
let config = Config::from_iter(vec!["choose", "10"]);
assert_eq!(
Vec::<&str>::new(),
config.opt.choice[0]
.get_choice_slice(&String::from("rust is pretty cool"), &config)
);
}
#[test]
fn print_1_to_3() {
let config = Config::from_iter(vec!["choose", "1:3"]);
assert_eq!(
vec!["is", "pretty"],
config.opt.choice[0]
.get_choice_slice(&String::from("rust is pretty cool"), &config)
);
}
#[test]
fn print_1_to_3_inclusive() {
let config = Config::from_iter(vec!["choose", "1:3", "-n"]);
assert_eq!(
vec!["is", "pretty", "cool"],
config.opt.choice[0]
.get_choice_slice(&String::from("rust is pretty cool"), &config)
);
}
#[test]
fn print_1_to_3_separated_by_hashtag() {
let config = Config::from_iter(vec!["choose", "1:3", "-f", "#"]);
assert_eq!(
vec!["is", "pretty"],
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 config = Config::from_iter(vec!["choose", "1:3", "-f", "#"]);
assert_eq!(
vec!["is", "pretty"],
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 config = Config::from_iter(vec!["choose", "1:3", "-f", "#", "-n"]);
assert_eq!(
vec!["is", "pretty", "cool"],
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 config = Config::from_iter(vec!["choose", "1:3", "-f", "[aeiou]"]);
assert_eq!(
vec![" q", "ck br"],
config.opt.choice[0].get_choice_slice(
&String::from("the quick brown fox jumped over the lazy dog"),
&config
)
);
}
#[test]
fn print_1_to_3_separated_by_regex_group_vowels_inclusive() {
let config = Config::from_iter(vec!["choose", "1:3", "-f", "[aeiou]", "-n"]);
assert_eq!(
vec![" q", "ck br", "wn f"],
config.opt.choice[0].get_choice_slice(
&String::from("the quick brown fox jumped over the lazy dog"),
&config
)
);
}
}
}

177
src/config.rs Normal file
View file

@ -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<String>,
/// 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<PathBuf>,
/// 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<Choice>,
}
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<Choice, ParseIntError> {
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());
//}
}
}

View file

@ -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<dyn Read>,
None => Box::new(io::stdin()) as Box<dyn Read>,
};
@ -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, "");
}