Rust 写脚手架,Clap你应该知道的二三事

努力做好自己,不要好高骛远

大家好,我是柒八九 。一个专注于前端开发技术/RustAI应用知识分享Coder

有感而发

最近,在和前端小伙伴聊天发现,在2024年,她们都有打算入局Rust学习的行列。毕竟前端现在太卷了,框架算是走到穷途末路了,无非就是在原有基础上修修补补。所有他们想在新的赛道弯道超车。但是,苦于各种原因,迟迟找不到入门之法。

确实如她们所言,Rust由于学习路径比较陡峭,加之和前端语言可以说是交集很少。然后,给大家一种学了马上就会忘记的感觉。并且,由于现在Rust在前端领域的应用少之又少。除了字节跳动的Rspack,还有VivoVivo Blue OS(我们在国货之光?用Rust编写的Vivo Blue OS有过介绍),就很少听说其他国内互联网公司有相关的产品和应用。

相比国外,我们的道路还任重而道远。像国外很多耳熟能详的公司都早已布局Rust开发。最明显的就是PhotoShop,它已经将只能在桌面运行的PS搬入了浏览器上。(这个我们也在之前的师夷长技以制夷:跟着PS学前端技术中有过相关介绍)

不过,从最新的招聘网站中搜索Rust相关岗位,相比前几年有了很好的改观。并且很多岗位都和前端相关。这说明,Rust在国内已经有了自己的市场,也意味着在前端领域也有了一席之地。那么作为职业前端,不想在红海中继续卷,那势必就需要选择蓝海,方可在千军万马之中,杀出一条光明之路。

其实,像我在学习Rust也遇到很她们一样的困境。知识点看了,也理解了。但是隔断时间就会忘记。周而复始,就会对这门语言产生一种抗拒感。毕竟,编程也算是一种技术工种,唯手熟尔。

后面,我就转变思路,那就是动手做一些自己认为可以解决前端痛点的事。哪怕做这个事情,其他语言也可以胜任,但是为什么我们不做更进一步的尝试呢。现阶段,Rust在前端赋能的场景,大部分都是提高编译效率方向。像Rspack/OXC

既然,大方向已经定了,然后就有了我们新的尝试。从那开始,就有了我们下面的尝试方向

  1. Rust 开发命令行工具(上)
  2. Rust 开发命令行工具(中)
  3. Rust 编译为 WebAssembly 在前端项目中使用
  4. Game = Rust + WebAssembly + 浏览器
  5. Rust 赋能前端-开发一款属于你的前端脚手架

就是基于上面的不断试错和尝试,到现在我们已经有了像f_clinpm包,并且已经部署到公司私库,并投入生产开发了。

同时,在最近的项目开发中,还利用Rust编写WebAssembly进行前端功能的处理。这块等有机会写一篇相关的文章。

前言

耽误了大家几分钟的时间,在上面絮叨了半天,其实就是想传达一个思想。Rust其实不可怕,可怕的是学了但是你没用到工作中。就是想着法都要让它贴切工作,应用于工作。

我们回到正题,其实Rust赋能前端这个方向我也在摸索,然后现阶段自我感觉能用到前端项目中的无非就两点

  1. 写一个脚手架,将一些繁琐操作工具化
  2. wasm模块,嵌入到前端逻辑中

大家不管是从哪个方面获取Rust知识点,想必大家尝试的第一个Rust应用就是Cli了。

那我们今天就来聊聊在Rust开发Cli时的神器 -clap

今天,我们只要是讲相关的概念,针对如何用Rust构建一个CLI,可以翻看我们之前的文章。

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 项目初始化
  2. 编写子命令
  3. 添加命令标志
  4. 交互式cli
  5. 其他有用的库

1. 项目初始化

首先,让我们通过运行以下命令来初始化我们的项目:cargo init clap_demo。随后我们再配置一下项目的基础信息。(description等)

toml 复制代码
[package]
name = "clap_demo"
version = "0.1.0"
edition = "2021"
description = "front789带你学习clap"

我们可以通过运行以下命令将 clap 添加到我们的程序中:

shell 复制代码
cargo add clap -F derive

这样在Cargo.toml中的[dependencies]中就有了相关的信息。

toml 复制代码
[dependencies]
clap = { version = "4.5.1", features = ["derive"] }

其中-F表示,我们只需要clap中的derive特性。

上述流程中,我们使用的clap的版本是最新版,有些和大家用过的语法有区别的话,需要大家甄别。

这里多说一嘴,如果对前端开发熟悉的同学是不是感觉到上述流程很熟悉。当我们创建一个前端项目时,是不是会遇到下面的步骤。

shell 复制代码
npm init 
yarn add xx 

项目实现

