在Jenkins Pipelin构建过程中,一个初学令人头疼的问题:在一个关键的部署步骤中,本应动态传递的参数和变量竟然没有被正确解析,导致构建失败。具体来说,负责部署的同事在 sh 步骤中错误地使用了单引号来包裹命令,导致 Groovy 内插失效,最终生成的脚本中依旧保留了类似${params.MY_PARAM} 的文本,而非预期的实际值。错误的结果不仅让调试过程变得十分困难,也影响了后续环境中依赖这些正确变量的执行流程。
日志显示的错误也让人摸不着头脑,如下
/data00/svn2git_trunk@tmp/durable-90494516/script.sh.copy: 3:
/data00/svn2git_trunk@tmp/durable-90494516/script.sh.copy: Bad substitution
问题暴露了在 Jenkins Pipeline 脚本中使用不同类型变量(如 parameters、environment、def)和引号(单引号、双引号、三单引号、三双引号)时存在的重要区别。如果混淆了这些用法,不仅会让变量值丢失,还可能导致整个流水线逻辑无法正确执行。
接下来的内容将详细介绍如何在 Jenkins Pipeline 脚本中正确使用这三种变量定义方式,以及如何根据场景选择合适的字符串引号,以避免类似的错误再次发生,让你的流水线脚本既清晰又健壮。
1. 三种变量定义方式
在 Jenkins Pipeline 中,我们常用以下三种方式来定义和使用变量,各自特点如下:
-
parameters
- 定义方式:在流水线最开始的
parameters块中定义,通常用于用户输入参数。 - 作用域:全局,整个 Pipeline 内均可访问。
- 生命周期:从构建启动开始,到构建结束为止。
- 引用方式:通过
params.变量名访问。例如:${params.MY_PARAM}。
- 定义方式:在流水线最开始的
-
environment
- 定义方式:在 Pipeline 或 stage 的
environment块中定义。 - 作用域:全局(在所有 stage 中都可用),并且其值会自动注入到调用
sh、bat等步骤的执行环境。 - 生命周期:在构建期间都存在。
- 引用方式:可以通过
env.变量名访问,或者在 shell 脚本中直接用$变量名(因 Jenkins 会将其注入到环境变量中)。
- 定义方式:在 Pipeline 或 stage 的
-
def 变量
- 定义方式:在 Groovy 脚本部分使用
def或直接声明变量。 - 作用域:局部,仅在定义所在的代码块或 stage 内有效。
- 生命周期:仅在当前脚本块或方法内,构建结束后便不再存在。
- 引用方式:直接使用变量名(例如:
${myVar})进行 Groovy 插值。
- 定义方式:在 Groovy 脚本部分使用
下面的表格总结了这三种变量定义方式的基本特性:
| 变量类型 | 定义方式 | 作用域 | 生命周期 | 引用方式 |
|---|---|---|---|---|
| parameters | parameters { ... } | 整个 Pipeline(全局) | 构建开始至构建结束 | params.变量名 |
| environment | environment { ... } | 整个 Pipeline(所有 stage 均有) | 构建期间 | env.变量名 或 $变量名 (shell环境) |
| def 变量 | def varName = "value" | 当前脚本块或 stage(局部) | 当前代码块内(临时变量) | 直接使用变量名 |
2. 在 sh 步骤中使用变量时的注意点
在 Pipeline 的 sh 步骤中,由于要执行 shell 脚本,需注意两个方面:
-
Groovy 内插与 Shell 变量扩展的区别
- Groovy 内插:在 Groovy 字符串中,如果使用双引号或三双引号,表达式
${...}会在执行前由 Groovy 解析并替换为对应值。 - Shell 变量扩展:如果传递给
sh步骤的命令包含$MY_ENV(注意前缀$),那么在执行时由 Shell 进行环境变量替换。
- Groovy 内插:在 Groovy 字符串中,如果使用双引号或三双引号,表达式
-
如何传递不同作用域的变量
- 使用
parameters与def定义的变量,只有在 Groovy 端能获取,所以若需要在 Shell 脚本中使用其值,就需要通过 Groovy 内插传递。 - 使用
environment定义的变量,Jenkins 会自动注入到执行环境中,因此可以直接在 Shell 脚本中通过$MY_ENV访问,或者通过 Groovy 内插${env.MY_ENV}。
- 使用
示例:
groovy
复制
pipeline {
agent any
parameters {
string(name: 'MY_PARAM', defaultValue: 'Hello', description: '示例参数')
}
environment {
MY_ENV = "World"
}
stages {
stage('示例') {
steps {
script {
def myVar = "Jenkins"
// 如果使用三重双引号 """...""",Groovy 会提前解析内插变量
sh """
echo 参数: ${params.MY_PARAM}
echo 环境变量: ${env.MY_ENV} # Groovy 内插,等效于 shell 中的 $MY_ENV
echo 局部变量: ${myVar}
"""
// 或者直接利用 shell 的环境变量扩展
sh '''
echo 参数: $MY_PARAM # 注意:如果参数没有注入到环境中,不会生效!
echo 环境变量: $MY_ENV
'''
}
}
}
}
}
说明:
- 在第一个
sh步骤中使用了三重双引号 (""" ... """),这是 Groovy 的多行 GString,会对${...}内的表达式进行内插。- 在第二个
sh步骤中使用了三单引号 (''' ... '''),该字符串为纯文本,不进行内插,适合直接传递给 shell,但此时参数(如params.MY_PARAM和局部变量myVar)不会被替换,只有环境变量(若 Jenkins 已注入到子进程环境中)可以使用$MY_ENV的方式扩展。
3. 不同引号在变量内插时的异同
Groovy 中支持多种引号形式,不同的引号对变量内插(插值)的处理不同。在 Jenkins Pipeline 中常用的有四种:
| 引号类型 | 是否进行 Groovy 内插 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
单引号 '...' | 否 | 需要传递纯文本,不希望变量被替换 | 无额外内插,书写简单 | 无法嵌入变量,若需要动态生成命令则不方便 |
双引号 "..." | 是 (GString) | 单行命令或简单变量替换场景 | 可直接内插变量,语法灵活 | 需要注意变量中可能包含特殊字符需适当转义 |
三单引号 '''...''' | 否 | 多行文本(例如复杂 Shell 脚本),不需要内插 | 保持多行书写格式,原样输出 | 不能内插变量,如需要动态变量则得拼接字符串 |
三双引号 """...""" | 是 | 多行复杂脚本中需要内插 Groovy 变量 | 既保留多行格式又支持变量内插 | 内插可能无意中混入不想替换的文本,需注意边界 |
总结建议:
- 如果你的 shell 脚本中需要引用 Groovy 的动态值(例如来自
params或def的变量),请使用双引号或三双引号。- 如果脚本内容基本固定,不需要 Groovy 内插,可以使用单引号或三单引号,从而降低因内插带来的意外替换风险。
- 对于环境变量,由于 Jenkins 会自动注入到 shell 执行环境中,可以直接在纯文本中使用
$MY_ENV,不必通过 Groovy 内插。
4. 总结归纳
变量定义与引用
-
parameters
- 用于构建参数,引用为
params.变量名 - 全局生效,生命周期覆盖整个构建
- 用于构建参数,引用为
-
environment
- 用于定义环境变量,引用为
env.变量名或直接在 shell 中使用$变量名 - 全局环境中可用,自动注入到子进程中
- 用于定义环境变量,引用为
-
def 变量
- 用于局部 Groovy 脚本内部变量,直接定义和引用
- 作用域有限,仅在声明所在块内有效
引号选择
- 单引号 / 三单引号:原样输出(无内插),适合传递固定文本。
- 双引号 / 三双引号:支持内插(GString),适合需要动态注入 Groovy 变量的场景。
下表归纳了两部分内容:
变量类型比较
| 变量类型 | 定义方式 | 作用域 | 生命周期 | 使用示例 |
|---|---|---|---|---|
| parameters | parameters { ... } | 全局(整个 Pipeline) | 构建开始至构建结束 | ${params.MY_PARAM} |
| environment | environment { ... } | 全局(所有 stage 均可用) | 构建期间 | ${env.MY_ENV} 或 $MY_ENV |
| def 变量 | def myVar = "value" | 局部(当前脚本块或 stage) | 当前代码块内 | ${myVar} |
引号选择比较
| 引号类型 | 是否内插变量 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
单引号 '...' | 否 | 需要传递原始文本 | 无内插风险,简单直接 | 无法嵌入 Groovy 变量 |
双引号 "..." | 是 | 单行命令中需要动态传值 | 适合变量内插,灵活 | 字符串中的特殊符号需注意转义 |
三单引号 '''...''' | 否 | 多行文本,不需要内插 | 保持原格式,多行书写方便 | 不支持变量内插 |
三双引号 """...""" | 是 | 多行脚本需要内插动态值 | 同时保留多行格式和内插能力 | 变量内插时需注意不当替换 |
常见坑点
sh 步骤中使用三重双引号(即 GString)时,所有形如 ${...} 或者 $变量名 的部分都会被 Groovy 在执行前进行内插。代码中的:
sh """#!/bin/bash -xe
echo ${params.USER_HOME}
export TMP_DIR=${params.USER_HOME}
echo $TMP_DIR
"""
出错原因就是由于 echo $TMP_DIR 部分,Groovy 会把 $TMP_DIR 解释为一个 Groovy 变量,但此变量在 Groovy 环境中并不存在,从而导致错误:
No such property: TMP_DIR for class: groovy.lang.Binding
如果你想要让 $TMP_DIR 保持原样传递给 Shell,则需要对美元符号进行转义,告诉 Groovy 这不是一个 Groovy 变量,而是应该原样输出。修改代码如下:
sh """#!/bin/bash -xe
echo ${params.USER_HOME}
export TMP_DIR=${params.USER_HOME}
echo \\$TMP_DIR
"""
在这里,echo $TMP_DIR 中的 $TMP_DIR 会被转换成文字 $TMP_DIR,从而交由 Shell 进行解析。
env中定义的环境变量,特别注意,不要与Jenkins内置或者插件定义的变量同名,例如env.GIT_URL和env.GIT_BRANCH 在使用checkout后会被修改成 checkout所使用的变量。
结论
- 在 Jenkins Pipeline 的
sh步骤中,如果需要在命令中动态引用 parameters 或 def 定义的值,务必使用支持内插的双引号(或三双引号); - 如果仅需要调用环境变量(通过
$来实现 shell 内的变量扩展),则可以选择使用不进行内插的单引号(或三单引号),以减少 Groovy 侧的干预; - 根据代码的复杂程度以及对内插的需求,合理选择引号可以使你的 Pipeline 既安全又易于维护。