Jenkins Pipeline脚本中参数与变量的避坑指北

150 阅读5分钟

在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 脚本中使用不同类型变量(如 parametersenvironmentdef)和引号(单引号、双引号、三单引号、三双引号)时存在的重要区别。如果混淆了这些用法,不仅会让变量值丢失,还可能导致整个流水线逻辑无法正确执行。

接下来的内容将详细介绍如何在 Jenkins Pipeline 脚本中正确使用这三种变量定义方式,以及如何根据场景选择合适的字符串引号,以避免类似的错误再次发生,让你的流水线脚本既清晰又健壮。

1. 三种变量定义方式

在 Jenkins Pipeline 中,我们常用以下三种方式来定义和使用变量,各自特点如下:

  • parameters

    • 定义方式:在流水线最开始的 parameters 块中定义,通常用于用户输入参数。
    • 作用域:全局,整个 Pipeline 内均可访问。
    • 生命周期:从构建启动开始,到构建结束为止。
    • 引用方式:通过 params.变量名 访问。例如:${params.MY_PARAM}
  • environment

    • 定义方式:在 Pipeline 或 stage 的 environment 块中定义。
    • 作用域:全局(在所有 stage 中都可用),并且其值会自动注入到调用 shbat 等步骤的执行环境。
    • 生命周期:在构建期间都存在。
    • 引用方式:可以通过 env.变量名 访问,或者在 shell 脚本中直接用 $变量名(因 Jenkins 会将其注入到环境变量中)。
  • def 变量

    • 定义方式:在 Groovy 脚本部分使用 def 或直接声明变量。
    • 作用域:局部,仅在定义所在的代码块或 stage 内有效。
    • 生命周期:仅在当前脚本块或方法内,构建结束后便不再存在。
    • 引用方式:直接使用变量名(例如:${myVar})进行 Groovy 插值。

下面的表格总结了这三种变量定义方式的基本特性:

变量类型定义方式作用域生命周期引用方式
parametersparameters { ... }整个 Pipeline(全局)构建开始至构建结束params.变量名
environmentenvironment { ... }整个 Pipeline(所有 stage 均有)构建期间env.变量名$变量名 (shell环境)
def 变量def varName = "value"当前脚本块或 stage(局部)当前代码块内(临时变量)直接使用变量名

2. 在 sh 步骤中使用变量时的注意点

在 Pipeline 的 sh 步骤中,由于要执行 shell 脚本,需注意两个方面:

  1. Groovy 内插与 Shell 变量扩展的区别

    • Groovy 内插:在 Groovy 字符串中,如果使用双引号或三双引号,表达式 ${...} 会在执行前由 Groovy 解析并替换为对应值。
    • Shell 变量扩展:如果传递给 sh 步骤的命令包含 $MY_ENV(注意前缀 $),那么在执行时由 Shell 进行环境变量替换。
  2. 如何传递不同作用域的变量

    • 使用 parametersdef 定义的变量,只有在 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 的动态值(例如来自 paramsdef 的变量),请使用双引号或三双引号。
  • 如果脚本内容基本固定,不需要 Groovy 内插,可以使用单引号或三单引号,从而降低因内插带来的意外替换风险。
  • 对于环境变量,由于 Jenkins 会自动注入到 shell 执行环境中,可以直接在纯文本中使用 $MY_ENV,不必通过 Groovy 内插。

4. 总结归纳

变量定义与引用

  • parameters

    • 用于构建参数,引用为 params.变量名
    • 全局生效,生命周期覆盖整个构建
  • environment

    • 用于定义环境变量,引用为 env.变量名 或直接在 shell 中使用 $变量名
    • 全局环境中可用,自动注入到子进程中
  • def 变量

    • 用于局部 Groovy 脚本内部变量,直接定义和引用
    • 作用域有限,仅在声明所在块内有效

引号选择

  • 单引号 / 三单引号:原样输出(无内插),适合传递固定文本。
  • 双引号 / 三双引号:支持内插(GString),适合需要动态注入 Groovy 变量的场景。

下表归纳了两部分内容:

变量类型比较

变量类型定义方式作用域生命周期使用示例
parametersparameters { ... }全局(整个 Pipeline)构建开始至构建结束${params.MY_PARAM}
environmentenvironment { ... }全局(所有 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_URLenv.GIT_BRANCH 在使用checkout后会被修改成 checkout所使用的变量。


结论

  • 在 Jenkins Pipeline 的 sh 步骤中,如果需要在命令中动态引用 parameters 或 def 定义的值,务必使用支持内插的双引号(或三双引号);
  • 如果仅需要调用环境变量(通过 $ 来实现 shell 内的变量扩展),则可以选择使用不进行内插的单引号(或三单引号),以减少 Groovy 侧的干预;
  • 根据代码的复杂程度以及对内插的需求,合理选择引号可以使你的 Pipeline 既安全又易于维护。