概述
我们的第二个项目从算术运算升级到文本处理:一个微型的grep克隆,它接受搜索模式和文件路径,然后只打印匹配的行。这个练习巩固了前一章中的参数处理,同时引入了标准库中的文件I/O和切片实用工具。#Command-line-flags, File.zig
我们不再逐字节流式处理,而是依赖Zig的内存安全助手来加载文件、将其拆分成行,并通过简单的子字符串检查来显示匹配项。每个失败路径在退出前都会产生用户友好的消息,因此该工具在shell脚本中表现可预测——这是我们将在下一个项目中延续的主题。有关相关API,请参见#Command-line-flags和File.zig,有关错误处理模式,请参见#Error-Handling。
学习目标
构建搜索框架
我们从连接CLI前端开始:分配参数、支持--help,并确认恰好有两个位置参数——模式和路径——存在。任何偏差都会打印使用横幅并以代码1退出,避免堆栈跟踪,同时仍然向调用者发出失败信号。
验证参数和使用路径
这个框架镜像了第5章的TempConv CLI,但现在我们将诊断信息发送到stderr,并在输入错误或无法打开文件时显式退出。printUsage将横幅保持在一个地方,而std.process.exit确保我们在消息写入后立即停止。
加载和拆分文件
我们不再处理部分读取,而是使用File.readToEndAlloc将文件加载到内存中,将大小限制为8兆字节以防止意外的巨大文件。然后,对std.mem.splitScalar的单个调用会生成一个按换行符分隔的段的迭代器,我们会为Windows风格的回车符进行修剪。
理解 std.fs 结构
在深入文件操作之前,了解Zig的文件系统API是如何组织的很有帮助。std.fs模块提供了一个分层层次结构,使文件访问具有可移植性和可组合性:
关键概念:
- 入口点:
std.fs.cwd()返回一个表示当前工作目录的Dir句柄 - 目录类型:提供目录级操作,如打开文件、创建子目录和迭代内容
- 文件类型:表示具有读/写操作的打开文件
- 链式调用:你调用
cwd().openFile()是因为openFile()是Dir类型上的方法
为什么这种结构对Grep-Lite很重要:
// 这就是我们这样写的原因:
const file = try std.fs.cwd().openFile(path, .{});
// ^ ^
// | +-- Dir上的方法
// +----------- 返回Dir句柄两步过程(cwd() → openFile())让你可以控制在哪个目录中打开文件。虽然这个例子使用当前目录,但你同样可以使用:
std.fs.openDirAbsolute()用于绝对路径dir.openFile()用于相对于任何目录句柄的文件std.fs.openFileAbsolute()完全跳过Dir
这种可组合的设计使文件系统代码可测试(使用临时目录)且可移植(相同的API在所有平台上工作)。
扫描匹配项
一旦我们拥有每行的切片,匹配就是使用std.mem.indexOf的一行代码。我们重用TempConv模式,将stdout保留用于成功输出,stderr用于诊断,使工具对管道友好。
完整的Grep-Lite清单
下面的完整清单展示了辅助函数如何组合在一起。注意将每个块与上面章节联系起来的注释。
const std = @import("std");
// 第6章 - Grep-Lite:逐行流式处理文件并仅回显匹配项到stdout,
// 同时将错误转换为stderr上的清晰诊断信息。
const CliError = error{MissingArgs};
fn printUsage() void {
std.debug.print("usage: grep-lite <pattern> <path>\n", .{});
}
fn trimNewline(line: []const u8) []const u8 {
if (line.len > 0 and line[line.len - 1] == '\r') {
return line[0 .. line.len - 1];
}
return line;
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) {
printUsage();
return;
}
if (args.len != 3) {
std.debug.print("error: expected a pattern and a path\n", .{});
printUsage();
std.process.exit(1);
}
const pattern = args[1];
const path = args[2];
var file = std.fs.cwd().openFile(path, .{ .mode = .read_only }) catch {
std.debug.print("error: unable to open '{s}'\n", .{path});
std.process.exit(1);
};
defer file.close();
// 使用现代Writer API的缓冲stdout
var out_buf: [8 * 1024]u8 = undefined;
var file_writer = std.fs.File.writer(std.fs.File.stdout(), &out_buf);
const stdout = &file_writer.interface;
// 第1.2节:积极加载完整文件,同时强制执行保护,
// 防止意外的多兆字节输入耗尽内存。
const max_bytes = 8 * 1024 * 1024;
const contents = file.readToEndAlloc(allocator, max_bytes) catch |err| switch (err) {
error.FileTooBig => {
std.debug.print("error: file exceeds {} bytes limit\n", .{max_bytes});
std.process.exit(1);
},
else => return err,
};
defer allocator.free(contents);
// 第2.1节:在换行符处分割缓冲区;每个切片引用
// 原始分配,因此不会产生额外的副本。
var lines = std.mem.splitScalar(u8, contents, '\n');
var matches: usize = 0;
while (lines.next()) |raw_line| {
const line = trimNewline(raw_line);
// 第2节:重用`std.mem.indexOf`以精确高亮匹配项,
// 而无需构建临时切片。
if (std.mem.indexOf(u8, line, pattern) != null) {
matches += 1;
try stdout.print("{s}\n", .{line});
}
}
if (matches == 0) {
std.debug.print("no matches for '{s}' in {s}\n", .{ pattern, path });
}
// 刷新缓冲stdout并定位文件位置
try file_writer.end();
}
$ zig run grep_lite.zig -- pattern grep_lite.zig std.debug.print("usage: grep-lite <pattern> <path>\n", .{});
std.debug.print("error: expected a pattern and a path\n", .{});
const pattern = args[1];
if (std.mem.indexOf(u8, line, pattern) != null) {
std.debug.print("no matches for '{s}' in {s}\n", .{ pattern, path });输出显示包含字面单词pattern的每个源代码行。当针对其他文件运行时,你的匹配列表会有所不同。
优雅地检测缺失文件
为了保持shell脚本的可预测性,当文件路径无法打开时,该工具会发出单行诊断信息并以非零状态退出。
$ zig run grep_lite.zig -- foo missing.txterror: unable to open 'missing.txt'注意事项
readToEndAlloc很简单但会加载整个文件;如果你需要处理非常大的输入,稍后添加流式读取器。- 大小限制防止失控的分配。一旦你信任你的部署环境,可以提高它或使其可配置。
- 这个例子使用缓冲的stdout写入器进行匹配,并使用
std.debug.print向stderr进行诊断;我们通过写入器的end()在退出时刷新(参见Io.zig)。
练习
替代方案与边缘情况
- Windows文件通常以
\r\n结束行;修剪回车符保持子字符串检查的清洁。 - 空模式当前匹配每一行。如果你倾向于将空字符串视为误用,请引入显式防护。
- 要与更大的构建集成,请将
zig run替换为zig build-exe步骤,并将二进制文件打包到你的PATH上。