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 做的事情远不止打印一行字。它可以是任何命令行、脚本、甚至是调用外部工具。

我来给你讲讲它内部是怎么运作的:

  1. Nextflow 为每个 process 创建一个独立的工作目录(work/ 下)。
  2. 把 script 块里的内容写入一个临时脚本文件。
  3. 在隔离的环境中执行该脚本(默认就是本地 shell)。
  4. 捕获 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脚本的核心要素:

第一个Nextflow脚本核心结构 workflow 块 process sayHello 内部结构 • 独立工作目录 • script 块:命令序列 • 捕获 stdout/stderr output 声明 返回值 • 标准输出 (stdout) • 文件、数值等 命令行启动 每个process都是独立单元,通过workflow组装成流水线

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跑通之后,试着改一改:比如加个参数让用户自定义问候语,或者再加一个进程来计算字符串长度。动手改一下,比单纯看十遍都有用。