规则编写基础:rule关键字、input、output、shell、run

玩转Snakemake,第一件事就是写好一个rule。它就像流水线上的工位——你告诉系统「把什么原材料(input)变成什么产品(output),用什么工具加工(shell或run)」。我个人习惯把每个rule看作一个声明式的配方,Snakemake帮你搞定顺序和依赖。下面我把核心要素拆开讲。

rule关键字的定义

rule是整个工作流的基石。它用名字标识一个处理步骤,后面跟一对花括号包裹的参数块。名字可以随便起,但最好直观——比如我叫bwa_alignment、qc_fastq,一看就知道在干什么。最基本的骨架长这样:

rule 步骤名称:
    input:   "输入文件"
    output:  "输出文件"
    shell:   "执行的命令"

注意缩进:整个rule内部统一用4个空格(别用Tab,我踩过这个坑,Snakemake会报解析错误)。每个参数后面跟冒号,不用的参数可以省略。

命名小建议:用下划线而不是驼峰,比如trim_galore而不是trimGalore。为什么?因为Snakemake内部用rule.xxx引用,一致性能省去很多麻烦。

核心思想:rule只管“如何做”,不管“什么时候做”。调度逻辑全交给Snakemake自己推理。

input和output:数据流的显式声明

说白了,input告诉系统“我需要什么文件才能干活”,output告诉系统“干完活会产出什么”。这两行决定了工作流的DAG(有向无环图)。我见过有人把input写成硬编码路径,结果换一个样本就得改整个文件——千万别这么干,要用通配符({sample})或函数动态生成。

rule bwa_mem:
    input:
        "data/genome.fa",
        "data/{sample}_R1.fastq",
        "data/{sample}_R2.fastq"
    output:
        "results/{sample}.bam"
    shell:
        "bwa mem {input} | samtools sort > {output}"

注意:多个输入用逗号隔开,可以写在多行(加个缩进更清晰)。output只写一个文件时直接写,多个输出同样用逗号隔开。实际项目中,output往往被下游rule当作input,这样链条就串起来了。

重要:output必须是规则的唯一标识。Snakemake通过output文件路径判断一个rule是否已经运行过。如果有两个rule产出同一个output,调度器会直接报错“multiple rules produce …”。

shell指令:调用外部程序

这是最常用的运行模式。在shell:后面写一段字符串,Snakemake会把它丢给系统的bash执行。你可以直接拼路径、用通配符展开:

rule fastqc:
    input:
        "data/{sample}.fastq"
    output:
        "qc/{sample}_fastqc.zip"
    shell:
        "fastqc -o qc {input}"

遇到复杂命令(管道、重定向、条件判断),我建议用Python的多行字符串加圆括号包裹,这样读起来舒服:

rule variant_calling:
    input:
        bam="results/{sample}.bam",
        ref="data/genome.fa"
    output:
        "results/{sample}.vcf.gz"
    shell:
        r"""
        bcftools mpileup -f {input.ref} {input.bam} | \
            bcftools call -mv -Oz -o {output}
        """

在shell字符串里,{input}、{output}、{wildcards}这些变量会在运行前被替换成真实路径。我曾经犯过迷糊:在shell里写{input.bam}时忘了加引号,结果文件名有空格导致命令炸了。现在我的习惯是所有变量外都套双引号"{input.bam}",尤其是样本名可能有特殊字符时。嗯,细节决定成败。

run指令:嵌入Python代码

当你的处理逻辑不是简单的调用一个外部命令,而是需要做一些条件判断、数据解析或调用Python库时,就用run:。它后面跟着一个缩进的Python代码块:

rule parse_results:
    input:
        "logs/summary.txt"
    output:
        "results/parsed.csv"
    run:
        with open(input[0]) as f:
            lines = f.readlines()
        # 做一些处理
        out_lines = [line.replace("\t", ",") for line in lines]
        with open(output[0], "w") as f:
            f.writelines(out_lines)

注意:在run块里,input、output、wildcards都是Python对象——input和output是列表(即使是单文件也是列表[path]),而shell里则是字符串。这个区别一定要记牢,不然你会拿到一个路径列表然后试图传给命令……我刚开始就中过招。

shell vs run:怎么选?

维度shellrun
执行环境子bash进程Snakemake所在Python进程
变量类型字符串模板({input}→路径串)Python对象(input→列表)
适用场景调用第三方工具、管道命令轻量数据处理、Python逻辑
错误处理非零退出码即失败抛出异常即失败
并行潜力由shell内部实现决定受GIL影响,CPU密集型不推荐

我个人选型标准:凡是能用一行命令解决的,用shell;需要写条件分支、循环、文件解析的,用run。还有一种混合模式(用script调用外部脚本),那是另一章的话题了。

性能提醒:run块默认占用Snakemake主进程的一条线程。如果你跑大量小样本,建议把纯计算部分放到shell里去并行的子进程,或者用threads和resources参数手动分配。

一句话总结:input和output定义数据依赖,shell和run定义操作逻辑。四个关键字组合,就能拼出一张可自动执行的工作流网络。

下面的SVG展示了rule的四个基本构成要素以及它们之间的数据流向。你可以把它当作一张“骨牌图”——每一步的output倒下,触发下一步的input。

rule 的基本构成与数据流 rule rule_name input: 文件 (依赖上游) output: 文件 (触发下游) shell: "命令..." 子bash进程调用 run: Python代码 Python进程内执行 执行核心 数据流规则 上游rule的output → 下游rule的input → 自动依赖推导 每个rule必须有且仅有一组output(可为多个文件)
新手常见错误:shell里写{input}在Python风格规则下会被当成列表,导致命令变成cat ['file1.txt', 'file2.txt']一样荒谬。我在shell里用{input:q}或者"{input}"来强制空格分离,或者在run里手动拼接字符串。