markdownlint 规则 MD014 深度解析:代码块中的 `$` 命令提示符与输出检测
markdownlint 规则 MD014 深度解析:代码块中的 $ 命令提示符与输出检测
本文围绕 markdownlint 内置规则 MD014(commands-show-output) 展开,全面讲解该规则的作用对象、触发与豁免条件、自动修复能力,并结合仓库源码(lib/md014.mjs)与测试用例(test/code_block_dollar.md、test/code_block_dollar_fence.md)深入剖析其底层检测逻辑。读完本文,你将掌握如何在 Markdown 文档中规范书写 Shell 命令示例,并能够熟练配置、豁免和验证该规则。
规则概览
MD014 是 markdownlint 内置规则之一,其完整元信息定义于 规则文档 与规则源码 lib/md014.mjs:
| 项目 | 值 |
|---|---|
| 规则编号 | MD014 |
| 别名(Aliases) | commands-show-output |
| 标签(Tags) | code |
| 是否可自动修复 | 是(部分违规可由工具修复) |
| 解析器(parser) | micromark |
| 默认启用 | 是(default: true 时生效) |
在 lib/rules.mjs 中,MD014 被导入并注册进内置规则数组(const md014 from "./md014.mjs" 与列表中的 md014 条目),因此无需额外配置即可参与默认 lint 流程。同时,规则源码中通过 addErrorContext 输出 fixInfo 修复信息,且 MD014 被列入 lib/constants.mjs 的 fixableRuleNames 数组,这意味着它具备被 applyFixes 等自动修复工具处理的能力。
规则作用对象:Shell 命令代码块
MD014 只针对展示待输入 Shell 命令的代码块。这里的"代码块"指两类:
- 缩进代码块(codeIndented):以 4 个空格(或一个制表符)缩进的行;
- 围栏代码块(codeFenced):以
```或~~~包裹的代码块。
从源码看,规则通过 filterByTypesCached(<a href="https://link.gitcode.com/i/cfc7f8f9a2bd10ea202fd8ca8f139f3e" target="_blank"> "codeFenced", "codeIndented" ]) 精确限定遍历范围(见 [lib/cache.mjs 中 filterByTypesCached 的实现),不涉及行内代码、表格单元格等内容。
触发场景:所有命令均带 $ 前缀
当代码块内每一行(即每个 codeFlowValue)都以 $ (美元符号加空格)开头时,规则被触发:
$ ls
$ cat foo
$ less bar
这种情况下 $ 提示符完全没有必要,且会干扰用户直接复制粘贴命令。正确写法是移除所有 $:
ls
cat foo
less bar
需要注意,MD014 的判定是全有或全无(all-or-nothing):只有当代码块中 100% 的行都匹配 $ 前缀时才报告;只要存在任何不以 $ 开头的行,整个代码块就不会被标记。
豁免条件:什么时候带 $ 是合法的
MD014 并非一刀切禁止 $ 符号,以下两种常见场景会被豁免:
场景一:命令显示了输出
当代码块同时包含命令和命令输出时,$ 正好起到区分"命令"与"输出"的作用,此时不触发本规则:
$ ls
foo bar
$ cat foo
Hello world
$ cat bar
baz
上述代码块中每一行命令下面都有对应输出,$ 帮助读者区分哪些是命令、哪些是结果,因此是合理用法。
场景二:部分命令没有输出
由于并非所有命令都会产生输出(例如 mkdir、cd 之后往往没有可见输出),只要部分命令下方出现了输出行,即使存在个别"命令无输出"的行,也不会触发本规则:
$ mkdir test
mkdir: created directory 'test'
$ ls test
这里 $ mkdir test 下方有 mkdir: 输出行,而 $ ls test 本身可能没有输出,但整体仍被视为"展示命令与输出"的合法用法。
源码实现剖析:检测逻辑与正则
规则核心逻辑集中在 lib/md014.mjs,整个实现非常精简,其判定流程如下:
-
遍历代码块:用
filterByTypesCached取出所有codeFenced与codeIndented类型的代码块。 -
提取代码行:通过
codeBlock.children.filter((child) => child.type === "codeFlowValue")获取代码块内每一行内容(micromark 解析树中的codeFlowValue节点)。 -
逐行匹配前缀:对每一行文本执行正则匹配:
const dollarCommandRe = /^(\s*)(\$\s+)/;该正则表达式的语义是:
^(\s*):匹配行首可能的空白字符(缩进),捕获到分组 1;(\$\s+):匹配一个美元符号$及其后至少一个空白字符(空格),捕获到分组 2。
也就是说,只有"行首空白 +
$+ 空格"这种形态才被认为是命令提示符前缀;$foo = 'bar';这类变量声明($后紧跟非空白字符)不会被匹配,从而避免了把 PHP 等语言中的变量误判为 Shell 提示符。 -
全有判定:若匹配到前缀的行数等于该代码块的总行数(
dollarMatches.length === codeFlowValues.length),则对每一处匹配调用addErrorContext上报违规。 -
附带修复信息:上报时通过
fixInfo提供editColumn(起始列)与deleteCount(删除长度),供自动修复工具删除$前缀。
由此可以推断,该规则天然支持以下边界行为:
- 空代码块(没有任何行)不触发;
- 带前导空格的
$命令(如$ ls example)同样会被识别并修复,因为正则允许\s*; - 行首
$后必须跟空白才构成提示符,$var形式不触发。
自动修复:$ 前缀删除
由于源码在每次上报违规时都携带了 fixInfo(editColumn 指向 $ 所在列,deleteCount 为 $ 的长度),MD014 属于"可由工具修复"的规则。使用者可通过以下途径触发修复:
- 使用支持
applyFixes的自动化流程(仓库测试 test/markdownlint-test-scenarios.mjs 中通过applyFixes(content, errors)生成修复后的快照); - 使用 markdownlint-cli / markdownlint-cli2 等命令行工具(见 README.md 中的使用说明)的自动修复选项。
修复效果示例如下。修复前:
$ mkdir test
$ cd test
$ ls test
修复后:
mkdir test
cd test
ls test
在 test/markdownlint-test-scenarios.mjs.snapshot 的快照中可以找到对应的修复结果(fixed 字段中 $ 前缀被移除、命令文本保留),这是规则可修复性的直接测试证据。
测试用例详解:边界行为验证
仓库通过两组专门测试文件验证 MD014 的全部行为,这些文件中的 {MD014} 标记会被 test/markdownlint-test-scenarios.mjs 解析为期望的违规行号。
缩进代码块测试:test/code_block_dollar.md
该文件覆盖了以下关键场景:
- 全部命令带
$:$ ls、$ less foo、$ cat bar全部标记{MD014}; - 命令附带输出:
$ ls下有foo bar、$ less foo下有Hello world、$ cat bar下有baz,不触发; - 变量声明:
$foo = 'bar';、$baz = 'qux';($后无空格),不触发; - 无
$的代码块:不触发; - 空围栏代码块:不触发(该文件第 34 行标注的是
MD046而非MD014,证明空代码块不属于本规则管辖); - 混合内容:代码块中同时存在"带输出的命令"和"无输出的命令",只要整体混有输出行,全部不触发;
- 全部命令无输出:
$ mkdir test、$ cd test、$ ls test三个命令均无输出,全部标记{MD014}; - 空格前缀命令:
$ ls example(行首带一个空格),同样触发并标记{MD014}。
围栏代码块测试:test/code_block_dollar_fence.md
该文件验证 $ 前缀在围栏代码块中的同样行为:
- 单个
$ code行:触发; - 多个
$ code行:触发; - 每行命令下方都有输出(
$ code换行code):不触发; - 带空行的围栏代码块(
$ npm install --save multimatch):触发; - 空格前缀命令(
$ ls example):触发。
这些测试从两个维度(缩进式与围栏式代码块)确认了规则的判定逻辑,与源码 filterByTypesCached([ "codeFenced", "codeIndented" ]) 的遍历范围完全对应。
此外,test/markdownlint-test.mjs 中的 styleAll 与 styleRelaxed 测试在 test/break-all-the-rules.md 上断言了 MD014: <a href="https://link.gitcode.com/i/c4bf59d624b74012f589175a737a440f" target="_blank">23] 的违规行号,印证了 MD014 是默认全量规则集([style/all.json)中的一员。
实战配置与使用建议
在配置文件中启用或禁用
MD014 默认启用。若需在项目中显式控制,可在 markdownlint 配置文件中写入(JSON 格式示例):
{
"MD014": true,
"commands-show-output": false
}
"MD014": true:显式启用;"commands-show-output": false:使用别名禁用(别名与编号等效)。
规则本身没有可调参数,因此配置时只需控制启停状态。
在文档中使用内联指令豁免
当文档某处确实需要展示"带 $ 且无输出"的示例、但又不希望触发规则时,可以在代码块前后使用 markdownlint 内联指令(这也是 规则文档 自身的写法):
<!-- markdownlint-disable commands-show-output -->
```markdown
$ ls
$ cat foo
$ less bar
其中 `markdownlint-disable` 与 `markdownlint-restore` 成对使用,保证豁免范围精确可控(内联指令的解析逻辑可参见 <a href="https://link.gitcode.com/i/81db66d7c8b18dee99b0dd3fabce863a" target="_blank">helpers/helpers.cjs</a> 中的 `inlineCommentStartRe` 正则,它识别 `markdownlint-disable` / `restore` / `capture` 等注释形式)。
### 写作规范建议
结合本规则的设计意图,在 Markdown 文档中书写 Shell 命令时建议遵循以下惯例:
1. **纯命令列表**:直接列出命令本身,不加 `$`,方便读者复制粘贴;
2. **命令 + 输出混排**:保留 `$` 作为命令与输出的视觉分隔符,提高可读性;
3. **命令无输出**:若后续没有输出行,就不要加 `$`,否则会触发 MD014;
4. **避免误用**:代码块中涉及 `$` 开头的变量(如 PHP)时,`$` 后不要跟空格,天然不会被误判。
## 设计原理与总结
MD014 的 Rationale(设计依据)在 <a href="https://link.gitcode.com/i/7b146fa0833d11aa72a08d6cb19f21d9" target="_blank">规则文档</a> 中有明确说明:当 `$` 提示符并非必需时,去掉它会让文档**更易于复制粘贴、更清爽**;而当命令与输出混排时,`$` 反而承担了信息分隔的职责,因此被豁免。这一设计兼顾了"可用性"与"清晰度"两个目标。
总结要点:
- MD014 只针对"代码块内所有行都以 `$ ` 开头"的 Shell 命令示例;
- 只要代码块中出现任何输出行,规则即整体豁免;
- `$` 后无空格(如变量 `$foo`)不会被误判;
- 规则可通过 `applyFixes` 自动删除冗余的 `$ ` 前缀;
- 缩进代码块与围栏代码块一视同仁,均由 <a href="https://link.gitcode.com/i/7b3b83832890af9014da3a947c2d6b8c" target="_blank">lib/md014.mjs</a> 中同一套 `dollarCommandRe` 正则逻辑处理。
借助本文的说明,你可以在自己的 markdownlint 项目中放心使用这一规则,并通过配置、内联豁免和自动修复机制,让文档中的 Shell 示例既规范又易用。