Bash 中的 Base64 解码:完整指南
每隔一阵子,就会有一串字母、数字,外加偶尔的 +、/ 或 = 落到你的终端里,而你需要把真正的内容找回来。粘贴到工单里的一个 JWT、附在支持邮件里的一个 .b64 文件、一个读起来像字母汤的 Kubernetes secret、一个藏在 HTML data: 标签里的 PNG。这是一份实战指南,教你在 Bash 和 shell 里把字节原样取回,用的都是你几乎肯定已经装好的工具。
一口气说清这个格式:Base64 把原始数据每三个字节重写成四个字符,字符取自一个 64 个字母的字母表(A-Z、a-z、0-9,再加 + 和 /),当字节数不是三的倍数时,末尾会加上一两个 = 号。解码就是这笔交易里缩水的那个方向:四个字符进去,三个字节出来。本站首页已经把格式一步步讲过了,所以这篇文章把时间花在该花的地方:活儿在 shell 这一侧。
下面是剧情反转:并不存在一个唯一的 base64 命令。这个名字被一个 GNU C 程序、一个 Rust 重写版、一个 BusyBox applet、一个 BSD 遗留物和一个 OpenSSL 小工具共用,而后者与它们的标志重叠得相当危险。大家对字母表没有异议,但对"坏输入长什么样"却未必一致,而脚本们倒下的地方正是这种差异。所以第一步是弄清楚:当你敲下 base64 时,到底是谁在应答。
先认清你对话的解码器
一条命令就能讲出大部分故事:
base64 --version
取决于机器,你打交道的会是下面其中之一:
| 你看到什么 | 你手上是什么 | 解码标志 |
|---|---|---|
base64 (GNU coreutils) 9.x |
经典的 C 实现,仍是大多数 Linux 发行版的默认 | -d 或 --decode |
base64 (uutils coreutils) 0.8.x |
coreutils 的 Rust 重写版,当前 Ubuntu 发行版的默认 userland | -d(而且一反常态,-D 也管用) |
| BSD 风格的用法文本,没有版本标志 | macOS 和各 BSD 上的 BSD base64,脱胎于古老的 bintrans 工具 |
-D(在这个家族里,小写 -d 表示调试,不是解码) |
BusyBox v1.x |
Alpine Linux 和嵌入式系统的一体化二进制 | -d |
注意那张表里缺了谁:OpenSSL。openssl base64 完全是另一种生物,它的 -d 标志表示解密,而不是解码。就这一个标志,比本文其他任何习惯制造出的"静悄悄的空输出文件"都多,所以我们会在后备方案那一节正式和它见面。
如果你的发行版并排装着不止一个家族(当前的 Ubuntu 就是),再多两条命令就能看清全貌:
command -v base64
base64 --version 2>&1 | head -1
四条单行命令,应付大多数日子
从标准输入解码一个字符串。这是你会做上千次的那一招,而 printf 保证了 shell 不会给你的负载添油加醋:
printf '%s' "SGVsbG8sIFdvcmxkIQ==" | base64 -d
解码一个文件。每个正经的实现都接受 FILE 参数,这也是把数据从 shell 的引号机器里隔离出来的最干净方式:
base64 -d payload.b64 > payload.bin
从 here-string 解码。here-string 会追加一个行尾换行,但所有解码器都把换行当作可忽略的空白,所以对小数据块来说这完全安全:
base64 -d <<< "SGVsbG8sIFdvcmxkIQ=="
用 heredoc 解码一个折行、多行的大块。给分隔符加上单引号,就能阻止 shell 解释里面的任何东西:
base64 -d <<'EOF'
SGVs
bG8s
IFdv
cmxk
IQ==
EOF
四条全都输出 Hello, World!。在 macOS 和各 BSD 上,把上面每个例子里的 -d 换成 -D 即可;其余语法完全一样。
脏乱的输入才是常态
你在野外碰到的 Base64 很少是一行干净的文本。它往往按 76 字符(MIME 约定)或 64 字符(PEM 约定)折行而来,带着 CRLF 行尾从 Windows 导出,或者从聊天窗口里复制出来、中间还夹着零散的空格。好消息是:只要换行是真正的换行,解码器根本不关心换行出现在哪里。
对付任何折行风格的通用解药,是解码之前先把换行洗干净:
tr -d '\r\n' < blob.b64 | base64 -d
回车符是特殊病例。换行是允许的输入,但 \r 对严格解码器来说不是换行。一个经过 Windows 系统的数据块会让 GNU 解码器栽跟头:它先吐出一个部分结果,然后失败;解决办法是先把回车符剥掉:
printf 'SGVs\r\nbG8s\r\n' | tr -d '\r' | base64 -d
如果你看到像 Hel 这样的残片、后面跟着一个错误,你就踩在一个 CRLF 数据块上了。解码之前做同样的清洗 tr -d '\r\n',是处理任何非你亲手产生的输入时的可移植习惯。
对于真正损坏的输入,GNU 和 uutils 提供 -i(--ignore-garbage)标志:跳过非字母表字符,能解多少解多少:
printf 'SGVs!bG8' | base64 -di
它输出 Hello。在把 -i 变成默认习惯之前,先知道标准为什么警告不要这么做:RFC 4648 第 3.3 节说,实现必须拒绝包含字母表外字符的数据,因为被忽略的字符可以被利用成一条隐蔽信道,夹带那些永远不会出现在解码输出里的数据。当从文档粘贴的内容捎带了标点符号时才伸手去拿 -i,而不是在你校验自己信任的数据时。
下面是三大解码器在 2026 年的工具(uutils 0.8.x、GNU coreutils 9.7、BusyBox 1.37)上面对边缘情况时的真实表现:
| 输入 | uutils 0.8.x | GNU 9.7 | BusyBox 1.37 |
|---|---|---|---|
SGVsbG8sIFdvcmxkIQ==,干净的 |
Hello, World!,退出码 0 |
Hello, World!,退出码 0 |
Hello, World!,退出码 0 |
SGVsbG8s,八个字符,无填充 |
Hello,,退出码 0 |
Hello,,退出码 0 |
Hello,,退出码 0 |
Zg,两个字符,无填充 |
f,退出码 0 |
f,退出码 0 |
报错,"truncated input" |
SGV,三个字符,无填充 |
报错,无输出 | 先输出 He,然后报错 |
报错,"truncated input" |
| 用 CRLF 折行的行 | 解码正常,退出码 0 | 部分输出,然后报错 | 解码正常,退出码 0 |
SGVs!bG8,零散的标点 |
报错(加 -i:Hello) |
部分输出(加 -i:Hello) |
部分输出(根本没有 -i 标志) |
SGV=,非规范的剩余位 |
报错,无输出 | 先输出 He,然后报错 |
He,退出码 0 |
TQ==junk,填充后面的垃圾 |
退出码 0,继续解码 junk |
退出码 0,继续解码 junk |
先输出 M,然后 "truncated input"(退出码 1) |
那张表能带走三个结论。第一,无填充的尾巴没有通用规则:BusyBox 要求长度是四的倍数,而 GNU 和 uutils 接受合法的余数,但前提是剩余的位全为零(这就是 Zg 能通过、SGV 不能的原因)。第二,GNU 和 BusyBox 在失败之前会先写出已经解出来的字节,所以一个把结果重定向到文件、之后再查退出码的脚本,会开开心心地留下一半解码的文件。永远检查退出状态,并把任何一次失败解码留下的文件都当作可疑。第三,看最后一行:uutils 和 GNU 在 == 之后继续解码 junk 并且以退出码 0 收场,因为没有任何东西告诉它们流本该结束;BusyBox 是例外,它输出填充前的那一个字节,然后以 truncated input 错误失败。如果你在意尾部的垃圾,就在信任输出之前先校验输入的形态。
base64url:令牌和 URL 的字母表
RFC 4648 第 5 节定义了第二种方言:同样的 6 位数学,只是用 - 和 _ 换掉了 + 和 /,并且去掉了填充,因为 URL 很少需要宣布精确的字节长度。RFC 对此说得毫不含糊:这种编码"不应被视为与 base64 编码相同"。只要你看过 JWT,就已经见过这种方言了,因为它的各段就是去掉填充的 base64url。
shell 里的配方是两步交换:先把 URL 安全字符翻译回它们的标准表亲,再解码:
printf '%s' "_k-C" | tr '_-' '/+' | base64 -d | xxd -p
它返回 fe4f82,三个碰巧穿了 URL 外衣的原始字节。交换是按位置进行的,所以方向很重要:编码走 tr '+/' '-_',解码走 tr '_-' '/+'。搞混了不会报错,只会安安静静地产出不同的字节,而这正是最糟糕的一类 bug。
下面是陷阱,专抓那些把 base64url 当普通 Base64 用的人。填充在这种方言里是可选的,而长度模四余三的段,恰恰是严格解码器会仔细审视的形状。稳妥的做法是先补回缺失的 = 字符,这也就是一个小函数:
b64url_decode () {
local s=$1
local n
n=$(printf '%s' "$s" | wc -c | tr -d '[:space:]')
while [ $(( n % 4 )) -ne 0 ]; do
s="${s}="
n=$(( n + 1 ))
done
printf '%s' "$s" | tr '_-' '/+' | base64 -d
}
b64url_decode "eyJzdWIiOiJob21lciJ9"
最后一行输出 {"sub":"homer"},一个没有半点修饰的 JSON 对象,几分钟前刚被某个 web 服务器签名或密封。长度模四余一的段本身就是畸形的,补多少填充也救不回来,所以这个函数拒绝那种情况反而是一个特性。
GNU coreutils 里还有一条原生路径:basenc,base64 的更大号的兄弟,直接理解这种方言:
printf '%s' "Zg==" | basenc --base64url -d
它输出 f,藏在两个字符里的一个字节。在拿它搭流水线之前先听一句警告:这个年代的 GNU basenc 甚至能不声不响地解码无填充的 base64url 输入(光秃秃的 Zg),而 uutils 的 basenc 仍然要求先补回填充,就像上面的例子展示的那样。上面那个小函数在哪里都管用,所以它才是可移植的选择。
拆开一个 JWT
按照 RFC 7515,一个 JSON Web Token 就是三个用点号连接的 base64url 段。前两个是纯 JSON(头部和载荷声明),所以能直接解码成可读文本。第三个是签名,一个原始的二进制摘要,所以别去碰它:解码它给你的是签名的字节,不是一条消息。
token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.abc123"
b64url_decode "$(printf '%s' "$token" | cut -d. -f2)"
它输出 {"sub":"42"},即 subject 声明,全程不需要任何服务器。读声明时,对解码是什么、不是什么保持清醒:它揭示,但不验证。签名本身什么都说明不了,直到持有密钥的服务器重新计算它,而那是 openssl dgst 的活儿(编码那篇文章展示了完整的铸造流程),不是这里的活儿。一个常见的现场错误是把解码出来的载荷当作令牌有效的证明;攻击者可以手工铸造未签名或弱签名的令牌,而解码器会毫不介意地把它们全都读出来。
文件、字节与变量这堵墙
解码交到你手上的是原始字节。它们可能拼出一句英语,也可能是某个 PNG 的中间部分、一个共享库,或者一个 zip 归档。在 shell 脚本里,文件是字节唯一绝对安全的地方,所以默认的工作流是解码到文件,然后再比较:
base64 -d photo.b64 > photo.png
当原始文件就在你手边时,逐字节比较才是唯一有意义的证明:
cmp photo.png photo.png.orig && echo "byte-for-byte identical"
当原始文件远在别处时,就改比校验和:
sha256sum expected.bin
base64 -d blob.b64 | sha256sum
两个哈希一致,你的解码就可证明地精确,这胜过在图片查看器或十六进制转储里目测一万遍。
shell 变量则是另一个故事,它是一堵墙,原因有二。命令替换 $(...) 会剥掉输出末尾的每一个换行,而且它完全装不下 NUL 字节:bash 会打印一条警告,然后悄悄把它们丢掉。一个由 41 00 42 三个字节组成的载荷,在一个演示里就能同时暴露这两个问题:
printf 'QQBC' | base64 -d > out.bin
xxd out.bin
文件里装着全部三个字节(41 00 42)。走变量这条路就不行了:
v=$(printf 'QQBC' | base64 -d)
printf '%s' "$v" | wc -c
你得到 2,外加标准错误上一条关于被忽略的空字节的警告。教训很短:如果数据可能是二进制,就解码到文件,用 xxd 检查,永远别让它经过变量。
Base64 在真实工作里藏身的角落
一旦解码变得顺手,你就会开始到处注意到这个格式。下面是真实 shell 工作中它现身的角落,每个都配上确切的招式。
Kubernetes secret。secret 里 .data 下的每个字段都是 Base64,官方文档特别强调这是编码,不是加密。把它读回来是一个经典单行命令:
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d; echo
Git 二进制补丁。当 diff 触及二进制文件时,git diff --binary 会输出一个 GIT binary patch 块。别在这里伸手去拿 base64 -d:那个块里的行是 git 自己的 base85 风格编码(每行以一个长度字符开头,A-Z 或 a-z,后面跟着 base85 数据),不是 Base64,普通解码器会卡在上面。正确的工具是这个格式的业主:
git diff --binary | grep -a -A2 'GIT binary patch'
把 diff 喂给 git apply 或 git am,让它们自己去解包。
Data URI。嵌在 HTML 或 CSS 里的图片长这样:data:image/png;base64,iVBOR...。把逗号(含逗号)之前的全部剥掉,洗净换行,解码,文件就在你手里了:
cut -d, -f2- icon.uri | tr -d '\r\n' | base64 -d > icon.png
PEM 护甲。证书和私钥把它们的 Base64 包在帧行里,而帧行根本不是 Base64。选中护甲块,去掉两行帧,解码成原始的 DER 二进制:
awk '/BEGIN CERTIFICATE/{f=1;next} /END CERTIFICATE/{f=0} f' cert.pem | tr -d '\r\n' | base64 -d > cert.der
把 CERTIFICATE 换成 PRIVATE KEY,或者你的文件上写着什么标签就换什么;形状是一样的。
MIME 邮件。任何带 Content-Transfer-Encoding: base64 的邮件部分,都按 76 字符折行并带 CRLF 行尾,因为 RFC 2045 就是这么规定的。可移植的组合拳是清洗加解码:
tr -d '\r\n' < attachment.b64 | base64 -d > attachment.bin
剪贴板粘贴。从浏览器、聊天窗口或文档复制来的文本,会带来零散的空格和一个行尾换行。空格不是字母表字符,所以严格解码器会拒绝这个粘贴;标准的救援是先把它扔掉:
tr -d ' \r\n' < pasted.b64 | base64 -d
字节优先:字符集与 Unicode
Base64 工作里重复最多的错误,是用字符思考,而这个格式只认字节。base64 -d 交给你的是原始字节,它们能不能变成可读文本,是接下来读它们的那个东西做的决定。那个决定就是字符集,发生在解码之后,绝不在解码之内。
UTF-8 是默认假设,通常也是对的。单词 café 用 UTF-8 表示是五个字节,解码过程赏心悦目:
printf 'Y2Fmw6k=' | base64 -d | xxd -p
那是 636166c3a9:caf 加上 é 的两个 UTF-8 字节 c3 a9。不过老系统会交给你 Latin-1(ISO-8859-1)字节,同样的字母在那里是单个字节 e9。把这样一个数据块解码后直接打印到 UTF-8 终端,你会得到一个烂掉的字符;解决办法是在任何别的东西看到这些字节之前,先用 iconv 重新解释它们:
base64 -d latin1.b64 | iconv -f ISO-8859-1 -t UTF-8 > utf8.txt
几条字节层面的事实,能省下实打实的调试时间:
- UTF-8 BOM 是三个字节
ef bb bf,编码后是77u/。注意那个/,一个 URL 天敌字符,而 base64url 存在的原因恰恰就是对付这种东西。如果一个解码出来的文件"前面有一团看不见的垃圾",用xxd检查前三个字节。 - 一个 emoji 比如 😀 是四个 UTF-8 字节,变成八个 Base64 字符
8J+YgA==。里面的+是标准字母表按设计正常工作;穿上 base64url 外衣后它变成8J-YgA。 - 无效的 UTF-8 序列作为字节能完美解码,然后显示成乱码或替换字符。这不是 Base64 的失败,解码已经尽职了。
xxd或hexdump -C能告诉你这些字节究竟是什么。 - 你终端的 locale 决定它如何渲染这些字节。"终端显示的是乱码"是一句关于显示的话,不是关于数据的话。这些字节在路上没有变过。
大块头:流、切分与速度
Base64 是流格式,解码器是一根真正的流式管道:一个 10 GB 的文件永远不会整个坐在内存里,它只是流过去。这让解码一侧在大规模时几乎无聊起来,而这正是你想要的。
当一个大数据块为了传输被切成了若干块(邮件限额、工单附件大小、IM 消息),重新拼装就是按正确顺序来一个 cat,再接上惯常的清洗:
cat part_* | tr -d '\r\n' | base64 -d > big.bin
对大小保持一个心智模型:编码形态永远比原始大约三分之一,每三个字节四个字符。所以当你听谁说 .b64 文件应该和它藏的东西一样大时,他错了,而且你现在能告诉他错多少:一个 300 MB 的载荷到达时大约是 400 MB 的文本。
速度在实践中不是问题。这些是对普通内存做的查表循环,在现在的机器上,GNU 解码一个 200 MB 的数据块大约只要零点一秒,而常见实现里最慢的(BusyBox)也只比它慢几倍,仍然远小于两秒。不管输入多大,内存占用都保持平稳,因为什么都不做缓冲。
当机器上没有 base64
大多数系统至少装有上面的工具之一,多数还有好几个。如果你的平台完全没有 coreutils(精简过的容器、不寻常的专用设备),安装路径长这样:
| 平台 | 怎么拿 | 备注 |
|---|---|---|
| Debian / Ubuntu | 预装;被精简过就 apt install coreutils |
当前 Ubuntu 发行版默认是 uutils 家族(自 25.10 起),GNU 版本仍可以 gnubase64 之名找到;Debian 13 默认仍装 GNU coreutils |
| RHEL / Fedora | dnf install coreutils |
实际上每个镜像都预装 |
| Alpine | apk add busybox(通常已经在了) |
busybox base64 -d;没有 -i 标志 |
| macOS | 内置;GNU 版本用 brew install coreutils |
BSD 标志:解码用 -D,行宽用 -b;brew 装出来的是 gbase64 |
而如果包管理器这条路完全走不通,下面的通用后备方案全都从标准输入读、把字节写到标准输出,所以能塞进同样的流水线:
openssl base64 -d -A -a < in.b64 > out.bin
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out.bin
python3 -c 'import base64, sys; sys.stdout.buffer.write(base64.b64decode(sys.stdin.buffer.read()))' < in.b64 > out.bin
在一个把 base64 applet 编译掉的 BusyBox 系统上,老门卫仍然管用:uuencode -m 产出 MIME Base64,而它的兄弟读得回来:
busybox uudecode -o out.bin in.uu
吃掉整个下午的坑
下面列出的全是你会在现场碰到的行为,集中放在一处:
| 坑 | 会发生什么 | 修复 |
|---|---|---|
单独使用 openssl base64 -d |
什么都不做,悄悄退出码 0,因为那里的 -d 意思是 Decrypt(解密) |
openssl base64 -d -A -a,并把零字节结果当作错误 |
在 macOS 上用 -d 解码 |
BSD base64 拒绝这个标志(或者把它当成调试) | -D,或者安装 coreutils 拿到 gbase64 |
| 输入里有 CRLF 行尾 | GNU 先输出部分结果然后失败;uutils 和 BusyBox 接受 | 先 tr -d '\r\n',对外来的输入永远如此 |
| 无填充的尾巴 | BusyBox 拒绝任何余数;GNU 和 uutils 只接受规范的尾巴 | 解码前补回缺失的 = 填充 |
填充后面的垃圾,例如 TQ==junk |
uutils 和 GNU 继续解码并以退出码 0 收场;BusyBox 报错 | 信任输出之前先校验输入的形态 |
非规范的剩余位,例如 SGV= |
uutils 和 GNU 拒绝(GNU 在部分输出之后);BusyBox 照样解码 | 输入损坏了;在上游重新生成编码 |
| 把解码输出读进变量 | $(...) 剥掉行尾换行,而且完全装不下 NUL 字节 |
解码到文件,用 xxd 检查 |
对不受信任的输入用 -i |
损坏变成静悄悄的成功;被忽略的字符可以携带隐藏数据 | 严格解码,读错误信息,修源头 |
| 搞混两套字母表 | 把 base64url 当标准解码(或反过来)会得到错误的字节或一个错误 | 解码前先弄清格式;交换是 tr '_-' '/+' |
| 把解码出来的秘密当成秘密 | 一条命令就能还原;RFC 记录过真实发生的凭据泄露事件 | 真正的加密,而不是编码 |
OpenSSL 那一行值得单独一段,因为它的失败太安静。在 OpenSSL 3.x 上,独立程序的 -d 是它通用的"解密"选项,而 Base64 处理是另一个用 -a 选择的模式。所以 openssl base64 -d 会读你的输入、什么都不做、什么都不打印,然后退出码 0。能用的解码是 openssl base64 -d -A -a,其中 -A 告诉它输入是一整条连续的行。如果你必须用 OpenSSL 来解码,那么每次、每次都把零字节输出当作错误。
一套紧凑的例行程序
这些习惯保证 Base64 永远占不到你的便宜:
- 大声失败。用
set -euo pipefail运行脚本,并检查退出码。所有常见解码器遇到坏输入都退出码 1;OpenSSL 是那个从吵闹变安静了的角色,所以对它,还要检查输出非空。 - 证明往返。在期望字节和还原字节之间用
cmp或sha256sum,这是唯一有意义的证明。永远不要目测二进制。 - 二进制进文件,绝不进变量。行尾换行和 NUL 字节都是命令替换的牺牲品。
- 洗掉外来输入。解码任何跨越过平台边界的东西之前,先
tr -d ' \r\n'。 - 默认保持严格。在弄清到底哪里错了之前,别开
-i;严格的失败会告诉你位置,宽容的失败什么也不告诉你。 - 点名字母表。标准 Base64 和 base64url 按 RFC 4648 是两种不同的编码。JWT 按 base64url 解码,邮件附件按标准解码,永远别在不明白为什么的情况下交换字符。
- 永远别打印你刚解码出来的东西。这个格式在秘密世界的全部意义就是不可见;日志的全部意义就是可见。这两个目标不兼容。
一个可移植的小垫片,回答"这台机器要哪个标志"的问题:
case "$(base64 --version 2>&1 | head -2)" in
*uutils*|*GNU*|*BusyBox*) b64d () { base64 -d; } ;;
*) b64d () { base64 -D; } ;;
esac
b64d < in.b64 > out.bin
case 语句按版本横幅抓住 GNU、uutils 和 BusyBox 三个家族 - 三者都收小写标志 - 对任何别的东西回落到 BSD 标志,而这恰好就是真实世界的四分法:GNU、uutils、BusyBox、BSD。
shell 是怎样学会解码的
格式很老;你敲的那个命令并不老。shell 一侧故事的简短时间线:
- 1980 年,伯克利。Mary Ann Horton 在加州大学伯克利分校写下
uuencode和uudecode,让二进制文件(通常是压缩过的)能穿过邮件。这个名字的意思是"Unix 到 Unix 编码",一种在可能不共享字符集的 Unix 系统之间搬运文件的安全编码。几十年里,shell 用户伸手拿的是这个,而不是 Base64。 - 拨号上网时代。最早的 base 编码骑在同一个问题上:UNIX 上的 uuencode,TRS-80 和 Apple II 上的 BinHex(Macintosh 落后一小步),每一种都只信任自己终端能打印的字符。
- 1993 年。MIME 为邮件标准化了 Base64(RFC 1521,后来的 RFC 2045),连同 76 字符的折行 - 它至今仍是
base64默认行为的规定。 - 2003 年和 2006 年 10 月。RFC 3548 把这族编码理顺,随后 RFC 4648 取代它,带来了这套字母表和本文反复引用的严格解码规则,base64url 也在其中。
- 2006 年 8 月 15 日。coreutils 6.0 加入了
base64命令本身,其 NEWS 文件直白地记着"base64 encoding and decoding (RFC 3548) functionality"。在那之前,Linux 上的 shell 用户伸手拿openssl base64、uuencode -m、Perl 或 Python - 这就是为什么这么多老脚本默认 OpenSSL 是镇上唯一的选项。 - OS X 10.7。macOS 自带自己的
base64,BSD 风味,带-D标志,默认不折行 --d与-D的分野正是从这里来的。 - 2024 年 3 月。coreutils 9.5 放松了解码器:解码不再强制要求填充,而带非零剩余位的编码会被诊断成损坏,而不是被悄悄接受。
- 2025 年。coreutils 的 Rust 重写(uutils)成为当前 Ubuntu 发行版的默认。同样的命令名、同样的标志,新引擎,还带着自己的边缘看法,比如把
-D当别名接受。
值得留存的冷知识
- 命令比格式年轻。Base64 从 1993 年起就在邮件里,但
base64命令 2006 年才出现。十三年里,shell 脚本用别的工具干着这份活,你到处都能找到它们留下的指纹。 - 帮助文本里的一具化石。uutils 的
base64在帮助里仍把字母表描述为"RFC 3548",那是 RFC 4648 已经退休的前任,而它的 GNU 兄弟早已引用现行标准。一具小小的化石,只有你读帮助才看得见。 - 工具箱里最贵的静默空操作。
openssl base64 -d什么都不做,退出码 0。整整一个调试小时,没了,而且退出码检查还通过了。 - BusyBox 保留剩余位。它乐呵呵地解码
SGV=- 剩余位非零,因此非规范 - 而它的 GNU 表亲遇到同样的输入会大发雷霆。同一个 RFC,不同的神经。 - 格式的名字在每台机器上都成立。
printf 'base64' | base64在 GNU、uutils、BusyBox 和 OpenSSL 上都给出YmFzZTY0。从 2006 年起一直如此,也将永远如此。 - Base85 不是 Base64。Git 的"binary patch"块在外行眼里像 Base64,但那些带长度前缀的行是一种 base85 风格的方言。那个让人绕道"grep 再解码"二十分钟的冒名顶替者。
- 十一个字符,六十四比特。一个 YouTube 视频 ID 是 11 字符的 base64url 字符串,一个穿了 URL 外衣的 64 位数字,这就是它能在 URL 里出现而不带一个百分号的原因。
- 解码器在一个字符上分成三派。一个像
SGV=的简单字符串就能把领域分成三个阵营,上面的行为表展示了它们。如果一次解码在"显然有效"的输入上失败了,你多半正站在剩余位这条界线上,而那个编码从一开始就非规范。
所以,下次当一串字母、数字、加号和斜杠落到你的终端里,你就知道了整个故事:哪个解码器在看着,哪套字母表在说话,换行藏在哪里,以及如何把字节原样取回、逐字节验证,而不至于在一个回车符上浪费一小时。而当活儿反过来 - 要把你自己的二进制装进一个文本信封上路时,下面链接的那篇 Base64 编码相关文章会用同样的深度讲那个仪式。
最后更新: 2026-09-08
相关文章: Bash 中的 Base64 编码:完整指南