1. Shell编程流程控制基础
在Linux系统管理中,shell脚本的流程控制是自动化运维的核心技能。不同于交互式命令行操作,脚本中的流程控制决定了命令的执行逻辑和顺序,这也是shell编程区别于简单命令组合的关键特征。
1.1 条件判断结构
if语句是shell中最基础的条件判断结构,其标准语法格式如下:
bash复制if [ condition ]; then
commands
elif [ condition ]; then
commands
else
commands
fi
这里的方括号[ ]实际上是test命令的简写,用于评估条件表达式。常见的条件测试包括:
- 数值比较:-eq(等于)、-ne(不等于)、-gt(大于)、-lt(小于)
- 字符串比较:= (相等)、!= (不等)、-z (空字符串)
- 文件测试:-e (存在)、-f (普通文件)、-d (目录)
实际案例:检查Apache服务是否运行
bash复制if ps -ef | grep httpd | grep -v grep; then
echo "Apache正在运行"
else
echo "Apache未运行,正在启动..."
systemctl start httpd
fi
注意:在条件判断中使用[ ]时,括号内侧必须保留空格,这是shell的语法要求。写成if[$var=value]会导致语法错误。
1.2 循环控制结构
for循环和while循环是shell脚本中最常用的两种循环结构:
- for循环的两种典型用法:
bash复制# 列表式循环
for i in 1 2 3 4 5; do
echo "数字 $i"
done
# C语言风格循环
for ((i=0; i<5; i++)); do
echo "计数 $i"
done
- while循环的基本结构:
bash复制count=1
while [ $count -le 5 ]; do
echo "循环次数 $count"
((count++))
done
实际应用案例:批量处理日志文件
bash复制# 查找并压缩7天前的日志
for logfile in $(find /var/log -name "*.log" -mtime +7); do
gzip $logfile
mv $logfile.gz /backup/logs/
done
1.3 case多分支选择
case语句提供了一种更清晰的多条件分支选择方式:
bash复制case $变量 in
模式1)
命令序列1
;;
模式2)
命令序列2
;;
*)
默认命令序列
;;
esac
典型应用:根据输入参数执行不同操作
bash复制case $1 in
start)
systemctl start nginx
;;
stop)
systemctl stop nginx
;;
restart)
systemctl restart nginx
;;
*)
echo "用法: $0 {start|stop|restart}"
exit 1
;;
esac
经验分享:case语句中的模式支持通配符匹配,如[0-9]可以匹配数字,[a-z]匹配小写字母等。合理使用通配符可以简化复杂的条件判断。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Shell函数详解
Shell函数是将一组命令封装为可重复调用的代码单元,是构建复杂脚本的基础模块。
2.1 函数定义与调用
基本函数定义语法:
bash复制function_name() {
命令序列
[return value]
}
或者使用function关键字:
bash复制function function_name {
命令序列
[return value]
}
函数调用示例:
bash复制# 定义欢迎函数
welcome() {
echo "欢迎, $1!"
local time=$(date +"%H:%M")
echo "当前时间: $time"
}
# 调用函数
welcome "管理员"
关键特性说明:
- 函数必须先定义后调用
- 通过位置参数($1,$2等)传递参数
- 使用local声明局部变量,避免污染全局命名空间
- return语句返回退出状态码(0-255),不是返回值
2.2 函数参数与返回值
虽然shell函数没有传统编程语言中的返回值概念,但有多种方式实现类似效果:
- 通过全局变量传递结果:
bash复制calculate() {
result=$(($1 + $2))
}
calculate 3 5
echo "结果是 $result"
- 使用echo输出并通过命令替换捕获:
bash复制square() {
local num=$1
echo $((num * num))
}
value=$(square 4)
echo "平方值是 $value"
- 返回状态码:
bash复制check_file() {
[ -f "$1" ] && return 0 || return 1
}
check_file "/etc/passwd"
if [ $? -eq 0 ]; then
echo "文件存在"
else
echo "文件不存在"
fi
2.3 函数库与模块化
将常用函数组织成函数库是大型脚本开发的最佳实践:
- 创建函数库文件(如lib.sh):
bash复制#!/bin/bash
# 日志记录函数
log() {
local level=$1
local message=$2
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $message" >> /var/log/myscript.log
}
# 安全检查函数
check_root() {
[ $(id -u) -eq 0 ] || { log "ERROR" "需要root权限"; exit 1; }
}
- 在主脚本中引用函数库:
bash复制#!/bin/bash
# 引入函数库
source /path/to/lib.sh
# 使用库函数
check_root
log "INFO" "脚本开始执行"
重要提示:使用source或.(点)命令加载函数库时,会直接在当前shell环境中执行文件内容。与直接执行脚本不同,这种方式使得函数库中定义的函数和变量在当前shell中可用。
3. 流程控制高级技巧
3.1 循环控制命令
break和continue命令可以改变循环的正常执行流程:
bash复制# 查找第一个满足条件的文件
for file in *; do
if [ -x "$file" ]; then
echo "找到可执行文件: $file"
break
fi
done
# 跳过特定条件的处理
for num in {1..10}; do
if [ $((num % 2)) -eq 0 ]; then
continue
fi
echo "奇数: $num"
done
3.2 循环重定向
循环结构支持I/O重定向,可以高效处理文件内容:
bash复制# 逐行处理文件
while read line; do
echo "处理行: $line"
# 复杂处理逻辑
done < input.txt
# 将循环输出重定向到文件
for user in $(cat users.list); do
echo "创建用户 $user"
useradd $user
done > create_users.log 2>&1
3.3 并行处理技巧
利用后台执行和wait命令实现简单并行:
bash复制# 启动多个后台进程
for i in {1..5}; do
process_data $i &
done
# 等待所有后台进程完成
wait
echo "所有处理完成"
更高级的并行控制可以使用GNU parallel工具:
bash复制# 使用parallel并行处理
cat file.list | parallel -j 4 "gzip {}"
4. 脚本调试与错误处理
4.1 调试技术
- 使用set命令开启调试模式:
bash复制#!/bin/bash
set -x # 开启命令打印
set -e # 遇到错误立即退出
set -o pipefail # 管道命令失败时退出
# 脚本内容...
- 常用调试选项组合:
bash复制#!/bin/bash
set -exo pipefail
- 局部调试技巧:
bash复制# 在特定代码段开启调试
set -x
关键代码段
set +x
4.2 错误处理机制
- 陷阱(trap)机制:
bash复制trap '错误处理函数' ERR
cleanup() {
echo "清理临时文件..."
rm -f /tmp/tempfile.*
}
trap cleanup EXIT
- 更健壮的错误检查模式:
bash复制command || {
echo "命令执行失败: $?"
exit 1
}
# 或者
if ! command; then
echo "命令执行失败"
exit 1
fi
4.3 常见问题排查
- 变量引用问题:
bash复制# 错误写法:变量名与字符串相连时需要用{}分隔
echo "$varible_not_exist"
# 正确写法
echo "${variable}_suffix"
- 空格敏感问题:
bash复制# 错误写法:等号两边有空格
var = value
# 正确写法
var=value
- 文件路径包含空格:
bash复制# 错误写法:未加引号
rm $file_with_spaces
# 正确写法
rm "$file_with_spaces"
- 整数运算语法:
bash复制# 错误写法:直接使用运算符
result=1+1
# 正确写法
result=$((1 + 1))
5. 综合应用案例
5.1 系统监控脚本
bash复制#!/bin/bash
set -eo pipefail
# 定义阈值
CPU_WARN=80
MEM_WARN=90
DISK_WARN=85
# 检查CPU使用率
check_cpu() {
local usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
if (( $(echo "$usage > $CPU_WARN" | bc -l) )); then
echo "警告: CPU使用率 ${usage}% 超过阈值 ${CPU_WARN}%"
return 1
fi
return 0
}
# 检查内存使用率
check_mem() {
local total=$(free -m | awk '/Mem:/ {print $2}')
local used=$(free -m | awk '/Mem:/ {print $3}')
local usage=$((used * 100 / total))
if [ $usage -gt $MEM_WARN ]; then
echo "警告: 内存使用率 ${usage}% 超过阈值 ${MEM_WARN}%"
return 1
fi
return 0
}
# 主监控函数
main() {
local errors=0
if ! check_cpu; then
((errors++))
fi
if ! check_mem; then
((errors++))
fi
[ $errors -eq 0 ] && echo "系统状态正常" || exit 1
}
main
5.2 备份脚本示例
bash复制#!/bin/bash
set -eo pipefail
# 配置变量
BACKUP_DIR="/backup"
LOG_FILE="/var/log/backup.log"
RETENTION_DAYS=7
# 日志函数
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 备份函数
backup_files() {
local src_dir=$1
local backup_name=$2
[ -d "$src_dir" ] || { log "错误: 源目录 $src_dir 不存在"; return 1; }
local timestamp=$(date +"%Y%m%d_%H%M%S")
local backup_file="${BACKUP_DIR}/${backup_name}_${timestamp}.tar.gz"
log "开始备份 $src_dir 到 $backup_file"
tar -czf "$backup_file" -C "$(dirname "$src_dir")" "$(basename "$src_dir")" || {
log "备份失败: $src_dir"
return 1
}
log "备份完成: $backup_file ($(du -h "$backup_file" | awk '{print $1}'))"
return 0
}
# 清理旧备份
clean_old_backups() {
log "清理超过${RETENTION_DAYS}天的旧备份"
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
}
# 主函数
main() {
mkdir -p "$BACKUP_DIR"
backup_files "/etc" "etc_backup"
backup_files "/var/www" "www_backup"
clean_old_backups
log "备份任务完成"
}
main
5.3 用户管理脚本
bash复制#!/bin/bash
set -eo pipefail
# 检查root权限
[ $(id -u) -eq 0 ] || { echo "需要root权限"; exit 1; }
# 用户数据文件
USER_FILE="/root/users.csv"
# 添加用户函数
add_user() {
local username=$1
local password=$2
local groups=$3
if id "$username" &>/dev/null; then
echo "用户 $username 已存在"
return 1
fi
useradd -m -s /bin/bash "$username" || {
echo "创建用户 $username 失败"
return 1
}
echo "$username:$password" | chpasswd || {
echo "设置密码失败"
userdel -r "$username"
return 1
}
if [ -n "$groups" ]; then
usermod -aG "$groups" "$username"
fi
echo "用户 $username 创建成功"
return 0
}
# 主处理函数
process_users() {
while IFS=, read -r username password groups; do
echo "正在处理用户 $username..."
add_user "$username" "$password" "$groups" || continue
done < "$USER_FILE"
}
# 输入检查
[ -f "$USER_FILE" ] || { echo "用户文件 $USER_FILE 不存在"; exit 1; }
process_users
echo "用户批量创建完成"
6. 性能优化与最佳实践
6.1 脚本性能优化
- 减少子shell创建:
bash复制# 低效写法:创建多个子shell
for i in $(seq 1 100); do
echo $i
done
# 高效写法:使用内置序列
for i in {1..100}; do
echo $i
done
- 避免不必要的管道:
bash复制# 低效写法:多个管道
ps aux | grep httpd | grep -v grep | awk '{print $2}'
# 高效写法:单次awk处理
ps aux | awk '/httpd/ && !/grep/ {print $2}'
- 使用here string替代echo+管道:
bash复制# 低效写法
echo "$var" | command
# 高效写法
command <<< "$var"
6.2 代码风格指南
- 变量命名规范:
bash复制# 不好的写法
a="value"
# 好的写法
config_file="/path/to/config"
- 注释规范:
bash复制# 单行注释描述下面代码的功能
: <<'COMMENT'
多行注释用于解释复杂逻辑
可以跨越多行
COMMENT
- 代码缩进与格式:
bash复制# 一致的缩进风格
if [ condition ]; then
command1
command2
else
command3
fi
6.3 安全编程实践
- 输入验证:
bash复制read -p "输入文件名: " filename
[ -z "$filename" ] && { echo "文件名不能为空"; exit 1; }
[ -f "$filename" ] || { echo "文件不存在"; exit 1; }
- 权限检查:
bash复制[ -w "/etc/config" ] || { echo "无写权限"; exit 1; }
- 安全执行外部命令:
bash复制# 不安全写法
command="rm -rf /tmp/${user_input}"
# 安全写法
command=("rm" "-rf" "/tmp/${user_input}")
"${command[@]}"
7. 跨平台兼容性
7.1 shebang选择
bash复制#!/bin/bash # 明确要求bash
#!/usr/bin/env bash # 更灵活的路径查找
7.2 特性检测
bash复制# 检查是否支持数组
if declare -a test_array &>/dev/null; then
echo "支持数组"
else
echo "不支持数组"
fi
7.3 兼容性写法
- 字符串替换:
bash复制# 传统写法
var="path/to/file"
ext="${var##*.}"
# 兼容性更好的写法
ext=$(echo "$var" | awk -F. '{print $NF}')
- 算术运算:
bash复制# bash特有写法
result=$((x + y))
# 兼容性写法
result=$(expr $x + $y)
8. 实用函数库示例
8.1 颜色输出函数
bash复制#!/bin/bash
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# 彩色输出函数
color_echo() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# 使用示例
color_echo $RED "错误信息"
color_echo $GREEN "成功信息"
color_echo $YELLOW "警告信息"
8.2 进度条函数
bash复制#!/bin/bash
progress_bar() {
local duration=${1:-10}
local width=${2:-50}
local progress=0
for ((i=0; i<=$width; i++)); do
# 计算百分比
progress=$((i * 100 / width))
# 打印进度条
printf "\r["
for ((j=0; j<i; j++)); do printf "#"; done
for ((j=i; j<width; j++)); do printf " "; done
printf "] %3d%%" $progress
# 模拟进度
sleep $(echo "scale=4; $duration/$width" | bc)
done
echo
}
# 使用示例
progress_bar 5 30
8.3 输入验证函数
bash复制#!/bin/bash
# 验证整数输入
validate_int() {
local input=$1
local min=${2:-0}
local max=${3:-2147483647}
[[ $input =~ ^[0-9]+$ ]] || return 1
(( input >= min && input <= max )) || return 1
return 0
}
# 验证IP地址
validate_ip() {
local ip=$1
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
OIFS=$IFS
IFS='.'
ip=($ip)
IFS=$OIFS
[[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && \
${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
stat=$?
fi
return $stat
}
# 使用示例
read -p "输入端口号: " port
validate_int $port 1 65535 || { echo "无效端口号"; exit 1; }
