markdownlint MD014 规则与空代码块测试分析:从 `code_block_dollar_fence-empty.md` 看命令前缀美元符号的检测逻辑

原创2026-10-14 20:01:291,814 阅读
文章标签:开发工具Lint静态分析代码质量

markdownlint MD014 规则与空代码块测试分析:从 code_block_dollar_fence-empty.md 看命令前缀美元符号的检测逻辑

导读

code_block_dollar_fence-empty.md 是 markdownlint 仓库中 MD014(commands-show-output)规则的一份专项测试夹具,专门验证「空代码块不会触发美元符号($)误报」这一边界行为。本文以该测试文件为切入点,结合 规则实现、场景测试框架 与 规则官方文档,完整剖析 MD014 的检测算法、可自动修复机制与内联配置语法,帮助你彻底理解「何时该写 $、何时不该写」这一 shell 代码块书写规范的工程化落地。


1. 测试文件本体:一份教科书式的边界用例

1.1 文件内容

测试文件 code_block_dollar_fence-empty.md 全文共 20 行,结构如下:

# Heading

```js





它包含三只**围栏代码块(fenced code block)**,语言标识分别为 `js`、`css`、`spaces`,其共同点是**内部没有任何有效命令内容**:

- `js` 块:只有一行空行;
- `css` 块:有两行空行(同时构成了对 MD009 `no-trailing-spaces` 的潜在考察点,见下文);
- `spaces` 块:有一行内容,但该行是**两个空格**(在快照的修复结果中以 `"  \n"` 形式保留)。

> 注意:文档中的 `{MD014}`、`{MD046:34}` 这类标记不是标准 Markdown 语法,而是本仓库场景测试框架特有的**预期断言标注**(参见 <a href="https://link.gitcode.com/i/3e710ab11aa08f92df0cd1cf7bcf8495" target="_blank">测试框架</a> 中的 `/\{(MD\d+)(?::([-+]?)(\d+))?\}/g` 正则)。该文件没有出现任何 `{MD014}` 标记,即声明「空代码块不应产生 MD014 错误」。

### 1.2 快照验证:预期输出为空

仓库的测试快照 <a href="https://link.gitcode.com/i/9505a99eca8ee8ba1ba9c4cebd9c205b" target="_blank">markdownlint-test-scenarios.mjs.snapshot</a> 中,该文件的断言结果为:

```json
{
  "errors": [],
  "fixed": "# Heading\n\n```js\n\n```\n\n```css\n\n\n```\n\n```spaces\n  \n```\n\n<!-- markdownlint-configure-file {\n  \"no-trailing-spaces\": {\n    \"br_spaces\": 2\n  }\n} -->\n"
}

两个关键结论:

  1. errors: []——三个空代码块均未触发任何规则错误,MD014 的「不误报」特性得到确证;
  2. fixed 与原内容基本一致——快照证明 applyFixes 对空块不做任何改写,修复操作是幂等的。

快照同时揭示了另一条细节:spaces 块中那行「两个空格」在修复后保留为 " \n",因为文件底部通过内联配置将 MD009 的 br_spaces 设为 2,允许恰好两个空格作为硬换行(br)保留——这是该文件同时作为 MD009 配置场景的佐证。


2. MD014 规则到底是什么:命令前缀 $ 的使用规范

2.1 规则身份信息

来自 规则实现 的元数据:

字段 值
规则编号 MD014
别名 commands-show-output
描述 Dollar signs used before commands without showing output(命令前使用美元符号但未展示输出)
标签 code
解析器 micromark
是否可修复 部分可修复(见 constants.mjs,MD014 在 fixableRuleNames 列表中)

在 规则索引文档 与 README 规则列表 中均登记为:

MD014 commands-show-output - Dollar signs used before commands without showing output

2.2 何时触发、何时不触发

