Jenkins Pipeline Shell 脚本编写指南

Jenkins Pipeline Shell 脚本编写指南

目录


1. 引言

1.1 适用范围

  • Jenkins Pipeline (Declarative & Scripted)
  • Linux 平台(sh 命令)
  • Windows 平台(bat 命令,原理相同)

2. 单引号与双引号的核心区别

2.1 单引号 '''...''':Shell 直接执行

特点:

  • Groovy 不处理任何变量
  • 整个字符串原封不动传递给 Shell
  • Shell 负责解析所有 ${}$ 语法

示例:

1
2
3
4
sh '''
echo "Test target: ${FINAL_TEST_TARGET}"
echo "Date: $(date +%Y-%m-%d)"
'''

执行流程:

1
Groovy → [不处理] → Shell 接收原始字符串 → Shell 解析变量

适用场景:

  • ✅ 纯 Shell 脚本逻辑
  • ✅ 使用 Shell 环境变量
  • ✅ 复杂的 Shell 语法(循环、条件、管道等)
  • ✅ 避免转义字符的困扰

2.2 双引号 """...""":Groovy 先处理

特点:

  • Groovy 先替换所有 ${...} 变量
  • 替换后的字符串才传递给 Shell
  • 需要转义 Shell 变量(\${VAR}\$(command)

示例:

1
2
3
4
sh """
echo "Branch: ${env.TARGET_BRANCH}"
echo "Build: ${BUILD_NUMBER}"
"""

执行流程:

1
Groovy → [替换变量] → Shell 接收已替换的字符串 → Shell 执行

适用场景:

  • ✅ 需要 Jenkins 构建变量(BUILD_NUMBER, BUILD_URL 等)
  • ✅ 需要环境变量(env.*
  • ✅ 需要 Pipeline 参数(params.*
  • ✅ 需要 Groovy 变量

2.3 对比示例

假设环境:

  • env.TARGET_BRANCH = "master"
  • BUILD_NUMBER = "123"
  • Shell 环境变量 TEST_VAR = "hello"

单引号示例

1
2
3
4
5
sh '''
echo "TEST_VAR: ${TEST_VAR}" # ✅ 输出: hello
echo "BUILD_NUMBER: ${BUILD_NUMBER}" # ✅ 输出: 123 (Jenkins 导出)
echo "TARGET_BRANCH: ${env.TARGET_BRANCH}" # ❌ 输出: (空,Shell 没有 env 对象)
'''

双引号示例

1
2
3
4
5
sh """
echo "TEST_VAR: ${TEST_VAR}" # ❌ 输出: (空,Groovy 没有此变量)
echo "BUILD_NUMBER: ${BUILD_NUMBER}" # ✅ 输出: 123
echo "TARGET_BRANCH: ${env.TARGET_BRANCH}" # ✅ 输出: master
"""

3. 环境变量访问规则

3.1 在 Groovy Script 块中

1
2
3
4
5
6
7
8
script {
// ✅ 正确:使用 env. 前缀
echo "Branch: ${env.TARGET_BRANCH}"
echo "Build: ${env.BUILD_NUMBER}"

// ✅ 也可以不用 env(Jenkins 全局变量)
echo "Build: ${BUILD_NUMBER}"
}

3.2 在 Shell 单引号中

1
2
3
4
5
6
7
8
sh '''
# ✅ 正确:直接使用变量名(无 env.)
echo "Branch: ${TARGET_BRANCH}"
echo "Target: ${FINAL_TEST_TARGET}"

# ❌ 错误:Shell 没有 env 对象
echo "Branch: ${env.TARGET_BRANCH}"
'''

原因: Jenkins 会自动将 env.* 变量导出到 Shell 环境中,所以可以直接访问。

3.3 在 Shell 双引号中

1
2
3
4
5
6
7
8
9
10
11
sh """
# ✅ 正确:Groovy 变量用 env.
echo "Branch: ${env.TARGET_BRANCH}"

# ✅ 正确:Shell 变量需转义
echo "Shell var: \${MY_VAR}"

# ✅ 正确:命令替换需转义
CURRENT_DATE=\$(date +%Y-%m-%d)
echo "Date: \${CURRENT_DATE}"
"""

4. 实战案例分析

4.1 案例:测试报告生成(双引号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
stage('Generate Test Results') {
steps {
script {
sh """
conda activate vltenv
python ci/scripts/generate_test_summary.py \\
--allure-dir allure-results \\
--build-number ${BUILD_NUMBER} \\
--branch ${env.TARGET_BRANCH} \\
--test-target "${env.FINAL_TEST_TARGET}"
"""
}
}
}

为什么用双引号:

  • 需要 ${BUILD_NUMBER}(Jenkins 变量)
  • 需要 ${env.TARGET_BRANCH}(环境变量)
  • 需要 ${env.FINAL_TEST_TARGET}(环境变量)

4.2 案例:路径验证(单引号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
stage('Validate Test Configuration') {
steps {
script {
sh '''
if [ ! -e "${FINAL_TEST_TARGET}" ]; then
echo "ERROR: Test target path does not exist"
exit 1
else
echo "✓ Test target path validation passed"
fi
'''
}
}
}

为什么用单引号:

  • 使用 Shell 环境变量 ${FINAL_TEST_TARGET}
  • 纯 Shell 条件判断逻辑
  • 避免转义字符的麻烦

4.3 案例:混合使用(双引号 + 转义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
stage('Run Tests') {
steps {
script {
sh """
# Jenkins 变量(不转义)
echo "Building branch: ${env.TARGET_BRANCH}"

# Shell 变量(转义)
for file in \$(find ${env.FINAL_TEST_TARGET} -name "*.py"); do
echo "Testing: \${file}"
pytest \${file} --build-id ${BUILD_NUMBER}
done
"""
}
}
}

关键点:

  • Jenkins 变量:${env.TARGET_BRANCH} 不转义
  • Shell 变量:\${file} 需转义
  • Shell 命令:\$(find ...) 需转义

4.4 案例:延迟设置构建状态

问题场景: 测试失败时,后续的报告生成和发送阶段被跳过。

原因: 在测试阶段立即设置 currentBuild.result = 'UNSTABLE',触发了 skipStagesAfterUnstable() 选项。

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
stage('Run Unit Tests') {
steps {
script {
def testResult = 'SUCCESS'
try {
sh """
pytest ${env.FINAL_TEST_TARGET}
"""
testResult = 'SUCCESS'
} catch (Exception e) {
testResult = 'UNSTABLE'
echo "⚠️ Will continue to generate and send test reports..."
}

// 保存结果,不立即设置构建状态
env.TEST_EXECUTION_RESULT = testResult
}
}
}

stage('Send Test Results') {
steps {
script {
def finalStatus = env.TEST_EXECUTION_RESULT ?: 'SUCCESS'

sh """
python ci/scripts/send_test_results.py \\
--build-status ${finalStatus}
"""

// 发送通知后再设置构建状态
if (finalStatus == 'UNSTABLE') {
currentBuild.result = 'UNSTABLE'
}
}
}
}

options {
// 移除此选项,允许 UNSTABLE 后继续执行
// skipStagesAfterUnstable()
}

关键改进:

  1. 不立即设置 currentBuild.result
  2. 使用环境变量 env.TEST_EXECUTION_RESULT 保存状态
  3. 移除 skipStagesAfterUnstable() 选项
  4. 在发送通知后再设置构建状态

5. 常见陷阱与解决方案

5.1 陷阱:双引号中使用 Shell 变量

错误写法:

1
2
3
4
sh """
MY_VAR="test"
echo ${MY_VAR} # Groovy 找不到 MY_VAR,输出为空
"""

正确写法:

1
2
3
4
sh """
MY_VAR="test"
echo \${MY_VAR} # 转义后,Shell 处理
"""

5.2 陷阱:单引号中使用 Jenkins 变量

错误写法:

1
2
3
sh '''
echo ${env.TARGET_BRANCH} # Shell 找不到 env 对象
'''

解决方案 A:改用双引号

1
2
3
sh """
echo ${env.TARGET_BRANCH} # Groovy 替换
"""

解决方案 B:先设置环境变量

1
2
3
4
5
6
script {
env.MY_BRANCH = env.TARGET_BRANCH
}
sh '''
echo ${MY_BRANCH} # Shell 使用环境变量
'''

5.3 陷阱:Windows 路径中的反斜杠

错误写法:

1
2
3
bat """
cd C:\Users\test # \U 和 \t 被 Groovy 解析为转义字符
"""

解决方案 A:使用单引号

1
2
3
bat '''
cd C:\Users\test # 单引号不处理转义
'''

解决方案 B:双反斜杠

1
2
3
bat """
cd C:\\Users\\test # 双反斜杠转义
"""

5.4 陷阱:命令替换被 Groovy 处理

错误写法:

1
2
3
sh """
CURRENT_TIME=$(date +%H:%M:%S) # Groovy 尝试执行 $(date)
"""

正确写法:

1
2
3
sh """
CURRENT_TIME=\$(date +%H:%M:%S) # 转义,让 Shell 处理
"""

5.5 陷阱:测试失败后跳过报告发送

错误配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
stage('Run Tests') {
steps {
script {
try {
sh "pytest"
} catch (Exception e) {
currentBuild.result = 'UNSTABLE' // 立即设置状态
}
}
}
}

options {
skipStagesAfterUnstable() // 导致后续阶段被跳过
}

正确配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
stage('Run Tests') {
steps {
script {
try {
sh "pytest"
env.TEST_RESULT = 'SUCCESS'
} catch (Exception e) {
env.TEST_RESULT = 'UNSTABLE' // 只记录,不设置
}
}
}
}

stage('Send Results') {
steps {
script {
sh "send_notification.py --status ${env.TEST_RESULT}"
// 发送后再设置构建状态
if (env.TEST_RESULT == 'UNSTABLE') {
currentBuild.result = 'UNSTABLE'
}
}
}
}

options {
// 移除 skipStagesAfterUnstable()
}

6. 最佳实践

6.1 选择原则

场景 推荐引号 原因
需要 Jenkins 变量 双引号 """ 最常见,约 60-70%
纯 Shell 命令 单引号 ''' 避免转义麻烦
两种变量都需要 双引号 + 转义 最灵活
不确定 双引号 更安全,出错容易发现

6.2 推荐模式

模式 1:默认使用双引号

1
2
3
4
sh """
echo "Branch: ${env.TARGET_BRANCH}"
python test.py --build-id ${BUILD_NUMBER}
"""

优点:可以使用所有 Jenkins 变量,代码更清晰


模式 2:简单命令用单引号

1
2
3
4
5
sh '''
rm -rf build/
mkdir -p dist/
ls -la
'''

优点:不需要转义,适合纯 Shell 命令


模式 3:复杂脚本写入文件

1
2
3
4
5
6
7
8
9
10
11
12
writeFile file: 'build.sh', text: '''
#!/bin/bash
set -e

# 复杂的 Shell 脚本
for i in {1..10}; do
echo "Processing batch ${i}"
./process.sh ${i}
done
'''

sh 'chmod +x build.sh && ./build.sh'

优点:

  • 避免引号和转义问题
  • 脚本可以独立测试
  • 代码更易维护

模式 4:环境变量传递(重要)

1
2
3
4
5
6
7
8
9
10
11
script {
// Groovy 层设置环境变量
env.TEST_DIR = env.FINAL_TEST_TARGET
env.BUILD_ID = BUILD_NUMBER
}

sh '''
# Shell 层直接使用
echo "Testing directory: ${TEST_DIR}"
pytest ${TEST_DIR} --id ${BUILD_ID}
'''

优点:

  • 分离变量定义和使用
  • Shell 脚本更简洁

6.3 编码规范

1. 变量命名

1
2
3
// 好的实践
env.FINAL_TEST_TARGET = testTarget
env.TEST_EXECUTION_RESULT = testResult

2. 长命令格式化

Linux(反斜杠续行):

1
2
3
4
5
6
sh """
python ci/scripts/send_results.py \\
--build-number ${BUILD_NUMBER} \\
--branch ${env.TARGET_BRANCH} \\
--recipients "${params.NOTIFICATION_RECIPIENTS}"
"""

Windows(^ 续行):

1
2
3
4
5
6
bat """
python ci/scripts/send_results.py ^
--build-number ${BUILD_NUMBER} ^
--branch ${env.TARGET_BRANCH} ^
--recipients "${params.NOTIFICATION_RECIPIENTS}"
"""

3. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
script {
try {
sh """
python ci/scripts/generate_report.py \\
--output-dir test-results
"""

if (fileExists('test-results/report.json')) {
echo "✓ Report generated successfully"
env.REPORT_AVAILABLE = 'true'
} else {
echo "⚠ Report file not found"
env.REPORT_AVAILABLE = 'false'
}

} catch (Exception e) {
echo "Failed to generate report: ${e.message}"
env.REPORT_AVAILABLE = 'false'
}
}

4. 条件执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
stage('Send Results') {
when {
expression {
params.SEND_RESULTS == true &&
env.REPORT_AVAILABLE == 'true'
}
}
steps {
sh """
python ci/scripts/send_results.py \\
--report test-results/report.json
"""
}
}

6.4 性能优化

避免重复激活环境

低效写法:

1
2
3
sh "conda activate myenv && python script1.py"
sh "conda activate myenv && python script2.py"
sh "conda activate myenv && python script3.py"

高效写法:

1
2
3
4
5
6
sh """
conda activate myenv
python script1.py
python script2.py
python script3.py
"""

合并相关命令

低效写法:

1
2
3
4
sh "mkdir -p build"
sh "cd build"
sh "cmake .."
sh "make"

高效写法:

1
2
3
4
5
6
sh """
mkdir -p build
cd build
cmake ..
make
"""

7. 决策流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
开始编写 sh/bat 脚本

问:是否需要 Jenkins 变量?
(BUILD_NUMBER, env.*, params.* 等)

┌───┴───┐
↓ ↓
是 否
↓ ↓
使用双引号 问:是否有复杂 Shell 语法?
""" (循环、条件、管道等)
↓ ↓
│ ┌───┴───┐
│ ↓ ↓
│ 是 否
│ ↓ ↓
│ 使用单引号 随意选择
│ ''' (推荐双引号)
│ ↓ ↓
└───┴───────┘

完成编写

8. 快速参考表

8.1 变量访问速查

上下文 Jenkins 变量 Shell 变量 示例
Groovy script ${env.VAR} ❌ 不可用 echo "${env.TARGET_BRANCH}"
Shell 单引号 ❌ 不可用 ${VAR} echo "${FINAL_TEST_TARGET}"
Shell 双引号 ${env.VAR} \${VAR} echo "${env.BRANCH}" \${MY_VAR}

8.2 转义速查

场景 双引号中的写法 说明
Jenkins 变量 ${env.VAR} 不转义
Shell 变量 \${VAR} 转义 $
命令替换 \$(command) 转义 $
反斜杠(Windows) C:\\Users 双反斜杠
换行续行(Linux) \\ 双反斜杠
换行续行(Windows) ^ Windows 专用

8.3 常用命令模板

Linux 平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 激活 Conda 环境并执行
sh """
eval "\$(conda shell.bash hook 2>/dev/null)" > /dev/null
conda activate myenv
python script.py
"""

// 条件判断
sh '''
if [ -f "file.txt" ]; then
echo "File exists"
fi
'''

// 循环处理
sh '''
for file in $(find . -name "*.py"); do
echo "Processing: ${file}"
done
'''

Windows 平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 激活 Conda 环境并执行
bat """
@echo off
chcp 65001 > nul
call conda activate myenv
python script.py
"""

// 条件判断
bat '''
@echo off
if exist file.txt (
echo File exists
)
'''

// 设置编码
bat """
@echo off
chcp 65001 > nul
set PYTHONIOENCODING=utf-8
python script.py
"""

9. 总结

核心要点

  1. 单引号 = Shell 直接执行,适合纯 Shell 脚本
  2. 双引号 = Groovy 先处理,适合需要 Jenkins 变量
  3. 实际项目中,双引号使用频率更高(约 60-70%)
  4. 不确定时,优先选择双引号
  5. 复杂脚本,考虑写入文件或使用环境变量传递

记忆口诀

1
2
3
单引号 → Shell 直接执行,Groovy 不插手
双引号 → Groovy 先替换,Shell 后执行
需转义 → 双引号里加 \,Shell 变量留

延伸阅读

适用 Jenkins 版本: 2.x 及以上