2. DSL1 基础语法:进程、流程、参数、变量的基本定义与使用
从零开始写 Nextflow 脚本,我个人觉得最关键的起步就是搞清楚 DSL1 的几个基础元素。说白了,你只需要掌握四个东西:进程、流程、参数 和 变量。这就像盖房子要认识砖、水泥、图纸和尺寸一样。本节我会带着你逐个拆解,顺便分享一些我踩过的坑。
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 传递数据必须用通道。
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 脚本,不要外包装。
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 中四个基础元素的全部内容。参数让你灵活控制输入,变量让你暂存中间对象,进程是实际干活的人,通道把所有人串联成流水线。把这几个点吃透,你就可以自己动手写简单的数据处理流水线了。遇到问题别怕,多在通道上 .view(),很快就能找到根源。