Skip to content

Shell 脚本编程

Shell 脚本把多条命令组合成可重复执行的自动化流程,是 AI 工程师管理训练任务、部署服务、处理数据的利器。本页涵盖 Bash 语法、重定向、退出码、实战脚本,以及数组、信号处理、参数解析和定时任务。

Linux 常用命令的速查见 Linux 基础与运维,本页侧重于把命令”编排”成脚本。

一个 Shell 脚本从解释器声明到最终退出,典型流程如下:

#!/usr/bin/env bash
# 严格模式:任何变量未定义、命令失败、管道失败都立即报错退出
set -euo pipefail
# ===== 变量 =====
MODEL_NAME="resnet50"
EPOCHS=100
DATASET_DIR="/data/imagenet"
# 变量引用必须加双引号,防止路径含空格时出错
echo "训练模型: ${MODEL_NAME}, 轮数: ${EPOCHS}"
# ===== 条件判断 =====
if [ -d "${DATASET_DIR}" ]; then
echo "数据目录存在"
elif [ -f "data.tar.gz" ]; then
echo "存在压缩包,解压中..."
tar -xzf data.tar.gz
else
echo "错误:找不到数据" >&2
exit 1
fi
# ===== 循环 =====
# 遍历多个 GPU 分别启动任务
for gpu in 0 1 2 3; do
echo "在 GPU ${gpu} 上启动任务..."
CUDA_VISIBLE_DEVICES=${gpu} python train.py --gpu ${gpu} &
done
wait # 等待所有后台任务完成
# ===== 函数 =====
check_gpu_memory() {
local required_gb=$1
# 提取第一块 GPU 的剩余显存(MB)
local free_mb
free_mb=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits | head -1)
if [ "${free_mb}" -lt $((required_gb * 1024)) ]; then
echo "显存不足:需要 ${required_gb}GB,剩余 $((free_mb / 1024))GB" >&2
return 1
fi
return 0
}
# 调用函数
check_gpu_memory 12 || exit 1
选项作用
-e (errexit)任何命令返回非零退出码,脚本立即停止
-u (nounset)引用未定义变量时报错(而非展开为空字符串)
-o pipefail管道中任一命令失败,整个管道算失败(否则只看最后一条)

💡 生产级脚本的第一行永远是 set -euo pipefail。一个未捕获的错误可能导致数据损坏或训练跑飞。

