第3章:Snakefile基础——规则、输入输出、通配符与shell命令

好了,终于到了能让你真正跑起来的一章。前面两章我们把批次效应的概念和Snakemake的安装环境都捋了一遍,现在该写点正经东西了。

Snakefile,说白了就是一个带“规则”的Python脚本。它不像普通Python那样从上到下顺序执行,而是告诉你:我要从A文件生成B文件,你看着办。

我个人习惯把Snakefile理解成一个“菜谱集合”。每个规则就是一道菜的做法:你需要什么原料(输入),最后端上什么菜(输出),中间怎么操作(shell命令或run代码块)。

3.1 规则的基础结构

一个最简单的Snakefile长这样:

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

运行一下试试看:

snakemake -c1 hello.txt

为什么会多写一个目标文件?这就是Snakemake的核心理念——一切以文件为中心。你告诉它「我要生成hello.txt」,它就去找到名为hello的规则,执行里面的shell命令。如果hello.txt已经存在且没有修改过,它甚至不会执行。

我在项目中遇到过不少新手直接在shell里写一串命令,然后问我为什么没跑通。其实多半是忘了指定目标文件。

小技巧:Snakemake默认以第一个规则为目标。如果你想设置默认行为,把第一个规则写成rule all:,里面只放output,不干活。

3.2 输入与输出文件

单个文件好理解,但现实工作流哪有这么简单?你从测序仪拿到的fastq文件,经过质控、比对、定量,每一步都要处理好几个文件。来看看带输入输出的规则怎么写:

rule fastqc:
    input:
        "sample_1.fastq"
    output:
        "sample_1_fastqc.html",
        "sample_1_fastqc.zip"
    shell:
        "fastqc sample_1.fastq"

这里有一个细节:input和output可以写多个文件,用逗号分隔。Snakemake会自动判断依赖关系——如果sample_1.fastq不存在或者更新了,它会重新跑fastqc。

这种文件级别的增量更新,说白了就是让计算机替你记住「哪些事做过了,哪些事还没做」。你在命令行里跑pipeline时最容易犯的错就是:某一步失败了,重新跑时又把前面几小时的工作覆盖了。在Snakemake里,这种情况几乎不会发生。

嗯,这里要注意:output文件一旦生成,Snakemake就不会再执行这个规则,除非你主动用snakemake --force强制重跑,或者input文件变化了。

3.3 通配符——让规则活起来

刚才的规则只能处理sample_1.fastq,你有100个样本就要写100条规则?开什么玩笑。通配符就是用来解决这个问题的:

rule fastqc:
    input:
        "{sample}.fastq"
    output:
        "{sample}_fastqc.html",
        "{sample}_fastqc.zip"
    shell:
        "fastqc {input}"

看到{sample}了吗?这就是通配符。当你执行:

snakemake -c1 sample_42_fastqc.html

Snakemake会把{sample}自动替换成sample_42,然后去找sample_42.fastq。是不是很优雅?

通配符的值由你指定的目标文件名决定。Snakemake会反向推导:目标文件名里有什么通配符,就从output的模板里提取出来,然后填入input。

我曾经在做一个RNA-seq分析流程时,用了三个通配符嵌套:{condition}_{replicate}_{lane}。一开始觉得挺酷,后来发现debug时脑子要转好几个弯。所以我的建议是:通配符别超过2个,能用一个解决的问题别用两个。

元素 语法 示例
单文件输出 output: "结果.txt" 直接写文件名
多文件输出 output: "a.txt", "b.txt" 用逗号分隔
通配符 "{wildcard}.txt" 花括号包裹变量名
引用输入 {input} 在shell里使用通配符展开

3.4 shell命令的正确写法

shell命令是Snakefile里最直接的部分——你原来在终端怎么敲,现在就怎么写。但有几个坑你必须知道。

第一,别直接用文件名。

# 错误写法
rule bad:
    input: "data.csv"
    output: "result.csv"
    shell: "python script.py data.csv result.csv"

# 正确写法
rule good:
    input: "data.csv"
    output: "result.csv"
    shell: "python script.py {input} {output}"

用{input}和{output}代替硬编码路径。这样Snakemake才能正确追踪文件依赖,通配符也能自动展开。

第二,小心引号嵌套。

shell命令本身就在双引号里,如果你要执行带单引号的命令怎么办?比如:

rule grep:
    input: "bigfile.txt"
    output: "filtered.txt"
    shell: "grep 'pattern with spaces' {input} > {output}"

这没问题,单引号在双引号内部是合法的。但如果你非要用双引号套双引号,记得用转义:

shell: "grep \"pattern with quotes\" {input} > {output}"

我个人更喜欢用单引号套双引号,或者反过来,眼不花。

第三,管线命令要加set -o pipefail。

默认情况下,Shell管道的返回码是最后一个命令的返回码。如果你的grep失败了但wc -l成功了,Snakemake会认为命令成功——这可能导致下游分析用了错误的数据。解决办法:

shell: "set -o pipefail; cut -f1 {input} | grep 'gene' | wc -l > {output}"

或者用Snakemake的shell语法里的pipefail选项,但我发现直接写更靠谱。

避坑指南:我曾经在一个生产流程里没加pipefail,结果某次数据量太大导致grep内存溢出,但后面wc -l写了个0到结果文件里。下游分析全用的这个0,排查了整整两天才发现问题。从那以后,凡是管线命令我必加set -o pipefail。

3.5 实战:写一个简单的可变样本规则

把前面学的串起来,写一个能处理多个样本的规则:

rule all:
    input:
        "sample_A_qc.html",
        "sample_B_qc.html",
        "sample_C_qc.html"

rule run_fastqc:
    input:
        "{sample}.fastq"
    output:
        "{sample}_qc.html"
    shell:
        "fastqc -o {output} {input}"

执行一下:

snakemake -c1

Snakemake看到rule all需要三个html文件,就去匹配run_fastqc规则的output模板。它会自动把sample_A、sample_B、sample_C三个通配符实例化,检查输入文件是否存在,然后并行或串行地执行。

你可以试试把sample_B.fastq删掉,再跑一次——Snakemake会报错说缺输入文件,但sample_A和sample_C的规则继续执行。这才是工作流引擎该有的样子。

好了,Snakefile的核心要素就这些。接下来你能写出足以处理真实批次效应检测的pipeline骨架了。记住:规则就是你定义的一个个工序,通配符让工序可复用,输入输出让流程自动衔接,shell命令就是你在终端里每天都在敲的东西——只不过Snakemake帮你管好了「哪些需要重跑」「哪些可以跳过」这些烦心事。

本章核心:Snakefile里,一切围绕文件运转。你定义的是「从X到Y的转换规则」,Snakemake帮你决定「什么时候执行、跳过、重试」。
Snakefile 核心概念结构 规则 (Rule) 输入文件 (input) 输出文件 (output) Shell 命令 通配符 {sample} 驱动匹配 核心流程 用户指定目标文件 → Snakemake提取通配符 → 匹配规则 → 检查输入/输出 → 执行Shell