DevToolBox免费
博客

YAML 多行字符串:块标量、折叠和字面量样式详解

6 分钟阅读作者 DevToolBox

YAML 多行字符串是 YAML 规范中最令人困惑的部分之一。有 6 种块标量组合、3 种裁剪指示符、缩进控制和流标量,很容易迷失方向。本指南通过 Docker Compose、Kubernetes、GitHub Actions 和 Ansible 的实际示例,覆盖所有多行字符串样式。

1. 为什么 YAML 多行字符串令人困惑

YAML 提供了至少 9 种不同的方式来编写跨多行的字符串。与 JSON(只能在双引号内使用 \n)或 TOML(使用三引号字符串)不同,YAML 让你精细控制换行符和尾部空白的处理方式。这种强大功能的代价是复杂性。

两种主要的块标量样式是字面量(|)和折叠(>)。每种都可以与三种裁剪指示符(clip、strip、keep)组合,产生 6 种核心组合。此外,你还可以指定显式缩进级别。让我们逐一分解。

  • 6 种块标量组合(|、|-、|+、>、>-、>+)
  • 缩进指示符(|2、>2 等)
  • 3 种流标量样式(纯文本、单引号、双引号)
  • 尾部换行行为的微妙差异
  • 与 YAML 解析器的交互(PyYAML、js-yaml、SnakeYAML)

2. 字面量样式(|):保留换行符

字面量块标量(|)完全按原样保留文本中的每个换行符。YAML 源代码中的每个换行都变成解析值中的字面 \n。默认会添加一个尾部换行符(clip 行为)。

适用场景:Shell 脚本、代码块、预格式化文本、SQL 查询、任何换行符有意义的内容。
# Literal style (|) — preserves newlines exactly
message: |
  Line one.
  Line two.
  Line three.

解析结果:

"Line one\nLine two\nLine three\n"
# Preserves indentation beyond the block indent level
script: |
  #!/bin/bash
  if [ "$ENV" = "prod" ]; then
    echo "Production mode"
    npm run build
  else
    echo "Development mode"
    npm run dev
  fi
# SQL query — newlines preserved for readability
query: |
  SELECT u.id, u.name, u.email
  FROM users u
  JOIN orders o ON u.id = o.user_id
  WHERE o.created_at > '2025-01-01'
  ORDER BY u.name ASC
  LIMIT 100;
要点:缩进会被去除(相对于块)。超出缩进级别的前导空格会被保留。默认添加尾部换行符。

3. 折叠样式(>):将换行符折叠为空格

折叠块标量(>)将单个换行符替换为空格,有效地将连续行合并为一个长行。但是,空行和"更多缩进"的行仍然会创建真正的换行符。默认添加一个尾部换行符。

适用场景:长描述、文本段落、任何你想为了可读性而换行但解析为单行的字符串。
# Folded style (>) — joins lines with spaces
description: >
  This is a very long string
  that I want to wrap across
  multiple lines for readability.

解析结果:

"This is a very long string that I want to wrap across multiple lines for readability.\n"

空行在折叠样式中创建真正的换行符:

# Blank lines create real newlines in folded style
content: >
  Paragraph one.

  Paragraph two.

  Paragraph three.

解析结果:

"Paragraph one.\nParagraph two.\nParagraph three.\n"

更多缩进的行也会保留换行符:

# More-indented lines preserve newlines
content: >
  Regular text
    indented line
    another indented
  back to normal

解析结果:

"Regular text\n  indented line\n  another indented\nback to normal\n"

4. 裁剪指示符:strip(-)、keep(+)、clip(默认)

裁剪控制块标量末尾的尾部换行符如何处理。这是 YAML 中最容易被误解的特性之一。

Clip(默认,无指示符)

保留一个尾部换行符。额外的尾部换行符被移除。

# Clip (default) — single trailing newline
message: |
  hello world


# (two blank lines at end)
# Result: "hello world\n"(一个尾部换行符)

Strip(- 指示符)

移除所有尾部换行符。字符串以最后一个非空行结束。

# Strip (-) — no trailing newline
message: |-
  hello world


# (two blank lines at end, all removed)
# Result: "hello world"(无尾部换行符)

Keep(+ 指示符)

完全按原样保留所有尾部换行符。

# Keep (+) — all trailing newlines preserved
message: |+
  hello world


# (two blank lines at end, all kept)
# Result: "hello world\n\n\n"(保留所有三个尾部换行符)

5. 全部 6 种块标量组合

