How to manage the ownership of a file held within a struct in Rust? - io

Is there a good way to handle the ownership of a file held within a struct using Rust? As a stripped down example, consider:
// Buffered file IO
use std::io::{BufReader,BufRead};
use std::fs::File;
// Structure that contains a file
#[derive(Debug)]
struct Foo {
file : BufReader <File>,
data : Vec <f64>,
}
// Reads the file and strips the header
fn init_foo(fname : &str) -> Foo {
// Open the file
let mut file = BufReader::new(File::open(fname).unwrap());
// Dump the header
let mut header = String::new();
let _ = file.read_line(&mut header);
// Return our foo
Foo { file : file, data : Vec::new() }
}
// Read the remaining foo data and process it
fn read_foo(mut foo : Foo) -> Foo {
// Strip one more line
let mut header_alt = String::new();
let _ = foo.file.read_line(&mut header_alt);
// Read in the rest of the file line by line
let mut data = Vec::new();
for (lineno,line) in foo.file.lines().enumerate() {
// Strip the error
let line = line.unwrap();
// Print some diagnostic information
println!("Line {}: val {}",lineno,line);
// Save the element
data.push(line.parse::<f64>().unwrap());
}
// Export foo
Foo { data : data, ..foo}
}
fn main() {
// Initialize our foo
let foo = init_foo("foo.txt");
// Read in our data
let foo = read_foo(foo);
// Print out some debugging info
println!("{:?}",foo);
}
This currently gives the compilation error:
error[E0382]: use of moved value: `foo.file`
--> src/main.rs:48:5
|
35 | for (lineno,line) in foo.file.lines().enumerate() {
| -------- value moved here
...
48 | Foo { data : data, ..foo}
| ^^^^^^^^^^^^^^^^^^^^^^^^^ value used here after move
|
= note: move occurs because `foo.file` has type `std::io::BufReader<std::fs::File>`, which does not implement the `Copy` trait
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
error: Could not compile `rust_file_struct`.
To learn more, run the command again with --verbose.
And, to be sure, this makes sense. Here, lines() takes ownership of the buffered file, so we can't use the value in the return. What's confusing me is a better way to handle this situation. Certainly, after the for loop, the file is consumed, so it really can't be used. To better denote this, we could represent file as Option <BufReader <File>>. However, this causes some grief because the second read_line call, inside of read_foo, needs a mutable reference to file and I'm not sure how to obtain one it it's wrapped inside of an Option. Is there a good way of handling the ownership?
To be clear, this is a stripped down example. In the actual use case, there are several files as well as other data. I've things structured in this way because it represents a configuration that comes from the command line options. Some of the options are files, some are flags. In either case, I'd like to do some processing, but not all, of the files early in order to throw the appropriate errors.

I think you're on track with using the Option within the Foo struct. Assuming the struct becomes:
struct Foo {
file : Option<BufReader <File>>,
data : Vec <f64>,
}
The following code is a possible solution:
// Reads the file and strips the header
fn init_foo(fname : &str) -> Foo {
// Open the file
let mut file = BufReader::new(File::open(fname).unwrap());
// Dump the header
let mut header = String::new();
let _ = file.read_line(&mut header);
// Return our foo
Foo { file : Some(file), data : Vec::new() }
}
// Read the remaining foo data and process it
fn read_foo(foo : Foo) -> Option<Foo> {
let mut file = foo.file?;
// Strip one more line
let mut header_alt = String::new();
let _ = file.read_line(&mut header_alt);
// Read in the rest of the file line by line
let mut data = Vec::new();
for (lineno,line) in file.lines().enumerate() {
// Strip the error
let line = line.unwrap();
// Print some diagnostic information
println!("Line {}: val {}",lineno,line);
// Save the element
data.push(line.parse::<f64>().unwrap());
}
// Export foo
Some(Foo { data : data, file: None})
}
Note in this case that read_foo returns an optional Foo due to the fact that the file could be None.
On a side note, IMO, unless you absolutely need the BufReader to be travelling along with the Foo, I would discard it. As you've already found, calling lines causes a move, which makes it difficult to retain within another struct. As a suggestion, you could make the file field simply a String so that you could always derive the BufReader and read the file when needed.
For example, here's a solution where a file name (i.e. a &str) can be turned into a Foo with all the line processing done just before the construction of the struct.
// Buffered file IO
use std::io::{BufReader,BufRead};
use std::fs::File;
// Structure that contains a file
#[derive(Debug)]
struct Foo {
file : String,
data : Vec <f64>,
}
trait IntoFoo {
fn into_foo(self) -> Foo;
}
impl IntoFoo for &str {
fn into_foo(self) -> Foo {
// Open the file
let mut file = BufReader::new(File::open(self).unwrap());
// Dump the header
let mut header = String::new();
let _ = file.read_line(&mut header);
// Strip one more line
let mut header_alt = String::new();
let _ = file.read_line(&mut header_alt);
// Read in the rest of the file line by line
let mut data = Vec::new();
for (lineno,line) in file.lines().enumerate() {
// Strip the error
let line = line.unwrap();
// Print some diagnostic information
println!("Line {}: val {}",lineno,line);
// Save the element
data.push(line.parse::<f64>().unwrap());
}
Foo { file: self.to_string(), data }
}
}
fn main() {
// Read in our data from the file
let foo = "foo.txt".into_foo();
// Print out some debugging info
println!("{:?}",foo);
}
In this case, there's no need to worry about the ownership of the BufReader because it's created, used, and discarded in the same function. Of course, I don't fully know your use case, so this may not be suitable for your implementation.

Related

how to pass every line from a text file as an argument in rust

i have made this code to check for alive urls in a text file it was first to check for a single url the script worked but then i wanted to make it multithreaded i got this error
error
here is the original code :
use hyper_tls::HttpsConnector;
use hyper::Client;
use tokio::io::BufReader;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let https = HttpsConnector::new();
let url = std::env::args().nth(1).expect("no list given");
let client = Client::builder().build::<_, hyper::Body>(https);
let reader = BufReader::new(url);
let lines = reader.lines();
for l in lines {
let sep = l.parse()?;
// Await the response...
let resp = client.get(sep).await?;
if resp.status() == 200 {
println!("{}", l);}
if resp.status() == 301 {
println!("{}", l); }
}
Ok(())
}
the issue seems to be that you are passing in the file's name as opposed to its content to the BufReader.
In order to read the contents instead, you can use a tokio::fs:File.
Here's an example of reading a file and printing its lines to stdout using tokio and a BufReader:
use tokio::{
fs::File,
io::{
// This trait needs to be imported, as the lines function being
// used on reader is defined there
AsyncBufReadExt,
BufReader
}
};
#[tokio::main]
async fn main() {
// get file command line argument
let file_argument = std::env::args().nth(1).expect("Please provide a file as command line argument.");
// open file
let file = File::open(file_argument).await.expect("Failed to open file");
// create reader using file
let reader = BufReader::new(file);
// get iterator over lines
let mut lines = reader.lines();
// this has to be used instead of a for loop, since lines isn't a
// normal iterator, but a Lines struct, the next element of which
// can be obtained using the next_line function.
while let Some(line) = lines.next_line().await.expect("Failed to read file") {
// print current line
println!("{}", line);
}
}

