Sublarge

// Linux · 2026-08-30

fortunes / fortunes-zh:从一条命令行到词库底层

一份文本、一张偏移表、一个加权随机。数据文件决定"有什么",.dat决定"怎么快",百分比语法决定"怎么偏"。理解了这三层,fortune在你眼里就不再是黑盒——它甚至不如你自己写的 Python 复杂。下次想在终端里"撞见"一句诗,记得先。

1. 这是两个什么东西

  • fortune(包名 fortune-mod):一条随机打印“格言 / 签语”的命令。名字来自西方谚语 Fortune cookie(幸运饼干),掰开就是一句签文。它是个上古 Unix 工具,Ken Arnold 在 1979 年写的,至今还活在几乎每个发行版里。
  • fortunes-zh:中文词库包,附带一个 fortune-zh 包装脚本。内容包含唐诗三百首、宋词一百首、成语、谚语、警句,甚至 Debian 社区礼仪、Docker 手册摘录等,默认配色输出。

一句话:fortune 是“抽签机器”,fortunes-zh 是“中文签文库”,两者一拼,终端就变成了一个会念诗的文化人。

2. 安装

Debian 系(Kali / Ubuntu)一行装完:

$ sudo apt install fortune-mod fortunes-zh
bash1 行

装完先别急着用 —— 会踩一个经典坑:二进制不在默认 PATH 里。

$ which fortune
fortune not found
bash2 行

Debian 把游戏类程序统一放在 /usr/games/,而这个目录往往不在普通用户的 PATH 里。看一下包归属:

$ dpkg -S /usr/games/fortune /usr/games/fortune-zh /usr/bin/strfile
fortune-mod: /usr/games/fortune
fortunes-zh: /usr/games/fortune-zh
fortune-mod: /usr/bin/strfile
bash4 行

所以要么写全路径,要么把目录加进 PATH(写入 ~/.zshrc / ~/.bashrc):

$ export PATH="$PATH:/usr/games"
bash1 行

顺带一提,strfile 是生成索引的工具,它乖乖待在 /usr/bin,后面第 6 节会用到。

3. 第一炮

验证版本(注意老工具不认 --version 这种 GNU 风格长选项,但会顺口把版本号吐出来):

