第一个Nextflow脚本:Hello World

好了,到了动真格的时候了。不管你是刚接触Nextflow,还是想看看它到底怎么和SLURM配合,我们得先跑通最基础的东西——Hello World。

别小看这个例子。我第一次写Nextflow脚本时就在这踩过坑。因为没搞懂process和workflow的关系,跑出来一团乱麻。所以这次,咱们一步步来。

1. 先看脚本长什么样

下面是一个完整的Hello World脚本。复制到hello.nf里,直接就能跑。

#!/usr/bin/env nextflow
nextflow.enable.dsl=2

process sayHello {

    input:
    val greeting

    output:
    stdout

    script:
    """
    echo 'Hello, $greeting!'
    """
}

workflow {
    greetings = Channel.of('World', 'Nextflow', 'SLURM')
    sayHello(greetings).view { it }
}

嗯,就这么几行。你可能会问:这玩意儿怎么跑?很简单:

nextflow run hello.nf

输出会长这样:

N E X T F L O W  ~  version 23.10.0
Launching `hello.nf` [distracted_curran] DSL2 - revision: abc1234567
Hello, World!
Hello, Nextflow!
Hello, SLURM!

提示:每次运行都会看到不同的「名字」,比如distracted_curran。那是Nextflow给这次job的随机代号。不用在意,只是好看。

2. 进程(process)是啥?

说白了,进程就是Nextflow里的最小执行单元。每个process跑一个任务,可以是一个命令、一个脚本,甚至是一段Python代码。

上面脚本里的sayHello就是一个进程。我习惯把进程想成一个「黑盒子」——给它输入,它执行,然后输出。具体内部怎么跑,你不用管,Nextflow帮你看好。

这里我列一下进程的基本结构:

组成部分 作用 我踩过的坑
process 名称 定义进程的名字 名字别用横线,用驼峰或下划线
input: 声明输入(值、文件、路径等) 输入类型必须匹配,否则报错
output: 声明输出(stdout、文件等) 输出必须明确声明,不声明就丢数据
script: 要执行的脚本/命令 记得用双引号,变量才能被替换

重点:script块里的$greeting是Nextflow的变量,不是Shell的。它会先被Nextflow替换成输入的值,再交给Shell执行。

说到变量,我建议你一开始就用${greeting}这种大括号形式。为什么?因为当你变量名后面跟着字母或数字时,不加括号会解析错。我曾经因为忘了加大括号,查了半小时才找到问题。

3. workflow块——指挥中心

脚本最后一段是workflow { }。这东西就是整个管道的入口。

在workflow里,我们做了三件事:

  • 创建数据通道: Channel.of('World', 'Nextflow', 'SLURM')。这是一个简单的值通道,会依次发出三个字符串。
  • 调用进程: sayHello(greetings)。把通道greetings传给进程sayHello。每个字符串都会触发一次进程执行。
  • 打印结果: .view { it }。把进程的输出(stdout)打印到屏幕上。这里it是Groovy的隐式变量,代表每个输出值。

实际上,我们可以用管道语法串联起来,写得更紧凑:

workflow {
    Channel.of('World', 'Nextflow', 'SLURM')
        | sayHello
        | view
}

这种风格更「管道化」。不过刚开始,分步写更容易理解。

注意:DSL2模式下,workflow块是强制要求的。以前的老写法(直接写process在外面)在DSL2中已经废弃了。如果你看到网上教程没有workflow,那很可能是旧版本的内容。

4. 执行流程——到底发生了什么?

你现在可能在想:进程是串行还是并行执行的?答案是——默认是并行的。

我们给了三个输入('World', 'Nextflow', 'SLURM'),这不是排队一个一个来,而是三个任务同时跑。每个任务在一个独立的工作目录里运行,互不干扰。

关键:这种并行不是多线程跑在一个节点里,而是提交多个独立job。在本地模式下,它会开多个进程(或线程);在SLURM上,它会提交多个作业。

下面这张图可以帮你理解整个流程:

Nextflow Hello World 执行流程 Channel.of(...) 自动并行分发 sayHello ('World') sayHello ('Nextflow') sayHello ('SLURM') Hello, World! Hello, Nextflow! Hello, SLURM! .view { it } 说明 三个任务并行执行 互不干扰,独立工作目录

这一套下来,你应该能感受到:进程负责干活,通道负责运数据,workflow负责调度。这就是Nextflow最核心的抽象。

5. 和SLURM有什么关系?

你可能会问:「这跟SLURM调度有什么关系?」嗯,现在还没有直接关系,因为我们用的是本地执行器。但是——

等我们后面配置了process.executor = 'slurm'之后,同样的脚本,同样的结构,会变成提交到集群的SLURM作业。进程里的每个输入,都会变成一个独立的sbatch任务。

你现在理解了并行+独立工作目录的概念,到时候配置SLURM就能秒懂。

小建议:第一次跑Hello World时,观察一下工作目录(默认是work/目录)。你会发现每个进程实例都有自己的子目录,里面放着输入、输出和日志。我一开始不知道这个,丢数据了也不知道去哪找。

好,这一章就到这里。把上面的脚本跑通,看看输出,然后自己改改Channel.of()里的内容试试。这就是你掌握Nextflow的第一步。