第一个 Snakefile:规则定义、输入输出、运行 snakemake
好,我们正式开始动手了。你可能会想,Snakefile 到底长什么样?说白了,它就是一个纯文本文件,里面写着一系列“规则”。每条规则告诉 Snakemake:你想要什么文件,以及怎么做出来。
我第一次写 Snakefile 时,最不习惯的就是“声明式”思维——不是写“先做A,再做B”,而是写“我要 B,而 B 依赖于 A”。这种反转一开始有点绕,但习惯之后你会发现,这才是管理复杂依赖的正确姿势。
规则的基本骨架
每个规则,核心就三部分:名字、输入、输出,再加上具体的执行命令。看下面这个最简例子:
rule all:
input:
"result.txt"
这是一个特殊的规则,名为 all。它没有输出,也没有执行块 —— 它只是告诉 Snakemake:“嘿,我最终要生成这个文件”。Snakemake 会沿着依赖链反向推导,找到所有需要执行的步骤。
再看一个真正的处理规则:
rule count_lines:
input:
"data/raw.txt"
output:
"result.txt"
shell:
"wc -l {input} > {output}"
这里 count_lines 规则读入一个原始文件,统计行数,把结果写入输出文件。{input} 和 {output} 是 Snakemake 提供的占位符,运行时会自动替换成实际路径。我个人习惯用花括号来引用输入输出,这样写 shell 命令特别清爽。
输入与输出
输入和输出可以是一个文件,也可以是多个文件。语法很简单:
| 文件数量 | 写法示例 | 占位符 |
|---|---|---|
| 单个 | input: "a.txt" |
{input} |
| 多个命名 | input: reads="sample1.fq", ref="genome.fa" |
{input.reads}、{input.ref} |
| 多个匿名 | input: ["a.txt", "b.txt"] |
{input[0]}、{input[1]} |
输出也类似:
rule merge:
input:
"1.txt",
"2.txt"
output:
"merged.txt"
shell:
"cat {input} > {output}"
这里的 {input} 会被展开成空格分隔的多个文件路径。Snakemake 会自动处理列表展开,你只需要写一次命令。
运行你的第一个工作流
有了 Snakefile,运行只需要一行命令。假设 Snakemake 已经安装好,并且当前目录下有一个 data/raw.txt 文件:
conda activate snakemake_env
snakemake -c 1
-c 1 表示使用 1 个 CPU 核心。Snakemake 会读取当前目录下的 Snakefile(默认文件名),检查 result.txt 是否存在。如果不存在,它会执行 count_lines 规则。如果 data/raw.txt 也比 result.txt 新,Snakemake 也会重新执行。
你可能会看到类似这样的输出日志:
Building DAG of jobs...
Job counts:
count jobs
1 count_lines
1
[Wed Jan 15 10:00:00 2025]
rule count_lines:
input: data/raw.txt
output: result.txt
jobid: 0
Reason: Missing output files: result.txt
Executing...
1 lines written to result.txt
Finished job 0.
1 of 1 steps (100%) done
看到 100% done 就表示你的第一个工作流跑通了。是不是很简单?嗯,确实就这几步。
cat result.txt
1000
如果你用 snakemake -c 1 --dryrun,它不会真的执行,而是打印将要做什么。这个参数我在调试依赖关系时几乎天天用。
你看这张图,逻辑很直白:Snakemake 从最终输出出发,找到对应规则,检查输入是否存在,然后执行。如果输入文件缺失,它会向上查找生成输入文件的其他规则,形成完整的依赖链。
--dag 参数可以输出依赖图 DOT 格式,配合 dot -Tpng 生成可视化。不过现阶段,记住 --dryrun 就够用了。
好了,你的第一个 Snakefile 已经跑通了。从规则定义到输入输出,再到命令行运行,全流程就是这么回事。别被那些花里胡哨的特性吓到,基础其实只有这么几行代码。接下来你要做的就是——多写几个规则,让它们互相依赖,感受一下自动编排的威力。