1. 理解 “bad substitution” 错误
Jenkins 是 CI/CD 流水线中的核心工具,但在使用过程中,我们可能会遇到一个常见的错误:“Pipeline sh bad substitution”。
这个错误通常出现在 Jenkins Pipeline 的 sh
步骤中,原因在于 Shell 脚本中变量的使用方式与 Jenkins 的解析机制不匹配。
例如下面这段代码:
sh 'curl -v --user user:password --data-binary ${buildDir}package${env.BUILD_NUMBER}.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package${env.BUILD_NUMBER}.tar"'
其中使用了 ${env.BUILD_NUMBER}
来动态插入构建编号,但若使用不当,就会触发 Bad substitution
错误。
常见原因包括:
✅ 使用了错误的引号(如单引号中嵌套变量)
✅ Shell 类型不一致(默认是 sh
,不支持某些 bash
特性)
✅ 变量作用域或语法错误
这个错误会导致当前阶段失败,进而中断整个流水线,影响自动化流程。
2. 实际案例分析
假设我们有一个 Jenkins Pipeline,用于将 .tar
包上传到 Artifactory,并使用 env.BUILD_NUMBER
动态命名文件:
pipeline {
agent any
environment {
buildDir = "/var/lib/jenkins/workspace/Package_Deploy_Pipeline/"
}
stages {
stage('Upload') {
steps {
sh 'curl -v --user user:password --data-binary ${buildDir}package${env.BUILD_NUMBER}.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package${env.BUILD_NUMBER}.tar"'
}
}
}
}
运行时 Jenkins 报错如下:
[Pipeline] sh
[Package_Deploy_Pipeline] Running shell script
/var/lib/jenkins/workspace/Package_Deploy_Pipeline@tmp/durable-4c8b7958/script.sh:
2: /var/lib/jenkins/workspace/Package_Deploy_Pipeline@tmp/durable-4c8b7958/script.sh:
Bad substitution
但如果我们把变量替换成固定值,比如:
sh 'curl -v --user user:password --data-binary ${buildDir}package113.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package113.tar"'
脚本就能正常运行。这说明问题出在变量插值的写法上。
3. 定位问题
我们可以先简化代码来复现错误:
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'echo ${env.BUILD_NUMBER}'
}
}
}
}
运行后仍然报错,说明问题出在 Shell 变量插值方式。
根本原因在于:
❌ 单引号 '
不会进行变量替换
✅ 双引号 "
会进行变量插值
⚠️ Groovy 与 Shell 变量嵌套时容易混淆
4. 解决方案一:使用双引号(Double Quotes)
将 sh
步骤的命令包裹在双引号中,使 Jenkins 正确解析变量:
sh "curl -v --user user:password
--data-binary ${buildDir}package${env.BUILD_NUMBER}.tar -X
PUT \"http://artifactory.mydomain.com/artifactory/release-packages/package${env.BUILD_NUMBER}.tar\""
优点:
✅ 简单直观
✅ 适用于简单命令
缺点:
❌ 需要转义内部的双引号
❌ 命令复杂时可读性差
5. 解决方案二:使用三重引号(Triple Double Quotes)
Groovy 支持三重引号字符串插值,适合处理多行命令和嵌套引号:
sh """curl -v --user user:password
--data-binary ${buildDir}package${env.BUILD_NUMBER}.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package${env.BUILD_NUMBER}.tar"""
优点:
✅ 更易读
✅ 不需要对内部双引号做转义
✅ 适合长命令
缺点:
❌ 语法稍复杂,初学者可能不熟悉
6. 解决方案三:指定 Bash Shell
默认使用的是 sh
,不支持某些 Bash 语法。可以通过指定 Shebang 来使用 Bash:
sh '''#!/bin/bash -xe
curl -v --user user:password
--data-binary ${buildDir}package${env.BUILD_NUMBER}.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package${env.BUILD_NUMBER}.tar"'''
优点:
✅ 支持更丰富的 Bash 语法
✅ 增强调试能力(-xe
)
缺点:
❌ 需要了解 Shebang 和 Bash 基础
❌ 增加了脚本复杂度
7. 解决方案四:使用 withEnv
设置环境变量
将 Jenkins 变量显式导出为环境变量,避免直接使用 env.xxx
:
withEnv(["BUILD_NUMBER=${env.BUILD_NUMBER}"]) {
sh '''#!/bin/bash -xe
curl -v --user user:password
--data-binary ${buildDir}package${BUILD_NUMBER}.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package${BUILD_NUMBER}.tar"'''
}
优点:
✅ 更清晰地控制变量作用域
✅ 降低变量插值错误风险
✅ 可组合使用其他技巧
缺点:
❌ 语法略复杂,需多写一层结构
8. 综合方案:多种技巧结合使用
将 withEnv
+ 三重引号 + Bash 指定结合使用,提高健壮性:
withEnv(["BUILD_NUMBER=${env.BUILD_NUMBER}"]) {
sh """#!/bin/bash -xe
curl -v --user user:password
--data-binary ${buildDir}package${BUILD_NUMBER}.tar -X
PUT "http://artifactory.mydomain.com/artifactory/release-packages/package${BUILD_NUMBER}.tar"""
}
这种写法兼顾:
✅ 可读性
✅ 可维护性
✅ 可调试性
9. 最佳实践总结
为避免 Jenkins Pipeline 中出现 bad substitution
错误,建议遵循以下最佳实践:
✅ 始终使用双引号或三重引号包裹 sh
命令
✅ 显式使用 withEnv
导出变量
✅ 指定 #!/bin/bash -xe
以启用 Bash 和调试模式
✅ 分段构建命令,逐步测试
✅ 避免在单引号中使用变量插值
✅ 了解 Jenkins 和 Shell 的变量作用域差异
10. 总结
本文详细分析了 Jenkins Pipeline 中常见的 sh bad substitution
错误的原因,并提供了多种解决方案。通过使用双引号、三重引号、显式 Bash、withEnv
等技巧,我们可以有效避免此类问题,提高流水线的稳定性。
关键点总结如下:
- ❌ 单引号不会进行变量插值
- ✅ 双引号和三重引号支持插值
- ✅ 推荐使用 Bash 而不是默认的
sh
- ✅ 使用
withEnv
显式导出变量,避免作用域问题 - ✅ 组合使用多种技巧,提升脚本健壮性
掌握这些技巧后,你将能更自信地编写 Jenkins Pipeline,避免踩坑。