Snakefile 基本语法:规则定义与核心组件
聊到 Snakefile 的语法,我个人的习惯是:先不要把它想成什么高深的工作流语言,你就当它是一张“菜谱”——每个步骤就是一道菜的做法。今天咱们就沉下心来,把这道“菜谱”的骨架拆明白。
核心结论:Snakefile 里的每个 rule,就是一个独立的“工作单元”。它告诉 Snakemake 三件事:要什么原料(input),出什么产品(output),怎么做(run/shell/script)。
rule 关键字——工作流的最小单元
你写 Snakefile,本质上就是在写一堆 rule。每个 rule 都有一个名字,比如 bwa_alignment、samtools_sort。这个名字只是给人看的,Snakemake 内部并不拿它当唯一标识,真正的标识是 output 文件的路径。
rule bwa_alignment:
input:
"data/raw/sample_1.fastq.gz"
output:
"results/alignment/sample_1.sam"
shell:
"bwa mem ref.fa {input} > {output}"
你看,这个规则叫 bwa_alignment,但 Snakemake 看到的是“我要生成 results/alignment/sample_1.sam”。如果你改了 rule 名字,只要 output 路径不变,工作流照样跑得起来。嗯,这一点我曾经在重构项目时踩过坑,以为改名就万事大吉,结果通配符解析全乱了——后面讲到通配符时再细说。
input 和 output——规则的生命线
input 和 output 的定义其实比你想象中灵活。常见的三种写法:
- 单个文件:直接写路径字符串。
- 多个文件(列表):用逗号分隔,Snakemake 会自动帮你展开。
- 命名文件(字典):给每个文件起个名字,后面引用时用
{input.name},可读性会好很多。
举个例子:
rule multi_input_example:
input:
reads1="data/sample_R1.fastq",
reads2="data/sample_R2.fastq",
reference="refs/hg38.fa"
output:
"results/sample.bam"
shell:
"bwa mem {input.reference} {input.reads1} {input.reads2} | samtools sort -o {output}"
我个人特别喜欢用命名文件的方式,尤其是在管道里参数一多的时候。不命名的话,你只能靠索引 {input[0]}、{input[1]},看着就头疼。我在项目里试过一次,后来自己都分不清哪个是 R1 哪个是 R2,果断改回了命名法。
小技巧:output 文件路径决定了规则是否被“跳过”。Snakemake 会检查 output 是否存在,如果存在且未过期,该规则就不会执行。所以,想要强制重跑某一步,最简单的办法就是删掉对应的 output 文件。
执行体:run / shell / script
这是规则里你真正“干活”的地方。三种方式,适用场景完全不同。
1. shell:最常用,适合命令行工具
刚才那个 bwa mem ... 就是 shell 的典型用法。你把命令行写成一个字符串,用 {input} 和 {output} 做占位符,Snakemake 会在执行时自动替换成真实路径。
rule samtools_index:
output:
"results/sample.bam.bai"
shell:
"samtools index results/sample.bam"
注意:shell 里的花括号会被 Snakemake 解析,如果你想在命令行里用原生的 bash 花括号(比如 {1..10}),必须写成 {{1..10}}。我第一次写循环时就被坑了,输出全都变成了空的。切记,逃逸。
2. run:用 Python 写逻辑
如果一个步骤需要复杂的条件判断、文件操作,或者你不想写 shell 脚本,就上 run。它允许你写一段 Python 代码,Snakemake 会在内部 Python 环境中执行它。output 文件还是需要用 touch() 或手动创建来标记完成。
rule merge_reports:
input:
"results/chr1_report.tsv",
"results/chr2_report.tsv"
output:
"results/final_report.tsv"
run:
import pandas as pd
frames = [pd.read_csv(f, sep="\t") for f in input]
merged = pd.concat(frames, ignore_index=True)
merged.to_csv(output[0], sep="\t", index=False)
这里要注意,output[0] 是 output 列表的第一项。如果你用了命名 output,也可以写成 output.report。我个人习惯在 run 里尽量少写长代码,太长了就拆成函数放到 script 里,下面会讲。
3. script:调用独立的脚本文件
当代码逻辑超过二三十行时,我建议你把它扔到一个单独的 .py 或 .R 文件里,然后用 script 引用。Snakemake 会自动把 {input}、{output}、{wildcards} 作为变量传进去,脚本里直接用 snakemake.input、snakemake.output 来访问。
rule plot_coverage:
input:
bam="results/sample.bam",
bed="data/targets.bed"
output:
"results/coverage_plot.pdf"
script:
"scripts/plot_coverage.py"
对应的 scripts/plot_coverage.py 内容:
import matplotlib.pyplot as plt
import pysam
bam_file = snakemake.input['bam']
bed_file = snakemake.input['bed']
out_pdf = snakemake.output[0]
# 你的绘图逻辑
plt.savefig(out_pdf)
为什么推荐 script? 因为你可以用你最喜欢的 IDE 编辑那个 .py 文件,语法高亮、调试、单元测试全都能用。而在 run 里写长代码,一旦格式化乱了,想改个参数都费劲。
规则之间的依赖:命名与自动推导
Snakemake 不要求你显式写明“规则 A 依赖规则 B”。只要规则 B 的 output 是规则 A 的 input,Snakemake 就会自动排序。我举个例子:
rule fastqc:
input:
"data/sample.fastq.gz"
output:
"results/qc/sample_fastqc.html"
shell:
"fastqc {input} -o results/qc/"
rule parse_fastqc:
input:
"results/qc/sample_fastqc.html"
output:
"results/qc/sample_summary.txt"
script:
"scripts/parse_fastqc.py"
执行 parse_fastqc 时,Snakemake 检查发现它的 input 文件 results/qc/sample_fastqc.html 由 fastqc 规则产出,于是自动先跑 fastqc,再跑 parse_fastqc。你不用写任何依赖声明,这就是工作流引擎的“自动依赖解析”。
说到这里,我不得不提一句:规则名字可以随意起,但 output 路径一定不能出错。曾经我手滑把 sample_fastqc.html 写成了 sample_fastqc.zip,结果解析规则一直报错,找了好久才发现是 output 文件名不一致。嗯,这种低级错误,犯过一回就永远记住了。
整个规则体系的直观理解
为了帮你把刚才的知识点串起来,我画了一张图。你看一眼,马上就能明白规则是怎么组合成一个工作流的。
从上图你可以看到:一个规则由 input → 执行体 → output 三要素构成。而规则之间通过 input/output 文件名自动“扣”在一起,形成有向无环图(DAG)。你只管写好每个规则,Snakemake 自己会帮你排好顺序。
常用参数速查
除了刚才讲的核心组件,我整理了一份你通常会用到的一批参数。不要求你全部记住,但看一眼大概知道有哪些东西,以后用到了回来查就行。
| 参数 | 作用 | 举例 |
|---|---|---|
input |
定义输入文件 | "data/{sample}.fastq" |
output |
定义输出文件 | "results/{sample}.bam" |
shell |
执行 shell 命令 | "bwa mem {input} > {output}" |
run |
执行 Python 代码块 | run: print("hello") |
script |
调用外部脚本 | "scripts/plot.R" |
params |
传入额外参数给执行体 | params: extra="--threads 4" |
log |
指定日志文件路径 | log: "logs/{sample}.log" |
threads |
声明该规则需要的线程数 | threads: 8 |
resources |
声明内存/磁盘等资源需求 | resources: mem_mb=4000 |
我的建议:刚开始写 Snakefile,你只需要掌握 input、output、shell 和 script 就够了。run 偶尔用,params、log、threads 这些等你遇到性能瓶颈或调试需求时再学,一次学太多容易晕。
两个常见误区
- output 的文件路径必须是唯一的:两个规则不能产出相同路径的文件,否则 Snakemake 会报错,因为它不知道哪个才是“真”的输出。我见过有人把
tmp/作为多个规则的 output 目录,结果一跑就崩。 - shell 命令里尽量用 {output} 而不是硬编码:偶尔偷懒直接写死路径,换环境或改目录结构时就得挨个改,得不偿失。记得我刚用 Snakemake 那年,以为“反正就一次”,结果项目扩到 50 个样本时,改路径改到手抽筋。
好了,这一章的知识就到这。你需要做的就是打开你的编辑器,写一个最简单的规则,让它能跑通。哪怕只是 echo "hello" 也算。动手永远比光看管用。