4. 第一个Nextflow脚本:Hello World示例

好了,理论铺垫得差不多了。咱们直接上手写一个真正的Nextflow脚本。通过这个经典的Hello World,你会理解process、workflow、run这三个最核心的概念。我个人习惯每次学新工具都从Hello World开始——它能让你快速建立信心。

你想想看,学编程语言时第一个例子往往是“Hello, World!”。为什么?因为它在最简单的代码里包含了这门语言的骨架。Nextflow也一样。我们写一个能打印“Hello, World!”的脚本,然后拆开看看它到底是怎么工作的。

4.1 一个最简单的Hello World脚本

先别管RNA-seq,也别管复杂的数据分析。咱们就写一个流程,让每个进程输出一句话。下面就是完整脚本:

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

process sayHello {
    output:
    stdout

    script:
    """
    echo 'Hello, World!'
    """
}

workflow {
    sayHello()
}

嗯,这里要注意:#!/usr/bin/env nextflow 这行不是必须的,但我建议你加上。为啥?它能让系统知道这是个Nextflow脚本,方便你直接 ./script.nf 运行。

把上面的内容保存为 hello.nf。然后运行:

nextflow run hello.nf

终端会输出类似这样的日志:

N E X T F L O W  ~  version 23.10.0
Launching `hello.nf` [focused_poincare] DSL2 - revision: 8c1f5a2f59
executor >  local (1)
[8c/1a5b2f] sayHello | 1 of 1 ✔️
Hello, World!

看到了吗?核心就是三行代码、一条命令。但这里面的门道可不少。

重要区分: Nextflow脚本(.nf文件)和普通的Shell脚本完全不同。Shell脚本里的每个命令按顺序执行;Nextflow脚本里的process是独立运行的,可以并行,也可以在不同的计算环境里执行。这个区别非常关键。

4.2 解剖process:最小的执行单元

process 是Nextflow里的基本计算单元。说白了,它就是一个可以独立运行的“黑盒”。这个黑盒接收输入,执行任务,产生输出。

在上面的例子里:

  • process 名称: sayHello – 给这个黑盒起个名字,方便后面调用。
  • output 声明: stdout – 告诉Nextflow,这个进程的标准输出会被捕获。
  • script 块: 花括号里包裹的是实际要执行的Shell代码。

你可能觉得这有点像写一个函数。对,就是这么回事。但有个关键区别:在普通编程语言里,函数是在当前进程里执行的;而Nextflow的process可以丢给集群、容器、云平台去跑。这也是它强大之处。

我曾见过一个团队,把几十个分析步骤全部写成独立的process,然后在workflow里串联起来。每个process都像一个乐高积木,拼一拼就搭出一个完整的分析流程。

4.3 process的四大基本要素

一个完整的process通常包含四个部分。我们直接看个更实际的例子:

process fastqc {
    input:
    path reads

    output:
    path "${reads.baseName}_fastqc.html"

    script:
    """
    fastqc -o . $reads
    """
}
要素 说明 本例
input 声明输入什么数据 path reads – 一个文件路径
output 声明输出什么数据 path "...html" – 生成一个HTML报告
script 实际运行的命令 调用fastqc工具
directives 运行时的配置选项 可指定CPU、内存、容器等

小贴士: 初期可以先只关注 input、output、script 三个块。directives 在后面的章节再慢慢熟悉。一口气吃不成胖子。

4.4 workflow:把process串起来的主线

workflow块是整个流程的“导演”。它告诉Nextflow:先运行哪个process,再运行哪个process,数据怎么流动。

在Hello World里:

workflow {
    sayHello()
}

这就是在说:执行 sayHello 这个process。如果我有多个process要按顺序执行呢?比如先做质量评估,再做比对:

workflow {
    fastqc(reads_ch)
    star_alignment(fastqc.out.html)
}

这里 fastqc.out.html 表示取 fastqc 这个process的输出结果,传给下一个process。这种“管道”式的数据传递,就是我特别欣赏Nextflow的地方。代码看得很清楚,数据流向一目了然。

