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 { echo "Branch: ${env.TARGET_BRANCH}" echo "Build: ${env.BUILD_NUMBER}" 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 { }
关键改进:
不立即设置 currentBuild.result
使用环境变量 env.TEST_EXECUTION_RESULT 保存状态
移除 skipStagesAfterUnstable() 选项
在发送通知后再设置构建状态
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 { }
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 { 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} '''
优点:
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 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 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. 总结 核心要点
单引号 = Shell 直接执行,适合纯 Shell 脚本
双引号 = Groovy 先处理,适合需要 Jenkins 变量
实际项目中 ,双引号使用频率更高(约 60-70%)
不确定时 ,优先选择双引号
复杂脚本 ,考虑写入文件或使用环境变量传递
记忆口诀 1 2 3 单引号 → Shell 直接执行,Groovy 不插手 双引号 → Groovy 先替换,Shell 后执行 需转义 → 双引号里加 \,Shell 变量留
延伸阅读
适用 Jenkins 版本: 2.x 及以上