惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
博客园_首页
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
D
Docker
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
量子位
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
Cloudbric
Cloudbric
博客园 - 司徒正美
H
Help Net Security
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
Latest news
Latest news
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
博客园 - Franky
S
Security Affairs
W
WeLiveSecurity
F
Full Disclosure
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
美团技术团队
PCI Perspectives
PCI Perspectives
C
Check Point Blog
Spread Privacy
Spread Privacy

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog NAT 类型初探 | Yesterday17's Blog
Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog
Yesterday17 · 2021-09-05 · via Yesterday17's Blog

Project Anni 的第一笔 commit 是从去年(2020年)12月20号开始的,而 clap 则是在当天的第三个 commit 中引入的。当时引入的是 2.3.3,也就是 clap 2 的最后一个版本,这个版本中还没有 derive 的身影。随着 clap 3.0.0-beta 版本的发布,带着尝鲜的心情,anniclap 版本也悄然升级。但依然只是使用了传统的 builder 方式,并没有关注 clap_derive

256 commit 留念
256 commit 留念

但随着 anni-cli 功能的不断增多与复杂化,传统的方式开始越来越不能满足需要了。以字符串形式约定的 value_of,层层传递的 matches,手动进行的参数类型转换,这一切都令人心生退意。随着参数的增加,情况只会越来越糟,在现有的模式下,基本看不到变好的可能……

于是,从 builderderive 的迁移之旅就开始了。

ToC

  • 回顾:builder 模式
  • 从 builder 到 derive
  • 处理 Subcommand
  • RE:thonk
  • 结语

回顾:builder 模式

回头望去,不难发现 builder 模式其实是很符合人类直觉的一种方式。在默认的基础上进行改动,并最终拼凑成形。我们以现在 Readme 为例:

let matches = App::new("My Super Program")

.version("1.0")

.author("Kevin K. <[email protected]>")

.about("Does awesome things")

.arg(Arg::new("config")

.short('c')

.long("config")

.value_name("FILE")

.about("Sets a custom config file")

.takes_value(true))

.arg(Arg::new("INPUT")

.about("Sets the input file to use")

.required(true)

.index(1))

.arg(Arg::new("v")

.short('v')

.multiple_occurrences(true)

.takes_value(true)

.about("Sets the level of verbosity"))

.subcommand(App::new("test")

.about("controls testing features")

.version("1.3")

.author("Someone E. <[email protected]>")

.arg(Arg::new("debug")

.short('d')

.about("print debug information verbosely")))

.get_matches();

// You can check the value provided by positional arguments, or option arguments

if let Some(i) = matches.value_of("INPUT") {

println!("Value for input: {}", i);

}

if let Some(c) = matches.value_of("config") {

println!("Value for config: {}", c);

}

// You can see how many times a particular flag or argument occurred

// Note, only flags can have multiple occurrences

match matches.occurrences_of("v") {

0 => println!("Verbose mode is off"),

1 => println!("Verbose mode is kind of on"),

2 => println!("Verbose mode is on"),

_ => println!("Don't be crazy"),

}

// You can check for the existence of subcommands, and if found use their

// matches just as you would the top level app

if let Some(ref matches) = matches.subcommand_matches("test") {

// "$ myapp test" was run

if matches.is_present("debug") {

// "$ myapp test -d" was run

println!("Printing debug info...");

} else {

println!("Printing normally...");

}

}

这种模式看起来很灵活,实际使用时也确实很灵活,但这种灵活性也是有代价的。

九 评 b u i l d e r

肉眼可见的就是代码长度。可以看到这种模式下,我们并没有太多的功能,但却用了足足 57 行代码来描述这些参数和功能。

其次是参数的类型。通过 value_ofvalues_of 获得的内容都是字符串(数组),这意味着当我们需要的参数类型并非字符串时,势必需要在某个地方对它们的类型进行转换。而这样的类型转换又会带来新的问题:是否要持久化转换后的参数?如果不持久化,那么在下次使用同一参数时又需要再重复一次反序列化的操作;但如果持久化,那我们又必须建立持久化对应的结构体,并手动创建它们;又或是在某一个较高层级对参数的类型进行调整,然后一级一级送下去?然后你就得到了一幅以 matches 为底,高层参数内容组成的结构体为边的等腰直角三角形世 界 名 画……咳咳,扯远了。

从 builder 到 derive

builderderive 的思想非常简单:将命令替换成 struct,子命令替换成 enum。以 anni repo 子命令为例,替换后的代码如下:

// 顶层命令

// 包含了 root 属性和不同的 subcommand

#[derive(Clap, Debug)]

pub struct RepoSubcommand {

#[clap(long, env = "ANNI_REPO")]

root: PathBuf,

#[clap(subcommand)]

action: RepoAction,

}

// 第二层的 Subcommand

// 包含各个子命令的声明方式

#[derive(Clap, Debug)]

pub enum RepoAction {

Add(RepoAddAction),

Edit(RepoEditAction),

Apply(RepoApplyAction),

Validate(RepoValidateAction),

Print(RepoPrintAction),

}

// 第三层的命令

// 实际执行的操作

#[derive(Clap, Debug)]

pub struct RepoAddAction {

#[clap(short = 'e', long)]

open_editor: bool,

#[clap(required = true)]

directories: Vec<PathBuf>,

}

首先我们需要 #[derive(Clap)],可以选择 Debug 以在解析结束之后输出解析结果,方便调试;然后我们需要通过 #[clap(xxx)] 表示每个参数的详细属性。比如 shortlong。这些参数可以简单地和 builder 中的同名函数对应,这里就不再展开了。

