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 会发现:

  1. 需要 results/sample1.sorted.bam → 找到 sort_bam 规则
  2. sort_bam 需要 results/sample1.sam → 找到 bwa_align 规则
  3. bwa_align 需要 data/sample1.fastq → 文件存在,开始执行

于是它会先跑 bwa_align,再跑 sort_bam。整个过程你只需要定义规则,依赖关系 Snakemake 自动推断。

注意:两个规则的 output 文件路径中通配符的名称必须一致(比如都用 {sample}),否则 Snakemake 没法匹配。我曾经犯过一个错:上游用 {sample},下游用 {id},结果它当成两个不相关的规则,各自报错找不到文件。通配符名字要保持统一。

流程图:一个完整的 Snakefile 核心结构

下面这张 SVG 图展示了一条完整的数据流,从原始 FastQ 到最终 BAM,两个规则通过中间文件 *.sam 串联起来:

Snakefile 核心结构:规则串联示例 bwa_align input: data/{sample}.fastq output: results/{sample}.sam sort_bam input: results/{sample}.sam output: results/{sample}.sorted.bam 文件匹配 data/sample.fastq (已存在) 输入依赖 results/sample.sorted.bam (目标) Snakemake 自动推导执行顺序
关键点:规则之间的连接完全靠 文件路径 和 通配符 匹配。你不需要写任何“先执行规则A再执行规则B”的命令,Snakemake 自己会建立 DAG(有向无环图)。我刚开始用的时候,总想用手动控制顺序,结果发现画蛇添足。放手让 Snakemake 干活,它比你想象中聪明。

避坑指南:常见错误

  • shell 命令里的引号嵌套:如果你在 shell 里用了双引号,外层 shell 块必须用单引号或三引号。我踩过这种雷:shell: "awk '{print \$1}' {input}" 里面的美元符号需要转义,否则会被 Snakemake 当作占位符。
  • 路径不要写死绝对路径:尽量用相对路径,便于移植。我曾经把数据放在 /home/user/project/data,结果换到服务器上路径全要改。用 config 文件或者环境变量管理路径更灵活。
  • 别忘了创建输出目录:Snakemake 不会自动创建 output 中声明的目录。如果目录不存在,shell 命令会报错。一个好习惯是在 shell 里先 mkdir -p $(dirname {output})。
个人经验:我一般会在 Snakefile 开头定义一个 rule all,里面只写最终想要的文件列表。这样你只需 snakemake -j 一次,Snakemake 会自动找到所有上游依赖并执行。比如:
rule all:
    input:
        "results/sample1.sorted.bam",
        "results/sample2.sorted.bam"
这样就不用每次手动指定目标文件了。