依据 官方规则文档,MD014 的判定核心是:一个代码块中的全部 shell 命令行都以 $ 开头,且没有任何输出行时才触发。具体场景:

  • ✅ 触发——代码块只含待输入的命令,全部带 $ 前缀:

    $ ls
    $ cat foo
    $ less bar
    

    此时文档建议去掉 $,直接写成:

    ls
    cat foo
    less bar
    
  • ❌ 不触发——命令带 $ 且展示了输出,$ 用于区分命令与输出:

    $ ls
    foo bar
    $ cat foo
    Hello world
    
  • ❌ 不触发——部分命令无输出(因为某些命令本就无输出,允许混用):

    $ mkdir test
    mkdir: created directory 'test'
    $ ls test
    

规则文档给出的理由(Rationale):省略不必要的 $ 更便于复制粘贴,也让文档更简洁。该规范源自 cirosantilli 的 Markdown 风格指南(Dollar signs in shell code 一节,规则文档中附有外链原文,本文按仓库规范不展开外链)。


3. 从源码看 MD014 的检测算法

3.1 核心实现

MD014 的完整实现位于 lib/md014.mjs,核心逻辑如下:

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

export default {
  "names": [ "MD014", "commands-show-output" ],
  "description": "Dollar signs used before commands without showing output",
  "tags": [ "code" ],
  "parser": "micromark",
  "function": function MD014(params, onError) {
    for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
      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);
      if (dollarMatches.length === codeFlowValues.length) {
        // 对每个 $ 前缀行报告错误,并携带可修复信息
        // editColumn / deleteCount 用于删除 "$ "
      }
    }
  }
};