Creates a temporary which is freed while still in use

I'm creating a small application that explores variable lifetimes and threads. I want to load in a file once, and then use its contents (in this case an audio file) in a separate channel. I am having issues with value lifetimes.
I'm almost certain the syntax is wrong for what I have so far (for creating a static variable), but I can't find any resources for File types and lifetimes. What I have thus far produces this error:
let file = &File::open("src/censor-beep-01.wav").unwrap();
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ creates a temporary which is freed while still in use
let x: &'static File = file;
------------- type annotation requires that borrow lasts for `'static`
The code I currently have is:
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_must_use)]
#![allow(unused_variables)]
use std::io::{self, BufRead, BufReader, stdin, Read};
use std::sync::mpsc::{self, TryRecvError};
use std::thread;
use std::time::Duration;
use std::fs::File;
use std::rc::Rc;
use rodio::Source;
fn main() {
let file = &File::open("src/censor-beep-01.wav").unwrap();
let x: &'static File = file;
loop {
let (tx, rx) = mpsc::channel();
thread::spawn(move || loop {
let tmp = x;
let (stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
let source = rodio::Decoder::new(BufReader::new(tmp)).unwrap();
stream_handle.play_raw(source.convert_samples());
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
break;
}
Err(TryRecvError::Empty) => {
println!("z");
thread::sleep(Duration::from_millis(1000));
}
}
});
let mut line = String::new();
let stdin = io::stdin();
let _ = stdin.lock().read_line(&mut line);
let _ = tx.send(());
return;
}
}
You need to wrap the file with Arc and Mutex like Arc::new(Mutex::new(file)) and then clone the file before passing it to the thread.
Arc is used for reference counting, which is needed to share the target object (in your case it is a file) across the thread and Mutex is needed to access the target object synchronously.
sample code (I have simplified your code to make it more understandable):
let file = Arc::new(Mutex::new(File::open("src/censor-beep-01.wav").unwrap()));
loop {
let file = file.clone();
thread::spawn(move || loop {
let mut file_guard = match file.lock() {
Ok(guard) => guard,
Err(poison) => poison.into_inner()
};
let file = file_guard.deref();
// now you can pass above file object to BufReader like "BufReader::new(file)"
});
}
reason for creates a temporary which is freed while still in use error:
You have only stored the reference of the file without the actual file object. so, the object will be droped in that line itself.

