引言
在操作系统实验中,Shell脚本扮演着至关重要的角色。无论是自动化系统管理任务、批量处理文件,还是实现复杂的系统监控,Shell脚本都是系统管理员和开发者的得力助手。然而,编写高效、健壮的Shell脚本并非易事,调试过程也常常充满挑战。本文将深入探讨Shell脚本的编写技巧和调试方法,帮助你在操作系统实验中更加得心应手。
一、Shell脚本编写基础
1.1 脚本结构与规范
一个良好的Shell脚本应该具备清晰的结构和规范的格式。以下是一个典型的Shell脚本结构:
#!/bin/bash
# 脚本名称: system_monitor.sh
# 功能描述: 监控系统资源使用情况
# 作者: 张三
# 日期: 2023-10-15
# 版本: 1.0
# 设置脚本选项
set -euo pipefail # 严格模式:遇到错误立即退出,未定义变量报错,管道命令失败则退出
# 定义变量
LOG_FILE="/var/log/system_monitor.log"
THRESHOLD_CPU=80
THRESHOLD_MEM=80
# 函数定义
check_cpu_usage() {
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
if (( $(echo "$cpu_usage > $THRESHOLD_CPU" | bc -l) )); then
echo "警告: CPU使用率过高: ${cpu_usage}%" | tee -a "$LOG_FILE"
return 1
fi
return 0
}
check_memory_usage() {
local mem_usage=$(free | grep Mem | awk '{printf "%.2f", $3/$2 * 100}')
if (( $(echo "$mem_usage > $THRESHOLD_MEM" | bc -l) )); then
echo "警告: 内存使用率过高: ${mem_usage}%" | tee -a "$LOG_FILE"
return 1
fi
return 0
}
# 主程序
main() {
echo "开始系统监控 $(date)" | tee -a "$LOG_FILE"
if check_cpu_usage && check_memory_usage; then
echo "系统状态正常" | tee -a "$LOG_FILE"
else
echo "系统状态异常,请检查" | tee -a "$LOG_FILE"
fi
echo "监控结束 $(date)" | tee -a "$LOG_FILE"
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
关键点说明:
- Shebang行:
#!/bin/bash指定使用Bash解释器 - 注释规范:包含脚本名称、功能、作者、日期和版本信息
- 严格模式:
set -euo pipefail提高脚本健壮性 - 变量定义:使用大写字母表示常量,小写字母表示局部变量
- 函数封装:将功能模块化,提高代码复用性
- 主函数入口:使用
main()函数组织主逻辑 - 执行检查:
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]确保脚本直接执行时才运行
1.2 变量与参数处理
1.2.1 变量类型与作用域
#!/bin/bash
# 全局变量(在函数外定义)
GLOBAL_VAR="全局变量"
# 函数内的局部变量
function test_variables() {
# 局部变量(使用local关键字)
local local_var="局部变量"
# 全局变量在函数内可访问
echo "在函数内访问全局变量: $GLOBAL_VAR"
# 修改全局变量
GLOBAL_VAR="修改后的全局变量"
# 局部变量只在函数内有效
echo "局部变量: $local_var"
}
# 测试变量作用域
echo "调用函数前全局变量: $GLOBAL_VAR"
test_variables
echo "调用函数后全局变量: $GLOBAL_VAR"
1.2.2 参数传递与处理
#!/bin/bash
# 处理命令行参数
process_arguments() {
# 显示所有参数
echo "所有参数: $@"
echo "参数个数: $#"
# 遍历参数
for arg in "$@"; do
echo "参数: $arg"
done
# 特殊参数
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "最后一个参数: ${@: -1}"
# 带空格的参数处理
if [[ "$1" == "--file" ]]; then
local file_path="$2"
echo "文件路径: $file_path"
fi
}
# 使用getopts处理选项参数
process_options() {
local opt
local file=""
local verbose=false
while getopts "f:v" opt; do
case $opt in
f)
file="$OPTARG"
;;
v)
verbose=true
;;
\?)
echo "无效选项: -$OPTARG" >&2
exit 1
;;
:)
echo "选项 -$OPTARG 需要参数" >&2
exit 1
;;
esac
done
echo "文件: $file"
echo "详细模式: $verbose"
}
# 主程序
main() {
echo "=== 参数处理演示 ==="
process_arguments "$@"
echo -e "\n=== 选项处理演示 ==="
process_options "$@"
}
# 执行
main "$@"
1.3 条件判断与循环
1.3.1 条件判断
#!/bin/bash
# 基本条件判断
basic_conditions() {
local num1=10
local num2=20
# 数值比较
if (( num1 > num2 )); then
echo "$num1 > $num2"
elif (( num1 == num2 )); then
echo "$num1 == $num2"
else
echo "$num1 < $num2"
fi
# 字符串比较
local str1="hello"
local str2="world"
if [[ "$str1" == "$str2" ]]; then
echo "字符串相等"
else
echo "字符串不相等"
fi
# 文件判断
local file="/etc/passwd"
if [[ -f "$file" ]]; then
echo "$file 是普通文件"
fi
if [[ -r "$file" ]]; then
echo "$file 可读"
fi
if [[ -w "$file" ]]; then
echo "$file 可写"
fi
if [[ -x "$file" ]]; then
echo "$file 可执行"
fi
if [[ -d "/etc" ]]; then
echo "/etc 是目录"
fi
if [[ -s "$file" ]]; then
echo "$file 非空"
fi
if [[ -e "$file" ]]; then
echo "$file 存在"
fi
}
# 复杂条件判断
complex_conditions() {
local user="root"
local process="sshd"
# 使用test命令
if test "$user" = "root" && test "$process" = "sshd"; then
echo "用户是root且进程是sshd"
fi
# 使用[[]]支持正则表达式
local email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "邮箱格式正确"
else
echo "邮箱格式错误"
fi
# 使用case语句
local status="running"
case "$status" in
"running")
echo "进程正在运行"
;;
"stopped")
echo "进程已停止"
;;
"failed")
echo "进程失败"
;;
*)
echo "未知状态"
;;
esac
}
# 主程序
main() {
echo "=== 基本条件判断 ==="
basic_conditions
echo -e "\n=== 复杂条件判断 ==="
complex_conditions
}
main
1.3.2 循环结构
#!/bin/bash
# for循环
for_loops() {
echo "=== for循环 ==="
# 基本for循环
for i in {1..5}; do
echo "数字: $i"
done
# 遍历数组
local fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 遍历文件
for file in /etc/*.conf; do
if [[ -f "$file" ]]; then
echo "配置文件: $file"
fi
done
# C风格for循环
for ((i=0; i<5; i++)); do
echo "C风格循环: $i"
done
}
# while循环
while_loops() {
echo -e "\n=== while循环 ==="
# 基本while循环
local count=0
while (( count < 5 )); do
echo "计数: $count"
((count++))
done
# 文件读取
local file="/etc/hosts"
if [[ -f "$file" ]]; then
echo "读取文件 $file:"
while IFS= read -r line; do
echo " $line"
done < "$file"
fi
# 无限循环
local counter=0
while true; do
echo "无限循环: $counter"
((counter++))
if (( counter >= 3 )); then
break
fi
done
}
# until循环
until_loops() {
echo -e "\n=== until循环 ==="
local count=0
until (( count >= 5 )); do
echo "until循环: $count"
((count++))
done
}
# select循环(交互式菜单)
select_loop() {
echo -e "\n=== select循环(交互式菜单) ==="
PS3="请选择操作: "
options=("查看系统信息" "检查磁盘使用" "退出")
select opt in "${options[@]}"; do
case $opt in
"查看系统信息")
echo "系统信息:"
uname -a
;;
"检查磁盘使用")
echo "磁盘使用:"
df -h
;;
"退出")
echo "退出程序"
break
;;
*)
echo "无效选项: $REPLY"
;;
esac
done
}
# 主程序
main() {
for_loops
while_loops
until_loops
select_loop
}
main
二、高级Shell脚本技巧
2.1 数组与关联数组
#!/bin/bash
# 数组操作
array_operations() {
echo "=== 数组操作 ==="
# 创建数组
local arr=("value1" "value2" "value3")
# 访问元素
echo "第一个元素: ${arr[0]}"
echo "所有元素: ${arr[@]}"
echo "数组长度: ${#arr[@]}"
# 添加元素
arr+=("value4" "value5")
# 删除元素
unset arr[1]
# 遍历数组
for i in "${!arr[@]}"; do
echo "索引 $i: ${arr[$i]}"
done
# 数组切片
echo "切片: ${arr[@]:1:2}"
}
# 关联数组(Bash 4.0+)
associative_arrays() {
echo -e "\n=== 关联数组 ==="
# 声明关联数组
declare -A user_info
# 添加键值对
user_info["name"]="张三"
user_info["age"]="25"
user_info["department"]="技术部"
# 访问值
echo "姓名: ${user_info["name"]}"
echo "年龄: ${user_info["age"]}"
# 遍历关联数组
for key in "${!user_info[@]}"; do
echo "$key: ${user_info[$key]}"
done
# 检查键是否存在
if [[ -v user_info["name"] ]]; then
echo "键 'name' 存在"
fi
}
# 多维数组模拟
multidimensional_arrays() {
echo -e "\n=== 多维数组模拟 ==="
# 使用关联数组模拟二维数组
declare -A matrix
# 设置值
matrix["1,1"]=10
matrix["1,2"]=20
matrix["2,1"]=30
matrix["2,2"]=40
# 读取值
echo "矩阵 [1,1]: ${matrix["1,1"]}"
echo "矩阵 [2,2]: ${matrix["2,2"]}"
# 遍历矩阵
for key in "${!matrix[@]}"; do
echo "位置 $key: ${matrix[$key]}"
done
}
# 主程序
main() {
array_operations
associative_arrays
multidimensional_arrays
}
main
2.2 字符串处理
#!/bin/bash
# 字符串操作
string_operations() {
echo "=== 字符串操作 ==="
local str="Hello, World!"
# 基本操作
echo "原始字符串: $str"
echo "长度: ${#str}"
echo "子串: ${str:7:5}" # 从索引7开始,取5个字符
echo "替换: ${str/World/Linux}"
echo "全局替换: ${str//o/O}"
echo "删除前缀: ${str#Hello, }"
echo "删除后缀: ${str% World!}"
# 大小写转换
echo "大写: ${str^^}"
echo "小写: ${str,,}"
# 字符串拼接
local str1="Hello"
local str2="Linux"
echo "拼接: ${str1}${str2}"
# 字符串比较
if [[ "$str1" < "$str2" ]]; then
echo "$str1 在字典序上小于 $str2"
fi
# 正则表达式匹配
local email="user@example.com"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "邮箱格式正确"
fi
# 提取匹配内容
local url="https://www.example.com/path"
if [[ "$url" =~ ^https?://([^/]+) ]]; then
echo "域名: ${BASH_REMATCH[1]}"
fi
}
# 高级字符串处理
advanced_string_processing() {
echo -e "\n=== 高级字符串处理 ==="
# 使用awk处理字符串
local text="Name: John, Age: 30, City: New York"
echo "原始文本: $text"
# 提取特定字段
local name=$(echo "$text" | awk -F': ' '{print $2}' | awk -F', ' '{print $1}')
echo "姓名: $name"
# 使用sed替换
local modified=$(echo "$text" | sed 's/New York/NYC/')
echo "修改后: $modified"
# 使用tr转换字符
local upper=$(echo "$text" | tr '[:lower:]' '[:upper:]')
echo "大写: $upper"
# 使用cut提取字段
local age=$(echo "$text" | cut -d',' -f2 | cut -d':' -f2 | tr -d ' ')
echo "年龄: $age"
}
# 主程序
main() {
string_operations
advanced_string_processing
}
main
2.3 函数与模块化
#!/bin/bash
# 函数定义与调用
function_definitions() {
echo "=== 函数定义与调用 ==="
# 基本函数
greet() {
local name="$1"
echo "你好, $name!"
}
greet "张三"
# 返回值处理
calculate_sum() {
local a=$1
local b=$2
local sum=$((a + b))
echo "$sum"
}
local result=$(calculate_sum 5 3)
echo "5 + 3 = $result"
# 函数返回状态码
check_file() {
local file="$1"
if [[ -f "$file" ]]; then
return 0 # 成功
else
return 1 # 失败
fi
}
if check_file "/etc/passwd"; then
echo "文件存在"
else
echo "文件不存在"
fi
}
# 模块化编程
modular_programming() {
echo -e "\n=== 模块化编程 ==="
# 导入其他脚本
source "./utils.sh" 2>/dev/null || {
echo "警告: utils.sh 不存在,创建示例函数"
# 创建示例工具函数
log_message() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message"
}
validate_input() {
local input="$1"
if [[ -z "$input" ]]; then
echo "输入不能为空" >&2
return 1
fi
return 0
}
}
# 使用工具函数
log_message "INFO" "开始处理数据"
local user_input=""
while true; do
read -p "请输入数据: " user_input
if validate_input "$user_input"; then
echo "输入有效: $user_input"
break
fi
done
log_message "INFO" "处理完成"
}
# 主程序
main() {
function_definitions
modular_programming
}
main
三、Shell脚本调试技巧
3.1 基础调试方法
3.1.1 使用set -x和set +x
#!/bin/bash
# 调试示例脚本
debug_example() {
echo "=== 调试示例 ==="
# 开启调试模式
set -x
local var1="value1"
local var2="value2"
# 这些命令的执行过程会被打印出来
local result=$(echo "$var1 $var2" | tr ' ' '_')
echo "结果: $result"
# 关闭调试模式
set +x
echo "调试模式已关闭"
}
# 主程序
main() {
debug_example
}
main
输出示例:
=== 调试示例 ===
+ local var1=value1
+ local var2=value2
+ local 'result=value1_value2'
+ echo '结果: value1_value2'
结果: value1_value2
+ set +x
调试模式已关闭
3.1.2 使用-v选项
#!/bin/bash
# 使用-v选项查看脚本内容
# 执行: bash -v script.sh
echo "这是一个带-v选项的示例"
echo "每行命令都会先被打印,然后执行"
3.2 高级调试技巧
3.2.1 使用trap捕获错误
#!/bin/bash
# 错误处理与调试
error_handling() {
echo "=== 错误处理与调试 ==="
# 设置错误处理函数
handle_error() {
local line_number=$1
local command=$2
local exit_code=$3
echo "错误发生在第 $line_number 行"
echo "命令: $command"
echo "退出码: $exit_code"
# 记录错误日志
echo "$(date): 错误 - 行 $line_number, 命令: $command, 退出码: $exit_code" >> /tmp/script_errors.log
}
# 捕获错误
trap 'handle_error ${LINENO} "$BASH_COMMAND" $?' ERR
# 模拟错误
local file="/nonexistent/file"
if [[ ! -f "$file" ]]; then
echo "文件不存在,尝试创建..."
# 这会触发错误
cat "$file"
fi
# 清理trap
trap - ERR
}
# 主程序
main() {
error_handling
}
main
3.2.2 使用PS4自定义调试输出
#!/bin/bash
# 自定义调试输出格式
custom_debug_output() {
echo "=== 自定义调试输出 ==="
# 设置调试提示符
export PS4='+ $(date "+%H:%M:%S") [${BASH_SOURCE[0]}:${LINENO}] '
# 开启调试
set -x
local counter=0
while (( counter < 3 )); do
echo "循环计数: $counter"
((counter++))
done
set +x
}
# 主程序
main() {
custom_debug_output
}
main
3.2.3 使用bashdb调试器
#!/bin/bash
# 使用bashdb调试器
# 安装: sudo apt-get install bashdb (Ubuntu/Debian)
# 或: sudo yum install bashdb (CentOS/RHEL)
# 示例脚本: debug_script.sh
cat > /tmp/debug_script.sh << 'EOF'
#!/bin/bash
# 设置断点
# 在第5行设置断点: break 5
# 运行: run
# 查看变量: print variable_name
# 单步执行: next
# 继续执行: continue
function calculate() {
local a=$1
local b=$2
local sum=$((a + b))
echo "和: $sum"
return $sum
}
main() {
local x=10
local y=20
local result=$(calculate $x $y)
echo "结果: $result"
}
main
EOF
echo "调试脚本已创建: /tmp/debug_script.sh"
echo "使用bashdb调试:"
echo " bashdb /tmp/debug_script.sh"
echo " 在bashdb中:"
echo " break 5 # 在第5行设置断点"
echo " run # 运行脚本"
echo " print x # 查看变量x的值"
echo " next # 单步执行"
echo " continue # 继续执行"
3.3 常见错误与解决方案
3.3.1 变量未定义错误
#!/bin/bash
# 变量未定义错误处理
undefined_variable() {
echo "=== 变量未定义错误处理 ==="
# 方法1: 使用默认值
local var1="${undefined_var:-default_value}"
echo "var1: $var1"
# 方法2: 检查变量是否定义
if [[ -z "${undefined_var:-}" ]]; then
echo "变量未定义,使用默认值"
undefined_var="default_value"
fi
# 方法3: 使用set -u(严格模式)
# set -u 会使未定义变量报错
# set -u
# echo "$undefined_var" # 这会报错
}
# 主程序
main() {
undefined_variable
}
main
3.3.2 管道错误处理
#!/bin/bash
# 管道错误处理
pipe_error_handling() {
echo "=== 管道错误处理 ==="
# 错误示例:管道中某个命令失败,但脚本继续执行
echo "错误示例:"
ls /nonexistent 2>/dev/null | grep "file"
echo "脚本继续执行(即使ls失败)"
# 正确示例:使用set -o pipefail
echo -e "\n正确示例:"
set -o pipefail
if ls /nonexistent 2>/dev/null | grep "file"; then
echo "命令成功"
else
echo "命令失败(因为ls失败)"
fi
# 恢复默认设置
set +o pipefail
}
# 主程序
main() {
pipe_error_handling
}
main
3.3.3 文件权限问题
#!/bin/bash
# 文件权限问题处理
permission_issues() {
echo "=== 文件权限问题处理 ==="
local file="/tmp/test_file.txt"
# 检查文件是否存在
if [[ ! -e "$file" ]]; then
echo "文件不存在,创建文件..."
touch "$file"
fi
# 检查读写权限
if [[ ! -r "$file" ]]; then
echo "文件不可读,尝试修改权限..."
chmod +r "$file"
fi
if [[ ! -w "$file" ]]; then
echo "文件不可写,尝试修改权限..."
chmod +w "$file"
fi
# 安全地写入文件
if [[ -w "$file" ]]; then
echo "写入数据到 $file"
echo "测试数据" > "$file"
else
echo "无法写入文件 $file" >&2
exit 1
fi
# 清理
rm -f "$file"
}
# 主程序
main() {
permission_issues
}
main
四、Shell脚本最佳实践
4.1 代码风格与规范
#!/bin/bash
# 代码风格示例
code_style_example() {
echo "=== 代码风格示例 ==="
# 1. 使用一致的缩进(4个空格)
# 2. 变量命名:局部变量小写,常量大写
local local_var="value"
CONSTANT_VALUE="constant"
# 3. 函数命名:小写,用下划线分隔
function process_data() {
echo "处理数据"
}
# 4. 使用双引号包裹变量
local name="张三"
echo "你好, $name" # 正确
# echo 你好, $name # 错误:可能被拆分成多个参数
# 5. 使用[[ ]]代替[ ](更安全,支持更多特性)
if [[ "$name" == "张三" ]]; then
echo "名字匹配"
fi
# 6. 使用$( )代替反引号(更易读,支持嵌套)
local current_dir=$(pwd)
echo "当前目录: $current_dir"
# 7. 使用let或(( ))进行算术运算
local num=10
((num++))
echo "递增后: $num"
# 8. 使用case语句代替多个if
local status="running"
case "$status" in
"running")
echo "进程运行中"
;;
"stopped")
echo "进程已停止"
;;
*)
echo "未知状态"
;;
esac
# 9. 使用函数封装重复代码
function log_message() {
local level="$1"
local message="$2"
echo "[$level] $message"
}
log_message "INFO" "脚本开始执行"
# 10. 添加适当的注释
# 这是一个重要的注释,解释代码的意图
local important_value=42
}
# 主程序
main() {
code_style_example
}
main
4.2 性能优化技巧
#!/bin/bash
# 性能优化示例
performance_optimization() {
echo "=== 性能优化示例 ==="
# 1. 避免不必要的子shell
echo "优化前:"
for i in {1..1000}; do
echo "数字: $i" | grep "5" # 每次循环都创建子shell
done
echo -e "\n优化后:"
for i in {1..1000}; do
if [[ "$i" == *"5"* ]]; then
echo "数字: $i"
fi
done
# 2. 使用内置命令代替外部命令
echo -e "\n内置命令优化:"
local str="hello world"
# 慢:使用外部命令
# local upper=$(echo "$str" | tr '[:lower:]' '[:upper:]')
# 快:使用内置命令
local upper="${str^^}"
echo "大写: $upper"
# 3. 批量处理文件
echo -e "\n批量处理优化:"
# 慢:逐个处理文件
# for file in *.txt; do
# cat "$file" | grep "error" >> errors.log
# done
# 快:批量处理
# grep "error" *.txt > errors.log
# 4. 使用数组代替多次命令执行
echo -e "\n数组优化:"
# 慢:多次执行命令
# for i in {1..10}; do
# echo "第 $i 次: $(date)"
# done
# 快:使用数组
local timestamps=()
for i in {1..10}; do
timestamps+=("$(date)")
done
for ts in "${timestamps[@]}"; do
echo "时间戳: $ts"
done
# 5. 使用进程替换代替临时文件
echo -e "\n进程替换优化:"
# 慢:使用临时文件
# sort file1.txt > temp1.txt
# sort file2.txt > temp2.txt
# diff temp1.txt temp2.txt
# rm temp1.txt temp2.txt
# 快:使用进程替换
# diff <(sort file1.txt) <(sort file2.txt)
}
# 主程序
main() {
performance_optimization
}
main
4.3 安全考虑
#!/bin/bash
# 安全考虑示例
security_considerations() {
echo "=== 安全考虑示例 ==="
# 1. 输入验证
validate_input() {
local input="$1"
# 检查输入是否为空
if [[ -z "$input" ]]; then
echo "输入不能为空" >&2
return 1
fi
# 检查输入是否包含特殊字符
if [[ "$input" =~ [;&|<>] ]]; then
echo "输入包含非法字符" >&2
return 1
fi
# 检查输入长度
if [[ ${#input} -gt 100 ]]; then
echo "输入过长" >&2
return 1
fi
return 0
}
# 2. 避免命令注入
echo -e "\n避免命令注入:"
local user_input=""
read -p "请输入文件名: " user_input
if validate_input "$user_input"; then
# 错误:直接使用用户输入
# cat "$user_input"
# 正确:限制文件路径
local base_dir="/tmp"
local safe_path="$base_dir/$user_input"
# 确保路径在允许的目录内
if [[ "$safe_path" == "$base_dir"/* ]]; then
if [[ -f "$safe_path" ]]; then
cat "$safe_path"
else
echo "文件不存在" >&2
fi
else
echo "路径非法" >&2
fi
fi
# 3. 使用临时文件的安全方法
echo -e "\n安全使用临时文件:"
# 创建临时文件
local temp_file=$(mktemp)
# 确保临时文件在脚本退出时被删除
trap 'rm -f "$temp_file"' EXIT
# 使用临时文件
echo "临时数据" > "$temp_file"
cat "$temp_file"
# 4. 权限最小化
echo -e "\n权限最小化:"
# 使用sudo时,指定具体命令
# 错误: sudo command # 可能执行任意命令
# 正确: sudo /usr/bin/command # 限制命令路径
# 5. 日志记录
echo -e "\n日志记录:"
local log_file="/var/log/my_script.log"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 记录重要操作
echo "[$timestamp] 用户执行了操作" >> "$log_file"
# 6. 错误处理
echo -e "\n错误处理:"
# 设置错误处理
set -euo pipefail
# 捕获错误
trap 'echo "脚本在第 $LINENO 行出错" >&2; exit 1' ERR
# 7. 输入清理
echo -e "\n输入清理:"
local raw_input="hello; rm -rf /"
local clean_input=$(echo "$raw_input" | sed 's/[;&|<>]//g')
echo "清理后: $clean_input"
}
# 主程序
main() {
security_considerations
}
main
五、实际应用案例
5.1 系统监控脚本
#!/bin/bash
# 系统监控脚本
system_monitor() {
echo "=== 系统监控脚本 ==="
# 配置
LOG_FILE="/var/log/system_monitor.log"
ALERT_THRESHOLD_CPU=80
ALERT_THRESHOLD_MEM=80
ALERT_THRESHOLD_DISK=90
# 日志函数
log_message() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# 检查CPU使用率
check_cpu() {
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
if (( $(echo "$cpu_usage > $ALERT_THRESHOLD_CPU" | bc -l) )); then
log_message "WARNING" "CPU使用率过高: ${cpu_usage}%"
return 1
fi
log_message "INFO" "CPU使用率正常: ${cpu_usage}%"
return 0
}
# 检查内存使用率
check_memory() {
local mem_usage=$(free | grep Mem | awk '{printf "%.2f", $3/$2 * 100}')
if (( $(echo "$mem_usage > $ALERT_THRESHOLD_MEM" | bc -l) )); then
log_message "WARNING" "内存使用率过高: ${mem_usage}%"
return 1
fi
log_message "INFO" "内存使用率正常: ${mem_usage}%"
return 0
}
# 检查磁盘使用率
check_disk() {
local disk_usage=$(df / | tail -1 | awk '{print $5}' | cut -d'%' -f1)
if (( disk_usage > ALERT_THRESHOLD_DISK )); then
log_message "WARNING" "磁盘使用率过高: ${disk_usage}%"
return 1
fi
log_message "INFO" "磁盘使用率正常: ${disk_usage}%"
return 0
}
# 主监控函数
monitor() {
log_message "INFO" "开始系统监控"
local cpu_ok=true
local mem_ok=true
local disk_ok=true
check_cpu || cpu_ok=false
check_memory || mem_ok=false
check_disk || disk_ok=false
if $cpu_ok && $mem_ok && $disk_ok; then
log_message "INFO" "系统状态正常"
else
log_message "ERROR" "系统状态异常"
# 发送警报(示例)
# send_alert "系统监控发现异常"
fi
log_message "INFO" "监控结束"
}
# 执行监控
monitor
}
# 主程序
main() {
system_monitor
}
main
5.2 文件批量处理脚本
#!/bin/bash
# 文件批量处理脚本
batch_file_processor() {
echo "=== 文件批量处理脚本 ==="
# 配置
SOURCE_DIR="/tmp/source"
TARGET_DIR="/tmp/target"
LOG_FILE="/tmp/batch_process.log"
# 日志函数
log_message() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# 验证目录
validate_directories() {
if [[ ! -d "$SOURCE_DIR" ]]; then
log_message "ERROR" "源目录不存在: $SOURCE_DIR"
return 1
fi
if [[ ! -d "$TARGET_DIR" ]]; then
log_message "INFO" "目标目录不存在,创建: $TARGET_DIR"
mkdir -p "$TARGET_DIR"
fi
return 0
}
# 处理单个文件
process_file() {
local source_file="$1"
local target_file="$2"
log_message "INFO" "处理文件: $source_file"
# 检查文件类型
local file_type=$(file -b "$source_file")
case "$file_type" in
*"text"*)
# 文本文件处理
log_message "INFO" "文本文件,进行格式化..."
# 示例:添加行号
cat -n "$source_file" > "$target_file"
;;
*"image"*)
# 图像文件处理
log_message "INFO" "图像文件,复制..."
cp "$source_file" "$target_file"
;;
*)
# 其他文件
log_message "INFO" "其他类型文件,直接复制..."
cp "$source_file" "$target_file"
;;
esac
# 检查处理结果
if [[ -f "$target_file" ]]; then
log_message "INFO" "文件处理成功: $target_file"
return 0
else
log_message "ERROR" "文件处理失败: $target_file"
return 1
fi
}
# 批量处理
batch_process() {
log_message "INFO" "开始批量处理文件"
local processed=0
local failed=0
for source_file in "$SOURCE_DIR"/*; do
if [[ -f "$source_file" ]]; then
local filename=$(basename "$source_file")
local target_file="$TARGET_DIR/$filename"
if process_file "$source_file" "$target_file"; then
((processed++))
else
((failed++))
fi
fi
done
log_message "INFO" "处理完成: 成功 $processed 个,失败 $failed 个"
}
# 主函数
main() {
log_message "INFO" "脚本开始执行"
if validate_directories; then
batch_process
fi
log_message "INFO" "脚本执行结束"
}
main
}
# 主程序
main() {
batch_file_processor
}
main
5.3 自动化部署脚本
#!/bin/bash
# 自动化部署脚本
auto_deploy() {
echo "=== 自动化部署脚本 ==="
# 配置
APP_NAME="myapp"
APP_VERSION="1.0.0"
DEPLOY_DIR="/opt/$APP_NAME"
BACKUP_DIR="/backup/$APP_NAME"
LOG_FILE="/var/log/$APP_NAME/deploy.log"
# 日志函数
log_message() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# 备份当前版本
backup_current() {
log_message "INFO" "备份当前版本..."
if [[ -d "$DEPLOY_DIR" ]]; then
local backup_path="$BACKUP_DIR/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_path"
cp -r "$DEPLOY_DIR" "$backup_path/"
log_message "INFO" "备份完成: $backup_path"
else
log_message "INFO" "当前版本不存在,无需备份"
fi
}
# 停止服务
stop_service() {
log_message "INFO" "停止服务..."
# 检查服务是否运行
if systemctl is-active --quiet "$APP_NAME"; then
systemctl stop "$APP_NAME"
log_message "INFO" "服务已停止"
else
log_message "INFO" "服务未运行"
fi
}
# 部署新版本
deploy_new() {
log_message "INFO" "部署新版本..."
# 创建部署目录
mkdir -p "$DEPLOY_DIR"
# 模拟部署过程
echo "部署应用 $APP_NAME 版本 $APP_VERSION" > "$DEPLOY_DIR/version.txt"
echo "部署时间: $(date)" >> "$DEPLOY_DIR/version.txt"
# 设置权限
chmod -R 755 "$DEPLOY_DIR"
chown -R root:root "$DEPLOY_DIR"
log_message "INFO" "新版本部署完成"
}
# 启动服务
start_service() {
log_message "INFO" "启动服务..."
# 模拟启动过程
# systemctl start "$APP_NAME"
# 检查服务状态
# if systemctl is-active --quiet "$APP_NAME"; then
# log_message "INFO" "服务启动成功"
# return 0
# else
# log_message "ERROR" "服务启动失败"
# return 1
# fi
log_message "INFO" "服务启动完成(模拟)"
return 0
}
# 验证部署
verify_deployment() {
log_message "INFO" "验证部署..."
# 检查版本文件
if [[ -f "$DEPLOY_DIR/version.txt" ]]; then
local deployed_version=$(grep "版本" "$DEPLOY_DIR/version.txt" | cut -d' ' -f2)
if [[ "$deployed_version" == "$APP_VERSION" ]]; then
log_message "INFO" "版本验证成功: $deployed_version"
return 0
else
log_message "ERROR" "版本验证失败"
return 1
fi
else
log_message "ERROR" "版本文件不存在"
return 1
fi
}
# 回滚函数
rollback() {
log_message "ERROR" "部署失败,开始回滚..."
# 查找最新备份
local latest_backup=$(ls -td "$BACKUP_DIR"/*/ 2>/dev/null | head -1)
if [[ -n "$latest_backup" ]]; then
log_message "INFO" "恢复备份: $latest_backup"
rm -rf "$DEPLOY_DIR"
cp -r "$latest_backup" "$DEPLOY_DIR"
start_service
log_message "INFO" "回滚完成"
else
log_message "ERROR" "无可用备份,无法回滚"
fi
}
# 主部署流程
deploy() {
log_message "INFO" "开始部署流程"
# 设置错误处理
set -e
trap 'rollback' ERR
# 执行部署步骤
backup_current
stop_service
deploy_new
start_service
# 验证
if verify_deployment; then
log_message "INFO" "部署成功"
trap - ERR # 清除错误处理
else
log_message "ERROR" "部署验证失败"
return 1
fi
}
# 主函数
main() {
log_message "INFO" "自动化部署脚本启动"
# 检查权限
if [[ $EUID -ne 0 ]]; then
log_message "ERROR" "此脚本需要root权限"
exit 1
fi
deploy
log_message "INFO" "脚本执行完成"
}
main
}
# 主程序
main() {
auto_deploy
}
main
六、调试工具与技巧
6.1 使用ShellCheck进行静态分析
#!/bin/bash
# ShellCheck使用示例
shellcheck_example() {
echo "=== ShellCheck使用示例 ==="
# 创建一个有问题的脚本
cat > /tmp/bad_script.sh << 'EOF'
#!/bin/bash
# 这个脚本有很多问题
echo "Hello World"
# 未引用变量
echo $UNDEFINED_VAR
# 使用未定义的变量
if [ $MISSING_VAR = "value" ]; then
echo "匹配"
fi
# 使用反引号(不推荐)
result=`ls -l`
# 使用[ ]而不是[[ ]]
if [ "$1" = "test" ]; then
echo "参数是test"
fi
# 未检查命令执行结果
grep "pattern" file.txt
# 使用未定义的数组
echo ${array[@]}
# 未使用local声明局部变量
function myfunc() {
var="value"
echo $var
}
EOF
echo "问题脚本已创建: /tmp/bad_script.sh"
echo -e "\n使用ShellCheck检查:"
echo " shellcheck /tmp/bad_script.sh"
echo -e "\n预期输出:"
echo " 1. 警告: 未引用的变量"
echo " 2. 警告: 使用未定义的变量"
echo " 3. 警告: 使用反引号"
echo " 4. 警告: 使用[ ]而不是[[ ]]"
echo " 5. 警告: 未检查命令执行结果"
echo " 6. 警告: 使用未定义的数组"
echo " 7. 警告: 未使用local声明局部变量"
# 创建修复后的脚本
cat > /tmp/good_script.sh << 'EOF'
#!/bin/bash
# 修复后的脚本
echo "Hello World"
# 引用变量
echo "${UNDEFINED_VAR:-}"
# 检查变量是否定义
if [[ -n "${MISSING_VAR:-}" ]] && [[ "${MISSING_VAR:-}" = "value" ]]; then
echo "匹配"
fi
# 使用$( )而不是反引号
result=$(ls -l)
# 使用[[ ]]而不是[ ]
if [[ "$1" == "test" ]]; then
echo "参数是test"
fi
# 检查命令执行结果
if grep "pattern" file.txt; then
echo "找到匹配"
fi
# 初始化数组
declare -a array=()
echo "${array[@]}"
# 使用local声明局部变量
function myfunc() {
local var="value"
echo "$var"
}
EOF
echo -e "\n修复后的脚本已创建: /tmp/good_script.sh"
echo "使用ShellCheck检查修复后的脚本:"
echo " shellcheck /tmp/good_script.sh"
echo " 预期: 无警告"
}
# 主程序
main() {
shellcheck_example
}
main
6.2 使用strace进行系统调用跟踪
#!/bin/bash
# strace使用示例
strace_example() {
echo "=== strace使用示例 ==="
# 创建一个简单的脚本
cat > /tmp/trace_script.sh << 'EOF'
#!/bin/bash
# 这个脚本会进行一些系统调用
echo "开始执行"
# 文件操作
touch /tmp/test_file.txt
echo "测试数据" > /tmp/test_file.txt
cat /tmp/test_file.txt
# 进程操作
sleep 1
# 网络操作(如果可用)
# curl -s http://example.com > /dev/null 2>&1
echo "执行完成"
EOF
chmod +x /tmp/trace_script.sh
echo "测试脚本已创建: /tmp/trace_script.sh"
echo -e "\n使用strace跟踪:"
echo " strace -f /tmp/trace_script.sh"
echo -e "\n常用选项:"
echo " -f: 跟踪子进程"
echo " -e trace=file: 只跟踪文件相关系统调用"
echo " -e trace=network: 只跟踪网络相关系统调用"
echo " -o output.txt: 输出到文件"
echo " -p PID: 跟踪已运行的进程"
echo -e "\n示例命令:"
echo " strace -f -e trace=file /tmp/trace_script.sh"
echo " strace -f -o /tmp/strace_output.txt /tmp/trace_script.sh"
}
# 主程序
main() {
strace_example
}
main
6.3 使用ltrace进行库函数跟踪
#!/bin/bash
# ltrace使用示例
ltrace_example() {
echo "=== ltrace使用示例 ==="
# 创建一个调用外部命令的脚本
cat > /tmp/ltrace_script.sh << 'EOF'
#!/bin/bash
# 这个脚本会调用外部命令
echo "开始执行"
# 调用外部命令
ls -l /tmp
date
whoami
# 调用多个命令
for i in {1..3}; do
echo "循环 $i"
sleep 0.5
done
echo "执行完成"
EOF
chmod +x /tmp/ltrace_script.sh
echo "测试脚本已创建: /tmp/ltrace_script.sh"
echo -e "\n使用ltrace跟踪:"
echo " ltrace /tmp/ltrace_script.sh"
echo -e "\n常用选项:"
echo " -c: 统计库函数调用次数和时间"
echo " -e: 排除指定库"
echo " -f: 跟踪子进程"
echo " -o output.txt: 输出到文件"
echo -e "\n示例命令:"
echo " ltrace -c /tmp/ltrace_script.sh"
echo " ltrace -f /tmp/ltrace_script.sh"
}
# 主程序
main() {
ltrace_example
}
main
七、总结
Shell脚本编写与调试是操作系统实验中的重要技能。通过本文的详细介绍,我们涵盖了从基础到高级的各个方面:
- 基础编写技巧:脚本结构、变量处理、条件判断和循环
- 高级技巧:数组、字符串处理、函数模块化
- 调试方法:使用
set -x、trap、bashdb等工具 - 最佳实践:代码风格、性能优化、安全考虑
- 实际案例:系统监控、文件处理、自动化部署
- 调试工具:ShellCheck、strace、ltrace
在实际操作中,建议:
- 始终使用
set -euo pipefail开启严格模式 - 编写详细的注释和文档
- 进行充分的测试,包括边界情况
- 使用版本控制管理脚本
- 定期审查和优化脚本
通过不断练习和应用这些技巧,你将能够编写出高效、健壮、易于维护的Shell脚本,为操作系统实验和系统管理工作提供强大支持。
