Add alternate Rust-y range syntax (#11)

Refactor to support choice 'kind'

Add ParseRangeError

Add rust syntax range parsing tests

Implement rust syntax parsing

Change parse to not anticipate exclusivity

Add choice tests

Implement Rust syntax choices

Show that there are multiple in opt.choice*s*

Refactor repeated code in choice tests

Update documentation to reflect Rust range syntax
This commit is contained in:
Ryan Geary 2020-06-08 14:25:15 -04:00
parent 63b9eea62c
commit 1ee01c6de0
8 changed files with 965 additions and 462 deletions

View file

@ -43,11 +43,11 @@ Please see our guidelines in [contributing.md](contributing.md).
```
$ choose --help
choose 1.1.1
choose 1.1.2
`choose` sections from each line of files
USAGE:
choose [FLAGS] [OPTIONS] <choice>...
choose [FLAGS] [OPTIONS] <choices>...
FLAGS:
-c, --character-wise Choose fields by character number
@ -65,8 +65,10 @@ OPTIONS:
-o, --output-field-separator <output-field-separator> Specify output field separator
ARGS:
<choice>... 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.
<choices>... Fields to print. Either a, a:b, a..b, or a..=b, where a and b are integers. The beginning or end
of a range can be omitted, resulting in including the beginning or end of the line,
respectively. a:b is inclusive of b (unless overridden by -x). a..b is exclusive of b and a..=b
is inclusive of b
```
### Examples

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,9 @@
use regex::Regex;
use std::num::ParseIntError;
use std::process;
use crate::choice::Choice;
use crate::choice::ChoiceKind;
use crate::opt::Opt;
lazy_static! {
static ref PARSE_CHOICE_RE: Regex = Regex::new(r"^(-?\d*):(-?\d*)$").unwrap();
}
pub struct Config {
pub opt: Opt,
pub separator: Regex,
@ -17,8 +12,10 @@ pub struct Config {
impl Config {
pub fn new(mut opt: Opt) -> Self {
if opt.exclusive {
for mut choice in &mut opt.choice {
for mut choice in &mut opt.choices {
if (opt.exclusive && choice.kind == ChoiceKind::ColonRange)
|| choice.kind == ChoiceKind::RustExclusiveRange
{
if choice.is_reverse_range() {
choice.start = choice.start - 1;
} else {
@ -68,132 +65,4 @@ impl Config {
output_separator,
}
}
pub fn parse_choice(src: &str) -> Result<Choice, ParseIntError> {
let cap = match PARSE_CHOICE_RE.captures_iter(src).next() {
Some(v) => v,
None => match src.parse() {
Ok(x) => return Ok(Choice::new(x, x)),
Err(e) => {
eprintln!("failed to parse choice argument: {}", src);
return Err(e);
}
},
};
let start = if cap[1].is_empty() {
0
} else {
match cap[1].parse() {
Ok(x) => x,
Err(e) => {
eprintln!("failed to parse range start: {}", &cap[1]);
return Err(e);
}
}
};
let end = if cap[2].is_empty() {
isize::max_value()
} else {
match cap[2].parse() {
Ok(x) => x,
Err(e) => {
eprintln!("failed to parse range end: {}", &cap[2]);
return Err(e);
}
}
};
return Ok(Choice::new(start, end));
}
pub fn parse_output_field_separator(src: &str) -> String {
String::from(src)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod parse_choice_tests {
use super::*;
#[test]
fn parse_single_choice_start() {
let result = Config::parse_choice("6").unwrap();
assert_eq!(6, result.start)
}
#[test]
fn parse_single_choice_end() {
let result = Config::parse_choice("6").unwrap();
assert_eq!(6, result.end)
}
#[test]
fn parse_none_started_range() {
let result = Config::parse_choice(":5").unwrap();
assert_eq!((0, 5), (result.start, result.end))
}
#[test]
fn parse_none_terminated_range() {
let result = Config::parse_choice("5:").unwrap();
assert_eq!((5, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_pos() {
let result = Config::parse_choice("5:7").unwrap();
assert_eq!((5, 7), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_neg() {
let result = Config::parse_choice("-3:-1").unwrap();
assert_eq!((-3, -1), (result.start, result.end))
}
#[test]
fn parse_neg_started_none_ended() {
let result = Config::parse_choice("-3:").unwrap();
assert_eq!((-3, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_none_started_neg_ended() {
let result = Config::parse_choice(":-1").unwrap();
assert_eq!((0, -1), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_neg() {
let result = Config::parse_choice("5:-3").unwrap();
assert_eq!((5, -3), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_pos() {
let result = Config::parse_choice("-3:5").unwrap();
assert_eq!((-3, 5), (result.start, result.end))
}
#[test]
fn parse_beginning_to_end_range() {
let result = Config::parse_choice(":").unwrap();
assert_eq!((0, isize::max_value()), (result.start, result.end))
}
#[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/errors.rs Normal file
View file

@ -0,0 +1,23 @@
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct ParseRangeError {
source_str: String,
}
impl ParseRangeError {
pub fn new(source_str: &str) -> Self {
ParseRangeError {
source_str: String::from(source_str),
}
}
}
impl fmt::Display for ParseRangeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.source_str)
}
}
impl Error for ParseRangeError {}

View file

@ -8,7 +8,10 @@ extern crate lazy_static;
mod choice;
mod config;
mod errors;
mod opt;
mod parse;
mod parse_error;
mod reader;
mod writeable;
mod writer;
@ -42,7 +45,7 @@ fn main() {
while let Some(line) = reader.read_line(&mut buffer) {
match line {
Ok(l) => {
let choice_iter = &mut config.opt.choice.iter().peekable();
let choice_iter = &mut config.opt.choices.iter().peekable();
while let Some(choice) = choice_iter.next() {
choice.print_choice(&l, &config, &mut handle);
if choice_iter.peek().is_some() {

View file

@ -2,7 +2,7 @@ use std::path::PathBuf;
use structopt::StructOpt;
use crate::choice::Choice;
use crate::config::Config;
use crate::parse;
#[derive(Debug, StructOpt)]
#[structopt(name = "choose", about = "`choose` sections from each line of files")]
@ -33,12 +33,13 @@ pub struct Opt {
pub non_greedy: bool,
/// Specify output field separator
#[structopt(short, long, parse(from_str = Config::parse_output_field_separator))]
#[structopt(short, long, parse(from_str = parse::output_field_separator))]
pub output_field_separator: Option<String>,
/// 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>,
/// Fields to print. Either a, a:b, a..b, or a..=b, where a and b are integers. The beginning
/// or end of a range can be omitted, resulting in including the beginning or end of the line,
/// respectively. a:b is inclusive of b (unless overridden by -x). a..b is
/// exclusive of b and a..=b is inclusive of b.
#[structopt(required = true, min_values = 1, parse(try_from_str = parse::choice))]
pub choices: Vec<Choice>,
}

270
src/parse.rs Normal file
View file

@ -0,0 +1,270 @@
use regex::Regex;
use crate::choice::{Choice, ChoiceKind};
use crate::errors::ParseRangeError;
use crate::parse_error::ParseError;
lazy_static! {
static ref PARSE_CHOICE_RE: Regex = Regex::new(r"^(-?\d*)(:|\.\.=?)(-?\d*)$").unwrap();
}
pub fn choice(src: &str) -> Result<Choice, ParseError> {
let cap = match PARSE_CHOICE_RE.captures_iter(src).next() {
Some(v) => v,
None => match src.parse() {
Ok(x) => return Ok(Choice::new(x, x, ChoiceKind::Single)),
Err(e) => {
eprintln!("failed to parse choice argument: {}", src);
return Err(ParseError::ParseIntError(e));
}
},
};
let start = if cap[1].is_empty() {
0
} else {
match cap[1].parse() {
Ok(x) => x,
Err(e) => {
eprintln!("failed to parse range start: {}", &cap[1]);
return Err(ParseError::ParseIntError(e));
}
}
};
let kind = match &cap[2] {
":" => ChoiceKind::ColonRange,
".." => ChoiceKind::RustExclusiveRange,
"..=" => ChoiceKind::RustInclusiveRange,
_ => {
eprintln!(
"failed to parse range: not a valid range separator: {}",
&cap[2]
);
return Err(ParseError::ParseRangeError(ParseRangeError::new(&cap[2])));
}
};
let end = if cap[3].is_empty() {
isize::max_value()
} else {
match cap[3].parse() {
Ok(x) => x,
Err(e) => {
eprintln!("failed to parse range end: {}", &cap[3]);
return Err(ParseError::ParseIntError(e));
}
}
};
return Ok(Choice::new(start, end, kind));
}
pub fn output_field_separator(src: &str) -> String {
String::from(src)
}
#[cfg(test)]
mod tests {
use crate::parse;
mod parse_choice_tests {
use super::*;
#[test]
fn parse_single_choice_start() {
let result = parse::choice("6").unwrap();
assert_eq!(6, result.start)
}
#[test]
fn parse_single_choice_end() {
let result = parse::choice("6").unwrap();
assert_eq!(6, result.end)
}
#[test]
fn parse_none_started_range() {
let result = parse::choice(":5").unwrap();
assert_eq!((0, 5), (result.start, result.end))
}
#[test]
fn parse_none_terminated_range() {
let result = parse::choice("5:").unwrap();
assert_eq!((5, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_pos() {
let result = parse::choice("5:7").unwrap();
assert_eq!((5, 7), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_neg() {
let result = parse::choice("-3:-1").unwrap();
assert_eq!((-3, -1), (result.start, result.end))
}
#[test]
fn parse_neg_started_none_ended() {
let result = parse::choice("-3:").unwrap();
assert_eq!((-3, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_none_started_neg_ended() {
let result = parse::choice(":-1").unwrap();
assert_eq!((0, -1), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_neg() {
let result = parse::choice("5:-3").unwrap();
assert_eq!((5, -3), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_pos() {
let result = parse::choice("-3:5").unwrap();
assert_eq!((-3, 5), (result.start, result.end))
}
#[test]
fn parse_beginning_to_end_range() {
let result = parse::choice(":").unwrap();
assert_eq!((0, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_bad_choice() {
assert!(parse::choice("d").is_err());
}
#[test]
fn parse_bad_range() {
assert!(parse::choice("d:i").is_err());
}
#[test]
fn parse_rust_inclusive_range() {
let result = parse::choice("3..=5").unwrap();
assert_eq!((3, 5), (result.start, result.end))
}
#[test]
fn parse_rust_inclusive_range_no_start() {
let result = parse::choice("..=5").unwrap();
assert_eq!((0, 5), (result.start, result.end))
}
#[test]
fn parse_rust_inclusive_range_no_end() {
let result = parse::choice("3..=").unwrap();
assert_eq!((3, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_rust_inclusive_range_no_start_or_end() {
let result = parse::choice("..=").unwrap();
assert_eq!((0, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_pos_rust_exclusive() {
let result = parse::choice("5..7").unwrap();
assert_eq!((5, 7), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_neg_rust_exclusive() {
let result = parse::choice("-3..-1").unwrap();
assert_eq!((-3, -1), (result.start, result.end))
}
#[test]
fn parse_neg_started_none_ended_rust_exclusive() {
let result = parse::choice("-3..").unwrap();
assert_eq!((-3, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_none_started_neg_ended_rust_exclusive() {
let result = parse::choice("..-1").unwrap();
assert_eq!((0, -1), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_neg_rust_exclusive() {
let result = parse::choice("5..-3").unwrap();
assert_eq!((5, -3), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_pos_rust_exclusive() {
let result = parse::choice("-3..5").unwrap();
assert_eq!((-3, 5), (result.start, result.end))
}
#[test]
fn parse_rust_exclusive_range() {
let result = parse::choice("3..5").unwrap();
assert_eq!((3, 5), (result.start, result.end))
}
#[test]
fn parse_rust_exclusive_range_no_start() {
let result = parse::choice("..5").unwrap();
assert_eq!((0, 5), (result.start, result.end))
}
#[test]
fn parse_rust_exclusive_range_no_end() {
let result = parse::choice("3..").unwrap();
assert_eq!((3, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_rust_exclusive_range_no_start_or_end() {
let result = parse::choice("..").unwrap();
assert_eq!((0, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_pos_rust_inclusive() {
let result = parse::choice("5..=7").unwrap();
assert_eq!((5, 7), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_neg_rust_inclusive() {
let result = parse::choice("-3..=-1").unwrap();
assert_eq!((-3, -1), (result.start, result.end))
}
#[test]
fn parse_neg_started_none_ended_rust_inclusive() {
let result = parse::choice("-3..=").unwrap();
assert_eq!((-3, isize::max_value()), (result.start, result.end))
}
#[test]
fn parse_none_started_neg_ended_rust_inclusive() {
let result = parse::choice("..=-1").unwrap();
assert_eq!((0, -1), (result.start, result.end))
}
#[test]
fn parse_full_range_pos_neg_rust_inclusive() {
let result = parse::choice("5..=-3").unwrap();
assert_eq!((5, -3), (result.start, result.end))
}
#[test]
fn parse_full_range_neg_pos_rust_inclusive() {
let result = parse::choice("-3..=5").unwrap();
assert_eq!((-3, 5), (result.start, result.end))
}
}
}

14
src/parse_error.rs Normal file
View file

@ -0,0 +1,14 @@
#[derive(Debug)]
pub enum ParseError {
ParseIntError(std::num::ParseIntError),
ParseRangeError(crate::errors::ParseRangeError),
}
impl ToString for ParseError {
fn to_string(&self) -> String {
match self {
ParseError::ParseIntError(e) => e.to_string(),
ParseError::ParseRangeError(e) => e.to_string(),
}
}
}