跳转至

Linux与Shell脚本(Linux & Shell Scripting)

1. 一句话概述

Linux是生信分析的基础操作系统,Shell脚本是实现流程自动化和HPC任务投递的核心技能。


2. 核心知识点

2.1 必会Linux命令

文件与目录操作

命令 用途 常用参数 生信场景示例
ls 列出目录内容 -lh(详细+可读大小), -a(显示隐藏), -t(按时间排序) ls -lhS *.fastq.gz 按大小排序查看测序文件
cd 切换目录 ..(上级), -(上一个目录), ~(家目录) cd /data/project01/raw_data/
pwd 显示当前路径 确认脚本运行时的工作目录
cp 复制文件 -r(递归复制目录), -p(保留权限) cp -r ./results/ /backup/ 备份分析结果
mv 移动/重命名 无特别参数 mv sample_001.fq.gz S1_R1.fq.gz 规范文件命名
rm 删除文件 -r(递归), -f(强制), -i(确认) rm -rf ./temp/ 清理临时文件(小心使用!)
mkdir 创建目录 -p(递归创建多级目录) mkdir -p results/{qc,assembly,binning}
ln 创建链接 -s(软链接) ln -s /database/silva138/ ./db/silva 链接数据库
find 查找文件 -name, -size, -type, -mtime find . -name "*.fastq.gz" -size +1G 找大文件
tar 打包/解包 -czf(压缩), -xzf(解压), -tf(查看) tar -czf results.tar.gz ./results/ 打包结果
gzip 压缩/解压 -d(解压), -k(保留原文件), -c(输出到stdout) gzip -k sample.fastq 压缩FASTQ保留原文件

文件查看

命令 用途 常用参数 生信场景示例
cat 查看文件全部内容 -n(显示行号) cat sample_list.txt 查看样本列表
head 查看文件前N行 -n 20(前20行) head -n 4 sample.fastq 查看一条read
tail 查看文件后N行 -n 20, -f(实时跟踪) tail -f pipeline.log 实时查看运行日志
less 分页查看大文件 /搜索, q退出, G末尾 less assembly.fasta 查看基因组文件
wc 统计行数/词数/字符数 -l(行数), -c(字节数) wc -l reads.fastq 统计行数

文本处理

命令 用途 常用参数 生信场景示例
grep 模式搜索 -c(计数), -v(反选), -i(忽略大小写), -r(递归), -E(扩展正则) grep -c "^>" genome.fasta 统计序列数
awk 列处理 -F(分隔符), NR(行号), NF(列数) awk -F'\t' '$3>50{print $1}' blast.out 筛选
sed 流编辑(替换、删除) s/old/new/g(替换), d(删除) sed 's/\./_/g' sample_list.txt 替换点为下划线
sort 排序 -n(数值), -r(逆序), -k(按列), -t(分隔符) sort -t'\t' -k3 -nr blast.out 按第3列数值逆序
uniq 去重 -c(计数), -d(只显示重复) sort taxa.txt \| uniq -c \| sort -rn 统计物种频率
cut 提取列 -f(列号), -d(分隔符) cut -f1,3 abundance.tsv 提取第1和第3列
paste 横向合并文件 -d(分隔符) paste sample1.count sample2.count > merged.tsv
xargs 将stdin转为命令参数 -I{}(占位符), -P(并行数) cat srr_list.txt \| xargs -P 4 -I{} prefetch {}

网络与传输

命令 用途 常用参数 生信场景示例
wget 下载文件 -c(断点续传), -P(指定目录) wget -c ftp://...silva.fasta.gz 下载数据库
curl HTTP请求/下载 -O(保存文件), -L(跟随重定向) curl -s "https://eutils.ncbi.nlm.nih.gov/..."
scp 远程复制 -r(递归), -P(端口) scp -r results/ user@server:/data/
rsync 增量同步 -avz(归档+压缩), --progress rsync -avz ./project/ server:/backup/

系统与进程

