第3章 手把手写你的第一个 Snakefile
从这章开始,我们要亲手触碰 Snakemake 的核心 —— Snakefile。说实话,我第一次看到 Snakefile 时觉得它很像 Makefile,但又多了很多 Python 的影子。它不复杂,但有几个关键概念必须吃透。
Snakefile 长什么样?
Snakefile 本质上是一个 Python 脚本,里面放了一堆 rule(规则)。每条 rule 定义了:
- 从哪些输入文件出发
- 要生成什么输出文件
- 用什么 shell 命令或 Python 代码来完成
我用一个最简单的例子让你感受一下:
rule say_hello:
output:
"hello.txt"
shell:
"echo 'Hello, Snakemake!' > hello.txt"
保存为 Snakefile,运行 snakemake -c1,你就能看到当前目录下多了一个 hello.txt。就是这么直白。
rule 的身份证:name, input, output, shell
每个 rule 都有四个最常见字段,我习惯把它们称为 rule 的「身份证」:
| 字段 | 作用 | 注意点 |
|---|---|---|
name | rule 的名字,用来唯一标识 | 不能重复,建议用下划线命名 |
input | 指定输入文件路径(可以是列表) | 无输入也合法,但流水线通常需要 |
output | 指定输出文件路径(同样可以是列表) | Snakemake 会根据它判断是否重跑 |
shell | 要执行的 shell 命令字符串 | 里面可以用 {input} {output} 占位 |
举个例子,我们在质控流水线中常遇到:用 fastp 对原始 FASTQ 进行质量过滤。
rule fastp_trim:
input:
"data/sample_R1.fastq.gz",
"data/sample_R2.fastq.gz"
output:
"results/trimmed/sample_R1.fastq.gz",
"results/trimmed/sample_R2.fastq.gz",
"results/trimmed/sample_fastp.html"
shell:
"fastp -i {input[0]} -I {input[1]} -o {output[0]} -O {output[1]} -h {output[2]}"
注意这里 {input[0]} 对应第一个输入文件,{input[1]} 对应第二个。output 同理。Snakemake 会自动把 Python 列表转换成有序索引。
str 或 list 书写,不要写太长的嵌套路径,否则后期改流程特别痛苦。我一般在项目根目录建 data/ 和 results/ 两个文件夹,rule 里只写相对路径。
shell 命令里的占位符魔法
Snakemake 会在执行 shell 前,把 {input}、{output} 替换成实际文件路径。如果输入输出是列表,它默认用空格拼接。但有些工具(比如 bwa、samtools)需要指定特定的顺序,这时可以用 {input[0]} 按位置取。
举个例子,我曾经被 fastqc 的参数坑过一次:
rule fastqc:
input:
"data/sample.fastq.gz"
output:
"results/fastqc/sample_fastqc.html",
"results/fastqc/sample_fastqc.zip"
shell:
"fastqc -o results/fastqc/ {input}"
看,输出只写了 output 声明,shell 里并没有用到 {output}。因为 fastqc 默认会生成同名的 html 和 zip,Snakemake 只是用 output 来检查是否已经生成——如果文件已经存在且输入没变,它就不跑了。
{input} 或 {output} 时,如果路径包含空格,Snakemake 默认会帮你加引号。但如果你用了通配符(下一章讲),要格外小心文件名里带特殊字符的情况。
Snakefile 的结构骨架
一个实用的 Snakefile 通常长这样:
# 全局配置(可选)
configfile: "config.yaml"
# 第一条 rule 通常是 all,用来定义最终目标
rule all:
input:
"results/final_report.html"
# 其他 rule 按依赖顺序组织
rule fastp_trim:
...
rule multiqc:
...
你不需要像写脚本一样按顺序排列 rule,Snakemake 会自动分析依赖关系。它从你执行时指定的目标(默认第一个 rule)出发,反向找到所有需要的输入。
下面这张图展示了 Snakefile 里 rule 之间的依赖关系:
你看,rule all 需要 multiqc 的输出,而 multiqc 又需要 fastp_trim 和 fastqc 的输出。Snakemake 会按照图的拓扑顺序执行,如果某个文件已经存在且没变化,它直接跳过。
写 rule 时的实用小技巧
- 把最顶层的 rule 命名为
all,这样你直接运行snakemake就构建所有最终产物。 - 输出文件的名字尽量和样本名关联,比如
sample1_fastp.html,方便后面用通配符(下章主角)。 - 用
shell时把命令写完整,不要依赖当前环境变量。我曾经在一个集群上因为 PATH 没加载全导致命令找不到,后来学乖了,直接在 shell 里写/usr/bin/fastp或者用conda环境。
shell 时用了 {output} 但没加 -o 参数,结果输出文件根本没生成,Snakemake 报错说找不到目标文件。后来我才意识到:shell 命令必须确实创造出 output 里声明的那几个文件,Snakemake 才会认为规则执行成功。
你还应该知道的:rule 的更多字段
除了 input、output、shell,rule 还可以有 params、log、threads、resources 等字段。不过新手阶段,掌握前三者就足够跑通质控流水线了。比如:
rule fastp_with_params:
input:
"data/{sample}_R1.fastq.gz",
"data/{sample}_R2.fastq.gz"
output:
"results/{sample}_R1.fastq.gz",
"results/{sample}_R2.fastq.gz"
params:
adapter1 = "AGATCGGAAGAGCACACGTCTGAACTCCAGTCA",
adapter2 = "AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGT"
shell:
"fastp -i {input[0]} -I {input[1]} "
"-o {output[0]} -O {output[1]} "
"--adapter_sequence {params.adapter1} "
"--adapter_sequence_r2 {params.adapter2}"
这样把参数单独放在 params 里,代码更清晰。而且 params 里还可以放 Python 表达式,比如根据样本名动态计算参数。
最后说两句
Snakefile 的语法其实很轻,你记住 rule 的骨架,然后大胆往里面填命令就行了。别怕写错,Snakemake 的错误提示都挺友好的,它会告诉你哪个文件缺失、哪个 rule 失败了。
嗯,这一章的内容就到这。你写一个最简单的 Snakefile 跑一下试试,然后可以试着把 fastp 或 fastqc 的命令塞进去。我在项目中踩过的那些坑,已经变成上面的避坑指南了,希望你别再走一遍。