规则编写基础: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,这样链条就串起来了。
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:怎么选?
| 维度 | shell | run |
|---|---|---|
| 执行环境 | 子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。
shell里写{input}在Python风格规则下会被当成列表,导致命令变成cat ['file1.txt', 'file2.txt']一样荒谬。我在shell里用{input:q}或者"{input}"来强制空格分离,或者在run里手动拼接字符串。