Shell函数与自动化:让脚本从“能用“进化到“好用“

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 "==================================="

相关推荐
MAHATMA玛哈特科技2 小时前
校平机在自动化产线中如何“无缝衔接“
运维·自动化·校平机·矫平机·校平机厂家
IT小黄人_9992 小时前
联想服务器更换硬盘后手动重建
运维·服务器
求知若渴,虚心若愚。2 小时前
Jenkins 自动化流水线(CICD)
运维·自动化·gitlab
困意少年2 小时前
Linux 进程概念深度解析:从 `task_struct` 到进程状态、优先级、调度与上下文切换
linux·运维
一头老黄牛@2 小时前
飞书 × OpenClaw 接入指南:不用服务器,用长连接把机器人跑起来
数据结构·人工智能·程序人生·算法·决策树·自动化·推荐算法
V搜xhliang024610 小时前
AI智能体的数据安全与合规实践
人工智能·学习·数据分析·自动化·ai编程
杨浦老苏10 小时前
家庭实验室监控仪表盘HomeLab-Monitor
运维·docker·监控·群晖
见合八方10 小时前
【滤波器】用于红外微型光谱仪的可调谐MEMS-FP滤光片-综述
自动化·soa·光通信·激光雷达·半导体光放大器
回忆2012初秋11 小时前
【Nginx】原理、配置与运维实战(2)
运维·nginx·策略模式