How to use a file with a BufReader and still be ab

2020-02-29 01:09发布

问题:

I want to open a file and read its contents as a BufReader using lines(). I also want to be able to seek to the end of the file and write some new lines.

Using let mut file lets me write to the file, but once I've given the file to the BufReader I can no longer write to it, as the main function no longer owns file:

fn main() {
    let filename = "tt.txt";

    // open a tt.txt file in the local directory
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(filename)
        .unwrap();

    // now read the whole file to get the latest state
    let date_re = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})").unwrap();
    let time_activity_re = Regex::new(r"^(\d{2}):(\d{2})\s*(.*)").unwrap();
    let reader = BufReader::new(file);
    let mut latest_date: Option<Date<Local>> = None;
    let mut latest_datetime: Option<DateTime<Local>> = None;
    let mut latest_activity: Option<String> = None;

    for wrapped_line in reader.lines() {
        let line = wrapped_line.unwrap();
        println!("line: {}", line);

        if date_re.is_match(&line) {
            let captures = date_re.captures(&line).unwrap();
            let year = captures.at(1).unwrap().parse::<i32>().unwrap();
            let month = captures.at(2).unwrap().parse::<u32>().unwrap();
            let day = captures.at(3).unwrap().parse::<u32>().unwrap();
            latest_date = Some(Local.ymd(year, month, day));
            latest_datetime = None;
            latest_activity = None;
        }

        if time_activity_re.is_match(&line) && latest_date != None {
            let captures = time_activity_re.captures(&line).unwrap();
            let hour = captures.at(1).unwrap().parse::<u32>().unwrap();
            let minute = captures.at(2).unwrap().parse::<u32>().unwrap();
            let activity = captures.at(3).unwrap();

            latest_datetime = Some(latest_date.unwrap().and_hms(hour, minute, 0));

            latest_activity = if activity.len() > 0 {
                // TODO: if latest_activity already constains a string, clear it and reuse it
                // as per: https://stackoverflow.com/questions/33781625/how-to-allocate-a-string-before-you-know-how-big-it-needs-to-be
                Some(activity.to_string())
            } else {
                None
            };

            println!("time activity: {} |{}|", latest_datetime.unwrap(), activity);
        }
    }

    // FIXME: I have to open a second file descriptor to the same file, in order to be able to write to it
    let mut out = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(filename)
        .unwrap();

    out.seek(End(0));

    let now = Local::now();
    if latest_date == None || latest_date.unwrap().year() != now.year()
        || latest_date.unwrap().month() != now.month()
        || latest_date.unwrap().day() != now.day()
    {
        if (latest_date != None) {
            // not an empy file, as far as tt is concerned
            out.write_all(b"\n\n");
        }
        out.write_all(format!("{}\n", now.format("%Y-%m-%d")).as_bytes());
        out.write_all(b"\n");
    }

    let activity = env::args().skip(1).join(" ");
    if (activity.len() > 0) {
        out.write_all(format!("{} {}\n", now.format("%H:%M"), activity).as_bytes());
    } else {
        // if there was no latest activity *and* there is no activity, then there's no point in writing a second blank line with just a time
        if latest_activity == None {
            return;
        }
        out.write_all(format!("{}\n", now.format("%H:%M")).as_bytes());
    }

    // FIXME: we're just relying on the program exit to close the two file descriptors (which point at the same file).
}

How can I use a single file descriptor to read existing lines and append new lines?

(Code from https://github.com/chrisdew/tt/blob/e899f252014391f2e01c3cc9e281cab1ab88936f/src/main.rs)

回答1:

To avoid moving a value, you can use a reference and a new scope. Here is how you could do this:

fn main() {
    let filename = "tt.txt";

    // open a tt.txt file in the local directory
    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(filename)
        .unwrap();

    // now read the whole file to get the latest state
    let date_re = Regex::new(r"^(\d{4})-(\d{2})-(\d{2})").unwrap();
    let time_activity_re = Regex::new(r"^(\d{2}):(\d{2})\s*(.*)").unwrap();
    {
        // BufReader now borrows the value instead of taking ownership.
        let reader = BufReader::new(&mut file);
        let mut latest_date: Option<Date<Local>> = None;
        let mut latest_datetime: Option<DateTime<Local>> = None;
        let mut latest_activity: Option<String> = None;

        for wrapped_line in reader.lines() {
            let line = wrapped_line.unwrap();
            println!("line: {}", line);

            if date_re.is_match(&line) {
                let captures = date_re.captures(&line).unwrap();
                let year = captures.at(1).unwrap().parse::<i32>().unwrap();
                let month = captures.at(2).unwrap().parse::<u32>().unwrap();
                let day = captures.at(3).unwrap().parse::<u32>().unwrap();
                latest_date = Some(Local.ymd(year, month, day));
                latest_datetime = None;
                latest_activity = None;
            }

            if time_activity_re.is_match(&line) && latest_date != None {
                let captures = time_activity_re.captures(&line).unwrap();
                let hour = captures.at(1).unwrap().parse::<u32>().unwrap();
                let minute = captures.at(2).unwrap().parse::<u32>().unwrap();
                let activity = captures.at(3).unwrap();

                latest_datetime = Some(latest_date.unwrap().and_hms(hour, minute, 0));

                latest_activity = if activity.len() > 0 {
                    // TODO: if latest_activity already constains a string, clear it and reuse it
                    // as per: https://stackoverflow.com/questions/33781625/how-to-allocate-a-string-before-you-know-how-big-it-needs-to-be
                    Some(activity.to_string())
                } else {
                    None
                };

                println!("time activity: {} |{}|", latest_datetime.unwrap(), activity);
            }
        }
    }
    // End of the scope, so now file is not borrowed anymore.

    file.seek(End(0));

    let now = Local::now();
    if latest_date == None || latest_date.unwrap().year() != now.year()
        || latest_date.unwrap().month() != now.month()
        || latest_date.unwrap().day() != now.day()
    {
        if (latest_date != None) {
            // not an empy file, as far as tt is concerned
            file.write_all(b"\n\n");
        }
        file.write_all(format!("{}\n", now.format("%Y-%m-%d")).as_bytes());
        file.write_all(b"\n");
    }

    let activity = env::args().skip(1).join(" ");
    if (activity.len() > 0) {
        file.write_all(format!("{} {}\n", now.format("%H:%M"), activity).as_bytes());
    } else {
        // if there was no latest activity *and* there is no activity, then there's no point in writing a second blank line with just a time
        if latest_activity == None {
            return;
        }
        file.write_all(format!("{}\n", now.format("%H:%M")).as_bytes());
    }

    // FIXME: we're just relying on the program exit to close the two file descriptors (which point at the same file).
}


回答2:

You can use BufReader::into_inner to "recover" the file after it's been passed to the BufReader. This can be used in conjunction with Read::by_ref to avoid giving away ownership of the BufReader<File> in the first place:

use std::fs::File;
use std::io::{BufRead, BufReader, Read, Write};

fn example(file: File) {
    let mut reader = BufReader::new(file);
    for _ in reader.by_ref().lines() {}

    let mut out = reader.into_inner();

    out.write_all(b"new stuff").unwrap();
}

Here is antoyo's solution with similar reduced code:

use std::fs::File;
use std::io::{BufRead, BufReader, Write};

fn example(mut file: File) {
    {
        let reader = BufReader::new(&file);
        for _ in reader.lines() {}
    }

    file.write_all(b"new stuff").unwrap();
}

When non-lexical lifetimes (NLL) are implemented, this will be able to be simplified by removing the extra braces.