Shell 脚本编程全解析:从入门到企业级实战
一、Shell 脚本基础认知
1.1 什么是 Shell
Shell 是一种命令行解释器,它为用户提供了与操作系统内核交互的接口。简单来说,Shell 就是用户与 Linux 系统之间的 "翻译官",将用户的命令转换为内核能够理解的指令。
而 Shell 脚本(Shell Script)则是将一系列 Shell 命令按顺序保存到文本文件中,赋予执行权限后可以批量执行的脚本文件。它类似于 Windows 系统的批处理文件(.bat),但功能更加强大、语法更加灵活。
1.2 Shell 的种类
Linux 系统中常见的 Shell 有多种类型:
- Bash(Bourne Again SHell):最常用的 Shell,是 Bourne Shell 的增强版,兼容 Bourne Shell 并增加了许多新特性
- Sh(Bourne Shell):最早的 Unix Shell,功能相对简单
- Csh(C Shell):语法类似 C 语言,有自己的特点
- Ksh(Korn Shell):结合了 Csh 和 Sh 的优点
- Zsh:功能更强大的 Shell,兼容 Bash 并增加了更多特性
在主流 Linux 发行版(如 CentOS、Ubuntu)中,默认的 Shell 都是 Bash,因此本文主要围绕 Bash 脚本展开。
可以通过以下命令查看系统默认 Shell:
bash
echo $SHELL
查看系统安装的所有 Shell:
bash
cat /etc/shells
二、为什么要学习 Shell 脚本编程
2.1 自动化运维的核心工具
在 Linux 服务器管理中,许多重复性工作可以通过 Shell 脚本自动化完成:
- 系统监控与报警
- 日志分析与统计
- 批量部署与配置
- 定时任务执行
- 数据备份与恢复
2.2 处理文本文件的优势
Linux 系统中绝大多数配置文件、日志文件都是纯文本格式,Shell 脚本结合文本处理工具(grep、sed、awk 等)可以高效处理这些文件:
- 快速提取关键信息
- 批量修改配置参数
- 分析日志并生成报告
- 处理格式化数据
2.3 职业发展的必备技能
对于运维工程师、开发工程师、测试工程师而言,Shell 脚本编程是一项重要技能:
- 提升工作效率,减少重复劳动
- 实现复杂的系统管理任务
- 更好地理解 Linux 系统原理
- 为学习更复杂的编程语言打下基础
三、Shell 脚本入门准备
3.1 环境准备
学习 Shell 脚本编程不需要特殊的开发环境,只需要:
- 一台 Linux 系统的计算机或服务器
- 一个文本编辑器(推荐 vim 或 nano)
- 基本的 Linux 命令操作能力
3.2 vim 编辑器配置
vim 是编写 Shell 脚本的理想工具,以下是一些实用配置:
bash
# 创建或编辑 vim 配置文件
vim ~/.vimrc
# 添加以下配置
set number # 显示行号
set syntax on # 语法高亮
set autoindent # 自动缩进
set smartindent # 智能缩进
set tabstop=4 # Tab 键宽度
set shiftwidth=4 # 自动缩进宽度
set showmatch # 括号匹配显示
配置生效后,使用 vim 编辑脚本会更加高效。
四、第一个 Shell 脚本
4.1 脚本创建与执行
创建一个简单的 Shell 脚本:
bash
# 创建脚本文件
vim hello.sh
# 输入以下内容
#!/bin/bash
# 这是一个简单的 Shell 脚本
echo "Hello, World!"
echo "当前时间: $(date)"
echo "当前用户: $USER"
echo "当前目录: $(pwd)"
脚本执行方法:
-
使用 bash 命令执行(不需要执行权限):bash hello.sh
-
赋予执行权限后直接执行:
bash
# 添加执行权限
chmod +x hello.sh
# 执行脚本
./hello.sh
- 使用绝对路径执行:
bash
# 假设脚本在 /home/user 目录下
/home/user/hello.sh
- 使用 source 命令执行(在当前 Shell 环境中执行):
bash
source hello.sh
# 或
. hello.sh
4.2 脚本结构解析
一个规范的 Shell 脚本通常包含以下几个部分:
- 解释器声明 :第一行的
#!/bin/bash
指定脚本使用 bash 解释器 - 注释信息 :以
#
开头的行,用于说明脚本功能、作者、版本等 - 脚本主体:具体的命令和控制结构
bash
#!/bin/bash
# 脚本名称: system_info.sh
# 作 者: Your Name
# 日 期: 2023-07-01
# 版 本: 1.0
# 功 能: 显示系统基本信息
echo "=== 系统信息 ==="
uname -a
echo -e "\n=== CPU 信息 ==="
lscpu | grep 'Model name\|CPU(s)'
echo -e "\n=== 内存信息 ==="
free -h
echo -e "\n=== 磁盘信息 ==="
df -h
五、Shell 变量与数据类型
5.1 变量定义与使用
Shell 变量不需要声明类型,直接定义即可:
bash
#!/bin/bash
# 定义变量(等号前后不能有空格)
name="John"
age=30
height=1.75
# 使用变量(在变量名前加 $)
echo "姓名: $name"
echo "年龄: $age"
echo "身高: $height"
# 变量重新赋值
age=31
echo "修改后的年龄: $age"
# 变量拼接
greeting="Hello, "$name"!"
echo $greeting
5.2 系统环境变量
系统预设了一些环境变量,可以直接使用:
bash
#!/bin/bash
echo "当前用户: $USER"
echo "用户家目录: $HOME"
echo "当前工作目录: $PWD"
echo "命令搜索路径: $PATH"
echo "主机名: $HOSTNAME"
echo "Shell 类型: $SHELL"
echo "终端类型: $TERM"
echo "进程 ID: $$" # 当前脚本的进程 ID
echo "退出状态: $?" # 上一条命令的退出状态,0 表示成功
5.3 位置参数变量
用于获取脚本执行时传入的参数:
bash
#!/bin/bash
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "第三个参数: $3"
echo "所有参数: $*" # 所有参数作为一个整体
echo "所有参数: $@" # 所有参数作为独立个体
echo "参数个数: $#" # 参数的数量
执行示例:
bash
./params.sh first second third
5.4 变量操作
bash
#!/bin/bash
# 字符串长度
str="Hello World"
echo "字符串长度: ${#str}"
# 字符串截取
echo "从位置 1 开始截取: ${str:1}"
echo "从位置 1 开始截取 5 个字符: ${str:1:5}"
# 字符串替换
echo "替换第一个匹配: ${str/Hello/Hi}"
echo "替换所有匹配: ${str//o/O}"
# 变量默认值
echo "变量存在: ${name:-默认值}" # 如果 name 未定义,使用默认值
name="John"
echo "变量存在: ${name:-默认值}" # 如果 name 已定义,使用变量值
六、Shell 运算符与表达式
6.1 算术运算符
bash
#!/bin/bash
a=10
b=3
# 算术运算方式 1: 使用 $((...))
echo "a + b = $((a + b))"
echo "a - b = $((a - b))"
echo "a * b = $((a * b))"
echo "a / b = $((a / b))"
echo "a % b = $((a % b))"
echo "a^2 = $((a **2))"
# 算术运算方式 2: 使用 $[...])
echo "a + b = $[a + b]"
# 算术运算方式 3: 使用 expr 命令
echo "a + b = $(expr $a + $b)"
echo "a * b = $(expr $a \* $b)" # 注意乘法需要转义
6.2 赋值运算符
bash
#!/bin/bash
a=10
# 基本赋值
b=$a
echo "b = $b"
# 复合赋值
b=5
b+=3 # 等价于 b=$((b + 3))
echo "b += 3 后: $b"
b=5
b*=2 # 等价于 b=$((b * 2))
echo "b *= 2 后: $b"
6.3 比较运算符
6.3.1 数值比较
bash
#!/bin/bash
a=10
b=20
# 使用 [ ] 进行比较,注意空格
if [ $a -eq $b ]; then
echo "$a 等于 $b"
else
echo "$a 不等于 $b"
fi
if [ $a -gt $b ]; then
echo "$a 大于 $b"
else
echo "$a 不大于 $b"
fi
# 常用数值比较运算符
# -eq: 等于
# -ne: 不等于
# -gt: 大于
# -lt: 小于
# -ge: 大于等于
# -le: 小于等于
6.3.2 字符串比较
bash
#!/bin/bash
str1="hello"
str2="world"
str3="hello"
if [ "$str1" = "$str2" ]; then
echo "$str1 等于 $str2"
else
echo "$str1 不等于 $str2"
fi
if [ "$str1" = "$str3" ]; then
echo "$str1 等于 $str3"
else
echo "$str1 不等于 $str3"
fi
# 字符串长度比较
if [ -z "$str1" ]; then
echo "$str1 是空字符串"
else
echo "$str1 不是空字符串,长度为 ${#str1}"
fi
# 常用字符串比较运算符
# =: 等于
# !=: 不等于
# -z: 字符串长度为 0
# -n: 字符串长度不为 0
6.4 逻辑运算符
bash
#!/bin/bash
a=10
b=20
c=30
# 逻辑与 -a 或 &&
if [ $a -lt $b -a $b -lt $c ]; then
echo "$a < $b < $c 成立"
fi
if [ $a -lt $b ] && [ $b -lt $c ]; then
echo "$a < $b < $c 成立"
fi
# 逻辑或 -o 或 ||
if [ $a -gt $b -o $b -lt $c ]; then
echo "条件成立"
fi
if [ $a -gt $b ] || [ $b -lt $c ]; then
echo "条件成立"
fi
# 逻辑非 !
if [ ! $a -gt $b ]; then
echo "$a 不大于 $b"
fi
6.5 文件测试运算符
bash
#!/bin/bash
file="test.txt"
dir="/tmp"
# 检查文件是否存在
if [ -e "$file" ]; then
echo "$file 存在"
else
echo "$file 不存在"
touch "$file"
echo "已创建 $file"
fi
# 检查是否为普通文件
if [ -f "$file" ]; then
echo "$file 是普通文件"
fi
# 检查是否为目录
if [ -d "$dir" ]; then
echo "$dir 是目录"
fi
# 检查文件是否可执行
if [ -x "$file" ]; then
echo "$file 可执行"
else
echo "$file 不可执行"
chmod +x "$file"
echo "已设置 $file 可执行权限"
fi
# 常用文件测试运算符
# -e: 文件或目录存在
# -f: 是普通文件
# -d: 是目录
# -r: 可读
# -w: 可写
# -x: 可执行
# -s: 文件大小不为 0
# -L: 是符号链接
七、流程控制语句
7.1 if 条件语句
7.1.1 基本语法
bash
#!/bin/bash
# 单分支 if 语句
age=18
if [ $age -ge 18 ]; then
echo "已成年"
fi
# 双分支 if 语句
score=75
if [ $score -ge 60 ]; then
echo "成绩合格"
else
echo "成绩不合格"
fi
# 多分支 if 语句
if [ $score -ge 90 ]; then
echo "优秀"
elif [ $score -ge 80 ]; then
echo "良好"
elif [ $score -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
7.1.2 实际应用示例
bash
#!/bin/bash
# 检查服务是否运行
service_name="sshd"
# 检查服务状态
if systemctl is-active --quiet $service_name; then
echo "$service_name 服务正在运行"
# 检查服务是否设置为开机启动
if systemctl is-enabled --quiet $service_name; then
echo "$service_name 服务已设置为开机启动"
else
echo "$service_name 服务未设置为开机启动"
read -p "是否设置 $service_name 为开机启动? (y/n) " choice
if [ "$choice" = "y" ] || [ "$choice" = "Y" ]; then
systemctl enable $service_name
echo "$service_name 已设置为开机启动"
fi
fi
else
echo "$service_name 服务未运行"
read -p "是否启动 $service_name 服务? (y/n) " choice
if [ "$choice" = "y" ] || [ "$choice" = "Y" ]; then
systemctl start $service_name
if [ $? -eq 0 ]; then
echo "$service_name 服务启动成功"
else
echo "$service_name 服务启动失败"
fi
fi
fi
7.2 case 语句
当需要匹配多个条件时,case 语句比 if 语句更简洁:
bash
#!/bin/bash
# 服务管理脚本
if [ $# -ne 1 ]; then
echo "用法: $0 [start|stop|restart|status]"
exit 1
fi
service_name="httpd"
case "$1" in
start)
systemctl start $service_name
echo "$service_name 服务已启动"
;;
stop)
systemctl stop $service_name
echo "$service_name 服务已停止"
;;
restart)
systemctl restart $service_name
echo "$service_name 服务已重启"
;;
status)
systemctl status $service_name
;;
*)
echo "无效参数: $1"
echo "用法: $0 [start|stop|restart|status]"
exit 1
;;
esac
7.3 for 循环
7.3.1 基本语法
bash
#!/bin/bash
# 遍历列表
fruits="apple banana cherry date"
for fruit in $fruits; do
echo "水果: $fruit"
done
# 遍历数字范围
echo -e "\n计数从 1 到 5:"
for i in {1..5}; do
echo $i
done
# 遍历带步长的数字范围
echo -e "\n偶数从 2 到 10:"
for i in {2..10..2}; do
echo $i
done
# C 风格的 for 循环
echo -e "\nC 风格循环:"
for ((i=1; i<=5; i++)); do
echo $i
done
7.3.2 实际应用示例
bash
#!/bin/bash
# 批量检查主机连通性
# 主机列表
hosts="192.168.1.1 192.168.1.10 192.168.1.20 www.baidu.com www.google.com"
# 检查每个主机
for host in $hosts; do
echo -n "检查 $host: "
# 发送 2 个 ICMP 包,超时时间 2 秒
ping -c 2 -W 2 $host &>/dev/null
if [ $? -eq 0 ]; then
echo "连通"
else
echo "不通"
fi
done
7.4 while 循环
while 循环在条件为真时重复执行命令:
bash
#!/bin/bash
# 基本 while 循环
count=1
echo "计数到 5:"
while [ $count -le 5 ]; do
echo $count
count=$((count + 1))
# 或者使用 let 命令: let count++
done
# 读取文件内容
echo -e "\n读取文件内容:"
file="test.txt"
# 创建测试文件
echo "第一行" > $file
echo "第二行" >> $file
echo "第三行" >> $file
# 使用 while 循环读取文件
while read line; do
echo "行内容: $line"
done < $file
# 删除测试文件
rm -f $file
# 无限循环(按 Ctrl+C 退出)
echo -e "\n无限循环,按 Ctrl+C 退出"
count=1
while true; do
echo "循环次数: $count"
count=$((count + 1))
sleep 1
# 循环 5 次后退出
if [ $count -gt 5 ]; then
break
fi
done
7.5 until 循环
until 循环与 while 循环相反,在条件为假时重复执行命令:
bash
#!/bin/bash
# 基本 until 循环
count=1
echo "计数到 5:"
until [ $count -gt 5 ]; do
echo $count
let count++
done
# 实际应用:等待服务启动
echo -e "\n等待 httpd 服务启动..."
until systemctl is-active --quiet httpd; do
echo "httpd 服务尚未启动,等待 3 秒..."
sleep 3
done
echo "httpd 服务已启动"
7.6 循环控制语句
break 和 continue 用于控制循环流程:
bash
#!/bin/bash
# break 示例:找到第一个偶数就退出
echo "寻找 1-10 中的第一个偶数:"
for num in {1..10}; do
if [ $((num % 2)) -eq 0 ]; then
echo "找到偶数: $num"
break # 退出循环
fi
echo "检查: $num"
done
# continue 示例:只显示奇数
echo -e "\n显示 1-10 中的奇数:"
for num in {1..10}; do
if [ $((num % 2)) -eq 0 ]; then
continue # 跳过本次循环的剩余部分
fi
echo $num
done
# break N 和 continue N:控制多层循环
echo -e "\n多层循环控制:"
for i in {1..3}; do
echo "外层循环: $i"
for j in {1..3}; do
if [ $j -eq 2 ]; then
break 2 # 退出两层循环
# continue 2 # 跳过外层循环的当前迭代
fi
echo " 内层循环: $j"
done
done
八、函数与模块化
8.1 函数定义与调用
bash
#!/bin/bash
# 基本函数定义
hello() {
echo "Hello, World!"
}
# 调用函数
echo "调用 hello 函数:"
hello
# 带参数的函数
greet() {
echo "Hello, $1!"
echo "函数参数个数: $#"
echo "所有参数: $*"
}
echo -e "\n调用 greet 函数:"
greet "John" "Doe"
# 带返回值的函数
add() {
local sum=$(( $1 + $2 )) # local 声明局部变量
echo $sum # 通过 echo 返回值
}
echo -e "\n调用 add 函数:"
result=$(add 10 20)
echo "10 + 20 = $result"
8.2 函数库与模块化
将常用函数放在单独的文件中,需要时引入使用:
- 创建函数库文件
myfuncs.sh
:
bash
#!/bin/bash
# 显示错误信息
error() {
echo "ERROR: $1" >&2
}
# 显示信息并退出
die() {
error "$1"
exit 1
}
# 检查命令是否存在
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# 计算文件大小总和
dir_size() {
if [ $# -ne 1 ] || [ ! -d "$1" ]; then
error "请提供一个目录作为参数"
return 1
fi
du -sh "$1" | awk '{print $1}'
}
- 在脚本中引入函数库:
bash
#!/bin/bash
# 引入函数库
source ./myfuncs.sh || die "无法加载函数库"
# 使用函数库中的函数
echo "检查 curl 命令是否存在:"
if command_exists curl; then
echo "curl 已安装"
else
error "curl 未安装"
fi
echo -e "\n计算当前目录大小:"
dir_size .
echo -e "\n测试错误处理:"
error "这是一个错误信息"
# die "这是一个致命错误,程序将退出"
九、文本处理三剑客
9.1 grep:文本搜索工具
bash
#!/bin/bash
# 创建测试文件
test_file="grep_test.txt"
cat > $test_file << EOF
apple
Banana
cherry
date
elderberry
fig
Grape
kiwi
lemon
mango
EOF
echo "查找包含 'e' 的行:"
grep "e" $test_file
echo -e "\n忽略大小写查找包含 'a' 的行:"
grep -i "a" $test_file
echo -e "\n查找不包含 'e' 的行:"
grep -v "e" $test_file
echo -e "\n显示行号:"
grep -n "e" $test_file
echo -e "\n使用正则表达式查找以 'b' 或 'B' 开头的行:"
grep -i "^b" $test_file
# 清理测试文件
rm -f $test_file
9.2 sed:流编辑器
bash
#!/bin/bash
# 创建测试文件
test_file="sed_test.txt"
cat > $test_file << EOF
apple
Banana
cherry
date
elderberry
fig
Grape
kiwi
lemon
mango
EOF
echo "原始文件内容:"
cat $test_file
echo -e "\n将 'e' 替换为 'E':"
sed 's/e/E/' $test_file
echo -e "\n全局替换 'e' 为 'E':"
sed 's/e/E/g' $test_file
echo -e "\n删除包含 'a' 的行:"
sed '/a/d' $test_file
echo -e "\n在行首添加序号:"
sed '=' $test_file | sed 'N;s/\n/. /'
echo -e "\n在文件中插入一行:"
sed '3i inserted line' $test_file
# 原地修改文件(备份原始文件)
sed -i.bak 's/^/fruit: /' $test_file
echo -e "\n原地修改后的文件:"
cat $test_file
# 清理测试文件
rm -f $test_file $test_file.bak
9.3 awk:文本处理语言
bash
#!/bin/bash
# 创建测试文件
test_file="awk_test.txt"
cat > $test_file << EOF
Name Age City
Alice 25 New York
Bob 30 London
Charlie 35 Paris
David 40 Tokyo
EOF
echo "原始文件内容:"
cat $test_file
echo -e "\n打印第一列和第三列:"
awk '{print $1, $3}' $test_file
echo -e "\n跳过表头,打印年龄大于 30 的行:"
awk 'NR > 1 && $2 > 30 {print $1, "is", $2, "years old"}' $test_file
echo -e "\n计算平均年龄:"
awk 'NR > 1 {sum += $2; count++} END {print "平均年龄:", sum/count}' $test_file
echo -e "\n格式化输出:"
awk 'NR == 1 {printf "%-10s %-5s %-10s\n", $1, $2, $3}
NR > 1 {printf "%-10s %-5d %-10s\n", $1, $2, $3}' $test_file
# 清理测试文件
rm -f $test_file
十、企业级实战案例
10.1 系统监控脚本
bash
#!/bin/bash
# 系统监控脚本,定期检查系统资源使用情况
# 配置
THRESHOLD_CPU=80 # CPU 使用率阈值(%)
THRESHOLD_MEM=80 # 内存使用率阈值(%)
THRESHOLD_DISK=80 # 磁盘使用率阈值(%)
LOG_FILE="/var/log/system_monitor.log"
CHECK_INTERVAL=60 # 检查间隔(秒)
# 日志记录函数
log() {
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] $1" >> $LOG_FILE
}
# 检查 CPU 使用率
check_cpu() {
local cpu_usage=$(top -b -n 1 | grep "%Cpu(s)" | awk '{print $2 + $4}')
local cpu_usage_int=$(printf "%.0f" $cpu_usage)
if [ $cpu_usage_int -ge $THRESHOLD_CPU ]; then
local message="CPU 使用率过高: $cpu_usage_int%"
echo $message
log $message
# 可以在这里添加报警逻辑,如发送邮件
else
echo "CPU 使用率正常: $cpu_usage_int%"
fi
}
# 检查内存使用率
check_memory() {
local mem_usage=$(free | grep Mem | awk '{print $3/$2 * 100.0}')
local mem_usage_int=$(printf "%.0f" $mem_usage)
if [ $mem_usage_int -ge $THRESHOLD_MEM ]; then
local message="内存使用率过高: $mem_usage_int%"
echo $message
log $message
# 可以在这里添加报警逻辑,如发送邮件
else
echo "内存使用率正常: $mem_usage_int%"
fi
}
# 检查磁盘使用率
check_disk() {
local disk_usage=$(df -h / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ $disk_usage -ge $THRESHOLD_DISK ]; then
local message="根目录磁盘使用率过高: $disk_usage%"
echo $message
log $message
# 可以在这里添加报警逻辑,如发送邮件
else
echo "根目录磁盘使用率正常: $disk_usage%"
fi
}
# 检查系统负载
check_load() {
local load_avg=$(uptime | awk -F 'load average: ' '{print $2}' | cut -d',' -f1)
local cpu_cores=$(grep -c ^processor /proc/cpuinfo)
echo "系统负载平均值(1分钟): $load_avg (CPU核心数: $cpu_cores)"
}
# 主函数
main() {
echo "=== 系统监控报告 $(date "+%Y-%m-%d %H:%M:%S") ==="
check_cpu
check_memory
check_disk
check_load
echo "==========================================="
echo
}
# 如果脚本带有参数 "once",则只运行一次
if [ "$1" = "once" ]; then
main
else
# 否则循环运行
while true; do
main
sleep $CHECK_INTERVAL
done
fi
10.2 日志分析脚本
bash
#!/bin/bash
# Nginx 访问日志分析脚本
# 配置
LOG_FILE="/var/log/nginx/access.log"
TOP_N=10 # 显示前 N 条记录
# 检查日志文件是否存在
if [ ! -f "$LOG_FILE" ]; then
echo "错误: 日志文件 $LOG_FILE 不存在"
exit 1
fi
# 显示菜单
show_menu() {
echo "Nginx 日志分析工具"
echo "1. 访问量最高的 IP 地址"
echo "2. 访问量最高的 URL"
echo "3. 访问量最高的 HTTP 状态码"
echo "4. 访问量最高的用户代理(UA)"
echo "5. 按小时统计访问量"
echo "6. 退出"
echo -n "请选择(1-6): "
}
# 分析访问量最高的 IP
analyze_top_ips() {
echo -e "\n访问量最高的 $TOP_N 个 IP 地址:"
awk '{print $1}' $LOG_FILE | sort | uniq -c | sort -nr | head -n $TOP_N
}
# 分析访问量最高的 URL
analyze_top_urls() {
echo -e "\n访问量最高的 $TOP_N 个 URL:"
awk '{print $7}' $LOG_FILE | sort | uniq -c | sort -nr | head -n $TOP_N
}
# 分析访问量最高的状态码
analyze_top_status() {
echo -e "\n访问量最高的 $TOP_N 个 HTTP 状态码:"
awk '{print $9}' $LOG_FILE | sort | uniq -c | sort -nr | head -n $TOP_N
}
# 分析访问量最高的用户代理
analyze_top_ua() {
echo -e "\n访问量最高的 $TOP_N 个用户代理:"
awk -F'"' '{print $6}' $LOG_FILE | sort | uniq -c | sort -nr | head -n $TOP_N
}
# 按小时统计访问量
analyze_hourly() {
echo -e "\n按小时统计访问量:"
awk '{print substr($4, 14, 5)}' $LOG_FILE | sort | uniq -c | sort -n
}
# 主循环
while true; do
show_menu
read choice
case $choice in
1)
analyze_top_ips
;;
2)
analyze_top_urls
;;
3)
analyze_top_status
;;
4)
analyze_top_ua
;;
5)
analyze_hourly
;;
6)
echo "退出程序"
exit 0
;;
*)
echo "无效选择,请重新输入"
;;
esac
echo -e "\n按 Enter 键继续..."
read
clear
done
10.3 自动备份脚本
bash
#!/bin/bash
# 自动备份脚本,支持文件和数据库备份
# 配置
BACKUP_DIR="/backup" # 备份存储目录
SOURCE_DIRS="/etc /var/www" # 需要备份的目录,用空格分隔
DB_NAME="mydatabase" # 数据库名称
DB_USER="backupuser" # 数据库用户名
DB_PASS="backuppassword" # 数据库密码
RETENTION_DAYS=7 # 备份保留天数
DATE=$(date +%Y%m%d_%H%M%S) # 当前日期时间
BACKUP_FILE="$BACKUP_DIR/full_backup_$DATE.tar.gz" # 备份文件名
# 确保备份目录存在
mkdir -p $BACKUP_DIR
# 日志函数
log() {
echo "[$(date +%Y-%m-%d %H:%M:%S)] $1"
}
# 检查依赖命令
check_dependencies() {
local dependencies=("tar" "mysqldump" "gzip")
for dep in "${dependencies[@]}"; do
if ! command -v $dep >/dev/null 2>&1; then
log "错误: 未找到命令 $dep,请安装后再运行"
exit 1
fi
done
}
# 备份文件
backup_files() {
log "开始备份文件..."
local temp_dir=$(mktemp -d)
# 复制要备份的目录
for dir in $SOURCE_DIRS; do
if [ -d "$dir" ]; then
mkdir -p $temp_dir$(dirname $dir)
cp -a $dir $temp_dir$dir
log "已备份目录: $dir"
else
log "警告: 目录 $dir 不存在,跳过备份"
fi
done
# 备份数据库
log "开始备份数据库 $DB_NAME..."
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME > $temp_dir/${DB_NAME}_backup.sql
if [ $? -eq 0 ]; then
log "数据库备份成功"
else
log "警告: 数据库备份失败"
fi
# 打包备份文件
log "正在打包备份文件..."
tar -zcf $BACKUP_FILE -C $temp_dir .
# 清理临时目录
rm -rf $temp_dir
if [ -f $BACKUP_FILE ]; then
log "备份文件已创建: $BACKUP_FILE"
log "备份大小: $(du -h $BACKUP_FILE | awk '{print $1}')"
return 0
else
log "错误: 备份文件创建失败"
return 1
fi
}
# 清理旧备份
cleanup_old_backups() {
log "开始清理 $RETENTION_DAYS 天前的旧备份..."
find $BACKUP_DIR -name "full_backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete
log "旧备份清理完成"
}
# 主函数
main() {
log "===== 开始执行自动备份 ====="
check_dependencies
if backup_files; then
cleanup_old_backups
log "===== 自动备份执行完成 ====="
exit 0
else
log "===== 自动备份执行失败 ====="
exit 1
fi
}
# 执行主函数
main
十一、Shell 脚本调试与优化
11.1 脚本调试方法
bash
#!/bin/bash
# 调试方法 1: 使用 -x 选项执行脚本,显示执行的每一行命令
# bash -x script.sh
# 调试方法 2: 在脚本中使用 set -x 和 set +x 控制调试范围
# set -x # 开始调试
# 要调试的代码
# set +x # 结束调试
# 调试方法 3: 使用 -n 选项检查脚本语法错误
# bash -n script.sh
# 示例脚本
name="John"
age=30
echo "姓名: $name"
echo "年龄: $age"
if [ $age -ge 18 ]; then
echo "已成年"
else
echo "未成年"
fi
11.2 脚本优化技巧
- 减少子进程创建 :尽量使用 Shell 内置命令,减少外部命令调用
2.优化循环 :避免在循环中执行耗时操作
-
使用管道替代临时文件
-
适当使用变量缓存结果 :避免重复计算
-
合并命令 **:减少进程创建开销
bash
#!/bin/bash
# 优化前
start_time=$(date +%s)
for i in {1..1000}; do
# 每次循环都创建新进程
echo $i >> numbers.txt
done
end_time=$(date +%s)
echo "优化前耗时: $((end_time - start_time)) 秒"
rm numbers.txt
# 优化后
start_time=$(date +%s)
# 只创建一个进程
{
for i in {1..1000}; do
echo $i
done
} >> numbers.txt
end_time=$(date +%s)
echo "优化后耗时: $((end_time - start_time)) 秒"
rm numbers.txt
十二、Shell 脚本最佳实践
1.** 脚本开头指定解释器 :#!/bin/bash
2.添加脚本描述信息 :包括功能、作者、版本等
3.检查脚本所需依赖 :确保必要的命令和工具存在
4.处理错误情况 :使用 set -e
自动退出错误,或显式检查错误
5.使用函数组织代码 :提高可读性和复用性
6.变量引用加引号 :防止空格等特殊字符引起的问题
7.使用局部变量 :函数内部变量使用 local
声明
8.避免使用硬编码 :配置参数集中定义,便于修改
9.添加注释 :解释复杂逻辑和关键步骤
- 进行边界测试 **:确保脚本在异常情况下的稳定性
十三、总结与进阶学习
Shell 脚本编程是 Linux 系统管理和自动化运维的重要技能,要进一步提升 Shell 脚本编程能力,可以:
- 阅读系统自带的 Shell 脚本(如
/etc/init.d/
目录下的服务脚本) - 学习更复杂的文本处理和正则表达式
- 掌握 Shell 脚本的调试和性能优化技巧
- 了解不同 Shell(如 zsh)的特性和扩展功能