markdownlint MD014 规则与空代码块测试分析:从 `code_block_dollar_fence-empty.md` 看命令前缀美元符号的检测逻辑
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"
}
两个关键结论:
errors: []——三个空代码块均未触发任何规则错误,MD014 的「不误报」特性得到确证;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 算法逐步拆解
-
筛选代码块:通过
filterByTypesCached(<a href="https://link.gitcode.com/i/60864d924f2a1110e069be991db64737" target="_blank"> "codeFenced", "codeIndented" ])同时覆盖围栏代码块与缩进代码块两类。该函数来自 [lib/cache.mjs,底层调用 micromark-helpers.cjs 的filterByTypes,并按参数做结果缓存(getCached),避免同一批 token 被多条规则重复遍历。 -
提取代码内容行:取每个代码块的
children中类型为codeFlowValue的节点——即代码块内部的实际内容文本节点,而非围栏本身(```)或缩进前缀。 -
逐行匹配
$前缀:正则dollarCommandRe = /^(\s*)(\$\s+)/分两组捕获:- 第 1 组
(\s*):行首的空白(可为 0 个); - 第 2 组
(\$\s+):$符号后跟至少一个空白。
注意两点细节:
- 要求
$后必须有空白,因此 shell 变量写法$foo = 'bar'不会误报(此场景在姊妹测试文件 code_block_dollar.md 中有明确覆盖); - 允许行首存在空格,即「空格前缀的命令」也能被识别(code_block_dollar_fence.md 中
" $ ls example"同样报 MD014)。
- 第 1 组
-
全体判定:
dollarMatches.length === codeFlowValues.length成立才报错——只有代码块内全部内容行都带$前缀时才触发。这正是「命令展示输出则不触发」「部分命令无输出则容忍」规则的实现来源。 -
空块天然豁免:当代码块内容为空(
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] })」双通道处理,然后:
- 用
{MD\d+}标记解析期望的违规行号; - 对比实际 lint 结果的
ruleNames[0]与行号; - 调用
applyFixes(content, errors)生成修复后文本,输出快照; - 对修复结果二次 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. 延伸阅读
- 规则实现:lib/md014.mjs
- 规则官方文档:doc/md014.md
- 完整规则索引:doc/Rules.md
- 场景测试框架:test/markdownlint-test-scenarios.mjs
- 相关快照:test/markdownlint-test-scenarios.mjs.snapshot
- 姊妹测试文件:test/code_block_dollar.md、test/code_block_dollar_fence.md
- 内联配置说明:README.md