Terminal window
text="hello_world_v2"
# 字符串长度
echo ${#text} # 14
# 子串截取(从位置 0 开始取 5 个字符)
echo ${text:0:5} # hello
# 删除前缀
echo ${text#hello_} # world_v2
# 删除后缀(贪婪匹配用 %%)
echo ${text%_*} # hello_world
echo ${text%%_*} # hello
# 替换(首个/全部)
echo ${text/world/WORLD} # hello_WORLD_v2
echo ${text//_/ -} # hello -world -v2
# 默认值(变量为空或未定义时用默认值)
echo ${OUTPUT_DIR:-/tmp/output}
Terminal window
# 标准输出重定向(覆盖)
python train.py > train_output.log
# 追加重定向(不覆盖已有内容)
python train.py >> combined.log 2>&1 # 2>&1 将标准错误也合并到标准输出
# Heredoc:多行文本写入文件
cat > config.yaml << 'EOF'
model:
name: resnet50
pretrained: true
training:
epochs: 100
batch_size: 256
learning_rate: 0.01
EOF
# Heredoc 不加引号时可以做变量插值
cat > run.sh << EOF
#!/bin/bash
# 自动生成:模型=${MODEL_NAME}
CUDA_VISIBLE_DEVICES=0 python train.py --model ${MODEL_NAME}
EOF
Terminal window
# /dev/null 黑洞:丢弃输出
python train.py > /dev/null 2>&1 # 静默运行
# 同时保存到文件和终端(tee)
python train.py 2>&1 | tee train.log # 覆盖
python train.py 2>&1 | tee -a train.log # 追加
# 进程替换:把命令输出当文件传给另一命令
diff <(ls dir1) <(ls dir2) # 比较两个目录的文件列表
# here-string:把字符串作为标准输入
grep "error" <<< "$LOG_TEXT"

每条命令执行后返回一个 0-255 的退出码(exit code),0 表示成功,非 0 表示失败。在脚本和 CI 流水线中,退出码是判断成功与否的唯一依据。

Terminal window
python preprocess.py
if [ $? -ne 0 ]; then
echo "预处理失败,终止流水线" >&2
exit 1
fi

💡 使用 set -e 后,任何命令返回非零退出码都会让脚本自动退出,无需手动检查 $?。set -u 拦截未定义变量,set -o pipefail 让管道中任一环节失败也算整体失败。三者组合 set -euo pipefail 是生产级脚本的标配。

退出码含义
0成功
1通用错误
2命令行参数错误(shell 内建命令约定)
126命令不可执行(无权限)
127命令未找到
128+N被信号 N 杀死(如 130 = SIGINT/Ctrl+C)
Terminal window
# 声明数组
gpus=(0 1 2 3)
datasets=("imagenet" "coco" "voc")
# 访问元素
echo ${gpus[0]} # 0
echo ${gpus[@]} # 所有元素:0 1 2 3
echo ${#gpus[@]} # 元素个数:4
# 追加元素
gpus+=(4 5)
# 遍历
for gpu in "${gpus[@]}"; do
echo "使用 GPU ${gpu}"
done
# 切片(取第 1-2 个元素)
echo ${datasets[@]:0:2} # imagenet coco

关联数组相当于字典/哈希表,需要 Bash 4.0+:

Terminal window
declare -A model_paths
# 赋值
model_paths["resnet50"]="/models/resnet50_v2.pt"
model_paths["vit"]="/models/vit_base.pt"
model_paths["yolo"]="/models/yolov8n.pt"
# 访问
echo ${model_paths["resnet50"]}
# 遍历所有 key
for model in "${!model_paths[@]}"; do
echo "${model} -> ${model_paths[$model]}"
done
# 元素个数
echo ${#model_paths[@]}

trap 命令在脚本收到信号时执行指定的清理操作。训练脚本用它来确保收到 Ctrl+C / kill 时能保存 checkpoint。

#!/usr/bin/env bash
set -euo pipefail
CLEANUP_DONE=false
cleanup() {
if [ "$CLEANUP_DONE" = true ]; then return; fi
CLEANUP_DONE=true
echo ""
echo "收到终止信号,正在清理..."
# 保存 checkpoint、删除临时文件、通知监控系统...
rm -f /tmp/train_lock_$$ # $$ 是当前进程 PID
echo "清理完成,退出"
exit 0
}
# 捕获 SIGINT(Ctrl+C)、SIGTERM(kill 默认信号)、EXIT(脚本正常退出)
trap cleanup SIGINT SIGTERM EXIT
echo "训练开始..."
while true; do
echo "训练中..."
sleep 2
done

💡 trap '...' EXIT 在脚本以任何方式退出(正常结束、被 kill、set -e 触发)时都会执行,是最可靠的清理机制。

#!/usr/bin/env bash
set -euo pipefail
# 默认参数
MODEL="resnet50"
EPOCHS=100
LR=0.001
GPU=0
usage() {
cat << EOF
用法: $0 [-m MODEL] [-e EPOCHS] [-l LR] [-g GPU] [-h]
-m MODEL 模型名称(默认: resnet50)
-e EPOCHS 训练轮数(默认: 100)
-l LR 学习率(默认: 0.001)
-g GPU GPU 编号(默认: 0)
-h 显示帮助
EOF
exit 0
}
# getopts 只支持短选项(-m),不支持长选项(--model)
while getopts ":m:e:l:g:h" opt; do
case ${opt} in
m ) MODEL=$OPTARG ;;
e ) EPOCHS=$OPTARG ;;
l ) LR=$OPTARG ;;
g ) GPU=$OPTARG ;;
h ) usage ;;
\? ) echo "无效选项: -$OPTARG" >&2; usage; exit 1 ;;
: ) echo "选项 -$OPTARG 需要参数" >&2; exit 1 ;;
esac
done
echo "模型=${MODEL}, 轮数=${EPOCHS}, 学习率=${LR}, GPU=${GPU}"
# 使用: ./train.sh -m vit -e 50 -g 1

如果需要 --model vit 这种长选项,可以用手动解析:

Terminal window
while [[ $# -gt 0 ]]; do
case $1 in
--model) MODEL="$2"; shift 2 ;;
--epochs) EPOCHS="$2"; shift 2 ;;
--lr) LR="$2"; shift 2 ;;
--gpu) GPU="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "未知参数: $1" >&2; exit 1 ;;
esac
done

cron 是 Linux 内置的定时任务调度器,适合定期清理磁盘、检查 GPU 状态、备份数据等。

