2. DSL1 基础语法:进程、流程、参数、变量的基本定义与使用

从零开始写 Nextflow 脚本,我个人觉得最关键的起步就是搞清楚 DSL1 的几个基础元素。说白了,你只需要掌握四个东西:进程、流程、参数 和 变量。这就像盖房子要认识砖、水泥、图纸和尺寸一样。本节我会带着你逐个拆解,顺便分享一些我踩过的坑。

DSL1 基础语法元素关系 DSL1 脚本 参数 (params) 通道 (Channel) 进程 (process) 变量 (def/var) 箭头表示数据流向:参数和通道为进程提供输入,进程输出通过通道传递,变量可绑定通道。

2.1 参数(params)—— 让脚本变灵活

参数是脚本里最方便的可变输入。我刚开始用 Nextflow 时总在脚本里硬编码文件路径,结果每次换数据都得改代码,后来发现 params 就是干这个的。

定义方式:直接在脚本顶部用 params.xxx 赋值。

params.in = "$projectDir/data/input.txt"
params.out_dir = "./results"
params.threads = 4

使用方式:在 process 或通道中引用 params.in 即可。

运行覆盖:命令行中用 --in other.txt 就能覆盖默认值。我有个同事曾经把参数名拼错,结果脚本用了默认值,折腾了两小时才找到原因。所以记住:参数名大小写敏感,而且一定要在脚本中先定义默认值。

小技巧:常用参数放在脚本顶部,并且加上注释说明用途。例如:// 输入文件路径 会让后期维护轻松很多。

2.2 变量(def/var)—— 脚本里的临时存储

变量用来存中间结果,比如文件列表、计算值或者通道对象。DSL1 里变量可以不用 def 直接赋值,但我个人习惯统一加 def,这样代码更清晰。

def sample_list = ["sample1", "sample2", "sample3"]
def threshold = 0.05
def input_ch = Channel.fromPath(params.in)

注意作用域:变量只在定义它的作用域内有效。我曾经在一个 process 内部定义了一个变量,想在外部用,结果一直报 null。后来才发现变量默认是局部作用域,想跨 process 传递数据必须用通道。

避坑指南:DSL1 中不要用 var 关键字,它不被识别。如果看到 var x = 1,手滑写成 Java 风格了?赶紧改成 def x = 1。

2.3 进程(process)—— 执行任务的最小单元

进程是 Nextflow 的核心。每个 process 就像一个独立的 shell 脚本盒子,有输入、输出和执行脚本。

基本结构:

process doSomething {
    input:
    path input_file

    output:
    path "result.txt"

    script:
    """
    echo "Hello $input_file" > result.txt
    """
}

关键点:

  • input:声明接收什么类型的数据,可以是文件、值、路径等。
  • output:声明输出什么文件或值,管道会自动收集。
  • script:用 """ 包裹的 shell 命令,变量用 $ 引用。
  • 标签 (label):可选,用来给进程打标签,便于资源分配和日志查看。

我曾经犯过一个错误:在 script 里用反引号包裹命令,结果 Nextflow 解析错误。记住,DSL1 里 script 必须用三引号 """,里面直接写 shell 脚本,不要外包装。

经验之谈:写进程时先想清楚 input 和 output 的类型。例如输入文件用 path,输出文件也用 path。如果输出多个文件,用 path "*.txt" 通配符,管道会收集成列表。

2.4 流程(通过通道串联进程)—— 构建流水线

在 DSL1 中,没有显式的 workflow 块。流程就是靠通道把进程的 input 和 output 连接起来。数据从起点一步一步流到终点。

典型模式:定义一个输入通道 → 传给第一个进程 → 第一个进程的输出通道传给第二个进程 → 以此类推。

// 定义输入通道
input_ch = Channel.fromPath(params.in)

process splitByLine {
    input:
    file input_file from input_ch
    output:
    file "chunk_*" into chunks_ch
    script:
    """
    split -l 1 $input_file chunk_
    """
}

process convertToUpper {
    input:
    file x from chunks_ch.flatten()
    output:
    stdout into result_ch
    script:
    """
    cat $x | tr '[a-z]' '[A-Z]'
    """
}

// 最终输出到控制台
result_ch.view { "RESULT: $it" }

关键语法:

  • from:从通道接收数据(必须写在 input 中)
  • into:将输出数据推送到指定通道
  • flatten():把文件列表展平为逐个元素(因为 chunks_ch 是一个文件集合,需要展平才能逐文件处理)

你看,没有 workflow{} 块,但流程已经出来了。我当时从 DSL1 过渡到 DSL2 时,最大的不适就是不再用 from/into,而是直接调用进程名。但 DSL1 的这种方式更贴近数据流图,对于管道调试特别直观。

调试技巧:在进程输出后面加 .view() 可以打印通道内容,看看数据流是否对。比如 chunks_ch.view() 就能看到 split 后产生了哪些文件。

2.5 综合示例:文件分片转大写

把上面的代码组合成一个完整脚本 main.nf:

#!/usr/bin/env nextflow
params.in = "$projectDir/data/input.txt"

input_ch = Channel.fromPath(params.in)

process splitByLine {
    input:
    file input_file from input_ch
    output:
    file "chunk_*" into chunks_ch
    script:
    """
    split -l 1 $input_file chunk_
    """
}

process convertToUpper {
    input:
    file x from chunks_ch.flatten()
    output:
    stdout into result_ch
    script:
    """
    cat $x | tr '[a-z]' '[A-Z]'
    """
}

result_ch.view { "RESULT: $it" }

运行命令:

nextflow run main.nf --in mydata.txt

结果会逐行打印转换后的大写内容。

重要事项:DSL1 已经逐渐被 DSL2 取代,但很多生产环境(尤其老项目)仍在使用。如果你开始一个新项目,我建议直接用 DSL2。但理解 DSL1 能帮你更好理解 Nextflow 的底层数据流模型。

以上就是 DSL1 中四个基础元素的全部内容。参数让你灵活控制输入,变量让你暂存中间对象,进程是实际干活的人,通道把所有人串联成流水线。把这几个点吃透,你就可以自己动手写简单的数据处理流水线了。遇到问题别怕,多在通道上 .view(),很快就能找到根源。