以下是一个综合参考表,展示每种样式和裁剪指示符组合的行为:

语法名称内部换行尾部换行使用场景
|字面量 Clip保留添加单个 \nShell 脚本、代码
|-字面量 Strip保留全部移除内联值、无尾部换行
|+字面量 Keep保留全部保留保留精确的空白
>折叠 Clip折叠为空格添加单个 \n长描述文本
>-折叠 Strip折叠为空格全部移除干净的单行值
>+折叠 Keep折叠为空格全部保留带尾部空格的段落

并排示例与解析结果:

# |  (literal clip) — preserves newlines, adds single trailing \n
a: |
  one
  two

# Result: "one\ntwo\n"

# |- (literal strip) — preserves newlines, no trailing \n
b: |-
  one
  two

# Result: "one\ntwo"

# |+ (literal keep) — preserves newlines, keeps ALL trailing \n
c: |+
  one
  two


# Result: "one\ntwo\n\n"

# >  (folded clip) — folds newlines, adds single trailing \n
d: >
  one
  two

# Result: "one two\n"

# >- (folded strip) — folds newlines, no trailing \n
e: >-
  one
  two

# Result: "one two"

# >+ (folded keep) — folds newlines, keeps ALL trailing \n
f: >+
  one
  two


# Result: "one two\n\n"

6. 缩进控制:|2、>2

默认情况下,YAML 从第一个非空行确定块标量的缩进级别。有时你需要覆盖此行为,特别是当内容以空格开头时。

为什么可能需要显式缩进:

  • 内容以空格开头(如缩进的代码)
  • YAML 解析器被前导空格困惑
  • 确保跨不同 YAML 库的一致解析

无缩进指示符(自动检测):

# Auto-detect indentation (default behavior)
# YAML detects 2-space indent from first non-empty line
content: |
  normal text
  more text

带显式缩进指示符:

# Explicit indentation: |2 means "strip 2 spaces"
# Useful when content starts with spaces
content: |2
    This line has 2 leading spaces after stripping
    This also has 2 leading spaces
  This has 0 leading spaces after stripping

# Result: "  This line has 2 leading spaces after stripping\n  This also has 2 leading spaces\nThis has 0 leading spaces after stripping\n"
# Folded with explicit indentation
description: >2
    This paragraph starts with
    two extra spaces that are
    preserved in the output.

# Result: "  This paragraph starts with two extra spaces that are preserved in the output.\n"
| 或 > 后面的数字指定要去除多少个空格的缩进。|2 表示"从每行去除 2 个空格的缩进"。
# Combining indentation with chomping indicators
script: |2-
    #!/usr/bin/env python
    def main():
        print("Hello")

# Result: "  #!/usr/bin/env python\n  def main():\n      print(\"Hello\")"
# (2 spaces stripped, no trailing newline due to -)

config: >2+
    This is indented content
    that will be folded
    with trailing newlines kept.


# Result: "  This is indented content that will be folded with trailing newlines kept.\n\n"
# (2 spaces stripped, newlines kept due to +)
你可以将缩进与裁剪组合使用:|2-、>2+、|4+ 等。

7. 实际应用示例

Docker Compose

Docker Compose 服务中的多行命令:

# docker-compose.yml
version: "3.8"

services:
  web:
    image: node:20-alpine
    # Literal style — each command on its own line
    command: |
      sh -c "
        npm install &&
        npm run migrate &&
        npm run start
      "
    environment:
      # Folded strip — long value on one line, no trailing newline
      DATABASE_URL: >-
        postgresql://user:password@db:5432/myapp
        ?sslmode=require
        &connect_timeout=10
    healthcheck:
      # Literal strip — script without trailing newline
      test: |-
        curl -f http://localhost:3000/health || exit 1
      interval: 30s
      timeout: 10s

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    # Folded style — long description for documentation
    labels:
      description: >
        This is the reverse proxy service that handles
        SSL termination, load balancing, and static file
        serving for the web application.

Kubernetes ConfigMap 和 Pod

在 ConfigMap 中存储配置文件和脚本:

# ConfigMap with embedded configuration files
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  # Literal style — preserve the entire nginx config as-is
  nginx.conf: |
    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://app:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location /static {
            root /var/www;
            expires 30d;
        }
    }

  # Literal style — init script
  init.sh: |
    #!/bin/bash
    set -euo pipefail
    echo "Initializing database..."
    psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL'
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT UNIQUE
      );
    SQL
    echo "Database initialized."