和前端开发类似,当我们把包下载到本地后,我们就需要在对应的入口文件中引入并执行。在前端开发中我们一般挑选的是项目根目录下的index.js。而对于Rust项目来讲,它的入口文件是src/main.rs。(作为二进制项目(Binary Projects)而言)

rust 复制代码
use clap::Parser;

#[derive(Parser)]
#[command(version, about)]
struct Cli {
    name: String
}

fn main() {
    let cli = Cli::parse();
  
    println!("Hello, {}!", cli.name);
}

我们来简单解释一下上面的代码。

在前端开发中我们一般使用import/require进行第三方库的引入,而在Rust中我们使用use来导入第三方库clap中的Parser trait。也就是说,通过use xx我们就可以使用clap中的特定功能。也就是把对应的功能引入到该作用域内。

定义了一个结构体,它使用 clap::Parserderive 宏和command宏,并且只接受一个参数,即 name

#[derive(Parser)]/#[command(version, about)]不是Rust内置的宏,它们是由clap库自定义的过程宏(procedural macros)。

Rust有两种类型的宏:

  1. 声明式宏(Declarative Macros):
    • 这些是Rust内置的,使用macro_rules定义,例如vec!println!等。
    • 它们主要用于元编程(metaprogramming),在编译期执行代码生成。
  2. 过程宏(Procedural Macros):
    • 这些是由外部crate定义的,在编译期间像函数一样被调用。
    • 它们可以用来实现自定义的代码生成、lint检查、trait派生,解析、操作和生成 AST等操作。

#[derive(Parser)]它使用 derive 属性来自动为 Cli 结构体实现 Parser trait。这意味着 Cli 结构体将获得解析命令行参数的功能,而无需手动实现 Parser trait。

#[command(version, about)]用于配置命令行应用程序的元数据。

  • version: 设置应用程序的版本信息。
  • about: 设置应用程序的简短描述。这里的信息就是我们在Cargo.toml中配置的description的信息。

最后,我们可以通过cargo run -- --help来查看对应的信息。

总的来说,这段代码使用 clap 库定义了一个命令行应用程序,它接受一个名为 name 的字符串参数。当运行这个应用程序时,它会打印出 "Hello, {name}"#[derive(Parser)]#[command(...)] 这两个属性分别用于自动实现 Parser trait 和配置应用程序的元数据。

当我们加载程序并使用 Cli::parse() 时,它将从 std::env::args 中获取参数(这个概念我们之前在环境变量:熟悉的陌生人有过介绍)。

  • 如果你尝试运行 cargo run front789,它应该会打印出 Hello, front789!
  • 但如果尝试不添加任何额外值运行它,它将打印出帮助菜单。Clap 在默认特性中包含了一个帮助功能,当输入的命令无效时会自动显示帮助菜单。

当然,如果想让我们的程序更加健壮,我们可以给name设定一个默认值,这样在没有提供参数的情况下,也能合理运行。

rust 复制代码
#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
    #[arg(default_value = "front789")]
    name: String
}

现在,尝试仅使用 cargo run 而不添加其他任何东西,它应该会打印出 Hello, front789!

当然,我们也可以像在f_cli中一样为参数添加更多的配置,来增强我们的Cli

如果想了解更多关于参数配置,可以翻看clap_command-attributes


2. 编写子命令

作为一个功能强大的CLI,我们有时候需要通过定义一些子命令来让我们的目的更加明确。

如果大家用过我们的f_cli,那就心领神会了。

下图是我们f_cli的根据用户提供的参数,默认构建前端项目的命令。

f_cli的实现中,我们就用到了子命令的操作。

下面我们来简单实现一个拥有子命令的cli。在之前代码的基础上,我们只需要将刚才结构体中再新增一个参数 - command并且其类型为实现sumcommad trait的枚举

rust 复制代码
use clap::{ Parser, Subcommand };

#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
    #[arg(default_value = "front789")]
    name: String,
    #[command(subcommand)]
    command: Commands
}

#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create,
    Replace,
    Update,
    Delete
}

fn main() {
    let cli = Cli::parse();
  
    println!("Hello, {:?}!", cli);
}

这样,我们就在上面的基础上拥有了一组子命令(CRUD)。这样我们就可以在cli中调用对应的子命令然后执行对应的操作了。


3. 添加命令标志

我们可以继续丰富我们子命令。上面的我们不是通过一个枚举Commands够了一个组件命令(Create/Replace/Update/Delete)吗。

有时候,在某一个子命令下,还需要收集更多的用户选择。那么我们就可以将枚举中的值关联成一个匿名结构体。这样,我们就可以针对某个子命令做更深的操作了。

还是举我们之前的f_cli的例子,在我们通过f_cli create xxx构建项目时,我们可以通过-x来像CLI传递Create所用到的必要信息。

rust 复制代码
use clap::{ Parser, Subcommand };

