Skip to content
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

Merged
merged 6 commits into from
Jun 12, 2018
Merged

add line-range #162

merged 6 commits into from
Jun 12, 2018

Conversation

tskinn
Copy link
Contributor

@tskinn tskinn commented Jun 1, 2018

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?

Copy link
Owner

@sharkdp sharkdp left a 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-",
Copy link
Owner

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")),
Copy link
Owner

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)> {
Copy link
Owner

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)>,
Copy link
Owner

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':' {
Copy link
Owner

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(&regions)?;
if printer.config.line_range.is_some() {
Copy link
Owner

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.

@tskinn
Copy link
Contributor Author

tskinn commented Jun 2, 2018

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.

@tskinn
Copy link
Contributor Author

tskinn commented Jun 4, 2018

@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.

Copy link
Owner

@sharkdp sharkdp left a 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 {
Copy link
Owner

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':' {
Copy link
Owner

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 {
Copy link
Owner

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").

Copy link
Contributor Author

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?

Copy link
Owner

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!");
Copy link
Owner

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").

Copy link
Contributor Author

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?

Copy link
Owner

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.

Copy link
Contributor Author

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);
}
Copy link
Owner

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>(
Copy link
Owner

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);
Copy link
Owner

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),
Copy link
Owner

@sharkdp sharkdp Jun 6, 2018

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

Copy link
Contributor Author

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.

Copy link
Owner

@sharkdp sharkdp left a 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!

@sharkdp sharkdp changed the title [WIP] add line-range add line-range Jun 12, 2018
@sharkdp sharkdp merged commit 65e6970 into sharkdp:master Jun 12, 2018
@sharkdp sharkdp mentioned this pull request Jun 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants