WDL基础语法:任务的定义、输入输出与命令块
说实话,WDL 的 task 是整个 pipeline 的基石。你想想看,无论多复杂的串联逻辑,落到最后都是一个个 task 在干活。我刚开始写 WDL 时,总觉得 workflow 最重要,后来踩了好几次坑才发现——task 定义得稳不稳,直接决定整个 pipeline 能不能跑通。
这一章咱们就专门啃 task。我保证,看完你能写出干净、可复用、不出幺蛾子的 task。
task 的骨架:声明、输入、输出、命令
一个 task 说白了就是四个部分:
- 输入声明:告诉 Cromwell 你需要什么文件或参数
- 命令块:真正执行的 shell 脚本或程序
- 输出声明:从运行结果中提取哪些文件或值
- 运行时属性:指定内存、CPU、容器等资源(后面章节细讲)
我贴个最简例子,你先感受下:
task run_fastqc {
input {
File read1
String sample_name
}
command {
fastqc -o ${sample_name}_fastqc ${read1}
}
output {
File html_report = "${sample_name}_fastqc.html"
File zip_report = "${sample_name}_fastqc.zip"
}
}
看到没?task 以 task 关键字开头,后面跟任务名。输入用 input {} 包着,输出用 output {}。命令块直接写 shell 命令,用 ${} 引用输入的变量。我个人习惯先声明输入,再写命令,最后声明输出——这样读起来逻辑最顺。
输入声明的几种写法
WDL 的输入类型比很多人想的要丰富。我最早以为只支持 File 和 String,后来发现 Int、Float、Boolean、Array、Map 全都可以。下面这张表你收好:
| 类型 | 写法示例 | 说明 |
|---|---|---|
| File | File bam_file | 单个文件,Cromwell 会自动 staging |
| String | String sample_id | 字符串,常见于命名、传递参数 |
| Int | Int threads | 整数,比如线程数、质量阈值 |
| Float | Float p_value | 浮点数,很少用但偶尔逃不掉 |
| Boolean | Boolean skip_dedup | 布尔值,控制流程开关 |
| Array[File] | Array[File] fastq_list | 文件数组,多样本批量输入 |
| Map[String, File] | Map[String, File] ref_files | 键值对,关联样本与文件 |
嗯,这里要注意:所有输入变量名在 task 内必须唯一。我见过有人一个 task 里声明了两个同名的变量——Cromwell 直接报错,查了半天才找到问题。
命令块的核心写法
命令块是 task 的灵魂。为什么?因为实际干活的就是它。你声明的所有输入,最终都要在命令里发挥作用。
最基本的引用方式我用过两种:
${variable_name}—— 最常用,兼容性好~{variable_name}—— WDL 的另一种写法,我很少用,因为容易和 shell 的~混淆
来看一个多输入组合的例子:
task bwa_mem {
input {
File reference
File read1
File read2
String sample
Int threads = 4
}
command {
bwa mem \
-t ${threads} \
-R "@RG\\tID:${sample}\\tSM:${sample}" \
${reference} \
${read1} \
${read2} \
> ${sample}.sam
}
output {
File sam_file = "${sample}.sam"
}
}
重点:命令块里的 ${threads} 和 ${sample} 会在运行时被替换成实际值。如果输入有默认值(比如 Int threads = 4),调用方不传也没问题。这是个省心的设计,我项目里 80% 的 task 都设了默认值。
输出声明的两种模式
搞明白输出,你的 task 才算完整。输出声明的语法其实就一行:变量类型 + 变量名 + 等号 + 表达式。
最常见的两种模式:
- 字面路径匹配:直接写文件名或路径,命令块里必须产出这个文件
- 通配符匹配:用
glob捕获多个文件
举两个例子,你一看就明白:
task collect_metrics {
input {
File bam
String prefix
}
command {
samtools flagstat ${bam} > ${prefix}_flagstat.txt
samtools stats ${bam} > ${prefix}_stats.txt
}
output {
# 字面路径:文件名必须和命令块严格一致
File flagstat = "${prefix}_flagstat.txt"
# 通配符:匹配所有 .txt 文件
Array[File] all_reports = glob("${prefix}_*.txt")
}
}
避坑指南:我曾经在输出声明里写了个 File result = "${prefix}_result.bam",但是命令块里产出的文件名有个小小的拼写错误——多写了一个下划线。Cromwell 不会帮你检查文件名是否真的存在,它只会等命令执行完,然后去找声明里的那个文件。找不到?直接 job fail。所以我现在的习惯是:写完输出声明后,回头看一眼命令块里的文件名,逐字核对。
把 task 放到 workflow 里跑
光定义 task 还不够,你得用 workflow 叫它。看这段:
workflow my_pipeline {
input {
File fastq
}
call run_fastqc {
input:
read1 = fastq,
sample_name = "sampleA"
}
}
调用语法很简单:call 任务名 { input: 变量赋值 }。赋值时用冒号,我一开始老写成等号,被 Cromwell 骂了好几次。
本章知识结构
下面这张 SVG 图帮你把 task 的生命周期串起来:
写 task 时我常犯的 3 个错误
说几个我踩过的坑,你绕过去就行:
- 输出文件名写错:命令块产出的文件名,和输出声明里的名字,差一个字符都不行。我后来养成了习惯——先跑个最小用例验证。
- 忘记给输入设默认值:有些参数比如线程数,95% 的情况都用同一个值。设个默认值能省很多事。
- 命令块里的变量引用写成了 shell 风格的
$var:WDL 要用${var}。如果写成$var,Cromwell 不会报错,但变量不会被替换——你得到的是一个奇怪的文字串。
我个人建议:每写一个 task,先用 java -jar womtool.jar validate my_task.wdl 做语法校验。这个工具能帮你捕获 90% 的低级错误,省得跑 Cromwell 时才报错,白白浪费时间。