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,避免踩坑。


原始标题:Fixing Jenkins “Pipeline sh bad substitution” Error