mirror of
https://github.com/theryangeary/choose
synced 2024-11-10 05:24:13 +00:00
Reorganize massively
This commit is contained in:
parent
f722cbcbbb
commit
c204f99444
3 changed files with 309 additions and 269 deletions
378
src/choice.rs
378
src/choice.rs
|
@ -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
177
src/config.rs
Normal 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());
|
||||
//}
|
||||
}
|
||||
|
||||
}
|
23
src/main.rs
23
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<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, "");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue