Skip to content
Published on

文档里的代码截图从什么时候开始撒谎 —— 把图片做成构建产物

分享
Authors

半年前的文档里那张截图在做什么

一份指南文档中间嵌着一张漂亮的代码截图。深色背景、圆角,左上角是红黄绿三个点。那是半年前有人在自己的编辑器里截下来贴上去的。

这期间函数名改了。少了一个选项。文档正文更新了。图片没有。现在这一页在上面的段落里说着新名字,在下面的图片里展示着旧名字。而第一次来的人通常会相信图片,因为图看起来比文字更具体。

问题不在于图片是错的,而在于没有人能发现它错了。如果是代码块,搜索时就能被搜到;图片搜不到。

截图是产物,却没有源码

把这个问题换个说法就是:躺在文档仓库里的那个 PNG 文件是一件产物。可仓库里没有把它重新做出来的办法。用的是哪个编辑器、哪套主题、多大字号、截的是哪一块,全都没有记录。

换到别的领域,我们会立刻把这种情况当成问题。把构建结果不带源码地提交进仓库,评审时一定会被指出来。可图片却莫名其妙地成了例外,因为我们觉得图片是文档的装饰,不是代码。

把标准统一起来,该做的事就清楚了。一行命令重建不出来的东西,就不要放进仓库。 图片也不例外。

在此之前还有一个更靠前的问题要问:这段代码非得是图片不可吗?文档里的代码大多数时候用代码块更好。它可以复制、能被搜到、屏幕阅读器能读、还会按屏幕宽度折行。图片站得住脚的场合很窄:当高亮颜色或编辑器标记这类无法转成文本的信息必须一同出现时;当要放进幻灯片或社交卡片这类装不下文本的媒介时;以及当终端的颜色和对齐本身就是说明的一部分时。如果这三种都不属于却在用图片,那么比起解决重建问题,先退回代码块更要紧。

goshot 做的事

goshot 是一个把代码和终端输出变成图片的 Go 库兼 CLI。README 把自己介绍成与 Carbon 或 Silicon 类似的工具。区别在于它跑在命令行上而不是网页里,而对本文的论点来说,这个区别就是全部。

只写我在功能清单里确认到的部分:语法高亮使用 chroma,支持数百种主题;带 ANSI 颜色的终端输出会被原样渲染;窗口装饰可以在 macOS、Windows 11、GNOME、KDE Breeze 的样子之间挑选。背景支持纯色、七种渐变和图片,输出支持 PNG、JPEG、BMP,也能送到剪贴板和标准输出。而且清单里还有一项:自动遮蔽 API 密钥、令牌和密码。

基本用法是这样的。

# 把一个文件变成图片
goshot main.go -o main.png

# 接受标准输入,送到剪贴板
cat main.go | goshot -c

# 指定主题、窗口样式和背景
goshot main.go -t catppuccin-mocha -C gnome -b '#1e1e2e'

# 只高亮特定行
goshot main.go --highlight-lines 10..14

安装方面,有 Go 的话就是 go install github.com/watzon/goshot/cmd/goshot@latest,另外还提供了 Arch 的 AUR 和 Ubuntu 的 PPA 包。

作为库使用时的结构也值得一看。README 把它描述成一条小小的流水线:把内容画成图片,用窗口装饰把它包起来,再放到背景之上。三个阶段各自是独立的概念,因此只改其中一个很容易。如果你要自己做文档工具,光是这种拆分本身就是值得借鉴的设计。当「换主题」「换窗口样式」「改留白」这三件事互不干扰时,日后要统一改样式,需要动的地方就集中到了一处。

变成命令之后改变的东西

并不是因为 goshot 本身有多特别。一旦图片生成变成了命令,就会带来三件事。

第一,更新文档和更新图片变成了同一件工作。 改完代码、改完文档,再跑一遍图片生成命令就行。人去打开截图工具、调窗口大小、裁剪的那个过程消失了。

第二,差异变得看得见。 图片如果是从源码确定性地生成的,那么代码没变时图片也不会变。于是「变更里出现了图片文件」这件事本身就成了一个信号。

