Skip to content

Day 25: std.fs模块:文件系统操作

欢迎来到第二十五天!昨天我们学习了 std.io 提供的通用I/O接口。今天,我们将专注于一个具体的I/O场景:文件系统操作。std.fs 模块提供了一套丰富的、跨平台的API,用于处理文件和目录。它允许你创建、删除、读取、写入文件,以及遍历目录等。std.fs 的设计与 std.io 紧密结合,文件对象本身就提供了 Reader 和 Writer 接口,使得文件操作无缝融入了Zig的I/O生态系统。

std.fs 中的操作通常始于一个目录。你可以通过 std.fs.cwd() 获取当前工作目录(Current Working Directory),或者通过 std.fs.openDirAbsolute 打开一个绝对路径的目录。这些函数返回一个 Dir 对象。

Dir 对象是进行文件操作的起点。你可以从一个 Dir 对象中打开或创建文件。

const std = @import("std");
pub fn main() !void {
const current_dir = std.fs.cwd();
// 打开一个已存在的文件以供读取
const file = try current_dir.openFile("my_file.txt", .{ .mode = .read_only });
defer file.close();
// 创建一个新文件以供写入
const new_file = try current_dir.createFile("new_file.txt", .{});
defer new_file.close();
}
  • openFile: 打开一个已存在的文件。.mode 选项可以控制是读、写还是读写。
  • createFile: 创建一个新文件。如果文件已存在,默认会报错。
  • openOrCreateFile: 打开或创建文件。

这些函数返回一个 File 对象,它代表一个打开的文件句柄。记住,打开的文件是系统资源,必须在使用后通过 defer file.close() 来关闭。

File 对象提供了多种读写方式。

使用 Reader 和 Writer 接口:

File 对象有 .reader() 和 .writer() 方法,可以获取到与 std.io 兼容的 Reader 和 Writer,这是最通用的方式。

const writer = new_file.writer();
try writer.print("Hello, file!\n", .{});

便捷的读写函数:

std.fs.File 也提供了一些便捷的方法来一次性读写整个文件。

  • readToEndAlloc(allocator, max_size): 读取从当前位置到文件末尾的所有内容,并使用提供的 allocator 分配一块新的内存来存储它们。max_size 用于防止意外读取超大文件耗尽内存。

  • writeAll(bytes: []const u8): 将一个字节切片的全部内容写入文件。

// 一次性读取整个文件
const contents = try file.readToEndAlloc(std.heap.page_allocator, 1 * 1024 * 1024); // 1MB limit
defer std.heap.page_allocator.free(contents);
// 一次性写入整个文件
try new_file.writeAll("some data");

std.fs.path 子模块提供了处理文件路径的工具,这些工具是跨平台的。

  • std.fs.path.dirname(path: []const u8): 返回路径中的目录部分。
  • std.fs.path.basename(path: []const u8): 返回路径中的文件名部分。
  • std.fs.path.join(allocator, paths: []const []const u8): 将多个路径片段连接成一个单一的、规范化的路径。
const path = "/home/user/file.txt";
const dir = std.fs.path.dirname(path); // "/home/user"
const file = std.fs.path.basename(path); // "file.txt"

假设我们有一个 data.csv 文件,内容如下:

name,age
Alice,30
Bob,25

让我们编写一个程序来解析它。

const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const file = try std.fs.cwd().openFile("data.csv", .{});
defer file.close();
const contents = try file.readToEndAlloc(allocator, 1024);
defer allocator.free(contents);
var lines = std.mem.split(u8, contents, "\n");
_ = lines.next(); // 跳过表头
while (lines.next()) |line| {
var fields = std.mem.split(u8, line, ",");
const name = fields.next() orelse "";
const age_str = fields.next() orelse "";
const age = try std.fmt.parseInt(u8, age_str, 10);
std.debug.print("User: {s}, Age: {d}\n", .{ name, age });
}
}

编写一个命令行工具,它接受两个参数:一个源文件名和一个目标文件名。程序应该将源文件的全部内容复制到目标文件中。如果目标文件已经存在,它应该被覆盖。

提示:

  1. 使用 std.process.argsAlloc 来获取命令行参数。
  2. 使用 std.fs.cwd().openFile 打开源文件。
  3. 使用 std.fs.cwd().createFile 创建目标文件。
  4. 使用 std.io.copy 将源文件的 Reader 内容复制到目标文件的 Writer。

问:如何检查文件是否存在?

答:Zig的设计哲学鼓励你直接尝试操作,然后处理可能发生的错误。因此,更“Zig风格”的方式是直接尝试 openFile,然后捕获 error.FileNotFound 错误,而不是先检查再打开。这种“请求原谅比请求许可更容易”(EAFP)的模式可以避免竞态条件(比如在你检查和打开之间,文件被删除了)。

问:如何进行原子写入(Atomic Write)?

答:原子写入(即写入操作要么完全成功,要么完全不发生,不会留下一个写了一半的坏文件)通常通过“写入到临时文件,然后重命名”的模式来实现。std.fs.Dir 提供了 rename 方法。你可以先将内容写入 temp.tmp,写入成功后,再将 temp.tmp 重命名为最终的目标文件名。

今天,我们学习了如何使用 std.fs 模块与文件系统进行交互。我们掌握了如何获取目录句柄,如何打开、创建、读取和写入文件,以及如何处理文件路径。通过将文件对象与 std.io 的 Reader/Writer 接口相结合,Zig提供了一套一致而强大的I/O操作模型。记住,始终使用 defer 来确保文件句柄等系统资源被正确释放,这是编写健壮的系统程序的基本要求。

明天,我们将深入研究 std.mem 模块,学习更多关于低级内存操作的知识。