环境安装

如果你是 Windows,恭喜你,等待被环境折腾吧(当然如果你Visual Studio 安装得比较全:主要指 VC++ BuildTool,那环境问题就要好不少了)。Windows用户可以考虑搭配 Windows Subsystem for Linux(安装文档:https://docs.microsoft.com/zh-cn/windows/wsl/install-win10)。
首先必须是先安装好 Rust 环境,详见:《Hello,Rust》。

起步

处理过程

Rust for Command-line - 图1
如图所示,抛开具体的执行逻辑来说,核心就是对参数的解析。以便确定后续具体的命令执行。

命令行参数

在命令行参数中,也有多种分类。按照你表现形式,可以分为如下几种:

  1. 核心参数(Args):命令行启动的核心参数,需要提供值
  2. 标记类(Flags):指不带参数值的标记项,如 -d -a -v -h 这类
  3. 可选项(Options):一般用于配置部分参数,值非必须,如 --os_type 这类

    常见参数解析

    一般来说,我们可以通过 std::env 来解析命令行参数(原始方式) ```rust use std::env;

fn main() { // 取出命令行参数 let mut args = env::args(); let arg1 = args.nth(1).expect(“请提供第一个参数”); println!(“第一个参数是:{}”, arg1); }

  1. <a name="VFu1H"></a>
  2. ### 命令解析核心包
  3. <a name="g7CsU"></a>
  4. #### clap
  5. [https://clap.rs/](https://clap.rs/) 命令行工具核心类库,可以实现参数绑定、子命令、帮助文档等能力
  6. ```rust
  7. use clap::Clap;
  8. /// Simple program to greet a person
  9. #[derive(Clap, Debug)]
  10. #[clap(name = "hello")]
  11. struct Hello {
  12. /// Name of the person to greet
  13. #[clap(short, long)]
  14. name: String,
  15. /// Number of times to greet
  16. #[clap(short, long, default_value = "1")]
  17. count: u8,
  18. }
  19. fn main() {
  20. let hello = Hello::parse();
  21. for _ in 0..hello.count {
  22. println!("Hello {}!", hello.name)
  23. }
  24. }

structopt

https://docs.rs/structopt/0.3.23/structopt/
基于 clap ,通过派生宏来定义代码。

  1. use std::path::PathBuf;
  2. use structopt::StructOpt;
  3. #[derive(Debug, StructOpt)]
  4. #[structopt(name = "example", about = "An example of StructOpt usage.")]
  5. struct Opt {
  6. /// Activate debug mode
  7. // short and long flags (-d, --debug) will be deduced from the field's name
  8. #[structopt(short, long)]
  9. debug: bool,
  10. /// Set speed
  11. // we don't want to name it "speed", need to look smart
  12. #[structopt(short = "v", long = "velocity", default_value = "42")]
  13. speed: f64,
  14. /// Input file
  15. #[structopt(parse(from_os_str))]
  16. input: PathBuf,
  17. /// Output file, stdout if not present
  18. #[structopt(parse(from_os_str))]
  19. output: Option<PathBuf>,
  20. /// Where to write the output: to `stdout` or `file`
  21. #[structopt(short)]
  22. out_type: String,
  23. /// File name: only required when `out-type` is set to `file`
  24. #[structopt(name = "FILE", required_if("out-type", "file"))]
  25. file_name: Option<String>,
  26. }
  27. fn main() {
  28. let opt = Opt::from_args();
  29. println!("{:?}", opt);
  30. }

使用 structopt 来解析参数

首先,是定义出一个参数对象,并附加一些信息为整个命令行程序的基本信息

  1. use structopt::StructOpt;
  2. #[derive(Debug, StructOpt)]
  3. #[structopt(
  4. name = "Ho",
  5. version = "0.0.1",
  6. about = "这是一个用 rust 编写的命令行工具 Demo"
  7. )]
  8. struct CommandLineOpt {}
  9. fn main() {
  10. let opt = CommandLineOpt::from_args();
  11. println!("{:?}", opt);
  12. }

通过执行命令 cargo run --bin rust-cli -- --help 可以看到如下效果:
image.png
其中 -h,-V 是工具提供的默认标记,用于查看帮助和版本
image.png
接下来我们来补充下 Options 参数

  1. struct CommandLineOpt {
  2. #[structopt(short, long, default_value = "12")]
  3. arg1: u32,
  4. #[structopt(short = "b", long = "arg2", default_value = "42")]
  5. arg2: u32,
  6. }

再执行 -h ,输出如下:
image.png
其中 short 表示简写参数名,如果不设定值,则会采用属性名首字母;long 表示参数名全称,不设定值,则直接使用属性名称。如果出现重复的短名称,则会产生编译错误。

接下来,再来添加核心参数:

  1. struct CommandLineOpt {
  2. #[structopt(short, long, default_value = "12")]
  3. arg1: u32,
  4. #[structopt(short = "b", long = "arg2", default_value = "42")]
  5. arg2: u32,
  6. #[structopt(required = true)]
  7. arg3: String,
  8. }

-h 的输出效果如下:
image.png
从如上的输出效果来看,并没有告诉用户参数的具体含义,那么我们可以通过 /// (比注释多一根斜杠)来增加命令行注释,修改后的代码如下:

在 structopt 中设置 about=”xxx”,也是命令注释,且优先级高于 ///。

  1. struct CommandLineOpt {
  2. /// 这是参数1,用于xxx
  3. #[structopt(short, long, default_value = "12")]
  4. arg1: u32,
  5. /// 这是参数2,别名 -b,用于 xxx
  6. #[structopt(short = "b", long = "arg2", default_value = "42")]
  7. arg2: u32,
  8. /// 这是参数3,必填,用于 xxx
  9. #[structopt(required = true)]
  10. arg3: String,
  11. }

