本教程将指导您完成写作 CLI(命令行界面)应用程序。 大约需要15分钟 让你有一个运行的程序。 在那之后,我们将继续调整我们的程序。
我们从简单的开始吧: 让我们编写一个小的 grep 。 这是一个我们给出路径的,它将只打印包含给定字符串的行。
最后, 我们希望能够这样运行我们的工具:
$ cat test.txt
foo: 10
bar: 20
baz: 30
$ grrs foo test.txt
foo: 10
$ grrs --help
[some help text explaining the available options]
项目设置
在你的电脑上安装Rust。
运行 cargo new grrs 在文件夹中创建行项目。查看文件目录:
Cargo.toml文件; 包括我们使用的依赖项/外部库的列表。src/main.rs文件,它是我们项目文件的入口点。
如果在 grrs 目录下执行 cargo run 然后收到“Hello World”,你就万事俱备了。
$ cargo new grrs
Created binary (application) `grrs` package
$ cd grrs/
$ cargo run
Compiling grrs v0.1.0 (/Users/pascal/code/grrs)
Finished dev [unoptimized + debuginfo] target(s) in 0.70s
Running `target/debug/grrs`
Hello, world!
解析命令行参数
我们的CLI工具的一个调用是这样的:
$ grrs foobar test.txt
我们希望程序查看 test.txt 并打印出包含 foobar 的行。 但是我们怎么得到这两个值呢?
程序名称后的文本通常被调用 “命令行参数”, 或者“命令行标志” 。
获取参数
标准库包含该函数 std::env::args() 。 第一个参数(索引 0 )将是您的程序被调用的名称(例如 grrs )。 后面的是用户之后写的内容。
fn main() {
let pattern = std::env::args().nth(1).expect("no pattern given");
let path = std::env::args().nth(2).expect("no path given");
println!("pattern: {:?}, path: {:?}", pattern, path)
}
我们可以使用 cargo run 来运行它, 通过在 -- 后写入参数来传递参数:
$ cargo run -- some-pattern some-file
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/grrs some-pattern some-file`
pattern: "some-pattern", path: "some-file"
观察 grrs foobar test.txt :首先,两者都是必需的。此外,我们还可以谈谈它们的类型: 模式应该是一个字符串, 而第二个参数预计是文件的路径。
定义了一个新的结构,它有两个字段用于存储数:
struct Cli {
pattern: String,
path: std::path::PathBuf,
}
现在,我们需要将程序得到的实际参数转换成这种形式。 一种选择是手动解析从操作系统获得的字符串列表 然后自己建造结构。 它看起来是这样的:
fn main() {
let pattern = std::env::args().nth(1).expect("no pattern given");
let path = std::env::args().nth(2).expect("no path given");
let args = Cli {
pattern,
path: std::path::PathBuf::from(path),
};
println!("pattern: {:?}, path: {:?}", args.pattern, args.path);
}
这是可行的,但不是很方便。 如何处理 --pattern="foo" 或 --pattern "foo" ? 如何实现 --help ?
使用Clap解析CLI参数
最流行的解析命令行参数的库称为 clap 。 它拥有你所期望的所有功能, 包括对子命令、shell和大量帮助消息的支持。
在 Cargo.toml 文件, [dependencies] 段 ,添加 clap = { version = "4.0", features = ["derive"] } 。
现在,我们可以在代码中写 use clap::Parser;
use clap::Parser;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
path: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}
不带任何参数运行它:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 10.16s
Running `target/debug/grrs`
error: The following required arguments were not provided:
<pattern>
<path>
USAGE:
grrs <pattern> <path>
For more information try --help
传递参数:
$ cargo run -- some-pattern some-file
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/grrs some-pattern some-file`
pattern: "some-pattern", path: "some-file"
读取文件
我们先打开手头的文件吧。
let content = std::fs::read_to_string(&args.path).expect("could not read file");
打印输出:
for line in content.lines() {
if line.contains(&args.pattern) {
println!("{}", line);
}
}
代码看起来是这样的:
use clap::Parser;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
path: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.path).expect("could not read file");
for line in content.lines() {
if line.contains(&args.pattern) {
println!("{}", line);
}
}
}
运行一下命令 cargo run -- main src/main.rs :
warning: `/Users/jutianfeng/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/Users/jutianfeng/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Compiling grrs v0.1.0 (/Users/jutianfeng/Desktop/cmd-rust/grrs)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.50s
Running `target/debug/grrs main src/main.rs`
fn main() {
更好的处理错误
Results
像 read_to_string 这样的函数不返回字符串。 相反,它返回 Result 包含 String 或者某种类型的错误 (在本例中 std::io::Error )。
你怎么知道是哪个? 因为 Result 是 enum , 您可以使用 match 来检查它是哪个:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
Ok(content) => { content },
Err(error) => { return Err(error.into()); }
};
println!("file content: {}", content);
Ok(())
}
现在,让我们首先通过添加 anyhow = "1.0" 到 [dependencies] 段。
完整的示例看起来像这样:
use anyhow::{Context, Result};
fn main() -> Result<()> {
let path = "test.txt";
let content = std::fs::read_to_string(path)
.with_context(|| format!("could not read file `{}`", path))?;
println!("file content: {}", content);
Ok(())
}
这将打印一个错误:
Error: could not read file `test.txt`
Caused by:
No such file or directory (os error 2)
最终的代码:
use anyhow::{Context, Result};
use clap::Parser;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
path: std::path::PathBuf,
}
fn main() -> Result<()> {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.path)
.with_context(|| format!("could not read file `{}`", args.path.display()))?;
for line in content.lines() {
if line.contains(&args.pattern) {
println!("{}", line);
}
}
Ok(())
}
打包和分发Rust工具
cargo publish
发布应用程序的最简单方法是使用cargo。 您还记得我们是如何将外部依赖项添加到项目中的吗? Cargo从它默认的“crate注册表”读取文件,从crate.io下载它们。 使用 cargo publish , 您也可以将crate发布到crate.io。
发布流程如下:
- 请在crate.io上创建一个帐户。 通过授权GitHub账号完成的创建。
- 在个人页面创建Token。
- 输入
cargo login <your-new-token>,在本地机器设置Token。 - 在
Cargo.toml添加了必要的元数据。
[package]
name = "grrs"
version = "0.1.0"
authors = ["Your Name <your@email.com>"]
license = "MIT OR Apache-2.0"
description = "A tool to search files"
readme = "README.md"
homepage = "https://github.com/you/grrs"
repository = "https://github.com/you/grrs"
keywords = ["cli", "search", "demo"]
categories = ["command-line-utilities"]
执行 cargo publish 成功以后,便可以在crate.io查看已经上传的包。
本文是rust-cli.github.io/book/index.… 中文翻译。只翻译自己感兴趣的部分内容,如果你有更多的兴趣,可以查看全文。