Rust Files and I/O
This chapter introduces I/O operations in Rust.
Receiving Command Line Arguments
Command line programs are the most basic form of computer programs, and almost all operating systems support command line programs, basing the execution of visual programs on command line mechanisms.
Command line programs must be able to receive parameters from the command line environment, which are often separated by spaces after a command in a command line.
In many languages (such as Java and C/C++), environment parameters are passed to the program as arguments to the main function (often a string array), but in Rust, the main function is a parameterless function, and environment parameters need to be retrieved by the developer through the std::env
module, which is quite simple:
Example
fn main() {
let args = std::env::args();
println!("{:?}", args);
}
Now run the program directly:
Args { inner: ["D:\\rust\\greeting\\target\\debug\\greeting.exe"] }
You might get a longer result, which is normal. In this result, the Args
struct contains an inner
array with a single string representing the location of the currently running program.
However, this data structure is hard to understand. No worries, we can simply iterate over it:
Example
fn main() {
let args = std::env::args();
for arg in args {
println!("{}", arg);
}
}
Running result:
D:\rust\greeting\target\debug\greeting.exe
Parameters are generally meant to be iterated over, right?
Now, open the long-neglected launch.json
, find "args": []
, and set the runtime parameters here, changing it to "args": ["first", "second"]
, then save and run the program again. The result:
D:\rust\greeting\target\debug\greeting.exe
first
second
As a true command line program, we have never really used it. As a language tutorial, we do not describe how to run Rust programs from the command line. But if you are a well-trained developer, you should be able to find the location of the executable file and try to enter the directory and use command line commands to test the program receiving command line environment parameters.
Command Line Input
Earlier chapters detailed how to use command line output, which is necessary for language learning as there is no way to debug without output. However, obtaining input from the command line is still quite important for a command line program.
In Rust, the std::io
module provides functionality related to standard input (considered command line input):
Example
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);
}
Making VSCode support command line input is a very tedious task, involving cross-platform issues and non-debuggable issues, so we run the program directly in the VSCode terminal. Run in the command line:
D:\rust\greeting> cd ./target/debug
D:\rust\greeting\target\debug> ./greeting.exe
tutorialpro
Your input line is
tutorialpro
std::io::Stdio
includes the read_line
method for reading a line of strings into a buffer, with the return value being a Result
enum class used to pass errors that occur during reading, so common functions like expect
or unwrap
are used to handle errors.
Note: Currently, the Rust standard library does not provide a method to directly read numbers or formatted data from the command line. We can read a line of strings and use string recognition functions to process the data.
File Reading
We create a file text.txt
in the D:\
directory of the computer with the following content:
This is a text file.
This is a program that reads the content of a text file into a string:
Example
use std::fs;
fn main() {
let text = fs::read_to_string("D:\\text.txt").unwrap();
println!("{}", text);
}
Running result:
This is a text file.
Reading an entire file that fits into memory in Rust is extremely simple, thanks to the read_to_string
method in the std::fs
module, which can easily read text files.
However, if the file to be read is binary, we can use the std::fs::read
function to read a collection of u8
types:
Example
use std::fs;
fn main() {
let content = fs::read("D:\\text.txt").unwrap();
println!("{:?}", content);
}
Running result:
[84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 116, 101, 120, 116, 32, 102, 105, 108, 101, 46]
Both of these methods are for reading files in one go, which is very suitable for web application development. However, for some low-level programs, the traditional stream reading method is still irreplaceable, especially when the file size may far exceed the memory capacity.
File stream reading in Rust:
Example
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);
}
Running result:
[84, 104, 105, 115, 32]
[105, 115, 32, 97, 32]
The File
class in the std::fs
module is used to describe files and can be used to open files. After opening a file, we can use the read
method of File
to read the next bytes of the file into a buffer (which is an array of u8
), with the number of bytes read being equal to the buffer's length.
Note: VSCode currently does not have the ability to automatically add standard library references, so sometimes an error like "function or method does not exist" may be due to a standard library reference issue. We can manually add the standard library by checking the annotation documentation (which appears when hovering over the text).
The open
method of std::fs::File
opens files in "read-only" mode and does not have a corresponding close
method, because the Rust compiler can automatically close the file when it is no longer in use.
File Writing
File writing can be done in one go or through streaming. For streaming, files can be opened in "create" or "append" modes.
One-time writing:
Example
use std::fs;
fn main() {
fs::write("D:\\text.txt", "FROM RUST PROGRAM")
.unwrap();
}
This is as simple and convenient as one-time reading. After running the program, the content of D:\text.txt
will be overwritten with "FROM RUST PROGRAM". Therefore, use one-time writing with caution! It will directly delete the file content (regardless of the file size). If the file does not exist, it will create a new file.
To write file content using a stream, you can use the create
method of std::fs::File
:
Example
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 opened file must be stored in a mutable variable to use the methods of File
!
The File
class does not have a static append
method, but we can use OpenOptions
to open files with specific methods:
Example
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, the content of D:\text.txt
will become:
FROM RUST PROGRAM APPEND WORD
OpenOptions is a flexible method for opening files, which allows setting various permissions, including read and write permissions in addition to append permissions. If we want to open a file with read and write permissions, we can write it like this:
Example
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, the content of the D:\text.txt file will become:
COVERRUST PROGRAM APPEND WORD