跳转至

Shell 脚本编程

Shell脚本编程

📌 学习时间:7-10天(重点章节) 📌 难度级别:⭐⭐⭐ 中级 📌 前置知识:Linux基础命令、文本处理


📚 章节概述

Shell 脚本编程是 Linux 系统管理和自动化运维的核心技能。本章全面讲解 Shell 类型、变量系统、条件判断、循环结构、函数、数组、字符串操作、命令替换、算术运算和调试技巧,并提供 15 个实用脚本示例。

🎯 学习目标

  • 了解常见 Shell 类型(bash/zsh/fish)
  • 掌握变量系统(局部/全局/环境变量)
  • 熟练使用条件判断(if/case/test)
  • 掌握循环结构(for/while/until)
  • 掌握函数定义、参数传递和返回值
  • 掌握数组和字符串操作
  • 能使用调试技巧排查脚本问题
  • 完成15个实用脚本示例

📖 1. Shell 类型

1.1 常见 Shell

Bash
# 查看系统可用 Shell
cat /etc/shells
# /bin/sh
# /bin/bash
# /bin/zsh
# /usr/bin/fish

# 查看当前使用的 Shell
echo $SHELL                  # 登录 Shell
echo $0                      # 当前 Shell

# 切换 Shell
chsh -s /bin/zsh             # 修改登录 Shell(下次登录生效)

Bash(Bourne Again Shell): - Linux 默认 Shell,最广泛使用 - 兼容 sh,功能丰富 - 本教程主要使用 Bash

Zsh(Z Shell): - Bash 的超集,功能更强大 - 更好的自动补全和主题支持 - macOS 默认 Shell - Oh My Zsh 框架很流行

Fish(Friendly Interactive Shell): - 开箱即用,不需要太多配置 - 语法与 Bash 不兼容 - 更好的语法高亮和建议

1.2 脚本基础

Bash
#!/bin/bash
# 上面一行叫 Shebang,告诉系统用哪个解释器执行

# 脚本注释用 #
# 这是一个注释

# 创建并执行脚本
cat > hello.sh << 'EOF'
#!/bin/bash
echo "Hello, World!"
echo "当前时间: $(date)"
echo "当前用户: $(whoami)"
EOF

# 添加执行权限
chmod +x hello.sh

# 三种执行方式
./hello.sh                   # 方式1:直接执行(需要x权限和shebang)
bash hello.sh                # 方式2:指定解释器
source hello.sh              # 方式3:在当前Shell环境执行(影响当前环境变量)
. hello.sh                   # 方式3的简写

# 脚本参数
# $0  脚本名
# $1-$9  第1-9个参数
# ${10}  第10个及以后的参数
# $#  参数总数
# $@  所有参数(每个参数是独立的字符串)
# $*  所有参数(所有参数作为一个字符串)
# $$  当前脚本的PID
# $?  上一个命令的退出状态(0=成功)
# $!  最后一个后台进程的PID

cat > args_demo.sh << 'SCRIPT'
#!/bin/bash
echo "脚本名: $0"
echo "第1个参数: $1"
echo "第2个参数: $2"
echo "参数个数: $#"
echo "所有参数: $@"
echo "退出状态: $?"
SCRIPT
chmod +x args_demo.sh
./args_demo.sh hello world

📖 2. 变量

2.1 变量基础

Bash
# 变量赋值(= 两边不能有空格!)
name="Alice"                 # 字符串变量
age=25                       # 数值变量(实际上也是字符串)
readonly PI=3.14159          # 只读变量

# 引用变量
echo $name
echo ${name}                 # 推荐使用花括号(避免歧义)
echo "Hello, ${name}!"

# 变量名规则
# - 由字母、数字、下划线组成
# - 不能以数字开头
# - 区分大小写
# - 建议:普通变量小写,常量和环境变量大写

# 删除变量
unset name

2.2 引号的区别

Bash
# 双引号 "" — 弱引用(允许变量替换和命令替换)
name="World"
echo "Hello, $name"          # Hello, World
echo "Today is $(date)"     # Today is Mon Jan 15 ...

# 单引号 '' — 强引用(原样输出,不做任何替换)
echo 'Hello, $name'         # Hello, $name
echo 'Today is $(date)'     # Today is $(date)

# 反引号 `` — 命令替换(推荐使用 $() 代替)
echo `date`                  # 等同于 echo $(date)
echo $(date)                 # 推荐写法(可以嵌套)

# 无引号 — 会进行分词和通配符展开
files=*.txt                  # 不展开
echo $files                  # 展开为匹配的文件名
echo "$files"                # 原样输出 *.txt

2.3 环境变量

Bash
# 环境变量 — 全局可见,子进程继承
export MY_VAR="global"       # 设置环境变量
env                          # 查看所有环境变量
printenv MY_VAR              # 查看特定环境变量

