Rust 入门:文件操作和文件 I/O

如何在 Rust 中开发命令行实用程序。

本文演示了如何在 Rust 中执行基本的文件和文件 I/O 操作,并介绍了 Rust 的所有权概念和 Cargo 工具。如果您是第一次接触 Rust 代码,本文应该能让您很好地了解 Rust 如何处理文件和文件 I/O;如果您之前使用过 Rust,您仍然会欣赏本文中的代码示例。

所有权

如果不先讨论所有权,就开始谈论 Rust,那是不公平的。所有权是 Rust 让开发者控制变量生命周期和语言以确保安全的方式。所有权意味着变量的传递也会将值的所有权传递给新变量。

另一个与所有权相关的 Rust 特性是借用。借用是指暂时控制一个变量,然后在之后归还该变量的所有权。虽然借用允许您对一个变量拥有多个引用,但在任何给定时间只能有一个引用是可变的。

与其继续理论性地讨论所有权和借用,不如看一个名为 ownership.rs 的代码示例


fn main() {
    // Part 1
    let integer = 321;
    let mut _my_integer = integer;
    println!("integer is {}", integer);
    println!("_my_integer is {}", _my_integer);
    _my_integer = 124;
    println!("_my_integer is {}", _my_integer);

    // Part 2
    let a_vector = vec![1, 2, 3, 4, 5];
    let ref _a_correct_vector = a_vector;
    println!("_a_correct_vector is {:?}", _a_correct_vector);

    // Part 3
    let mut a_var = 3.14;
    {
        let b_var = &mut a_var;
        *b_var = 3.14159;
    }
    println!("a_var is now {}", a_var);
}

那么,这里发生了什么?在第一部分,您定义了一个整数变量 (integer) 并基于 integer 创建了一个可变变量。Rust 对原始数据类型执行完整复制,因为它们成本较低,因此在本例中,integer_my_integer 变量彼此独立。

但是,对于其他类型,例如向量,在将变量赋值给另一个变量后,您不允许更改该变量。此外,您应该为上面示例的第 2 部分的 _a_correct_vector 变量使用引用,因为 Rust 不会复制 a_vector

程序的最后一部分是借用的示例。如果您删除花括号,代码将无法编译,因为您将有两个可变变量(a_varb_var)指向相同的内存位置。花括号使 b_var 成为引用 a_var 的局部变量,更改其值,并在到达代码块末尾后将所有权返回给 a_var。由于 a_varb_var 共享相同的内存地址,因此对 b_var 的任何更改也会影响 a_var

执行 ownership.rs 会创建以下输出


$ ./ownership
integer is 321
_my_integer is 321
_my_integer is 124
my_vector is [1, 2, 3, 4, 5]
a_var is now 3.14159

请注意,Rust 在编译时捕获与所有权相关的错误——它使用所有权来提供代码安全性。

本文中显示的其余 Rust 代码非常简单;您不需要了解所有权即可理解它,但了解 Rust 的工作方式和思考方式是件好事。

Cargo 工具

Cargo 是 Rust 的包和编译管理器,它是一个用于在 Rust 中创建项目的有用工具。在本节中,我将使用一个小的 Rust 项目示例介绍 Cargo 的基础知识。使用 Cargo 创建名为 LJ 的 Rust 项目的命令是 cargo new LJ --bin

--bin 命令行参数告诉 Cargo,项目的结果将是一个可执行文件,而不是一个库。之后,您将拥有一个名为 LJ 的目录,其中包含以下内容


$ cd LJ
$ ls -l
total 8
-rw-r--r--  1 mtsouk  staff  117 Jul 14 21:58 Cargo.toml
drwxr-xr-x  3 mtsouk  staff   96 Jul 14 21:58 src
$ ls -l src/
total 8
-rw-r--r--  1 mtsouk  staff  45 Jul 14 21:58 main.rs

接下来,您通常需要编辑以下一个或两个文件


$ vi Cargo.toml
$ vi ./src/main.rs

图 1 显示了该最小 Cargo 项目的所有文件和目录,以及 Cargo.toml 的内容。

""

图 1. 使用 Cargo 创建 Rust 项目

