在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 既安全又易于维护。