第4章:Nextflow基础语法——参数、脚本、执行
学Nextflow,说白了就是搞明白三件事:参数怎么传、脚本怎么写、进程怎么跑。我刚开始接触时也觉得概念多,但拆开来看,其实每个部分都挺直白的。这一章咱们就把这三个核心要素捋清楚。
4.1 参数系统:让工作流学会"看人下菜碟"
工作流写死了肯定不行。你想想,今天跑A项目,明天跑B项目,数据路径、输出目录、阈值参数全都不一样。所以Nextflow设计了params这套机制。
4.1.1 参数定义
在脚本里用params.xxx定义一个参数,再给它一个默认值。这是最基础的用法。
// main.nf
params.reads = "$projectDir/data/reads/*.fastq"
params.outdir = "./results"
params.genome = "hg38"
params.pval = 0.05
我个人习惯把params定义放在脚本最前面,一眼就能看到所有可调参数。这样别人接手你的工作流,不用翻到底层逻辑,改改参数就能跑。
4.1.2 命令行覆盖参数
运行时用--参数名 值就能覆盖默认值。注意是两个短横线,不是单个。单个短横线那是给Nextflow自己用的指令参数。
nextflow run main.nf --reads "/data/project/samples/*.fastq" --pval 0.01
4.1.3 使用配置文件
项目大了之后,参数会越来越多。全堆在命令行里既不优雅也不好维护。这时nextflow.config就派上用场了。
// nextflow.config
params {
reads = "$projectDir/data/reads/*.fastq"
outdir = "./results"
genome = "hg38"
pval = 0.05
}
process {
executor = 'slurm'
queue = 'normal'
}
你看,配置文件和脚本分离,参数归参数,逻辑归逻辑。我在项目中遇到过团队协作时有人在脚本里硬编码路径,结果换环境就炸了……从那以后我强制要求所有可变字段走params。
4.2 脚本块:工作流真正干活的地方
进程里的script块,说白了就是告诉计算机"这一步你要执行什么命令"。它支持多种语言,但最常用的是Bash。
4.2.1 基础写法
process fastqc {
input:
path reads
output:
path "${reads}.html"
path "${reads}.zip"
script:
"""
fastqc -o . $reads
"""
}
注意外面的双引号。你要是用单引号,变量就不会被解析。嗯,这里有个坑——我几年前就因为引号用错,折腾了整整一个下午……
python、R、awk都可以,但记得写清楚。
4.2.2 多语言脚本
有些场景下Bash不够用,比如需要复杂的数据处理。Nextflow允许你在script块里直接嵌入Python或R。
process stats_plot {
input:
path data
output:
path "plot.png"
script:
"""
#!/usr/bin/env python3
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("$data")
plt.figure(figsize=(10,6))
df['value'].hist()
plt.savefig('plot.png')
"""
}
我曾经在生信项目里遇到过样本量超过10万,Bash处理实在太慢,换成Python脚本后速度提升了一个数量级。灵活切换语言,这才是Nextflow的爽点。
4.2.3 字符串引号的学问
脚本块里的字符串处理是个容易翻车的地方。我总结了一下:
| 写法 | 变量解析 | 适用场景 |
|---|---|---|
""" ... """ |
解析 $变量 | 常用,含变量拼接 |
''' ... ''' |
不解析,纯文本 | 含大量$符号的代码 |
"..." |
解析 $变量 | 单行简单命令 |
'...' |
不解析,纯文本 | 单行纯字符串 |
怎么选?如果你脚本里有$开头的shell变量或者Perl正则,就用三单引号,否则Nextflow会尝试解析它,导致报错。
4.3 进程执行:理解背后的"调度"逻辑
进程是Nextflow的最小执行单元。你定义好输入、输出、脚本,Nextflow负责调度它什么时候跑、在哪里跑。
4.3.1 进程基本结构
process 进程名 {
// 指令区:资源、执行器、标签
executor 'slurm'
cpus 4
memory '8 GB'
// 输入声明
input:
path fastq
val sample_id
// 输出声明
output:
path "${sample_id}_aligned.bam"
// 执行脚本
script:
"""
bwa mem -t 4 $fastq | samtools sort -o ${sample_id}_aligned.bam
"""
}
指令区、输入、输出、脚本——就这四个部分。我这里有个习惯:输入输出一定要写清晰,命名别偷懒。否则工作流一旦复杂起来,你自己都搞不清哪个文件从哪来的。
4.3.2 输入输出声明的作用
你可能会问:为什么Nextflow非要我写input和output?让它自己推不行吗?答案很直接——不行。Nextflow靠这些声明来构建依赖图。只有明确知道每个进程要什么、产什么,它才能决定谁先跑、谁后跑、哪些可以并行。
4.3.3 进程的执行机制
每个进程默认是串联执行的——上一个完了下一个才开始。但加上.set()和channel操作,就能变成并行。
workflow {
// 读取样本列表
Channel.fromPath(params.sample_list)
| splitCsv(header: true)
| map { row -> [row.sample_id, file(row.fastq)] }
| set { samples }
// 并行执行fastqc
fastqc(samples)
// 等所有fastqc完成后,执行汇总
multiqc(fastqc.out.collect())
}
这里fastqc会对每个样本并行执行。我曾经管理过2000个样本的项目,靠这种并行机制,整个分析周期从两周压缩到两天。这就是Nextflow的威力。
4.4 三者如何协同:一个完整示例
光讲概念不过瘾。我们写一个完整的例子,把参数、脚本、执行串起来。
// main.nf
params.reads = "$projectDir/data/*.fastq"
params.outdir = "./results"
params.genome = "hg38"
process align {
input:
path reads
val genome
output:
path "${reads.baseName}_sorted.bam"
script:
"""
bwa mem $genome $reads | samtools sort -o ${reads.baseName}_sorted.bam
"""
}
process count {
input:
path bam
output:
path "${bam.baseName}_counts.txt"
script:
"""
featureCounts -a genes.gtf -o ${bam.baseName}_counts.txt $bam
"""
}
workflow {
Channel.fromPath(params.reads)
| map { reads -> [ reads, params.genome ] }
| set { read_pairs }
align(read_pairs)
count(align.out)
}
这个流程很简单:传参→比对→计数。但你看,参数定义了输入源和输出位置,脚本定义了每一步的具体命令,而workflow决定了执行顺序和依赖关系。三者缺一不可。
本章知识结构
下面这张图把参数、脚本、执行三者的关系可视化了一下。你可以把它当成Nextflow基础语法的"骨架"来记。
从这张图可以看得很清楚:参数定义和配置文件位于顶层,它们共同决定了"跑什么";中间的进程封装了脚本块,决定了"怎么跑";最底层的工作流执行负责调度编排,决定了"谁先跑、谁后跑"。三个层级之间通过箭头传递数据和依赖。
好了,这一章的内容就这么多。参数、脚本、执行——你把这三样东西刻在脑子里,Nextflow的基础语法就算拿下了。后面章节我们会在这个基础上继续往上搭,把工作流写得更健壮、更工程化。