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_var
和 b_var
)指向相同的内存位置。花括号使 b_var
成为引用 a_var
的局部变量,更改其值,并在到达代码块末尾后将所有权返回给 a_var
。由于 a_var
和 b_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::Read
和 std::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 中使用 stdin
、stdout
和 stderr
。每个 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 使用 eprint
和 eprintln
宏写入标准错误,这是一种非常方便的方法。或者,您可以将文本写入 std::io::stderr()
。这两种技术都在 std.rs
中进行了说明。
正如您可能预期的那样,您可以使用 print
和 println
宏写入标准输出。最后,您可以使用 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 stdin
、stdout
和 stderr
。
现在让我们看看如何逐行读取纯文本文件,这是处理纯文本文件最常见的方式。在程序结束时,屏幕上将打印字符总数以及读取的行数——将此视为 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 文件权限,该文件将作为命令行参数提供给使用 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 命令行工具。
资源