YAML行尾不可见字符引发的问题

一、介绍

YAML(YAML Ain’t Markup Language)作为一种人类可读性强的配置语言被广泛应用在各个应用系统中,包括:Kubernetes、Ansible、Docker Compose等等。

1.1 基本语法规则

键值对:

name: John
age: 30

缩进表示层级:

  • 使用空格缩进,禁止使用 tab
  • 通常每层缩进用 2 或 4 个空格
person:
  name: John
  age: 30

列表(数组):

persons:
  - name: John
    age: 30
  - name: Mike
    age: 28

嵌套结构:

server:
  host: localhost
  ports:
    - 80
    - 443

多行字符表示:

  • 使用 | 表示保留换行
  • 使用 > 表示折叠成一行
description: |
  Line one
  Line two

summary: >
  This is a very long
  line that will be folded.

注释:

name: John  # 这是注释

布尔、null、数字:

enabled: true
disabled: false
nothing: null
count: 100

1.2 常见使用错误

  • 缩进混用空格和tab
  • 缩进不对齐
  • 键名重复
  • 行尾隐藏字符(如空格、零宽字符等不可见字符)

二、行尾不可见字符

常见的 YAML 行尾不可见字符

  1. 空格(space, 0x20
    • 多余空格有时会影响多行字符串的拼接或注释识别。
  2. 制表符(tab, 0x09
    • YAML 禁止使用 tab 来缩进,但若 tab 出现在行尾,不一定会报错,却可能影响格式或行为。
  3. 不可见的 Unicode 字符(如 U+200B 零宽空格)
    • 通常是从复制粘贴中引入,肉眼难以察觉。

三、Go 程序解析

行尾不可见字符,程序解析 YAML 内容时,并不会读取行尾不可见字符,如果希望程序读取某个变量时,需要内容末尾的空格,那么整个内容需要通过引号进行包裹。

例如:

version: 1

name: 'Alice '
age: 30

四、Go 程序渲染

以下程序用于将 test.yaml 中的内容嵌入作为 Config 结构体实例中 Content 变量的内容,并输出为 Config 结构体的 yaml 文本。

package main

import (
    _ "embed"
    "fmt"

    "gopkg.in/yaml.v3"
)

type Config struct {
    Content string `yaml:"content"`
}

//go:embed test.yaml
var content string

func main() {
    m := Config{
        Content: content,
    }

    raw, err := yaml.Marshal(m)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(raw))
}

首先,正常 test.yaml 内容如下:

version: 1

name: Alice
age: 30

对应输出文本如下:

content: |-
    version: 1

    name: Alice
    age: 30

可见,content 中的内容,依旧保持着 yaml 语法的人类可读性强的特点。

然而,如果内容中某一行末尾存在不可见字符,比如下面 test.yaml 中的 name 行 Alice 后面还有一个空格:

version: 1

name: Alice 
age: 30

那么在渲染文本时,YAML 就会对内容进行转义,对应输出文件如下:

content: "version: 1\n\nname: Alice \nage: 30"

那么,遇到确实内容末尾需要加上空格,就必须使用引号进行包裹,比如:

version: 1

name: 'Alice '
age: 30

其对应输出如下:

content: |-
    version: 1

    name: 'Alice '
    age: 30

依旧能够保持良好的可读性。

五、Kubernetes Config Map

以下是一份因为行尾跟随了不可见字符而渲染之后的 Kubernetes Config Map 配置:

apiVersion: v1
data:
  api-config: "server:\n  address: 0.0.0.0:80 \n  mode: debug"
kind: ConfigMap

而这样的因为行尾不可见字符渲染出来的配置,往往会给排查环境问题工作带来麻烦。

而其对应正常内容应该如下:

apiVersion: v1
data:
  api-config: |-
    server:
      address: 0.0.0.0:80
      mode: debug
kind: ConfigMap

六、检查与防范

6.1 检查工具 cat

通过 cat 命令可以显示不可见字符:

$ cat -A file.yaml

其中:末尾 $ 表示行尾

version: 1$
$
name: Alice $
age: 30$

6.2 清理

通过 sed 命令可批量移除行尾空格:

$ sed -i 's/[ \t]*$//' file.yaml

6.4 预防建议

  • 统一使用空格缩进,禁用 tab
  • 启用编辑器的“保存时自动去除行尾空格”
  • 使用版本控制 + Lint 工具进行 YAML 格式校验
  • 避免从网页、PDF、Excel 等非纯文本源复制内容进 YAML
Author: ismdeep
License: Copyright (c) 2021 CC-BY-NC-4.0 LICENSE