Snakefile 基本结构:规则、输入、输出、shell 命令
Snakefile 是 Snakemake 的命根子。说直白点,你所有流程的编排、依赖、并行度,全写在这个文件里。我刚开始接触的时候,第一反应是“这不就是 Makefile 的 Python 亲戚吗?” 还真有点像。但 Snakemake 更强,也更适合生信、机器学习这类多步骤、多输入输出的场景。
一个 Snakefile 由若干 规则(rule) 组成。每个规则就是一个小黑盒:你告诉它要什么 输入,它给出什么 输出,然后通过 shell 命令或 run 块来执行。Snakemake 会根据你定义的文件依赖关系,自动推导出整个流程的执行顺序。
规则的基本写法
一个最简的规则长这样:
rule align:
input:
"data/sample.fastq"
output:
"results/sample.sam"
shell:
"bwa mem ref.fa {input} > {output}"
看到了吗?input 和 output 是文件的路径,shell 里是你要跑的命令。{input} 和 {output} 是 Snakemake 自动替换的占位符。你想想看,要是每次改了样本名都得手动改命令,那多折腾。Snakemake 帮你做了这件事。
这里有个坑:input 和 output 的顺序不能写反,也不能漏掉冒号。我曾经一粗心写成 input "data/sample.fastq"(没有冒号),Snakemake 直接报语法错误,排查了半天才发现。嗯,细节决定效率。
输入(input)与输出(output)
| 元素 | 作用 | 示例 |
|---|---|---|
| input | 声明规则依赖的上游文件 | "data/{sample}.fastq" |
| output | 声明规则产生的下游文件 | "results/{sample}.bam" |
输入和输出都可以是列表。比如一个规则需要两个输入文件:
rule merge:
input:
"chr1.vcf",
"chr2.vcf"
output:
"merged.vcf"
shell:
"cat {input} > {output}"
这里 {input} 会自动展开成 chr1.vcf chr2.vcf,多个文件用空格隔开。我个人习惯用列表的方式写,因为可读性更好。如果你只有单个输入,直接写字符串也行,不影响。
{sample} 是让规则变成泛型的关键。你不需要为每个样本写一个规则,只要在 output 里用 {sample},Snakemake 就会自动理解你需要“一个文件,里面的样本名是占位符”,然后在执行时从实际文件路径中提取出来。
shell 命令——规则的实际干活部分
shell 命令就是你想在终端里敲的那句话。Snakemake 在执行时,会把 {input}、{output}、{params} 等占位符替换成真实路径,然后丢给系统 shell 去跑。默认用的是 /bin/bash。
看一个更真实的例子:
rule samtools_sort:
input:
"results/{sample}.sam"
output:
"results/{sample}.sorted.bam"
shell:
"samtools sort -@ 4 -o {output} {input}"
这里我用了 -@ 4 指定 4 个线程。如果你需要把线程数也参数化,可以用 params:
rule bwa_mem:
input:
ref="genome.fa",
reads="data/{sample}.fastq"
output:
"results/{sample}.sam"
params:
threads=8
shell:
"bwa mem -t {params.threads} {input.ref} {input.reads} > {output}"
哦对了,input 还可以用命名参数,这样在 shell 里就能用 {input.ref} 和 {input.reads} 分别引用,比数字下标清晰多了。我在项目中处理过一批双端测序数据,就是靠这种命名方式把 R1 和 R2 区分开,再也没搞混过。
规则之间的连接——通过文件传递
Snakemake 不用你写 what depends on what,它只看文件。你定义好每个规则的输出,然后下游规则的输入如果和上游规则的输出匹配(包括通配符一致),就自动形成依赖。
看一个两个规则串联的例子:
rule bwa_align:
output: "results/{sample}.sam"
input: "data/{sample}.fastq"
shell: "bwa mem ref.fa {input} > {output}"
rule sort_bam:
output: "results/{sample}.sorted.bam"
input: "results/{sample}.sam"
shell: "samtools sort -o {output} {input}"
如果你运行 snakemake results/sample1.sorted.bam,Snakemake 会发现:
- 需要
results/sample1.sorted.bam→ 找到sort_bam规则 sort_bam需要results/sample1.sam→ 找到bwa_align规则bwa_align需要data/sample1.fastq→ 文件存在,开始执行
于是它会先跑 bwa_align,再跑 sort_bam。整个过程你只需要定义规则,依赖关系 Snakemake 自动推断。
output 文件路径中通配符的名称必须一致(比如都用 {sample}),否则 Snakemake 没法匹配。我曾经犯过一个错:上游用 {sample},下游用 {id},结果它当成两个不相关的规则,各自报错找不到文件。通配符名字要保持统一。
流程图:一个完整的 Snakefile 核心结构
下面这张 SVG 图展示了一条完整的数据流,从原始 FastQ 到最终 BAM,两个规则通过中间文件 *.sam 串联起来:
避坑指南:常见错误
- shell 命令里的引号嵌套:如果你在 shell 里用了双引号,外层 shell 块必须用单引号或三引号。我踩过这种雷:
shell: "awk '{print \$1}' {input}"里面的美元符号需要转义,否则会被 Snakemake 当作占位符。 - 路径不要写死绝对路径:尽量用相对路径,便于移植。我曾经把数据放在
/home/user/project/data,结果换到服务器上路径全要改。用config文件或者环境变量管理路径更灵活。 - 别忘了创建输出目录:Snakemake 不会自动创建
output中声明的目录。如果目录不存在,shell 命令会报错。一个好习惯是在 shell 里先mkdir -p $(dirname {output})。
rule all,里面只写最终想要的文件列表。这样你只需 snakemake -j 一次,Snakemake 会自动找到所有上游依赖并执行。比如:
rule all:
input:
"results/sample1.sorted.bam",
"results/sample2.sorted.bam"
这样就不用每次手动指定目标文件了。