Shell函数与自动化:让脚本从"能用"进化到"好用"
前面几篇我们已经能写出带判断、有循环的脚本了。但随着脚本越来越长,你可能会发现一个问题:同样的代码在好几个地方重复出现,改一处漏一处,维护起来很头疼。这时候就需要函数来帮忙了。今天我们聊聊Shell函数、数组、信号处理,以及如何用expect实现自动化交互。
一、函数:代码复用的利器
1.1 函数的定义和调用
Shell函数有两种定义方式:
bash
# 方式1:function关键字
function say_hello {
echo "Hello, $1!"
}
# 方式2:函数名+括号(推荐)
say_hello() {
echo "Hello, $1!"
}
# 调用函数
say_hello "World"
注意: 函数必须先定义后调用,否则会报"command not found"。
1.2 函数参数和返回值
Shell函数的参数传递方式和脚本参数一样,用$1、$2接收:
bash
#!/bin/bash
# 计算两数之和
add() {
local num1=$1 # local声明局部变量
local num2=$2
echo $(( num1 + num2 ))
}
result=$(add 3 5)
echo "3 + 5 = $result"
关于返回值: Shell函数有两种"返回"方式:
return:返回状态码(0-255),通过$?获取echo:返回任意值,通过$(函数名)获取
bash
#!/bin/bash
# 演示两种返回方式
# 方式1:return返回状态码
is_root() {
if [ "$(whoami)" == "root" ]; then
return 0 # 成功
else
return 1 # 失败
fi
}
if is_root; then
echo "当前是root用户"
else
echo "当前不是root用户"
fi
# 方式2:echo返回数据
get_ip() {
local ip=$(hostname -I | awk '{print $1}')
echo "$ip"
}
my_ip=$(get_ip)
echo "本机IP: $my_ip"
1.3 局部变量:local关键字
在函数中使用local关键字声明的变量,只在函数内部有效,不会污染外部环境:
bash
#!/bin/bash
name="全局变量"
test_func() {
local name="局部变量"
echo "函数内: $name"
}
test_func
echo "函数外: $name"
# 输出:
# 函数内: 局部变量
# 函数外: 全局变量
1.4 函数库:代码拆分与复用
当脚本变得很长时,可以把公共函数抽到单独的文件中作为"函数库":
bash
# lib/common.sh - 公共函数库
# 日志输出函数
log_info() {
echo -e "\033[32m[INFO] $(date '+%F %T') - $1\033[0m"
}
log_error() {
echo -e "\033[31m[ERROR] $(date '+%F %T') - $1\033[0m"
}
# 检查命令是否存在
check_command() {
if ! command -v "$1" &> /dev/null; then
log_error "命令 $1 未安装"
return 1
fi
}
# 确保目录存在
ensure_dir() {
[ -d "$1" ] || mkdir -p "$1"
}
bash
#!/bin/bash
# 主脚本 - 加载函数库
# 加载公共函数
source ./lib/common.sh
log_info "开始执行部署..."
check_command "docker"
ensure_dir "/opt/app/logs"
log_info "部署完成"
1.5 实战:系统信息采集函数
bash
#!/bin/bash
# 功能:系统信息采集
# CPU使用率
get_cpu_usage() {
top -bn1 | grep "Cpu(s)" | awk '{print $2}'
}
# 内存使用率
get_mem_usage() {
free | awk '/Mem/{printf "%.1f", $3/$2*100}'
}
# 磁盘使用率
get_disk_usage() {
df -h | awk '$NF=="/"{print $5}'
}
# 系统负载
get_load_average() {
uptime | awk -F'load average:' '{print $2}' | tr -d ' '
}
# 采集信息
echo "========== 系统状态 =========="
echo "CPU使用率: $(get_cpu_usage)%"
echo "内存使用率: $(get_mem_usage)%"
echo "磁盘使用率: $(get_disk_usage)"
echo "系统负载: $(get_load_average)"
echo "==============================="
二、数组:批量数据处理
2.1 索引数组
bash
# 定义数组
fruits=("apple" "banana" "cherry" "date")
# 访问元素
echo ${fruits[0]} # apple(下标从0开始)
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 数组长度
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 按下标遍历
for i in "${!fruits[@]}"; do
echo "索引$i: ${fruits[$i]}"
done
# 添加元素
fruits+=("elderberry")
# 删除元素
unset fruits[1] # 删除banana
2.2 关联数组(字典)
Shell 4.0+支持关联数组,可以用字符串作为下标:
bash
# 声明关联数组(必须用declare -A)
declare -A user_info
# 赋值
user_info[name]="张三"
user_info[age]=25
user_info[city]="北京"
# 访问
echo "姓名: ${user_info[name]}"
echo "年龄: ${user_info[age]}"
# 遍历所有key
for key in "${!user_info[@]}"; do
echo "$key: ${user_info[$key]}"
done
2.3 实战:服务管理脚本
bash
#!/bin/bash
# 功能:多服务管理
# 定义服务操作映射
declare -A service_ops=(
[start]="systemctl start"
[stop]="systemctl stop"
[restart]="systemctl restart"
[status]="systemctl status"
)
# 服务列表
services=("nginx" "mysql" "redis")
# 显示菜单
echo "========== 服务管理 =========="
echo "服务列表: ${services[*]}"
echo "操作类型: ${!service_ops[@]}"
echo "==============================="
read -p "请输入服务名: " svc_name
read -p "请输入操作: " action
# 检查服务是否在列表中
if [[ " ${services[*]} " =~ " $svc_name " ]]; then
if [[ -v service_ops[$action] ]]; then
echo "执行: ${service_ops[$action]} $svc_name"
${service_ops[$action]} "$svc_name"
else
echo "无效操作: $action"
fi
else
echo "未知服务: $svc_name"
fi
三、信号处理与脚本健壮性
3.1 Linux信号基础
Linux通过信号与进程通信。常见的信号有:
| 信号 | 编号 | 说明 |
|---|---|---|
| SIGHUP | 1 | 挂起进程 |
| SIGINT | 2 | 中断进程(Ctrl+C) |
| SIGQUIT | 3 | 退出进程 |
| SIGKILL | 9 | 强制终止(不可捕获) |
| SIGTERM | 15 | 优雅终止 |
| SIGTSTP | 20 | 暂停进程(Ctrl+Z) |
3.2 trap:捕获信号
trap命令可以让你自定义脚本收到信号时的行为:
bash
#!/bin/bash
# 演示trap信号捕获
# 定义清理函数
cleanup() {
echo ""
echo "收到退出信号,正在清理..."
rm -f /tmp/lock_file
echo "清理完成,退出脚本"
exit 0
}
# 捕获SIGINT和SIGTERM信号
trap cleanup SIGINT SIGTERM
# 创建锁文件
touch /tmp/lock_file
echo "脚本运行中,按Ctrl+C退出..."
while true; do
echo "$(date '+%T') - 运行中..."
sleep 2
done
3.3 脚本退出时的清理
bash
#!/bin/bash
# 确保脚本退出时一定执行清理
temp_dir=$(mktemp -d)
# 捕获EXIT信号(脚本退出时触发)
trap "rm -rf $temp_dir; echo '临时目录已清理'" EXIT
# 使用临时目录
echo "数据" > "$temp_dir/data.txt"
ls "$temp_dir"
# 脚本正常结束时会自动触发EXIT
echo "脚本执行完成"
四、expect:自动化交互
4.1 为什么需要expect
在实际运维中,很多操作需要交互式输入,比如SSH登录输入密码、passwd命令修改密码等。手动操作没问题,但批量执行时就需要自动化工具了。expect就是专门解决这个问题的。
4.2 安装expect
bash
# CentOS/RHEL
yum install -y expect
# Ubuntu/Debian
apt-get install -y expect
4.3 expect基本语法
bash
#!/usr/bin/expect
# expect脚本的基本结构
# 设定超时时间
set timeout 30
# 启动一个进程
spawn ssh root@10.0.0.12
# 等待匹配关键字
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "123456\r" }
}
# 交还控制权给用户
interact
核心命令:
spawn:启动一个新进程expect:等待匹配指定字符串send:发送字符串到进程interact:交还控制权给用户exp_continue:继续匹配后续的expect
4.4 Shell中嵌入expect
实际工作中,我们通常在Shell脚本中嵌入expect代码:
bash
#!/bin/bash
# 功能:SSH免密码登录配置
remote_host="$1"
remote_pass="$2"
if [ -z "$remote_host" ] || [ -z "$remote_pass" ]; then
echo "用法: $0 <远程主机> <密码>"
exit 1
fi
# 生成密钥对
[ -f ~/.ssh/id_rsa ] || ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa
# 使用expect自动发送公钥
/usr/bin/expect << EOF
set timeout 30
spawn ssh-copy-id -i ~/.ssh/id_rsa.pub root@$remote_host
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "$remote_pass\r" }
}
expect eof
EOF
if [ $? -eq 0 ]; then
echo "免密码配置成功"
else
echo "免密码配置失败"
fi
4.5 实战:批量远程执行命令
bash
#!/bin/bash
# 功能:批量在远程主机执行命令
# 主机列表
host_list=("10.0.0.12" "10.0.0.13" "10.0.0.14")
login_pass="123456"
remote_cmd="$1"
if [ -z "$remote_cmd" ]; then
echo "用法: $0 <远程命令>"
exit 1
fi
for host in "${host_list[@]}"; do
echo "========== $host =========="
/usr/bin/expect << EOF
set timeout 30
spawn ssh root@$host "$remote_cmd"
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "$login_pass\r" }
}
expect eof
EOF
echo ""
done
五、实战综合案例
5.1 堡垒机脚本
把前面学到的知识综合起来,实现一个简单的堡垒机:
bash
#!/bin/bash
# 功能:简易堡垒机
# 配置信息
declare -A hosts=(
[1]="10.0.0.12"
[2]="10.0.0.13"
[3]="10.0.0.14"
)
declare -A host_names=(
[1]="Nginx服务器"
[2]="MySQL服务器"
[3]="Redis服务器"
)
login_user="root"
login_pass="123456"
# 显示菜单
show_menu() {
echo -e "\033[31m"
echo "========================================="
echo " 欢迎使用堡垒机系统"
echo "========================================="
echo -e "\033[0m"
for key in $(echo "${!hosts[@]}" | tr ' ' '\n' | sort); do
echo " $key) ${host_names[$key]} (${hosts[$key]})"
done
echo " q) 退出"
echo "========================================="
}
# SSH连接
ssh_connect() {
local host="$1"
expect << EOF
set timeout 30
spawn ssh $login_user@$host
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "$login_pass\r" }
}
interact
EOF
}
# 主循环
while true; do
show_menu
read -p "请选择主机编号: " choice
if [ "$choice" == "q" ]; then
echo "再见!"
exit 0
elif [[ -v hosts[$choice] ]]; then
echo "正在连接 ${host_names[$choice]}..."
ssh_connect "${hosts[$choice]}"
else
echo "无效选项,请重新选择"
fi
done
5.2 日志分析脚本
bash
#!/bin/bash
# 功能:Nginx访问日志分析
LOG_FILE="${1:-/var/log/nginx/access.log}"
if [ ! -f "$LOG_FILE" ]; then
echo "日志文件不存在: $LOG_FILE"
exit 1
fi
echo "========== 日志分析报告 =========="
echo "日志文件: $LOG_FILE"
echo "日志行数: $(wc -l < "$LOG_FILE")"
echo ""
# 访问量TOP10的IP
echo "--- 访问量TOP10的IP ---"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
echo ""
# HTTP状态码统计
echo "--- HTTP状态码统计 ---"
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
echo ""
# 访问量TOP10的URL
echo "--- 访问量TOP10的URL ---"
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
echo ""
# 每小时访问量统计
echo "--- 每小时访问量 ---"
awk -F'[/:]' '{print $2}' "$LOG_FILE" | sort | uniq -c | sort -k2n
echo "==================================="