4. 进程与输入输出定义:process 的 input、output、script、publishDir 等关键指令详解

讲到 Nextflow 的 process,说白了就是整个流程最基本的执行单元。每个 process 就像生物信息学里的一个“函数”——给它什么原料(input),它输出什么产物(output),中间用什么工具处理(script),最后产物放哪(publishDir)。这四个指令几乎占据了每个 .nf 文件的 80% 内容。下面我就把这四个核心指令掰开揉碎讲清楚。

章节核心图:Process 的输入-处理-输出-发布生命周期

Process 生命周期:输入 → 脚本执行 → 输出 → 发布 input 定义数据来源 script 执行实际命令 output 声明产出文件 publishDir 发布到目标目录 工作目录 (work/) 临时存储 最终结果目录

4.1 input:定义进程的输入数据

input 块是 process 的“门口”。它决定了这个进程要接收什么样的数据,以及这些数据以什么形式进入。

我刚开始写 Nextflow 时犯过一个低级错误:以为 input 可以随便写,结果 run 的时候提示找不到变量。后来才明白,input 里的变量名必须和后面的 script 里保持一致。

4.1.1 基本声明方式

process FASTQC {
    input:
    path reads

    script:
    """
    fastqc ${reads}
    """
}

上面这个例子,reads 就是一个 path 类型的输入。什么意思呢?它会接收一个文件路径,并在进程执行时自动 Stage 到工作目录。

4.1.2 常见的输入类型

类型说明我常用的场景
val接收任意值(字符串、数字、列表等)传递样本名称、测序平台信息
path接收文件或目录路径FASTQ、BAM、参考基因组文件
env接收环境变量设置临时路径或软件版本
stdin接收标准输入流管道数据传输
tuple接收组合值(常见于多文件输入)成对的 R1/R2 FASTQ,或样本名+文件

我的个人习惯:只要是文件类输入,几乎只用 path,不用 val 来包装文件路径。因为 path 会自动处理文件 Stage 和权限,而 val 不会。曾经有个项目,我把文件路径当成 val 传进去,结果在集群上跑时一直报“文件不存在”——因为它根本没把文件拷贝到工作目录。

4.1.3 多输入与命名规范

一个 process 可以接受多个输入。每个输入占一行,写清楚类型和变量名。

process ALIGN {
    input:
    path index_dir
    tuple val(sample_id), path(reads)

    script:
    """
    bwa mem ${index_dir}/genome.fa ${reads} > ${sample_id}.sam
    """
}

请注意:path 类型后面的变量名,在 script 里直接引用就行(比如 ${reads})。但如果是 tuple 里的变量,得用括号拆开。

4.2 output:声明进程的产出

output 块告诉 Nextflow:“我这个进程会产生哪些东西,后面要用的话怎么拿”。这就像给流水线画了一个产出口。

4.2.1 基本声明方式

process FASTQC {
    input:
    path reads

    output:
    path "*.html", emit: html_report

    script:
    """
    fastqc ${reads}
    """
}

output 里的 path "*.html" 表示:进程结束后,工作目录中所有 .html 文件都会被收集起来,并通过 emit 名称为 html_report 的通道暴露给下游。

4.2.2 emit 通道命名

我个人强烈建议每个 output 都加上 emit 别名。为什么?因为这样可以按名称引用产出,而不是靠顺序。

process TRIM {
    input:
    path raw_fastq

    output:
    path "*.trimmed.fastq.gz", emit: trimmed_fastq
    path "trimming_report.txt", emit: report

    script:
    """
    trimmomatic SE ${raw_fastq} ${raw_fastq.baseName}.trimmed.fastq.gz \\
        LEADING:3 TRAILING:3 SLIDINGWINDOW:4:15 MINLEN:36
    """
}

之后在其他 process 里,你就可以直接用 TRIM.out.trimmed_fastq 来拿数据,不用记它在 output 列表里的第几个位置。

我曾经踩过的坑:output 里写的 wildcard 要足够精确。比如你想输出 .bam 文件,但工作目录里可能也有 .bam.bai 或其他同后缀文件。当时为了图方便写了 path "*.bam",结果把索引文件也一起抓进来了,导致下游 process 逻辑混乱。建议用 path "*.sorted.bam" 或更具体的模式。

4.2.3 输出路径控制

默认情况下,process 的输出直接从工作目录里找。但如果你想控制输出文件的相对路径(比如放在子目录里),可以在 script 里自己 mkdir。

process SPLIT {
    input:
    path bam_file

    output:
    path "chunks/*.bam", emit: bam_chunks

    script:
    """
    mkdir -p chunks
    samtools split ${bam_file} -o chunks/chunk_%*.bam
    """
}

output 里写 path "chunks/*.bam" 就能捕捉到子目录里的文件。

4.3 script:编写执行逻辑

script 块是 process 的“引擎”。它决定了工作目录里最终跑什么命令。你写的所有 shell 命令都放在这里。

