编写第一个流程:从 process 到日志
终于到了动手写流程的环节。说实话,我第一次接触 Nextflow 的时候,最困惑的就是「这玩意儿到底怎么跑起来的?」。光看文档,总觉得差口气。后来我带着一个最简单的任务——把文件从 A 复制到 B——从头写了一遍,才真正把 process、输入输出、脚本块这些东西串起来。
这一章,我们就干这一件事。定义 process,声明输入输出,写脚本块,然后提交运行,再回头翻日志。你跟着我走一遍,心里就有底了。
4.1 什么是 process?
process 是 Nextflow 流程的「最小执行单元」。说白了,每个 process 就是一段独立的可执行脚本,外加它需要什么、产出什么。你可以把它理解成一个函数——不过这个函数可以跑在本地、集群、甚至是云端。
一个 process 长这样:
process PROCESS_NAME {
[ 指令部分 ]
input:
[ 输入声明 ]
output:
[ 输出声明 ]
script:
"""
[ 要执行的命令或脚本 ]
"""
}
注意几个关键点:
- process 关键字后跟 process 名,名称为大写约定(非强制,但我个人建议都用大写,一眼就能区分 process 和变量)。
- 指令部分是可选的,用来设置资源、容器、标签等。
- input/output 声明决定了 process 之间怎么传数据。
- script 块里面放的是真正的执行命令,用三引号包裹。
我个人习惯把 process 定义看作一个「黑盒声明」:你告诉 Nextflow:给我这些输入,然后执行这段脚本,最后输出那些东西。至于它什么时候跑、在哪跑、跑几次——由流程的依赖关系和参数决定。
4.2 第一个 process:copy_file
我们从最简单的任务开始:把一个文件从输入路径复制到输出路径。这个 process 不需要任何外部工具,只用 cp 命令就够了。
先写一个 main.nf 文件:
#!/usr/bin/env nextflow
nextflow.enable.dsl=2
process COPY_FILE {
input:
path input_file
output:
path "copied_${input_file}"
script:
"""
cp $input_file copied_${input_file}
"""
}
workflow {
COPY_FILE( file('/path/to/your/sample.txt') )
}
让我解释一下这里每行的含义:
#!/usr/bin/env nextflow- Shebang 声明,让系统知道这是 Nextflow 脚本。nextflow.enable.dsl=2- 启用 DSL2。Nextflow 有两个语法版本,DSL2 是目前的主流,推荐使用。process COPY_FILE- process 名,我习惯用大写。input: path input_file- 声明一个路径类型的输入,变量名是input_file。output: path "copied_${input_file}"- 声明输出路径,文件名是copied_加上原文件名。script: """ ... """- 脚本块,里面放要执行的 shell 命令。workflow { COPY_FILE( file('...') ) }- 流程入口,调用 process 并传入文件路径。
这里有个小坑。我在项目中第一次跑的时候,输出的文件名直接写了 copied_reads.fastq,结果输入变了名字,输出还是固定的。后来我改用 ${input_file} 动态生成,才真正「通用」起来。
4.3 输入输出声明:路径、值和文件
输入输出的声明方式决定了 process 之间的数据流。最常用的有三种:
| 声明方式 | 说明 | 示例 |
|---|---|---|
path |
文件或目录路径,会自动 staging(暂存)到工作目录 | input: path fastq |
val |
普通值,不会 staging,只在脚本中用变量引用 | input: val sample_id |
stdout |
捕获脚本的标准输出作为输出 | output: stdout |
区别在于:path 会自动把文件复制到 process 的工作目录下,而 val 只是一个字符串或数字的引用,不会动文件。
举个例子,你有一个样本 ID 列表和一个对应的 FASTQ 文件:
process PROCESS_READS {
input:
val sample_id
path reads
output:
path "processed_${sample_id}.txt"
script:
"""
echo "Processing $sample_id with $reads" > processed_${sample_id}.txt
"""
}
这里 val sample_id 只是一个字符串,而 path reads 会把文件 staging 过来。我个人建议:只要涉及文件操作,就用 path;只有元数据或标识符才用 val。
关于输出声明还有一个容易忽略的地方:输出文件的路径是基于 process 的工作目录的。你脚本写死了 ./result.txt,输出声明就必须是 path "result.txt" 或者 path "${name}.txt"。不能写绝对路径,否则 Nextflow 找不到文件,流程会报错——我曾经在这个上面花了一下午。
4.4 脚本块:格式、引号与变量
脚本块是 process 的心脏。Nextflow 的脚本块使用 Shell 语法,默认是 Bash。有三点需要注意:
- 变量引用:Nextflow 变量用
$variable或${variable},和 Bash 是一样的。但如果你想在脚本中直接用 Bash 变量,就要小心不要跟 Nextflow 变量冲突。 - 引号选择:三引号
"""允许内部使用双引号和单引号。如果你需要转义美元符号,用\$。 - 多行命令:脚本块可以包含多行命令,只要用
;或换行分隔就行。
看一个多命令的例子:
process QC {
input:
path fastq
output:
path "qc_report.txt"
script:
"""
fastqc $fastq -o .
echo "QC done for $fastq" > qc_report.txt
"""
}
你想想看,如果这里 Nextflow 变量和 Bash 变量混在一起会怎样?比如:
num_samples=3
echo "Total samples: $num_samples"
实际上,上面的 $num_samples 会被 Nextflow 当成自身变量去解析——如果这个变量不存在,脚本会报错。要解决这个问题,可以用 \$num_samples 转义,或者把 Bash 变量定义在脚本块外用 Nextflow 变量包裹。
我在项目里养成了一个习惯:脚本块里所有 $ 开头的变量,只要不是 Nextflow 的 input/output,都加反斜杠转义。这样可以避免很多莫名其妙的 bug。
4.5 运行你的第一个流程
有了 main.nf,就可以提交了。在终端里运行:
nextflow run main.nf
如果一切正常,你会看到类似这样的输出:
N E X T F L O W ~ version 24.04.0
Launching `main.nf` [prickly_poincare] DSL2 - revision: 1a2b3c4d5e
executor > local (1)
[21/3a45b6] process > COPY_FILE (1) [100%] 1 of 1 ✔
每一行解释一下:
executor > local (1)- 执行器是 local,代表在本机运行,有 1 个任务。[21/3a45b6]- 这个是进程的运行 ID,我在项目里经常用它来快速定位工作目录。process > COPY_FILE (1)- 执行的 process 名及其索引(第 1 个副本)。[100%] 1 of 1 ✔- 进度和状态。
注意:第一次运行 Nextflow 会自动创建一个 work 目录来存放所有临时文件、脚本和中间产物。这个目录可能会很大,我建议你在运行完毕后用 nextflow clean 清理,或者设置 NXF_WORK 环境变量指向临时分区。
4.6 日志查看:.nextflow.log 和过程日志
日志是排查问题的最佳帮手。Nextflow 会记录三部分日志:
- .nextflow.log - 主日志文件,记录框架级别的事件。
- 执行器日志 - 每个 process 的标准输出和标准错误,放在工作目录下。
- 进程的 stdout/stderr - 脚本块中打印的内容。
先说 .nextflow.log。它默认生成在流程启动目录,包含了每一步的详细时间、状态、任务 ID。你可以这样查看:
tail -n 50 .nextflow.log
输出大概像这样:
Jan-30 14:23:15.456 [Task submit] Submitted process COPY_FILE (1) - work/21/3a45b6...
Jan-30 14:23:15.712 [Task monitor] Process COPY_FILE (1) completed
Jan-30 14:23:15.713 [Workflow] Workflow complete - Succeeded
再说工作目录。你注意到运行输出里的 [21/3a45b6] 了吗?这就是工作目录的哈希路径:work/21/3a45b6...。进去看看:
cd work/21/3a45b6*
ls -la
里面至少有这几个文件:
.command.sh- 实际执行的脚本。.command.out- 脚本的标准输出。.command.err- 脚本的标准错误。.command.log- 包含 shell 本身的日志。sample.txt- 被 staging 过来的输入文件。copied_sample.txt- process 生成的输出文件。
我在项目中最常查的就是 .command.err。很多脚本运行时的报错都会写到这里,而不会在终端直接显示。比如你脚本里有个命令找不到,或者参数拼错了,去 .command.err 里一看就明白了。
有个小技巧:如果流程跑了一半中断了,想从失败点重跑,可以执行 nextflow run main.nf -resume。它会自动跳过已经成功的步骤,只重新运行失败的。我就是靠这个功能在修 bug 时代替了无数手动清理重跑。
4.7 用一个图总结 process 生命周期
下面这张 SVG 把 process 从定义到运行到日志查看的路径画了出来。我画它的时候特意把工作目录和日志文件放进去,因为我发现很多初学者做完「提交脚本」之后就不知道中间发生了啥。
4.8 一个完整的实战练习
最后,咱们把整章的知识点串成一个可运行的小流程。假设你有一个 data/ 目录,里面有几个样本的 FASTQ 文件,目标是打印每个样本的文件信息并生成汇总报告。
#!/usr/bin/env nextflow
nextflow.enable.dsl=2
params.input_dir = "$projectDir/data"
params.outdir = "$projectDir/results"
process GENERATE_REPORT {
input:
path fastq
output:
path "report_${fastq.baseName}.txt"
script:
"""
echo "Sample: ${fastq.baseName}" > report_${fastq.baseName}.txt
echo "Size: \$(stat --printf=%s $fastq)" >> report_${fastq.baseName}.txt
echo "Lines: \$(wc -l < $fastq)" >> report_${fastq.baseName}.txt
"""
}
workflow {
fastq_files = Channel.fromPath("${params.input_dir}/*.fastq").collect()
GENERATE_REPORT( fastq_files )
GENERATE_REPORT.out.collectFile(name: "summary.txt", storeDir: params.outdir)
}
这里多了一个 collectFile 操作,把所有 process 的输出合并到一个 summary.txt 里。好,跑一下:
mkdir -p data
echo "read1\nread2\nread3" > data/sample1.fastq
echo "read4\nread5" > data/sample2.fastq
nextflow run main.nf
跑完之后查看结果:
cat results/summary.txt
应该能看到每个样本的报告内容被合并到了一起。这就是 process + 通道的最基本用法。
我在项目里就是用这种「先打印信息、再逐步加分析命令」的方式来构建复杂流程的。每加一个 process,就跑一遍,检查日志和输出,确认无误后再往下画。这样不容易跑偏。