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
装完先别急着用 —— 会踩一个经典坑:二进制不在默认 PATH 里。
$ which fortune
fortune not found
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
所以要么写全路径,要么把目录加进 PATH(写入 ~/.zshrc / ~/.bashrc):
$ export PATH="$PATH:/usr/games"
顺带一提,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]
直接抽签:
$ /usr/games/fortune
佳节清明桃李笑,野田荒冢只生愁。
-- 黄庭坚《清明》
这是英文版工具的默认输出吗?不是。fortune 没带参数时扫的是默认数据目录 /usr/share/games/fortunes/,而里面装的全是中文词库,所以抽出来自然全是中文。
用官方包装脚本 fortune-zh,输出更讲究(作者一行用黄色、书名用绿色):
$ /usr/games/fortune-zh
花褪残红青杏小。燕子飞时,绿水人家绕。枝上柳绵吹又少,天涯何处无芳草!
墙里秋千墙外道。墙外行人,墙里佳人笑。笑渐不闻声渐悄,多情却被无情恼。
-- 苏轼《蝶恋花》
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
-e 等概率模式下,抽到的可能是任何词库,包括唐诗:
$ fortune -e
《奉和圣制从蓬莱向兴庆阁道中留春雨中春望之作应制》
作者:王维
渭水自萦秦塞曲,黄山旧绕汉宫斜。
銮舆迥出千门柳,阁道回看上苑花。
云里帝城双凤阙,雨中春树万人家。
-c 显示来源:
$ fortune -c
(tang300)
%
《秋日赴阙题潼关驿楼》
作者:许浑
红叶晚萧萧,长亭酒一瓢。
-f 列出词库清单(先记住这张表,第 8 节要回来对账):
$ fortune -f
100.00% /usr/share/games/fortunes
92.79% chinese
1.69% song100
5.52% tang300
-m 是全库正则搜索,输出到 stdout,词条带 % 分隔符 —— 也就是说重定向出去就是一份合法的 fortune 词库文件:
$ fortune -m 明月 tang300
(/usr/share/games/fortunes/tang300)
%
《月下独酌》
作者:李白
花间一壶酒,独酌无相亲。
举杯邀明月,对影成三人。
%
《关山月》
作者:李白
明月出天山,苍茫云海间。
小贴士:
-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
每个词库三个文件:
- 数据文件(
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善义推...
注意 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
\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 精确解析:
$ python3 -c "
import struct
d = open('/usr/share/games/fortunes/chinese.dat','rb').read()
ver, nums, longest, shortest, flags = struct.unpack_from('>IIIII', d, 0)
offs = struct.unpack_from('>%dI' % ((len(d)-24)//4), d, 24)
print('version =', ver)
print('numstr =', nums, '(条目数)')
print('longest =', longest, ' shortest =', shortest)
print('delim =', repr(chr(d[20])))
print('offset 数量 =', len(offs), '= numstr + 1')
print('前 8 个偏移 =', offs[:8])
print('最后一个偏移 =', offs[-1])
"
version = 2
numstr = 5262 (条目数)
longest = 26553 shortest = 4
delim = '%'
offset 数量 = 5263 = numstr + 1
前 8 个偏移 = (0, 356, 736, 1236, 1808, 2339, 3365, 4636)
最后一个偏移 = 2115534
结构一目了然:
┌─────────────────────────────────────────┐
│ 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] = 文件总大小(哨兵) │
└─────────────────────────────────────────┘
所以 fortune 抽一条的逻辑就三步:
- 随机取
i ∈ [0, numstr); - 打开数据文件,
lseek到off[i],读off[i+1] - off[i]个字节; - 去掉末尾的
%\n,打印。
看一眼最直观的证据——用一个小到能一眼看穿的词库:
$ cat /tmp/fortune_demo/myfortunes
第一条我的小纸条。
--- 自己
%
君不见黄河之水天上来,奔流到海不复回。
--- 李白
%
第三条,最短验证。
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
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(=文件总大小)
对照原文字节数:第一条 0–43 字节、第二条 44–117、第三条 118–145,文件总共 146 字节。偏移数组精确地把每条签文“框”住了。fortune 读起来:
$ fortune /tmp/fortune_demo/myfortunes
君不见黄河之水天上来,奔流到海不复回。
--- 李白
7. fortune-zh 脚本解剖(底层 · 三)
/usr/games/fortune-zh 只有 33 行,是个 POSIX shell 脚本,核心就一句:
$ cat /usr/games/fortune-zh
#!/bin/sh
# fortune-zh
set -e
FORTUNE="/usr/games/fortune"
[ -x $FORTUNE ] || ( echo "E: Please install package 'fortune-mod'."; false )
# The old version (1.*) of fortune-zh contains only tang300 and song100.
# Note, $\sum_{i} P_i = 1$, i.e. all the possibilities must sum to 1.
DICT="7% tang300 2% song100 91% chinese"
# check LANG
if [ ! -z $LC_ALL ]; then
LANG="$LC_ALL"
fi
# output according to LANG
case "$LANG" in
"zh_CN.GB2312")
LANG=zh_CN.UTF8 $FORTUNE $DICT | iconv -c -f utf8 -t gbk
;;
"zh_TW.Big5")
LANG=zh_TW.UTF8 $FORTUNE $DICT | iconv -c -f utf8 -t big5
;;
*)
if [ ! -z "$FORTUNEZH_NOCOLOR" ]; then
$FORTUNE $DICT | sed -r "s/\x1b\[([0-9]{1,2}(;[0-9]{1,2})?)?m//g"
else
$FORTUNE $DICT
fi
;;
esac
逐段拆解:
- 加权词库:
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
世间无限丹青手,一片伤心画不成。
-- 高蟾《金陵晚望》
对比带色的原始输出,能明显看到剥掉的是 \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%
也就是说,没给百分比时,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 短句
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
剩下 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
显式百分比原样生效。
于是加权规则收敛成一个简单模型:
对每个词库:显式给了 N% → 权重就是 N
没给百分比 → 平分剩余百分比,再按条目数比例细分
取随机数 r ∈ [0, 总和),按权重顺序累加,落在哪个区间就抽哪个文件
9. 用 30 行 Python 重写 fortune 核心
把第 6 节的逻辑落地成脚本,跑出来的效果和原生 fortune 一模一样:
#!/usr/bin/env python3
"""minifortune: 用 30 行重现 fortune 的底层取词逻辑"""
import os, random, struct
def pick(datafile):
datfile = datafile + ".dat"
with open(datfile, "rb") as f:
d = f.read()
# 24 字节大端头: version, numstr, longest, shortest, flags, delim, delimlen
ver, numstr, longest, shortest, flags = struct.unpack_from(">IIIII", d, 0)
delim = chr(d[20])
offsets = struct.unpack_from(">%dI" % (numstr + 1), d, 24)
# 随机选一条,读 [off[i], off[i+1]),去掉末尾的分隔符 "%\n"
i = random.randrange(numstr)
with open(datafile, "rb") as f:
f.seek(offsets[i])
entry = f.read(offsets[i + 1] - offsets[i])
entry = entry.rstrip(b"%\n")
return entry, numstr, longest, shortest, delim
entry, numstr, longest, shortest, delim = pick("/usr/share/games/fortunes/chinese")
print(">> minifortune 解析: numstr=%d longest=%d shortest=%d delim=%r"
% (numstr, longest, shortest, delim))
print(entry.decode("utf-8"))
跑一次:
$ python3 mini_fortune.py
>> minifortune 解析: numstr=5262 longest=26553 shortest=4 delim='%'
口封得住,人口封不住。
-- 《谚语》
底层真相就藏在这 30 行里:随机 = 先随机一个下标,再用偏移表直读那一段字节。
10. 自己造一个词库
三步走,5 秒钟搞定:
① 写数据文件,条目间用独占一行的 % 分隔:
$ cat > /tmp/mine <<'EOF'
早起的鸟儿有虫吃,早起的虫子被鸟吃。
--- 我自己
%
代码写得好,Bug 逃得早。
%
最后的签文:人生苦短,我用 Python。
EOF
② strfile 生成索引:
$ /usr/bin/strfile /tmp/mine
"/tmp/mine.dat" created
There were 3 strings
③ 直接当参数用,或放进 FORTUNE_PATH 指向的目录当默认库:
$ fortune /tmp/mine
代码写得好,Bug 逃得早。
$ mkdir -p ~/.myfortunes && cp /tmp/mine /tmp/mine.dat ~/.myfortunes/
$ FORTUNE_PATH=~/.myfortunes fortune
早起的鸟儿有虫吃,早起的虫子被鸟吃。
--- 我自己
以后每次新开终端想被灌一句鸡汤,把它加进 ~/.zshrc:
echo "$(/usr/games/fortune-zh)"
11. 一张图看懂全链路
你敲: fortune-zh
│
▼
fortune-zh 脚本 (shell) ←── 3 层外壳
├─ 拼参数: 7% tang300 2% song100 91% chinese
├─ 按 LANG 决定是否 iconv 转码
└─ 按 FORTUNEZH_NOCOLOR 决定是否剥色
│
▼
fortune 程序 (C) ←── 2 层索引
├─ 读各 .dat 24 字节头 → 拿到条目数 numstr
├─ 加权随机选一个文件、一个下标 i
└─ lseek 到 off[i],读 off[i+1]-off[i] 字节
│
▼
数据文件 chinese / tang300 / song100 ←── 1 层文本
└─ 一段以 "%\n" 结尾的 UTF-8 文本(内含 ANSI 配色)
│
▼
stdout: 一句古诗/名言(带色)
结语
一个 40 年前的命令行小玩具,剥开看其实是三件套:一份文本、一张偏移表、一个加权随机。数据文件决定“有什么”,.dat 决定“怎么快”,百分比语法决定“怎么偏”。理解了这三层,fortune 在你眼里就不再是黑盒——它甚至不如你自己写的 Python 复杂。
下次想在终端里“撞见”一句诗,记得先 export PATH="$PATH:/usr/games"。
原文 https://blog.csdn.net/2301_79518550/article/details/164194705