markdownlint 规则详解:MD014(commands-show-output)——去除 Shell 命令代码块中多余的美元符号
markdownlint 规则详解:MD014(commands-show-output)——去除 Shell 命令代码块中多余的美元符号
MD014 是 markdownlint 中针对代码块类别(Tags: code)的一条格式检查规则,专门检查"展示待执行 Shell 命令的代码块中是否不必要地使用了美元符号($)"。当代码块中展示的全部 Shell 命令都以 $ 开头、却又没有展示任何命令输出时,该规则就会触发告警。本文结合仓库源码(lib/md014.mjs)与配套测试(test/code_block_dollar.md、test/code_block_dollar_fence.md),完整讲解 MD014 的触发条件、不触发场景、底层实现原理、自动修复行为以及工程实践建议,帮助你准确理解并合理使用这条规则。
规则概览
| 属性 | 值 |
|---|---|
| 规则编号 | MD014 |
| 别名(Aliases) | commands-show-output |
| 所属标签(Tags) | code |
| 可修复性(Fixable) | 部分违规可由工具自动修复 |
| 解析器(Parser) | micromark(见 lib/md014.mjs) |
| 默认状态 | 随 markdownlint 全部规则默认启用,可通过配置禁用 |
规则的官方描述为"Dollar signs used before commands without showing output"(在未展示输出的命令前使用了美元符号)。该规则在仓库的规则总览文档 doc/Rules.md 中有与 doc/md014.md 完全一致的完整说明。
MD014 的核心设计动机(Rationale)是:当美元符号不是必需时,省略它可以让命令更便于复制粘贴,同时让代码块看起来更干净、噪声更少。这一风格参考了 Ciro Santilli 的 Markdown 风格指南中关于"Shell 代码中的美元符号"(dollar signs in shell code)的约定。
触发条件:全部命令带 $ 且无输出
当文档中存在展示"待键入 Shell 命令"的代码块,并且该代码块中的所有 Shell 命令都带美元符号前缀时,MD014 会认为这些美元符号是不必要的,并触发违规。
以下代码块会触发 MD014(每行命令前的 $ 均为多余):
$ ls
$ cat foo
$ less bar
正确写法是省略美元符号,直接给出命令本身:
ls
cat foo
less bar
命中规则的判定要点
从源码实现看,判定逻辑的关键在于两个条件(详见 lib/md014.mjs):
- 代码块类型:规则只处理
codeFenced(围栏代码块,即```包裹)和codeIndented(缩进代码块,即行首缩进 4 个空格)两类代码块; - 逐行匹配且全部命中:规则对代码块内每一行内容(
codeFlowValue文本)用正则/^(\s*)(\$\s+)/进行匹配,只有所有行都匹配成功(即每一行都以$加空白开头),该代码块才会被判定为违规。只要存在任何一行不是"$开头的命令"(例如是输出行),整个代码块就不会触发。
正则 /^(\s*)(\$\s+)/ 中的两个捕获组含义为:
(\s*):匹配命令前的缩进空白(允许代码块内命令带前置空格);(\$\s+):匹配美元符号$及其后至少一个空白字符(空格或 Tab)。
不触发 MD014 的场景
1. 命令展示了输出(可区分命令与输出)
如果带 $ 的命令下方跟有输出内容,$ 恰恰起到了区分命令与输出的作用,此时不触发规则:
$ ls
foo bar
$ cat foo
Hello world
$ cat bar
baz
2. 部分命令没有输出
由于部分命令天然不产生输出(例如 mkdir、cd),只要部分命令伴随输出,$ 前缀就有存在意义,不算违规:
$ mkdir test
mkdir: created directory 'test'
$ ls test
注意上例中 mkdir test 与 ls test 两条命令之间,输出只出现在第一条命令下方,ls test 没有输出——但正因"并非所有命令都无输出",整个代码块不触发 MD014。
3. 变量名或内容中的 $(非命令前缀)
正则要求 $ 后面必须跟空白字符(\s+),因此像 $foo = 'bar'; 这样的变量赋值代码不会被误判为命令前缀,也不触发规则。这一边界情况在测试文件 test/code_block_dollar.md 中有明确覆盖。
4. 空代码块 / 无 $ 的代码块
没有任何内容的空围栏代码块、以及完全不含美元符号的代码块,自然不触发 MD014(参见 test/code_block_dollar.md)。
5. 混合内容(命令与输出交错)
即使代码块中同时存在"带 $ 无输出"的行和输出行,只要不是所有行都是"带 $ 的命令",就不触发。测试文件中的 "Mixed content" 用例即属于此类(见 test/code_block_dollar.md),其中 $ git branch、$ tail cat 等无输出的行同样不会单独报错。
6. 输出中含空行的命令
命令输出中间夹杂空行也不影响判定——只要代码块中存在非命令行(输出),该块整体视为"展示了输出"而不触发,详见 test/code_block_dollar.md 中的 dig example.com 用例。
源码实现深度剖析
MD014 的实现位于 lib/md014.mjs,完整源码仅 47 行,逻辑非常清晰,可以拆解为三个步骤:
第一步:遍历代码块节点
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
规则通过 lib/cache.mjs 中导出的 filterByTypesCached 从 micromark 解析出的 token 流中筛选 codeFenced(围栏代码块)与 codeIndented(缩进代码块)两类节点。该函数会对同一参数组合的过滤结果做缓存(按参数 JSON 序列化后的 key 缓存),避免同一文档多次 lint 时重复遍历 token 流,属于性能优化手段。
第二步:逐行提取命令文本并匹配
const codeFlowValues = codeBlock.children.filter((child) => child.type === "codeFlowValue");
const dollarMatches = codeFlowValues
.map((codeFlowValue) => ({
"result": codeFlowValue.text.match(dollarCommandRe),
"startColumn": codeFlowValue.startColumn,
"startLine": codeFlowValue.startLine,
"text": codeFlowValue.text
}))
.filter((dollarMatch) => dollarMatch.result);
代码块内容由 codeFlowValue 类型的子节点构成,每一行对应一个节点。规则对每个节点文本执行 dollarCommandRe 匹配,并记录该行的起始列(startColumn)、起始行(startLine)及原始文本,随后过滤出匹配成功(即"以 $ 开头")的行。
第三步:全命中才报告,并携带自动修复信息
if (dollarMatches.length === codeFlowValues.length) {
for (const dollarMatch of dollarMatches) {
const column = dollarMatch.startColumn + dollarMatch.result[1].length;
const length = dollarMatch.result[2].length;
addErrorContext(
onError,
dollarMatch.startLine,
dollarMatch.text,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length
}
);
}
}
只有当 dollarMatches.length === codeFlowValues.length,即代码块内每一行都命中 $ 前缀时,才对每一行逐一报错。这里有两个关键细节:
- 报错范围(errorRange):
[column, length]定位到$符号本身。其中column是行起始列加上缩进组(\s*)的长度,length是$加后续空白的总长度——例如$ ls中范围为第 1 列起、长度 2(即$两个字符); - 自动修复信息(fixInfo):
editColumn指向$所在列,deleteCount为待删除字符数(即$与其后空白的长度)。这对应了规则文档头部声明的 "Fixable: Some violations can be fixed by tooling"。
错误对象通过 helpers/helpers.cjs 中的 addErrorContext 生成,它会用 ellipsify 统一换行符并对超长上下文做省略处理,然后调用 onError 回调把 lineNumber、context、range、fixInfo 等字段交给 lint 引擎。
自动修复:MD014 如何被 --fix 处理
MD014 报告的每个违规都附带 fixInfo,因此支持工具自动修复。从测试快照(test/markdownlint-test-scenarios.mjs.snapshot)可以看到,$ ls {MD014} 这类行的修复信息为:
{
"lineNumber": 5,
"ruleNames": ["MD014", "commands-show-output"],
"errorRange": [5, 2],
"fixInfo": {
"editColumn": 5,
"deleteCount": 2
}
}
修复的效果是删除 $ 与紧随其后的空白:$ ls → ls,同时保留命令前原有的缩进。
在 lint 引擎侧,修复信息的消费逻辑位于 lib/markdownlint.mjs 的 applyFixes 函数:它会先对所有违规的 fixInfo 按行号、列排序,再剔除同一位置重复的修复,最终按 editColumn - 1 计算索引,执行 line.slice(0, editIndex) + insertText + line.slice(editIndex + deleteCount) 完成删除(对应 applyFix)。这意味着你可以在命令行通过 markdownlint --fix 或调用 API 时传入修复选项,一键清理文档中多余的 $ 前缀。
测试验证:边界行为一览
MD014 的行为在仓库测试中有系统性的覆盖,主要分布在两个测试文件:
缩进代码块场景(test/code_block_dollar.md)
该文件逐一验证了以下情况(行内 {MD014} 标记表示该行预期报错):
- 缩进代码块中每行均为
$命令且无输出 → 每行报{MD014}(第 5-8 行); - 命令带输出、
$用于区分命令与输出 → 不报错; $foo = 'bar';变量名 → 不报错($后无空白);- 完全无
$的代码块 → 不报错; - 命令前带空格缩进(
$ ls example)→ 同样报{MD014}(说明(\s*)捕获组正确覆盖了前置缩进); - 全部命令均无输出(
mkdir、cd、ls三连)→ 每行报{MD014},这是"部分命令无输出可豁免、全部无输出不可豁免"的对照用例。
围栏代码块场景(test/code_block_dollar_fence.md)
该文件验证了 ```fence、```sh 等围栏代码块中的相同逻辑,包括空行位于命令之间的情况、命令带前置空格等情况,确认围栏与缩进两种代码块类型行为一致。
快照与集成测试
- test/markdownlint-test-scenarios.mjs.snapshot 中的 "break-all-the-rules" 场景展示了
$ dollar {MD014}的完整错误对象结构(errorRange: [5, 2]、fixInfo.editColumn: 5、deleteCount: 2); - test/MD011-MD021.md 中
$ command with no output {MD014}与 MD011-MD021 等其他规则共用同一测试文件,验证规则间互不干扰。
配置与工程实践建议
按需禁用或启用
MD014 与其他 markdownlint 规则一样,支持通过配置文件、行内指令等渠道启用或禁用。由于该规则别名是 commands-show-output,在配置中两种写法等价:
{
"MD014": false
}
{
"commands-show-output": false
}
在文档内部,可以使用 HTML 注释形式的行内指令临时禁用再恢复(这也是 doc/md014.md 官方示例自身采用的方式):
<!-- markdownlint-disable commands-show-output -->
$ ls
$ cat foo
<!-- markdownlint-restore -->
什么时候该保留 $
综合原文档与源码判定逻辑,建议遵循以下实践准则:
- 仅展示待键入命令(教程式片段、文档中的快速上手示例)→ 省略
$,最利于读者复制粘贴; - 命令伴随输出(终端会话实录)→ 保留
$,利用它区分命令行与输出行,这是 MD014 明确放行的场景; - 部分命令天然无输出但整体会话展示了输出 → 可保留
$,不会触发 MD014; - 混有
$变量名的脚本代码(如$foo = 'bar')→ 不受本规则影响,正常书写即可。
与其他规则的协作
MD014 作用于代码块内部内容,与同样针对代码块的规则(如围栏代码块周围空行的 MD031、代码围栏样式一致的 MD048 等)互不冲突、各司其职。在仓库默认样式配置(style/all.json 等)中无需为 MD014 做特殊处理,保持默认启用即可获得一致的检查体验。
小结
MD014(commands-show-output)是一条聚焦"Shell 命令代码块可复制性"的实用规则:当代码块中每一行都是带 $ 前缀的命令且没有任何输出时,它会逐行报告并支持自动删除多余的 $ 前缀;而一旦代码块中存在输出行、部分命令无输出、$ 属于变量名或内容的一部分,规则都会正确放行。理解"全命中才报告"这一核心判定逻辑,再配合 --fix 自动修复与行内禁用指令,你就能在保证文档可读性的同时,让 Shell 示例真正做到"所见即所得、一键可复制"。