规则中的 shell 命令:从入门到实战
好,我们直接进入正题。Snakemake 的规则里,shell 指令是最常用的执行方式。说白了,你写一条命令行,Snakemake 就帮你跑一条。但这里头有不少门道,我先讲讲最基础的写法。
基础 shell 命令写法
最简单的规则,就是 input、output、shell 三件套。举个例子:
rule align_reads:
input:
"sample.fastq"
output:
"sample.sam"
shell:
"bwa mem reference.fasta {input} > {output}"
你看到没?{input} 和 {output} 是 Snakemake 内置的占位符。运行时它会自动替换成你定义的文件路径。我个人习惯把这种简单的命令直接写在 shell 块里,清晰明了。
但如果文件多了,建议用索引指定,比如
{input[0]}、{output[1]},避免混乱。
字符串格式化:花括号里的学问
嗯,这里我们要细说 {} 的用法。Snakemake 在 shell 里支持 Python 的字符串格式化语法。你可以在花括号里写变量、属性,甚至函数调用。
我碰到过一个坑:文件名里带空格。直接写 {input} 会被 shell 解析成两个参数,命令就炸了。解决办法是用引号包裹:
rule bad_example:
input:
"my sample.fastq"
output:
"my sample.sam"
shell:
"bwa mem ref.fasta {input} > {output}"
# 正确写法:加双引号
rule good_example:
input:
"my sample.fastq"
output:
"my sample.sam"
shell:
"bwa mem ref.fasta \"{input}\" > \"{output}\""
说白了,Snakemake 不会帮你处理文件名里的特殊字符。你得自己考虑 shell 解释规则。我曾经因为这个 bug 排查了俩小时……嗯,后来学乖了,只要路径里有空格,一律加引号。
变量引用:不止是 input 和 output
Snakemake 在 shell 里可以引用很多变量。我给你列张表,都是我常用的:
| 变量名 | 含义 | 示例用法 |
|---|---|---|
{input} |
输入文件(列表或单文件) | bwa mem ref.fa {input} |
{output} |
输出文件(列表或单文件) | samtools sort {output} |
{params} |
用户自定义参数 | java -Xmx{params.mem}G -jar |
{wildcards} |
通配符捕获的值 | mv {wildcards.sample}_temp.txt {output} |
{threads} |
线程数 | --threads {threads} |
{log} |
日志文件 | cmd &> {log} |
注意,{params} 需要你在规则里定义 params 块。我最常用它来传一些程序参数,比如:
rule varscan_somatic:
input:
tumor="tumor.bam",
normal="normal.bam"
output:
"somatic.vcf"
params:
min_coverage=10,
min_var_freq=0.05
shell:
"varscan somatic {input.tumor} {input.normal} "
"--min-coverage {params.min_coverage} "
"--min-var-freq {params.min_var_freq} "
"> {output}"
你想想看,把参数抽象出来放在 params 里,修改起来多方便。不用每改一次阈值就去翻 shell 命令。
shell 块里,{} 是 Snakemake 的格式化标记。如果你在命令里需要字面上的花括号(比如某些工具用 {} 作为占位符),你得转义:写成 {{ 和 }}。我曾经干过这事:
"echo {{hello}}" 输出的是 {hello} 而不是 hello,因为 Snakemake 先处理了单层花括号。
多行 shell 命令:换行与续行
Shell 命令一长就不好看。Snakemake 允许你用 Python 的隐式字符串拼接来换行,或者直接用三引号 """。
# 方法一:括号内的隐式拼接
rule complex_pipeline:
input:
"input.bam"
output:
"output.vcf"
shell:
("samtools view -bS {input} | "
"samtools sort -@ {threads} - | "
"freebayes -f ref.fasta - > {output}")
# 方法二:三引号(保持换行)
rule alternative:
input:
"input.bam"
output:
"output.vcf"
shell:
"""
samtools view -bS {input} |
samtools sort -@ {threads} - |
freebayes -f ref.fasta - > {output}
"""
我个人倾向于用三引号,因为能看到实际的管道结构。但要注意,三引号里的换行会被保留,命令执行时 shell 会逐条执行——除非你用了反斜杠 \ 续行。
shell 命令中的 Python 表达式
Snakemake 的 shell 块本质上是一个格式化字符串。你可以在花括号里写合法的 Python 表达式。比如需要根据输入文件名动态生成输出:
rule rename_by_date:
input:
"data_{date}.txt"
output:
"processed_{date}.csv"
shell:
"python process.py --today {wildcards.date} < {input} > {output}"
更骚的操作是直接写 lambda 或列表推导。不过,这个不常用。你如果真要这么写,建议放到 run 块里用 Python 控制,别在 shell 里玩花样,容易把自己绕晕。
shell 里只做一件事——把参数拼好,交给 /bin/sh 执行。复杂的逻辑判断、条件分支,统统交给 params 或 run 块去处理。
避坑:关于变量作用域
对了,还有个坑。你在 shell 里引用的变量,必须是 Snakemake 能识别的名称。比如你不能直接写 {MY_ENV_VAR} 这样引用环境变量——Snakemake 会以为你想引用一个叫 MY_ENV_VAR 的变量,然后报 KeyError。
正确的做法是用 os.environ 读取,或者通过 params 传进来:
import os
rule use_env_var:
output:
"env_test.txt"
params:
my_var=os.environ.get("MY_VAR", "default_val")
shell:
"echo {params.my_var} > {output}"
记住:shell 块里的 {} 是 Snakemake 的格式化引擎处理的,不是 shell 的。你如果真想在 shell 里引用系统环境变量,用 $MY_VAR 或者 ${MY_VAR},但别包在 Snakemake 的花括号里。
从 shell 到可复用的 Bash 命令集
最后,我分享个实践经验。当你一个 pipeline 里多次用到相同命令片段时,可以把它抽成 Bash 函数,在 shell 里调用:
rulegroup utility:
shell:
"""
function sort_bam() {
local input=$1
local output=$2
samtools sort -@ {threads} -m 2G "$input" | samtools index - "$output"
}
"""
rule call_variants:
input:
"aligned.bam"
output:
"sorted.bam"
threads: 8
shell:
"sort_bam {input} {output}"
这种做法能让你的 Snakefile 像脚本库一样整洁。我常在大型项目里这么干,把常用操作(排序、去重、压缩)封装成函数,减少重复代码。
好了,shell 命令这块基本就这些。说白了就是一个规则:先搞清楚 {} 里能写什么、不能写什么,剩下的就是 shell 基本功了。
嗯,整个流程你看这张图就清楚了。从输入、参数、通配符,到格式化引擎拼接,最后交给 shell 执行。每个环节都有注意事项,但只要你照着上面的例子写,基本不会出大问题。