mirror of
https://github.com/theryangeary/choose
synced 2024-11-22 19:03:05 +00:00
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:
parent
63b9eea62c
commit
1ee01c6de0
8 changed files with 965 additions and 462 deletions
10
readme.md
10
readme.md
|
@ -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
|
||||
|
|
949
src/choice.rs
949
src/choice.rs
File diff suppressed because it is too large
Load diff
141
src/config.rs
141
src/config.rs
|
@ -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
23
src/errors.rs
Normal 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 {}
|
|
@ -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() {
|
||||
|
|
15
src/opt.rs
15
src/opt.rs
|
@ -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
270
src/parse.rs
Normal 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
14
src/parse_error.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue