markdownlint 规则 MD014 深度解析:代码块中的 `$` 命令提示符与输出检测

原创2026-10-14 12:22:401,351 阅读
文章标签:开发工具Lint静态分析代码质量

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,整个实现非常精简,其判定流程如下:

  1. 遍历代码块:用 filterByTypesCached 取出所有 codeFenced 与 codeIndented 类型的代码块。

  2. 提取代码行:通过 codeBlock.children.filter((child) => child.type === "codeFlowValue") 获取代码块内每一行内容(micromark 解析树中的 codeFlowValue 节点)。

  3. 逐行匹配前缀:对每一行文本执行正则匹配:

    const dollarCommandRe = /^(\s*)(\$\s+)/;
    

    该正则表达式的语义是:

    • ^(\s*):匹配行首可能的空白字符(缩进),捕获到分组 1;
    • (\$\s+):匹配一个美元符号 $ 及其后至少一个空白字符(空格),捕获到分组 2。

    也就是说,只有"行首空白 + $ + 空格"这种形态才被认为是命令提示符前缀;$foo = 'bar'; 这类变量声明($ 后紧跟非空白字符)不会被匹配,从而避免了把 PHP 等语言中的变量误判为 Shell 提示符。

  4. 全有判定:若匹配到前缀的行数等于该代码块的总行数(dollarMatches.length === codeFlowValues.length),则对每一处匹配调用 addErrorContext 上报违规。

  5. 附带修复信息:上报时通过 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 示例既规范又易用。
登录后查看全文
markdownlint