# 常用环境变量
echo $HOME                   # 家目录
echo $USER                   # 当前用户
echo $PATH                   # 命令搜索路径
echo $SHELL                  # 登录 Shell
echo $LANG                   # 语言设置
echo $PWD                    # 当前目录
echo $OLDPWD                 # 上一个目录
echo $HOSTNAME               # 主机名
echo $RANDOM                 # 随机数 (0-32767)
echo $LINENO                 # 当前行号

# 修改 PATH
export PATH="$PATH:/opt/myapp/bin"

# 永久生效
echo 'export PATH="$PATH:/opt/myapp/bin"' >> ~/.bashrc
source ~/.bashrc

# 局部变量 vs 环境变量
local_var="I'm local"        # Shell变量(未export,仅当前Shell可见)
export env_var="I'm global"  # 环境变量(子进程可见)

# 验证
bash -c 'echo $local_var'   # 空(子进程看不到)
bash -c 'echo $env_var'     # I'm global(子进程可见)

2.4 特殊变量

Bash
# 变量默认值操作
${var:-default}              # 如果 var 未设置或为空,返回 default
${var:=default}              # 如果 var 未设置或为空,设置为 default 并返回
${var:+value}                # 如果 var 已设置且非空,返回 value
${var:?error_msg}            # 如果 var 未设置或为空,输出错误信息并退出

# 示例
echo ${username:-"Anonymous"}    # 如果 username 没设置,默认 Anonymous
log_dir=${LOG_DIR:="/var/log"}   # 如果没设置,赋值为 /var/log

📖 3. 条件判断

3.1 test 命令与 [ ]

Bash
# test 命令和 [ ] 是等价的
test -f /etc/passwd          # test 写法
[ -f /etc/passwd ]           # [ ] 写法(注意空格!)

# 文件测试
[ -f file ]       # 文件存在且为普通文件
[ -d dir ]        # 目录存在
[ -e path ]       # 路径存在(文件或目录)
[ -r file ]       # 可读
[ -w file ]       # 可写
[ -x file ]       # 可执行
[ -s file ]       # 文件存在且不为空
[ -L file ]       # 是符号链接
[ file1 -nt file2 ]  # file1 比 file2 新(newer than)
[ file1 -ot file2 ]  # file1 比 file2 旧(older than)

# 字符串测试
[ -z "$str" ]     # 字符串为空
[ -n "$str" ]     # 字符串非空
[ "$a" = "$b" ]   # 字符串相等
[ "$a" != "$b" ]  # 字符串不等

# 数值比较
[ "$a" -eq "$b" ] # 等于 (equal)
[ "$a" -ne "$b" ] # 不等于 (not equal)
[ "$a" -gt "$b" ] # 大于 (greater than)
[ "$a" -ge "$b" ] # 大于等于
[ "$a" -lt "$b" ] # 小于 (less than)
[ "$a" -le "$b" ] # 小于等于

# 逻辑运算
[ cond1 ] && [ cond2 ]   # AND
[ cond1 ] || [ cond2 ]   # OR
[ ! cond ]                # NOT
[ cond1 -a cond2 ]       # AND(在 [ ] 内部)
[ cond1 -o cond2 ]       # OR(在 [ ] 内部)

3.2 [[ ]] — Bash 增强版

Bash
# [[ ]] 是 Bash 扩展,功能更强大
# 优势:支持正则匹配、不需要变量引号、更好的逻辑运算

[[ "$str" == pattern* ]]    # 模式匹配
[[ "$str" =~ regex ]]       # 正则匹配
[[ "$a" > "$b" ]]           # 字符串大于比较
[[ cond1 && cond2 ]]        # AND
[[ cond1 || cond2 ]]        # OR

# 正则匹配示例
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "有效的邮箱地址"
fi

# 模式匹配示例
if [[ "$filename" == *.txt ]]; then
    echo "这是一个文本文件"
fi

3.3 if 语句

Bash
# 基本 if
if [ condition ]; then
    commands
fi

# if-else
if [ condition ]; then
    commands
else
    commands
fi

# if-elif-else
if [ condition1 ]; then
    commands
elif [ condition2 ]; then
    commands
else
    commands
fi

# 实际示例
#!/bin/bash
# 检查文件是否存在
FILE="/etc/nginx/nginx.conf"
if [ -f "$FILE" ]; then
    echo "Nginx 配置文件存在"
    nginx -t
else
    echo "Nginx 配置文件不存在"
    echo "请先安装 Nginx: sudo apt install nginx"
    exit 1
fi

# 检查用户权限
if [ "$(id -u)" -ne 0 ]; then
    echo "错误:此脚本需要 root 权限运行"
    echo "请使用 sudo 执行"
    exit 1
