第3章:Hello World——你的第一个Nextflow Pipeline

聊了那么多概念,不如直接上手跑一个。

我个人习惯,学任何新工具都先从最简示例开始。不是Hello World就是打印个日期。Nextflow也不例外。这一章我们就写一个真正的pipeline——虽然只有一步,但五脏俱全:有process、有workflow、有输入输出、还能看到日志。

3.1 先搭个脚手架

打开终端,新建一个目录。我习惯把每个pipeline单独放一个文件夹,避免文件乱飞。

mkdir hello_nextflow
cd hello_nextflow

然后创建一个文件,就叫main.nf。Nextflow脚本的默认后缀是.nf,你改成.nf或.nextflow都行,但社区约定用.nf。

嗯,这里要注意,文件名别用中文,也别带空格。我在项目里见过有人用了我的第一个pipeline.nf,结果解析路径出了各种奇怪问题。

3.2 写一个process

Process是Nextflow里最核心的执行单元。说白了,它就是一段可以被独立调度、监控、重试的脚本块。你可以把它理解成一个函数——接收输入,干活,吐输出。

写一个最简单的process,接收一个字符串,然后打印出来。

process sayHello {

    input:
    val greeting

    output:
    stdout

    script:
    """
    echo "$greeting"
    """
}

解释一下:

  • input: 声明这个process接收一个名为greeting的值(val类型)。
  • output: 声明输出是标准输出stdout,也就是打印到终端的文本。
  • script: 里面的内容会被当作Shell脚本执行。$greeting会被替换成实际传入的值。

为什么用val而不是file?

因为我们的输入是一个简单的字符串,不是文件。val适用于标量值(字符串、数字、布尔等),file适用于文件路径。后面章节你会频繁用到file。

3.3 定义workflow块

Nextflow 22.04版本之后,官方推荐使用workflow块来组织pipeline的执行逻辑。老版本用workflow { }也可以,但新特性都在这个块里。

在我们的workflow块里,调用刚才定义的sayHello process,传入一个问候语。

workflow {

    greeting = "Hello, Nextflow!"
    sayHello(greeting)
}

注意,process名sayHello被当作一个函数来调用,参数就是我们在input里声明的greeting。Nextflow会自动匹配——按顺序对应的。

我当时第一次写的时候,以为要像Python那样写sayHello(greeting=greeting),结果直接报错。Nextflow的语法糖比我想象的简洁很多。

3.4 完整的Pipeline代码

把上面两部分拼到一起,加上一个#!/usr/bin/env nextflow的shebang(其实不是必须的,但加了好识别类型)。

#!/usr/bin/env nextflow

process sayHello {

    input:
    val greeting

    output:
    stdout

    script:
    """
    echo "$greeting"
    """
}

workflow {

    greeting = "Hello, Nextflow!"
    sayHello(greeting)
}

保存好main.nf,准备运行。

3.5 跑起来,看日志

执行命令非常简单:

nextflow run main.nf

你会看到类似下面的输出:

N E X T F L O W  ~  version 24.04.2
Launching `main.nf` [angry_gauss] DSL2 - revision: 1234abcd

executor >  local (1)
[30/a1b2c3] process > sayHello (1) [100%] 1 of 1 ✔

Hello, Nextflow!

每一行都有信息量:

  • 版本号:确保你用的是24.x系列,否则有些语法可能不兼容。
  • 执行ID:[angry_gauss]——每次运行Nextflow都会随机生成一个绰号,方便你区分不同的运行实例。这在并行跑多个实验时非常有用。
  • revision:git commit的hash,如果代码在git仓库里的话。
  • executor:默认是local,即在本地机器上运行。后面你会学到如何切到SLURM、AWS Batch等。
  • 进程状态:[30/a1b2c3] process > sayHello (1) —— 这个哈希是进程的工作目录路径,可以在work/目录下找到它。
  • 完成百分比:[100%] 1 of 1 ✔——任务完成状态。如果失败会显示✘。

小技巧:如果你想看到更详细的日志,加-ansi-log false参数,输出会更适合重定向到文件。

nextflow run main.nf -ansi-log false > run.log 2>&1

3.6 work目录里有什么?

运行结束后,去work/目录看看。

ls work/
ls work/30/a1b2c3/

你会看到类似这样的结构:

work/
└── 30/
    └── a1b2c3/
        ├── .command.sh
        ├── .command.out
        ├── .command.err
        ├── .command.log
        └── .exitcode

每一个process执行时,Nextflow都会在work/下创建一个唯一的目录(由哈希生成)。里面存了:

  • .command.sh:实际执行的shell脚本,就是你在script块里写的内容。
  • .command.out:标准输出(stdout)。
  • .command.err:标准错误(stderr)。
  • .command.log:完整日志,包括环境变量等。
  • .exitcode:退出码,0表示成功,非0表示失败。

我在项目中遇到过一个问题:某个process一直报错,但看终端日志只显示✘,根本没有具体错误信息。最后我直接去work/目录下翻.command.err文件,才发现是某个依赖的二进制文件没装——排查起来非常高效。

注意: work/目录会随着每次运行不断膨胀。如果磁盘空间吃紧,可以用nextflow clean -f清理。也可以设置.nextflow/config里的workDir指向一块独立的大容量盘。

// 在 nextflow.config 中配置工作目录
workDir = "/scratch/nextflow_work"

3.7 一张图看懂这个Pipeline

下面这张图展示了我们这个Hello World Pipeline的完整结构。

Nextflow Hello World Pipeline 结构 workflow { } 主程序块 定义变量 greeting = "Hello, Nextflow!" process sayHello { } input: val greeting 接收字符串值 output: stdout 输出标准输出 script: echo 执行Shell命令 终端输出:Hello, Nextflow! work/ 目录下生成 .command.sh 等文件

3.8 总结一下关键点

组件 作用 小提示
process 定义可执行单元,封装输入、输出、脚本 每个process可以独立并行、重试
input 声明process接收的数据类型 val用于标量,file用于文件,path用于路径
output 声明process输出的数据类型 stdout是标准输出,也可以用file指定文件
script 真正执行的Shell脚本段落 支持变量插值,但注意特殊符号转义
workflow { } 主入口,连接各个process 22.04版本后必须用这个块
work/目录 存放每个process的执行痕迹 调试时第一站

你看,其实就这么简单。一个Hello World pipeline,不到二十行代码,却把Nextflow最核心的概念全跑通了:process声明、workflow组织、数据流传递、工作目录机制。

跑通了这个示例,你就可以开始玩更复杂的了——比如让process接收文件作为输入,或者让多个process串联起来形成链条。下一章我们就搞点实际的。不过在那之前,我建议你多改改这个Hello World:换个问候语,加一个process,试试传文件——自己动手踩坑,才是最快的成长方式。

一句话记住这章:

一个process就是一段被Nextflow封装好的Shell脚本,workflow块就是编排这些脚本的舞台。