CentOS Stream 9 中的 Shell 编程 ---语法详解与实战案例
一、Shell 编程概述
Shell 是操作系统的命令解释器,Shell 脚本是将多个 Shell 命令按逻辑组合成的可执行文件。
1.1 Shell 脚本概述
- 本质:文本文件,包含一系列命令
- 作用:自动化任务、系统管理、批量处理
- 常见 Shell:
bash(最常用)、sh、zsh、ksh - CentOS Stream 9 默认使用
bash
1.2 运行 Shell 脚本的几种方法
✅ 方法 1:赋予执行权限 + 直接运行(推荐)
bash
chmod +x script.sh
./script.sh
✅ 方法 2:使用解释器显式调用
bash
bash script.sh
sh script.sh
✅ 方法 3:使用 source 或 .(在当前 Shell 环境执行)
bash
source script.sh
. script.sh
⚠️ 区别:
./script.sh:新开子进程,变量不继承回父 Shellsource script.sh:在当前 Shell 执行,变量可保留
✅ 案例:对比不同执行方式对变量的影响
bash
#!/bin/bash
# 文件名:test_exec.sh
echo "当前脚本 PID:$$"
MY_VAR="Hello from script"
export MY_VAR
echo "脚本内变量:$MY_VAR"
bash
# 终端执行:
chmod +x test_exec.sh
# 方式1:子进程执行 → 变量不会影响当前终端
./test_exec.sh
echo "终端变量:$MY_VAR" # 输出空
# 方式2:source 执行 → 变量保留
source test_exec.sh
echo "终端变量:$MY_VAR" # 输出:Hello from script
二、Shell 语法基础
2.1 变量类型
Shell 中变量无需声明类型,默认为字符串,支持:
- 局部变量:仅在当前脚本/函数中有效
- 环境变量 :通过
export导出,子进程可继承 - 位置参数 :
$0(脚本名)、$1、$2...(参数) - 特殊变量 :
$?(上条命令返回值)、$$(当前 PID)、$#(参数个数)
2.2 变量定义和访问
➤ 定义变量
bash
name="Alice"
age=25
PI=3.14159
⚠️ 注意:
- 等号
=两边不能有空格- 变量名区分大小写
- 首字符不能是数字
➤ 访问变量
bash
echo $name
echo ${name} # 推荐使用花括号,避免歧义
➤ 变量赋值技巧
bash
# 默认值(变量未设置时使用)
echo ${USER_NAME:-"Guest"}
# 设置默认值(若未设置,则赋值)
echo ${USER_NAME:="DefaultUser"}
# 错误提示(若未设置则报错退出)
echo ${USER_NAME:?"USER_NAME 未定义!"}
# 删除匹配前缀/后缀
filename="report.txt"
echo ${filename%.txt} # → report
echo ${filename#report.} # → txt
✅ 案例:安全获取用户输入并设置默认值
bash
#!/bin/bash
read -p "请输入用户名(默认 admin): " INPUT_USER
USERNAME=${INPUT_USER:-"admin"} # 若为空则用 admin
echo "最终用户名:$USERNAME"
2.3 引号的使用
| 引号类型 | 说明 | 示例 |
|---|---|---|
单引号 ' |
原样输出,不解析变量和命令 | echo '$HOME' → $HOME |
双引号 " |
解析变量和命令 | echo "$HOME" → /home/alice |
| 反引号 ````` | 命令替换(旧式,不推荐) | echo "当前目录:pwd" |
$() |
命令替换(推荐) | echo "当前目录:$(pwd)" |
✅ 案例:引号使用对比
bash
#!/bin/bash
name="Alice"
today=$(date +%Y-%m-%d)
echo 'Hello $name, today is $today' # → Hello $name, today is $today
echo "Hello $name, today is $today" # → Hello Alice, today is 2025-09-16
echo "当前用户:$(whoami)" # → 当前用户:alice
2.4 命令替换
将命令的输出赋值给变量。
➤ 语法
bash
output=$(command)
# 或(旧式,不推荐)
output=`command`
✅ 案例:获取系统信息并格式化输出
bash
#!/bin/bash
HOSTNAME=$(hostname)
KERNEL=$(uname -r)
UPTIME=$(uptime -p)
echo "🖥️ 主机名:$HOSTNAME"
echo "🐧 内核版本:$KERNEL"
echo "⏱️ 运行时间:$UPTIME"
2.5 输入
➤ read 命令读取用户输入
bash
read variable
read -p "提示信息: " variable
read -s variable # 静默输入(密码)
read -t 5 variable # 5秒超时
✅ 案例:带超时和默认值的输入
bash
#!/bin/bash
echo "⏳ 请在5秒内输入文件名(默认:backup.tar.gz):"
read -t 5 -p "> " FILENAME
if [ -z "$FILENAME" ]; then
FILENAME="backup.tar.gz"
echo "⏰ 超时,使用默认值:$FILENAME"
else
echo "✅ 输入文件名:$FILENAME"
fi
2.6 输出
➤ echo 输出
bash
echo "Hello"
echo -e "第一行\n第二行" # 启用转义
echo -n "不换行" # 不换行
➤ printf 格式化输出(推荐用于复杂格式)
bash
printf "姓名:%s,年龄:%d\n" "Alice" 25
✅ 案例:格式化输出磁盘使用率
bash
#!/bin/bash
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
printf "📊 根分区使用率:%3d%%\n" "$DISK_USAGE"
if [ "$DISK_USAGE" -gt 80 ]; then
printf "\033[31m⚠️ 磁盘空间紧张!\033[0m\n"
elif [ "$DISK_USAGE" -gt 60 ]; then
printf "\033[33m🔶 磁盘使用中等\033[0m\n"
else
printf "\033[32m✅ 磁盘空间充足\033[0m\n"
fi
2.7 数组
➤ 索引数组(从0开始)
bash
# 定义
fruits=("apple" "banana" "cherry")
# 访问
echo ${fruits[0]} # apple
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 元素个数
# 遍历
for fruit in "${fruits[@]}"; do
echo "水果:$fruit"
done
➤ 关联数组(需 declare -A)
bash
declare -A user
user[name]="Alice"
user[age]=25
user[email]="alice@example.com"
echo "姓名:${user[name]}"
echo "邮箱:${user[email]}"
# 遍历键值对
for key in "${!user[@]}"; do
echo "$key: ${user[$key]}"
done
✅ 案例:使用关联数组存储服务器信息
bash
#!/bin/bash
declare -A SERVERS
SERVERS["web01"]="192.168.1.10"
SERVERS["db01"]="192.168.1.11"
SERVERS["cache01"]="192.168.1.12"
echo "🌐 服务器列表:"
for name in "${!SERVERS[@]}"; do
printf "%-10s → %s\n" "$name" "${SERVERS[$name]}"
done
三、表达式
Shell 中表达式主要用于条件判断,使用 test 命令或 [ ]、[[ ]]。
四、Shell 控制结构
4.1 分支结构:if 语句
➤ 基本语法
bash
if [ condition ]; then
commands
elif [ condition ]; then
commands
else
commands
fi
➤ 使用 [[ ]](推荐,功能更强)
bash
if [[ $var == "yes" ]]; then
echo "确认"
fi
✅ 案例:判断用户输入是否为数字
bash
#!/bin/bash
read -p "请输入一个数字: " INPUT
# 使用正则判断是否为数字
if [[ $INPUT =~ ^[0-9]+$ ]]; then
echo "✅ $INPUT 是有效数字"
else
echo "❌ $INPUT 不是数字"
fi
4.2 循环结构:for 语句
➤ 列表循环
bash
for item in item1 item2 item3; do
echo $item
done
➤ C 风格循环
bash
for ((i=1; i<=5; i++)); do
echo "第 $i 次循环"
done
➤ 遍历文件
bash
for file in *.txt; do
echo "处理文件:$file"
done
✅ 案例:批量重命名 .log 文件
bash
#!/bin/bash
i=1
for file in *.log; do
if [ -f "$file" ]; then
newname="log_$(printf "%03d" $i).log"
mv "$file" "$newname"
echo "重命名:$file → $newname"
((i++))
fi
done
4.3 循环结构:while 和 until 语句
➤ while 循环(条件为真时执行)
bash
count=1
while [ $count -le 5 ]; do
echo "计数:$count"
((count++))
done
➤ until 循环(条件为假时执行)
bash
count=1
until [ $count -gt 5 ]; do
echo "计数:$count"
((count++))
done
✅ 案例:猜数字游戏
bash
#!/bin/bash
target=$((RANDOM % 100 + 1))
guess=0
echo "🎮 猜数字游戏(1-100)"
while [ $guess -ne $target ]; do
read -p "请输入你的猜测: " guess
if [ $guess -lt $target ]; then
echo "太小了!"
elif [ $guess -gt $target ]; then
echo "太大了!"
else
echo "🎉 恭喜你,猜对了!答案是 $target"
fi
done
五、Shell 函数
5.1 函数的定义
bash
function_name() {
commands
}
# 或
function function_name {
commands
}
5.2 函数调用与参数传递
函数内使用 $1, $2, ... 获取参数,$@ 获取所有参数。
✅ 案例:带参数的问候函数
bash
#!/bin/bash
greet() {
local name=${1:-"Guest"} # 局部变量 + 默认值
local time=$(date +%H)
local msg=""
if [ $time -lt 12 ]; then
msg="早上好"
elif [ $time -lt 18 ]; then
msg="下午好"
else
msg="晚上好"
fi
echo "👋 $msg, $name!"
}
# 调用
greet "Alice"
greet "Bob"
greet # 使用默认值
5.3 函数的返回值
- 使用
return返回整数(0-255),通常 0 表示成功 - 使用
echo输出字符串,调用时用$()捕获
✅ 案例:计算两数之和(两种返回方式)
bash
#!/bin/bash
# 方式1:return 返回状态码(不推荐用于数值)
add_return() {
local sum=$(( $1 + $2 ))
return $sum # ❌ 错误!只能返回 0-255,且用于状态
}
# 方式2:echo 输出结果(推荐)
add_echo() {
echo $(($1 + $2))
}
# 调用
result=$(add_echo 15 25)
echo "15 + 25 = $result"
# 错误示范(超过255会溢出)
add_return 200 100
echo "返回值:$?" # 输出 44(300 % 256 = 44)
✅ 正确做法:用 echo 返回值,用 $? 返回状态
bash
safe_divide() {
if [ $2 -eq 0 ]; then
echo "错误:除数不能为零" >&2
return 1
else
echo $(($1 / $2))
return 0
fi
}
result=$(safe_divide 10 2)
if [ $? -eq 0 ]; then
echo "结果:$result"
else
echo "计算失败"
fi
六、Shell 进阶
6.1 test 命令及其别名
test 命令用于条件判断,等价于 [ ]。
bash
test -f file && echo "存在"
[ -f file ] && echo "存在"
[[ ]] 是 bash 扩展,支持更多功能(模式匹配、逻辑组合等)。
6.2 数值比较运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
-eq |
等于 | [ $a -eq $b ] |
-ne |
不等于 | [ $a -ne $b ] |
-lt |
小于 | [ $a -lt $b ] |
-le |
小于等于 | [ $a -le $b ] |
-gt |
大于 | [ $a -gt $b ] |
-ge |
大于等于 | [ $a -ge $b ] |
✅ 案例:成绩等级判断
bash
#!/bin/bash
read -p "请输入成绩(0-100): " score
if [ $score -ge 90 ]; then
grade="A"
elif [ $score -ge 80 ]; then
grade="B"
elif [ $score -ge 70 ]; then
grade="C"
elif [ $score -ge 60 ]; then
grade="D"
else
grade="F"
fi
echo "成绩等级:$grade"
6.3 逻辑运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
! |
非 | [ ! -f file ] |
-a |
与(旧) | [ $a -gt 0 -a $a -lt 10 ] |
-o |
或(旧) | [ $a -eq 1 -o $a -eq 2 ] |
&& |
与(推荐) | [[ $a -gt 0 && $a -lt 10 ]] |
| ` | ` |
✅ 案例:验证用户名和密码
bash
#!/bin/bash
read -p "用户名: " USER
read -s -p "密码: " PASS
echo
if [[ "$USER" == "admin" && "$PASS" == "secret123" ]]; then
echo -e "\n✅ 登录成功!"
else
echo -e "\n❌ 用户名或密码错误!"
fi
6.4 字符串比较和检测运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
= 或 == |
等于 | [[ $str1 == $str2 ]] |
!= |
不等于 | [[ $str1 != $str2 ]] |
-z |
字符串长度为0 | [[ -z $str ]] |
-n |
字符串长度非0 | [[ -n $str ]] |
=~ |
正则匹配(仅[[) | [[ $email =~ @ ]] |
✅ 案例:邮箱格式验证
bash
#!/bin/bash
read -p "请输入邮箱: " email
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "✅ 邮箱格式正确"
else
echo "❌ 邮箱格式错误"
fi
6.5 文件测试运算符
| 运算符 | 说明 | 示例 |
|---|---|---|
-e |
文件存在 | [ -e file ] |
-f |
普通文件 | [ -f file ] |
-d |
目录 | [ -d dir ] |
-r |
可读 | [ -r file ] |
-w |
可写 | [ -w file ] |
-x |
可执行 | [ -x file ] |
-s |
文件非空 | [ -s file ] |
-nt |
file1 比 file2 新 | [ file1 -nt file2 ] |
✅ 案例:安全文件备份脚本
bash
#!/bin/bash
SOURCE="/etc/nginx/nginx.conf"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/nginx.conf.$DATE"
# 检查源文件
if [ ! -f "$SOURCE" ]; then
echo "❌ 源文件不存在:$SOURCE"
exit 1
fi
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 检查备份目录可写
if [ ! -w "$BACKUP_DIR" ]; then
echo "❌ 备份目录不可写:$BACKUP_DIR"
exit 1
fi
# 执行备份
cp "$SOURCE" "$BACKUP_FILE"
if [ $? -eq 0 ]; then
echo "✅ 备份成功:$BACKUP_FILE"
ls -l "$BACKUP_FILE"
else
echo "❌ 备份失败!"
fi
七、综合案例:自动化任务初探索
🎯 综合案例 1:自动化备份脚本(带日志和清理)
功能:备份指定目录,保留最近7天备份,记录日志
bash
#!/bin/bash
# 文件名:auto_backup.sh
# 功能:自动化备份 + 日志 + 清理旧备份
# ===== 配置 =====
SOURCE_DIR="/var/www/html"
BACKUP_DIR="/backup"
RETENTION_DAYS=7
LOG_FILE="/var/log/backup.log"
# ===== 函数定义 =====
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
create_backup() {
local date_stamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$BACKUP_DIR/backup_${date_stamp}.tar.gz"
log_message "开始备份:$SOURCE_DIR → $backup_file"
# 创建备份
tar -czf "$backup_file" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")" 2>/dev/null
if [ $? -eq 0 ]; then
log_message "✅ 备份成功:$(du -h "$backup_file" | cut -f1)"
return 0
else
log_message "❌ 备份失败!"
return 1
fi
}
cleanup_old_backups() {
log_message "清理 $RETENTION_DAYS 天前的备份..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | \
while read file; do
log_message "🗑️ 删除旧备份:$file"
done
}
# ===== 主程序 =====
log_message "=== 备份任务开始 ==="
# 检查目录
mkdir -p "$BACKUP_DIR"
if [ ! -d "$SOURCE_DIR" ]; then
log_message "❌ 源目录不存在:$SOURCE_DIR"
exit 1
fi
# 执行备份
create_backup
if [ $? -ne 0 ]; then
exit 1
fi
# 清理旧备份
cleanup_old_backups
log_message "=== 备份任务完成 ==="
echo
💡 使用方法:
bash
chmod +x auto_backup.sh
sudo ./auto_backup.sh
# 可添加到 crontab 定时执行
🎯 综合案例 2:批量用户管理脚本
功能:从文件读取用户名,批量创建/删除用户,设置密码
bash
#!/bin/bash
# 文件名:batch_user_manager.sh
# 用法:./batch_user_manager.sh create users.txt
# ./batch_user_manager.sh delete users.txt
ACTION=$1
USER_FILE=$2
if [ $# -ne 2 ]; then
echo "用法:$0 {create|delete} 用户文件"
echo "用户文件格式:每行一个用户名"
exit 1
fi
if [ ! -f "$USER_FILE" ]; then
echo "❌ 用户文件不存在:$USER_FILE"
exit 1
fi
create_user() {
local username=$1
if id "$username" >/dev/null 2>&1; then
echo "⚠️ 用户已存在:$username"
else
useradd -m -s /bin/bash "$username"
echo "$username:DefaultPass123!" | chpasswd
echo "✅ 创建用户:$username"
fi
}
delete_user() {
local username=$1
if id "$username" >/dev/null 2>&1; then
userdel -r "$username" 2>/dev/null
echo "🗑️ 删除用户:$username"
else
echo "⚠️ 用户不存在:$username"
fi
}
# 主循环
while IFS= read -r username; do
# 跳过空行和注释
[[ -z "$username" || "$username" =~ ^# ]] && continue
case $ACTION in
create)
create_user "$username"
;;
delete)
delete_user "$username"
;;
*)
echo "❌ 未知操作:$ACTION"
exit 1
;;
esac
done < "$USER_FILE"
echo "🎉 批量用户操作完成!"
💡 用户文件示例(users.txt):
alice
bob
charlie
# 这是注释
david
💡 使用:
bash
chmod +x batch_user_manager.sh
sudo ./batch_user_manager.sh create users.txt
sudo ./batch_user_manager.sh delete users.txt
🎯 综合案例 3:日志分析与告警脚本
功能:分析 Nginx 访问日志,统计 IP 访问次数,对高频 IP 发出警告
bash
#!/bin/bash
# 文件名:log_analyzer.sh
# 功能:分析日志,统计IP访问量,对异常IP告警
LOG_FILE=${1:-"/var/log/nginx/access.log"}
THRESHOLD=${2:-100} # 默认阈值:100次
REPORT_FILE="/tmp/ip_report_$(date +%Y%m%d).txt"
echo "📊 分析日志文件:$LOG_FILE"
echo "🚨 告警阈值:$THRESHOLD 次访问"
# 检查日志文件
if [ ! -f "$LOG_FILE" ]; then
echo "❌ 日志文件不存在:$LOG_FILE"
exit 1
fi
# 提取IP并统计
echo "⏳ 正在统计IP访问次数..."
awk '{print $1}' "$LOG_FILE" | \
sort | \
uniq -c | \
sort -nr > "$REPORT_FILE"
echo "✅ 统计完成,报告保存至:$REPORT_FILE"
echo
# 显示前10名
echo "🔝 访问量前10的IP:"
head -10 "$REPORT_FILE"
echo
# 检查异常IP
echo "🔍 检查异常访问IP(> $THRESHOLD 次):"
while read count ip; do
if [ "$count" -gt "$THRESHOLD" ]; then
echo -e "\033[31m⚠️ 告警:IP $ip 访问 $count 次\033[0m"
# 可在此处添加邮件告警或防火墙封禁
# sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="$ip" drop'
fi
done < "$REPORT_FILE"
echo
echo "📈 分析完成!"
💡 使用:
bash
chmod +x log_analyzer.sh
sudo ./log_analyzer.sh /var/log/nginx/access.log 50
✅ Shell 编程最佳实践
- 脚本开头 :
#!/bin/bash明确解释器 - 变量引用 :始终使用
"${var}"避免空格问题 - 条件判断 :优先使用
[[ ]]而非[ ] - 错误处理 :检查命令返回值
$? - 函数封装:提高代码复用性
- 日志记录:重要操作记录日志
- 权限检查:执行前验证文件/目录权限
- 参数验证:检查参数个数和合法性
- 临时文件 :使用
mktemp创建安全临时文件 - 退出状态:脚本结束返回适当状态码(0=成功)
📚 附录:Shell 速查表
| 类别 | 语法/命令 | 说明 |
|---|---|---|
| 变量 | name="value" |
定义变量 |
echo "$name" |
使用变量 | |
${name:-default} |
默认值 | |
| 输入 | read -p "提示" var |
读取用户输入 |
| 输出 | echo -e "Hello\nWorld" |
启用转义 |
printf "%s %d\n" "Name" 25 |
格式化输出 | |
| 数组 | arr=("a" "b" "c") |
索引数组 |
declare -A map; map[key]="value" |
关联数组 | |
| 条件判断 | if [[ $a -eq $b ]]; then ... fi |
if 语句 |
| 循环 | for i in {1..5}; do ... done |
for 循环 |
while [[ cond ]]; do ... done |
while 循环 | |
| 函数 | func() { ... } |
定义函数 |
result=$(func arg) |
获取函数返回值 | |
| 文件测试 | [ -f file ] |
文件存在且为普通文件 |
| 数值比较 | [ $a -gt $b ] |
大于 |
| 字符串 | [[ $str == "hello" ]] |
字符串相等 |
| 正则 | [[ $email =~ @ ]] |
正则匹配 |
这份文档覆盖了 CentOS Stream 9 Shell 编程的全部核心知识点 + 语法细节 + 实用案例 + 综合项目,所有代码均含详细注释,可直接用于教学、自学或生产环境参考。