第三章 WDL 基础语法:从文件骨架到命名习惯
开始写 WDL 之前,我们先搞清楚它的“骨架”长什么样。说白了,WDL 文件就是一个结构化的文本,你得告诉 Cromwell 三个东西:这是什么版本、整个流程怎么跑、每个步骤具体干什么。
我个人习惯把 WDL 文件拆成三块来看——版本、工作流定义、任务定义。理解了这三块,你就掌握了 WDL 80% 的语法。
3.1 文件结构与版本声明
WDL 文件的第一行必须是版本声明。为什么?因为 WDL 版本迭代较快,Cromwell 需要知道用哪个解析器来理解你的代码。我见过有人忘了写版本声明,结果 Cromwell 直接罢工,报了个奇怪的语法错误。
一个标准的 WDL 文件长这样:
version 1.0
workflow my_first_workflow {
input {
String sample_name
File reads
}
call run_alignment {
input:
reads = reads
}
output {
File aligned_bam = run_alignment.aligned_bam
}
}
task run_alignment {
input {
File reads
String reference = "hg38"
}
command {
bwa mem ${reference} ${reads} > aligned.sam
}
output {
File aligned_bam = "aligned.sam"
}
runtime {
docker: "biocontainers/bwa:v0.7.17"
memory: "4 GB"
cpu: 2
}
}
嗯,这里要注意:version 1.0 是当前最常用的版本。之前有 draft-2 和 draft-3,但现在已经很少有人用了。如果你看到老项目还在用旧版本,建议尽快迁移。
command {} 外面套 String out = "result" 这种写法。结果 Cromwell 2.x 版本完全不认,折腾了两天才全部改完。
3.2 workflow 块:流程的指挥中心
workflow 块是整个工作流的入口。你可以把它想象成一个导演,负责调度各个 task 的执行顺序、传递参数、汇总输出。
一个 workflow 块包含四个核心部分:
- input 块:定义工作流的输入参数,比如样本名、文件路径。
- call 语句:调用具体的 task,并传递参数。
- scatter 语句:并行处理多个输入(后面章节会细说)。
- output 块:定义工作流的最终输出。
我之前犯过一个低级错误,在 workflow 里直接写 call task_name { input: ... },但忘了定义 task 的 input 参数。Cromwell 会报“missing required input”错误,排查起来挺头疼的。
3.3 task 块:能干活的“小兵”
每个 task 代表一个具体的步骤。比如比对、排序、变异检测。task 块里必须包含 command 和 output,其他像 input、runtime、meta 是可选的。
来看一个典型的 task 结构:
task samtools_sort {
input {
File bam_file
String memory_gb = "4"
}
command {
samtools sort -m ${memory_gb}G -o sorted.bam ${bam_file}
}
output {
File sorted_bam = "sorted.bam"
}
runtime {
docker: "biocontainers/samtools:v1.15"
memory: memory_gb + " GB"
cpu: 2
}
meta {
description: "Sort a BAM file by coordinates"
}
}
你想想看,这里的关键是什么?command 块里的命令是直接传给 Docker 容器执行的。所以写的时候要确保容器里安装了对应工具,不然 Cromwell 会报“command not found”。
cat file | grep pattern | awk '{print $1}',结果在某个旧版 Docker 里 awk 版本不一致,输出了完全不同的结果。后来我养成了一个习惯——在 task 里尽量用显式命令,少用管道,或者明确指定工具版本。
还有一点,runtime 块里可以设置资源参数。Cromwell 会根据这些参数向云端申请资源。如果你不写 memory 和 cpu,Cromwell 会用默认值,但那个值往往偏小,跑大任务时会 OOM(内存溢出)。
3.4 命名规范:看上去很小的事,影响很大
命名规范这东西,很多人觉得无所谓。我刚开始写 WDL 时也这么想。直到有一天,我在一个项目里看到了 task task123、call call_call 这种命名,整个人都不好了。
WDL 的命名规则很宽松,但有几个硬性要求:
- 只能包含字母、数字、下划线。
- 不能以数字开头。
- 区分大小写,
SampleName和samplename是两个不同的变量。 - 建议使用 camelCase(驼峰命名),WDL 官方推荐这种做法。
一些常见的命名习惯对比:
| 推荐 | 不推荐 | 原因 |
|---|---|---|
run_alignment |
runAlignment123XYZ |
过于冗长,难以阅读 |
aligned_bam |
alignedBAM_output_v2_final |
版本号不需要放在名字里 |
sample_id |
Sample-Id |
连字符不合法 |
input_reads |
input |
过于通用,容易混淆 |
run_bwa_mem、sort_bam;输出变量用名词短语,比如 aligned_bam、sorted_bam。这样看代码时一目了然。
另外,WDL 里有几个保留关键字,不能用作变量名或 task 名,比如 workflow、task、call、input、output、scatter、if、then、else、runtime、meta、parameter_meta。
我见过有人把 task 命名为 call,结果 Cromwell 解析到 call call {} 时直接报 syntax error。你要记好了,这些关键字不能碰。
3.5 完整的 WDL 文件示例
把上面讲的东西串起来,一个完整的 WDL 工作流长这样:
version 1.0
workflow qc_pipeline {
input {
File fastq_file
String sample_id
}
call fastqc_analysis {
input:
fastq = fastq_file,
sample = sample_id
}
call multiqc_summary {
input:
fastqc_report = fastqc_analysis.html_report
}
output {
File qc_report = multiqc_summary.summary_html
}
}
task fastqc_analysis {
input {
File fastq
String sample
}
command {
fastqc -o /output ${fastq}
mv ${fastq}_fastqc.html ${sample}_fastqc.html
}
output {
File html_report = "${sample}_fastqc.html"
}
runtime {
docker: "biocontainers/fastqc:v0.11.9"
memory: "2 GB"
cpu: 1
}
}
task multiqc_summary {
input {
File fastqc_report
}
command {
multiqc . -o /output
}
output {
File summary_html = "multiqc_report.html"
}
runtime {
docker: "ewels/multiqc:v1.11"
memory: "2 GB"
cpu: 1
}
}
这个例子看起来简单,但包含了我们刚才讲的所有要点:版本声明、workflow 块、task 块、输入输出、命名规范。你可以把它当作一个模板,以后写 WDL 文件就照着这个结构来。
这张图把 WDL 文件的结构串起来了。你看,顶层是文件,下面分出三个主要部分:版本声明、workflow 块、task 块。workflow 块内部有 input、call、output;task 块内部有 command、output、runtime。再加上命名规范这个贯穿始终的“软性要求”,就构成了完整的 WDL 基础语法。
我个人建议,初学阶段不用急着背语法细节。你只需要记住 WDL 文件的这个骨架结构,然后对照着写几个小例子,很快就能上手。我在项目里教新人写 WDL 时,都是让他们先抄这个结构,再慢慢加功能。