Terminal window
# 编辑当前用户的 crontab
crontab -e
# 查看当前用户的定时任务
crontab -l
# 分钟 小时 日 月 星期 命令
# ─────────────────────────────
*/5 * * * * 命令 # 每 5 分钟
0 2 * * * 命令 # 每天凌晨 2:00
0 */6 * * * 命令 # 每 6 小时
30 1 * * 1 命令 # 每周一凌晨 1:30
0 0 1 * * 命令 # 每月 1 号 0:00
Terminal window
# === crontab 内容 ===
# 每天凌晨 3 点清理 7 天前的日志
0 3 * * * find /data/logs -name "*.log" -mtime +7 -delete
# 每 10 分钟检查 GPU 状态并记录
*/10 * * * * nvidia-smi --query-gpu=index,memory.used,utilization.gpu --format=csv,noheader >> /var/log/gpu_cron.log 2>&1
# 每周日凌晨 2 点备份模型目录
0 2 * * 0 tar -czf /backup/models_$(date +\%Y\%m\%d).tar.gz /models >> /var/log/backup.log 2>&1

⚠️ cron 环境变量极简(没有 PATH、没有 conda activate),命令中要用绝对路径,或在脚本开头手动设置环境。% 在 crontab 中需要转义为 \%。

#!/usr/bin/env bash
# gpu_monitor.sh —— 持续监控 GPU 并记录日志,显存超阈值时告警
set -euo pipefail
LOG_FILE="/var/log/gpu_monitor.log"
THRESHOLD_GB=80 # 显存告警阈值(GB)
INTERVAL=60 # 监控间隔(秒)
echo "GPU 监控已启动,间隔 ${INTERVAL}s,阈值 ${THRESHOLD_GB}GB" | tee -a "${LOG_FILE}"
while true; do
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 逐行读取每块 GPU 的信息
nvidia-smi --query-gpu=index,name,memory.used,memory.total,utilization.gpu \
--format=csv,noheader,nounits | while IFS=',' read -r idx name used total util; do
# 去除多余空格
idx=$(echo "$idx" | xargs)
used=$(echo "$used" | xargs)
total=$(echo "$total" | xargs)
# 转换为 GB
used_gb=$((used / 1024))
total_gb=$((total / 1024))
echo "[${timestamp}] GPU ${idx} (${name}): ${used_gb}GB / ${total_gb}GB, 利用率 ${util}%" \
| tee -a "${LOG_FILE}"
# 显存超阈值告警
if [ "${used_gb}" -gt "${THRESHOLD_GB}" ]; then
echo "[${timestamp}] ⚠️ 告警:GPU ${idx} 显存使用 ${used_gb}GB 超过阈值 ${THRESHOLD_GB}GB" \
| tee -a "${LOG_FILE}"
fi
done
sleep "${INTERVAL}"
done

保存为 gpu_monitor.sh,用 chmod +x gpu_monitor.sh 赋予执行权限后即可后台运行:nohup ./gpu_monitor.sh &。

#!/usr/bin/env bash
# batch_train.sh —— 在多块 GPU 上并行调度多个模型训练
set -euo pipefail
# 模型列表
declare -a MODELS=("resnet50" "vit_base" "convnext" "efficientnet")
NUM_GPUS=4
echo "开始批量训练:${#MODELS[@]} 个模型,${NUM_GPUS} 块 GPU"
# 每块 GPU 分配一个模型,GPU 满了就等待
running=0
for model in "${MODELS[@]}"; do
gpu=$((running % NUM_GPUS))
echo "[$(date +%H:%M:%S)] 启动 ${model} 在 GPU ${gpu}"
CUDA_VISIBLE_DEVICES=${gpu} python train.py --model "${model}" \
--output "/data/results/${model}" > "logs/${model}.log" 2>&1 &
((running++))
# GPU 全部占用时,等待至少一个任务完成
if (( running % NUM_GPUS == 0 )); then
wait -n # 等待任意一个后台任务完成(Bash 4.3+)
fi
done
wait # 等待剩余任务
echo "[$(date +%H:%M:%S)] 所有训练完成"
术语英文解释
严格模式strict modeset -euo pipefail,出错即停、未定义变量报错、管道失败传播
退出码exit code命令执行结果的 0-255 数值,0=成功
关联数组associative arrayBash 4.0+ 的键值对数组,类似 Python dict
信号signal操作系统发给进程的异步通知(SIGINT、SIGTERM 等)
陷阱trap捕获信号并在收到时执行清理命令
定时任务cron jobcrontab 定义的周期性自动执行任务