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 -e 和 set -u 的作用?
set -e:遇到非零退出状态立即退出脚本。set -u:引用未定义变量时报错退出(默认未定义变量为空字符串)。生产脚本推荐使用set -euo pipefail。
🔧 练习题¶
- 编写脚本接收用户名参数,显示该用户的 UID、家目录和 Shell
- 编写脚本统计 /var/log 目录下各类型日志文件的数量和总大小
- 编写脚本实现简单的计算器(加减乘除)
- 编写脚本从 CSV 文件中读取数据并生成格式化报表
- 完成以上15个脚本的修改和运行测试
✅ 自我检查¶
- 能编写带参数处理的 Shell 脚本
- 掌握 if/case/for/while 所有控制结构
- 能定义和使用函数
- 理解数组和字符串操作
- 能使用 set -euo pipefail 进行错误处理
- 完成了 15 个脚本示例的练习