1. Shell脚本自动化基础概述
作为一名Linux系统管理员,我每天都要处理大量重复性任务。从服务器监控到日志分析,从批量文件处理到自动化部署,Shell脚本始终是我最得力的助手。今天我想分享的是Shell脚本编程中最基础但至关重要的自动化知识,这些内容是我在十年运维工作中反复验证过的核心技能。
Shell脚本本质上是一个包含一系列命令的文本文件,它能够自动化执行复杂的操作流程。与手动逐条输入命令相比,脚本具有可重复使用、便于修改和错误率低等显著优势。特别是在处理批量任务时,一个精心编写的脚本可以节省数小时甚至数天的工作量。
学习Shell脚本编程需要掌握几个关键要素:条件判断、循环控制、数据结构和函数封装。这些构成了脚本自动化的基础骨架。下面我将通过实际案例,详细解析每个核心概念的应用场景和实用技巧。
2. 字符串操作与比较
2.1 基本字符串比较
字符串处理是Shell脚本中最常见的操作之一。在bash中,我们可以使用test命令或方括号[]进行字符串比较。这两种方式在功能上是等价的,但方括号形式更符合编程习惯。
bash复制# 直接比较字符串字面量
test "hello" == "world"; echo $? # 输出1表示不相等
[ "hello" != "world" ] && echo "不同" || echo "相同" # 输出"不同"
# 比较变量值
str1="Linux"
str2="Shell"
[ "$str1" == "$str2" ] && echo "匹配" || echo "不匹配"
重要提示:在[]中使用==比较时,等号两边必须保留空格。变量引用最好用双引号包裹,避免空变量导致语法错误。
2.2 空值检测技巧
检查变量是否为空是脚本中的常见需求,-z和-n操作符专门用于这种场景:
bash复制unset var # 确保变量未设置
[ -z "$var" ] && echo "变量为空" # -z检测空值
var="content"
[ -n "$var" ] && echo "变量非空" # -n检测非空值
实际工作中,我经常用这些检测来验证用户输入或配置文件参数:
bash复制read -p "请输入用户名: " username
[ -z "$username" ] && { echo "用户名不能为空"; exit 1; }
2.3 字符串模式匹配
除了精确比较,Shell还支持通配符模式匹配:
bash复制filename="backup_20230715.tar.gz"
[[ "$filename" == backup_*.tar.gz ]] && echo "匹配备份文件格式"
这里使用双中括号[[ ]]支持更强大的模式匹配功能,包括正则表达式:
bash复制email="user@example.com"
[[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] && echo "有效邮箱"
3. 数值比较与计算
3.1 整数比较操作符
Shell中使用特定的操作符进行数值比较,这与大多数编程语言不同:
bash复制num1=15
num2=20
[ $num1 -eq $num2 ] # 等于(equal)
[ $num1 -ne $num2 ] # 不等于(not equal)
[ $num1 -gt $num2 ] # 大于(greater than)
[ $num1 -ge $num2 ] # 大于等于(greater or equal)
[ $num1 -lt $num2 ] # 小于(less than)
[ $num1 -le $num2 ] # 小于等于(less or equal)
3.2 算术运算的多种方式
Shell中有多种进行算术运算的方法,各有适用场景:
bash复制# 使用$(( ))表达式
sum=$((num1 + num2))
# 使用let命令
let "sum=num1+num2"
# 使用expr命令(较老的方式)
sum=`expr $num1 + $num2`
# 使用$[ ]表达式(已过时,不推荐)
sum=$[num1 + num2]
在实际脚本中,我推荐使用$(( ))形式,因为它最简洁且兼容性好。
3.3 浮点数计算方案
原生bash不支持浮点运算,但可以通过其他工具实现:
bash复制# 使用bc计算器
pi=$(echo "scale=2; 4*a(1)" | bc -l) # 计算π值,保留2位小数
# 使用awk
result=$(awk "BEGIN {print 3.14 * 5.67}")
# 实战案例:计算磁盘使用百分比
total=$(df -h / | awk 'NR==2 {print $2}' | tr -d 'G')
used=$(df -h / | awk 'NR==2 {print $3}' | tr -d 'G')
usage=$(awk "BEGIN {printf \"%.1f\", $used/$total*100}")
echo "磁盘使用率: $usage%"
4. 文件系统检测与判断
4.1 文件存在性检查
文件操作是自动化脚本的核心功能之一,bash提供了丰富的测试操作符:
bash复制file="/path/to/file"
[ -e "$file" ] # 文件/目录是否存在
[ -f "$file" ] # 是否是普通文件
[ -d "$file" ] # 是否是目录
[ -L "$file" ] # 是否是符号链接
[ -r "$file" ] # 当前用户是否有读权限
[ -w "$file" ] # 当前用户是否有写权限
[ -x "$file" ] # 当前用户是否有执行权限
[ -s "$file" ] # 文件大小是否大于0字节
4.2 文件比较技巧
比较两个文件是否相同是常见需求,特别是配置文件变更检测:
bash复制file1="/etc/nginx/nginx.conf"
file2="/backup/nginx.conf.bak"
# 比较文件内容是否相同
cmp -s "$file1" "$file2" && echo "文件内容相同"
# 比较文件修改时间
[ "$file1" -nt "$file2" ] && echo "file1比file2新"
[ "$file1" -ot "$file2" ] && echo "file1比file2旧"
# 检查硬链接关系
[ "$file1" -ef "$file2" ] && echo "是同一个inode"
4.3 文件属性实战案例
结合文件检测可以编写实用的系统管理脚本:
bash复制# 检查日志目录是否存在并可写
LOG_DIR="/var/log/myapp"
if [ ! -d "$LOG_DIR" ]; then
mkdir -p "$LOG_DIR" || { echo "无法创建日志目录"; exit 1; }
fi
[ -w "$LOG_DIR" ] || { echo "日志目录不可写"; exit 1; }
# 检查配置文件是否存在且有内容
CONFIG_FILE="/etc/myapp.conf"
[ -s "$CONFIG_FILE" ] || { echo "配置文件不存在或为空"; exit 1; }
5. 条件控制结构
5.1 if语句深度解析
if是Shell脚本中最基础的条件控制结构,其完整语法如下:
bash复制if 条件测试1; then
# 条件1为真时执行的命令
elif 条件测试2; then
# 条件2为真时执行的命令
else
# 所有条件都为假时执行的命令
fi
实际案例:检查系统负载并报警
bash复制load=$(uptime | awk -F'[a-z]:' '{print $2}' | cut -d, -f1 | tr -d ' ')
threshold=5.0
if (( $(echo "$load > $threshold" | bc -l) )); then
echo "[警告] 系统负载过高: $load" >&2
# 可以添加邮件或短信报警逻辑
elif (( $(echo "$load > $threshold/2" | bc -l) )); then
echo "[注意] 系统负载中等: $load"
else
echo "[正常] 系统负载: $load"
fi
5.2 case语句模式匹配
case语句非常适合处理多分支选择,比多层if更清晰:
bash复制read -p "请输入操作命令(start|stop|restart|status): " cmd
case "$cmd" in
start|s)
echo "启动服务..."
# 启动命令
;;
stop|k)
echo "停止服务..."
# 停止命令
;;
restart|reload|r)
echo "重启服务..."
# 重启命令
;;
status|st)
echo "服务状态..."
# 状态检查
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
case语句支持通配符模式,可以实现更灵活的匹配:
bash复制filename="backup_20230715.tar.gz"
case "$filename" in
*.tar|*.tar.gz|*.tgz)
echo "tar归档文件"
;;
*.zip|*.rar|*.7z)
echo "压缩文件"
;;
*.sh)
echo "Shell脚本"
;;
*)
echo "未知文件类型"
;;
esac
6. 循环结构详解
6.1 for循环的多种形式
Shell中的for循环有两种主要形式:列表式和C语言式。
列表式for循环:
bash复制# 直接枚举值
for i in 1 2 3 4 5; do
echo "数字: $i"
done
# 使用序列生成
for i in {1..5}; do
echo "数字: $i"
done
# 遍历文件
for file in *.log; do
echo "处理日志文件: $file"
# 可以添加gzip压缩或日志分析命令
done
C语言式for循环:
bash复制for ((i=1; i<=5; i++)); do
echo "计数: $i"
done
# 复杂示例:倒计时
for ((sec=10; sec>=0; sec--)); do
echo -ne "倒计时: $sec\r"
sleep 1
done
echo "时间到!"
6.2 while和until循环
while循环在条件为真时持续执行:
bash复制# 简单计数器
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done
# 读取文件行
while IFS= read -r line; do
echo "处理行: $line"
done < "/path/to/file"
until循环与while相反,条件为假时执行:
bash复制# 等待服务启动
timeout=60
attempt=0
until [ $attempt -ge $timeout ] || systemctl is-active --quiet nginx; do
echo "等待nginx启动...($attempt/$timeout)"
((attempt++))
sleep 1
done
6.3 循环控制语句
break和continue可以改变循环的正常执行流程:
bash复制# 查找第一个符合条件的文件
for file in *; do
if [ -f "$file" ] && [ -s "$file" ]; then
echo "找到第一个非空文件: $file"
break
fi
done
# 跳过特定条件的处理
for num in {1..10}; do
if [ $((num % 2)) -eq 0 ]; then
continue # 跳过偶数
fi
echo "奇数: $num"
done
7. 数组操作进阶
7.1 索引数组的完整用法
Shell中的数组功能虽然不如高级语言强大,但足以应对大多数脚本需求:
bash复制# 数组声明与赋值
colors=("red" "green" "blue")
# 另一种赋值方式
fruits=()
fruits[0]="apple"
fruits[1]="banana"
fruits[2]="orange"
# 访问数组元素
echo "第一个颜色: ${colors[0]}"
echo "所有水果: ${fruits[@]}"
# 数组长度
echo "颜色数量: ${#colors[@]}"
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 数组切片
echo "前两个颜色: ${colors[@]:0:2}"
# 数组合并
combined=("${colors[@]}" "${fruits[@]}")
7.2 关联数组实用技巧
bash 4.0+支持关联数组(类似其他语言的字典):
bash复制# 必须先声明
declare -A user
user=(
[name]="Alice"
[age]=25
[email]="alice@example.com"
)
# 访问元素
echo "用户名: ${user[name]}"
echo "年龄: ${user[age]}"
# 遍历关联数组
for key in "${!user[@]}"; do
echo "$key => ${user[$key]}"
done
# 实际应用:统计单词频率
declare -A freq
words=("apple" "banana" "apple" "orange" "banana" "apple")
for word in "${words[@]}"; do
((freq[$word]++))
done
for word in "${!freq[@]}"; do
echo "$word 出现 ${freq[$word]} 次"
done
8. 函数封装与模块化
8.1 函数定义与调用
函数是代码复用的基本单元,良好的函数设计能显著提升脚本质量:
bash复制# 简单函数定义
say_hello() {
echo "Hello, $1!"
}
# 调用函数
say_hello "World"
# 带返回值的函数
add() {
local sum=$(( $1 + $2 ))
return $sum # 只能返回整数
}
add 3 5
echo "3 + 5 = $?"
# 更好的返回值方式:通过echo输出
multiply() {
local product=$(( $1 * $2 ))
echo $product
}
result=$(multiply 4 6)
echo "4 × 6 = $result"
8.2 函数参数与变量作用域
理解参数传递和变量作用域对编写可靠函数至关重要:
bash复制# 参数处理示例
show_params() {
echo "参数个数: $#"
echo "所有参数: $@"
echo "第一个参数: $1"
echo "第二个参数: $2"
}
show_params "one" "two" "three"
# 变量作用域
global_var="outside"
scope_demo() {
local local_var="inside"
echo "函数内global_var: $global_var"
echo "函数内local_var: $local_var"
global_var="modified"
}
scope_demo
echo "函数外global_var: $global_var"
echo "函数外尝试访问local_var: ${local_var:-未定义}"
8.3 实用函数库示例
将常用功能封装成函数库可以大大提高脚本开发效率:
bash复制#!/bin/bash
# 文件名: utils.sh
# 日志记录函数
log() {
local level=$1
local msg=$2
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $msg" >> "/var/log/myscript.log"
# 根据日志级别决定是否输出到终端
case $level in
ERROR|WARN) echo "$msg" >&2 ;;
esac
}
# 检查命令是否存在
check_command() {
type "$1" &> /dev/null
}
# 安全创建目录
create_dir() {
local dir=$1
[ -z "$dir" ] && { log ERROR "目录参数为空"; return 1; }
if [ -d "$dir" ]; then
log INFO "目录已存在: $dir"
return 0
fi
mkdir -p "$dir" || {
log ERROR "无法创建目录: $dir"
return 1
}
log INFO "成功创建目录: $dir"
return 0
}
# 在其他脚本中引用这个库
# source /path/to/utils.sh
9. Shell脚本调试与优化
9.1 调试技巧与常见错误
编写健壮的脚本需要掌握调试方法:
bash复制#!/bin/bash
# 启用调试模式
set -x # 打印执行的每一条命令
# 示例代码
var="test"
echo "变量值: $var"
# 关闭调试
set +x
# 其他有用的调试选项
set -e # 命令失败时立即退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中任意命令失败则整个管道失败
# 组合使用
set -euo pipefail
# 常见错误1:未加引号的变量
filename="my file.txt"
[ -f $filename ] # 错误!应该用[ -f "$filename" ]
# 常见错误2:数字比较用错操作符
num=10
[ $num == 10 ] # 可以但不推荐,应该用[ $num -eq 10 ]
9.2 性能优化建议
Shell脚本虽然方便,但性能不如编译型语言。以下优化技巧可以提升脚本效率:
- 减少子进程创建:
bash复制# 慢:每次循环都创建grep进程
for word in $(cat bigfile.txt | grep "pattern"); do
echo "$word"
done
# 快:使用内置字符串操作
while IFS= read -r line; do
[[ "$line" =~ "pattern" ]] && echo "$line"
done < bigfile.txt
- 使用内置命令替代外部命令:
bash复制# 不推荐:使用外部tr命令
echo "$var" | tr '[:lower:]' '[:upper:]'
# 推荐:使用bash内置参数扩展
echo "${var^^}"
- 批量操作替代单次操作:
bash复制# 慢:每次循环都调用mkdir
for dir in dir{1..100}; do
mkdir "$dir"
done
# 快:一次性创建
mkdir dir{1..100}
- 使用临时文件的最佳实践:
bash复制# 不安全的方式
tempfile="/tmp/tempfile"
echo "data" > "$tempfile"
# 更好的方式
tempfile=$(mktemp /tmp/script.XXXXXX) || exit 1
trap 'rm -f "$tempfile"' EXIT # 确保脚本退出时删除临时文件
echo "data" > "$tempfile"
10. 实战案例:自动化备份脚本
结合前面所学,我们开发一个完整的自动化备份脚本:
bash复制#!/bin/bash
# 文件名: auto_backup.sh
# 描述:自动化目录备份脚本
# 加载函数库
source /path/to/utils.sh
# 配置变量
BACKUP_DIR="/backup"
SOURCE_DIRS=("/etc" "/home" "/var/www")
MAX_BACKUPS=30
COMPRESS_LEVEL=6
DATE=$(date "+%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/backup_$DATE.tar.gz"
# 检查备份目录
create_dir "$BACKUP_DIR" || exit 1
# 检查磁盘空间
available=$(df -P "$BACKUP_DIR" | awk 'NR==2 {print $4}')
required=$(du -sc "${SOURCE_DIRS[@]}" 2>/dev/null | awk 'END {print $1}')
if [ "$available" -lt "$required" ]; then
log ERROR "磁盘空间不足: 需要${required}KB,可用${available}KB"
exit 1
fi
# 执行备份
log INFO "开始备份: ${SOURCE_DIRS[*]}"
if tar -czf "$BACKUP_FILE" --exclude='*.tmp' --exclude='*.log' -p "${SOURCE_DIRS[@]}"; then
log INFO "备份成功: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
else
log ERROR "备份失败"
exit 1
fi
# 清理旧备份
backup_count=$(ls -1 "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l)
if [ "$backup_count" -gt "$MAX_BACKUPS" ]; then
oldest=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz | tail -n 1)
rm -f "$oldest"
log INFO "删除旧备份: $(basename "$oldest")"
fi
# 添加备份校验
if gzip -t "$BACKUP_FILE"; then
log INFO "备份文件完整性验证通过"
else
log ERROR "备份文件损坏!"
exit 1
fi
这个脚本展示了Shell脚本编程的多个关键点:
- 使用函数库组织代码
- 严格的错误检查和处理
- 文件操作和条件判断
- 日志记录和通知机制
- 资源管理和清理
在实际工作中,我会根据具体需求扩展这个脚本,比如添加远程备份、加密备份、邮件通知等功能。