命令 用途 常用参数 生信场景示例
top/htop 查看进程和资源 监控分析任务的CPU和内存使用
df 查看磁盘使用 -h(可读单位) df -h /data/ 检查数据盘剩余空间
du 查看文件/目录大小 -sh(总计+可读), --max-depth=1 du -sh ./*/ 查看每个子目录大小
chmod 修改权限 +x(加执行权限), 755, 644 chmod +x pipeline.sh 给脚本加执行权限
chown 修改所有者 -R(递归) chown -R bioinfo:bioinfo ./project/
nohup 后台运行(不挂断) 配合 & 使用 nohup bash pipeline.sh > log.txt 2>&1 &
screen/tmux 终端复用器 screen -S name, tmux new -s name 长时间分析任务放在 screen/tmux 中运行

2.2 文本处理三剑客

grep —— 模式搜索

# ============================================================
# grep:按模式搜索文本行
# 记忆:grep = Global Regular Expression Print
# ============================================================

# 基本用法
grep "pattern" file.txt               # 搜索包含pattern的行
grep -c "^>" genome.fasta             # 统计FASTA序列数(以>开头的行数)
grep -v "^#" annotation.gff           # 排除注释行(以#开头)
grep -i "escherichia" taxa.txt        # 忽略大小写搜索
grep -r "ATCGATCG" ./sequences/       # 递归搜索目录中所有文件
grep -E "gene|CDS|mRNA" file.gff     # 扩展正则:匹配多个模式(OR关系)
grep -w "rRNA" annotation.txt         # 精确匹配整个单词(不匹配rRNA-like)
grep -A 1 "^>" genome.fasta          # 显示匹配行及其后1行(查看序列名+第一行序列)
grep -B 2 "ERROR" pipeline.log        # 显示匹配行及其前2行(查看错误上下文)

# 生信实战
grep -c "^@" sample_R1.fastq          # 统计FASTQ中的reads数
grep "^>" genome.fasta | wc -l        # 另一种统计FASTA序列数的方法
zgrep -c "^@" sample.fastq.gz         # 直接搜索压缩文件(不用先解压!)
grep -f gene_list.txt annotation.tsv  # 从文件读取搜索模式(批量查找)

awk —— 列处理

# ============================================================
# awk:按列处理结构化文本
# 记忆:awk = Aho, Weinberger, Kernighan(三个发明者的名字)
# ============================================================

# 基本语法:awk '条件{动作}' 文件

# 核心概念
# $0 = 整行, $1 = 第1列, $2 = 第2列, ...
# NR = 当前行号, NF = 当前行的列数
# FS = 字段分隔符(默认空格/tab), OFS = 输出分隔符

# 基本用法
awk '{print $1}' file.tsv             # 打印第1列
awk -F'\t' '{print $1, $3}' file.tsv  # 指定tab分隔,打印第1和第3列
awk 'NR==1' file.tsv                  # 打印第1行(表头)
awk 'NR>1' file.tsv                   # 跳过表头,打印数据行
awk -F'\t' '$3 > 100' blast.out       # 筛选第3列大于100的行

# 生信实战
# 计算某列的平均值(如计算平均覆盖度)
awk -F'\t' 'NR>1{sum+=$3; n++} END{print "平均值:", sum/n}' coverage.tsv

# 从GFF文件提取基因名
awk -F'\t' '$3=="gene"{print $9}' annotation.gff

# FASTQ转FASTA
awk 'NR%4==1{print ">"substr($0,2)} NR%4==2{print}' reads.fastq

# 统计FASTA文件中每条序列的长度
awk '/^>/{if(name)print name, len; name=$0; len=0; next} {len+=length($0)} END{print name, len}' genome.fasta

# 计算BLAST结果的命中率
awk -F'\t' '$3>=90 && $4>=100' blast.out | wc -l  # identity>=90%且比对长度>=100

sed —— 流编辑

# ============================================================
# sed:流式文本编辑(Stream Editor)
# 记忆:sed 擅长"替换"和"删除"
# ============================================================

# 基本用法
sed 's/old/new/' file.txt             # 替换每行第一个匹配
sed 's/old/new/g' file.txt            # 替换每行所有匹配(g = global)
sed -i 's/old/new/g' file.txt         # -i 直接修改原文件(小心使用!)
sed '1d' file.txt                     # 删除第1行
sed '/^#/d' file.txt                  # 删除所有注释行
sed -n '10,20p' file.txt              # 只打印第10到20行

# 生信实战
# 去除FASTA序列名中的空格后面的描述
sed 's/ .*//' genome.fasta            # 只保留>后第一个空格前的ID

