-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add line-range #162
add line-range #162
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much!
I added some inline comments. You can leave out the error handling part at first, if you want. This will probably be quite challenging if you are not (yet) too familiar with Rust.
I wasn't sure of the best place to put the logic for checking line range. I started with putting it in the print_line function but moved it out to be able to short circuit and stop processing once out of the range.
👍 This way we can also easily write unit tests (which we should add before merging this).
I didn't add the ability to do multiple ranges yet but it should be fairly simple.
Okay. Let's leave this out for now and add in a second PR.
How robust of error checking do we need on validating the line ranges? Like do we want to sort or make sure they are not conflicting or that they are in least to greatest order or anything like that?
I think we can also silently ignore it if someone puts --line-range 9:3
. They won't get any output. Showing an error would also be fine for me.
As for multiple ranges, I think they cannot be conflicting because we would add the ranges, right? --line-range 3:5,7:9
would show lines 3,4,5,7,8,9. --line-range 3:7,5:9
would show all lines from 3 to 9.
src/app.rs
Outdated
.help("Print range of lines") | ||
.long_help( | ||
"Print a specified range of lines from the files \ | ||
--line-range 30-40 or --line-range 30-", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe: "for example '--line-range 30-40' or .."?
src/app.rs
Outdated
@@ -269,6 +280,7 @@ impl App { | |||
term_width: Term::stdout().size().1 as usize, | |||
files, | |||
theme: self.matches.value_of("theme"), | |||
line_range: get_ranges(self.matches.value_of("line-range")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would write this as self.matches.value_of("line-range").map(get_ranges)
. This would allow us to get rid of the outermost match
in get_ranges
. The signature would change to fn get_ranges(value: &str) -> ..
.
src/app.rs
Outdated
} | ||
|
||
fn is_truecolor_terminal() -> bool { | ||
env::var("COLORTERM") | ||
.map(|colorterm| colorterm == "truecolor" || colorterm == "24bit") | ||
.unwrap_or(false) | ||
} | ||
|
||
fn get_ranges(value: Option<&str>) -> Option<(usize, usize)> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few things:
- Maybe rename this to
parse_ranges
? - This should probably return a
Result<...>
in order to properly handle the errors that may occur. - The
Option<..>
is probably not necessary if we use.map(get_ranges)
, as suggested above.
src/app.rs
Outdated
@@ -322,10 +334,37 @@ pub struct Config<'a> { | |||
pub term_width: usize, | |||
pub files: Vec<Option<&'a str>>, | |||
pub theme: Option<&'a str>, | |||
pub line_range: Option<(usize, usize)>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be worth to write a small struct LineRange
that instead of (usize, usize)
. The get_ranges
/parse_ranges
function could then be a static LineRange::new
or LineRange::from
function.
I think it would also be cleaner to represent the open bounds as None
instead of min_value
/max_value
:
struct LineRange {
lower: Option<usize>,
upper: Option<usize>,
}
impl LineRange {
pub fn new(value: &str) -> Self {
// ...
}
}
This might also complicate some things, so I'm not quite sure.
src/app.rs
Outdated
None => None, | ||
Some(str_range) => { | ||
let mut new_range = (usize::min_value(), usize::max_value()); | ||
if str_range.bytes().nth(0).expect("Something should be here!") == b':' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We typically try to avoid expect
or unwrap
because these methods will panic (i.e. immediately abort the program) if the input is not as expected, preventing any potential cleanup operations from happening. The safe way to do this is via Result<..>
, as suggested above (see https://doc.rust-lang.org/book/second-edition/ch09-02-recoverable-errors-with-result.html - in particular the usage of the ?
operator).
The help text uses n-m
, but here you seem to implement n:m
?
src/features.rs
Outdated
while reader.read_until(b'\n', &mut buffer)? > 0 { | ||
{ | ||
let line = String::from_utf8_lossy(&buffer); | ||
let regions = highlighter.highlight(line.as_ref()); | ||
|
||
printer.print_line(®ions)?; | ||
if printer.config.line_range.is_some() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should probably match
on the Option
here in order to avoid the unwrap()
calls below.
I appreciate the feedback. I will start working on this. Thanks for you patience. Feel free to let me know if you think I'm going in the completely wrong direction with this. |
@sharkdp I changed the code quite a bit and learned enough rust to try to implement some of the things you mentioned, but I wan't able to implement it exactly like you suggested in a few cases. I kept running in circles trying to fit your different suggestions together with my limited knowledge of rust but it is working! I would appreciate more feedback and guidance. I'm sure there are better ways to do some of these things and probably some style changes are needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much for these great changes and for your persistence!
src/app.rs
Outdated
} | ||
|
||
impl LineRange { | ||
pub fn from(range_raw: &str) -> LineRange { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should also return a Result<LineRange>
(by removing the unwrap_or
call below). This way, we can pass the error messages through to the call-site.
src/app.rs
Outdated
pub fn parse_range(range_raw: &str) -> Result<LineRange> { | ||
let mut new_range = LineRange::new(); | ||
|
||
if range_raw.bytes().nth(0).ok_or("No first byte")? == b':' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the error message here (and below) should be "Empty line range"?
} | ||
|
||
let line_numbers: Vec<&str> = range_raw.split(':').collect(); | ||
if line_numbers.len() == 2 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have an else
branch here which returns a error ("expected single ':' character").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a silly question but how do I go about creating an error? When I try to return
Err("expected single....")
I get a mismatched types error: expected struct `errors::Error`, found reference
Do I need to implement a new error type or something like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to use Err("expected single....".into())
.
src/app.rs
Outdated
|
||
#[test] | ||
fn test_parse_line_range_full() { | ||
let range = LineRange::from(Some("40:50")).expect("Shouldn't fail on test!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would turn into ::from("40:50")
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this only work if the tests are in a new module called LineRange?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry. I just meant to say that the Some
part will not be needed anymore. I would still use LineRange::from
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh duh. Gotcha
src/app.rs
Outdated
let range = LineRange::from(Some("40:")).expect("Shouldn't fail on test!"); | ||
assert_eq!(40, range.lower); | ||
assert_eq!(usize::max_value(), range.upper); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we change the signature of LineRange::from
to return a Result
, we can also add some tests for negative cases.
src/features.rs
Outdated
None => Box::new(stdin.lock()), | ||
Some(filename) => Box::new(BufReader::new(File::open(filename)?)), | ||
}; | ||
fn print_file_ranges<'a>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be really great if we had just a single function and would avoid this code duplication. I would try to change the ranges
parameter to accept an Option<Range>
.
@@ -41,6 +41,7 @@ mod errors { | |||
Clap(::clap::Error); | |||
Io(::std::io::Error); | |||
SyntectError(::syntect::LoadingError); | |||
ParseIntError(::std::num::ParseIntError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
src/app.rs
Outdated
@@ -269,6 +282,7 @@ impl App { | |||
term_width: Term::stdout().size().1 as usize, | |||
files, | |||
theme: self.matches.value_of("theme"), | |||
line_range: self.matches.value_of("line-range").map(LineRange::from), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we change the signature of LineRange::from
, as suggested above, we could use
line_range: tranpose(self.matches.value_of("line-range").map(LineRange::from))
such that the parsing-error-messages would bubble up to the main function (and reported to the user).
Here, transpose
is a little helper function that turns a "Option of Result" into a "Result of Option". It could be implemented like this:
fn transpose<T, E>(opt: Option<Result<T, E>>) -> Result<Option<T>, E> {
opt.map_or(Ok(None), |res| res.map(Some))
}
This function is actually an experimental feature which means that we could soon replace this by a call to the standard-library .transpose()
: https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.transpose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I think I see what you are saying now. We want to bubble up any errors that happen when parsing the linerange to bubble up from the config() function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much!
This is my first attempt at actually writing some rust code so bear with me.
I wasn't sure of the best place to put the logic for checking line range. I started with putting it in the print_line function but moved it out to be able to short circuit and stop processing once out of the range.
I didn't add the ability to do multiple ranges yet but it should be fairly simple.
How robust of error checking do we need on validating the line ranges? Like do we want to sort or make sure they are not conflicting or that they are in least to greatest order or anything like that?