管道与重定向进阶:stdin/stdout/stderr/文件描述符
生信流程本质是数据在管道中流动:FASTQ → 质控 → 比对 → SAM → 排序 → BAM → 定量 → 计数矩阵 → 差异表达。每一步都是 stdin → tool → stdout。理解管道和重定向不只是语法技巧,而是理解整个流程数据流的基础。
本文不讲 | 和 > 的基础语法,聚焦 进程替换、tee 分流、命名管道、文件描述符操作 等进阶技巧,覆盖 SAM 排序压缩、FASTQ 质控过滤、批量 GFF 处理等场景。
实测环境:Debian 12,Bash 5.2.32。
1. 三兄弟:stdin、stdout、stderr
每个 Linux 进程生来就有三个文件描述符:
| 描述符 | 名称 | 编号 | 默认指向 | 生信用途 |
|---|---|---|---|---|
| stdin | 标准输入 | 0 | 键盘 | 管道传入的数据 |
| stdout | 标准输出 | 1 | 屏幕 | 正常结果(FASTQ、BAM、TSV) |
| stderr | 标准错误 | 2 | 屏幕 | 日志、警告、进度信息 |
理解了这个,重定向语法就一通百通:
# 基础重定向command > file # stdout → file(覆盖)command >> file # stdout → file(追加)command < file # file → stdincommand 2> file # stderr → filecommand 2>&1 # stderr → stdout(合并到同一个流)command &> file # stdout + stderr → file(Bash 4.0+)
# 生信中最常见的模式samtools view -bS aligned.sam > aligned.bam 2> samtools.log# stdout(BAM 内容)→ aligned.bam# stderr(日志)→ samtools.log1.1 进程视角的公式
如果用数学表达一个进程的 I/O 模型:
Shell 的 > 和 < 就是重新绑定 和 的目的地:
2. 管道——生信流程的骨架
2.1 管道的基础模型
# 管道连接 stdout → stdinsamtools view aligned.bam | head -100
# 等价于:# samtools view 的 stdout → head 的 stdin管道的理论基础是生产者-消费者模型:
如果前一步(producer)快、后一步(consumer)慢,管道会自动缓冲(默认 64KB)。如果前一步慢、后一步快,消费者会阻塞等待。管道的速度由最慢的步骤决定。
2.2 生信管道实战场景
# 场景1:FASTQ 质控 + 比对一气呵成fastp -i R1.fastq.gz -I R2.fastq.gz --stdout 2> fastp.log \ | bwa mem -t 8 -p /opt/refs/hg38.fa - 2> bwa.log \ | samtools sort -@ 4 -o aligned.sorted.bam - 2> sort.log
# 这里用了三个技巧:# 1. fastp --stdout:输出到 stdout 而非文件# 2. bwa mem 用 - 表示从 stdin 读取# 3. samtools sort 也用 - 从 stdin 读
# 场景2:统计 BAM 中每条染色体的 reads 数samtools idxstats aligned.bam \ | awk '$3 > 0' \ | sort -k3 -nr \ | head -10
# 场景3:从 VCF 中提取高质量 SNP 并统计bcftools view -i 'QUAL>30 && TYPE="snp"' variants.vcf.gz \ | bcftools query -f '%CHROM\t%POS\t%REF\t%ALT\n' \ | wc -l3. tee——数据流的分叉器
tee 把 stdin 复制两份:一份继续走管道,一份写入文件。比喻就是河流分叉:
# 在管道中间"偷看"数据,同时保存samtools view aligned.bam \ | tee intermediate.sam \ | awk '{print $3}' \ | sort | uniq -c | sort -rn \ | tee chr_stats.txt \ | head -10
# 第一条 tee:把 SAM 内容保存到文件,同时继续传给 awk# 第二条 tee:把统计结果保存,同时传给 head 显示3.1 同时输出到 stdout 和 stderr
# 用进程替换,tee 把日志同时打到终端和文件samtools view -bS aligned.sam 2>&1 | tee samtools.log# 2>&1 先把 stderr 合并到 stdout,tee 再复制3.2 多个文件 + 管道
# 同时写入多个文件(生信:备份 + 继续处理)cat sample_list.txt \ | tee >(grep "^S[0-9]" > samples.txt) \ | tee >(grep "^C[0-9]" > controls.txt) \ > all.txt# >(...) 是进程替换——把命令当作"文件"4. 进程替换——最被低估的 Bash 特性
4.1 本质
进程替换 <(command) 和 >(command) 创建一个匿名管道(named pipe 的一种),把命令的 stdout/stdin 伪装成文件路径:
# 不用临时文件直接比较两个命令的输出diff <(cut -f1 file1.txt | sort) <(cut -f1 file2.txt | sort)
# 等价但无需中间文件:# cut -f1 file1.txt | sort > /tmp/file1_sorted# cut -f1 file2.txt | sort > /tmp/file2_sorted# diff /tmp/file1_sorted /tmp/file2_sorted# rm /tmp/file1_sorted /tmp/file2_sorted4.2 生信场景
# 场景1:比较两个样本的基因列表(不生成中间文件)comm -12 \ <(awk '$8>0 {print $1}' sample1_counts.tsv | sort) \ <(awk '$8>0 {print $1}' sample2_counts.tsv | sort) \ > common_genes.txt
# 场景2:快速检查两个 BAM 文件的 header 差异diff <(samtools view -H sample1.bam) <(samtools view -H sample2.bam)
# 场景3:GFF 和 BED 坐标交集(不需要中间文件)bedtools intersect \ -a <(awk '$3=="gene"' annotation.gff3 | gff2bed) \ -b <(awk '$5>10' peaks.bed) \ > overlapping_genes.bed
# 场景4:把 stderr 也送入管道处理samtools flagstat aligned.bam 2> >(grep "mapped" > mapping_stats.txt)5. 命名管道——跨进程通信
5.1 创建和使用
# 创建命名管道(FIFO)mkfifo my_pipe
# 终端1:写入端gzip -c huge.fastq > my_pipe
# 终端2:读取端(另一个终端/进程)bwa mem ref.fa my_pipe > aligned.sam
# 清理rm my_pipe5.2 生信实战——并行解压与比对
#!/bin/bash# 并行解压 + 比对,避免磁盘 I/O 瓶颈
PIPE=$(mktemp -u)mkfifo "${PIPE}"
# 后台:解压 FASTQ 到命名管道gzip -dc sample.fastq.gz > "${PIPE}" &
# 前台:从管道读取并用 bwa 比对bwa mem -t 16 /opt/refs/hg38.fa "${PIPE}" \ | samtools sort -@ 8 -o aligned.sorted.bam -
rm "${PIPE}"
# 优势:解压和比对并行,避免把解压后的几十 GB 数据写入磁盘对于磁盘 I/O 瓶颈的场景,命名管道能显著提速。时间模型:
当解压出的数据量很大时,。
6. xargs——跨越管道的限制
管道的核心限制:每个 | 右边只能是一个命令的 stdin,不能是命令参数。xargs 就是打破这个限制的工具。
6.1 基础用法
# 问题:管道只能传 stdin,不能传参数# 下面的写法是错的:echo "aligned.bam" | samtools view -h # 错误!samtools view 需要参数不是 stdin
# 正确:用 xargs 把 stdin 变成参数echo "aligned.bam" | xargs samtools view -h | head -20
# xargs 的核心:把 stdin 的每一行变成后面命令的参数6.2 并行 xargs——生信批量处理加速
# 对所有 BAM 文件并行生成索引(-P 指定并行数)find . -name "*.bam" | xargs -P 8 -I {} samtools index {}
# -P 8 → 同时运行 8 个进程# -I {} → {} 是占位符,代表每个输入行对于独立任务(每个 BAM 文件互不影响),加速比近似为:
其中 是并行数, 是无法并行的比例(I/O 等待通常占 10-20%)。
6.3 生信批量处理模板
# 1. 批量质控ls *.fastq.gz | xargs -P 4 -I {} fastqc {} -o qc_results/
# 2. 批量比对ls *_R1.fastq.gz | sed 's/_R1.fastq.gz//' | xargs -P 8 -I {} sh -c \ 'bwa mem -t 2 /opt/refs/hg38.fa {}_R1.fastq.gz {}_R2.fastq.gz | samtools sort -@ 2 -o aligned/{}_sorted.bam -'
# 3. 批量统计ls *.bam | xargs -P 8 -I {} sh -c 'samtools flagstat {} > stats/{}.flagstat'6.4 处理文件名中的空格
# 文件名含空格时,用 -0 配合 find 的 -print0find . -name "*.bam" -print0 | xargs -0 -P 4 -I {} samtools index {}
# -print0:用 \0 分隔(而不是换行)# -0:xargs 以 \0 作为分隔符7. 高级重定向技巧
7.1 交换 stdout 和 stderr
# 3>&2 2>&1 1>&3- 这个咒语的拆解# 场景:想让 samtools 的日志走管道,但 BAM 数据写入文件
exec 3>&1 # 备份原始 stdout 到 fd3samtools view -bS aligned.sam 2>&1 1>&3 | tee samtools.logexec 3>&- # 关闭 fd3
# 解释:2>&1 让 stderr 合并到当前的 stdout(此时是终端)# 1>&3 让 stdout 恢复到原始的 fd3→终端# 结果:stderr → 管道 → tee,stdout → 文件(通过重定向)7.2 Here Documents——内嵌数据
# 把多行文本直接传给命令bcftools view -i 'QUAL>30' << 'EOF'##fileformat=VCFv4.2#CHROM POS ID REF ALT QUAL FILTER INFOchr1 12345 . A G 45.2 PASS .chr1 23456 . C T 12.1 PASS .EOF
# 在脚本里内嵌 GTF 注解的某几行grep "BRCA1" << 'END_GTF' | cut -f1,4,5chr17 41196312 41197819 BRCA1chr17 41199659 41199719 BRCA1END_GTF7.3 Here Strings——单行快速输入
# <<< 把字符串当 stdinbcftools view -h variants.vcf.gz | grep "fileDate" <<< ""# 或grep "SRR12345678" <<< "$(cat sample_list.txt)"
# 用 bc 做简单计算bc <<< "scale=4; 35000000 / 1000000"# 输出:35.00008. 踩坑记录
坑1:管道里的 set -e 不生效。 set -e 只看管道最后一个命令的退出码。如果 false | true,整个管道返回 0(因为 true 成功了)。解决方案:set -o pipefail 让任何一步失败都导致管道失败。
坑2:大管道缓冲溢出。 管道缓冲区默认 64KB(Linux),如果前一步输出快、后一步处理慢,超出缓冲区后前一步会阻塞。在 SAM→BAM 这种场景不明显,但在超大数据流(如全基因组测序 raw data)中可能成为瓶颈。可以用 pv 监控管道流量:cmd1 | pv | cmd2。
坑3:xargs 遇到空输入时默认仍执行命令。 echo "" | xargs rm 会执行 rm(无参数,可能报错)。用 xargs -r(--no-run-if-empty)防止空输入时执行。
坑4:命名管道没有读取端时写入端会阻塞。 mkfifo mypipe && gzip -c data > mypipe 如果没有另一个进程在读 mypipe,这个命令会永久挂起。务必先准备好读取端,或在写入端设置超时。
坑5:进程替换 <(...) 的退出码无法直接获取。 diff <(cmd1) <(cmd2) 中 cmd1 或 cmd2 失败了你不知道。Bash 4.4+ 可以用 wait 或临时文件来获取退出码。
坑6:2>&1 和 1>&2 的区别。 2>&1 是把 stderr 重定向到 stdout 当前指向的地方(注意”当前”两个字——在重定向链中顺序至关重要)。command 2>&1 > file 是错的:先把 stderr 指向当前 stdout(终端),再把 stdout 指向 file。正确是 command > file 2>&1。
坑7:xargs 中复合命令的引号嵌套地狱。 xargs -I {} sh -c 'command "{}"' 里引号嵌套很容易出错。改用 xargs -I {} bash -c "command '{}'" 或直接写成独立脚本。生信批量处理推荐写成独立脚本然后用 xargs 调用。
本文于 2025-11-22 在 Debian 12 上实测。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!