第一个 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 接收输入、执行脚本、产生输出。

Nextflow Pipeline 骨架(Hello World) workflow { } process sayHello input / output script stdout 输出 workflow 调用 process,process 内部由 script 实现,输出通过 stdout 传回。

小建议:每次写完 process,先用 nextflow info main.nf 检查一下语法。它能帮你发现比如花括号没配对、漏了逗号这类低级错误,省得跑半天才报错。

注意:process 里的 script 块默认使用单引号包裹的三引号(""")。如果你需要在脚本中直接使用 $ 符号而不让它被 Nextflow 插值,请用单引号:'''。我曾因为忘记这个细节,折腾了半天才发现 $HOME 被替换成了变量。

好了,你已经完成了第一个 pipeline 的编写、运行和语法拆解。接下来多试试改一改输入参数、加上 publishDir,跑几次就熟悉了。记住:process 是积木,workflow 是设计图,搭好了就能自动化执行复杂的分析任务。