4. 进程与输入输出定义:process 的 input、output、script、publishDir 等关键指令详解
讲到 Nextflow 的 process,说白了就是整个流程最基本的执行单元。每个 process 就像生物信息学里的一个“函数”——给它什么原料(input),它输出什么产物(output),中间用什么工具处理(script),最后产物放哪(publishDir)。这四个指令几乎占据了每个 .nf 文件的 80% 内容。下面我就把这四个核心指令掰开揉碎讲清楚。
章节核心图:Process 的输入-处理-输出-发布生命周期
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 都先把这四个块定好,后面的嵌套逻辑就不会乱套。