表达式与运算:算术运算、字符串拼接、逻辑判断、条件表达式

表达式是WDL脚本的「计算细胞」。我在项目里见过不少脚本跑崩,根源往往就是一个表达式的优先级搞错了。说白了,表达式写不对,后面所有数据校验和压缩逻辑都白搭。

这一节我们聚焦四类表达式:算术运算、字符串拼接、逻辑判断和条件表达式。它们会贯穿你写的每一个WDL脚本,尤其是做数据校验的时候——你想想看,校验不就是各种条件组合和数值计算嘛。

核心要点:表达式是WDL的「胶水」,把输入、参数、中间变量粘合成完整的处理逻辑。掌握这四类运算,你就能写出灵活又健壮的校验与压缩脚本。

WDL 表达式与运算体系 WDL 表达式 算术运算 字符串拼接 逻辑判断 条件表达式 + - * / % + 拼接 / ${} 插值 ~{} 命令插值 && || ! == != < > <= >= if (cond) then a else b 💡 表达式是WDL脚本的「计算细胞」 算术 → 算数值 · 拼接 → 组字符串 · 逻辑 → 做判断 · 条件 → 选分支

4.1 算术运算:数值的「加减乘除」

算术运算在WDL里很直白。支持 +、-、*、/、% 五个基本操作。操作数通常是 Int 或 Float 类型。我在项目中经常用它来计算数据分片大小、资源分配比例。

运算符 含义 示例 结果
+ 加法 100 + 50 150
- 减法 200 - 30 170
* 乘法 8 * 1024 8192
/ 除法 7 / 2 3(整数除法)
% 取模 7 % 2 1

⚠️ 整数除法的陷阱:WDL 中两个 Int 相除,结果还是 Int,直接截断小数部分。我曾经因为没注意这个细节,算出来的分片数比预期少了一块,导致压缩任务分配不均。如果你需要保留小数,先把其中一个转为 Float。

举个实际例子。做数据压缩时,我经常需要根据文件大小动态调整分片数量:

Int file_size_gb = 120
Int chunk_size_gb = 10
Int num_chunks = file_size_gb / chunk_size_gb
# num_chunks = 12,整数除法没问题

# 但如果文件大小是 125GB,你会得到 12 还是 13?
Int file_size_gb2 = 125
Int num_chunks2 = file_size_gb2 / chunk_size_gb
# 结果是 12!因为整数除法截断了小数
# 需要向上取整:
Int num_chunks_real = (file_size_gb2 + chunk_size_gb - 1) / chunk_size_gb
# 现在 num_chunks_real = 13

我个人习惯在涉及分片、批次的地方,先用取模运算检查一下边界情况,避免文件「掉缝里」。

4.2 字符串拼接:把碎片拼成完整信息

字符串拼接是WDL脚本里最常用的操作之一。你想想看——构建输入文件路径、生成输出文件名、组装命令行参数,哪一步离得开它?

WDL 提供三种拼接方式:

  1. 直接使用 + 运算符:简单直观
  2. 字符串插值 ${}:在 String 字面量中嵌入变量或表达式
  3. 命令插值 ~{}:在 command 块中嵌入表达式(可包含空格和特殊字符)

看个对比就清楚了:

String sample_id = "NA12878"
String prefix = "/data/raw/"
String suffix = ".fastq.gz"

# 方式一:+ 拼接
String read1 = prefix + sample_id + "_R1" + suffix
# 结果:"/data/raw/NA12878_R1.fastq.gz"

# 方式二:${} 插值(推荐)
String read2 = "${prefix}${sample_id}_R2${suffix}"
# 结果:"/data/raw/NA12878_R2.fastq.gz"

# 方式三:~{} 命令插值(适用于 command 块)
command {
  bwa mem ref.fa ~{read1} ~{read2} > ~{sample_id}.sam
}

💡 我的建议:在 String 定义中用 ${} 插值,在 command 块中用 ~{} 插值。~{} 会自动处理空格和特殊字符,帮你避免命令行解析错误。我以前被路径中的空格坑过好几次,用了 ~{} 之后就再没出过问题。

做数据校验时,字符串拼接常用来生成校验文件的路径:

String base_dir = "/data/analysis"
String batch_id = "BATCH_2024_001"
String sample = "SAMPLE_05"

# 拼接校验文件路径
String md5_path = "${base_dir}/${batch_id}/${sample}/${sample}.md5"
String report_path = "${base_dir}/${batch_id}/${sample}/qc_report.html"

# 输出日志
String log_msg = "Start validation for " + sample + " at " + batch_id

4.3 逻辑判断:给脚本装上「决策大脑」

逻辑判断是数据校验的核心。没有它,脚本就只会机械执行,遇到异常数据也不知道停。

WDL 的逻辑运算符分两类:

