规则中的 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 块里,清晰明了。

小技巧:如果你只有一个输入和一个输出,Snakemake 会自动匹配第一个 input 和 output。
但如果文件多了,建议用索引指定,比如 {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 命令执行核心流程 输入文件 {input} 参数 {params} 通配符 {wildcards} Snakemake 格式化引擎 → 拼接字符串 /bin/sh 执行命令 输出文件 {output} 花括号内可写 Python 表达式

嗯,整个流程你看这张图就清楚了。从输入、参数、通配符,到格式化引擎拼接,最后交给 shell 执行。每个环节都有注意事项,但只要你照着上面的例子写,基本不会出大问题。