3.2 算法逐步拆解

  1. 筛选代码块:通过 filterByTypesCached(<a href="https://link.gitcode.com/i/60864d924f2a1110e069be991db64737" target="_blank"> "codeFenced", "codeIndented" ]) 同时覆盖围栏代码块与缩进代码块两类。该函数来自 [lib/cache.mjs,底层调用 micromark-helpers.cjs 的 filterByTypes,并按参数做结果缓存(getCached),避免同一批 token 被多条规则重复遍历。

  2. 提取代码内容行:取每个代码块的 children 中类型为 codeFlowValue 的节点——即代码块内部的实际内容文本节点,而非围栏本身(```)或缩进前缀。

  3. 逐行匹配 $ 前缀:正则 dollarCommandRe = /^(\s*)(\$\s+)/ 分两组捕获:

    • 第 1 组 (\s*):行首的空白(可为 0 个);
    • 第 2 组 (\$\s+):$ 符号后跟至少一个空白。

    注意两点细节:

    • 要求 $ 后必须有空白,因此 shell 变量写法 $foo = 'bar' 不会误报(此场景在姊妹测试文件 code_block_dollar.md 中有明确覆盖);
    • 允许行首存在空格,即「空格前缀的命令」也能被识别(code_block_dollar_fence.md 中 " $ ls example" 同样报 MD014)。
  4. 全体判定:dollarMatches.length === codeFlowValues.length 成立才报错——只有代码块内全部内容行都带 $ 前缀时才触发。这正是「命令展示输出则不触发」「部分命令无输出则容忍」规则的实现来源。

  5. 空块天然豁免:当代码块内容为空(codeFlowValues 长度为 0)时,0 === 0 本会成立,但循环遍历 dollarMatches(也为空)不会产生任何错误,因此空代码块不会误报——这与 markdownlint-test.mjs 等断言中 "MD014": [23] 的按行计数方式也保持一致(空文件无 MD014 命中)。

3.3 可自动修复机制

MD014 是「部分可修复(Fixable)」规则。报错时通过 addErrorContext(实现见 helpers/helpers.cjs)附带 fixInfo:

{
  "editColumn": column,   // 从 $ 起始列开始编辑
  "deleteCount": length   // 删除 "$ " 两个字符
}

其中 column 由 startColumn + 空白组长度 计算而来(dollarMatch.result<a href="https://link.gitcode.com/i/92ca69386d641ede44d9273b0dcb19f2" target="_blank">1].length),即精确指向 $ 符号所在列。快照中的修复结果印证了这一点——对比 [code_block_dollar_fence.md 的修复快照,$ code {MD014} 被改写为 code {MD014},而保留了输出行的 $(如 $ code + code 混排的块未被改写)。

提示:{MD014} 标记在 applyFixes 之后仍保留在文本中,因为它是测试断言标记而非真实文件内容的一部分;真实修复只删除 $ 及其后空格。


4. 测试框架如何验证这份文件

4.1 场景测试的运行机制

该文件由 markdownlint-test-scenarios.mjs 驱动:测试会遍历 ./test 目录下所有 *.md 文件(L108-L115),对每个文件执行「读文件 + lint({ files: [file] })」双通道处理,然后:

  1. 用 {MD\d+} 标记解析期望的违规行号;
  2. 对比实际 lint 结果的 ruleNames[0] 与行号;
  3. 调用 applyFixes(content, errors) 生成修复后文本,输出快照;
  4. 对修复结果二次 lint,断言不存在残留的、带 fixInfo 的可修复错误(L95-L102),保证修复完整性。

4.2 与其他 MD014 测试文件的互补

测试文件 覆盖重点
code_block_dollar_fence-empty.md 空围栏块不误报(本文主题)
code_block_dollar_fence.md 围栏块内全 $ 行触发、输出混排不触发、空格前缀触发
code_block_dollar.md 缩进块内全 $ 行触发、输出/变量名/无 $ 块不触发、混合内容、{MD046:34} 空块联动
MD011-MD021.md 多规则场景下 MD014 与其他规则共存
break-all-the-rules.md 全规则同时触发的压力场景

其中 code_block_dollar.md 末尾的 {MD046:34} 与空 bash 块共同构成「空代码块同时豁免 MD014 与 MD046(代码围栏样式一致性)」的组合验证,与本文文件的思路一脉相承。


5. 内联配置:文件级规则调优

测试文件底部是一段 markdownlint 的文件级内联配置:

<!-- markdownlint-configure-file {
  "no-trailing-spaces": {
    "br_spaces": 2
  }
} -->

依据 README 内联配置章节:

  • 语法为 <!-- markdownlint-configure-file { JSON } -->,作用于整个文件,与注释位置无关;
  • 多个此类注释按自上而下顺序合并生效;
  • 默认解析 JSON,可通过 options.configParsers 支持其他格式;
  • 同一家族的指令还包括 markdownlint-disable-file / markdownlint-enable-file(README.md#L280-L281)。

此处配置将 MD009 的 br_spaces 设为 2。从 md009.mjs 实现 看,br_spaces 的默认值即 2((brSpaces === undefined) ? 2 : brSpaces),且代码块行默认被 MD009 排除(code_blocks 默认 false),因此该配置的存在更像是显式声明语义:允许 spaces 块中恰好 2 个空格作为硬换行符保留,避免修复阶段被误删——这解释了快照 fixed 中 " \n" 得以保留的原因。


6. 实战自查清单

对照 MD014 与空块行为,可总结出以下自查规则:

  • 代码块只含命令、且全部以 $ 开头、无输出行 → 应去掉 $ ,否则报 MD014(可自动修复);
  • 命令与输出混排、或部分命令确实无输出 → 允许 $ ,不报 MD014;
  • shell 变量(如 $foo) → $ 后无空格,不会误报;
  • 空代码块(无论 js/css/自定义语言标识)→ 天然豁免 MD014,也不会被 applyFixes 改动;
  • 行首空格 + $ (如 " $ ls")→ 仍会被识别为命令前缀并触发;
  • 需要文件级豁免或参数调整时,使用 <!-- markdownlint-configure-file {...} --> 内联配置。

7. 延伸阅读

登录后查看全文
markdownlint