类型 运算符 含义 示例
逻辑 && 逻辑与 a > 0 && b < 100
|| 逻辑或 is_valid || force_override
! 逻辑非 !has_error
比较 == 等于 status == "PASS"
!= 不等于 file_size != 0
< / > 小于 / 大于 depth > 30
<= / >= 小于等于 / 大于等于 score >= 0.9

我实际项目中写过一个数据完整性校验的逻辑组合:

# 输入参数
File input_bam
Int min_size_mb = 10
Float min_map_ratio = 0.8
Boolean skip_validation = false

# 校验条件
Boolean file_exists = size(input_bam) > 0
Boolean size_ok = size(input_bam) / 1048576 >= min_size_mb
Boolean mapping_ok = mapping_ratio >= min_map_ratio

# 综合判断:要么全部通过,要么跳过校验
Boolean validation_passed = (file_exists && size_ok && mapping_ok) || skip_validation

# 如果没通过,直接终止(配合 assert 使用)
assert(validation_passed) about: "BAM 文件校验未通过,请检查:" + input_bam

⚠️ 短路求值:WDL 的逻辑运算符采用短路求值。也就是说,A && B 如果 A 为 false,就不会计算 B。我曾经在 B 里放了一个可能报错的表达式,结果因为 A 为 false 跳过了报错,排查了好久才发现。切记——不要在 && 的右侧放有副作用的操作。

4.4 条件表达式:用一行代码做分支选择

条件表达式(也叫三元表达式)是WDL里优雅处理二选一场景的工具。语法是:

result = if (condition) then value_if_true else value_if_false

它在数据校验和压缩场景中非常实用。举个例子:

# 根据校验结果选择输出路径
String output_dir = if (validation_passed) then "/data/processed/" else "/data/quarantine/"

# 根据数据量选择压缩级别
Int compression_level = if (file_size_gb > 50) then 6 else 9

# 根据文件类型选择压缩工具
String compressor = if (file_format == "BAM") then "samtools" else "gzip"

# 拼接完整输出路径
String final_path = output_dir + sample_id + ".cram"

条件表达式可以嵌套,但我不建议超过两层。嵌套太多,代码可读性断崖式下跌。

# 两层嵌套还行
String label = if (score >= 0.9) 
               then "HIGH" 
               else if (score >= 0.5) 
                    then "MEDIUM" 
                    else "LOW"

# 超过两层?写成独立的 if 块或者函数吧

💡 实用技巧:条件表达式可以赋值给 Boolean、Int、String、File 等任何类型。我在项目中常用它来动态切换输入源——比如「如果缓存文件存在就用缓存,否则重新下载」。一行代码搞定,清爽又安全。

4.5 综合实战:用表达式搭建校验逻辑

来看一个完整的例子。假设我们需要校验一批测序数据,然后根据校验结果决定压缩策略:

version 1.0

workflow validate_and_compress {
  input {
    File reads_r1
    File reads_r2
    String sample_id
    Int min_reads = 1000000
    Float min_quality = 30.0
    Boolean force_overwrite = false
  }

  # 算术运算:计算实际数据量
  Int total_reads = count_reads(reads_r1) + count_reads(reads_r2)
  Float avg_quality = compute_avg_quality(reads_r1, reads_r2)

  # 逻辑判断:组合校验条件
  Boolean reads_ok = total_reads >= min_reads
  Boolean quality_ok = avg_quality >= min_quality
  Boolean skip_flag = force_overwrite

  Boolean can_proceed = (reads_ok && quality_ok) || skip_flag

  # 条件表达式:动态确定输出路径和压缩参数
  String out_prefix = "/data/compressed/" + sample_id
  String out_suffix = if (can_proceed) then ".cram" else ".failed"
  Int compression_level = if (total_reads > 5000000) then 5 else 9

  # 字符串拼接:组装最终输出文件路径
  String output_path = out_prefix + "_L" + compression_level + out_suffix

  command {
    # 用 ~{} 插值构建命令行
    compress_tool \
      --input1 ~{reads_r1} \
      --input2 ~{reads_r2} \
      --output ~{output_path} \
      --level ~{compression_level} \
      --validate ~{can_proceed}
  }

  output {
    File compressed_file = output_path
    String summary = "${sample_id}: reads=${total_reads}, quality=${avg_quality}, passed=${can_proceed}"
  }
}

这个脚本把四种表达式串在了一起:

  • 算术运算算总数据量和平均质量
  • 逻辑判断做组合校验
  • 条件表达式动态选路径和参数
  • 字符串拼接组装最终输出

核心收获:表达式不是孤立的语法点。把它们组合起来,就能写出灵活、健壮的数据处理脚本。我在实际生产里,80% 的校验逻辑都是用这四类表达式搭出来的。掌握它们,你就能应对绝大多数数据校验场景。

嗯,说完了。表达式这块儿看似基础,但用好了就是利器。下次写脚本的时候,你可以试试把复杂的 if-else 用条件表达式简化,把路径拼接用插值统一——会清爽很多。