规则详解:rule 的零件箱

一条 Snakemake 规则,说白了就是告诉工作流引擎三件事:原料在哪、成品长什么样、怎么加工。这三个问题分别对应 input、output 和 shell/run。再加上 params 这类辅助零件,你就能拼出任何流程。

我最早写规则时,总觉得 input 就是把文件名列出来就完了。后来被通配符坑过几次,才明白这哥们儿背后藏着「模式匹配引擎」。咱们今天就把它彻底拆开看。

骨架:一条规则长什么样

先看一个最简单的例子:

rule fastqc:
    input:
        "data/sample1.fastq"
    output:
        "results/fastqc/sample1_fastqc.html"
    shell:
        "fastqc {input} -o results/fastqc/"

你肯定一眼就看懂了。但别急,真正的项目里,input 和 output 不会这么平铺直叙。咱们得学会「泛化」,让一条规则处理所有样本。

input:不只是列表,更是模式

我的建议是:尽量少写硬编码路径。用通配符 {wildcard} 把规则变成模板。比如:

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

这里 {sample} 就是通配符。Snakemake 会从 output 里反推它的值,然后回填到 input。整个过程自动匹配——你只管写模式,引擎替你算。

小技巧:如果你需要多个输入文件,可以用命名输入,这样在 shell 里引用更清晰:
input: reads="data/{sample}.fastq", adapters="refs/adapters.fa"
然后在 shell 里写 "{input.reads}" 和 "{input.adapters}"。

output:规则的身份证

输出文件决定了规则什么时候跑、是否重跑。我个人习惯把 output 全部放在 results/ 下,按工具和样本分层。这样清理时也方便。

你可以用 {wildcard} 做输出,还可以用 {wildcard} 做目录。比如:

output: "results/multiqc/{date}/multiqc_report.html"

但注意:Snakemake 不会自动创建中间目录。我在项目中遇到过两次报错,都是因为目录不存在。解决办法是在 shell 开头加一句 mkdir -p $(dirname {output}),或者用 run 块调 Python 的 os.makedirs。

踩坑记录:output 里的通配符必须和 input 里的通配符数量和名称一致,否则规则会报「WildcardError」。我曾经把样本名写成 {sample_id},input 里却用了 {sample},结果排查了十分钟…注意对着写。

shell vs run:选谁?

这俩干的事差不多,但使用场景完全不同。

特性 shell run
执行环境 子 shell 调用 Python 函数
适用场景 已有命令行工具 纯 Python 逻辑或预处理
变量替换 用 {} 自动替换 需手动读取 input,output
调试难度 低(出错看 stdout) 中等(需加 print)

我的原则:能用 shell 搞定就别折腾 run。但如果你要批量改文件名、写配置文件,那 run 更灵活。举个例子:

rule generate_config:
    output: "configs/{sample}.yaml"
    params:
        threads = 4,
        mem = "8G"
    run:
        config = {
            "sample": wildcards.sample,
            "threads": params.threads,
            "mem": params.mem
        }
        with open(output[0], "w") as f:
            yaml.dump(config, f)

看到没?run 块里可以写完整的 Python 代码,甚至调第三方库。但代价是 Snakemake 没法直接给你做 shell 里的自动变量替换了,你得手动从 wildcards 和 input、output 字典里取值。

params:给规则灌额外参数

有些参数既不是输入文件,也不是输出路径,比如线程数、内存、额外 flag。这时候用 params 最干净:

rule bwa_mem:
    input:
        ref = "refs/hg38.fa",
        reads = "data/{sample}_R{read}.fastq"
    output:
        "results/bam/{sample}.bam"
    params:
        extra = "-M -R '@RG\\tID:{sample}'",
        threads = 8
    shell:
        "bwa mem -t {params.threads} {params.extra} {input.ref} {input.reads} | samtools sort - > {output}"

你想想看,如果把 -M -R '@RG...' 直接嵌进 shell 字符串里,万一哪天需要改,你得翻整个字符串。用 params 拆分后,配置一目了然,也方便后续用 config 或命令行覆盖。

经验之谈:params 里可以放 Python 对象,比如列表、字典。我曾在 params 里放一个 lambda 函数,用来动态计算适配器序列——虽然有点骚,但确实好用。不过别滥用,保持可读性更重要。

其他零件:threads、log、benchmark

作为基础规则,建议加上 threads 和 log。前者控制并行,后者存标准输出/错误。写法如下:

rule fastqc:
    input: "data/{sample}.fastq"
    output: "results/fastqc/{sample}_fastqc.html"
    threads: 4
    log: "logs/fastqc/{sample}.log"
    shell:
        "fastqc -t {threads} {input} -o results/fastqc/ 2> {log}"

我曾经漏写 log,结果出错了连错误信息都找不到,只能重跑。现在养成习惯:每条规则至少配一个 log,哪怕 shell 里只写 2>&1 | tee {log}。

本章知识结构图

下面这张图把一条规则的零件和它们的关系串起来了:

Snakemake 规则组成结构 rule <name> input output shell | run params threads / resources log / benchmark 关键解读 • input/output 是规则成对出现的「契约」:Snakemake 通过 output 文件是否存在判断是否执行。 • shell 和 run 二选一:前者适合命令,后者适合 Python 代码。 • params 是「参数收纳盒」:把杂乱的 flag、阈值、配置都放进去,让规则更整洁。 • log/threads 是「质量保障」:不加 log 等于蒙眼跑流程。

从图上你能看到,一条规则的「核心三件套」——input、output、shell/run——是必选项。其他都是锦上添花的辅助零件。但千万别小看辅助件,在实际项目中,它们决定了你的工作流能否复用、能否被队友看懂。

快速上手清单

总结一下写规则时的检查点,每次写之前过一遍:

  1. input 和 output 的通配符是否对称?—— 数量、名称都得一致。
  2. output 目录是否已创建?—— 要么在 shell 里 mkdir,要么在 run 里 os.makedirs。
  3. shell 里的变量名是否和 input/output/params 对得上?—— 比如 {input.ref} 别写成 {input.reference}。
  4. 是否写了 log?—— 不然出错了只能去系统日志里大海捞针。
  5. params 里有没有硬编码路径?—— 尽量用 {wildcards} 或 config 替换。

嗯,零件讲完了。你拿着这张零件清单,就能搭出任何生信流程。记住:Snakemake 的规则不复杂,复杂的是你把流程逻辑想清楚。规则只是把逻辑写出来而已。