- CLI
- 开发工具
- 文本处理
【免费下载链接】sd
Intuitive find & replace CLI (sed alternative)
sd(search & displace)是一个以"直觉化"著称的查找替换命令行工具,被定位为sed在日常文本替换场景下的替代品。本文以仓库根目录下的 CHANGELOG.md 为骨架,结合 sd/src/lib.rs、sd-cli/src/cli.rs、sd/src/unescape.rs 等源码与测试,逐版本拆解 sd 的接口设计、处理引擎、正则能力与发布工程的关键变迁。读完本文,你将理解 sd 从 0.3.0 到 1.0.0 乃至当前 Unreleased 版本的核心演进逻辑,并能准确使用--fixed-strings、--across、-f/--flags、-n/--max-replacements等参数,同时了解其逐行处理与原子写入的底层原理。
一、版本演进地图:先看全貌
CHANGELOG 记录的版本序列如下(v0.6.3 ~ v0.7.6 之间的历史缺失,原文档已注明):
| 版本 | 时间 | 核心主题 |
|---|---|---|
| 0.3.0 | 2018-12-29 | 命令行形态定型:-i/--in-place重构,文件路径移到最后,引入原子写入 |
| 0.4.0 | 2018-12-30 | 正则 flags(m/c/i)、智能大小写、支持多文件 |
| 0.4.1 | 2019-01-01 | 显著性能优化 |
| 0.4.2 | 2019-01-02 | 替换表达式支持 Unicode 与特殊字符(如\n) |
| 0.5.0 | 2019-02-22 | Windows 支持 |
| 0.6.0 | 2019-06-15 | 内存映射文件、-p/--preview预览、w正则标志 |
| 1.0.0 | 2023-11-07 | 接口收口:--string-mode更名--fixed-strings、捕获组歧义硬报错、符号链接安全、gen 静态资产 |
| Unreleased | 当前 | 逐行处理成为默认(--across/-A保留整文件模式)、宽松 unescape、原子写入保留所有权 |
从这张表可以看出一条清晰的主线:先是把"查找 + 替换"两段式命令形态和正则能力做扎实,再在 1.0.0 收口公共接口,最后在 Unreleased 阶段把内存占用与流式输出做到极致。下面按这条主线逐层深入。
二、命令行形态的定型(0.3.0)
CHANGELOG 明确指出 0.3.0 是一次 Breaking Change:-i/--input被重构,文件路径从"跟在-i后面"移到参数末尾,--input也更名为--in-place。
# 之前(0.3.0 之前) sd -s 'str' '' -i file.txt # 现在 sd -i -s 'str' '' file.txt # 未来形态(支持通配符) sd -i -s 'str' '' *.txt这一形态沿用至今:sd [OPTIONS] <FIND> <REPLACE_WITH> [FILES]...。当前 sd-cli/src/cli.rs 中位置参数find、replace_with与可选files(Vec<PathBuf>)正是这一设计的结果;文件列表为空时自动退化为从 STDIN 读取,由 sd/src/input.rs 中的Source::from_stdin()/Source::from_paths()完成路由。
同时,0.3.0 起文件写入改为原子写入(原文档标注对应 issue #3):不直接改写原文件,而是先在同一目录写临时文件、再替换。这一机制在今天的 write_with_temp 中仍然可见——tempfile::NamedTempFile::new_in(path.parent())创建同目录临时文件,随后temp.persist(&path)完成原子替换,中途失败不会留下半截文件。
三、正则能力层的演进(0.4.0 → 0.6.0)
3.1 正则 flags 与智能大小写(0.4.0)
0.4.0 加入-f/--flags选项,支持m(多行)、c(区分大小写)、i(不区分大小写),并默认启用"智能大小写"——即查找串含有大写字母时视为区分大小写,否则不区分。
当前版本的 flags 已扩展为六种,定义在 cli.rs,可组合使用(如-f mc):
| 标志 | 含义 |
|---|---|
c | 区分大小写 |
e | 禁用多行匹配 |
i | 不区分大小写 |
m | 多行匹配 |
s | 让.匹配换行符 |
w | 仅匹配完整单词 |
从 sd/src/replacer/mod.rs 的Replacer::new实现看,这些 flags 被逐一翻译成regex::bytes::RegexBuilder的配置:c关闭case_insensitive、i开启、e关闭multi_line、s开启dot_matches_new_line,而w则是把查找串改写为\b<pattern>\b后重新构造正则。m本身是默认行为(构建时regex.multi_line(true)),所以在 flags 中显式传入时什么都不做。
3.2 单词匹配与预览(0.6.0)
0.6.0 带来三件事:
- 内存映射文件:支持对任意大小的文件做替换(CHANGELOG 原文:"replacement on files of any size")。这一能力在后来 #183 中被迁移出未维护的 crate,并在 Unreleased 阶段被逐行模式取代(见下文)。
-p/--preview预览:先输出结果到 stdout、不落盘。1.0.0 的 #124 又去掉了预览时的文件路径显示并修正了测试中的着色处理;当前 lib.rs 中,预览路径直接write_all输出替换结果,多文件时以----- FILE <path> -----分隔。w正则标志:即上表中的全单词匹配,例如sd -f w foo bar file.txt。
0.4.1 的"显著性能提升"与 0.4.2 的"替换表达式支持 Unicode 与特殊字符(如\n)"则分别对应 README 中列出的基准测试与 unescape.rs 的转义体系(详见第五节)。
四、1.0.0:接口收口与发布体系成型
1.0.0(2023-11-07)是公开接口的一次集中收口,CHANGELOG 将其中的变更明确标为 Breaking。
4.1--string-mode更名--fixed-strings(#192)
原-s/--string-mode被重命名为-f/--fixed-strings(短选项为-F),以对齐同类工具的命名习惯。为保持兼容,旧的-s与--string-mode仍可继续使用,只是不再出现在文档中。这一设计在当前源码中体现为 cli.rs 的short_alias = 's', alias = "string-mode"。1.0.0 的 Docs 条目(#279、#281、#320)也专门修复了 man page 与 README 中该参数改名后的示例。
注意:
-F/--fixed-strings与-f/--flags是两个不同的选项。前者让 FIND 与 REPLACE_WITH 都按字面字符串处理,后者设置正则标志。当前两个版本帮助文本容易混淆,README_zh-CN.md 的快速指南中已统一使用新的-F写法。
4.2 捕获组歧义硬报错(#258)
正则模式下,替换文本中的$1、$2等表示编号捕获组。此前若写$1foo,会因regex库的插值规则把1foo整体当作一个(无法存在的)捕获组名,导致静默替换为空。1.0.0 起,sd 在构造替换器时预先校验替换文本:一旦发现编号捕获组后紧跟非数字字符(如$1foo),立即报错并给出修正提示。
实现位于 sd/src/replacer/validate.rs 的validate_replace:遍历替换文本中所有$开头的捕获引用,若首字符是 ASCII 数字而后续又出现字母/下划线,则返回InvalidReplaceCapture错误。错误信息会精确指出歧义位置并用^^^^标注,例如:
error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. before $1bad after ^^^^对应的 CLI 集成测试在 sd-cli/tests/cli.rs 的ambiguous_replace_basic、ambiguous_replace_multibyte_char、ambiguous_replace_issue_44中覆盖了普通文本、多字节字符(如😈$1bad😇)与复杂函数调用等场景。正确写法是给编号加上花括号:${1}foo。注意该校验只作用于正则模式——--fixed-strings模式下$1只是普通字符(测试fixed_strings_ambiguous_replace_is_fine验证了这一点)。
同样值得注意的还有$$转义:在替换文本中$$表示一个字面$(README 的 #207 对此做了文档化),其解析逻辑见 validate.rs 中ReplaceCaptureIter对b'$'的处理,以及interpolation_matches_upstream这一与regex-automata上游插值行为对拍的 property test。
4.3 符号链接安全(#115)
此前对符号链接执行原地替换会把链接本身替换成普通文件。1.0.0 修复为跟随符号链接、改写其指向的目标文件,并保留链接本身。当前实现中,write_with_temp与write_file_line_by_line都先调用fs::canonicalize(path)解析真实路径,再在目标文件所在目录创建临时文件并persist;测试 in_place_following_symlink 同时断言目标文件内容已更新且链接类型仍是 symlink。
4.4 gen 静态资产与发布形态
1.0.0 起,shell 补全与 man page 从构建脚本迁移到cargo-xtask任务(#196),产物统一放在仓库根目录的 gen 目录下,并随预编译发布包一起分发。CHANGELOG 对此的说明是:"The generated shell completions and man page are now in thegendirectory of the repo."
xtask/src/generate.rs 展示了完整流程:gen_shell用clap_complete::generate_to为所有支持的 shell 生成补全(见 gen/completions 下的_sd、sd.bash、sd.fish等);gen_man用clap_mangen+roff渲染 gen/sd.1。关键设计是(#231):man page 直接由 clap 定义驱动生成,与--help输出天然保持同步,杜绝手工维护导致的漂移。
4.5 平台与预编译目标扩充
1.0.0 及其后的 Unreleased 阶段持续扩充发布矩阵,CHANGELOG 逐条记录了每个 target 的落地:
- ARM 生态:
arm-unknown-linux-gnueabihf(11295fb)、armv7-unknown-linux-gnueabihf(#204)、aarch64-unknown-linux-gnu(c864c58) - Apple:
aarch64-apple-darwin(#114),由于 ARM Apple 目标难以跑测试,#205 调整为只构建不执行测试 - Windows:
x86_64-pc-windows-gnu与x86_64-windows-musl回归(#206),Unreleased 又新增aarch64-pc-windows-msvc(90bc67d) - 其他:
aarch64-ubuntu-linux-musl(#179,同时开始 strip 发布二进制)
这些 target 由 CI 中的 Cargo.toml([profile.release]下opt-level = 3、lto = true、strip = true)配合发布流程产出。Windows 支持本身最早可追溯到 0.5.0。
五、Unreleased:逐行处理成为默认(本次演进的核心)
Unreleased 部分的第一条 Breaking 是 #328:把逐行处理设为默认,并新增--across/-A保留整文件处理。这是 CHANGELOG 当前阶段信息量最大的变更,源码侧给出了完整印证。
5.1 逐行模式 vs across 模式
main.rs 中!options.across被直接作为line_by_line参数传给process_sources;lib.rs 的process_sources_line_by_line是逐行模式的入口。其核心循环 process_reader_line_by_line 用 8KB 的 chunk 缓冲逐行读取:遇到\n就对该行做一次替换并立即写出,文件末尾若没有换行符也照常处理残行。
逐行模式带来的行为收益(CHANGELOG 与 README 均明确列出):
- 内存占用极低:任意时刻只保留一行在内存中。README 的基准对比中,across 模式峰值 RSS 约 74MB,逐行模式仅约 3MB;
- stdin 流式输出:结果边读边出,不等 EOF 就能看到输出;
^/$语义更直观:默认multi_line(true)使^/$匹配每行首尾,且逐行处理时不会出现"幽灵匹配"(测试 line_by_line_caret_no_phantom 验证^只命中真实行首);- 行尾处理更安全:
\s+$可以干净地去掉行尾空白而不吞掉换行符(测试 line_by_line_whitespace_trim)。
代价是跨行模式匹配不再可用。需要匹配跨行边界的内容(例如把换行符替换为逗号:sd -A '\n' ',',或跨行捕获),必须显式加-A/--across:
echo -e "hello\nworld" | sd -A '\n' ',' # hello,world性能上,README 基准显示逐行模式比 across 慢约 2~3 倍,但在正则替换场景下仍快于 sed(1M 行 36MB 文件、(\w+) world替换的对比中,sed 为 4432ms,逐行 sd 为 566ms,across sd 为 254ms)。逐行模式的本质是用可控的吞吐换取内存的恒定与流式能力,这是处理超大文件时的关键权衡。
5.2 宽松的 unescape(#313)
替换文本在正则模式下需要先经过反转义(unescape)处理,把\n、\t等字面反斜杠序列转换为真实字符。旧实现是"全有或全无":一旦某个转义序列非法,整个解析失败。#313 改为宽松策略——逐个字符尝试转义,成功就替换,失败就原样保留反斜杠与后续字符继续解析。
实现在 sd/src/unescape.rs 的unescape函数中,支持的转义序列为:
| 序列 | 结果 |
|---|---|
\n | 换行 |
\r | 回车 |
\t | 制表符 |
\'/\" | 单/双引号 |
\\ | 反斜杠 |
\uXXXX | Unicode 码点(4 位十六进制) |
\xXX | 字节(2 位十六进制) |
快照测试 sd__unescape__test__unescape.snap 详细记录了行为边界:\xG、\u00Z1这类非法序列会被原样保留(\xG -> \xG),而a\t\xG\n这种混合输入则是"能转的转、不能转的留"(输出a\t\xG\n中 tab 被转换)。回归测试 escaping_unnecessarily 直接引用了 issue #313:\n{应转为换行加左花括号,\n\{中多余的\{则保留字面反斜杠。
这一改动让替换文本的容错性大幅提升:输入里偶尔混入的"伪转义"不再导致整个替换失败,而是以近似字面语义继续执行。
5.3 原子写入保留所有权(#326)
#326 让原子写入路径在替换文件时保留原文件的 uid/gid。配合 0.3.0 以来的临时文件 +persist机制(write_with_temp、write_file_line_by_line),替换后文件的权限(set_permissions复制原权限位)与属主均不会意外改变,这在以其他用户身份批量处理文件、或依赖属主做后续操作(如 CI、部署脚本)的场景中尤为重要。
5.4 内部工程:代码库重组(#265)与依赖治理
Unreleased 与 1.0.0 的 Internal 条目共同描绘了 sd 的工程质量路线:
- #265 整体代码库重组:重构应用结构、错误处理与测试组织。当前工作区在根 Cargo.toml 中拆为
sd(核心库,见 sd/Cargo.toml)、sd-cli(命令行封装)与xtask(静态资产生成)三个 crate,错误类型集中在 sd/src/error.rs,FailedJobs会在多文件处理中汇总所有失败项后一次性报告; - clap v4 迁移(#189):从 structopt 迁移到 clap v4 derive,当前 cli.rs 即 clap v4 风格定义;
clap_mangen接管的 man page 生成(#231)随之受益; - 2021 edition(#245):升级到 Rust 2021 edition(工作区现为 edition 2024,见 Cargo.toml);
- 依赖治理:#183 换掉未维护的文件映射 crate、#188 用
is-terminal替代atty、libc 从 0.2.149 升到 0.2.155、assert_cmd升级并修复 clippy 警告; - CI 与发布:#249 起从
actions-rs迁到dtolnay@rust-toolchain、改用$GITHUB_ENV,发布流程沉淀为 RELEASE.md 与 release.toml。
六、当前 CLI 参考速查
综合 cli.rs 的参数定义、gen/sd.1 的 man page 与 README 帮助文本,当前 sd 的完整命令行形态为:
sd [OPTIONS] <FIND> <REPLACE_WITH> [FILES]...| 参数 | 说明 | 默认值 |
|---|---|---|
<FIND> | 要查找的正则表达式(或-F时的字面字符串) | 必填 |
<REPLACE_WITH> | 替换内容;正则模式下支持$1、$2、${name}等捕获引用 | 必填 |
[FILES]... | 目标文件;省略时从 STDIN 读取并输出到 STDOUT | 可选 |
-p, --preview | 以可读形式预览改动(格式细节可能变化) | 否 |
-F, --fixed-strings | 将 FIND 与 REPLACE_WITH 按字面字符串处理(兼容旧-s/--string-mode) | 否 |
-n, --max-replacements <LIMIT> | 每个文件最多替换次数,0表示不限 | 0 |
-f, --flags <FLAGS> | 正则标志,可组合如-f mc(c/e/i/m/s/w见上表) | 无 |
-A, --across | 整文件处理而非逐行;允许跨行匹配,但更耗内存、无法流式 | 否(默认逐行) |
-h, --help | 打印帮助 | — |
-V, --version | 打印版本 | — |
退出码约定(见 gen/sd.1 的 EXIT STATUS):0成功、1失败、101panic。
几个高频用法示例(与 README 一致):
# 字面字符串替换(不用写正则转义) echo 'lots((([]))) of special chars' | sd -F '((([])))' '' # 去掉行尾空白 echo 'lorem ipsum 23 ' | sd '\s+$' '' # 编号捕获组 echo 'cargo +nightly watch' | sd '(\w+)\s+\+(\w+)\s+(\w+)' 'cmd: $1, channel: $2, subcmd: $3' # 命名捕获组(Python 风格 (?P<name>...)) echo "123.45" | sd '(?P<dollars>\d+)\.(?P<cents>\d+)' '$dollars dollars and $cents cents' # 原地修改文件(默认行为,配合 -p 预览) sd -p 'window.fetch' 'fetch' http.js # 跨行替换需要 --across echo -e "hello\nworld" | sd -A '\n' ','关于--边界:sd 会把所有以-开头的参数当作标志解析,因此当查找/替换文本以-开头时,用--显式结束标志解析(如echo "./hello foo" | sd "foo" -- "-w")。此外替换文本中的$需要写$$才是字面量。
七、总结:从 CHANGELOG 读出的设计哲学
纵观 CHANGELOG,sd 的演进始终围绕四件事展开:
- 接口克制:0.3.0 定下
FIND / REPLACE_WITH / FILES三段式后,唯一的 Breaking 集中在命名对齐(--fixed-strings)与行为纠偏(逐行默认、歧义捕获报错)上; - 内存与吞吐的权衡:从 0.6.0 的内存映射整文件处理,到 Unreleased 的逐行默认 +
--across保留旧路径,sd 把"大文件可用"与"流式输出"这两个诉求统一到同一条命令行上,由用户按场景选择; - 容错优先:#313 的宽松 unescape 与 #258 的早期报错看似相反,实则一致——能在输入层识别的问题立即报错(歧义捕获),无法判定意图的输入则尽量保留原样(转义失败);
- 工程可持续性:gen 静态资产、xtask、clap v4 自动同步 man page、跨平台发布矩阵,让 1.0.0 之后的功能迭代能稳定落地到各平台。
对于想深入源码的读者,建议按这条链路阅读:sd-cli/src/cli.rs(参数入口)→ sd-cli/src/main.rs(装配)→ sd/src/lib.rs(两种处理模式与原子写入)→ sd/src/replacer/mod.rs(替换引擎)→ sd/src/replacer/validate.rs(捕获校验)→ sd/src/unescape.rs(转义解析),配合 sd-cli/tests/cli.rs 与 sd/src/replacer/tests.rs 中的行为断言,即可完整还原 CHANGELOG 中每一项变更的落地细节。
- CLI
- 开发工具
- 文本处理
【免费下载链接】sd
Intuitive find & replace CLI (sed alternative)
相关推荐
GeoPandas 版本演进全解析:从 CHANGELOG 读懂核心能力变迁与迁移指南
GeoPandas 版本演进全解析:从 CHANGELOG 读懂核心能力变迁与迁移指南 GeoPandas 是面向地理数据的 Python 工具库,其官方变更日
数据分析GISBootbox 版本演进全解析:从 CHANGELOG 看 1.x 到 6.0.4 的 API 与能力变迁
Bootbox 版本演进全解析:从 CHANGELOG 看 1.x 到 6.0.4 的 API 与能力变迁 本文以 CHANGELOG.md https://l
前端UI组件nyc 版本演进全解:从 CHANGELOG 看 Istanbul CLI 的功能变迁与当前实现
nyc 版本演进全解:从 CHANGELOG 看 Istanbul CLI 的功能变迁与当前实现 nyc 是 Istanbul 的命令行界面(CLI),用于为
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考