#[derive(Parser,Debug)]
#[command(version, about)]
struct Cli {
    #[arg(default_value = "front789")]
    name: String,
    #[command(subcommand)]
    command: Commands
}

#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create{
         #[arg(default_value = "front789")]
        name: String,
         #[arg(default_value = "山西")]
        address: String,
    },
    Replace,
    Update,
    Delete
}

这样我们就对Create进一步处理,并且在create的时候,它会从命令行中寻找对应的name/address信息,并且收集到clap实例中。

随后,我们就可以在主函数中通过match来匹配枚举信息,然后执行相对应的操作。

Rust 中的匹配是穷举式的:必须穷举到最后的可能性来使代码有效

为了节约代码量,我们通过_占位符来处理其他的逻辑。

rust 复制代码
fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Create{name,address} => {
            println!("我是{},来自:{}", name,address);
        },
       _=>(),
    }
}

当我们运行cargo run create时,由于我们提供了默认值,在控制台就会输出对应的信息。当然,我们也可以通过-- name xx -- address xx来进行操作。

有人会觉得输入较长的子命令不是很友好,我们可以通过short = 'n'来为子命令提供一个别名。同时我们还可以通过help="xxx"设置对应在--help时,提供给用户的帮助信息。

对应的代码如下:

rust 复制代码
#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create{
         #[arg(
            short = 'n',
            long="name",
            help = "用户信息",
            default_value = "front789"
        )]
        name: String,
         #[arg(
            short = 'a',
            long="address",
            help = "地址信息",
            requires = "name",
            default_value = "山西"
        )]
        address: String,
    },
    Replace,
    Update,
    Delete
}

4. 交互式cli

在上一节中我们通过对CLI枚举进行改造,让其能够拥有了子命令的功能。其实到这步已经能够获取到cli中用户输入的值,并且能够进行下一步的操作了。

但是呢,你是一个精益求精的人。见多识广的你突然有一个想法,为什么不能像vite/create/next一样。在触发对应的构建和更新操作后,有一个人机交互 的过程。然后,用户可以根据自己的喜好来选择我们cli的内置功能。这样是不是显的更加友好。

像我们的f_cli就是这种交互流程。用户通过人机交互的方式可以选择内置功能。

那我们就再次用一个简单的例子来介绍一下哇。

安装新的包

首先,我们需要安装几个用于交互的包。

csharp 复制代码
cargo add anyhow
cargo add dialoguer
cargo add console

随后,就他们就会自动被注入到Cargo.toml中了。关于anyhow/dialoguer/console我们就不在这里过多介绍了。大家感兴趣可以去对应的官网查找.

现在,我们需要在src/main.rs中引入相关的功能,同时我们在处理cli变量的时候,用的是枚举值,所以我们需要引入clap中针对这类的操作。

diff 复制代码
use clap::{ 
+    builder::EnumValueParser, 
     Parser, 
     Subcommand, 
+    ValueEnum 
};

+use dialoguer::{ 
+  console::Term, 
+  theme::ColorfulTheme, 
+  Select 
+};
+use console::style;

新增枚举信息

前面说过,我们想通过人机交互的方式,在cli运行过程中让用户自己选择我们内置的功能点。所以,这些内置功能我们可以需要事先设定好。

rust 复制代码
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Name {
    N1,
    N2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Address {
    A1,
    A2
}

处理结构体中参数的默认值

既然,已经有了对应的默认值,那么我们就需要限制我们cli中的参数必须是这些内置参数中值。

diff 复制代码
#[derive(Subcommand, Debug, Clone)]
enum Commands {
    Create{
         #[arg(
            short = 'n',
            long="name",
            help = "用户信息",
+            value_parser = EnumValueParser::<Name>::new(),
            ignore_case = true
        )]
+        name: Option<Name>,
         #[arg(
            short = 'a',
            long="address",
            help = "地址信息",
            requires = "name",
+           value_parser = EnumValueParser::<Address>::new(),
        )]
+       address: Option<Address>,
    }
}

上面的配置,见名知意,就是从对应的枚举中解析对应的值。

主函数

其实,这步的操作和之前是差不多的,我们还是利用matchcli.command进行匹配处理。不过我们这里又进一步的做了容错处理。

  1. 首先判断是否提供子命令
  2. 在提供子命令的情况下,再判断是否是Craete

因为,在进行操作中我们会有错误抛出,所以我们对main的返回值也做了处理。(anyhow::Result<()>)

rust 复制代码
fn main() ->anyhow::Result<()> {
    let cli = Cli::parse();
    match cli.command {
        // - 如果有子命令,则根据子命令执行相应的逻辑;
        Some(command) => {
            match command {
                Commands::Create {
                    name,
                    address,
                } => 
                operation_params(
                  name,
                  address
                )?,
            }
        },
         _ => panic!("Fatal: cli为提供参数,退出处理."),
    }
    Ok(())
}

