DevKitLab Logo DevKitLab
YAML / 字符串 / 解析 / 配置文件

YAML 多行字符串:什么时候用 |,什么时候用 >

YAML 里写多行的值,有两个块符号:一个竖线,一个大于号;此外单引号、双引号、甚至不加引号也能跨行。每种保留还是折叠换行的规则都不一样,选错就把一段脚本压成一行。这篇把它讲全,连带那些会咬人的角落。

你想在 YAML 里写一个多行的值。可能是一段 shell 脚本,一段长描述,或者一段 SQL。YAML 给了你两个符号:|>。选错了不会报错,值只是悄悄地不对。用 > 写的脚本会被压成一行,跑不起来。用 | 写的描述,会带上你只为了塞进屏幕才敲的每个换行。

这两个符号,一般叫「字面(literal)」和「折叠(folded)」。名字没错,可到了要选的那一刻没什么用。真正帮你定下来的问题是:这段文字里的换行,有意义吗?

有意义,就保留,用 |。脚本里的换行用来分隔命令。表格里的换行用来分隔各行。没意义,比如你只是把一句长话折成两行好塞进编辑器,那就让它们消失,用 >

这能解决大多数情况。但块标量的角落比这多,其中几个会咬人。这篇把它讲全:两个符号、末尾的换行、缩进的坑,还有一件事——块标量根本不是写多行字符串的唯一办法。

| 保留换行,> 把换行折成空格

同样的输入,换两个符号,得到两个不同的字符串。用 |

message: |
  line one
  line two

得到 "line one\nline two\n"。换行留住了。用 >

message: >
  line one
  line two

得到 "line one line two\n"。换行变成了一个空格,就像你本来写成了一整行。

这个空格就是 > 的用途。它是给长文字的。你在文件里把一段长描述折行,好让它读着舒服,但它最终该是一行。只要换行是结构的一部分,用它就全错了。看它怎么毁掉一段脚本:

script: >
  set -e
  echo hi
  echo bye

它折叠成 "set -e echo hi echo bye\n"。三条命令挤成一行。同一段换成 |,得到 "set -e\necho hi\necho bye\n",这才对。拿不准就用 |。多留几个用不上的换行通常没事,但丢掉要紧的换行就会出错。

> 不折叠的两样东西:更深的缩进和空行

折叠有个例外,值得知道。比块内其余行缩进更深的一行,不会被折叠。它保留自己的换行,原样留下。看:

note: >
  prose that wraps
  onto two lines
    indented line, kept as-is
  back to prose

得到 "prose that wraps onto two lines\n indented line, kept as-is\nback to prose\n"。折行的散文被折成了空格。那行缩进更深的,换行和缩进都留住了。所以一个折叠块里能藏一座「字面小岛」:你既能折叠一段描述,又能在里面嵌一段代码。

它也是个坑。不小心多缩进一格,那一行就变成字面的。输出里凭空多出一个你没想要的换行,还没人提醒你。

空行是 > 不折叠的另一样东西。折叠块里的空行不会被压掉,它会变成一个换行。所以 > 照样能分段,哪怕它把每段内部的换行都折掉了:

text: >
  first paragraph

  second paragraph

得到 "first paragraph\nsecond paragraph\n"。段落内部的换行会折成空格。段落之间的那个空行,保留成一个换行。

chomping:末尾留几个换行

选好 | 还是 > 之后,块的末尾还剩一件小事:最后一行之后的那个换行怎么办。YAML 把这叫「chomping(末尾截断)」。它有三种。

  • 默认(clip,留一个):不管你在末尾留几个空行,最终只保留一个换行。| 得到 "text\n"
  • strip,-(全去掉):末尾一个换行都不留。|- 得到 "text"
  • keep,+(全保留):你写了几个末尾换行就留几个。一个后面跟两个空行的块,用 |+ 得到 "text\n\n\n"

大多数时候默认就够,你根本不用想它。它在两种情况下才要紧。第一种,末尾那个换行会搞坏东西,比如一个要参与哈希的值、一个 token、一个文件名。这时用 -,写成 key: |-。第二种,末尾的空行有意义、必须留住。这时用 +。其余情况,别动它。

当内容本身以空格开头

