简介
命令行程序会接收可执行文件名之后的值,将正常结果写入标准输出,并通过标准错误和非零退出状态报告失败。这些进程边界可以帮助用户和脚本区分成功与失败。
你将完成一个小型文本搜索命令。搜索函数已经准备在软件包的库源文件中,因此每个步骤都可以专注于参数解析、文件读取、无匹配时的行为,以及最终的进程通信。
解析命令行参数
在此步骤中,你将从进程参数中验证并提取搜索关键词和文件路径。
项目位于 /home/labex/project/mini-search。src/main.rs 是命令行二进制程序,src/lib.rs 包含下一步会使用的已准备好的可复用搜索逻辑。进入项目并打开二进制源文件:
cd /home/labex/project/mini-search
nano src/main.rs
env::args() 会将每个参数生成为一个 String,其中包括索引为零的可执行文件路径。已准备好的 main 会将这些参数收集到一个向量中,然后把 &args 传给参数 args: &[String]。
类型 &[String] 表示 String 值的切片:它是对向量元素的只读借用窗口。它与 &str 使用相同的借用理念,但查看的是一系列 String 元素,而不是文本字节。run 读取这些元素时,向量仍由 main 所有。
数据流如下:
shell words → env::args() → Vec<String> in main → borrowed &[String] in run
将 run 中占位的 Err(...) 替换为:
if args.len() != 3 {
return Err(String::from("usage: mini-search <query> <file>"));
}
let query = args[1].clone();
let path = args[2].clone();
Ok(vec![format!("Query: {query}"), format!("File: {path}")])
正好有三个条目,表示可执行文件名加上两个用户参数。当参数数量不符合要求时,显式的 return Err(...) 会立即停止执行 run。这属于提前返回,不同于你在函数实验中使用的最终表达式返回:只有参数数量有效时,下面的代码才会执行。
对借用切片进行索引会得到借用的字符串。这里的两个 clone 调用有意只为搜索关键词和路径创建所有权副本,使 run 的其余部分可以将它们作为本地拥有的值进行处理。这是有意的所有权选择,并不是解决移动错误的通用方法。然后,vec![first, second] 宏会创建一个包含两个元素的向量,Ok 暂时返回这两行检查信息。
使用 Ctrl+O 保存,按 Enter 确认,然后使用 Ctrl+X 退出。使用两个参数运行命令:
cargo run --quiet -- rust data/notes.txt
第一个 --quiet 会减少 Cargo 自身输出的信息。单独的 -- 告诉 Cargo 停止解析选项;它后面的所有内容都会传给你的程序。
Query: rust
File: data/notes.txt
这证明参数已经按预期位置传入。
读取文件并调用库逻辑
在此步骤中,你将把临时的检查结果替换为实际的文件读取和搜索结果。
打开二进制源文件:
nano src/main.rs
在 use std::env; 下方添加以下导入:
use std::fs;
use mini_search::find_lines;
软件包名称 mini-search 会转换为 Rust crate 名称 mini_search。find_lines 上已准备好的 pub 使该函数可以在库 crate 外部访问。导入它后,二进制程序就可以调用 src/lib.rs 中的公共函数;将搜索逻辑保留在那里,可以把可复用的数据处理与特定于进程的参数处理分开。这是对库/二进制程序和可见性模型的简要预览,后续模块实验会完整讲解这些内容。
将临时的 Ok(vec![...]) 行替换为:
let contents = fs::read_to_string(&path)
.map_err(|error| format!("could not read {path}: {error}"))?;
Ok(find_lines(&query, &contents))
读取错误会保留请求的路径,并通过 ? 传播出去。读取成功后,二进制程序会借用搜索关键词和文件内容,同时库会返回拥有所有权的匹配行。
保存并退出,然后检查并运行:
cargo check
cargo run --quiet -- rust data/notes.txt
Rust makes ownership explicit.
Cargo builds Rust packages.
Rust tools help beginners.
准备好的 main 中的 Ok 分支会将正常搜索结果打印到标准输出。
将空搜索转换为有用的错误
在此步骤中,你将区分「搜索成功并找到结果」和「搜索有效但没有找到任何内容」。
打开源文件:
nano src/main.rs
将 Ok(find_lines(&query, &contents)) 替换为:
let matches = find_lines(&query, &contents);
if matches.is_empty() {
return Err(format!("no lines matched '{query}'"));
}
Ok(matches)
空向量本身可以被观察到,但如果命令能说明没有找到匹配内容,使用起来会更有帮助。保存并退出,然后尝试搜索不存在的关键词:
cargo run --quiet -- python data/notes.txt
no lines matched 'python'
在这个中间阶段,准备好的 Err 分支仍会通过标准输出打印信息,并且会成功退出。下一步将为错误设置正确的进程行为。
将错误发送到 stderr 并以非零状态退出
在此步骤中,你将完成命令行边界,把正常输出与失败信息分开。
标准输出(stdout)承载请求的结果。标准错误(stderr)独立承载诊断信息。退出状态为零表示成功;非零退出状态表示失败。打开二进制源文件:
nano src/main.rs
在现有的标准库导入语句下方添加:
use std::process;
将单行的 Err(error) 分支替换为:
Err(error) => {
eprintln!("{error}");
process::exit(1);
}
eprintln! 会将一行内容写入 stderr。process::exit(1) 会立即结束进程,并将退出状态设为 1。保存并退出,然后先构建一次,这样直接运行二进制程序时,Cargo 就不会额外添加自己的失败信息:
cargo build --quiet
./target/debug/mini-search python data/notes.txt
诊断信息仍然是:
no lines matched 'python'
立即打印上一条命令的状态:
echo $?
echo 会打印它的参数,shell 会将 $? 展开为最近一条命令的退出状态:
1
现在,相同的边界行为也适用于无效用法和文件缺失。运行 ./target/debug/mini-search 会报告用法信息;使用 data/missing.txt 这样的路径会报告读取错误。这两种情况都只会将信息写入 stderr,并以状态 1 退出;而匹配成功的搜索会将结果写入 stdout,并以状态 0 退出。
总结
你收集并验证了命令行参数,使用了 Cargo 的 -- 分隔符,将搜索逻辑保留在已准备好的库边界中,读取了请求的文件,并为成功情况和三种失败情况完成了规范的 stdout、stderr 和退出状态行为。