# 替换染色体命名(如chr1 → 1)
sed 's/^chr//' annotation.bed

# 给FASTA序列名添加前缀
sed 's/^>/&sample01_/' genome.fasta   # &表示匹配到的内容

# 提取两个模式之间的内容
sed -n '/START/,/END/p' config.txt

三剑客对比

特点 grep awk sed
核心功能 搜索匹配行 列处理和计算 文本替换和编辑
输出 匹配的行 自定义格式 编辑后的文本
擅长 过滤、计数、查找 分列提取、统计计算 替换、删除、插入
典型用法 grep "pattern" file awk '{print $1}' file sed 's/a/b/g' file

2.3 Shell脚本规范

#!/bin/bash
# ============================================================
# 脚本名称: pipeline.sh
# 功能描述: 宏基因组分析流水线示例
# 作者: your_name
# 日期: 2025-01-01
# 用法: bash pipeline.sh <input_dir> <output_dir>
# ============================================================

set -euo pipefail
# set -e   : 命令出错立即退出(Exit on error)
# set -u   : 使用未定义变量时报错(Undefined variable = error)
# set -o pipefail : 管道中任何命令失败则整个管道失败

# ===================== 变量定义 =====================

INPUT_DIR="${1:?用法: bash pipeline.sh <input_dir> <output_dir>}"  # 必需参数
OUTPUT_DIR="${2:?缺少输出目录参数}"                                  # 必需参数
THREADS=${3:-8}                       # 可选参数,默认8线程
LOG_FILE="${OUTPUT_DIR}/pipeline.log"  # 日志文件路径
DB_PATH="/database/silva138"          # 数据库路径

# ===================== 函数定义 =====================

# 日志函数:记录时间戳和消息
log_msg() {
    local msg="$1"                    # local变量,函数内有效
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${msg}" | tee -a "${LOG_FILE}"
    # tee -a : 同时输出到屏幕和追加到日志文件
}

# 错误处理函数
error_exit() {
    log_msg "错误: $1"
    exit 1
}

# 检查依赖工具是否安装
check_dependency() {
    local tool="$1"
    command -v "${tool}" > /dev/null 2>&1 || error_exit "${tool} 未安装"
    # command -v : 检查命令是否存在
    # > /dev/null 2>&1 : 丢弃stdout和stderr
}

# ===================== 前置检查 =====================

mkdir -p "${OUTPUT_DIR}"              # 创建输出目录

log_msg "=== 流水线开始 ==="
log_msg "输入目录: ${INPUT_DIR}"
log_msg "输出目录: ${OUTPUT_DIR}"
log_msg "线程数: ${THREADS}"

# 检查输入目录是否存在
[[ -d "${INPUT_DIR}" ]] || error_exit "输入目录不存在: ${INPUT_DIR}"

# 检查依赖工具
check_dependency fastp
check_dependency megahit

# ===================== 条件判断 =====================

# if-else 结构
if [[ -f "${OUTPUT_DIR}/qc_done.flag" ]]; then
    log_msg "质控已完成,跳过"
else
    log_msg "开始质控..."
    # ... 质控代码 ...
    touch "${OUTPUT_DIR}/qc_done.flag"   # 创建完成标记
fi

# ===================== 循环处理 =====================