4.3.1 多行脚本与缩进

process VARIANTCALL {
    input:
    path bam
    path reference

    output:
    path "*.vcf.gz", emit: vcf

    script:
    """
    bcftools mpileup -f ${reference} ${bam} | \\
        bcftools call -mv -Oz -o variants.vcf.gz
    tabix -p vcf variants.vcf.gz
    """
}

这里要注意:script 块本身是字符串,所以里面所有 shell 变量、命令、管道符都直接写。Nextflow 只会替换 ${xxx} 形式的变量。

4.3.2 字符串拼接与变量替换

在 script 里,有两种方式引用 input/output 变量:

  • ${variable}:标准替换,推荐使用。比如 ${reads} 会被替换成 reads 的实际路径。
  • $variable:简化写法,但容易和 shell 变量混淆。我自己只在纯环境变量上用这种形式。

我的建议:脚本内部如果需要使用 shell 变量(比如循环计数器、临时文件名),请先用反斜杠转义 \$var,这样 Nextflow 就不会尝试替换它。之前在数组索引时忘了加反斜杠,结果 debug 了好久才发现是变量被提前替换了。

4.3.3 多语言脚本

默认 script 块是 bash。但你可以切到其他语言:

process PYTHON_SCRIPT {
    input:
    path data

    output:
    path "result.csv"

    script:
    """
    #!/usr/bin/env python3
    import pandas as pd
    df = pd.read_csv("${data}")
    df_summary = df.describe()
    df_summary.to_csv("result.csv")
    """
}

直接在 script 块里写 Python 或 R 脚本,Nextflow 会把它当作多行字符串传给 shell 执行。

4.4 publishDir:把产出发布到最终目录

process 默认的产出都留在 work/ 目录里,目录名是一串哈希。publishDir 就是你指定“真正的结果该放哪”的地方。

4.4.1 基本用法

process FASTQC {
    input:
    path reads

    publishDir "results/fastqc", mode: 'copy'

    output:
    path "*.html"

    script:
    """
    fastqc ${reads}
    """
}

这样所有 output 声明里的文件(*.html)都会被复制到 results/fastqc/ 下。注意,publishDir 默认是符号链接(symlink),如果你设成 mode: 'copy' 就会复制实际文件。

4.4.2 mode 参数对比

mode行为适用场景
link创建符号链接(默认)开发和调试阶段,节省磁盘
copy复制文件正式提交结果,确保完整性
move移动文件(工作目录中不再保留)磁盘空间紧张时,但我不太推荐
copyNoFollow复制实际文件,不跟随链接处理符号链接指向的大文件时用

4.4.3 条件发布与路径控制

在项目中,我经常要根据样本或参数不同,把结果发布到不同的子目录。可以用 publishDir 里的模式字符串来动态控制:

process QUANT {
    input:
    tuple val(sample_id), path(bam)

    publishDir "results/${sample_id}/quant", mode: 'copy', pattern: "*.tsv"

    output:
    path "*.tsv"
    path "*.log"

    script:
    """
    salmon quant -t transcripts.fa -l A -a ${bam} -o .
    """
}

publishDir 里的 pattern 参数可以只发布特定文件(比如这里只发布 .tsv,不发布 .log)。

避坑指南:我曾经给多个样本共用同一个 publishDir,结果因为并发写入,文件内容被覆盖了。后来改用 ${sample_id} 动态目录,每个样本撒到自己的子文件夹里,才彻底解决冲突。如果你有大量并行任务,千万别懒——每个任务的结果单独放。

4.5 四个指令的组合使用示例

最后,我放一个完整的实战例子,把 input、output、script、publishDir 串起来。这是一个常见的 RNA-seq 定量流程:

process SALMON_QUANT {
    tag "${sample_id}"
    publishDir "results/${sample_id}/salmon", mode: 'copy', overwrite: false

    input:
    tuple val(sample_id), path(reads)

    output:
    path "quant.sf", emit: salmon_quant
    path "aux_info/", emit: aux_info

    script:
    """
    salmon quant \\
        -i /ref/salmon_index \\
        -l A \\
        -1 ${reads[0]} -2 ${reads[1]} \\
        -o . \\
        --gcBias \\
        --seqBias
    """
}

在这里:

  • input 接收一个 tuple,里面有样本 ID 和成对的 reads 文件
  • output 声明了两个产物:主定量结果 quant.sf 和辅助目录 aux_info
  • script 执行 salmon 命令,所有替换变量都带 ${}
  • publishDir 把结果复制到以样本名命名的目录里,并且不覆盖已有文件

这四大指令搭配好,你就可以构建出稳定、可重复、结果清晰的管道了。记住:input 管好入口,output 管好出口,script 管好执行逻辑,publishDir 管好结果存放。我的经验是,每写一个 process 都先把这四个块定好,后面的嵌套逻辑就不会乱套。