接收命令行参数

RustBeginner
立即练习

简介

命令行程序会接收可执行文件名之后的值,将正常结果写入标准输出,并通过标准错误和非零退出状态报告失败。这些进程边界可以帮助用户和脚本区分成功与失败。

你将完成一个小型文本搜索命令。搜索函数已经准备在软件包的库源文件中,因此每个步骤都可以专注于参数解析、文件读取、无匹配时的行为,以及最终的进程通信。

解析命令行参数

在此步骤中,你将从进程参数中验证并提取搜索关键词和文件路径。

项目位于 /home/labex/project/mini-searchsrc/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_searchfind_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 和退出状态行为。