表达式与运算:算术运算、字符串拼接、逻辑判断、条件表达式
表达式是WDL脚本的「计算细胞」。我在项目里见过不少脚本跑崩,根源往往就是一个表达式的优先级搞错了。说白了,表达式写不对,后面所有数据校验和压缩逻辑都白搭。
这一节我们聚焦四类表达式:算术运算、字符串拼接、逻辑判断和条件表达式。它们会贯穿你写的每一个WDL脚本,尤其是做数据校验的时候——你想想看,校验不就是各种条件组合和数值计算嘛。
核心要点:表达式是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 提供三种拼接方式:
- 直接使用
+运算符:简单直观 - 字符串插值
${}:在String字面量中嵌入变量或表达式 - 命令插值
~{}:在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 用条件表达式简化,把路径拼接用插值统一——会清爽很多。