请注意,Cargo.toml 配置文件是您声明项目依赖项以及 Cargo 编译项目所需的其他元数据的地方。要构建您的 Rust 项目,请发出以下命令


$ cargo build

您可以在以下路径中找到可执行文件的调试版本


$ ls -l target/debug/LJ
-rwxr-xr-x  2 mtsouk  staff  491316 Jul 14 22:02
 ↪target/debug/LJ

通过执行 cargo clean 清理 Cargo 项目。

读取器和写入器

Rust 使用读取器和写入器分别用于读取和写入文件。Rust 读取器是一个您可以从中读取的值;而 Rust 写入器是一个您可以向其中写入数据的值。读取器和写入器有各种 trait,但标准的是 std::io::Readstd::io::Write。类似地,创建读取器和写入器最常见和通用的方法是借助 std::fs::File::open()std::fs::File::create()。注意:std::fs::File::open() 以只读模式打开文件。

以下代码(保存为 readWrite.rs)展示了 Rust 读取器和写入器的用法


use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
    let mut file = File::create("/tmp/LJ.txt")?;
    let buffer = "Hello Linux Journal!\n";
    file.write_all(buffer.as_bytes())?;
    println!("Finish writing...");

    let mut input = File::open("/tmp/LJ.txt")?;
    let mut input_buffer = String::new();
    input.read_to_string(&mut input_buffer)?;
    print!("Read: {}", input_buffer);
    Ok(())
}

因此,readWrite.rs 首先使用写入器将字符串写入文件,然后使用读取器从该文件读取数据。因此,执行 readWrite.rs 会创建以下输出


$ rustc readWrite.rs
$ ./readWrite
Finish writing...
Read: Hello Linux Journal!
$ cat /tmp/LJ.txt
Hello Linux Journal!

文件操作

现在让我们看看如何在 Rust 中使用 operations.rs 的代码删除和重命名文件


use std::fs;
use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
    let mut file = File::create("/tmp/test.txt")?;
    let buffer = "Hello Linux Journal!\n";
    file.write_all(buffer.as_bytes())?;
    println!("Finish writing...");

    fs::rename("/tmp/test.txt", "/tmp/LJ.txt")?;
    fs::remove_file("/tmp/LJ.txt")?;
    println!("Finish deleting...");
    Ok(())
}

Rust 重命名和删除文件的方式非常直接,因为每个任务都需要执行一个函数。此外,您可以看到 /tmp/test.txt 是使用 readWrite.rs 中的技术创建的。编译和执行 operations.rs 会生成以下类型的输出


$ ./operations
Finish writing...
Finish deleting...

operations.rs 的代码远非完整,因为它没有错误处理代码。请随时改进它!

使用命令行参数

本节解释了如何访问和处理 Rust 程序的命令行参数。cla.rs 的 Rust 代码如下


use std::env;

fn main()
{
    let mut counter = 0;
    for argument in env::args()
    {
        counter = counter + 1;
        println!("{}: {}", counter, argument);
    }
}

让我们看看这个例子中发生了什么。首先,它使用了 std crate 的 env 模块,因为这是获取程序命令行参数的方式,这些参数将保存在 env::args() 中,它是一个进程参数的迭代器。然后,您使用 for 循环迭代这些参数。

假设您想添加程序的命令行参数(那些有效的整数),以便找到它们的总和。您可以使用下一个 for 循环,该循环包含在 cla.rs 的最终版本中


let mut sum = 0;
for input in env::args()
{
    let _i = match input.parse::<i32>() {
      Ok(_i) => {
          sum = sum + _i
      },
      Err(_e) => {
          println!("{}: Not a valid integer!", input)
      }
    };
}

println!("Sum: {}", sum);

在这里,您迭代 env::args() 迭代器,但这次的目的是不同的,即查找作为有效整数的命令行参数并将它们加起来。

如果您习惯了 C、Python 或 Go 等编程语言,您很可能会发现上述代码对于如此简单的任务来说过于复杂,但这就是 Rust 的工作方式。此外,cla.rs 包含与错误处理相关的 Rust 代码。

请注意,您应该编译 cla.rs 并创建一个可执行文件,然后再运行它,这意味着 Rust 不能轻易地用作脚本编程语言。因此,在这种情况下,使用一些命令行参数编译和执行 cla.rs 会创建这种类型的输出