# for 循环:遍历所有样本
for sample_r1 in ${INPUT_DIR}/*_R1.fastq.gz; do
    # 提取样本名
    sample_name=$(basename "${sample_r1}" _R1.fastq.gz)
    sample_r2="${INPUT_DIR}/${sample_name}_R2.fastq.gz"

    log_msg "处理样本: ${sample_name}"

    # 检查配对文件是否存在
    [[ -f "${sample_r2}" ]] || error_exit "找不到配对文件: ${sample_r2}"

    # 断点续跑:检查输出是否已存在
    if [[ -f "${OUTPUT_DIR}/qc/${sample_name}_clean_R1.fq.gz" ]]; then
        log_msg "  ${sample_name} 质控结果已存在,跳过"
        continue                      # 跳到下一个样本
    fi

    # 运行fastp质控
    fastp \
        -i "${sample_r1}" \
        -I "${sample_r2}" \
        -o "${OUTPUT_DIR}/qc/${sample_name}_clean_R1.fq.gz" \
        -O "${OUTPUT_DIR}/qc/${sample_name}_clean_R2.fq.gz" \
        --thread "${THREADS}" \
        --html "${OUTPUT_DIR}/qc/${sample_name}_fastp.html" \
        2>> "${LOG_FILE}"             # stderr追加到日志

    log_msg "  ${sample_name} 质控完成"
done

# while 循环:逐行读取文件
while IFS=$'\t' read -r sample_id group condition; do
    log_msg "样本: ${sample_id}, 分组: ${group}, 条件: ${condition}"
done < sample_metadata.tsv

log_msg "=== 流水线完成 ==="

变量引用规则

# ============================================================
# 变量引用的正确方式
# ============================================================

name="sample_01"

# 正确:用双引号包裹变量
echo "${name}"                        # 推荐:大括号明确变量边界
echo "$name"                          # 可以,但不如上面清晰

# 重要区别
echo "${name}_results"                # 输出: sample_01_results(正确)
echo "$name_results"                  # 错误!bash会找变量 $name_results

# 特殊变量
echo "$0"                             # 脚本名称
echo "$1"                             # 第一个参数
echo "$#"                             # 参数个数
echo "$@"                             # 所有参数(推荐)
echo "$?"                             # 上一个命令的退出码(0=成功)
echo "$$"                             # 当前进程PID

2.4 HPC/集群投递

PBS脚本模板

#!/bin/bash
#PBS -N metagenome_assembly           # 任务名称(Name)
#PBS -q batch                         # 队列名称(Queue)
#PBS -l nodes=1:ppn=16                # 节点数:每节点CPU核数
#PBS -l mem=64gb                      # 内存需求
#PBS -l walltime=48:00:00             # 最大运行时间(48小时)
#PBS -o assembly.stdout               # 标准输出文件
#PBS -e assembly.stderr               # 标准错误文件
#PBS -j oe                            # 合并stdout和stderr
#PBS -m abe                           # 邮件通知(a=abort, b=begin, e=end)
#PBS -M your_email@example.com        # 邮箱地址

# 切换到工作目录(PBS默认在家目录运行)
cd $PBS_O_WORKDIR                     # PBS_O_WORKDIR = 提交任务时的目录

# 加载环境
source activate metagenome            # 激活conda环境

# 打印信息(调试用)
echo "任务开始: $(date)"
echo "节点: $(hostname)"
echo "工作目录: $(pwd)"

# 运行分析
megahit \
    -1 clean_R1.fq.gz \
    -2 clean_R2.fq.gz \
    -o megahit_out \
    -t 16 \
    --min-contig-len 500

echo "任务完成: $(date)"
# PBS常用命令
qsub job.pbs                          # 提交任务
qstat                                 # 查看所有任务
qstat -u $USER                        # 查看自己的任务
qdel 12345                            # 删除任务(12345是Job ID)
qstat -f 12345                        # 查看任务详细信息

SLURM脚本模板

#!/bin/bash
#SBATCH --job-name=metagenome         # 任务名称
#SBATCH --partition=normal            # 分区(类似PBS的队列)
#SBATCH --nodes=1                     # 节点数
#SBATCH --ntasks-per-node=1           # 每节点任务数
#SBATCH --cpus-per-task=16            # 每任务CPU数
#SBATCH --mem=64G                     # 内存
#SBATCH --time=48:00:00               # 最大运行时间
#SBATCH --output=%j_%x.out            # %j=Job ID, %x=任务名
#SBATCH --error=%j_%x.err             # 错误输出

# SLURM自动在提交目录运行(不需要cd)

source activate metagenome

echo "任务开始: $(date)"
echo "节点: $(hostname)"
echo "SLURM_JOB_ID: ${SLURM_JOB_ID}"

megahit \
    -1 clean_R1.fq.gz \
    -2 clean_R2.fq.gz \
    -o megahit_out \
    -t ${SLURM_CPUS_PER_TASK} \
    --min-contig-len 500

echo "任务完成: $(date)"
# SLURM常用命令
sbatch job.slurm                      # 提交任务
squeue                                # 查看所有任务
squeue -u $USER                       # 查看自己的任务
scancel 12345                         # 取消任务
sinfo                                 # 查看集群分区和节点状态
sacct -j 12345 --format=JobID,Elapsed,MaxRSS  # 查看已完成任务的资源使用

PBS vs SLURM 对照表

功能 PBS/Torque SLURM
提交任务 qsub sbatch
查看任务 qstat squeue
取消任务 qdel scancel
查看节点 pbsnodes sinfo
交互式 qsub -I srun --pty bash
CPU指定 #PBS -l nodes=1:ppn=16 #SBATCH --cpus-per-task=16
内存指定 #PBS -l mem=64gb #SBATCH --mem=64G
工作目录 需要 cd $PBS_O_WORKDIR 自动在提交目录

2.5 断点续跑策略

# ============================================================
# 断点续跑:让流水线能从中断处继续
# 对长时间运行的宏基因组分析尤其重要
# ============================================================

# ---------- 策略一:检查输出文件 ----------

if [[ -f "${output_file}" && -s "${output_file}" ]]; then
    # -f : 文件存在
    # -s : 文件大小>0(非空)
    echo "输出已存在且非空,跳过"
else
    echo "开始运行..."
    run_analysis ...
fi

# ---------- 策略二:使用标记文件(.done) ----------

STEP="assembly"
DONE_FLAG="${OUTPUT_DIR}/.${STEP}.done"

if [[ -f "${DONE_FLAG}" ]]; then
    echo "步骤 ${STEP} 已完成,跳过"
else
    echo "开始 ${STEP}..."
    megahit -1 R1.fq.gz -2 R2.fq.gz -o assembly_out

    # 只有成功完成才创建标记文件
    if [[ $? -eq 0 ]]; then           # $? = 上一个命令的退出码
        touch "${DONE_FLAG}"          # 创建完成标记
        echo "步骤 ${STEP} 完成"
    else
        echo "步骤 ${STEP} 失败!" >&2
        exit 1
    fi
fi

# ---------- 策略三:完整的断点续跑框架 ----------

run_step() {
    local step_name="$1"              # 步骤名称
    local done_flag="${OUTPUT_DIR}/.${step_name}.done"
    shift                             # 移除第一个参数,剩余的是要执行的命令

    if [[ -f "${done_flag}" ]]; then
        log_msg "[跳过] ${step_name} 已完成"
        return 0
    fi

    log_msg "[开始] ${step_name}"

    # 执行传入的命令
    if "$@"; then                     # "$@" 展开为所有剩余参数
        touch "${done_flag}"
        log_msg "[完成] ${step_name}"
    else
        log_msg "[失败] ${step_name}"
        return 1
    fi
}

# 使用方式
run_step "01_qc" fastp -i R1.fq.gz -I R2.fq.gz -o clean_R1.fq.gz -O clean_R2.fq.gz
run_step "02_assembly" megahit -1 clean_R1.fq.gz -2 clean_R2.fq.gz -o assembly_out
run_step "03_binning" metabat2 -i contigs.fa -a depth.txt -o bins/bin

3. 面试常问点

★ 如何统计FASTQ文件的reads数?

参考答案:

FASTQ 文件中每条 read 占 4 行(@序列名、序列、+、质量值),所以统计行数除以 4 就是 reads 数。

# 方法一:wc -l 统计行数再除以4(最常用)
echo $(( $(wc -l < sample.fastq) / 4 ))

# 方法二:grep统计以@开头的行(注意:质量行也可能以@开头,不完全准确)
grep -c "^@SRR" sample.fastq    # 限定具体前缀更准确

# 方法三:处理压缩文件
echo $(( $(zcat sample.fastq.gz | wc -l) / 4 ))

推荐用方法一,最准确也最快。如果用 grep -c "^@" 要注意质量行可能以 @ 开头造成误计。


★ awk和sed的区别?各自擅长什么?

参考答案:

awk 擅长"列操作",特别适合处理表格数据(TSV/CSV),可以按列筛选、提取、计算,支持变量和简单编程。比如从 BLAST 结果中筛选 identity > 90% 的行:awk -F'\t' '$3>90' blast.out

sed 擅长"文本替换和编辑",用来做查找替换、删除行、插入行。比如把 FASTA 序列名中的空格后内容去掉:sed 's/ .*//' genome.fasta

