规则详解:rule 的零件箱
一条 Snakemake 规则,说白了就是告诉工作流引擎三件事:原料在哪、成品长什么样、怎么加工。这三个问题分别对应 input、output 和 shell/run。再加上 params 这类辅助零件,你就能拼出任何流程。
我最早写规则时,总觉得 input 就是把文件名列出来就完了。后来被通配符坑过几次,才明白这哥们儿背后藏着「模式匹配引擎」。咱们今天就把它彻底拆开看。
骨架:一条规则长什么样
先看一个最简单的例子:
rule fastqc:
input:
"data/sample1.fastq"
output:
"results/fastqc/sample1_fastqc.html"
shell:
"fastqc {input} -o results/fastqc/"
你肯定一眼就看懂了。但别急,真正的项目里,input 和 output 不会这么平铺直叙。咱们得学会「泛化」,让一条规则处理所有样本。
input:不只是列表,更是模式
我的建议是:尽量少写硬编码路径。用通配符 {wildcard} 把规则变成模板。比如:
rule fastqc:
input:
"data/{sample}.fastq"
output:
"results/fastqc/{sample}_fastqc.html"
shell:
"fastqc {input} -o results/fastqc/"
这里 {sample} 就是通配符。Snakemake 会从 output 里反推它的值,然后回填到 input。整个过程自动匹配——你只管写模式,引擎替你算。
input: reads="data/{sample}.fastq", adapters="refs/adapters.fa"然后在 shell 里写
"{input.reads}" 和 "{input.adapters}"。
output:规则的身份证
输出文件决定了规则什么时候跑、是否重跑。我个人习惯把 output 全部放在 results/ 下,按工具和样本分层。这样清理时也方便。
你可以用 {wildcard} 做输出,还可以用 {wildcard} 做目录。比如:
output: "results/multiqc/{date}/multiqc_report.html"
但注意:Snakemake 不会自动创建中间目录。我在项目中遇到过两次报错,都是因为目录不存在。解决办法是在 shell 开头加一句 mkdir -p $(dirname {output}),或者用 run 块调 Python 的 os.makedirs。
{sample_id},input 里却用了 {sample},结果排查了十分钟…注意对着写。
shell vs run:选谁?
这俩干的事差不多,但使用场景完全不同。
| 特性 | shell |
run |
|---|---|---|
| 执行环境 | 子 shell 调用 | Python 函数 |
| 适用场景 | 已有命令行工具 | 纯 Python 逻辑或预处理 |
| 变量替换 | 用 {} 自动替换 |
需手动读取 input,output |
| 调试难度 | 低(出错看 stdout) | 中等(需加 print) |
我的原则:能用 shell 搞定就别折腾 run。但如果你要批量改文件名、写配置文件,那 run 更灵活。举个例子:
rule generate_config:
output: "configs/{sample}.yaml"
params:
threads = 4,
mem = "8G"
run:
config = {
"sample": wildcards.sample,
"threads": params.threads,
"mem": params.mem
}
with open(output[0], "w") as f:
yaml.dump(config, f)
看到没?run 块里可以写完整的 Python 代码,甚至调第三方库。但代价是 Snakemake 没法直接给你做 shell 里的自动变量替换了,你得手动从 wildcards 和 input、output 字典里取值。
params:给规则灌额外参数
有些参数既不是输入文件,也不是输出路径,比如线程数、内存、额外 flag。这时候用 params 最干净:
rule bwa_mem:
input:
ref = "refs/hg38.fa",
reads = "data/{sample}_R{read}.fastq"
output:
"results/bam/{sample}.bam"
params:
extra = "-M -R '@RG\\tID:{sample}'",
threads = 8
shell:
"bwa mem -t {params.threads} {params.extra} {input.ref} {input.reads} | samtools sort - > {output}"
你想想看,如果把 -M -R '@RG...' 直接嵌进 shell 字符串里,万一哪天需要改,你得翻整个字符串。用 params 拆分后,配置一目了然,也方便后续用 config 或命令行覆盖。
params 里放一个 lambda 函数,用来动态计算适配器序列——虽然有点骚,但确实好用。不过别滥用,保持可读性更重要。
其他零件:threads、log、benchmark
作为基础规则,建议加上 threads 和 log。前者控制并行,后者存标准输出/错误。写法如下:
rule fastqc:
input: "data/{sample}.fastq"
output: "results/fastqc/{sample}_fastqc.html"
threads: 4
log: "logs/fastqc/{sample}.log"
shell:
"fastqc -t {threads} {input} -o results/fastqc/ 2> {log}"
我曾经漏写 log,结果出错了连错误信息都找不到,只能重跑。现在养成习惯:每条规则至少配一个 log,哪怕 shell 里只写 2>&1 | tee {log}。
本章知识结构图
下面这张图把一条规则的零件和它们的关系串起来了:
从图上你能看到,一条规则的「核心三件套」——input、output、shell/run——是必选项。其他都是锦上添花的辅助零件。但千万别小看辅助件,在实际项目中,它们决定了你的工作流能否复用、能否被队友看懂。
快速上手清单
总结一下写规则时的检查点,每次写之前过一遍:
- input 和 output 的通配符是否对称?—— 数量、名称都得一致。
- output 目录是否已创建?—— 要么在 shell 里 mkdir,要么在 run 里 os.makedirs。
- shell 里的变量名是否和 input/output/params 对得上?—— 比如
{input.ref}别写成{input.reference}。 - 是否写了 log?—— 不然出错了只能去系统日志里大海捞针。
- params 里有没有硬编码路径?—— 尽量用
{wildcards}或 config 替换。
嗯,零件讲完了。你拿着这张零件清单,就能搭出任何生信流程。记住:Snakemake 的规则不复杂,复杂的是你把流程逻辑想清楚。规则只是把逻辑写出来而已。