$ rustc cla.rs
$ ./cla 12 a -1 10
1: ./cla
2: 12
3: a
4: -1
5: 10
./cla: Not a valid integer!
a: Not a valid integer!
Sum: 21

无论如何,关于程序命令行参数的内容就到此为止。下一节介绍使用三个标准的 UNIX 文件。

标准输入、输出和错误

本节介绍如何在 Rust 中使用 stdinstdoutstderr。每个 UNIX 操作系统都为其进程始终打开三个文件。这三个文件是 /dev/stdin、/dev/stdout 和 /dev/stderr,您也可以使用文件描述符 0、1 和 2 分别访问它们。UNIX 程序将常规数据写入标准输出,将错误消息写入标准错误,同时从标准输入读取数据。

以下 Rust 代码(保存为 std.rs)从标准输入读取数据,并写入标准输出和标准错误


use std::io::Write;
use std::io;

fn main() {
    println!("Please give me your name:");
    let mut input = String::new();
    match io::stdin().read_line(&mut input) {
        Ok(n) => {
            println!("{} bytes read", n);
            print!("Your name is {}", input);
        }
        Err(error) => println!("error: {}", error),
    }

    let mut stderr = std::io::stderr();
    writeln!(&mut stderr, "This is an error message!").unwrap();
    eprintln!("That is another error message!")
}

Rust 使用 eprinteprintln 宏写入标准错误,这是一种非常方便的方法。或者,您可以将文本写入 std::io::stderr()。这两种技术都在 std.rs 中进行了说明。

正如您可能预期的那样,您可以使用 printprintln 宏写入标准输出。最后,您可以使用 io::stdin().read_line() 函数从标准输入读取数据。编译和执行 std.rs 会创建以下输出


$ rustc std.rs
$ ./std
Please give me your name:
Mihalis
8 bytes read
Your name is Mihalis
This is an error message!
That is another error message!

如果您在 Linux 机器上使用 Bash shell,您可以通过将标准输出或标准错误数据重定向到 /dev/null 来丢弃它们


$ ./std 2>/dev/null
Please give me your name:
Mihalis
8 bytes read
Your name is Mihalis
$ ./std 2>/dev/null 1>/dev/null
Mihalis

之前的命令取决于您使用的 UNIX shell,与 Rust 无关。请注意,还存在各种其他技术用于在 Rust 中使用 UNIX stdinstdoutstderr

使用纯文本文件

现在让我们看看如何逐行读取纯文本文件,这是处理纯文本文件最常见的方式。在程序结束时,屏幕上将打印字符总数以及读取的行数——将此视为 wc(1) 命令行实用程序的简化版本。

Rust 实用程序的名称是 lineByLine.rs,其代码如下


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

fn main() {
    let mut total_lines = 0;
    let mut total_chars = 0;
    let mut total_uni_chars = 0;

    let args: Vec<_> = env::args().collect();
    if args.len() != 2 {
        println!("Usage: {} text_file", args[0]);
        return;
    }

    let input_path = ::std::env::args().nth(1).unwrap();
    let file = BufReader::new(File::open(&input_path).unwrap());
    for line in file.lines() {
        total_lines = total_lines + 1;
        let my_line = line.unwrap();
        total_chars = total_chars + my_line.len();
        total_uni_chars = total_uni_chars + my_line.chars().count();
    }

    println!("Lines processed:\t\t{}", total_lines);
    println!("Characters read:\t\t{}", total_chars);
    println!("Unicode Characters read:\t{}", total_uni_chars);
}

lineByLine.rs 实用程序使用缓冲读取,正如 std::io::{BufReader,BufRead} 的使用所表明的那样。输入文件使用 BufReader::new()File::open() 打开,并使用 for 循环读取,该循环会一直进行,只要输入文件中还有内容可读取。

此外,请注意,当处理包含 Unicode 字符的文本文件时,len() 函数的输出和 chars().count() 函数的输出可能不相同,这是在 lineByLine.rs 中同时包含它们的主要原因。对于 ASCII 文件,它们的输出应该相同。请记住,如果您的目的是分配一个缓冲区来存储字符串,则 len() 函数是正确的选择。

