使用Rust开发命令行应用程序

310 阅读4分钟

image.png 本教程将指导您完成写作 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 )。

你怎么知道是哪个? 因为 Resultenum , 您可以使用 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。

发布流程如下:

  1. 请在crate.io上创建一个帐户。 通过授权GitHub账号完成的创建。
  2. 个人页面创建Token。
  3. 输入 cargo login <your-new-token>,在本地机器设置Token。
  4. 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.… 中文翻译。只翻译自己感兴趣的部分内容,如果你有更多的兴趣,可以查看全文。