3. 第一个 Nextflow 脚本:Hello World
学任何新东西,我都喜欢先从“跑起来”开始。
Nextflow 也不例外。你不需要把 DSL2 的语法全背下来,先让一个最简单的 workflow 跑通,感受一下数据流是怎么走的,比什么都重要。
这一节,我们来写一个“Hello World”。它很简单,但足以帮你理解 Nextflow 脚本的基本骨架。
3.1 先写一个最小 workflow
新建个文件,就叫 hello.nf。我习惯把每个章节的练习脚本都按章节命名,方便回头找。
对着下面这个代码,直接抄进去:
#!/usr/bin/env nextflow
nextflow.enable.dsl=2
params.greeting = "Hello"
process sayHello {
publishDir "./results", mode: 'copy'
input:
val x
output:
path "hello.txt"
script:
"""
echo "${x}, World!" > hello.txt
"""
}
workflow {
sayHello(params.greeting)
sayHello.out.view { "写到文件中的内容是: $it" }
}
保存后,在终端里跑一条命令:
nextflow run hello.nf
如果你看到类似这样的输出,那就成了:
N E X T F L O W ~ version 23.10.0
Launching `hello.nf` [golden_gauss] DSL2 - revision: abc1234
executor > local (1)
[6d/9e8f4c] process > sayHello (1) [100%] 1 of 1
写到文件中的内容是: Hello, World!
终端上打印了一行,工作目录里也会生成一个 hello.txt,里面写着 Hello, World!。
嗯,这就是你的第一个 Nextflow 脚本。
如果报错说找不到
nextflow 命令,先去装一下。Nextflow 需要 Java 8 或更高版本,装好后 nextflow -version 能正常显示就说明环境没问题。
3.2 脚本结构:三大部分,缺一不可
刚才那段代码看着不长,但 Nextflow DSL2 脚本的核心部件都在里面了。我拆开来跟你讲。
我们来看一个骨架图,先有个整体感觉:
说白了,一份完整的 DSL2 脚本就是三个部分拼在一起。
| 组成部分 | 作用 | 你写的时候要注意什么 |
|---|---|---|
| 头部声明 | 指定解释器、启用 DSL2 模式 | 第一行是 shebang,第二行必须写 nextflow.enable.dsl=2,少了这一行,DSL2 语法全不认。 |
| process 定义 | 封装单个计算步骤 | 每个 process 就像一个黑盒:接收输入、产生输出、跑一段脚本。内部的脚本在独立工作目录里执行。 |
| workflow 块 | 编排 process 的调用和数据流向 | DSL2 里所有流程逻辑都写在 workflow {} 里。process 在这里被“调用”而非直接写在全局。 |
头部声明里
nextflow.enable.dsl=2 一定要放在 params 定义的前面。顺序错了,Nextflow 会直接报语法错误。
3.3 理解代码的运行机制
我们来过一遍执行流程,你对照着上面的代码看。
- 脚本启动:Nextflow 读到
hello.nf,先看头部声明,确认是 DSL2 模式。 - 参数解析:读到
params.greeting = "Hello",把默认值存好。你可以在运行命令时用--greeting Hi覆盖它。 - 编译 process:扫到
process sayHello块,但不立即执行——只是注册这个步骤。 - 进入 workflow:执行
workflow {}内部逻辑。先调用sayHello(params.greeting),把参数传进去。 - process 执行:Nextflow 为
sayHello分配一个唯一的工作目录(比如work/6d/9e8f4c.../),在里面运行echo "Hello, World!" > hello.txt。 - 输出通道:process 完成后,
hello.txt被发布到./results目录。同时,sayHello.out通道自动推送文件路径。 - 查看结果:
sayHello.out.view { "写到文件中的内容是: $it" }从通道中取出文件路径,用$it拼接成字符串打印到终端。
你想想看,这个模式其实很通用。你改改 script 里的命令,换个参数,就能处理不同的任务了。
3.4 print 和 view 的区别
脚本里我用了 view,没有用 print。这两个都是输出用的,但场景完全不同。
| 操作符 | 作用对象 | 典型用法 |
|---|---|---|
view | 通道(channel) | 取出通道中的每个元素,打印出来 |
print / println | 普通变量 | 在 workflow 块或 process 的 script: 内用 |
view 是 Nextflow 的通道操作符。它会监听通道里的每一个值,当新值进入时就执行你给的闭包。我很多项目调试时,会在关键步骤后面加一个 .view 看看数据流是否正常。
print 其实就是 Groovy 原生的 System.out.print,在 workflow 块里也能用,但它不能直接“监听”通道。举个例子:
workflow {
sayHello(params.greeting)
// 用 view 看通道内容
sayHello.out.view { "view 看到: $it" }
// 用 println 打印普通字符串
println "workflow 已经结束"
}
有一次我想在 workflow 里直接
print sayHello.out,结果打印出一串看不懂的对象地址。后来才意识到,通道不是普通变量,它的内容需要 view 才能看到。
记住:通道里的数据流,要用通道操作符去访问。
3.5 完整示例:调整一下试试
学到这里,我建议你动手改几个地方,跑跑看:
- 把
params.greeting改成自己的名字,比如"Hi"。 - 把
hello.txt改成greeting.txt,同时更新output块中的路径。 - 在
script里多写一行date >> greeting.txt,看看输出文件会不会多一行时间戳。 - 试着用
--greeting Bonjour运行:nextflow run hello.nf --greeting Bonjour。
我第一次学 Nextflow 的时候,就靠这样不断改参数、加命令来熟悉这套模式。你也会很快上手的。
跑完后,去
./work 目录看看。你会发现每个 process 都生成了一个哈希命名的文件夹,里面放着输入、脚本、输出和日志。这是 Nextflow 天然的“实验记录本”。
3.6 避坑指南
写第一个脚本时,有几个地方容易卡住。我直接列出来:
- 忘写
nextflow.enable.dsl=2:报错信息会说DSL2 constructs not recognized。看到这个,第一反应就是去检查头部。 - process 内的脚本用错引号:
script:块要用三引号"""包裹。单引号或双引号在 Groovy 里有不同含义,容易出问题。 - 忘了加
publishDir:不写这一行,输出文件只留在工作目录里,你很难找到。 - 路径格式写错:
publishDir "./results", mode: 'copy'中间是逗号,不是冒号。
我曾经在一台新服务器上部署流程,忘了启用 DSL2 模式,结果花了一个小时检查为什么 workflow {} 就是不被识别。嗯,从那以后我每次新建脚本,第一件事就是敲那两行头部声明。
把这一节的内容消化掉,你就迈过了 Nextflow 入门的第一道门槛。接下来,等你用得更熟,会慢慢体会到这个脚本结构设计得有多巧妙。