---
# Pod with multiline command
apiVersion: v1
kind: Pod
metadata:
  name: debug-pod
  annotations:
    # Folded style — long description
    description: >
      This pod is used for debugging network
      connectivity issues in the cluster. It
      includes curl, nslookup, and ping tools.
spec:
  containers:
    - name: debug
      image: busybox
      # Literal strip — command without trailing newline
      command: ["sh", "-c"]
      args:
        - |-
          echo "Starting diagnostics..."
          nslookup kubernetes.default
          curl -s -o /dev/null -w "%{http_code}" http://app-service:8080/health
          echo "Diagnostics complete."

GitHub Actions

工作流步骤中的多步骤 Shell 脚本:

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup and Test
        # Literal style — multi-line shell script
        run: |
          echo "Installing dependencies..."
          npm ci
          echo "Running linter..."
          npm run lint
          echo "Running tests..."
          npm test -- --coverage
          echo "All checks passed!"

      - name: Build Docker Image
        run: |
          docker build \
            --build-arg NODE_ENV=production \
            --build-arg VERSION=${{ github.sha }} \
            -t myapp:${{ github.sha }} \
            -t myapp:latest \
            .

      - name: Set environment variables
        # Literal style — setting multi-line env vars
        run: |
          echo "DEPLOY_MESSAGE<<EOF" >> $GITHUB_ENV
          echo "Deployed commit ${{ github.sha }}" >> $GITHUB_ENV
          echo "Branch: ${{ github.ref_name }}" >> $GITHUB_ENV
          echo "Author: ${{ github.actor }}" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV

      - name: Notify
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          # Folded strip — JSON payload on one line
          payload: >-
            {
              "text": "CI failed for ${{ github.repository }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "Build failed on ${{ github.ref_name }}"
                  }
                }
              ]
            }

Ansible Playbooks

Ansible 任务中的 Shell 命令和模板:

# playbook.yml
---
- name: Deploy application
  hosts: webservers
  become: yes
  tasks:
    - name: Create deployment script
      # Literal style — shell script content
      copy:
        dest: /opt/deploy.sh
        mode: '0755'
        content: |
          #!/bin/bash
          set -euo pipefail

          APP_DIR="/opt/myapp"
          BACKUP_DIR="/opt/backups"

          echo "[$(date)] Starting deployment..."

          # Backup current version
          if [ -d "$APP_DIR" ]; then
            cp -r "$APP_DIR" "$BACKUP_DIR/$(date +%Y%m%d_%H%M%S)"
          fi

          # Pull and restart
          cd "$APP_DIR"
          git pull origin main
          docker compose down
          docker compose up -d --build

          echo "[$(date)] Deployment complete."

    - name: Configure systemd service
      copy:
        dest: /etc/systemd/system/myapp.service
        content: |
          [Unit]
          Description=My Application
          After=docker.service
          Requires=docker.service

          [Service]
          Type=oneshot
          RemainAfterExit=yes
          WorkingDirectory=/opt/myapp
          ExecStart=/usr/bin/docker compose up -d
          ExecStop=/usr/bin/docker compose down

          [Install]
          WantedBy=multi-user.target

    - name: Display deployment summary
      debug:
        # Folded style — long message
        msg: >
          Deployment completed successfully on
          {{ inventory_hostname }}. The application
          is now running on port {{ app_port }}.
          Check the logs at /var/log/myapp.log
          for any issues.

8. 流标量:纯文本、单引号、双引号

除了块标量(| 和 >),YAML 还有流标量来以不同方式处理多行字符串。为了完整性,理解这些很重要。

纯文本标量(无引号)

