任务定义:task 结构、运行时参数、输入输出声明
好,咱们开始深入 WDL 的核心——task。说白了,WDL 里的每个工作单元就是一个 task。你可以把它理解成一个“脚本任务盒子”:你把输入放进去,告诉它用什么工具跑、配多少资源,它执行完再把输出还给你。
我个人习惯把 task 分成三个部分来看:声明(input/output)、执行体(command)和运行时(runtime)。三者缺一不可,但很多新手最容易在输入输出和运行时上踩坑。咱们一个一个拆解。
task 结构的基本骨架
先看一个最简单的 task 定义:
task hello_world {
input {
String name
}
command {
echo "Hello, ~{name}!"
}
output {
String greeting = read_string("stdout")
}
runtime {
docker: "ubuntu:latest"
memory: "1 GB"
cpu: 1
}
}
这里面的每个块都是必须的吗?其实只有 command 是必须的。但我建议你把 input、output、runtime 都写上,哪怕是空的。为什么呢?
嗯,因为你想想看,没有显式声明输入输出,别人读你的代码就像看天书,而且后续做任务依赖时也会出问题。
我的习惯: 即使 task 没有外部输入,我也会在 input 块里留个注释。这就像是给代码留下了“接口文档”,后期维护起来轻松很多。
输入输出声明:类型与表达式
WDL 的输入输出支持多种基本类型:String、Int、Float、Boolean,还有文件类型 File 和数组 Array[Type]。我以前遇到过一哥们,把所有输入都写成 String,然后自己在 command 里做类型转换……结果可读性极差,还容易出错。
输入输出的关键点:
- 输入变量可以在
command里通过~{var_name}引用 - 输出必须是从执行结果中“捕获”出来的,常见方式有
read_string()、read_int()、glob()等 - 输出文件类型要和生成的文件路径对应准确
看一个文件处理的例子:
task process_bam {
input {
File bam_file
String sample_id
Int min_qual = 20
}
command {
samtools view -h -q ~{min_qual} ~{bam_file} \
-o ~{sample_id}.filtered.bam
}
output {
File filtered_bam = "~{sample_id}.filtered.bam"
Int read_count = read_int("read_count.txt")
}
}
我曾经踩过的坑: 注意看输出声明中的字符串插值 ~{sample_id}.filtered.bam。如果 sample_id 里包含了路径分隔符或空格,可能会导致文件路径解析错误。我建议对样本 ID 做校验,或者使用 basename() 函数处理一下。
运行时参数:资源控制的核心
运行时参数 (runtime) 是你的 task 向执行引擎“申请资源”的地方。这直接决定了任务能不能跑、跑多快、花多少钱。我刚开始用 WDL 时,经常忘记设内存,结果任务被默认的 1GB 内存活活卡死。
常见运行时参数一览:
| 参数名 | 说明 | 常用值举例 |
|---|---|---|
docker |
容器镜像,保证环境一致性 | "ubuntu:20.04" |
memory |
申请的内存资源 | "4 GB" 或 "8192 MB" |
cpu |
申请的 CPU 核数 | 2 或 4 |
disks |
本地磁盘空间(不同后端格式不同) | "local-disk 20 SSD" |
maxRetries |
任务失败后重试次数 | 2 |
preemptible |
是否允许抢占式实例(云环境) | true 或 3 (尝试次数) |
timeout |
任务超时时间 | 3600 (秒) |
个人经验: 大的 task(比如比对、变异检测)我通常给 memory: "8 GB" 起步,再根据输入数据量逐步上调。小的工具类 task 给 memory: "1 GB" 加 cpu: 1 就够了。
另外,maxRetries 设成 2 或 3 是个好习惯——网络抖动、磁盘满这种偶发问题能自动恢复,不用手动重跑。
command 块:执行体的写法
command 块里写入的就是你在 shell 里会敲的命令。WDL 允许你使用 ~{} 嵌入输入变量。这里有个小技巧:如果命令比较长,可以写成多行,WDL 会自动把换行处理成空格。
command {
python3 <
这是个嵌入 Python 脚本的例子。你看,WDL 对于复杂处理逻辑是包容的——你完全可以把 Python、R、Perl 脚本直接写在 command 里。但要注意缩进和引号的转义。
为什么这么做? 有时候把逻辑写在 WDL 里比写在单独脚本文件中更方便,特别是这个逻辑只在这个 task 里用一次。但如果你有共享的脚本库,我更推荐放在容器镜像里,或者用 File 输入传进来。
Task 内部的变量作用域
WDL 2.0 引入了 parameter_meta 和一些高级特性,但咱们还是先把基本的作用域搞清楚:
input { }里声明的变量可以在整个 task 内使用command里通过~{}引用输入变量output { }里的声明不能直接引用其他输出变量,只能引用输入和执行结果- 你可以在 command 里定义临时 shell 变量,但这些变量对 output 不可见(因为 WDL 通过捕获 stdout 或文件来实现输出)
嗯,这里要注意:output 里的文件路径是相对于 task 执行目录的。所以如果你的命令生成了 /tmp/result.txt,而你在 output 里声明 result.txt,WDL 是找不到的。除非你改了工作目录,或者把文件复制到当前目录。
一个完整的 task 示例:把知识点串起来
task fastqc_analysis {
input {
File fastq_file
String sample_id
Int threads = 2
}
command {
mkdir -p ~{sample_id}_fastqc
fastqc -t ~{threads} -o ~{sample_id}_fastqc ~{fastq_file}
}
output {
File html_report = "~{sample_id}_fastqc/~{sample_id}_fastqc.html"
File zip_file = "~{sample_id}_fastqc/~{sample_id}_fastqc.zip"
}
runtime {
docker: "biocontainers/fastqc:v0.11.9_cv8"
memory: "2 GB"
cpu: 2
maxRetries: 1
}
parameter_meta {
fastq_file: "原始 FASTQ 文件,支持 .gz 压缩"
sample_id: "样本标识,用于输出文件命名"
threads: "线程数,默认 2"
html_report: "FastQC 的 HTML 质量报告"
zip_file: "FastQC 的压缩结果包"
}
}
你看,这个 task 就涵盖了:输入声明、command 编写、输出捕获、运行时参数、参数元信息。每一块我都加了注释,这就是一个可读性高、易于维护的 task 模板。
我曾经踩过的坑: 有一天我发现某个 task 的输出文件总为空,查了半天才发现——command 里输出到 ./result/,而 output 里写的是 "result/summary.txt",没有用 ./ 前缀。不同 WDL 引擎对相对路径的解析可能不一样。我建议:output 里的路径尽量用反斜杠对齐命令里的写法,或者干脆用绝对路径。
知识结构总结
把今天的内容画成一张图,更直观:
这张图把 task 的四个组成部分和它们之间的依赖关系理清楚了。你看,从任务定义出发,分成输入、执行、输出三条主线,然后运行时参数“兜底”整个执行过程。结构清晰,各司其职。