编写第一个流程:从 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。有三点需要注意:

  1. 变量引用:Nextflow 变量用 $variable 或 ${variable},和 Bash 是一样的。但如果你想在脚本中直接用 Bash 变量,就要小心不要跟 Nextflow 变量冲突。
  2. 引号选择:三引号 """ 允许内部使用双引号和单引号。如果你需要转义美元符号,用 \$。
  3. 多行命令:脚本块可以包含多行命令,只要用 ; 或换行分隔就行。

看一个多命令的例子:

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 会记录三部分日志:

  1. .nextflow.log - 主日志文件,记录框架级别的事件。
  2. 执行器日志 - 每个 process 的标准输出和标准错误,放在工作目录下。
  3. 进程的 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 从定义到运行到日志查看的路径画了出来。我画它的时候特意把工作目录和日志文件放进去,因为我发现很多初学者做完「提交脚本」之后就不知道中间发生了啥。

process 生命周期:定义 → 运行 → 日志 1. process 定义 input / output / script 声明 2. 提交运行 nextflow run main.nf 3. 工作目录生成 work/hash/… 4. 脚本执行 .command.sh → 输出文件 5. 日志查看 .command.err / .command.out 6. 流程完成 ✅ 100% 1 of 1 关键观察点 工作目录内的 .command.sh / .command.err / .command.out 是调试主战场

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,就跑一遍,检查日志和输出,确认无误后再往下画。这样不容易跑偏。