Shell 脚本高级特性完全指南
信号与信号捕捉工具 trap
信号概述
信号是操作系统与运行程序通信的机制,用于响应特定事件:
- 用户按下
Ctrl+C 时发送 SIGINT 信号
kill 命令默认发送 SIGTERM 信号
- 系统异常时自动发送特定信号
信号列表 (64个信号)
传统信号 (1-31号)
| 编号 |
信号名 |
描述 |
| 1 |
SIGHUP |
终端挂起或控制进程终止 |
| 2 |
SIGINT |
中断进程(通常由 Ctrl+C 产生) |
| 3 |
SIGQUIT |
退出进程并生成核心转储文件(通常由 Ctrl+\ 产生) |
| 4 |
SIGILL |
非法指令 |
| 5 |
SIGTRAP |
跟踪/断点陷阱 |
| 6 |
SIGABRT |
由 abort(3) 产生的信号 |
| 7 |
SIGBUS |
总线错误(非法的内存地址) |
| 8 |
SIGFPE |
算术异常(如除零错误) |
| 9 |
SIGKILL |
立即杀死进程(不能被捕获、阻塞或忽略) |
| 10 |
SIGUSR1 |
用户定义的信号 1 |
| 11 |
SIGSEGV |
段错误(非法的内存访问) |
| 12 |
SIGUSR2 |
用户定义的信号 2 |
| 13 |
SIGPIPE |
管道破裂(向没有读端的管道写数据) |
| 14 |
SIGALRM |
闹钟信号(由 alarm(2) 产生的信号) |
| 15 |
SIGTERM |
终止进程(请求进程终止) |
| 16 |
SIGSTKFLT |
协处理器栈错误 |
| 17 |
SIGCHLD |
子进程状态改变(子进程停止或退出) |
| 18 |
SIGCONT |
继续执行被停止的进程 |
| 19 |
SIGSTOP |
停止进程的执行(不能被捕获、阻塞或忽略) |
| 20 |
SIGTSTP |
停止进程的执行(通常由 Ctrl+Z 产生) |
| 21 |
SIGTTIN |
后台进程试图从控制终端读取 |
| 22 |
SIGTTOU |
后台进程试图向控制终端写入 |
| 23 |
SIGURG |
套接字上有紧急数据可读 |
| 24 |
SIGXCPU |
CPU 时间限制超时 |
| 25 |
SIGXFSZ |
文件大小限制超时 |
| 26 |
SIGVTALRM |
虚拟定时器信号 |
| 27 |
SIGPROF |
剖析定时器信号 |
| 28 |
SIGWINCH |
窗口大小改变 |
| 29 |
SIGIO |
I/O 可能事件 |
| 30 |
SIGPWR |
电源故障(重启) |
| 31 |
SIGSYS |
非法的系统调用(SVID 扩展) |
实时信号 (34-64号)
| 编号 |
信号名 |
描述 |
| 34 |
SIGRTMIN |
实时信号最小值(用户自定义) |
| 35 |
SIGRTMIN+1 |
用户自定义实时信号 |
| 36 |
SIGRTMIN+2 |
用户自定义实时信号 |
| 37 |
SIGRTMIN+3 |
用户自定义实时信号 |
| 38 |
SIGRTMIN+4 |
用户自定义实时信号 |
| 39 |
SIGRTMIN+5 |
用户自定义实时信号 |
| 40 |
SIGRTMIN+6 |
用户自定义实时信号 |
| 41 |
SIGRTMIN+7 |
用户自定义实时信号 |
| 42 |
SIGRTMIN+8 |
用户自定义实时信号 |
| 43 |
SIGRTMIN+9 |
用户自定义实时信号 |
| 44 |
SIGRTMIN+10 |
用户自定义实时信号 |
| 45 |
SIGRTMIN+11 |
用户自定义实时信号 |
| 46 |
SIGRTMIN+12 |
用户自定义实时信号 |
| 47 |
SIGRTMIN+13 |
用户自定义实时信号 |
| 48 |
SIGRTMIN+14 |
用户自定义实时信号 |
| 49 |
SIGRTMIN+15 |
用户自定义实时信号 |
| 50 |
SIGRTMAX-14 |
用户自定义实时信号 |
| 51 |
SIGRTMAX-13 |
用户自定义实时信号 |
| 52 |
SIGRTMAX-12 |
用户自定义实时信号 |
| 53 |
SIGRTMAX-11 |
用户自定义实时信号 |
| 54 |
SIGRTMAX-10 |
用户自定义实时信号 |
| 55 |
SIGRTMAX-9 |
用户自定义实时信号 |
| 56 |
SIGRTMAX-8 |
用户自定义实时信号 |
| 57 |
SIGRTMAX-7 |
用户自定义实时信号 |
| 58 |
SIGRTMAX-6 |
用户自定义实时信号 |
| 59 |
SIGRTMAX-5 |
用户自定义实时信号 |
| 60 |
SIGRTMAX-4 |
用户自定义实时信号 |
| 61 |
SIGRTMAX-3 |
用户自定义实时信号 |
| 62 |
SIGRTMAX-2 |
用户自定义实时信号 |
| 63 |
SIGRTMAX-1 |
用户自定义实时信号 |
| 64 |
SIGRTMAX |
实时信号最大值(用户自定义) |
重要提示:
- SIGKILL (9) 和 SIGSTOP (19) 不能被捕获、阻塞或忽略
- 实时信号(34-64)没有预定义含义,用于应用程序自定义
- 信号 32 和 33 为系统保留,未列出
- 使用
kill -L 命令可查看当前系统的完整信号列表
trap 基本功能
trap 是 Shell 脚本中用于捕获和处理信号的强大工具:
- 捕获特定信号并执行指定操作
- 实现优雅退出和资源清理
- 忽略特定信号防止意外中断
- 记录错误日志和诊断信息
基本语法
trap command signal [signal ...]
| 参数 |
说明 |
command |
信号发生时执行的命令或函数 |
signal |
信号名(大小写不敏感,SIG前缀可选)或信号数值 |
'' |
忽略信号(空字符串) |
- |
恢复信号的默认行为 |
常用信号处理场景
| 信号 |
典型场景 |
默认行为 |
| SIGINT |
用户按下 Ctrl+C 中断脚本 |
终止进程 |
| SIGTERM |
系统请求进程终止 |
终止进程 |
| SIGHUP |
终端断开连接 |
终止进程 |
| SIGQUIT |
用户按下 Ctrl+\ 退出 |
终止进程并生成核心转储 |
| SIGTSTP |
用户按下 Ctrl+Z 暂停 |
暂停进程 |
| SIGCONT |
继续被暂停的进程 |
继续进程 |
trap 使用示例
示例 1:捕获 SIGINT 执行清理
#!/bin/bash
cleanup() {
echo "脚本被中断,正在执行清理操作..."
# 清理资源:关闭文件、删除临时文件等
rm -f /tmp/tempfile
exit 1
}
# 捕获 SIGINT 信号并执行 cleanup 函数
trap cleanup SIGINT
echo "脚本正在运行,按 Ctrl+C 测试信号捕获..."
while true; do
sleep 1
done
示例 2:捕获多个信号
#!/bin/bash
handle_signals() {
case $1 in
SIGINT)
echo "接收到中断信号,正在退出..."
;;
SIGTERM)
echo "接收到终止信号,正在退出..."
;;
*)
echo "接收到未知信号: $1"
;;
esac
# 执行清理操作
exit 1
}
# 捕获多个信号
trap 'handle_signals SIGINT' SIGINT
trap 'handle_signals SIGTERM' SIGTERM
echo "脚本正在运行,等待信号(可发送 SIGINT 或 SIGTERM 测试)..."
while true; do
sleep 1
done
示例 3:忽略特定信号
#!/bin/bash
# 忽略 CTRL+C 信号 (SIGINT)
trap '' INT
echo "已忽略 Ctrl+C 中断信号,脚本将无法通过 Ctrl+C 终止"
echo "测试:尝试按下 Ctrl+C..."
sleep 5
echo "测试完成"
示例 4:恢复默认行为
#!/bin/bash
# 初始忽略 SIGINT
trap '' SIGINT
echo "当前忽略 Ctrl+C"
# 5秒后恢复默认行为
sleep 5
trap - SIGINT
echo "已恢复 Ctrl+C 的默认行为"
echo "现在可以按 Ctrl+C 终止脚本..."
while true; do
sleep 1
done
mktemp(临时文件创建工具)
基本语法
mktemp [OPTION]... [TEMPLATE]
TEMPLATE:必须包含至少 6 个连续 X (如 tmp.XXXXXX)
常用选项
| 选项 |
说明 |
-d |
创建临时目录 |
-u |
仅打印名称(不安全) |
-q |
失败时不输出诊断信息 |
--suffix=SUFF |
添加后缀 |
-p DIR |
指定父目录 |
使用示例
# 创建临时文件
mktemp tmp.XXXXXX
# 创建临时目录
mktemp -d tmpdir.XXXXXX
# 指定父目录
mktemp -p /custom/path tmp.XXXXXX
# 添加后缀
mktemp --suffix=.log tmpXXXXXX
# 预览模式
mktemp -u tmp.XXXXXX
注意事项
- 默认权限:仅所有者可访问 (600)
- 需手动删除临时文件
- 避免使用
-u 防止竞态条件
install 命令
基本语法
install [OPTION]... SOURCE DEST
install [OPTION]... SOURCE... DIRECTORY
install [OPTION]... -d DIRECTORY...
常用选项
| 选项 |
说明 |
-d |
创建目录 |
-m MODE |
设置权限 |
-o OWNER |
设置所有者 (需 root) |
-g GROUP |
设置所属组 |
-p |
保留时间戳 |
-s |
删除符号表 |
-t DIR |
指定目标目录 |
使用示例
# 复制文件并设置权限
install -m 0755 script.sh /usr/local/bin/
# 复制文件并设置所有者
install -o user -g group file.txt /target/
# 创建目录
install -d /path/to/newdir
# 复制多个文件
install file1 file2 /target/directory/
# 保留时间戳
install -p source dest
注意事项
- 需要足够权限修改目标位置
- 默认覆盖已存在文件(使用
--backup 备份)
- 组合选项实现复杂操作
数组操作
普通数组
# 声明与赋值
arr[0]="A"
arr[1]="B"
arr=("A" "B" "C")
# 访问元素
echo ${arr[1]} # 输出 "B"
# 遍历元素
for item in "${arr[@]}"; do
echo $item
done
# 数组长度
echo ${#arr[@]} # 输出 3
# 删除元素
unset arr[1] # 删除索引1的元素
# 删除整个数组
unset arr
关联数组
# 声明与赋值
declare -A assoc_arr
assoc_arr["key1"]="value1"
assoc_arr["key2"]="value2"
# 访问元素
echo ${assoc_arr["key1"]} # 输出 "value1"
# 遍历键值对
for key in "${!assoc_arr[@]}"; do
echo "$key: ${assoc_arr[$key]}"
done
# 检查键存在
if [[ -v assoc_arr["key1"] ]]; then
echo "键存在"
fi
# 删除操作
unset assoc_arr["key1"] # 删除单个元素
unset assoc_arr # 删除整个数组
字符串切片
常用方法
# ${string:start:length} 语法
str="Hello, World!"
echo ${str:7:5} # 输出 "World"
# ${string:start} 语法
str="Hello, World!"
echo ${str:7} # 输出 "World!"
# ${string: -length} 语法
str="Hello, World!"
echo ${str: -6} # 输出 "World!"
# 使用 cut 命令
echo "Hello, World!" | cut -c 8-12 # 输出 "World"
# 模式匹配
str="Hello123World"
num=${str##*[!0-9]*}
num=${num%%*[!0-9]*}
echo $num # 输出 "123"
变量的高级用法
变量配置方式表
| 配置方式 |
未配置 |
空字符串 |
已配置非空 |
var=${str-expr} |
var=expr |
var=“” |
var=$str |
var=${str:-expr} |
var=expr |
var=expr |
var=$str |
var=${str+expr} |
var=“” |
var=“” |
var=expr |
var=${str:+expr} |
var=“” |
var=“” |
var=expr |
var=${str=expr} |
str=expr, var=expr |
str=“”, var=“” |
var=$str |
var=${str:=expr} |
str=expr, var=expr |
str=expr, var=expr |
var=$str |
var=${str?expr} |
报错退出 |
通常不触发 |
var=$str |
var=${str:?expr} |
报错退出 |
报错退出 |
var=$str |
注意:
- "保持未赋值状态"可能意味着赋值为空字符串
var=${str=expr}在str为空时的行为不常见,应避免使用
- 具体行为可能因Shell版本而异
变量的间接引用
引用方法
# ${!var} 语法 (Bash 4.3+)
var_name="my_var"
my_var="Hello"
echo ${!var_name} # 输出 "Hello"
# eval 命令
var_name="my_var"
my_var="Hello"
eval temp_var=\$$var_name
echo $temp_var # 输出 "Hello"
注意事项
- ${!var}:更安全简洁(推荐)
- eval:兼容性好但需防注入攻击
- 避免处理用户输入时使用间接引用
IFS(Internal Field Separator)
基本概念
# 默认值:空格、制表符、换行符
echo "默认IFS: '$IFS'"
# 修改IFS
OLDIFS=$IFS
IFS=','
# 恢复IFS
IFS=$OLDIFS
使用示例
#!/bin/bash
# 处理CSV数据
data="John,30,New York"
# 保存原始IFS
OLDIFS=$IFS
IFS=','
# 读取字段
read -r name age city <<< "$data"
echo "Name: $name"
echo "Age: $age"
echo "City: $city"
# 恢复IFS
IFS=$OLDIFS
注意事项
- 修改前保存原始值
- 子shell修改不影响父shell
- 特殊字符需正确处理
- 影响命令:read, for循环, 参数扩展等