简单记忆:awk 看列,sed 改行。在实际工作中两者经常配合管道一起用。


★ set -euo pipefail 每个选项什么意思?

参考答案:

这是 Shell 脚本的"严格模式",建议所有生信脚本都加上:

  • -e(errexit):任何命令返回非0退出码时,脚本立即终止。防止错误被忽略后继续执行导致更大问题。
  • -u(nounset):引用未定义的变量时报错退出。防止因为拼错变量名(如 $SAMPEL 写成 $SAMPLE)而导致空字符串被传入命令。
  • -o pipefail:管道中任何一个命令失败,整个管道的退出码就是失败的。默认情况下管道只看最后一个命令的退出码,可能掩盖中间步骤的错误。

在我写分析流水线时一定会加这行,因为生信分析步骤多、文件依赖复杂,任何一步出错都应该立即发现而不是等到最后才发现中间某步的结果是空的。


★ 如何在后台运行长时间任务?

参考答案:

有三种常用方法:

方法一:nohup

nohup bash pipeline.sh > pipeline.log 2>&1 &
nohup 使任务不受终端断开影响,& 放到后台,2>&1 将错误输出也重定向到日志。

方法二:screen/tmux(推荐)

screen -S my_analysis    # 创建一个命名会话
bash pipeline.sh         # 在里面运行
# Ctrl+A, D              # 断开会话(任务继续运行)
screen -r my_analysis    # 重新连接