这里有个隐蔽的坑。YAML 从块的第一非空行决定这个块的缩进。通常这正合你意。但有时你的内容开头就带着真正的空格,比如一段本来就缩进好的代码。YAML 会把这些开头的空格当成块自己的缩进,剥掉。更糟的是,如果后面某行缩进更少,块会提前结束,直接报解析错误。

解法是缩进指示符:在 |> 后面紧跟一个数字。它直接说明缩进,不靠猜。

code: |2
      leading spaces kept
    less-indented line

那个 2 表示相对父级缩进再加两格,不是固定的「第 2 列」。这里键在第 0 列,所以内容从第 2 列开始;同一个键往里再嵌一层,|2 就表示比那一层再多两格。所以 leading spaces kept 留住四格空格, less-indented line 留住两格。它能和 chomping 组合,前后顺序都行:|2-|-2 一个意思。

块标量不是唯一的办法

不加任何符号,你也能写多行字符串。plain(不加引号)、单引号、双引号,全都能跨行。这三种都按同样的方式折叠:两行之间普通的换行变成空格,但空行仍会变成一个换行。(「缩进更深的行保留」是 > 独有的规则,这里不适用。)

plain:  first line
  second line
single: 'first line
  second line'
double: "first line
  second line"

三个都得到 "first line second line"。主要差别在转义、引号,以及 plain scalar 自身的语法限制,而不只是折叠方式。

双引号认转义。"a\nb" 是两行。"tab\there" 里有个真正的制表符。行尾一个反斜杠,会把下一行接上、不留空格。单引号一概不认。'a\nb' 是一个字面的反斜杠加一个 n。想在单引号里放一个引号,就写两个:'it''s here' 得到 it's here。块标量(|>)根本不解释转义——里面的 \n 就是反斜杠加 n

把五种并排看一下:

写法换行\n 转义末尾换行
| 字面保留一个,可用 - / +
> 折叠折成空格(空行变换行;更深缩进的行保留)一个,可用 - / +
"双引号"普通换行折成空格;空行变换行
'单引号'普通换行折成空格;空行变换行否('' 表示一个引号)
plain普通换行折成空格;空行变换行

这张表能看出两件事。想要真正的换行,只有 | 不用转义就能给。想要 \n\t 这类转义,只有双引号会读。

所有行尾都会变成 \n

还有一条归一化,容易忘。YAML 会把标量里的每个换行都改写成 \n。一个在 Windows 上存成 CRLF 的文件,解析出来还是 \n,不是 \r\n。你没法从源文件里夹带一个回车进去。真要一个回车,就在双引号字符串里转义它:"line\r\n"

这些在实际项目里怎么咬人

同一条规律反复出现。只要值本身是一个文件或一段脚本,它就要用 |

  • Kubernetes 的 ConfigMap 把整个配置文件当成一个值嵌进去。那个值几乎总是 |。换行就是文件本身。
  • GitHub Actions 用 run: | 写 shell 步骤。每一行是一条命令,换行必须留住。
  • Docker Compose 和 Ansible 对内联脚本、内联文件也一样。

> 出现在相反的场合:给人看的文字。一个长长的 description: 字段。一段你为了宽度折行的注释。值是散文,就折;值是文件,就留。

一句话决定

  • 换行有意义吗? 有 → |。没有,只是折行 → >
  • 拿不准?|。保留换行安全,丢掉换行是 bug。
  • 换行属于内容结构时(代码、脚本、表格、内嵌文件)用 |
  • 需要 \n\t 转义? 双引号。需要字面的换行? |
  • 末尾换行由 chomping 决定:默认一个 \n- 一个不留,+ 全留。
  • 内容以空格开头? 用缩进指示符,比如 |2

去看真实的字符串

这些光读 YAML 都看不出来。一个 \n 和一个空格,在纸面上一模一样。所以要去看。把文件贴进 YAML 转换器,读 JSON 输出。每个换行显示成明白的 \n,每次折叠显示成一个空格,每个末尾换行也如实呈现。你看到的是程序真正拿到的字符串,不是源文件的样子。

这是配置格式系列的一篇。旁边两篇是支柱:JSON、YAML、TOML 到底怎么选,和YAML 为什么老出问题(讲响亮和静默两种出错方式)。块标量又是一处:YAML 做了件合理、却和你以为的不一样的事。解法照旧——去看真实的值,别信源文件的样子。