处理 Subcommand

——命令解析成功,我们得到了一个完整的 struct,代表了用户的具体输入。下一步就是处理了。

我们知道,一个 struct 代表了一种命令,而一个 enum 则代表了一个子命令。 structenum 交替嵌套,形成了整个工具。如果想要实现简单的处理逻辑,就要从顶层的 command 开始,逐层匹配是否命中对应的 subcommand

举一个简单的例子吧,只有一层子命令的情况:

#[derive(Clap)]

struct SimpleCommand {

#[clap(subcommand)]

subcommand: SimpleSubcommand;

}

#[clap(Clap)]

enum SimpleSubcommand {

A(SubcommandA),

B(SubcommandB),

}

#[clap(Clap)]

struct SubcommandA {

#[clap(short, long)]

arg1: String,

}

#[clap(Clap)]

struct SubcommandB {

#[clap(short, long)]

arg2: String,

}

可以看到,最外层的命令只有一个,是 SimpleCommand;在这个命令中有两个 Subcommand,对应了两个命令各自的结构体。简单的层级关系如下图所示:

图中,我们将 SimpleCommand 称为根命令SubcommandASubcommandB 称为子命令。特别地,在只有一层子命令嵌套的关系下,我们还可以把这两个子命令称为叶子命令

那想要解析 SubcommandA,我们需要做什么呢?

不难发现,想要到达 SubcommandA,我们需要经过 SimpleCommandSimpleSubcommand 两层。根据从上到下的解析顺序,只要我们给每一个层级增加一个 handle 函数,再对每一个 structenum 都实现这个方法,最终就可以通过调用根命令的 handle 来处理整个树的执行了。

对于 SimpleCommand,由于其拥有子命令,因此结构体中必定存在一个 #[clap(subcommand)] 的字段表示所属的子命令;而对于 SimpleSubcommand,其 enum 中每一个 variant 都实际对应了一个子命令。因此这两部分都能简单地通过过程宏实现。

pub trait Handle {

fn handle(&self) -> anyhow::Result<()>;

}

#[proc_macro_derive(ClapHandler)]

pub fn derive_clap_handler(item: TokenStream) -> TokenStream {

let input = parse_macro_input!(item as DeriveInput);

let name = &input.ident;

let expanded = match input.data {

Data::Struct(DataStruct { ref fields, .. }) => {

match fields {

Fields::Named(ref fields_name) => {

// find struct field which has #[clap(subcommand)]

let subcommand_field: Option<syn::Ident> = fields_name

.named

.iter()

.find_map(|field| {

for attr in field.attrs.iter() {

if attr.path.is_ident("clap") {

let ident: syn::Ident = attr.parse_args().ok()?;

if ident == "subcommand" {

return Some(field.ident.clone().unwrap());

}

}

}

None

})

.expect("Failed to find #[clap(subcommand)] in struct!");

quote! {

impl Handle for #name {

fn handle(&self) -> anyhow::Result<()> {

self.#subcommand_field.handle()

}

}

}

}

_ => panic!("ClapHandler is not implemented for unnamed or None struct"),

}

}

Data::Enum(DataEnum { variants, .. }) => {

// list enum variants

let subcommands: Vec<_> = variants

.iter()

.map(|v| {

let ident = &v.ident;

quote! { #name::#ident }

})

.collect();

quote! {

impl Handler for #name {

#[inline(always)]

fn handle(&self) -> anyhow::Result<()> {

match self {

#(#subcommands(s) => s.handle(),)*

}

}

}

}

}

_ => panic!("ClapHandler is not implemented for union type"),

};

expanded.into()

}

RE:thonk

好,回答一个问题:这样就够了吗?

考虑这样一种情形。顶层的 Command 读取配置并构造了一个 ABCManager,而子命令则需要根据 ABCManagerABC 三种不同方法进行处理,同时需要读入各自不同的参数。

很自然地,我们将 ABC 归为三个(叶)子命令。但这样的话,问题就出现了。我们的叶子命令 A 需要依赖上一层命令解析得到的结果 ABCManager,而这在现有的体系下是无法做到的。我们的 trait Handle 只支持了无参数的 handle 方法,怎样才能把参数传递下去呢?

最简单的想法就是直接把参数传递下去,这也是目前 Anni 使用的方案。这种方案简单粗暴,但也存在着很大的问题。最大的问题就是可扩展性太差了,很难处理复杂的依赖情况,但对目前的 Anni 而言也还算足够,于是就这样先用着了。

但实际上,对于实际执行到叶子命令,我们还可以提供更多的依赖信息。观察命令解析的树,不难发现,子命令的解析实际上就是在寻找一条到对应 Subcommand 的路径。而在这条路径中,同级的其他子命令是不会被执行的。也就是说,对于这个寻找的过程而言,其完全可以把所有权交给下一级的处理者——也就是 self 而非 &self。对于实际执行的叶子命令,其完全可以获得一个 Owned 的,能够逐级反推最后得到根命令 struct 的参数。

结语

builderderiveAnni 的命令解析终于进入了可维护的轨道,但这也只是刚刚开始。正如 RE:thonk 部分所描述的,目前使用的方案还是最简单粗暴的。并且由于个人对宏的了解尚浅,现在的实现可以说是把头都绕没了,还有很大的改进空间。

Anni 的旅程也远不止 clap 那么简单。恰恰相反,clap 只是整个工具的第一步。从 Project Anni 的编写过程中所学到的,就静侯后续的其他记录吧。