3. Groovy 脚本语言基础:变量、闭包、列表、映射、文件操作,在 Nextflow 中的常见用法

说实话,Nextflow 的管道脚本看起来像是一套专门的 DSL(领域特定语言),但它的内核就是 Groovy。很多初学者以为只需要会写 process 和 channel 就够了,结果一碰到复杂逻辑就卡壳。我刚开始接触 Nextflow 时也踩过这坑——想当然地写了一段逻辑,结果变量作用域没搞明白,调试了一下午。所以这一章,咱们把 Groovy 在 Nextflow 里最常用的几个点过一遍。

3.1 变量声明与作用域

Groovy 里的变量声明比 Java 灵活得多。我个人习惯在 Nextflow 脚本里尽量用 def 关键字——它明确告诉读者“这是个局部变量”,不容易跟 Nextflow 的保留变量混在一起。

// 普通变量
def sampleId = "SRR123456"
def readCount = 25000000

// 字符串插值 - 超好用
def fastqFile = "${sampleId}.fastq.gz"
println "处理样本: ${sampleId}"

作用域问题一定要注意。Nextflow 里,你在 workflow 块外部定义的变量会被当作全局变量,但 process 内部只能访问通过 input: 和 output: 传入的数据。我见过有人把全局变量直接用在 process 脚本里,结果并行执行时数据全乱了。

避坑:我曾在多线程场景下,因为全局变量被多个 process 同时读写,导致样本 ID 串了。记住:Nextflow 的并行化是基于 channel 的,不是靠共享内存。

3.2 闭包——Nextflow 管道的灵魂

闭包(Closure)说白了就是一段可传递的代码块。在 Nextflow 里,你几乎每天都在用闭包,只是没意识到。

比如 channel 的 map 操作:

Channel.fromPath("data/*.fastq")
    | map { file -> 
        def sampleName = file.baseName - "_R1" - "_R2"
        tuple(sampleName, file)
    }

这里的 { file -> ... } 就是一个闭包。Groovy 里闭包可以用 it 隐式参数简化——我个人不太推荐这种做法,可读性会下降。明确定义参数名,几天后回头看代码时不用猜。

经验之谈:闭包经常配合集合方法使用,比如 .collect()、.findAll()。我在做样本分组时,常用闭包来筛选和转换数据,比自己写 for 循环干净得多。
// 筛选出大于 100MB 的 BAM 文件
def largeFiles = allBamFiles.findAll { f -> f.size() > 100_000_000 }
largeFiles.each { f -> println "大文件: ${f.name}" }

3.3 列表与映射——数据流的容器

列表(List)和映射(Map)是 Nextflow 脚本中最常用的数据结构。前者用于顺序集合,后者用于键值对。

列表操作:

def samples = ["S1", "S2", "S3", "S4"]

// 遍历
samples.each { println it }

// 索引访问
println samples[0]     // S1
println samples[-1]    // S4(负索引从尾部开始)

// 连接
def pairs = samples.collate(2)  // [[S1,S2], [S3,S4]]

映射操作:

def metadata = [
    sample: "S1",
    platform: "Illumina",
    readLength: 150
]

metadata["library"] = "TruSeq"
println metadata.sample   // 也可以点号访问

// 常见场景:用映射传参数
process RUN_ALIGNMENT {
    input:
    val meta
    path fastq

    script:
    """
    bwa mem -R '@RG\\tID:${meta.sample}' ref.fa $fastq > ${meta.sample}.sam
    """
}

为什么特别强调列表和映射?因为在 Nextflow 里,channel 中的元数据经常以 tuple 的形式传递,而 tuple 本质上就是固定长度的列表。我建议你习惯用一个 map 来存样本的所有属性,然后把这个 map 跟着文件一起传——这样后面任意一个 process 都能拿到它需要的字段。

3.4 文件操作——别被路径坑了

Nextflow 天然支持 path 类型,但 Groovy 自身的文件操作在配置和预处理阶段也非常有用。

读取文件内容:

def configFile = new File("samples.csv")
def lines = configFile.readLines()   // 返回所有行

// 或者一行一行处理
configFile.eachLine { line ->
    def fields = line.split(",")
    println "样本: ${fields[0]}, 路径: ${fields[1]}"
}

写入文件:

def outFile = new File("file_list.txt")
outFile.text = "SRR123456\nSRR123457\nSRR123458"

// 追加模式
outFile.append("SRR123459\n")

注意:这些操作只适用于 workflow 启动前的初始化阶段,或者 script: 块 外部。在 process 内部,你应该用 Nextflow 的 path 类型来操作文件,因为它自动处理了临时目录、stage 和任务目录这些细节。

现场纪实:有一次我在 process 里直接用 new File() 读取输入文件,结果本地没问题,换到集群上就报错——因为任务的工作目录是隔离的,输入文件路径不对。后来改成用 input: path fastq 的方式,一切就正常了。

3.5 知识结构总览

下面这张 SVG 图把本章涉及的 Groovy 元素以及它们在 Nextflow 中的位置串了起来,方便你对照回顾。

Groovy 语言基础在 Nextflow 中的角色 Nextflow 管道 变量与作用域 def / 全局 vs 局部 闭包 map / each / findAll 列表 & 映射 List / Map / Tuple 文件操作 readLines / eachLine Channel 数据变换 参数配置预处理 使用原则 1. process 内部避免 new File(),使用 Nextflow 的 path 类型 2. 变量作用域保持最小化,尽量用 def 3. 用 map/闭包替代显式循环,可读性更佳

3.6 几个容易踩的坑

  • 闭包参数默认是 it —— 小脚本时方便,但嵌套闭包时要显式命名,不然代码根本没法读。
  • 不可变列表和映射 —— 如果你用 [1,2,3] 创建的列表,默认是 ArrayList,可以修改;但用了 as ImmutableList 后还想加元素,会抛异常。
  • 字符串双引号和单引号 —— 双引号支持插值("${var}"),单引号是纯字符串。在 Nextflow 的 script: 块里,外部变量必须用双引号才能被识别。
  • 文件编码问题 —— 我在读取中文样本名时遇到过乱码,后来在 new File() 时指定了 "UTF-8" 编码就解决了。
小建议:调试 Groovy 代码时,可以临时加一行 println something 看看值对不对。Nextflow 默认把 stdout 记录在 .nextflow.log 里,非常适合排查变量问题。