使用纯文本文件作为输入编译和执行 lineByLine.rs 将生成这种类型的输出


$ ./lineByLine lineByLine.rs
Lines processed:            28
Characters read:            756
Unicode Characters read:    756

请注意,如果您将 total_lines 重命名为 totalLines,您很可能会在尝试编译代码时收到来自 Rust 编译器的以下警告消息


warning: variable `totalLines` should have a snake case name
such as `total_lines`
 --> lineByLine.rs:7:6
  |
7 |     let mut totalLines = 0;
  |         ^^^^^^^^^^^^^^
  |
  = note: #[warn(non_snake_case)] on by default

您可以关闭该警告消息,但遵循 Rust 定义变量名称的方式应被视为一种良好的实践。(在未来的 Rust 文章中,我将更多地介绍 Rust 中的文本处理,敬请期待。)

文件复制

接下来,让我们看看如何在 Rust 中复制文件。copy.rs 实用程序需要两个命令行参数,分别是源文件名和目标文件名。copy.rs 的 Rust 代码如下


use std::env;
use std::fs;

fn main()
{
    let args: Vec<_> = env::args().collect();
    if args.len() >= 3
    {
        let input = ::std::env::args().nth(1).unwrap();
        println!("input: {}", input);
        let output = ::std::env::args().nth(2).unwrap();
        println!("output: {}", output);
        match fs::copy(input, output)
        {
            Ok(n) => println!("{}", n),
            Err(err) => println!("Error: {}", err),
        };
    } else {
        println!("Not enough command line arguments")
    }
}

所有繁重的工作都由 fs::copy() 函数完成,该函数用途广泛,因为您不必处理打开文件进行读取或写入,但它不让您控制过程,这有点像作弊。还存在其他复制文件的方法,例如使用缓冲区以小字节块进行读取和写入。如果您执行 copy.rs,您将看到如下输出


$ ./copy copy.rs /tmp/output
input: copy.rs
output: /tmp/output
515

您可以使用方便的 diff(1) 命令行实用程序来验证文件副本是否与原始文件相同。(使用 diff(1) 留给读者作为练习。)

UNIX 文件权限

本节介绍如何查找和打印文件的 UNIX 文件权限,该文件将作为命令行参数提供给使用 permissions.rs Rust 代码的程序


use std::env;
use std::os::unix::fs::PermissionsExt;

fn main() -> std::io::Result<()> {
    let args: Vec<_> = env::args().collect();
    if args.len() < 2 {
        panic!("Usage: {} file", args[0]);
    }
    let f = ::std::env::args().nth(1).unwrap();
    let metadata = try!(std::fs::metadata(f));
    let perm = metadata.permissions();
    println!("{:o}", perm.mode());
    Ok(())
}

所有工作都由应用于 std::fs::metadata() 的返回值的 permissions() 函数完成。请注意 println() 宏中的 {:o} 格式代码,它指示输出应以八进制系统打印。再一次,Rust 代码乍一看很丑陋,但您肯定会在一段时间后习惯它。

执行 permissions.rs 会产生如下输出——输出的最后三位是您想要的数据,其余值与文件类型以及文件或目录的粘滞位有关


$ ./permissions permissions
100755
$ ./permissions permissions.rs
100644
$ ./permissions /tmp/
41777

请注意,permissions.rs 仅在 UNIX 机器上有效。

结论

本文介绍了在 Rust 中执行文件输入和输出操作,以及使用命令行参数、UNIX 权限以及使用标准输入、输出和错误。由于篇幅限制,我无法介绍 Rust 中处理文件和文件 I/O 的每一种技术,但应该清楚的是,Rust 是创建任何类型的系统实用程序(包括处理文件、目录和权限的工具)的绝佳选择,前提是您有时间学习它的特性。但最终,开发人员应该自己决定是否应该使用 Rust 或另一种系统编程语言来创建 UNIX 命令行工具。

资源

Mihalis Tsoukalos 是一位 UNIX 管理员和开发人员,一位 DBA 和数学家,喜欢技术写作。他是 Go Systems ProgrammingMastering Go 的作者。您可以通过 http://www.mtsoukalos.eu 和 @mactsouk 联系他。

加载 Disqus 评论