第一个 Pipeline:Hello World
记得我刚接触 Nextflow 那会儿,第一件事就是跑通一个最简单的流程。当时就一个想法——先让它动起来。看完官方文档我写了个不到十行的脚本,运行后看到终端输出 “Hello World” 的那一刻,心里踏实了不少。
这一章我们就从那个最经典的例子开始。你跟着敲一遍,几分钟就能跑起来。之后我再带你拆解 process 和 workflow 这两个核心语法。
一个简单到不能再简单的例子
新建一个文件,就叫 main.nf,然后写入下面这段代码:
process sayHello {
output:
stdout
script:
"""
echo "Hello World"
"""
}
workflow {
sayHello()
}
解释一下:sayHello 是一个 process,它做的事就是执行一句 shell 命令 echo "Hello World",并把标准输出暴露出来。而 workflow { sayHello() } 就是告诉 Nextflow:运行这个流程,调用这个 process。
看,是不是很直观?我个人习惯把 process 想象成一个独立的小黑盒——你给它输入,它跑一段脚本,再吐输出。外面的人不用关心里面具体怎么操作的,只要知道接口就行。
运行起来:nextflow run
保存好文件,打开终端,执行:
nextflow run main.nf
你应该会看到类似这样的输出:
N E X T F L O W ~ version 23.04.1
Launching `main.nf` [cranky_poisson] DSL2 - revision: 459f1d4a1c
executor > local (1)
[ce/5d9a2c] sayHello [100%] 1 of 1 ✔
Hello World
注意最后一行 “Hello World”——那就是 process 的标准输出被 workflow 自动收集并打印了出来。你想想看,要是没有 workflow 块,Nextflow 根本不知道要运行哪些 process,也不会自动帮你处理输出。所以 workflow 就是整条流水线的调度中心。
我这里踩过一个小坑:早期版本(DSL1)的语法和现在不同,如果你用的 Nextflow 版本比较旧,可能还得加 projectDir 参数。我建议你先升级到 22.10 以上,直接用 DSL2,省心很多。
拆解基础语法:process 和 workflow
第一个例子跑通了,咱们就来深挖一下这两个关键结构。
process——定义计算单元
一个 process 通常包含以下几部分:
- directives:指令,比如
publishDir、label,控制资源、输出位置等。 - input 声明:定义从 workflow 接收哪些参数或文件。
- output 声明:定义要暴露出来的结果(文件或标准输出)。
- script:实际要执行的 shell 代码,也可以嵌套 Python、R 等。
看一个带输入和发布目录的例子:
process greet {
input:
val name
output:
path "greeting.txt"
script:
"""
echo "Hello ${name}!" > greeting.txt
"""
}
这里 input: val name 表示接收一个值类型的参数;output: path "greeting.txt" 表示这个 process 会生成一个文件 greeting.txt。注意文件名可以写通配符,比如 *.txt,这样无论文件名怎么变都能匹配上。
我曾经在项目里吃过亏——没在 process 里加 publishDir,结果跑了半天,所有中间结果都散落在 work 目录下,根本找不到最终产物。后来我养成习惯:每个生成最终文件的 process 都加上 publishDir 指令:
process greet {
publishDir "${params.outdir}", mode: 'copy'
input:
val name
output:
path "greeting.txt"
script:
"""
echo "Hello ${name}!" > greeting.txt
"""
}
publishDir 会把产出文件拷贝或链接到你指定的目录,避免去 work 里翻。默认是硬链接,想把它变成拷贝就加 mode: 'copy'。
workflow——编排流程
workflow 块负责声明哪些 process 要运行,以及它们之间的连接关系。最简单的就是直接调用 process 名字:
workflow {
greet("World")
}
如果有多个 process,或者有数据依赖,可以这么写:
workflow {
// 第一个 process
strings = Channel.of("Hello", "Bonjour", "Hola")
greetings = greet(strings)
// 第二个 process,依赖第一个的输出
uppercase(greetings)
}
process greet {
input:
val name
output:
path "greeting.txt"
script:
"""
echo "Hello ${name}!" > greeting.txt
"""
}
process uppercase {
input:
path file
output:
stdout
script:
"""
tr 'a-z' 'A-Z' < ${file}
"""
}
这里用了一个 Channel.of() 创建通道,传入 process 后会自动并行处理。workflow 中可以用 = 把 process 的输出赋给变量,再传给下一个 process。
从结构上看,process 和 workflow 的分工很清晰:
| 特性 | process | workflow |
|---|---|---|
| 职责 | 包装具体计算(shell、Python 等) | 定义流程拓扑,调用 process |
| 输入输出 | 明确定义 input / output | 通过 Channel 连接不同 process |
| 可复用性 | 可被多个 workflow 调用 | 一个脚本只有一个 workflow 块 |
| 执行逻辑 | 每个实例独立运行 | 决定执行顺序与并行度 |
一张图看懂 Nextflow pipeline 的骨架
下面这个 SVG 图展示了一个最简 pipeline 的构成:workflow 作为入口,调度 process,process 接收输入、执行脚本、产生输出。
小建议:每次写完 process,先用 nextflow info main.nf 检查一下语法。它能帮你发现比如花括号没配对、漏了逗号这类低级错误,省得跑半天才报错。
注意:process 里的 script 块默认使用单引号包裹的三引号(""")。如果你需要在脚本中直接使用 $ 符号而不让它被 Nextflow 插值,请用单引号:'''。我曾因为忘记这个细节,折腾了半天才发现 $HOME 被替换成了变量。
好了,你已经完成了第一个 pipeline 的编写、运行和语法拆解。接下来多试试改一改输入参数、加上 publishDir,跑几次就熟悉了。记住:process 是积木,workflow 是设计图,搭好了就能自动化执行复杂的分析任务。