fi

# 检查命令是否存在
if command -v docker &>/dev/null; then
    echo "Docker 已安装: $(docker --version)"
else
    echo "Docker 未安装"
fi

3.4 case 语句

Bash
# case 适合多条件分支
case "$variable" in
    pattern1)
        commands
        ;;
    pattern2|pattern3)
        commands
        ;;
    *)
        default_commands
        ;;
esac

# 实际示例:服务管理脚本
#!/bin/bash
case "$1" in
    start)
        echo "Starting service..."
        systemctl start myapp
        ;;
    stop)
        echo "Stopping service..."
        systemctl stop myapp
        ;;
    restart)
        echo "Restarting service..."
        systemctl restart myapp
        ;;
    status)
        systemctl status myapp
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

# 文件类型判断
case "$filename" in
    *.tar.gz|*.tgz)  tar xzf "$filename" ;;
    *.tar.bz2)       tar xjf "$filename" ;;
    *.tar.xz)        tar xJf "$filename" ;;
    *.zip)           unzip "$filename" ;;
    *.gz)            gunzip "$filename" ;;
    *)               echo "未知的文件格式: $filename" ;;
esac

📖 4. 循环结构

4.1 for 循环

Bash
# C 风格 for
for ((i=1; i<=10; i++)); do
    echo $i
done

# for-in 风格
for item in apple banana cherry; do
    echo "水果: $item"
done