执行 -h 后输出效果如下:
image.png
最后,我们再来整体看下参数解析效果,执行 cargo run --bin rust-cli xxxx -a 33 -b 44 效果如下:image.png

使用 stuctopt 实现子命令

如果要用到子命令,核心就是将整个参数对象用枚举来表示。样例如下:

  1. // 文件地址:src/bin/subc.rs
  2. use std::path::PathBuf;
  3. use structopt::StructOpt;
  4. #[derive(Debug, StructOpt)]
  5. #[structopt(
  6. name = "Hos",
  7. version = "0.0.1",
  8. author = "Jay(hm910705@163.com)",
  9. about = "这是一个用 rust 编写的仿 Git 的子命令 Demo"
  10. )]
  11. enum CommandLineOpt {
  12. #[structopt(name = "clone", about = "Clone a repository into a new directory.")]
  13. Clone {
  14. /// 远端仓库地址
  15. #[structopt(required = true)]
  16. url: PathBuf,
  17. },
  18. #[structopt(
  19. name = "init",
  20. about = "Create an empty Git repository or reinitialize an existing one."
  21. )]
  22. Init {},
  23. }
  24. fn main() {
  25. let opt = CommandLineOpt::from_args();
  26. println!("{:?}", opt);
  27. }

直接执行 cargo run --bin subc -- clone [http://xxx](http://xxx) 的效果如图:
image.png

高级功能

显示进度条

命令行程序要显示进度条是一个比较常见的诉求,在 Rust 中,推荐使用 indicatif([https://crates.io/crates/indicatif](https://crates.io/crates/indicatif)) 来实现进度条功能,具体使用代码如下:

  1. fn main() {
  2. let opt = CommandLineOpt::from_args();
  3. println!("{:?}", opt);
  4. let pb = ProgressBar::new(100);
  5. let dur = Duration::from_millis(5);
  6. for _ in 1..=100 {
  7. thread::sleep(dur);
  8. // pb.println(format!("[+] finished #{}", i));
  9. // 每次循环 + 1
  10. pb.inc(1);
  11. }
  12. pb.reset();
  13. for i in 1..=100 {
  14. thread::sleep(dur);
  15. // 直接设置
  16. pb.set_position(i);
  17. }
  18. pb.finish_with_message("done");
  19. println!("done!");
  20. }

效果如下:
image.png

测试命令行应用

测试命令行的工具库是 assert_cmd([https://docs.rs/assert_cmd/2.0.1/assert_cmd/](https://docs.rs/assert_cmd/2.0.1/assert_cmd/)) ,我们现在 [dev-dependencies] 中安装它。除此之外,还需要装下 predicates([https://docs.rs/predicates/2.0.2/predicates/](https://docs.rs/predicates/2.0.2/predicates/)) 来辅助断言。

测试 rust-cli 这个 bin 的代码可以如下:

  1. use assert_cmd::prelude::*; // Add methods on commands
  2. use predicates::prelude::*; // Used for writing assertions
  3. use std::process::Command; // Run programs
  4. #[test]
  5. fn test_rust_cli_success() -> Result<(), Box<dyn std::error::Error>> {
  6. // 通过 bin 名称来实例化一个命令行应用
  7. let mut cmd = Command::cargo_bin("rust-cli")?;
  8. // 配置参数,如下三个参数的拼接结果是:"required arg" "--arg1" "12" "-b" "44"
  9. cmd.arg("required arg");
  10. cmd.arg("--arg1").arg("12");
  11. cmd.arg("-b").arg("44");
  12. // 开始断言
  13. let result = cmd.assert();
  14. // 命令行要执行成功,通过控制台输出中需要包含 required arg
  15. result
  16. .success()
  17. .stdout(predicate::str::contains("required arg"));
  18. Ok(())
  19. }
  20. #[test]
  21. fn test_rust_cli_failuer() -> Result<(), Box<dyn std::error::Error>> {
  22. let mut cmd = Command::cargo_bin("rust-cli")?;
  23. // 直接执行命令行会出错,同时错误信息需要包含 error 这段文本
  24. cmd.assert().failure().stderr(predicate::str::contains(
  25. "error: The following required arguments were not provided",
  26. ));
  27. Ok(())
  28. }

测试结果如下:
image.png
更多测试细节还需要持续探索,非三言两语就能理解到。

发布命令行应用

更多发布说明,请查阅:https://doc.rust-lang.org/1.39.0/cargo/reference/publishing.html

  1. 首先,我们得先注册到 crates.io 这个 rust 的包管理中心。该站点提供了直接 github 账号登录(OAuth)

image.pngimage.png

  1. 执行上一步骤中的 cargo login xxxx 即可完成 crates.io 的登录。
  2. 在发布包之前,首先我们得填写包的信息,大致填写内容如下(填写在 Cargo.toml 中):

    1. [package]
    2. name = "rust-cli"
    3. version = "0.1.0"
    4. edition = "2018"
    5. authors = ["Your Name <your@email.com>"]
    6. license = "MIT OR Apache-2.0"
    7. description = "A tool to search files"
    8. readme = "README.md"
    9. homepage = "https://github.com/you/grrs"
    10. repository = "https://github.com/you/grrs"
    11. keywords = ["cli", "search", "demo"]
    12. categories = ["command-line-utilities"]
  3. 之后则是执行 cargo publish 命令了~