4、基础语法:进程定义,输入输出声明,脚本块

进程(process)是 Nextflow 里最基本的执行单元。说白了,每个进程就像一个小车间——原料从 input 门进来,经过 script 车间的加工,再从 output 门出去。我刚开始学的时候,总把这套流程跟函数调用搞混,但其实它更像是一个可以独立调度的小任务。

咱们这一节就把进程的骨架拆开看。嗯,主要就三块:定义骨架、声明输入输出、写执行脚本。掌握了这些,你就能写出第一个真正能跑的 Nextflow 流水线。

4.1 进程定义的基本结构

一个进程从 process 关键字开始,后面跟着名称和大括号。别小看这个名称——它会出现在日志、报表和中间目录里,所以尽量起有含义的名字。

process my_task {
    // 指令区(可选):资源、容器、调度参数等
    // 输入声明
    input:
    // 输出声明
    output:
    // 脚本块
    script:
    """
    bash 命令
    """
}

我个人习惯把指令区放在最上面,然后是 input、output,最后是 script。Nextflow 其实不强制顺序,但团队协作时统一风格能省很多麻烦。我见过有人把 script 写在 input 前面,结果同事改代码时找了半天……

4.2 input 声明:告诉进程你要吃什么

input 块里定义进程“消费”的数据。常见的声明方式有四种:文件、值、路径、元组。我把它们的典型用法整理在下面这张表里,方便你对照。

类型关键字示例说明
文件filefile("input.bam")接收单个文件,严格匹配名称或通配符
路径pathpath(sample_dir)1.4 以后推荐,支持目录和灵活匹配
值valval(sample_id)接收任何 Java/Groovy 对象,比如字符串、数字
元组tupletuple(val(id), path(reads))将多个输入打包成一个整体,常用于 pair-end 数据

注意:input 声明的顺序会被 script 里的 $input 引用顺序匹配。我早期吃过亏——调换了 input 里两个文件的顺序,结果脚本里 $1 和 $2 全反了。嗯,所以建议你养成习惯:按脚本中出现的先后顺序排列 input。

process align_reads {
    input:
    path genome_index
    path reads_fastq
    val sample_name

    script:
    """
    bwa mem $genome_index $reads_fastq > ${sample_name}.sam
    """
}

4.3 output 声明:生产出来的东西怎么运出去

output 块语法和 input 几乎对称。一个进程可以有多个输出,通过 emit 关键字给输出命名,后面你就能用 .out 通道来引用它了。

process filter_variants {
    input:
    path vcf

    output:
    path "*.filtered.vcf", emit: filtered_vcf

    script:
    """
    vcftools --vcf $vcf --minQ 30 --recode --out filtered
    """
}

这里有个要点:output 路径是“相对于执行目录”的。如果你在脚本里 cd 到了一个子目录,那么 output 里写的路径要基于那个子目录。我曾经因为没注意这个,结果输出的文件总是找不到,浪费了半小时调试。所以建议尽量用绝对路径或保持工作目录整洁。

4.4 script 块:真正的干活代码

script 块里放的是实际执行的命令。默认是 Bash 脚本,但你可以通过 shell 指令改成其他解释器。Nextflow 会用 """(三个双引号)把这个字符串传递到执行环境里。

变量插值是用 $variable 或 ${variable} 的形式。如果字符串里本来就带美元符号(比如 Bash 的 $PATH),记得用反斜杠转义或改用单引号。我刚开始写的时候,经常在 $$ 和 \$ 之间犯迷糊,后来干脆定了个规矩:所有 Nextflow 变量用 ${} 括起来,Bash 系统变量用 $ 并转义。

process count_reads {
    input:
    path fastq
    val sample_name

    script:
    """
    total=\$(zcat $fastq | wc -l)   # Bash 变量用 \$ 转义
    echo -e "$sample_name\t\$total" > counts.txt
    """
}

另外,script 块里可以写多行命令,用分号或换行都行。但如果你需要写复杂逻辑(比如 if/for 循环),强烈建议写成一个独立的脚本文件,然后用 script: 来调用它。这样既清晰又方便本地测试。

4.5 一个完整的进程示例

我把上面几块拼起来,写一个真正能用的进程:从 fastq 文件里统计序列条数,输出一个计数文件。

process count_seq {
    publishDir './results/', mode: 'copy', overwrite: true

    input:
    path fastq_file
    val sample_id

    output:
    path "counts_${sample_id}.txt"

    script:
    """
    seq_count=\$(zcat $fastq_file | echo \$((\$(wc -l)/4)))
    echo -e "$sample_id\t\$seq_count" > counts_${sample_id}.txt
    """
}

这个进程用 publishDir 把结果复制到统一的结果目录。注意我在 script 里混合使用了 Nextflow 变量 $sample_id 和 Bash 变量 \$seq_count —— 只要转义对了,完全没问题。

4.6 避坑指南

  • input/output 里的文件路径不是实际路径,而是声明式的。最终实际路径由 Nextflow 的工作目录管理。你只要关心输入通道里的数据,别硬编码绝对路径。
  • script 块里的 $ 引用:想引用 Nextflow 变量用 $var,想输出字面 $ 用 \$。我团队里有个新同事没注意,在脚本里写了 $HOME,结果 Nextflow 把 $HOME 当成变量替换了,后来发现根本不是想要的路径。
  • 多进程串联时,output 的名称要一致:比如下游进程的 input 会匹配上游 output 的 emit 名称,命名错了链就断了。
  • 不建议在 script 里写 Python 或 R 的复杂逻辑:把脚本内容单独写成 *.py 文件,然后用 python script.py $input 方式调用,既便于调试,也方便其他人复用。

4.7 本章知识结构图

下面这张图把进程的组成和数据流向画出来了,你可以把它贴在旁边当速查。

Nextflow 进程内部结构 process my_task { ... } input: output: script: """ 命令序列 """ executor: local/docker/sge... input 声明消费数据 output 声明产生结果 script 块写执行命令 executor 决定在哪里运行

4.8 小结

这一章我们啃完了进程的三大核心块:input、output、script。记住它们就像数据流水线上的三段——进料口、出料口、加工机器。别小看这三块,Nextflow 里 90% 的写代码时间都在和它们打交道。你可能会觉得目前这些例子还很简单,别急,后面几章我们会加入条件分支、错误重试、动态资源分配,到时候你就知道今天打的基础有多重要了。

小提示:创建一个新流水线时,我建议先写一个最简单的进程(只做 echo),跑通后再逐步添加 input/output。这样出问题能快速定位是语法错误还是逻辑错误。