# 遍历文件
for file in /var/log/*.log; do
    echo "日志文件: $file ($(du -sh "$file" | cut -f1))"
done

# 使用序列
for i in {1..10}; do         # 1到10
    echo $i
done

for i in {0..100..5}; do     # 0到100,步长5
    echo $i
done

# 使用 seq
for i in $(seq 1 5); do
    echo $i
done

# 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
    echo "用户: $user"
done

# 遍历数组
servers=("web01" "web02" "db01" "cache01")
for server in "${servers[@]}"; do
    echo "检查服务器: $server"
    ping -c 1 -W 1 "$server" &>/dev/null && echo "  ✅ 在线" || echo "  ❌ 离线"
done

4.2 while 循环

Bash
# 基本 while
count=1
while [ $count -le 5 ]; do
    echo "Count: $count"
    ((count++))
done

# 读取文件(逐行处理)
while IFS= read -r line; do
    echo "行: $line"
done < input.txt

# 读取 CSV
while IFS=, read -r name age city; do
    echo "姓名: $name, 年龄: $age, 城市: $city"
done < data.csv

# 无限循环
while true; do
    echo "$(date): 系统负载 $(uptime | awk -F'load average:' '{print $2}')"
    sleep 60
done

# 等待条件满足
while ! ping -c 1 -W 1 server01 &>/dev/null; do
    echo "等待 server01 上线..."
    sleep 5
done
echo "server01 已上线!"

# 读取管道输入
ps aux | while read -r line; do
    echo "$line" | awk '{if ($3 > 50) print "高CPU进程:", $0}'
done

4.3 until 循环

Bash
# until — 条件为假时循环(与 while 相反)
count=1
until [ $count -gt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# 等待服务启动
until systemctl is-active nginx &>/dev/null; do
    echo "等待 Nginx 启动..."
    sleep 2
done
echo "Nginx 已启动!"

4.4 循环控制

Bash
# break — 退出循环
for i in {1..100}; do
    if [ $i -eq 10 ]; then
        break
    fi
    echo $i
done

# continue — 跳过本次迭代
for i in {1..10}; do
    if [ $((i % 2)) -eq 0 ]; then
        continue           # 跳过偶数
    fi
    echo $i                # 只打印奇数
done

# break N — 退出N层循环
for i in {1..3}; do
    for j in {1..3}; do
        if [ $j -eq 2 ]; then
            break 2        # 退出两层循环
        fi
        echo "$i-$j"
    done
done

📖 5. 函数

5.1 函数定义与调用

Bash
# 函数定义方式
function greet() {
    echo "Hello, $1!"
}

# 或者(更推荐的 POSIX 兼容写法)
greet() {
    echo "Hello, $1!"
}

# 调用函数
greet "Alice"                # Hello, Alice!
greet "Bob"                  # Hello, Bob!

# 函数参数
show_info() {
    echo "函数名: $FUNCNAME"
    echo "参数个数: $#"
    echo "第1个参数: $1"
    echo "第2个参数: $2"
    echo "所有参数: $@"
}
show_info "hello" "world"

5.2 返回值

Bash
# return 返回退出状态码(0-255)
is_root() {
    if [ "$(id -u)" -eq 0 ]; then
        return 0             # 成功
    else
        return 1             # 失败
    fi
}

if is_root; then
    echo "是 root 用户"
else
    echo "不是 root 用户"
fi

# 通过标准输出返回字符串
get_hostname() {
    hostname
}
HOST=$(get_hostname)
echo "主机名: $HOST"

# 返回计算结果
add() {
    echo $(($1 + $2))
}
result=$(add 10 20)
echo "结果: $result"          # 结果: 30

5.3 局部变量

Bash
# 使用 local 声明局部变量
my_func() {
    local name="Alice"       # 局部变量
    global_var="I'm global"  # 全局变量
    echo "函数内: $name"
}

my_func
echo "函数外: $name"          # 空(局部变量不可见)
echo "函数外: $global_var"    # I'm global

5.4 实用函数库

Bash
#!/bin/bash
# 通用函数库 common.sh

# 日志函数
log_info()  { echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_warn()  { echo "[WARN]  $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }

# 错误处理
die() {
    log_error "$*"
    exit 1
}

# 确认操作
confirm() {
    local msg="${1:-确认继续}"
    read -p "$msg [y/N] " response
    [[ "$response" =~ ^[Yy]$ ]]
}

# 检查命令是否存在
require_cmd() {
    command -v "$1" &>/dev/null || die "命令 '$1' 未安装"
}

# 检查 root 权限
require_root() {
    [ "$(id -u)" -eq 0 ] || die "需要 root 权限"
}

# 重试函数
retry() {
    local max_attempts=${1:-3}
    local delay=${2:-5}
    shift 2
    local attempt=1

    while [ $attempt -le $max_attempts ]; do
        "$@" && return 0
        log_warn "第 $attempt 次尝试失败,${delay}秒后重试..."
        sleep $delay
        ((attempt++))
    done
    log_error "已达最大重试次数 ($max_attempts)"
    return 1
}

# 使用方法:
# source common.sh
# require_root
# require_cmd docker
# log_info "开始部署..."
# retry 3 5 curl -s http://example.com

📖 6. 数组

Bash
# 索引数组
fruits=("apple" "banana" "cherry" "date")

# 访问元素
echo ${fruits[0]}            # apple
echo ${fruits[2]}            # cherry
echo ${fruits[-1]}           # date(最后一个)

# 所有元素
echo ${fruits[@]}            # apple banana cherry date
echo ${fruits[*]}            # 同上

# 数组长度
echo ${#fruits[@]}           # 4

# 添加元素
fruits+=("elderberry")
fruits[10]="fig"             # 可以跳跃赋值

# 删除元素
unset fruits[1]              # 删除 banana

# 遍历数组
for fruit in "${fruits[@]}"; do
    echo "水果: $fruit"
done

# 数组切片
echo ${fruits[@]:1:3}        # 从索引1开始取3个

# 关联数组(类似字典/映射,Bash 4.0+)
declare -A config
config[host]="192.168.1.100"
config[port]="8080"
config[user]="admin"

echo ${config[host]}         # 192.168.1.100

# 遍历关联数组
for key in "${!config[@]}"; do
    echo "$key = ${config[$key]}"
done

# 数组实用操作
# 数组反转
arr=(1 2 3 4 5)
for ((i=${#arr[@]}-1; i>=0; i--)); do
    echo "${arr[$i]}"
done

# 数组去重
arr=(1 2 2 3 3 3 4)
unique=($(echo "${arr[@]}" | tr ' ' '\n' | sort -u))
echo "${unique[@]}"          # 1 2 3 4

📖 7. 字符串操作

Bash
str="Hello, World!"

# 字符串长度
echo ${#str}                  # 13

# 子串提取
echo ${str:0:5}               # Hello
echo ${str:7}                 # World!
echo ${str: -6}               # orld!(注意冒号后的空格)

# 字符串替换
echo ${str/World/Linux}       # Hello, Linux!(替换第一个)
echo ${str//l/L}              # HeLLo, WorLd!(替换所有)

# 删除匹配
filename="backup.2024.01.15.tar.gz"
echo ${filename#*.}           # 2024.01.15.tar.gz(删除第一个.及之前)
echo ${filename##*.}          # gz(删除最后一个.及之前 — 获取扩展名)
echo ${filename%.*}           # backup.2024.01.15.tar(删除最后一个.及之后)
echo ${filename%%.*}          # backup(删除第一个.及之后 — 获取基本名)

# 文件路径操作
filepath="/home/alice/documents/report.txt"
echo ${filepath##*/}          # report.txt(获取文件名,类似 basename)
echo ${filepath%/*}           # /home/alice/documents(获取目录,类似 dirname)

# 大小写转换(Bash 4.0+)
str="Hello World"
echo ${str^^}                 # HELLO WORLD(全大写)
echo ${str,,}                 # hello world(全小写)
echo ${str^}                  # Hello world(首字母大写)

# 字符串拼接
first="Hello"
second="World"
result="${first}, ${second}!"
echo $result                  # Hello, World!

📖 8. 算术运算

Bash
# 方式1:双括号(推荐)
echo $((2 + 3))              # 5
echo $((10 / 3))             # 3(整数除法)
echo $((10 % 3))             # 1(取余)
echo $((2 ** 10))            # 1024(幂)

a=10
((a++))                      # a = 11
((a += 5))                   # a = 16
((a *= 2))                   # a = 32

# 条件中使用
if ((a > 20)); then
    echo "$a 大于 20"
fi

# 方式2:let
let "a = 5 + 3"
let "a++"

# 方式3:expr(POSIX兼容,但更慢)
expr 2 + 3                   # 注意操作符两边的空格
result=$(expr 10 \* 5)       # * 需要转义

# 浮点数运算(bash不支持,使用 bc)
echo "scale=2; 10 / 3" | bc  # 3.33
echo "scale=4; sqrt(2)" | bc -l  # 1.4142
result=$(echo "1.5 + 2.3" | bc)
echo $result                 # 3.8

# awk 进行浮点运算
awk 'BEGIN{printf "%.2f\n", 10/3}'  # 3.33

📖 9. 命令替换与进程替换

Bash
# 命令替换 — 将命令输出作为值
today=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)
ip_addr=$(hostname -I | awk '{print $1}')

# 嵌套命令替换
echo "文件数: $(ls -1 $(dirname $(which python3)) | wc -l)"

# 进程替换 — 将命令输出作为文件
# <(command) 创建一个临时文件描述符
diff <(ls dir1) <(ls dir2)           # 比较两个目录内容
comm <(sort file1) <(sort file2)     # 比较两个文件

# >(command) 写入到命令
tee >(grep error > errors.log) >(grep warn > warnings.log) < logfile

📖 10. 调试技巧

Bash
# 方法1:set 选项
set -x          # 开启调试模式(显示每条命令)
set +x          # 关闭调试模式
set -e          # 遇到错误立即退出
set -u          # 使用未定义的变量时报错
set -o pipefail # 管道中任一命令失败则整个管道失败

# 推荐在脚本开头使用
#!/bin/bash
set -euo pipefail

# 方法2:bash -x 执行
bash -x script.sh            # 调试模式执行

# 方法3:部分调试
#!/bin/bash
echo "正常代码"

set -x                       # 开始调试
# 需要调试的代码段
problematic_function
set +x                       # 结束调试

echo "继续正常执行"

# 方法4:trap — 信号捕获
trap 'echo "脚本在第 $LINENO 行退出,状态码: $?"' ERR
trap 'echo "收到中断信号"; cleanup; exit 1' INT TERM
trap 'cleanup' EXIT          # 脚本退出时执行清理

# 完整的调试模板
#!/bin/bash
set -euo pipefail

# 错误处理
trap 'echo "错误发生在第 $LINENO 行" >&2' ERR

# 清理函数
cleanup() {
    echo "执行清理..."
    rm -f /tmp/myapp_*.tmp
}
trap cleanup EXIT

# 日志函数
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

log "脚本开始执行"
# ... 脚本主体 ...
log "脚本执行完成"

📖 11. 15 个实用脚本示例

脚本1:系统信息收集

Bash
#!/bin/bash
# 收集系统基本信息
set -euo pipefail

echo "==============================="
echo "  系统信息报告"
echo "  生成时间: $(date)"
echo "==============================="
echo ""
echo "--- 主机信息 ---"
echo "主机名:     $(hostname)"
echo "系统版本:   $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
echo "内核版本:   $(uname -r)"
echo "运行时间:   $(uptime -p)"
echo ""
echo "--- CPU 信息 ---"
echo "CPU 型号:   $(grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)"
echo "CPU 核数:   $(nproc)"
echo "系统负载:   $(uptime | awk -F'load average:' '{print $2}' | xargs)"
echo ""
echo "--- 内存信息 ---"
# 注意:不能用 free -h 配合 awk 做算术,因为单位可能不一致(如 Gi vs Mi)
free | awk 'NR==2{printf "已用/总计: %.1fGi / %.1fGi (%.1f%%)\n", $3/1048576, $2/1048576, $3/$2*100}'
echo ""
echo "--- 磁盘信息 ---"
df -h | grep '^/dev' | awk '{printf "%-20s %s 已用 / %s 总计 (%s)\n", $6, $3, $2, $5}'
echo ""
echo "--- 网络信息 ---"
echo "IP 地址:    $(hostname -I | awk '{print $1}')"
echo "网关:       $(ip route | grep default | awk '{print $3}')"

脚本2:日志清理

Bash
#!/bin/bash
# 清理指定天数之前的日志文件
set -euo pipefail

LOG_DIR="${1:-/var/log}"
DAYS="${2:-30}"
DRY_RUN="${3:-false}"

echo "日志清理脚本"
echo "目录: $LOG_DIR"
echo "清理 $DAYS 天前的日志"
echo ""

total_size=0
file_count=0

while IFS= read -r -d '' file; do
    size=$(stat -c%s "$file" 2>/dev/null || echo 0)
    total_size=$((total_size + size))
    ((file_count++))

    if [ "$DRY_RUN" = "true" ]; then
        echo "[DRY-RUN] 将删除: $file ($(numfmt --to=iec $size))"
    else
        rm -f "$file"
        echo "已删除: $file ($(numfmt --to=iec $size))"
    fi
done < <(find "$LOG_DIR" -name "*.log" -mtime +"$DAYS" -type f -print0)

echo ""
echo "总计: $file_count 个文件, $(numfmt --to=iec $total_size)"

脚本3:备份脚本

Bash
#!/bin/bash
# 自动备份脚本

set -euo pipefail

BACKUP_DIR="/backup"
SOURCE_DIRS=("/etc" "/home" "/var/www")
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/backup_${DATE}.tar.gz"
KEEP_DAYS=7

log() { echo "[$(date '+%H:%M:%S')] $*"; }

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 执行备份
log "开始备份..."
tar czf "$BACKUP_FILE" "${SOURCE_DIRS[@]}" 2>/dev/null
log "备份完成: $BACKUP_FILE ($(du -sh "$BACKUP_FILE" | cut -f1))"

# 清理旧备份
log "清理 ${KEEP_DAYS} 天前的备份..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +"$KEEP_DAYS" -delete

log "所有操作完成"

脚本4:批量文件重命名

Bash
#!/bin/bash
# 批量重命名文件
set -euo pipefail

if [ $# -lt 3 ]; then
    echo "用法: $0 <目录> <查找模式> <替换内容>"
    echo "示例: $0 ./photos 'IMG_' 'vacation_'"
    exit 1
fi

DIR="$1"
SEARCH="$2"
REPLACE="$3"

count=0
for file in "$DIR"/*"$SEARCH"*; do
    [ -e "$file" ] || continue
    newname="${file//$SEARCH/$REPLACE}"
    if [ "$file" != "$newname" ]; then
        mv -v "$file" "$newname"
        ((count++))
    fi
done

echo "共重命名 $count 个文件"

脚本5:端口扫描

Bash
#!/bin/bash
# 简单的端口扫描器
set -euo pipefail

HOST="${1:-localhost}"
START_PORT="${2:-1}"
END_PORT="${3:-1024}"

echo "扫描 $HOST 的端口 $START_PORT-$END_PORT"
echo ""

for ((port=START_PORT; port<=END_PORT; port++)); do
    (echo >/dev/tcp/"$HOST"/$port) 2>/dev/null && \
        echo "端口 $port 开放" &
done
wait
echo ""
echo "扫描完成"

脚本6:菜单系统

Bash
#!/bin/bash
# 交互式菜单系统
set -uo pipefail
# 注意:交互式长运行脚本不加 -e,避免用户输入异常导致脚本退出

show_menu() {
    clear
    echo "==============================="
    echo "    系统管理工具"
    echo "==============================="
    echo "1. 查看系统信息"
    echo "2. 查看磁盘使用"
    echo "3. 查看内存使用"
    echo "4. 查看登录用户"
    echo "5. 查看网络连接"
    echo "0. 退出"
    echo "==============================="
    read -p "请选择 [0-5]: " choice
}

while true; do
    show_menu
    case $choice in
        1) uname -a; read -p "按回车继续..." ;;
        2) df -h; read -p "按回车继续..." ;;
        3) free -h; read -p "按回车继续..." ;;
        4) w; read -p "按回车继续..." ;;
        5) ss -tuln; read -p "按回车继续..." ;;
        0) echo "再见!"; exit 0 ;;
        *) echo "无效选择"; sleep 1 ;;
    esac
done

脚本7:监控磁盘使用率

Bash
#!/bin/bash
# 磁盘使用率监控,超过阈值发送告警
set -euo pipefail

THRESHOLD=${1:-80}
MAILTO="admin@example.com"

df -h | grep '^/dev' | while read -r line; do
    usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
    mount=$(echo "$line" | awk '{print $6}')

    if [ "$usage" -ge "$THRESHOLD" ]; then
        echo "⚠️ 警告: ${mount} 使用率 ${usage}% (阈值: ${THRESHOLD}%)"
        # 可发送邮件或钉钉/企业微信通知
    fi
done

脚本8:进程监控并自动重启

Bash
#!/bin/bash
# 监控进程,挂了自动重启
set -uo pipefail
# 注意:守护进程不加 -e,避免 systemctl restart 失败时脚本退出

PROCESS_NAME="${1:?请指定进程名}"
CHECK_INTERVAL=30

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

while true; do
    if ! pgrep -x "$PROCESS_NAME" > /dev/null; then
        log "进程 $PROCESS_NAME 未运行,正在重启..."
        systemctl restart "$PROCESS_NAME"
        sleep 5
        if pgrep -x "$PROCESS_NAME" > /dev/null; then
            log "重启成功"
        else
            log "重启失败!请检查"
        fi
    fi
    sleep "$CHECK_INTERVAL"
done

脚本9:SSH 批量执行

Bash
#!/bin/bash
# 在多台服务器上批量执行命令
set -euo pipefail

SERVERS=("web01" "web02" "web03" "db01")
CMD="${1:?请指定要执行的命令}"
# ⚠️ 安全警告:$CMD 来自用户输入,会被远程 Shell 解释执行,存在命令注入风险。
# 生产环境中应对输入进行严格校验/白名单过滤,或使用 ssh 强制命令(ForceCommand)限制可执行操作。

for server in "${SERVERS[@]}"; do
    echo "=== $server ==="
    ssh -o ConnectTimeout=5 "$server" "$CMD" 2>&1 || echo "  连接失败"
    echo ""
done

脚本10:文件监控(inotify)

Bash
#!/bin/bash
# 监控目录变化
set -uo pipefail
# 注意:长运行监控脚本不加 -e,避免 inotifywait 偶发异常导致脚本退出

WATCH_DIR="${1:-.}"

# 需要安装 inotify-tools
# sudo apt install inotify-tools

inotifywait -m -r -e create,delete,modify,move "$WATCH_DIR" |
while read -r directory event filename; do
    echo "[$(date '+%H:%M:%S')] $event: ${directory}${filename}"
done

脚本11:密码生成器

Bash
#!/bin/bash
# 随机密码生成器
set -euo pipefail

LENGTH=${1:-16}
COUNT=${2:-5}

echo "生成 ${COUNT} 个长度为 ${LENGTH} 的随机密码:"
echo ""
for ((i=1; i<=COUNT; i++)); do
    password=$(tr -dc 'A-Za-z0-9!@#$%^&*()_+' < /dev/urandom | head -c "$LENGTH")
    echo "  $i. $password"
done

脚本12:JSON 解析(无需 jq)

Bash
#!/bin/bash
# 简易 JSON 值提取(适合简单场景)
set -euo pipefail

json='{"name":"Alice","age":25,"city":"Beijing"}'

# 使用 grep + sed
get_json_value() {
    echo "$1" | grep -o "\"$2\":[^,}]*" | sed "s/\"$2\"://;s/\"//g"  # sed流编辑器:文本替换与转换
}

echo "Name: $(get_json_value "$json" "name")"
echo "Age: $(get_json_value "$json" "age")"

# 如果有 jq(推荐复杂 JSON)
# echo "$json" | jq -r '.name'

脚本13:数据库备份

Bash
#!/bin/bash
# MySQL 数据库备份

set -euo pipefail

DB_USER="${DB_USER:-root}"
DB_HOST="${DB_HOST:-localhost}"
BACKUP_DIR="/backup/mysql"
DATE=$(date +%Y%m%d)
KEEP_DAYS=7

mkdir -p "$BACKUP_DIR"

# 获取所有数据库
databases=$(mysql -u"$DB_USER" -h"$DB_HOST" -e "SHOW DATABASES;" | \
    grep -Ev "(Database|information_schema|performance_schema|sys)")

for db in $databases; do  # Shell for循环
    backup_file="${BACKUP_DIR}/${db}_${DATE}.sql.gz"
    echo "备份: $db -> $backup_file"
    mysqldump -u"$DB_USER" -h"$DB_HOST" "$db" | gzip > "$backup_file"
done

# 清理旧备份
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +"$KEEP_DAYS" -delete
echo "备份完成"

脚本14:系统健康检查

Bash
#!/bin/bash
# 综合系统健康检查
set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; }

echo "=== 系统健康检查 $(date) ==="
echo ""

# CPU 负载检查
load=$(awk '{print $1}' /proc/loadavg)  # awk文本处理:按列提取和格式化数据
cores=$(nproc)
if (( $(echo "$load < $cores" | bc -l) )); then  # |管道:将前一命令的输出作为后一命令的输入
    pass "CPU 负载正常 ($load / $cores 核)"
else
    fail "CPU 负载过高 ($load / $cores 核)"
fi

# 内存检查
mem_usage=$(free | awk 'NR==2{printf "%.0f", $3/$2*100}')
if [ "$mem_usage" -lt 80 ]; then
    pass "内存使用 ${mem_usage}%"
elif [ "$mem_usage" -lt 90 ]; then
    warn "内存使用 ${mem_usage}%"
else
    fail "内存使用 ${mem_usage}%"
fi

# 磁盘检查
df -h | grep '^/dev' | while read -r line; do  # grep文本搜索:按模式匹配行
    usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
    mount=$(echo "$line" | awk '{print $6}')
    if [ "$usage" -lt 80 ]; then
        pass "磁盘 $mount 使用 ${usage}%"
    elif [ "$usage" -lt 90 ]; then
        warn "磁盘 $mount 使用 ${usage}%"
    else
        fail "磁盘 $mount 使用 ${usage}%"
    fi
done

# 僵尸进程检查
zombies=$(ps aux | awk '$8 ~ /Z/ {count++} END{print count+0}')
if [ "$zombies" -eq 0 ]; then
    pass "无僵尸进程"
else
    warn "发现 $zombies 个僵尸进程"
fi

echo ""
echo "检查完成"

脚本15:自动化部署简板

Bash
#!/bin/bash
# 简单的应用部署脚本

set -euo pipefail

APP_NAME="myapp"
DEPLOY_DIR="/opt/$APP_NAME"
REPO_URL="https://github.com/user/myapp.git"
BRANCH="${1:-main}"

log() { echo "[$(date '+%H:%M:%S')] $*"; }  # $()命令替换:执行命令并获取输出

trap 'log "部署失败!回滚..."; rollback' ERR

rollback() {
    if [ -d "${DEPLOY_DIR}.bak" ]; then  # 条件测试:-f文件存在 -d目录存在 -z空字符串
        rm -rf "$DEPLOY_DIR"
        mv "${DEPLOY_DIR}.bak" "$DEPLOY_DIR"
        log "已回滚到上一个版本"
    fi
}

# 1. 备份当前版本
log "备份当前版本..."
[ -d "$DEPLOY_DIR" ] && cp -a "$DEPLOY_DIR" "${DEPLOY_DIR}.bak"  # &&前一个成功才执行后一个;||前一个失败才执行

# 2. 拉取最新代码
log "拉取最新代码 (分支: $BRANCH)..."
if [ -d "$DEPLOY_DIR/.git" ]; then
    cd "$DEPLOY_DIR"
    git fetch origin
    git checkout "$BRANCH"
    git pull origin "$BRANCH"
else
    git clone -b "$BRANCH" "$REPO_URL" "$DEPLOY_DIR"
    cd "$DEPLOY_DIR"
fi

# 3. 安装依赖
log "安装依赖..."
pip install -r requirements.txt 2>/dev/null || true

# 4. 重启服务
log "重启服务..."
sudo systemctl restart "$APP_NAME"

# 5. 健康检查
sleep 5
if systemctl is-active "$APP_NAME" &>/dev/null; then
    log "✅ 部署成功!"
    rm -rf "${DEPLOY_DIR}.bak"
else
    log "❌ 服务未正常启动"
    rollback
    exit 1
fi

📖 12. 面试要点

高频面试题

Q1:$@$* 的区别?

不加引号时二者相同。加引号后 "$@" 将每个参数保持独立("a" "b" "c"),"$*" 将所有参数合并为一个字符串("a b c")。遍历参数时应使用 "$@"

Q2:如何判断文件是否存在?

Bash
[ -f /path/file ] && echo "exists"     # 普通文件
[ -d /path/dir ] && echo "exists"      # 目录
[ -e /path ] && echo "exists"          # 任何类型

Q3:单引号和双引号的区别?

双引号允许变量替换和命令替换($var $(cmd))。单引号原样输出所有内容,不做任何替换。

Q4:set -eset -u 的作用?

set -e:遇到非零退出状态立即退出脚本。set -u:引用未定义变量时报错退出(默认未定义变量为空字符串)。生产脚本推荐使用 set -euo pipefail


🔧 练习题

  1. 编写脚本接收用户名参数,显示该用户的 UID、家目录和 Shell
  2. 编写脚本统计 /var/log 目录下各类型日志文件的数量和总大小
  3. 编写脚本实现简单的计算器(加减乘除)
  4. 编写脚本从 CSV 文件中读取数据并生成格式化报表
  5. 完成以上15个脚本的修改和运行测试

✅ 自我检查

  • 能编写带参数处理的 Shell 脚本
  • 掌握 if/case/for/while 所有控制结构
  • 能定义和使用函数
  • 理解数组和字符串操作
  • 能使用 set -euo pipefail 进行错误处理
  • 完成了 15 个脚本示例的练习

上一章05-进程管理 下一章07-网络管理