Easy Tutorial
❮ Rust Ownership Rust Slice ❯

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
❮ Rust Ownership Rust Slice ❯