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 全都可以。下面这张表你收好:

类型写法示例说明
FileFile bam_file单个文件,Cromwell 会自动 staging
StringString sample_id字符串,常见于命名、传递参数
IntInt threads整数,比如线程数、质量阈值
FloatFloat p_value浮点数,很少用但偶尔逃不掉
BooleanBoolean 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 的生命周期串起来:

WDL Task 生命周期与核心语法 1. 声明输入 2. 执行命令 3. 声明输出 工作流调用:call task_name { input: 变量赋值 } 输入类型 File, String, Int, Float Boolean, Array[X], Map[K,V] 支持默认值:Int threads = 4 输出声明 字面路径:File x = "out.txt" 通配符:Array[File] = glob("*.txt") 文件必须真实存在,否则 job fail 核心原则 输入变量用 ${} 引用 · 输出文件名需与命令块产出严格一致 · 通配符可以捕获多文件

写 task 时我常犯的 3 个错误

说几个我踩过的坑,你绕过去就行:

  1. 输出文件名写错:命令块产出的文件名,和输出声明里的名字,差一个字符都不行。我后来养成了习惯——先跑个最小用例验证。
  2. 忘记给输入设默认值:有些参数比如线程数,95% 的情况都用同一个值。设个默认值能省很多事。
  3. 命令块里的变量引用写成了 shell 风格的 $var:WDL 要用 ${var}。如果写成 $var,Cromwell 不会报错,但变量不会被替换——你得到的是一个奇怪的文字串。

我个人建议:每写一个 task,先用 java -jar womtool.jar validate my_task.wdl 做语法校验。这个工具能帮你捕获 90% 的低级错误,省得跑 Cromwell 时才报错,白白浪费时间。