Files and I/O in Rust
This chapter introduces I/O operations in the Rust programming language.
Command Line Arguments
Command-line programs are the most fundamental form of computer programs. Almost all operating systems support command-line programs and base the execution of graphical programs on command-line mechanisms.
Command-line programs must be able to receive arguments from the command-line environment. These arguments are typically separated by spaces after a command.
While many languages (like Java and C/C++) pass environment arguments as parameters to the main function (usually as a string array), in Rust, the main function takes no parameters. Developers need to retrieve environment arguments through the std::env module. The process is quite simple:
fn main() {
let args = std::env::args();
println!("{:?}", args);
}
Running the program directly gives:
Args { inner: ["D:\\rust\\greeting\\target\\debug\\greeting.exe"] }
Your result might be longer, which is normal. The Args struct contains an inner array with a single string representing the location of the currently running program.
If this data structure seems difficult to understand, we can simply iterate through it:
fn main() {
let args = std::env::args();
for arg in args {
println!("{}", arg);
}
}
Output:
D:\rust\greeting\target\debug\greeting.exe
Arguments are typically meant to be iterated over, aren't they?
Now, let's open the long-untouched launch.json and find "args": []. Here we can set runtime arguments. Let's change it to "args": ["first", "second"], save, and run the program again:
D:\rust\greeting\target\debug\greeting.exe
first
second
As a real command-line program, we've never truly used it this way. While this tutorial won't cover how to run Rust programs from the command line, if you're a trained developer, you should be able to locate the executable and test the program's argument reception using command-line commands.
Command Line Input
Earlier chapters detailed command-line output extensively, as it's necessary for debugging during language learning. However, getting input from the command line remains equally important for command-line programs.
In Rust, the std::io module provides functionality for standard input (command-line input):
use std::io::stdin;
fn main() {
let mut str_buf = String::new();
stdin().read_line(&mut str_buf)
.expect("Failed to read line.");
println!("Your input line is \n{}", str_buf);
}
Running in the command line:
D:\rust\greeting> cd ./target/debug
D:\rust\greeting\target\debug> ./greeting.exe
RUNOOB
Your input line is
RUNOOB
std::io::Stdio contains the read_line method for reading a line of text into a buffer. It returns a Result enum for handling potential reading errors, typically managed using expect or unwrap functions.
Note: Currently, Rust's standard library doesn't provide methods to directly read numbers or formatted data from the command line. We can read a line as a string and use string parsing functions to process the data.
File Reading
Let's create a file text.txt in the D:\ directory with the following content:
This is a text file.
Here's a program that reads the text file content into a string:
use std::fs;
fn main() {
let text = fs::read_to_string("D:\\text.txt").unwrap();
println!("{}", text);
}
Output:
This is a text file.
In Rust, reading an entire file that fits into memory is extremely simple. The read_to_string method in the std::fs module can easily handle text file reading.
For binary files, we can use std::fs::read to read into a collection of u8 types:
use std::fs;
fn main() {
let content = fs::read("D:\\text.txt").unwrap();
println!("{:?}", content);
}
Output:
[84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 116, 101, 120, 116, 32, 102, 105, 108, 101, 46]
The above two methods read the entire file at once, which is ideal for web application development. However, for some low-level programs, traditional stream reading remains irreplaceable, as files often exceed available memory.
Here's how to read files as a stream in Rust:
use std::io::prelude::*;
use std::fs;
fn main() {
let mut buffer = [0u8; 5];
let mut file = fs::File::open("D:\\text.txt").unwrap();
file.read(&mut buffer).unwrap();
println!("{:?}", buffer);
file.read(&mut buffer).unwrap();
println!("{:?}", buffer);
}
Output:
[84, 104, 105, 115, 32]
[105, 115, 32, 97, 32]
The File class in the std::fs module describes files and can be used to open them. After opening, we can use File's read method to read the next few bytes into a buffer (a u8 array). The number of bytes read equals the buffer length.
Note: VSCode currently doesn't automatically add standard library imports, so "function or method not found" errors might be due to missing imports. We can check the standard library documentation (shown on mouseover) to manually add imports.
The open method of std::fs::File opens files in "read-only" mode and doesn't have a corresponding close method, as the Rust compiler automatically closes files when they're no longer in use.
File Writing
File writing can be done either all at once or as a stream. Stream writing requires opening the file in either "create" or "append" mode.
Writing all at once:
use std::fs;
fn main() {
fs::write("D:\\text.txt", "FROM RUST PROGRAM")
.unwrap();
}
This is as simple as reading all at once. After execution, the content of D:\text.txt will be overwritten with "FROM RUST PROGRAM". Use one-time writing cautiously as it deletes the file's content (regardless of size). If the file doesn't exist, it will be created.
For stream writing, we can use std::fs::File's create method:
use std::io::prelude::*;
use std::fs::File;
fn main() {
let mut file = File::create("D:\\text.txt").unwrap();
file.write(b"FROM RUST PROGRAM").unwrap();
}
This program is equivalent to the previous one.
Note: The file must be stored in a mutable variable to use File's methods!
While the File class doesn't have a static append method, we can use OpenOptions to open files with specific permissions:
use std::io::prelude::*;
use std::fs::OpenOptions;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.append(true).open("D:\\text.txt")?;
file.write(b" APPEND WORD")?;
Ok(())
}
After running, D:\text.txt will contain:
FROM RUST PROGRAM APPEND WORD
OpenOptions is a flexible way to open files with specific permissions. Besides append, it supports read and write permissions. To open a file with read and write permissions:
use std::io::prelude::*;
use std::fs::OpenOptions;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.read(true).write(true).open("D:\\text.txt")?;
file.write(b"COVER")?;
Ok(())
}
After running, D:\text.txt will contain:
COVERRUST PROGRAM APPEND WORD