markdownlint 规则详解:MD014(commands-show-output)——去除 Shell 命令代码块中多余的美元符号

原创2026-10-14 13:34:41539 阅读
文章标签:开发工具Lint静态分析代码质量

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):

  1. 代码块类型:规则只处理 codeFenced(围栏代码块,即 ``` 包裹)和 codeIndented(缩进代码块,即行首缩进 4 个空格)两类代码块;
  2. 逐行匹配且全部命中:规则对代码块内每一行内容(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 -->

什么时候该保留 $

综合原文档与源码判定逻辑,建议遵循以下实践准则:

  1. 仅展示待键入命令(教程式片段、文档中的快速上手示例)→ 省略 $,最利于读者复制粘贴;
  2. 命令伴随输出(终端会话实录)→ 保留 $,利用它区分命令行与输出行,这是 MD014 明确放行的场景;
  3. 部分命令天然无输出但整体会话展示了输出 → 可保留 $,不会触发 MD014;
  4. 混有 $ 变量名的脚本代码(如 $foo = 'bar')→ 不受本规则影响,正常书写即可。

与其他规则的协作

MD014 作用于代码块内部内容,与同样针对代码块的规则(如围栏代码块周围空行的 MD031、代码围栏样式一致的 MD048 等)互不冲突、各司其职。在仓库默认样式配置(style/all.json 等)中无需为 MD014 做特殊处理,保持默认启用即可获得一致的检查体验。

小结

MD014(commands-show-output)是一条聚焦"Shell 命令代码块可复制性"的实用规则:当代码块中每一行都是带 $ 前缀的命令且没有任何输出时,它会逐行报告并支持自动删除多余的 $ 前缀;而一旦代码块中存在输出行、部分命令无输出、$ 属于变量名或内容的一部分,规则都会正确放行。理解"全命中才报告"这一核心判定逻辑,再配合 --fix 自动修复与行内禁用指令,你就能在保证文档可读性的同时,让 Shell 示例真正做到"所见即所得、一键可复制"。

登录后查看全文
markdownlint