跳转到内容

Rust 命令行参数

很多适合拿来练手的小项目,比如待办清单、单词计数器,都需要从命令行接收输入,而不是把值写死在源代码里。这一页讲的就是这方面的基础。

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}

假如把它编译成一个叫 greet 的程序,然后这样运行:

Terminal window
./greet Alice

你会看到:

["greet", "Alice"]

第一个元素永远是程序自己的名字,你真正输入的参数从索引 1 开始。

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("用法:greet <名字>");
return;
}
let name = &args[1];
println!("你好,{name}!");
}

用 cargo run -- Alice 运行,会得到:

你好,Alice!

如果不带参数运行,你会看到用法提示,而不是程序崩溃。在用索引取值之前先检查 args.len(),就能避免有人忘了传参数时发生 panic。

每个参数进来时都是 String,所以你经常需要用 .parse()(见类型转换一页)把它变成数字:

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("用法:square <数字>");
return;
}
match args[1].parse::<i32>() {
Ok(n) => println!("{}", n * n),
Err(_) => println!("这不是一个有效的数字"),
}
}

标志(flag)是用来打开某个功能的参数,比如 --shout。在用 crate 之前,先看看自己动手怎么处理会很有帮助。让它容易测试的诀窍是:把逻辑放进一个函数,让这个函数以 slice 的形式(&[String])接收参数。这样 main 可以传入真正的参数,而你在试验时可以传入一个手工构造的列表:

fn run(args: &[String]) {
let mut shout = false;
let mut names = Vec::new();
// 跳过索引 0,也就是程序名
for arg in &args[1..] {
if arg == "--shout" {
shout = true;
} else {
names.push(arg);
}
}
if names.is_empty() {
println!("Usage: greet [--shout] <name>...");
return;
}
for name in names {
let line = format!("Hello, {name}!");
if shout {
println!("{}", line.to_uppercase());
} else {
println!("{line}");
}
}
}
fn main() {
// 在真正的程序里,你会这样写:
// let args: Vec<String> = std::env::args().collect();
// 这里我们手工构造列表,这样在 Playground 里也能运行。
let args = vec![
String::from("greet"),
String::from("--shout"),
String::from("Ana"),
String::from("Ben"),
];
run(&args);
}

输出:

HELLO, ANA!
HELLO, BEN!
▶ 动手试一试

这里的问候语保留了英文,因为 to_uppercase() 只有作用在拉丁字母上才看得出效果:汉字没有大小写之分。

有几点值得注意:

  • &args[1..] 是除了程序名以外的所有参数。标志可以出现在任何位置:greet Ana --shout Ben 也一样能用。
  • 把 --shout 从列表里删掉再运行一次,你会得到普通的问候。再把名字也删掉,就会看到用法提示。
  • 因为 run 不在乎列表是从哪儿来的,所以换成 std::env::args().collect() 只需要改 main 里的一行。

一两个标志这样处理就够了。可一旦你还想要短选项(-s)、标志后面跟值(--count 3)和 --help 帮助信息,手写的循环很快就会膨胀起来。这时就该换用 clap 了。

一旦需要标志(--verbose)、可选值或者好用的 --help 输出,手动对 args 取索引很快就会变得难以维护。标准的解决方案是 clap crate,这样添加:

Terminal window
cargo add clap --features derive
use clap::Parser;
#[derive(Parser)]
struct Args {
name: String,
#[arg(short, long, default_value_t = 1)]
count: u32,
}
fn main() {
let args = Args::parse();
for _ in 0..args.count {
println!("Hello, {}!", args.name);
}
}

参数解析、校验和一个能用的 --help 帮助信息,全都从结构体定义自动生成。写个临时小脚本不需要 clap,但只要是你会反复使用的工具,它都能帮你省下大量手工活。

Playground 没法传入真正的参数,所以两道练习都从一个手工构造的 Vec<String> 开始,它和 env::args().collect() 返回的结果一模一样。在你自己的电脑上,可以把它换成真正的参数,再试试 cargo run -- ...。

给定 vec!["notes", "add", "buy milk"](都是 String),先打印用户输入了几个参数(不算程序名),再从 1 开始按位置逐个打印出来。

提示:参数个数是 args.len() - 1。用 for i in 1..args.len() 循环,就能拿到程序名之后的每个索引。

查看答案
fn main() {
// 假装这是 std::env::args().collect() 的结果
let args = vec![
String::from("notes"),
String::from("add"),
String::from("buy milk"),
];
println!("你传入了 {} 个参数", args.len() - 1);
for i in 1..args.len() {
println!("{i}: {}", args[i]);
}
}

输出:

你传入了 2 个参数
1: add
2: buy milk
▶ 动手试一试

"buy milk" 算作一个参数:在真正的命令行上,你会给它加上引号(cargo run -- add "buy milk"),shell 会把它当作一个完整的字符串传进来。索引 0 依然是程序名,所以循环从 1 开始。

给定 vec!["sum", "4", "10", "x", "6"],把所有有效数字的参数加起来,遇到不是数字的就打印一条警告。最后打印总和。

提示:遍历 &args[1..],用 match arg.parse::<i32>(),就像把参数解析成数字里那样。在循环外面先写好 let mut total = 0;。

查看答案
fn main() {
let args = vec![
String::from("sum"),
String::from("4"),
String::from("10"),
String::from("x"),
String::from("6"),
];
let mut total = 0;
for arg in &args[1..] {
match arg.parse::<i32>() {
Ok(n) => total += n,
Err(_) => println!("跳过“{arg}”:不是数字"),
}
}
println!("合计:{total}");
}

输出:

跳过“x”:不是数字
合计:20
▶ 动手试一试

每个参数进来时都是文本,就连 "4" 也是,所以每一个都得解析。遇到错误输入时打印一条消息再跳过,比直接崩溃友好得多。如果你的程序更适合在第一个错误处停下,也可以用 return。

  • std::env::args().collect() 会给你一个装着命令行参数的 Vec<String>,索引 0 永远是程序名。
  • 用索引取值前先检查 args.len(),免得缺少参数时程序 panic。
  • .parse() 把 String 类型的参数变成数字,和别处的用法一样。
  • 把参数处理放进一个接收 &[String] 的函数里,这样就能用手工构造的列表来测试。
  • 只要参数比两三个普通参数更复杂,clap crate 就是公认的、久经考验的标准选择。

小测验

1. 调用 env::args().collect() 后,得到的 Vec 的第一个元素是什么?

2. 为什么在对 args 取索引前要先检查 args.len()?

3. 和手动对 args 取索引相比,clap crate 能多给你什么?

得分: 0 / 3