ani-cli 源码解析(一):670 行 POSIX Shell 如何构建一个完整 CLI 应用
【免费下载链接】ani-cliA cli tool to browse and play anime项目地址: https://gitcode.com/gh_mirrors/an/ani-cli
ani-cli 是一个在终端里搜索、播放和下载动漫(anime)的命令行工具,它没有依赖任何框架,整个应用就是一个 670 行的 POSIX Shell 脚本。本文将从源码结构、参数解析、抓取流程和跨平台适配四个角度,带你拆解这个"极简但完整"的 Shell CLI 应用是如何写出来的。
一个文件就是一个完整 CLI 应用
传统观点认为:做一个像样的 CLI 需要选语言、选框架、管依赖。ani-cli 打破了这个印象——整个项目主体就是这一个可执行脚本:
- 主程序:ani-cli(670 行,
#!/bin/sh开头,兼容 POSIX sh) - 手册页:ani-cli.1
- Homebrew 配方:Formula/ani-cli.rb
- 二次开发指南:hacking.md
想本地跑起来的话:
git clone https://gitcode.com/gh_mirrors/an/ani-cli sudo cp ani-cli/ani-cli /usr/local/bin📌 为什么坚持 POSIX Shell?答案在 CONTRIBUTING.md 里:项目要求通过shfmt与shellcheck -s sh(严格 POSIX 模式)检查,并且明确写着"No extra dependencies unless absolutely necessary"。极小的体积带来一个巨大好处——随处可跑:Linux、macOS、Windows (Git Bash/WSL)、Android Termux、iOS iSH、Steam Deck,甚至 FreeBSD。
快速总览:670 行的功能分区
打开 ani-cli 你会发现它按注释被清晰地切成了六大区块,这是阅读长脚本最好的"地图":
| 区块 | 行号范围 | 职责 |
|---|---|---|
# UI | ani-cli#L5-L44 | 交互菜单、彩色输出、帮助信息 |
# Bookkeeping | ani-cli#L108-L148 | 自动更新、依赖检查、清理 |
# SCRAPING | ani-cli#L150-L308 | 搜索、剧集列表、m3u8 解密 |
# HISTORY | ani-cli#L310-L340 | 观看历史(续看功能的数据基础) |
# PLAYING | ani-cli#L342-L437 | 多播放器适配、分集播放 |
# MAIN | ani-cli#L439-L670 | 全局配置、参数解析、主流程 |
项目官方文档 hacking.md 甚至为整套交互设计了一张状态机式的 UX 规格图,搜索 → 选片 → 选集 → 播放 → 后菜单的完整闭环一目了然:
命令行参数解析:一个 while + case 搞定所有选项
很多语言需要引入 argparse、clap 这类库,Shell 的"标准答案"是while循环 +case模式匹配。ani-cli#L492-L560 用一个约 70 行的解析循环覆盖了全部 17 个选项:
while [ $# -gt 0 ]; do case "$1" in -q | --quality) quality="$2"; shift ;; -c | --continue) source=history ;; ... *) query="...$1" ;; # 非选项参数拼成搜索词 esac shift done几个值得学习的设计:
- 长短选项等价:
-q | --quality写在同一个分支里,零成本支持两种风格; - 位置无关:
[ $# -lt 2 ] && die "missing argument!"对带参数的选项做校验,让用户可以把选项放在查询词前后(帮助信息里明确承诺了这一点,见 ani-cli#L48-L51); - 剩余参数即查询:
*)分支把所有非选项参数合并成一个查询字符串,空格转+,天然实现"多词搜索"; - 无框架的 help/version:
-h输出由printf模板生成(ani-cli#L46-L101),-V直接读第 3 行的version_number变量——版本号只有一处定义,自更新时靠sed提取它(ani-cli#L113),保证全局一致。
💡 这种"case 表驱动"的写法,是所有 Shell CLI 项目最值得照抄的参数解析范式。
交互菜单:三层抽象让 fzf / rofi / dmenu 互换
ani-cli 的交互核心是两个函数:
menu()(ani-cli#L9-L16):按menu_program分发到 fzf / rofi / dmenu 或任意自定义程序,每个后端的参数差异被压缩进一个case;nth()(ani-cli#L19-L35):接收"序号 + Tab + 数据"格式的标准输入,让用户选一行或一段范围,再统一切出结果字段。
整个 UI 层因此与具体菜单程序解耦:环境变量ANI_CLI_MENU、命令行--rofi/--dmenu都能切换,甚至连"非终端环境"(如管道调用)都会自动降级到 dmenu(ani-cli#L479-L482)。播放结束后还有"后菜单"循环(ani-cli#L635-L653):next / replay / previous / select / change_quality / quit,同样复用nth(),一行代码都不用重写。
抓取流程:从搜索词到 m3u8 的四步管道
hacking.md 里有一张官方流程图,概括了 ani-cli 作为爬虫的完整链路:
对应到代码,每一步都是一个"小函数 + sed 正则"的组合:
- 搜索:ani-cli#L199-L210 请求搜索页后,用三级
sed管道把 HTML 压平、按film-detail切块、抽取id \t 标题; - 剧集列表:ani-cli#L213-L219 从剧集页抽出
id \t 集数; - 取流地址:ani-cli#L222-L242 是最硬核的一步——内嵌播放器把配置做成了
base64(json XOR 密钥),deobfuscate_blob() 用od取字节、在子 shell 里逐字节异或还原出 JSON,再从中抠出 m3u8 地址和字幕; - 选清晰度:select_quality() 按
best/worst/1080等规则从"分辨率 > URL"列表里挑一条。
🔍 值得注意的是防封处理:hianime_curl() 封装了带超时的 curl,检查 HTTP 状态码并识别 Cloudflare 的"Just a moment"拦截页;curl 本身也会按curl_firefox135 → curl_chrome136 → … → curl的顺序自动降级选择(dep_ch_failover),普通用户无感。
跨平台适配与自更新:Shell 项目的两个硬骨头
平台差异用一个uname分发集中解决(ani-cli#L457-L469):
- macOS:默认播放器 IINA(Apple 上 mpv 的友好替代);
- Android (Termux):
android_mpv走am start拉起 mpv 应用,referrer 通过共享配置文件传递(ani-cli#L355-L362); - Windows/WSL:直接用
mpv.exe; - iOS (iSH):输出 OSC8 超链接,点一下在 VLC 打开(ani-cli#L399-L402);
- Linux:mpv → flatpak mpv → vlc 逐级回退。
而play_episode()里那段按播放器分发的case(ani-cli#L371-L404)则展示了另一个技巧:不同播放器传参风格完全不同(mpv 用--sub-file、VLC 用:input-slave=、IINA 要转义冒号),统一收口在一个函数里,按名字模式匹配分派,比抽象类更"Shell"。
自更新则是 Shell 圈的经典骚操作:-U拉取远端脚本 →diff对比自己($0)→ 用patch原地修补(update_script())。没有包管理器、没有下载器,脚本自己给自己打补丁。
写在前面:这套写法能学到什么
ani-cli 证明了 Shell 不是"临时脚本语言":清晰的区块注释、单一数据格式(id \t name贯穿全链路)、case表驱动、failover 依赖探测、trap清理(ani-cli#L581),这些模式组合起来就是一个可维护的完整应用。
如果你也想改它的抓取逻辑或移植到新站点,建议从官方指南 hacking.md 入手,其中给出了搜索页、剧集页、播放器三处改造的完整方法论;而 ani-cli.1 则是一份适合快速查选项的手册页。
下一篇我们计划深入deobfuscate_blob()的异或解密细节与观看历史系统,敬请期待。
【免费下载链接】ani-cliA cli tool to browse and play anime项目地址: https://gitcode.com/gh_mirrors/an/ani-cli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考