$ fortune -h 2>&1 | head -1
fortune-mod version 9708
fortune [-afilosw] [-m pattern] [-n number] [ [#%] file/directory/all]
bash3 行

直接抽签:

$ /usr/games/fortune
佳节清明桃李笑,野田荒冢只生愁。
    -- 黄庭坚《清明》
bash3 行

这是英文版工具的默认输出吗?不是。fortune 没带参数时扫的是默认数据目录 /usr/share/games/fortunes/,而里面装的全是中文词库,所以抽出来自然全是中文。

用官方包装脚本 fortune-zh,输出更讲究(作者一行用黄色、书名用绿色):

$ /usr/games/fortune-zh
花褪残红青杏小。燕子飞时,绿水人家绕。枝上柳绵吹又少,天涯何处无芳草!
墙里秋千墙外道。墙外行人,墙里佳人笑。笑渐不闻声渐悄,多情却被无情恼。
      -- 苏轼《蝶恋花》
bash4 行

4. 常用选项

fortune 的选项不多,个个有用:

选项 含义
-s / -l 只要短句 / 只要长句
-n N 定义“短句”的字符数阈值,默认 160
-e 所有词库等概率
-c 顺便打印词条来自哪个文件
-f 只列出会扫描的词库清单,不输出签文
-m 正则 在词库里搜索所有匹配的正则,输出到 stdout
-i 配合 -m 忽略大小写
-w 读完按字数等一会儿再退出(用于登出提示)

实测几个:

$ fortune -s          # 只要短句(阈值 160 字符)
$ fortune -l          # 只要长句
$ fortune -n 30 -s    # 阈值收到 30,短句更短
Orz
bash4 行

-e 等概率模式下,抽到的可能是任何词库,包括唐诗:

$ fortune -e
《奉和圣制从蓬莱向兴庆阁道中留春雨中春望之作应制》
作者:王维
渭水自萦秦塞曲,黄山旧绕汉宫斜。
銮舆迥出千门柳,阁道回看上苑花。
云里帝城双凤阙,雨中春树万人家。
bash6 行

-c 显示来源:

$ fortune -c
(tang300)
%
《秋日赴阙题潼关驿楼》
作者:许浑
红叶晚萧萧,长亭酒一瓢。
bash6 行

-f 列出词库清单(先记住这张表,第 8 节要回来对账):

$ fortune -f
100.00% /usr/share/games/fortunes
    92.79% chinese
     1.69% song100
     5.52% tang300
bash5 行

-m 是全库正则搜索,输出到 stdout,词条带 % 分隔符 —— 也就是说重定向出去就是一份合法的 fortune 词库文件:

$ fortune -m 明月 tang300
(/usr/share/games/fortunes/tang300)
%
《月下独酌》
作者:李白
花间一壶酒,独酌无相亲。
举杯邀明月,对影成三人。
%
《关山月》
作者:李白
明月出天山,苍茫云海间。
bash11 行

小贴士:-m 把文件名打到 stderr,签文打到 stdout。想要纯净版词库,只重定向 stdout 即可。

5. 词库长什么样(底层 · 一)

数据全部躺在 /usr/share/games/fortunes/:

$ ls -la /usr/share/games/fortunes/
.rw-r--r-- 2.1M root  6 Nov  2024 chinese
.rw-r--r--  21k root  6 Nov  2024 chinese.dat
lrwxrwxrwx    - root  6 Nov  2024 chinese.u8 -> chinese
.rw-r--r--  29k root  6 Nov  2024 song100
.rw-r--r-- 412  root  6 Nov  2024 song100.dat
lrwxrwxrwx    - root  6 Nov  2024 song100.u8 -> song100
.rw-r--r--  89k root  6 Nov  2024 tang300
.rw-r--r-- 1.3k root  6 Nov  2024 tang300.dat
lrwxrwxrwx    - root  6 Nov  2024 tang300.u8 -> tang300
bash10 行

每个词库三个文件:

  • 数据文件(chinese、tang300、song100):纯文本,就是签文本体。
  • 索引文件(.dat):strfile 生成的偏移表,让 fortune 能“随机直达”任意一条,下一节细讲。
  • .u8 符号链接:Debian 惯例,指向同名数据文件,标记“这份文件已经是 UTF-8”。对应 fortune -u 选项(“不要把 UTF-8 词条转换成 locale”),这样程序就知道不需要再对内容做编码转换。

打开数据文件看格式。每条签文用独占一行的 % 分隔:

$ xxd /usr/share/games/fortunes/chinese | head -6
00000000: e8a6 81e6 9c89 e7a4 bce8 b28c 0a0a e59c  要有礼貌\n\n在
00000010: a820 4465 6269 616e 20e8 bf99 e7a7 8de8  在 Debian 这...
00000020: a784 e6a8 a1e7 9a84 e9a1 b9e7 9bae e4b8  样的项目...
...
00000150: 6d0a 250a e596 84e6 848f e68e a8e5 ae9a  m\n%\n善义推...
bash6 行

注意 250a 就是 %\n,它是条与条之间的“封口”。另外还能看到签文里嵌着 ANSI 转义序列——颜色不是程序在输出时加的,而是直接写在数据文件文本里的:

$ xxd /usr/share/games/fortunes/chinese
00000110: e59b b4e3 8082 0a0a 1b5b 3333 6d20 2020  围。\n\n\x1b[33m
00000120: 202d 2d20 4465 6269 616e 201b 5b33 326d    -- Debian .[32m
bash3 行

\x1b[33m(黄色)打在作者名前、\x1b[32m(绿色)打在书名前、\x1b[m 复位——所以那份黄绿配色是“印”在词库文本里的,不是 fortune 程序渲染的。

三个词库的规模(用 Python 解析 .dat 头部得到,见下节):

词库 条目数 最长条目(字节)
chinese 5262 26553
tang300 313 2991
song100 96 910

6. .dat 索引:随机访问的关键(底层 · 二)

大文件有 2 万多条签文,fortune 不能每次从头读到尾——那样“随机”太慢。办法是维护一张偏移表:每条签文的起始字节位置。这就是 .dat 文件。

.dat 的二进制格式很古老:24 字节大端头 + 一串 4 字节偏移量。用 Python 精确解析:

结构一目了然:

┌─────────────────────────────────────────┐
│  24 字节头部(大端序)                    │
│  version    : uint32 = 2                 │
│  numstr     : uint32 = 5262  条目数      │
│  longest    : uint32 = 26553 最长条目    │
│  shortest   : uint32 = 4     最短条目    │
│  flags      : uint32 = 0                 │
│  delim      : 1 字节 = '%'               │
│  delimlen   : uint16 = 0                 │
│  1 字节填充                              │
├─────────────────────────────────────────┤
│  偏移数组  off[0]..off[numstr]           │
│  每条 uint32(大端),共 numstr+1 个      │
│  off[i] = 第 i 条签文的起始字节位置       │
│  off[numstr] = 文件总大小(哨兵)          │
└─────────────────────────────────────────┘
text16 行

所以 fortune 抽一条的逻辑就三步:

  1. 随机取 i ∈ [0, numstr);
  2. 打开数据文件,lseek 到 off[i],读 off[i+1] - off[i] 个字节;
  3. 去掉末尾的 %\n,打印。

看一眼最直观的证据——用一个小到能一眼看穿的词库:

$ cat /tmp/fortune_demo/myfortunes
第一条我的小纸条。
   --- 自己
%
君不见黄河之水天上来,奔流到海不复回。
   --- 李白
%
第三条,最短验证。
bash8 行

strfile 生成索引(注意它报的 3 条 / 最长 72 字节 / 最短 28 字节,跟字节流完全对得上):

$ /usr/bin/strfile /tmp/fortune_demo/myfortunes
"/tmp/fortune_demo/myfortunes.dat" created
There were 3 strings
Longest string: 72 bytes
Shortest string: 28 bytes
bash5 行

3 条签文,40 字节的 .dat(24 头 + 4×4 偏移):

$ xxd /tmp/fortune_demo/myfortunes.dat
00000000: 0000 0002 0000 0003 0000 0048 0000 001c   ver=2  n=3  longest=72 shortest=28
00000010: 0000 0000 2500 0000 0000 0000 0000 002c   flags=0 delim='%'  off[0]=0  off[1]=44
00000020: 0000 0076 0000 0092                       off[2]=118  off[3]=146(=文件总大小)
bash4 行

对照原文字节数:第一条 0–43 字节、第二条 44–117、第三条 118–145,文件总共 146 字节。偏移数组精确地把每条签文“框”住了。fortune 读起来:

$ fortune /tmp/fortune_demo/myfortunes
君不见黄河之水天上来,奔流到海不复回。
   --- 李白
bash3 行

7. fortune-zh 脚本解剖(底层 · 三)

/usr/games/fortune-zh 只有 33 行,是个 POSIX shell 脚本,核心就一句:

逐段拆解:

  • 加权词库:DICT="7% tang300 2% song100 91% chinese" —— 三个词库按 7% / 2% / 91% 的概率被抽到。这解释了为什么 fortune-zh 抽到的大多是 chinese 里那些杂学内容,唐诗宋词反而少见。
  • 编码适配:老系统 zh_CN.GB2312 / zh_TW.Big5 环境,用 iconv 把 UTF-8 数据实时转成目标编码再输出;现代 UTF-8 环境直接原样输出。
  • 去色开关:数据文件里嵌着 ANSI 颜色码,嫌花哨就设 FORTUNEZH_NOCOLOR=1,脚本用 sed 把 \x1b[NNm 全部剥掉:
$ FORTUNEZH_NOCOLOR=1 fortune-zh
世间无限丹青手,一片伤心画不成。
    -- 高蟾《金陵晚望》
bash3 行

对比带色的原始输出,能明显看到剥掉的是 \x1b[33m、\x1b[32m 这类序列。

8. 加权算法:实测而非臆测(底层 · 四)

-f 默认输出(第 4 节)显示 chinese 占 92.79%、song100 占 1.69%、tang300 占 5.52%。这三个数哪来的?是按各词库条目数算的:

chinese 5262 + song100 96 + tang300 313 = 5671 条
chinese: 5262 / 5671 = 92.79%
song100:  96 / 5671 =  1.69%
tang300: 313 / 5671 =  5.52%
text4 行

也就是说,没给百分比时,fortune 把目录里所有文件当成“平摊剩余额度”,再按条目数(不是文件字节数)分配。man page 里原话就是 “twice as big (in number of fortunes, not raw file size)”。

那给了显式百分比呢?man 里只含混地说“n% 概率”。别猜,直接实验。

实验 A:50% short 50% long,两个文件各 10 条,但最长条目差 175 倍(17 字节 vs 2977 字节)。 跑 1000 次数分布:

$ fortune 50% short 50% long | …   # 统计 1000 次
    510 长句
    490 短句
bash3 行

510/490 ≈ 五五开。结论:显式百分比下,“最长条目长度”完全不参与加权——即使一个文件条目长得离谱,50% 就是 50%。

实验 B:90% short many long,其中 short 10 条、many 30 条、long 10 条,只有 short 标了百分比。

$ fortune -f 90% short many long
90.00% /tmp/fortune_demo/w/short
 7.50% /tmp/fortune_demo/w/many
 2.50% /tmp/fortune_demo/w/long
bash4 行

剩下 10% 分给了两个没标百分比的文件,且按条目数 30:10 拆成 7.5% : 2.5%。实测 500 次分布 88 / 8.6 / 3.2,符合预期。

实验 C:fortune -f 7% tang300 2% song100 91% chinese(正是 fortune-zh 那句):

$ fortune -f 7% tang300 2% song100 91% chinese
91.00% /usr/share/games/fortunes/chinese
 2.00% /usr/share/games/fortunes/song100
 7.00% /usr/share/games/fortunes/tang300
bash4 行

显式百分比原样生效。

于是加权规则收敛成一个简单模型:

对每个词库:显式给了 N%  → 权重就是 N
            没给百分比     → 平分剩余百分比,再按条目数比例细分
取随机数 r ∈ [0, 总和),按权重顺序累加,落在哪个区间就抽哪个文件
text3 行

9. 用 30 行 Python 重写 fortune 核心

把第 6 节的逻辑落地成脚本,跑出来的效果和原生 fortune 一模一样:

跑一次:

$ python3 mini_fortune.py
>> minifortune 解析: numstr=5262 longest=26553 shortest=4 delim='%'
口封得住,人口封不住。
    -- 《谚语》
bash4 行

底层真相就藏在这 30 行里:随机 = 先随机一个下标,再用偏移表直读那一段字节。

10. 自己造一个词库

三步走,5 秒钟搞定:

① 写数据文件,条目间用独占一行的 % 分隔:

$ cat > /tmp/mine <<'EOF'
早起的鸟儿有虫吃,早起的虫子被鸟吃。
   --- 我自己
%
代码写得好,Bug 逃得早。
%
最后的签文:人生苦短,我用 Python。
EOF
bash8 行

② strfile 生成索引:

$ /usr/bin/strfile /tmp/mine
"/tmp/mine.dat" created
There were 3 strings
bash3 行

③ 直接当参数用,或放进 FORTUNE_PATH 指向的目录当默认库:

$ fortune /tmp/mine
代码写得好,Bug 逃得早。

$ mkdir -p ~/.myfortunes && cp /tmp/mine /tmp/mine.dat ~/.myfortunes/
$ FORTUNE_PATH=~/.myfortunes fortune
早起的鸟儿有虫吃,早起的虫子被鸟吃。
   --- 我自己
bash7 行

以后每次新开终端想被灌一句鸡汤,把它加进 ~/.zshrc:

echo "$(/usr/games/fortune-zh)"
bash1 行

11. 一张图看懂全链路

结语

一个 40 年前的命令行小玩具,剥开看其实是三件套:一份文本、一张偏移表、一个加权随机。数据文件决定“有什么”,.dat 决定“怎么快”,百分比语法决定“怎么偏”。理解了这三层,fortune 在你眼里就不再是黑盒——它甚至不如你自己写的 Python 复杂。

下次想在终端里“撞见”一句诗,记得先 export PATH="$PATH:/usr/games"。

原文 https://blog.csdn.net/2301_79518550/article/details/164194705