Pier 源码深读:一文读懂用 Serde 反序列化实现脚本别名自动注入 📅 2026/8/22 14:50:12 Pier 源码深读一文读懂用 Serde 反序列化实现脚本别名自动注入【免费下载链接】pierA CLI to organize and run short Unix shell scripts项目地址: https://gitcode.com/gh_mirrors/pi/pierPier是一个 Rust 编写的命令行脚本管理工具CLI to organize and run short Unix shell scripts帮你把散落在各处的 shell 一行命令集中管理、打标签、一键运行。本文带你深读 Pier 源码中一个精巧的设计通过自定义 Serde 反序列化Deserialize在解析 TOML 配置文件时自动把表名注入为脚本别名alias——无需手动维护两份数据。为什么 Pier 需要别名自动注入Pier 的核心工作流只有三步把常用命令写进 TOML 配置文件给每条命令起一个表名即别名在终端里输入别名直接执行一份典型的 Pier 配置长这样参考 examples/example.tml[scripts.refresh-wifi] command ip link set wlp58s0 down sleep 5 ip link set wlp58s0 up tags [wifi] [scripts.flush-docker] command docker container stop $(docker container ls -a -q) docker system prune -a -f --volumes description A script to clear out old Docker containers and images tags [docker, flush]注意看refresh-wifi、flush-docker这些别名不是配置里的字段而是表名。这就带来一个反序列化难题——如果用 Serde 的derive宏自动派生Script结构体里的alias字段永远拿不到值因为 TOML 里根本没有alias ...这一行。这就是 Pier 手写反序列化的原因让表名流进结构体字段。关键数据结构一个被刻意跳过的字段先看 src/script.rs 中的Script结构体第 11-19 行#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Script { #[serde(skip)] pub alias: String, pub command: String, pub description: OptionString, pub reference: OptionString, pub tags: OptionVecString, }两个细节值得玩味#[serde(skip)]alias字段既不会被序列化也不参与反序列化。也就是说Pier 把配置文件写回磁盘时不会多写一行alias xxx保证 TOML 文件干净、别名只来自表名Single Source of Truth。Script自身派生的Deserialize实现依然存在这为下面逐条解析每个脚本表提供了基础。而 src/config.rs 中的Scripts则是一个包装BTreeMapString, Script的元组结构体第 33-34 行——只派生了Serialize故意不派生Deserialize把反序列化的工作留给了手写的Visitor。核心原理三步看懂 ScriptsVisitor 的注入过程整个别名自动注入机制位于 src/config.rs一共三个部分第 1 步声明一个地图访问者 Visitorstruct ScriptsVisitor { marker: PhantomDatafn() - Scripts }PhantomData在这里告诉编译器这个 Visitor 的产出类型是Scripts。Visitor 模式是 Serde 反序列化的底层抽象——框架把 TOML 里的 map 一条条喂给你你来决定如何组装成最终结构体。第 2 步逐条消费 map 条目顺手注入别名fn visit_mapM(self, mut access: M) - ResultSelf::Value, M::Error where M: MapAccessde, { let mut map Scripts(BTreeMap::new()); while let Some((key, mut value)) access.next_entry::String, Script()? { value.alias key.clone(); // ← 核心就这一行表名 → alias map.0.insert(key, value); } Ok(map) }access.next_entry::String, Script()会依次取出[refresh-wifi, Script{...}]这样的键值对key 是表名value 是 Serde 已按派生规则解析好的Script。紧接着value.alias key.clone()把表名注入到字段里——自动注入全部就发生在这一行没有任何额外配置。第 3 步把 Visitor 挂上自定义 Deserializeimplde Deserializede for Scripts { fn deserializeD(deserializer: D) - ResultSelf, D::Error where D: Deserializerde, { deserializer.deserialize_map(ScriptsVisitor::new()) } }这样只要Config结构体src/config.rs派生Deserialize并包含scripts: Scripts字段toml::from_str解析时就会自动走上面这条自定义路径。整个数据流可以概括为TOML 文件 →Config::from()src/config.rs 调用toml::from_str→Scripts的自定义Deserialize→ScriptsVisitor::visit_map逐表解析表名注入 alias→ 内存中的BTreeMap别名, Script注入的 alias 在哪里派上用场别名注入不只是为了好看它在运行时承担了两个实际角色见 src/script.rs 与 src/lib.rs作为脚本参数传入run_with_cli_interpretersrc/script.rs执行sh -c command时会把alias作为额外参数追加。于是你在脚本里可以通过$0拿到自己的别名方便按名字做逻辑分支。作为临时可执行文件名带#!shebang 的脚本会走run_with_shebangsrc/script.rsPier 用alias作为临时文件名创建、执行、清理——这解释了为什么每个别名必须唯一。查找与增删fetch_script、remove_script、add_scriptsrc/lib.rs全都以 alias 为键操作脚本pier alias的执行入口run_script同样依赖它。动手体验30 秒跑通 Pier安装cargo install pier或从 GitHub release 下载二进制Nix 用户可用nix-env -if derivation.nix见根目录 derivation.nix 与 Makefile添加脚本pier add ip link set wlp58s0 down sleep 5 ip link set wlp58s0 up --alias refresh-wifi执行pier refresh-wifi用pier list查看表格化的脚本清单配置文件默认保存在~/.config/pier/config.toml查找顺序见 README 的pier --help输出。打开你的配置文件你会发现 Pier 写入的内容没有alias字段——它只写在表名里。下次pier读取文件时就是本文讲的 Visitor 悄悄把表名填回了alias。小结这个模式能迁移到你自己的项目吗设计点Pier 的做法适用场景元信息藏在键里别名 TOML 表名任何配置项自身带标签的场景字段与键解耦#[serde(skip)]避免重复存储减少冗余、保证单一数据源键值同步注入手写DeserializeVisitor::visit_map需要解析时做跨字段加工的序列化如果你写 Rust 序列化代码时遇到字段值必须取自 map 的 key这类需求Pier 的 src/config.rs 就是一个可以直接照抄的模板约 50 行代码零额外依赖逻辑清晰到一行注释就能讲完——这正是源码深读最让人有收获的部分。【免费下载链接】pierA CLI to organize and run short Unix shell scripts项目地址: https://gitcode.com/gh_mirrors/pi/pier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考