3. 第一个Nextflow脚本:Hello World,运行本地流程
好了,终于到了动手写代码的时候。你可能会觉得,一个Hello World而已,有什么好讲的?但说实话,我第一次跑通这个简单的脚本时,才真正理解了Nextflow的设计哲学。
我记得当时我盯着终端,看着那个绿色的"done"字样,心里在想:嗯,原来流程引擎是这样工作的。它不像普通脚本那样一条线跑到底,而是把每个任务都包装成一个独立的小世界。
3.1 搭建最小骨架
我们先创建一个目录,叫 hello-world/。所有文件都放在里面。然后新建一个 main.nf,这就是Nextflow的入口文件。
mkdir hello-world
cd hello-world
touch main.nf
打开 main.nf,写下这一段代码:
#!/usr/bin/env nextflow
process sayHello {
output:
stdout
script:
"""
echo 'Hello, World!'
"""
}
workflow {
sayHello()
}
你可能会问:怎么这么长?不是一行就能打印吗?别急,我给你拆解一下。
这个脚本有三个主要部分:
- process 定义:用
process关键字声明一个可执行的任务。这里叫sayHello。 - output 声明:告诉Nextflow,这个进程会把标准输出传出来。
- workflow 块:定义流程的执行顺序。这里直接调用
sayHello()。
关键概念:Nextflow的流程是由多个 process 组成的DAG(有向无环图)。每个process都运行在自己的工作目录里,互不干扰。
3.2 运行它
保存文件后,在终端执行:
nextflow run main.nf
我第一次运行的时候,心里很忐忑——万一环境没配好怎么办?幸好,输出是这样的:
N E X T F L O W ~ version 23.04.1
Launching `main.nf` [wise_turing] DSL2 - revision: d1a2b3c4d5
executor > local (1)
[2e/1a2b3c] sayHello | 1 of 1 ✔
这时你可以在终端看到打印的消息吗?没有。因为Nextflow默认把进程的标准输出捕获到日志里,而不是直接打到屏幕上。如果你要看结果,需要加上 --publish true 或者查看工作目录的文件。
我更习惯直接加一个 view 操作来观察输出:
workflow {
sayHello().view { "Process said: $it" }
}
运行后,你会看到:
Process said: Hello, World!
小技巧:开发阶段多用 .view() 输出调试信息。生产环境则建议用日志文件记录。
3.3 理解 process 内部机制
你可能会觉得,一个 echo 命令没必要这么复杂。但实际项目中,process 做的事情远不止打印一行字。它可以是任何命令行、脚本、甚至是调用外部工具。
我来给你讲讲它内部是怎么运作的:
- Nextflow 为每个 process 创建一个独立的工作目录(
work/下)。 - 把
script块里的内容写入一个临时脚本文件。 - 在隔离的环境中执行该脚本(默认就是本地 shell)。
- 捕获 stdout/stderr,并根据
output声明来传递数据。
我记得有一次在项目中,一个同事把文件路径写死在 script 里,结果换了环境就炸了。后来我告诉他:永远不要假定进程能直接访问外部目录。Nextflow 默认是沙箱化的。
3.4 加入一点参数
Hello World 太简单?那我们加点变化。比如让用户指定问候语:
params.greeting = "Hello, World!"
process saySomething {
input:
val greeting
output:
stdout
script:
"""
echo '$greeting'
"""
}
workflow {
saySomething(params.greeting).view()
}
现在,你可以通过命令行覆盖默认值:
nextflow run main.nf --greeting "Hi, there!"
注意:参数名前加两个短横线 --,但参数值不要带等号。Nextflow 命令行解析比较严格,我之前踩过坑:写成了 --greeting=xxx 结果没生效,后来才发现要用空格。
这样你就掌握了最基础的参数传递方式。实际项目中,参数是连接用户和流程的桥梁。
3.5 本章知识结构
下面这张图总结了第一个Nextflow脚本的核心要素:
3.6 尝试多个进程
一个workflow里当然可以调用多个process。比如我们再加一个 sayGoodbye:
process sayHello {
output:
stdout
script:
"""
echo 'Hello'
"""
}
process sayGoodbye {
output:
stdout
script:
"""
echo 'Goodbye'
"""
}
workflow {
hello = sayHello()
goodbye = sayGoodbye()
hello.view()
goodbye.view()
}
运行后你会看到两个进程并行执行。Nextflow默认会根据资源自动并行。本地模式下,只要你的CPU核心数够,进程就会同时跑。
但注意:这两个进程之间没有数据依赖关系,所以它们可以同时运行。如果你希望先跑完 sayHello 再跑 sayGoodbye,就需要把第一个的输出作为第二个的输入。这会在后面的章节讲。
实践建议:从这个简单的例子开始,逐步增加复杂度。我曾见过有人一上来就写几百行的流程,结果调试时根本不知道哪里出问题。从小处着手,每次只加一个功能。
3.7 调试与常见报错
| 错误信息 | 可能原因 | 解决办法 |
|---|---|---|
Command not found |
进程里用了系统没有的命令 | 检查环境变量或安装工具 |
No such file or directory |
进程内引用了外部路径 | 使用 ${projectDir} 或输入通道 |
Process execution failed |
脚本返回非零退出码 | 查看 .nextflow.log 或工作目录下的日志 |
我曾经被最后一个错误折腾了半天:一个看似正确的脚本一直报错,后来发现是脚本最后多了一个空行里的隐藏字符。从那以后,我写 script 块时会特别注意不要留多余的空格和换行。
调试技巧:用 nextflow run main.nf -resume 可以重用之前成功的中间结果。下次改代码时,不用重跑所有任务,省时又省心。
3.8 小结
到这儿,你已经亲手写了一个完整的Nextflow流程。别看它简单,它包含了所有核心概念:process、workflow、input/output、参数。后续所有复杂的流程都是在这个基础上堆叠起来的。
我建议你把这个Hello World跑通之后,试着改一改:比如加个参数让用户自定义问候语,或者再加一个进程来计算字符串长度。动手改一下,比单纯看十遍都有用。