无引号字符串。换行符被折叠为空格(类似 > 样式)。不能以特殊字符开头(: # [ ] { } 等)。

# Plain scalar — no quotes, newlines folded to spaces
description: This is a long
  string that spans multiple
  lines in the YAML source.

# Result: "This is a long string that spans multiple lines in the YAML source."

单引号标量

用单引号包围。换行符被折叠为空格。无转义序列(\n 是字面值)。使用 '' 包含字面单引号。

# Single-quoted — no escape sequences, newlines folded
message: 'This is a long
  string with ''escaped'' single
  quotes inside it.'

# Result: "This is a long string with 'escaped' single quotes inside it."
# Note: \n is NOT interpreted — it stays as literal characters
path: 'C:\Users\name\docs'
# Result: "C:\Users\name\docs" (backslashes are literal)

双引号标量

用双引号包围。换行符被折叠为空格。支持转义序列(\n、\t、\\、\"、\uXXXX)。使用 \\ 表示字面反斜杠。

# Double-quoted — supports escape sequences
message: "Line one\nLine two\nLine three"
# Result: "Line one\nLine two\nLine three" (real newlines!)

# Newlines in source are folded to spaces
long_message: "This is a long
  string that wraps across
  lines in the source."
# Result: "This is a long string that wraps across lines in the source."

# Use \\ for literal backslash
path: "C:\\Users\\name\\docs"
# Result: "C:\Users\name\docs"

# Unicode escapes
emoji: "\u2764 \u2728"
# Result: "heart sparkles" (actual unicode characters)

流标量比较:

样式转义序列换行处理特殊字符
纯文本折叠为空格受限(开头不能用 : #)
单引号折叠为空格全部允许(用 '')
双引号折叠为空格全部允许

9. 决策流程图:何时使用哪种样式

使用此快速参考来选择正确的多行字符串样式:

Q1: 你的字符串是否包含必须保留的字面换行符?

是 -> 使用字面量样式 |

否 -> 继续下一个问题

Q2: 它是否是一个你想为了可读性而换行的长段落?

是 -> 使用折叠样式 >

否 -> 使用流标量(纯文本、单引号或双引号)

Q3: 你需要尾部换行符吗?

是 -> 使用 clip(默认)或 keep(+)

否 -> 使用 strip(-)

Q4: 你的内容是否以空格开头?

是 -> 添加显式缩进:|2 或 >2

否 -> 让 YAML 自动检测缩进

Q5: 你需要转义序列(\n、\t)吗?

是 -> 使用双引号流标量

否 -> 使用纯文本或单引号标量

快速总结:

# Shell scripts / code blocks       ->  | (literal)
# Long descriptions / paragraphs    ->  > (folded)
# No trailing newline needed         ->  add - (strip)
# Keep all trailing newlines         ->  add + (keep)
# Content starts with spaces         ->  add N (e.g., |2, >2)
# Single-line, no special chars      ->  plain scalar
# Need escape sequences (\n, \t)    ->  "double quoted"
# String with special YAML chars     ->  'single quoted' or "double"
  • Shell 脚本 / 代码块 -> |(字面量)
  • 长描述 / 段落 -> >(折叠)
  • 无特殊字符的单行 -> 纯文本标量
  • 含特殊字符的字符串 -> "双引号"
  • 不需要尾部换行 -> 添加 -(strip)

10. 常见问题

YAML 中 | 和 > 的区别是什么?

|(字面量)完全按原样保留所有换行符。>(折叠)将单个换行符替换为空格,将行合并为段落。两者默认都添加一个尾部换行符。脚本和代码使用 |;长描述使用 >。

如何移除 YAML 块标量的尾部换行符?

在样式字符后添加 strip 指示符(-):|- 或 >-。这会移除解析字符串中的所有尾部换行符。例如,"message: |-\n Hello world" 解析为 "Hello world"(无尾部 \n)。

可以在同一个 YAML 文件中混合使用 | 和 > 吗?

完全可以。每个键都可以使用不同的标量样式。例如,你可以在同一个 Kubernetes 清单或 Docker Compose 文件中对 Shell 脚本使用 | 而对描述使用 >。

|2 或 >2 中的数字是什么意思?

数字是显式缩进指示符。它告诉 YAML 解析器从每行去除多少个空格的缩进。当你的内容以空格开头时这很有用。例如,|2 表示去除 2 个空格的前导缩进。

为什么我的 YAML 多行字符串有多余的空格或换行符?

常见原因:(1) 使用 > 但实际想用 |(换行被折叠为空格)。(2) 忘记裁剪指示符(默认 clip 添加一个尾部 \n)。(3) 块内缩进不一致。(4) 块末尾有多余的空行。使用 YAML 解析器或验证器来查看字符串的精确解析方式。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

Y{}JSON ↔ YAML ConverterTYTOML ↔ YAMLYMLYAML Validator & Formatter🐳Docker Compose Generator

相关文章

YAML 语法与验证:常见错误及修复方法

掌握 YAML 语法:缩进规则、常见解析错误、数据类型和配置文件最佳实践。

YAML 锚点、别名与合并键

使用锚点(&)、别名(*)和合并键(<<)掌握 YAML DRY 原则。减少 Docker Compose、CI/CD 流水线和 Kubernetes 配置中的重复。