operation_params

main中我们通过match是可以获取到cli中参数的,而此时我们还需要根据参数做进一步的处理。我们把这个逻辑提取到了一个函数中了。

rust 复制代码
fn operation_params (
    name: Option<Name>,
    address: Option<Address>
) -> anyhow::Result<()> {
     let n = match name {
        Some(na) => na,
        None => {
            multiselect_msg("选择一个姓名:");
            message("使用上/下箭头进行选择,使用空格或回车键确认。");
            let items = vec!["张三", "王五"];
            let selection = Select::with_theme(&ColorfulTheme::default())
                .items(&items)
                .default(0)
                .interact_on_opt(&Term::stderr())?;

            match selection {
                Some(0) => Name::N1,
                Some(1) => Name::N2,
                _ => panic!("Fatal: 用户信息制定错误."),
            }
        }
    };
     let a = match address {
        Some(na) => na,
        None => {
            multiselect_msg("选择一个地址:");
            message("使用上/下箭头进行选择,使用空格或回车键确认。");
            let items = vec!["太原", "晋中"];
            let selection = Select::with_theme(&ColorfulTheme::default())
                .items(&items)
                .default(0)
                .interact_on_opt(&Term::stderr())?;

            match selection {
                Some(0) => Address::A1,
                Some(1) => Address::A2,
                _ => panic!("Fatal: 地址信息制定错误."),
            }
        }
    };
    println!("name:{:?},地址:{:?}",n,a);
   Ok(())
}

其实上面的逻辑也是比较简单明了的。 我们接收cli中的参数name/address。因为他们都是枚举类型,所以我们继续用match进行对应值的匹配。

虽然,我们对两个枚举值都做了处理,但是他们的逻辑都是相同的。

上面的逻辑就是当我们运行子命令时候

  • 当提供对应的参数的话,那就原封不动的返回对应的值
  • 当没有提供对应的参数的话,我们就调用dialoguer::Select进行我们预设值的选择。

这样,不管我们上面那种情况,我们最后都可以拿到对应的值。这样我们方便我们后期进行其他操作。


5. 其他有用的库

上面我们通过几个例子,讲了很多clap的应用例子,其中我们还配合dialoguer进行人机交互的处理。如果我们想实现功能更加强大的cli我们还可以借助其他的工具。下面我们就来简单介绍几种。

Crossterm

crossterm 是一款跨终端的crate。 它具有各种很酷的功能,如能够更改背景和文本颜色、操作终端本身和光标,以及捕获键盘和其他事件。

comfy-table

comfy-table 是一个设计用于在终端中创建漂亮表格的 crate

以下是其官网的案例。用仅仅几句话就可以实现一个在终端展示的表格。

rust 复制代码
use comfy_table::Table;

fn main() {
    let mut table = Table::new();
    table
        .set_header(vec!["Header1", "Header2", "Header3"])
        .add_row(vec![
            "This is a text",
            "This is another text",
            "This is the third text",
        ])
        .add_row(vec![
            "This is another text",
            "Now\nadd some\nmulti line stuff",
            "This is awesome",
        ]);

    println!("{table}");
}

执行后的效果如下:

vbnet 复制代码
+----------------------+----------------------+------------------------+
| Header1              | Header2              | Header3                |
+======================================================================+
| This is a text       | This is another text | This is the third text |
|----------------------+----------------------+------------------------|
| This is another text | Now                  | This is awesome        |
|                      | add some             |                        |
|                      | multi line stuff     |                        |
+----------------------+----------------------+------------------------+

inquire

inquire 是一个用于构建终端上交互式提示的 crate。它支持单选、多选、选择日历等功能:

下面的动图是其官网的案例。其中最吸引我的就是那个多选。哈哈。


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。

相关推荐
我是ed8 分钟前
# thingjs 基础案例整理
前端
Ashore_14 分钟前
从简单封装到数据响应:Vue如何引领开发新模式❓❗️
前端·vue.js
落魄实习生17 分钟前
小米路由器开启SSH,配置阿里云ddns,开启外网访问SSH和WEB管理界面
前端·阿里云·ssh
bug丸25 分钟前
v8引擎垃圾回收
前端·javascript·垃圾回收
安全小王子26 分钟前
攻防世界web第三题file_include
前端
&活在当下&27 分钟前
ref 和 reactive 的用法和区别
前端·javascript·vue.js
百事老饼干30 分钟前
VUE前端实现防抖节流 Lodash
前端
web Rookie34 分钟前
React 高阶组件(HOC)
前端·javascript·react.js
云白冰1 小时前
hiprint结合vue2项目实现静默打印详细使用步骤
前端·javascript·vue.js
葡萄架子1 小时前
Python中的logger作用(from loguru import logger)
java·前端·python