CentOS Stream 9入门学习教程,从入门到精通,CentOS Stream 9 中的 Shell 编程 —语法详解与实战案例(11)

CentOS Stream 9 中的 Shell 编程 ---语法详解与实战案例


一、Shell 编程概述

Shell 是操作系统的命令解释器,Shell 脚本是将多个 Shell 命令按逻辑组合成的可执行文件。

1.1 Shell 脚本概述

  • 本质:文本文件,包含一系列命令
  • 作用:自动化任务、系统管理、批量处理
  • 常见 Shell:bash(最常用)、shzshksh
  • 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:新开子进程,变量不继承回父 Shell
  • source 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 编程最佳实践

  1. 脚本开头#!/bin/bash 明确解释器
  2. 变量引用 :始终使用 "${var}" 避免空格问题
  3. 条件判断 :优先使用 [[ ]] 而非 [ ]
  4. 错误处理 :检查命令返回值 $?
  5. 函数封装:提高代码复用性
  6. 日志记录:重要操作记录日志
  7. 权限检查:执行前验证文件/目录权限
  8. 参数验证:检查参数个数和合法性
  9. 临时文件 :使用 mktemp 创建安全临时文件
  10. 退出状态:脚本结束返回适当状态码(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 编程的全部核心知识点 + 语法细节 + 实用案例 + 综合项目,所有代码均含详细注释,可直接用于教学、自学或生产环境参考。

相关推荐
胡图图不糊涂^_^40 分钟前
测试BUG篇
学习·bug·测试
一起逃去看海吧1 小时前
dify-03
java·linux·开发语言
fengyehongWorld1 小时前
Linux 根据端口进行的相关查询
linux
lihao lihao1 小时前
linux匿名管道
linux·运维·服务器
うちは止水1 小时前
weston出图调试
linux·wayland·weston
STDD1 小时前
Farming Simulator 25(模拟农场 25) Linux 专服搭建完全指南
linux·运维·javascript
好好风格2 小时前
宝塔面板 HTTPS 端口证书不生效排查记录
linux·运维·nginx
用户2367829801682 小时前
Linux pgrep 命令详解:按名称查找进程 PID 的高效方法
linux
zzipeng2 小时前
Linux LCD驱动
linux·运维·服务器