方法三:集群投递(生产环境)

qsub pipeline.pbs        # PBS系统
sbatch pipeline.slurm    # SLURM系统

在百迈客实习时,短任务我用 screen,正式的分析流水线都通过集群投递系统提交。


★ 如何查看服务器资源使用情况?

参考答案:

# CPU和内存实时监控
top                       # 基本版
htop                      # 增强版(推荐,更直观)

# 磁盘空间
df -h                     # 查看各分区使用率
du -sh /data/project/*    # 查看项目占用空间

# 内存使用
free -h                   # 查看总内存和已用内存

# 当前谁在占用资源
ps aux --sort=-%mem | head -10  # 按内存排序看前10个进程

做生信分析前我会先检查磁盘空间(df -h),因为宏基因组数据很大,如果空间不够会导致分析中途失败。


★ 如何批量处理多个样本?

参考答案:

主要有三种方式:

方法一:for循环(最直观)

for sample in sample_01 sample_02 sample_03; do
    fastp -i ${sample}_R1.fq.gz -I ${sample}_R2.fq.gz \
          -o clean_${sample}_R1.fq.gz -O clean_${sample}_R2.fq.gz
done

方法二:读取样本列表文件

while read sample; do
    echo "处理: ${sample}"
    # ... 分析命令 ...
done < sample_list.txt

方法三:xargs并行(加速)

cat sample_list.txt | xargs -P 4 -I{} bash run_one_sample.sh {}
# -P 4 : 同时并行4个任务

在百迈客做项目时,我通常会写一个处理单个样本的函数,然后用 for 循环遍历样本列表,加上断点续跑的逻辑。如果样本多、集群允许,就用 xargs 并行或提交多个集群任务。


★ 管道符(|)的原理是什么?

参考答案:

管道符 | 把前一个命令的标准输出(stdout)直接连接到后一个命令的标准输入(stdin),数据像水管一样流过去,不需要中间临时文件。

cat taxa.txt | sort | uniq -c | sort -rn | head -10
# 读取 → 排序 → 去重计数 → 按数量逆序 → 取前10

管道的好处是: 1. 节省磁盘:不需要写中间文件 2. 节省时间:各命令并行执行(前一个产生数据,后一个立即处理) 3. 灵活组合:小工具组合成复杂功能(Unix哲学)

注意:管道只传递 stdout,stderr 不会进入管道。如果想把 stderr 也传进去,需要 2>&1 |


4. 易错/易混淆点

4.1 单引号 vs 双引号 vs 反引号

name="bioinfo"

echo '${name} is $name'              # 单引号:原样输出,不解析变量
# 输出: ${name} is $name

echo "${name} is great"              # 双引号:解析变量
# 输出: bioinfo is great

echo "Today is $(date)"              # $() 命令替换:执行命令并插入结果
# 输出: Today is Sun Apr 27 ...

echo "Today is `date`"               # 反引号:同 $(),但不推荐(不能嵌套)
# 输出: Today is Sun Apr 27 ...

记忆口诀:单引原样不动,双引变量替换,$()执行命令

4.2 $() vs 反引号(``)

# 推荐用 $(),原因:
# 1. 可以嵌套
result=$(echo $(date +%Y))           # 嵌套正确
# result=`echo \`date +%Y\``         # 反引号嵌套需要转义,容易出错

# 2. 更清晰易读
files=$(find . -name "*.fasta")      # 清晰
# files=`find . -name "*.fasta"`     # 视觉上和单引号混淆

4.3 > vs >> 重定向

echo "hello" > file.txt              # > 覆盖写入(文件原内容被清空!)
echo "world" >> file.txt             # >> 追加写入(在末尾添加)

# 危险场景:
echo "new header" > results.tsv      # 小心!会清空原来的分析结果!

# 安全做法:
cp results.tsv results.tsv.bak       # 先备份
echo "new header" > results.tsv      # 再覆盖

4.4 权限数字含义

# 权限 = 读(r=4) + 写(w=2) + 执行(x=1)
# 三组数字 = 所有者 + 组 + 其他人

chmod 755 script.sh
# 7 = 4+2+1 = rwx (所有者:可读可写可执行)
# 5 = 4+0+1 = r-x (组:可读可执行)
# 5 = 4+0+1 = r-x (其他人:可读可执行)

chmod 644 data.tsv
# 6 = 4+2+0 = rw- (所有者:可读可写)
# 4 = 4+0+0 = r-- (组:只读)
# 4 = 4+0+0 = r-- (其他人:只读)

# 常用权限
# 755 : 可执行脚本(所有人可运行)
# 700 : 私有可执行文件(只有自己能运行)
# 644 : 普通数据文件(所有人可读,自己可写)
# 600 : 私有文件(只有自己能读写,如.ssh/config)

4.5 kill vs kill -9

kill 12345                            # 发送 SIGTERM(信号15):请求进程优雅退出
                                      # 进程可以捕获这个信号,做清理工作后退出

kill -9 12345                         # 发送 SIGKILL(信号9):强制杀死进程
                                      # 进程无法捕获,立即终止,可能导致:
                                      # - 临时文件残留
                                      # - 数据库锁未释放
                                      # - 不完整的输出文件

# 正确做法:先 kill,等几秒,不行再 kill -9
kill 12345
sleep 5
kill -0 12345 2>/dev/null && kill -9 12345  # kill -0 检查进程是否还在

4.6 常见路径陷阱

# 文件名带空格(生信中少见但要防范)
cp my file.txt backup/               # 错误!cp 会以为是两个文件
cp "my file.txt" backup/             # 正确:用引号包裹

# 变量中的路径一定要引号
rm -rf ${DIR}/temp                    # 危险!如果DIR为空 → rm -rf /temp
rm -rf "${DIR}/temp"                  # 安全:如果DIR为空 → rm -rf "/temp"(会报错但不会误删)

# 更安全的做法
[[ -n "${DIR}" ]] && rm -rf "${DIR}/temp"  # 先检查变量非空

5. 快速参考卡片

管道组合经典用法

# 统计FASTA文件中每条序列的长度并排序
grep -v "^>" genome.fasta | awk '{print length}' | sort -rn | head -10

# 统计GFF文件中各feature类型的数量
awk -F'\t' '$3!=""{print $3}' annotation.gff | sort | uniq -c | sort -rn

# 找出占用空间最大的10个目录
du -sh ./*/ 2>/dev/null | sort -rh | head -10

# 从多个文件中提取特定列并合并
for f in *.tsv; do cut -f1,3 "$f"; done | sort -u > combined.tsv

# 批量重命名文件
ls *.fq.gz | sed 's/\(.*\)_001.fq.gz/mv & \1.fq.gz/' | bash