Reading ZIP file in Rust causes data owned by the current function

I'm new to Rust and am likely have a huge knowledge gap. Basically, I'm hoping to be create a utility function that would except a regular text file or a ZIP file and return a BufRead where the caller can start processing line by line. It is working well for non ZIP files but I am not understanding how to achieve the same for the ZIP files. The ZIP files will only contain a single file within the archive which is why I'm only processing the first file in the ZipArchive.
I'm running into the the following error.
error[E0515]: cannot return value referencing local variable `archive_contents`
--> src/file_reader.rs:30:9
|
27 | let archive_file: zip::read::ZipFile = archive_contents.by_index(0).unwrap();
| ---------------- `archive_contents` is borrowed here
...
30 | Ok(Box::new(BufReader::with_capacity(128 * 1024, archive_file)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
It seems the archive_contents is preventing the BufRead object from returning to the caller. I'm just not sure how to work around this.
file_reader.rs
use std::ffi::OsStr;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::path::Path;
pub struct FileReader {
pub file_reader: Result<Box<BufRead>, &'static str>,
}
pub fn file_reader(filename: &str) -> Result<Box<BufRead>, &'static str> {
let path = Path::new(filename);
let file = match File::open(&path) {
Ok(file) => file,
Err(why) => panic!(
"ERROR: Could not open file, {}: {}",
path.display(),
why.to_string()
),
};
if path.extension() == Some(OsStr::new("zip")) {
// Processing ZIP file.
let mut archive_contents: zip::read::ZipArchive<std::fs::File> =
zip::ZipArchive::new(file).unwrap();
let archive_file: zip::read::ZipFile = archive_contents.by_index(0).unwrap();
// ERRORS: returns a value referencing data owned by the current function
Ok(Box::new(BufReader::with_capacity(128 * 1024, archive_file)))
} else {
// Processing non-ZIP file.
Ok(Box::new(BufReader::with_capacity(128 * 1024, file)))
}
}
main.rs
mod file_reader;
use std::io::BufRead;
fn main() {
let mut files: Vec<String> = Vec::new();
files.push("/tmp/text_file.txt".to_string());
files.push("/tmp/zip_file.zip".to_string());
for f in files {
let mut fr = match file_reader::file_reader(&f) {
Ok(fr) => fr,
Err(e) => panic!("Error reading file."),
};
fr.lines().for_each(|l| match l {
Ok(l) => {
println!("{}", l);
}
Err(e) => {
println!("ERROR: Failed to read line:\n {}", e);
}
});
}
}
Any help is greatly appreciated!
It seems the archive_contents is preventing the BufRead object from returning to the caller. I'm just not sure how to work around this.
You have to restructure the code somehow. The issue here is that, well, the archive data is part of the archive. So unlike file, archive_file is not an independent item, it is rather a pointer of sort into the archive itself. Which means the archive needs to live longer than archive_file for this code to be correct.
In a GC'd language this isn't an issue, archive_file has a reference to archive and will keep it alive however long it needs. Not so for Rust.
A simple way to fix this would be to just copy the data out of archive_file and into an owned buffer you can return to the parent. An other option might be to return a wrapper for (archive_contents, item_index), which would delegate the reading (might be somewhat tricky though). Yet another would be to not have file_reader.
Thanks to #Masklinn for the direction! Here's the working solution using their suggestion.
file_reader.rs
use std::ffi::OsStr;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Cursor;
use std::io::Error;
use std::io::Read;
use std::path::Path;
use zip::read::ZipArchive;
pub fn file_reader(filename: &str) -> Result<Box<dyn BufRead>, Error> {
let path = Path::new(filename);
let file = match File::open(&path) {
Ok(file) => file,
Err(why) => return Err(why),
};
if path.extension() == Some(OsStr::new("zip")) {
let mut archive_contents = ZipArchive::new(file)?;
let mut archive_file = archive_contents.by_index(0)?;
// Read the contents of the file into a vec.
let mut data = Vec::new();
archive_file.read_to_end(&mut data)?;
// Wrap vec in a std::io::Cursor.
let cursor = Cursor::new(data);
Ok(Box::new(cursor))
} else {
// Processing non-ZIP file.
Ok(Box::new(BufReader::with_capacity(128 * 1024, file)))
}
}
While the solution you have settled on does work, it has a few disadvantages. One is that when you read from a zip file, you have to read the contents of the file you want to process into memory before proceeding, which might be impractical for a large file. Another is that you have to heap allocate the BufReader in either case.
Another possibly more idiomatic solution is to restructure your code, such that the BufReader does not need to be returned from the function at all - rather, structure your code so that it has a function that opens the file, which in turn calls a function that processes the file:
use std::ffi::OsStr;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::path::Path;
pub fn process_file(filename: &str) -> Result<usize, String> {
let path = Path::new(filename);
let file = match File::open(&path) {
Ok(file) => file,
Err(why) => return Err(format!(
"ERROR: Could not open file, {}: {}",
path.display(),
why.to_string()
)),
};
if path.extension() == Some(OsStr::new("zip")) {
// Handling a zip file
let mut archive_contents=zip::ZipArchive::new(file).unwrap();
let mut buf_reader = BufReader::with_capacity(128 * 1024,archive_contents.by_index(0).unwrap());
process_reader(&mut buf_reader)
} else {
// Handling a plain file.
process_reader(&mut BufReader::with_capacity(128 * 1024, file))
}
}
pub fn process_reader(reader: &mut dyn BufRead) -> Result<usize, String> {
// Example, just count the number of lines
return Ok(reader.lines().count());
}
fn main() {
let mut files: Vec<String> = Vec::new();
files.push("/tmp/text_file.txt".to_string());
files.push("/tmp/zip_file.zip".to_string());
for f in files {
match process_file(&f) {
Ok(count) => println!("File {} Count: {}", &f, count),
Err(e) => println!("Error reading file: {}", e),
};
}
}
This way, you don't need any Boxes and you don't need to read the file into memory before processing it.
A drawback to this solution would if you had multiple functions that need to be able to read from zip files. One way to handle that would be to define process_file to take a callback function to do the processing. First you would change the definition of process_file to be:
pub fn process_file<C>(filename: &str, process_reader: C) -> Result<usize, String>
where C: FnOnce(&mut dyn BufRead)->Result<usize, String>
The rest of the function body can be left unchanged. Now, process_reader can be passed into the function, like this:
process_file(&f, count_lines)
where count_lines would be the original simple function to count the lines, for instance.
This would also allow you to pass in a closure:
process_file(&f, |reader| Ok(reader.lines().count()))

Delegating the creation of data structures

I’m very new to Rust. While trying out small things, I have written the following code. It simply scans files (given as arguments) for a specific string (“Started “) and prints out the matching lines:
use std::os;
use std::io::BufferedReader;
use std::io::File;
fn main() {
for target in os::args().iter() {
scan_file(target);
}
}
fn scan_file(path_str: &String) {
let path = Path::new(path_str.as_bytes());
let file = File::open(&path);
let mut reader = BufferedReader::new(file);
for line in reader.lines() {
match line {
Ok(s) => {
if s.as_slice().contains("Started ") {
print!("{}", s);
}
}
Err(_) => return,
}
}
}
My question is: how can I refactor the function scan_file so that it looks something like this (or similar enough)?:
fn scan_file(path_str: &String) {
for line in each_line_in_file_with_path(path_str) {
match line {
Ok(s) => {
if s.as_slice().contains("Started ") {
print!("{}", s);
}
}
Err(_) => return,
}
}
}
In this new version of the function, the three variable declarations are gone. Instead, the function each_line_in_file_with_path is expected to handle all the “turn a path into lines”, returning an iterator.
I’ve tried a number of things unsuccessfully, always due to variables going out of scope too early for my needs. I understand the problems I have (I think), but can’t find anywhere a good explanation of how this should be handled.
It is not possible to implement a working each_line_in_file_with_path function — at least, not without adding some overhead and unsafe code.
Let's look at the values involved and their types. First is path, of type Path (either posix::Path or windows::Path). The constructors for these types receive a BytesContainer by value, therefore they take ownership of it. No issues here.
Next is file, of type IoResult<File>. File::open() clones the path it receives, so again, no issues here.
Next is reader, of type BufferedReader<IoResult<File>>. Just like Path, the constructor for BufferedReader takes its argument by value and takes ownership of it.
The problem is with reader.lines(). This value is of type Lines<'r, T: 'r>. As the type signature suggests, this struct contains a borrowed reference. The signature of lines shows the relationship between the loaner and the borrower:
fn lines<'r>(&'r mut self) -> Lines<'r, Self>
How do we define each_line_in_file_with_path now? each_line_in_file_with_path cannot return a Lines directly. You probably tried writing the function like this:
fn each_line_in_file_with_path<'a, T>(path: &T) -> Lines<'a, BufferedReader<IoResult<File>>>
where T: BytesContainer {
let path = Path::new(path);
let file = File::open(&path);
let reader = BufferedReader::new(file);
reader.lines()
}
This gives a compilation error:
main.rs:46:5: 46:11 error: `reader` does not live long enough
main.rs:46 reader.lines()
^~~~~~
main.rs:42:33: 47:2 note: reference must be valid for the lifetime 'a as defined on the block at 42:32...
main.rs:42 where T: BytesContainer {
main.rs:43 let path = Path::new(path);
main.rs:44 let file = File::open(&path);
main.rs:45 let reader = BufferedReader::new(file);
main.rs:46 reader.lines()
main.rs:47 }
main.rs:42:33: 47:2 note: ...but borrowed value is only valid for the block at 42:32
main.rs:42 where T: BytesContainer {
main.rs:43 let path = Path::new(path);
main.rs:44 let file = File::open(&path);
main.rs:45 let reader = BufferedReader::new(file);
main.rs:46 reader.lines()
main.rs:47 }
error: aborting due to previous error
That's because we're trying to return a Lines that refers to a BufferedReader that ceases to exist when the function returns (the Lines would contain a dangling pointer).
Now, one might think, “I'll just return the BufferedReader along with the Lines”.
struct LinesInFileIterator<'a> {
reader: BufferedReader<IoResult<File>>,
lines: Lines<'a, BufferedReader<IoResult<File>>>
}
impl<'a> Iterator<IoResult<String>> for LinesInFileIterator<'a> {
fn next(&mut self) -> Option<IoResult<String>> {
self.lines.next()
}
}
fn each_line_in_file_with_path<'a, T>(path: &T) -> LinesInFileIterator<'a>
where T: BytesContainer {
let path = Path::new(path);
let file = File::open(&path);
let reader = BufferedReader::new(file);
LinesInFileIterator {
reader: reader,
lines: reader.lines()
}
}
This doesn't work either:
main.rs:46:16: 46:22 error: `reader` does not live long enough
main.rs:46 lines: reader.lines()
^~~~~~
main.rs:40:33: 48:2 note: reference must be valid for the lifetime 'a as defined on the block at 40:32...
main.rs:40 where T: BytesContainer {
main.rs:41 let path = Path::new(path);
main.rs:42 let file = File::open(&path);
main.rs:43 let reader = BufferedReader::new(file);
main.rs:44 LinesInFileIterator {
main.rs:45 reader: reader,
...
main.rs:40:33: 48:2 note: ...but borrowed value is only valid for the block at 40:32
main.rs:40 where T: BytesContainer {
main.rs:41 let path = Path::new(path);
main.rs:42 let file = File::open(&path);
main.rs:43 let reader = BufferedReader::new(file);
main.rs:44 LinesInFileIterator {
main.rs:45 reader: reader,
...
main.rs:46:16: 46:22 error: use of moved value: `reader`
main.rs:46 lines: reader.lines()
^~~~~~
main.rs:45:17: 45:23 note: `reader` moved here because it has type `std::io::buffered::BufferedReader<core::result::Result<std::io::fs::File, std::io::IoError>>`, which is non-copyable
main.rs:45 reader: reader,
^~~~~~
error: aborting due to 2 previous errors
Basically, we can't have a struct that contains a borrowed reference that points to another member of the struct, because when the struct is moved, the reference would become invalid.
There are 2 solutions:
Make a function that returns a BufferedReader from a file path, and call .lines() on it in your for loop.
Make a function that accepts a closure that receives each line.
fn main() {
for target in os::args().iter() {
scan_file(target.as_slice());
}
}
fn for_each_line_in_file_with_path_do(path: &str, action: |IoResult<String>|) {
let path = Path::new(path.as_bytes());
let file = File::open(&path);
let mut reader = BufferedReader::new(file);
for line in reader.lines() {
action(line);
}
}
fn scan_file(path_str: &str) {
for_each_line_in_file_with_path_do(path_str, |line| {
match line {
Ok(s) => {
if s.as_slice().contains("Started ") {
print!("{}", s);
}
}
Err(_) => return,
}
});
}
You won't be able to do it without some boilerplate. You need to have some source of data, and because iterators return their data in chunks, they either have to contain the data or to have a reference into some other source of this data (this also includes iterators which return data from external source, e.g. lines in a file).
However, because you want to "encapsulate" your iterator into a function call, this iterator cannot be of the second kind, i.e. it cannot contain references, because all references it could contain would point to this function call stack. Consequently, the iterator's source can only be contained in this iterator.
And this is the boilerplate problem - in general there is no such iterator in the standard library. You will need to create it yourself. In this particular case, though, you can get away without implementing Iterator trait manually. You only need to create some simple structural wrapper:
use std::os;
use std::io::{BufferedReader, File, Lines};
fn main() {
for target in os::args().iter() {
scan_file(target.as_slice());
}
}
struct FileLines {
source: BufferedReader<File>
}
impl FileLines {
fn new(path_str: &str) -> FileLines {
let path = Path::new(path_str.as_bytes());
let file = File::open(&path).unwrap();
let reader = BufferedReader::new(file);
FileLines { source: reader }
}
fn lines(&mut self) -> Lines<BufferedReader<File>> {
self.source.lines()
}
}
fn scan_file(path_str: &str) {
for line in FileLines::new(path_str).lines() {
match line {
Ok(s) => {
if s.as_slice().contains("Started ") {
print!("{}", s);
}
}
Err(_) => return,
}
}
}
(I also changed &String to &str because it is more idiomatic and general)
The FileLines structure owns the data and encapsulates all of the complex logic in its constructor. Then its lines() method just returns an iterator into its internals. This is rather common pattern in Rust, and usually you will be able to find the main owner of your data and build your program around it with methods which return iterators/references into this owner.
This is not exactly what you wanted (there are two function calls in for loop initializer - new() and lines()), but I believe that for all practical purposes they have the same expressiveness and usability.

How to combine reading a file line by line and iterating over each character in each line?

I started from this code, which just reads every line in a file, and which works well:
use std::io::{BufRead, BufReader};
use std::fs::File;
fn main() {
let file = File::open("chry.fa").expect("cannot open file");
let file = BufReader::new(file);
for line in file.lines() {
print!("{}", line.unwrap());
}
}
... but then I tried to also loop over each character in each line, something like this:
use std::io::{BufRead, BufReader};
use std::fs::File;
fn main() {
let file = File::open("chry.fa").expect("cannot open file");
let file = BufReader::new(file);
for line in file.lines() {
for c in line.chars() {
print!("{}", c.unwrap());
}
}
}
... but it turns out that this innermost for loop is not correct. I get the following error message:
error[E0599]: no method named `chars` found for type `std::result::Result<std::string::String, std::io::Error>` in the current scope
--> src/main.rs:8:23
|
8 | for c in line.chars() {
| ^^^^^
You need to handle the potential error that could arise from each IO operation, represented by an io::Result which can contain either the requested data or an error. There are different ways to handle errors.
One way is to just ignore them and read whatever data we can get.
The code shows how this can be done:
use std::io::{BufRead, BufReader};
use std::fs::File;
fn main() {
let file = File::open("chry.fa").expect("cannot open file");
let file = BufReader::new(file);
for line in file.lines().filter_map(|result| result.ok()) {
for c in line.chars() {
print!("{}", c);
}
}
}
The key points: file.lines() is an iterator that yields io::Result. In the filter_map, we convert the io::Result into an Option and filter any occurrences of None. We're then left with just plain lines (i.e. strings).

Resources