我记得以前用纯Shell脚本写流程,数据传递基本上靠文件名约定:input.fastq → output.sam。脚本稍微一长,根本分不清哪个文件是从哪个步骤来的。Nextflow把这个痛点彻底解决了。

4.5 run:执行你的流程

写完脚本怎么跑?用 nextflow run 命令。咱们再深入看看这个命令的结构:

nextflow run <脚本文件或项目地址> [选项]

举个例子:

nextflow run hello.nf -process.executor local

这里 -process.executor local 是告诉Nextflow:“所有process都在本地计算机上跑”。这是最简模式。以后你还可能用 slurm、pbs、awsbatch 等执行器。

常见坑: 我第一次使用时,以为 nextflow run 后面只能写 .nf 文件。后来发现它可以直接从GitHub仓库拉取流程。比如 nextflow run nf-core/rnaseq 就能运行一个完整的RNA-seq分析流程。这种“一键运行”的能力非常强力,但新手建议先练本地文件。

4.6 理解执行过程:从日志到输出

运行Nextflow脚本后,你会看到一堆日志输出。这些日志不是乱码,它们包含了重要信息。

日志里最关键的几行:

[8c/1a5b2f] sayHello | 1 of 1 ✔️

拆开分析:

  • [8c/1a5b2f] – 这个process运行的工作目录。Nextflow会给每个process分配一个唯一的工作目录,路径在 work/8c/1a5b2f... 里。
  • sayHello – process的名字。
  • 1 of 1 – 总共要运行1次,这是第1次。
  • ✔️ – 表示执行成功。

为什么要单独给每个process分配工作目录?这是可重复性的关键。每个process的所有临时文件、中间结果都存在自己的目录里,互不干扰。就算某个process失败了,你也可以直接进到它的工作目录里看log、查错误。

我曾经卡在一个看似简单的Hello World脚本上半天。后来发现是我本地没有安装某依赖,但日志里只报了一个很奇怪的内存错误。如果我当时知道可以直接看工作目录里的 .command.log 文件,就能立刻发现问题。

4.7 Nextflow的执行模型:一个简单的可视化

为了让你更直观地理解Nextflow的执行过程,我画了个流程图。这个图展示了Hello World脚本在底层是怎么被处理的:从脚本解析开始,到DSL2编译器检查语法,再到每个process被独立调度执行。你仔细看看,会发现每个process都有自己的工作目录(work dir),这就是Nextflow隔离不同任务的方式。

Nextflow执行模型示意图 Nextflow 执行模型 hello.nf DSL2 解析器 workflow 定义 执行调度器 每个 process 被独立调度执行 sayHello work/8c/1a5b2f echo 'Hello, World!' .command.sh 文件 输出文件 .command.out 文件 终端显示: Hello, World! 注意 每个process都有 独立的工作目录 互不干扰 失败时可单独 查日志

这个图展示了从脚本到最终输出的完整路径:

  1. 脚本解析: Nextflow读取 hello.nf,先检查DSL2语法。
  2. workflow定义: 解析出 workflow 块中的执行顺序。
  3. 调度执行: 为每个process分配执行器(这里是local)。
  4. 工作目录: 每个process都在自己的目录下运行,所有临时文件都存在那里。
  5. 输出捕获: process的标准输出被收集到 .command.out 文件,然后打印到终端。

这个执行模型正是Nextflow“可重复性”的基石。想想看:每个process都在一个干净的、隔离的环境里运行,依赖、输入、输出都被严格控制。结果怎么可能不一致?

实战建议: 运行完Hello World后,去 work 目录下看看。你会看到类似 work/8c/1a5b2f 这样的文件夹。进去查看 .command.sh(实际执行的命令)、.command.out(标准输出)、.command.err(错误输出)。这些文件在调试阶段非常有用。我曾经靠检查 .command.err 发现一个软件版本不对的问题,节省了整整一天。