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 行为)。
# 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 | 保留 | 添加单个 \n | Shell 脚本、代码 |
| |- | 字面量 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"# 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 +)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 解析器或验证器来查看字符串的精确解析方式。