Snakefile基础结构:规则、输入、输出、运行shell命令

好,现在咱们动手写第一个Snakefile。说实话,很多人第一次看到Snakefile会觉得它像某种配置文件,其实它就是Python代码,只不过加了一些“魔力方法”。Snakemake的核心——规则,本质上就是一个作业说明书。

一句话记住:规则 = 输入 + 输出 + 要执行的命令。

规则的基本骨架

一个最简单的规则长这样:

rule say_hello:
    output:
        "hello.txt"
    shell:
        "echo 'Hello, Snakemake!' > hello.txt"

每个规则都有名字,这里叫 say_hello。output 告诉Snakemake这个规则会生成什么文件,shell 就是真正要做的事。运行的时候,Snakemake会先检查 hello.txt 存不存在,如果不存在,就执行后面的shell命令。

嗯,这里要注意:规则名不重要,输出文件才重要。Snakemake是根据文件是否存在来判断要不要重新运行的,不是根据规则名。

输入与输出——建立依赖的基石

实际工作中很少有单打独斗的规则。比如你要先下载数据,再处理,再分析。这就需要有输入了。看个例子:

rule download:
    output:
        "data/raw.txt"
    shell:
        "wget -O data/raw.txt https://example.com/sample.txt"

rule process:
    input:
        "data/raw.txt"
    output:
        "data/clean.txt"
    shell:
        "python scripts/clean.py {input} {output}"

这里 process 规则指定了输入文件 data/raw.txt,而 download 规则恰好会生成这个文件。Snakemake会自动发现这个依赖链:当你要生成 data/clean.txt 时,它会先检查 data/raw.txt 是否存在,不存在就去执行 download 规则。

我的习惯:把输入输出文件写得完整一点,不要只写文件名。我在项目中吃过亏——写了"raw.txt",结果snakemake在工作目录乱找,加上路径就清爽很多。

shell命令中的占位符

Snakemake提供了非常方便的占位符:{input} 自动展开成所有输入文件的路径,{output} 自动展开成所有输出文件的路径。如果只有一个输入一个输出,那就是对应的路径字符串。

多个输入文件怎么办?默认会以空格拼接。你也可以用 {input:q} 给每个路径加上引号,防止路径中有空格出问题。

看个综合例子:

rule fastqc:
    input:
        "data/sample_R1.fastq",
        "data/sample_R2.fastq"
    output:
        "results/fastqc/sample_R1_fastqc.html",
        "results/fastqc/sample_R2_fastqc.html"
    shell:
        "fastqc -o results/fastqc {input}"

这里 {input} 会展开成 data/sample_R1.fastq data/sample_R2.fastq,正好符合 fastqc 命令的语法。

第一个完整Snakefile

我们把前面的片段拼起来,写一个能跑通的流程。假设我们要:

  • 从某个地址下载原始数据
  • 用 tr 命令把大写字母转成小写
  • 统计行数

Snakefile如下:

rule all:
    input:
        "results/line_count.txt"

rule download_data:
    output:
        "data/raw.txt"
    shell:
        "echo 'HELLO SNAKEMAKE' > data/raw.txt"

rule to_lower:
    input:
        "data/raw.txt"
    output:
        "data/lower.txt"
    shell:
        "tr 'A-Z' 'a-z' < {input} > {output}"

rule count_lines:
    input:
        "data/lower.txt"
    output:
        "results/line_count.txt"
    shell:
        "wc -l {input} > {output}"

这里我加了一个 rule all。它是一种特殊规则,只指定最终要的文件,不干活。Snakemake默认会用第一条规则来确定最终目标文件。不写 rule all 的话,就要在命令行里指定目标文件:snakemake results/line_count.txt。

我曾经踩过的坑:忘记建 data/ 和 results/ 目录,Snakemake虽然会在运行shell命令时自动处理,但最好用 directory() 或先创建再写文件。另外,如果输出文件路径中的目录不存在,有些命令会报错。建议在shell命令前加 mkdir -p $(dirname {output}) 。

直接运行

保存上面的内容为 Snakefile,然后在终端运行:

snakemake -j 1

参数 -j 1 表示只用1个CPU核。如果一切顺利,你会看到Snakemake逐条执行:创建目录、运行 echo、tr、wc。最后在 results/line_count.txt 里看到行数。

再跑一次呢?Snakemake会发现所有文件都已经存在并且没有更新,直接提示 Nothing to be done. ——这就是增量构建的魅力。

Snakefile内部结构一览

下面这张图可以帮你从宏观上理解Snakefile的组织方式:

Snakefile 核心结构 Snakefile 全局配置(可选) configfile, params, 导入模块 规则 (rule) input / output / shell / run rule all(入口规则) 指定最终目标文件 多个规则通过文件依赖串联 A的输出 → B的输入 Snakemake 执行流程 解析 Snakefile → 构建 DAG → 依序执行缺失的输出 成功生成所有目标文件 提示 文件驱动 依赖解析 免重跑

这张图展示了Snakefile的整体骨架:最上面是全局配置,中间是规则实体,通过文件依赖形成有向无环图(DAG)。rule all 作为入口告诉Snakemake最终目标,Snakemake反向推导出所有需要执行的规则。

规则中的其他常用字段

字段作用示例
input指定依赖的文件input: "data/raw.txt"
output指定生成的文件output: "results/out.txt"
shell要执行的shell命令shell: "cp {input} {output}"
run用Python代码执行run: open(output[0],"w").write("done")
params传递给shell的额外参数params: extra="--verbose"
log日志文件(不参与依赖判断)log: "log/process.log"
threads此规则使用的CPU核数threads: 4
resources内存、磁盘等资源限制resources: mem_mb=2000

你会发现 shell 和 run 是互斥的——用了shell就不能用run,反之亦然。我个人推荐:能用shell搞定的就用shell,因为bioinfo命令行工具多,而且shell命令容易调试。复杂的逻辑再考虑用Python的 run 块。

一个小技巧:在 shell 命令里用 {input} 和 {output} 时,Snakemake会自动处理多个文件的情况。但如果你的命令需要区分不同输入,比如一个作为参考文件,一个作为样本文件,那就在 input 里用命名输入:input: ref="genome.fa", sample="sample.fastq",然后在shell里用 {input.ref} 和 {input.sample} 分别引用。这个在后面的章节会详细讲。

总结一下本章要义

  • Snakefile由规则组成,每个规则描述一个工作步骤
  • 规则核心三要素:输入文件、输出文件、要执行的命令
  • 输出文件是规则的“身份证”——Snakemake通过文件是否存在决定是否运行
  • 规则之间通过输入/输出文件自动建立依赖,形成DAG
  • rule all 作为默认目标,指明最终要生成的文件
  • 运行 snakemake -j N 即可触发整个流程

写第一个Snakefile其实很简单,把你想做的每一步拆成规则,写好输入输出和命令,Snakemake帮你搞定依赖和增量运行。你在实际项目中可能一下子就有十几个规则,但只要理解了这里的核心结构,后面扩展起来非常顺手。