第三,一致性变成了规则。 主题、字体、留白都写在命令的参数里,就不会再出现每个人用自己那套编辑器主题截出来的图混在一起的情况。

终端输出也有同样的毛病

文档里更容易过期的其实不是代码,而是终端输出。因为命令的输出格式会随着工具版本升级而悄悄改变。goshot 提供了一个子命令,可以执行命令并把它的输出直接做成图片。

# 执行命令并把输出做成图片
goshot exec -A -p -- go test ./...

# 让标题栏颜色自然地贴合内容
goshot exec -A -p --title-bar-color auto -- ls -la

这里重要的不是图片好看,而是那段输出是构建文档时真正跑出来的结果。手工誊抄的输出,会在誊抄者删行或润色的过程中与原样产生偏差,而这种偏差之后无从验证。

遮蔽敏感信息不是附赠功能

在整份功能清单里,我认为实务上最值钱的是自动遮蔽。通过截图泄露凭据之所以危险,不在于泄露本身,而在于很难被发现。提交进仓库的文本密钥会被密钥扫描器抓到,藏在图片里的密钥抓不到,会一直待在那里,直到有人用眼睛看出来。

goshot 主打自动遮蔽 API 密钥、令牌和密码,并且遮蔽的方式也可以挑选。

goshot config.go --redact --redact-style blur

不过这里要说清楚一件事。自动遮蔽是一种对已知模式作出反应的装置,而公司内部系统特有的格式可能不在这些模式里。所以这个功能应当被放在第一道网,而不是最后一道防线。真正的防线,是构建文档时不要使用装着真实凭据的环境。

把样式配置放进仓库

goshot 允许把常用标志的默认值写进配置文件。按 README 的说法,在 ~/.config/goshot/config.yaml 里以扁平的形式写下标志名和取值,而命令行上给出的标志永远优先。

theme: catppuccin-mocha
chrome: mac
background: '#1e1e2e'
corner-radius: 12

这个文件如果放在家目录里,每个人跑出来的结果就会不一样。所以如果是构建文档的仓库,把这些值放到项目里而不是家目录里、并由脚本显式传入,会更好。这样样式的改动会进到代码评审里,也不会哪天早上突然发现整份文档的背景色变了。

这周就能加上的最小配置

最小的形态是这样的。把要作为图片源码的代码片段单独放成文件,再写一条从这些文件生成图片的规则。

# 从 docs/snippets/*.go 生成 docs/images/*.png
SNIPPETS := $(wildcard docs/snippets/*.go)
IMAGES   := $(patsubst docs/snippets/%.go,docs/images/%.png,$(SNIPPETS))

images: $(IMAGES)

docs/images/%.png: docs/snippets/%.go
	goshot $< -o $@ -t catppuccin-mocha -C mac --redact

这样一来,代码片段就成了真正的编译对象,片段一旦过期,构建或 lint 会先告诉你。图片跟在后面。这套配置的要点在于:与其用图片去解决图片过期的问题,不如把它退回成源码过期的问题

这里有件事要老实交代。这份 Makefile 并不是我装了 goshot 实际跑出来的结果。goshot 的命令和标志是我在仓库 README 里确认的,Makefile 里模式规则那部分是通用的 make 语法。真要引入时,请先拿一个小片段跑一遍。

小结与出处

关键不在工具的名字,而在于:对进入文档的图片,也应当适用与其他产物相同的标准。它能不能用一条命令重建?如果不能,那张图片总有一天会在无人察觉的情况下开始说错话。

  • watzon/goshot 仓库 —— 功能清单、CLI 示例、配置文件格式、许可证。正文中关于 goshot 的所有说法,都是在这个仓库的 README 中确认的。
  • 通过仓库元数据确认的信息:许可证是 MIT,主语言是 Go,仓库创建于 2024 年 11 月。这是一个由单人开发者维护的项目,而不是有大公司在背后支撑的工具,请据此判断是否引入。
  • 正文中的 CLI 示例搬自 README,Makefile 示例没有实际运行验证过。