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 脚本里,结果并行执行时数据全乱了。
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 和任务目录这些细节。
new File() 读取输入文件,结果本地没问题,换到集群上就报错——因为任务的工作目录是隔离的,输入文件路径不对。后来改成用 input: path fastq 的方式,一切就正常了。
3.5 知识结构总览
下面这张 SVG 图把本章涉及的 Groovy 元素以及它们在 Nextflow 中的位置串了起来,方便你对照回顾。
3.6 几个容易踩的坑
- 闭包参数默认是 it —— 小脚本时方便,但嵌套闭包时要显式命名,不然代码根本没法读。
- 不可变列表和映射 —— 如果你用
[1,2,3]创建的列表,默认是 ArrayList,可以修改;但用了as ImmutableList后还想加元素,会抛异常。 - 字符串双引号和单引号 —— 双引号支持插值(
"${var}"),单引号是纯字符串。在 Nextflow 的script:块里,外部变量必须用双引号才能被识别。 - 文件编码问题 —— 我在读取中文样本名时遇到过乱码,后来在
new File()时指定了"UTF-8"编码就解决了。
println something 看看值对不对。Nextflow 默认把 stdout 记录在 .nextflow.log 里,非常适合排查变量问题。