4. 第一个Nextflow流程:编写Hello World工作流,理解进程和输出
说实话,很多人学Nextflow的第一反应就是跑个Hello World。但你别小看这一步。我见过不少同事,把Nextflow当普通脚本写,结果跑完连“输出到底在哪”都搞不清楚。我们这章就把这个最简单的流程拆开来讲。你跟着我走一遍,后面写质控节点就顺了。
4.1 创建一个工作流文件
Nextflow的工作流本质就是一个.nf文件。里面的核心单元叫进程(process)。进程负责执行具体的任务,比如运行一个命令、调一个Python脚本。而工作流本身用来编排进程间的依赖关系。
我们新建一个文件,就叫hello.nf。我个人习惯把工作流文件和配置分开,但初学阶段,一个文件搞定就好。
4.2 最简单的进程——无输入无输出
先写一个极端简单的进程,只打印一句话。
process sayHello {
script:
"""
echo 'Hello, Nextflow!'
"""
}
workflow {
sayHello()
}
保存后,在终端执行:
nextflow run hello.nf
你会看到终端输出类似:
N E X T F L O W ~ version 24.10.0
Launching `hello.nf` [agitated_wozniak] DSL2 - revision: 1164db094b
executor > local (1)
[a7/3b9c8e] process > sayHello (1) [100%] 1 of 1 ✔
Hello, Nextflow!
看到了吗?最下面一行就是我们echo的内容。但如果你以为这样就把工作流写完了,那就错了。这种进程没有任何输入和输出,在实际项目中几乎用不到。
SayHello(首字母大写),Nextflow会直接报错。我当初就被这个坑过,排查了半天。
4.3 理解进程的输入/输出声明
刚才那个进程是“一次性”的。真实场景下,进程需要接收数据、产出结果。Nextflow通过input和output块来声明。
我们改一下:让进程接收一个字符串,然后打印它,再输出一个文件。
process greet {
input:
val greeting
output:
path "greeting.txt"
script:
"""
echo "$greeting" > greeting.txt
"""
}
workflow {
greet("Hello from workflow!")
}
这里有几个关键点:
val greeting:定义一个值类型的输入,名为greeting。path "greeting.txt":声明进程会生成一个文件,名字叫greeting.txt。script块里的$greeting:通过美元符号引用输入变量,用法和bash一样。- 工作流调用时传参:
greet("Hello from workflow!")。
运行一下:
nextflow run hello.nf
输出:
process > greet (1) [100%] 1 of 1 ✔
但这次你会在工作目录下发现一个greeting.txt文件。打开看,内容正是“Hello from workflow!”。
work/目录下独立的文件夹里,文件名也是原样。但你不用直接去work/里找文件,后面我们会讲怎么通过publishDir把结果收集出来。
4.4 输出到底是什么?——理解通道(channel)
这才是我最想讲的部分。很多人把进程的输出理解为“文件”,没错,但不全面。实际上,进程的输出会变成一个通道(channel)。通道是Nextflow里数据流动的“管道”。
拿上面那个例子来说,greet进程的输出是一个通道,里面装着一个文件路径。我们可以把这个通道传给另一个进程。
我们再加一个进程,用来读取刚才生成的文件并打印内容:
process greet {
input:
val greeting
output:
path "greeting.txt"
script:
"""
echo "$greeting" > greeting.txt
"""
}
process readGreeting {
input:
path greeting_file
output:
stdout()
script:
"""
cat $greeting_file
"""
}
workflow {
greeting_ch = greet("Hello from workflow!")
readGreeting(greeting_ch)
}
看到没?greet的输出赋值给了greeting_ch(一个通道变量),然后直接传给readGreeting的输入。工作流内部就是通过通道把数据从一个进程“流”到另一个进程。
output块声明的不是文件本身,而是通道的发射内容。每个输出声明都会在进程执行成功后,把生成的文件或值发射到对应的输出通道。工作流里用=来捕获这些通道。
4.5 知识结构总览
下面这张图总结了本章的核心逻辑关系:
这张图的核心就一句话:进程的output变成通道,工作流用赋值拿到通道,再传给下一个进程。弄懂这个,Nextflow的流程编排你就掌握了30%。
4.6 常见的输入/输出类型
除了val和path,Nextflow还支持其他几种常用类型。我做了一个对照表:
| 声明方式 | 含义 | 示例 | 典型用途 |
|---|---|---|---|
val x | 值类型,传一个变量 | val name | 样本ID、参数控制 |
path f | 文件路径,传一个或多个文件 | path reads | 测序数据、参考基因组 |
env VAR | 环境变量 | env PATH | 设置外部工具路径 |
stdout() | 捕获进程的标准输出 | stdout() | 从命令输出里提取结果 |
tuple(a,b) | 元组,组合多个值或文件 | tuple val(sample_id), path(bam) | 把ID和文件打包传递 |
在质控节点里,你最常用的是path和tuple。比如FastQC输入是fastq文件,输出是zip和html,通常用tuple把它们绑在一起。
4.7 理解工作目录与输出文件的位置
每次运行Nextflow,它都会在work/目录下生成一些子目录,里面放着每个进程的执行现场。包括脚本、输入文件的符号链接、输出文件、以及运行日志。你可以把它想象成每个进程的“小隔间”。
但实际项目中,我们不想去work/里翻文件。常见的做法是用publishDir指令把结果发布到指定目录。我给个示例:
process greet {
publishDir "results", mode: 'copy'
input:
val greeting
output:
path "greeting.txt"
script:
"""
echo "$greeting" > greeting.txt
"""
}
workflow {
greet("Publish this!")
}
加上publishDir后,进程执行完会自动把greeting.txt拷贝到项目根目录下的results/文件夹里。你还可以设置mode: 'link'创建硬链接或符号链接,节省磁盘空间。我个人习惯用copy,因为后续分析环境迁移时文件不会断链。
publishDir默认只在进程成功结束后执行一次。如果你把同一个进程跑多次(比如并行处理多个样本),每个实例都会往同一个results/里写文件。如果输出文件名一样,后面的会覆盖前面的。解决方法是把样本ID等唯一标识放进输出文件名,或者用publishDir的pattern选项按规则分目录。我们后面讲质控节点时会细说。
4.8 本章小结:你应该带走的三个要点
- 进程必须有输入/输出声明才算完整。 无输入无输出的进程除了测试,基本用不上。
- 输出通道是数据流动的载体。 工作流通过
=捕获进程的输出通道,然后传给下一个进程的input。这就是Nextflow的“工作流”本质。 - 善用
publishDir管理结果。 不要自己去work/里翻,把关键的输出发到项目目录,后续处理也会更顺手。