Snakemake 规则核心:run、shell、script、wrapper
好,我们直接进入正题。一个 Snakemake 工作流的核心是什么?说白了就是规则(rule)。而每条规则的核心,就是它要执行的指令。
Snakemake 给了我们四种方式:run、shell、script、wrapper。我最初接触的时候,觉得这几种方式差不多,选哪个都行。后来在真实项目里踩了坑,才明白它们的差异和适用场景。
这四种指令,你选对了,能省大量时间。选错了,可能调试到崩溃。
run:纯 Python 环境
run 指令是最直观的。你在规则里写一段 Python 代码,Snakemake 直接执行。
适用场景:轻量处理、数据验证、日志统计、测试。
rule process_log:
input: "raw.log"
output: "processed.log"
run:
with open(input[0], 'r') as f_in:
data = f_in.readlines()
with open(output[0], 'w') as f_out:
for line in data:
if "ERROR" in line:
f_out.write(line)
run 块。这会导致 Snakefile 变得臃肿,也难以复用。 run 里的代码,本质上是内嵌在 Snakefile 中的。它无法被模块化导出。
我个人习惯是:run 只用于 10 行以下的简单逻辑。超过这个规模,我会考虑用 script。
shell:直接调用系统命令
shell 指令是最灵活、最常用的。它把指令字符串交给 Bash 去执行。这也是我日常工作里用得最多的指令。
rule bwa_alignment:
input:
ref = "genome.fa",
fq1 = "sample_R1.fastq",
fq2 = "sample_R2.fastq"
output:
bam = "aligned.bam"
shell:
"bwa mem {input.ref} {input.fq1} {input.fq2} | samtools sort -o {output.bam}"
你看,这里我用了管道,直接把 BWA 的输出交给 samtools。这种组合在 run 里实现起来就很麻烦。但 shell 里,就是一条命令的事儿。
shell 指令里,你可以直接使用 {input}、{output}、{params} 等通配符。Snakemake 会自动把它们替换成实际路径。记住,input 如果有多个文件,在 shell 里会被展开成空格分隔的字符串。
script:将脚本分离出来
当你的逻辑变得复杂,比如需要几百行的 Python 或 R 代码,塞进 run 块里就很不合适了。这时候,script 指令登场。
它的核心思想是:把脚本放在单独的文件里,规则只负责调用。Snakemake 会自动传递 snakemake 对象给脚本,脚本可以通过 snakemake.input、snakemake.output 访问文件和参数。
rule complex_analysis:
input: "data.csv"
output: "results.rds"
script:
"scripts/analyze.R"
在 scripts/analyze.R 中:
library(tidyverse)
df <- read.csv(snakemake@input[[1]])
result <- df %>%
group_by(group) %>%
summarise(mean = mean(value), sd = sd(value))
saveRDS(result, snakemake@output[[1]])
我曾经在项目里把所有的生物信息学分析逻辑都写在 Snakefile 里,那文件足足有 3000 行。后来重构,把所有核心算法提取到 script 目录里。不仅 Snakefile 变清爽了,脚本还能被其他工作流复用。嗯,这是一个明智的决定。
script 指令下的脚本,默认会获得全局的 snakemake 对象(Python)或 snakemake S4 对象(R)。脚本可以从这个对象中读取输入、输出、参数、通配符等信息。这意味着你的脚本完全与 Snakemake 上下文解耦,只通过 API 通信。
wrapper:站在巨人的肩膀上
wrapper 是四种指令中概念最简单的,但用好了效率最高。说白了,它就是指向一个 Git 仓库中的预定义工作流模块。
Snakemake 官方和社区维护了一个庞大的 wrapper 仓库:snakemake-wrappers。里面封装了常见的生物信息学工具,比如 fastp、bwa、samtools、picard 等。
rule fastp_trimming:
input:
fq1 = "sample_R1.fastq",
fq2 = "sample_R2.fastq"
output:
fq1 = "trimmed_R1.fastq",
fq2 = "trimmed_R2.fastq",
html = "fastp_report.html"
params:
extra = "-q 20 -u 30"
wrapper:
"v3.10.1/bio/fastp"
注意这里的 wrapper 路径。v3.10.1 是版本 tag,bio/fastp 是仓库子目录。Snakemake 会自动下载并执行这个 wrapper。
| 指令 | 执行环境 | 代码复用性 | 适用场景 |
|---|---|---|---|
| run | Snakemake 内部 Python | 低 | 简单逻辑、测试、后处理 |
| shell | 系统 Shell(Bash) | 中 | 调用命令行工具、管道组合 |
| script | 独立脚本(Python/R/Julia 等) | 高 | 复杂分析、模块化设计 |
| wrapper | 预定义社区模块 | 极高 | 快速集成标准工具 |
wrapper,结果发现有些 wrapper 的版本与内部环境不兼容。务必先检查 wrapper 的依赖和版本号。另外,wrapper 是“黑盒”,你无法轻易修改内部逻辑。如果有定制需求,老老实实用 shell 或 script。
好了,四种方式讲完了。你选哪个?没有绝对的正确答案。我个人的选择顺序是:
- 有没有现成的 wrapper? 有就用,能省大量时间。
- 没有 wrapper,那逻辑复杂吗? 复杂就抽成
script,保持整洁。 - 不复杂,但需要调命令行工具? 用
shell,直接高效。 - 只是简单的数据清洗或验证? 用
run,方便快捷。
这个顺序,你可以在实际项目中试一试。我用这套规则写了十几个工作流,几乎没有翻过车。