Shell函数与自动化:让脚本从“能用“进化到“好用“
Shell函数与自动化:让脚本从"能用"进化到"好用"
前面几篇我们已经能写出带判断、有循环的脚本了。但随着脚本越来越长,你可能会发现一个问题:同样的代码在好几个地方重复出现,改一处漏一处,维护起来很头疼。这时候就需要函数来帮忙了。今天我们聊聊Shell函数、数组、信号处理,以及如何用expect实现自动化交互。
一、函数:代码复用的利器
1.1 函数的定义和调用
Shell函数有两种定义方式:
# 方式1:function关键字functionsay_hello{echo"Hello,$1!"}# 方式2:函数名+括号(推荐)say_hello(){echo"Hello,$1!"}# 调用函数say_hello"World"注意:函数必须先定义后调用,否则会报"command not found"。
1.2 函数参数和返回值
Shell函数的参数传递方式和脚本参数一样,用$1、$2接收:
#!/bin/bash# 计算两数之和add(){localnum1=$1# local声明局部变量localnum2=$2echo$((num1+num2))}result=$(add35)echo"3 + 5 =$result"关于返回值:Shell函数有两种"返回"方式:
return:返回状态码(0-255),通过$?获取echo:返回任意值,通过$(函数名)获取
#!/bin/bash# 演示两种返回方式# 方式1:return返回状态码is_root(){if["$(whoami)"=="root"];thenreturn0# 成功elsereturn1# 失败fi}ifis_root;thenecho"当前是root用户"elseecho"当前不是root用户"fi# 方式2:echo返回数据get_ip(){localip=$(hostname-I|awk'{print $1}')echo"$ip"}my_ip=$(get_ip)echo"本机IP:$my_ip"1.3 局部变量:local关键字
在函数中使用local关键字声明的变量,只在函数内部有效,不会污染外部环境:
#!/bin/bashname="全局变量"test_func(){localname="局部变量"echo"函数内:$name"}test_funcecho"函数外:$name"# 输出:# 函数内: 局部变量# 函数外: 全局变量1.4 函数库:代码拆分与复用
当脚本变得很长时,可以把公共函数抽到单独的文件中作为"函数库":
# 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;thenlog_error"命令$1未安装"return1fi}# 确保目录存在ensure_dir(){[-d"$1"]||mkdir-p"$1"}#!/bin/bash# 主脚本 - 加载函数库# 加载公共函数source./lib/common.sh log_info"开始执行部署..."check_command"docker"ensure_dir"/opt/app/logs"log_info"部署完成"1.5 实战:系统信息采集函数
#!/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 索引数组
# 定义数组fruits=("apple""banana""cherry""date")# 访问元素echo${fruits[0]}# apple(下标从0开始)echo${fruits[@]}# 所有元素echo${#fruits[@]}# 数组长度# 遍历数组forfruitin"${fruits[@]}";doecho"水果:$fruit"done# 按下标遍历foriin"${!fruits[@]}";doecho"索引$i:${fruits[$i]}"done# 添加元素fruits+=("elderberry")# 删除元素unsetfruits[1]# 删除banana2.2 关联数组(字典)
Shell 4.0+支持关联数组,可以用字符串作为下标:
# 声明关联数组(必须用declare -A)declare-Auser_info# 赋值user_info[name]="张三"user_info[age]=25user_info[city]="北京"# 访问echo"姓名:${user_info[name]}"echo"年龄:${user_info[age]}"# 遍历所有keyforkeyin"${!user_info[@]}";doecho"$key:${user_info[$key]}"done2.3 实战:服务管理脚本
#!/bin/bash# 功能:多服务管理# 定义服务操作映射declare-Aservice_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_nameread-p"请输入操作: "action# 检查服务是否在列表中if[["${services[*]}"=~"$svc_name"]];thenif[[-vservice_ops[$action]]];thenecho"执行:${service_ops[$action]}$svc_name"${service_ops[$action]}"$svc_name"elseecho"无效操作:$action"fielseecho"未知服务:$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命令可以让你自定义脚本收到信号时的行为:
#!/bin/bash# 演示trap信号捕获# 定义清理函数cleanup(){echo""echo"收到退出信号,正在清理..."rm-f/tmp/lock_fileecho"清理完成,退出脚本"exit0}# 捕获SIGINT和SIGTERM信号trapcleanup SIGINT SIGTERM# 创建锁文件touch/tmp/lock_fileecho"脚本运行中,按Ctrl+C退出..."whiletrue;doecho"$(date'+%T')- 运行中..."sleep2done3.3 脚本退出时的清理
#!/bin/bash# 确保脚本退出时一定执行清理temp_dir=$(mktemp-d)# 捕获EXIT信号(脚本退出时触发)trap"rm -rf$temp_dir; echo '临时目录已清理'"EXIT# 使用临时目录echo"数据">"$temp_dir/data.txt"ls"$temp_dir"# 脚本正常结束时会自动触发EXITecho"脚本执行完成"四、expect:自动化交互
4.1 为什么需要expect
在实际运维中,很多操作需要交互式输入,比如SSH登录输入密码、passwd命令修改密码等。手动操作没问题,但批量执行时就需要自动化工具了。expect就是专门解决这个问题的。
4.2 安装expect
# CentOS/RHELyuminstall-yexpect# Ubuntu/Debianapt-getinstall-yexpect4.3 expect基本语法
#!/usr/bin/expect# expect脚本的基本结构# 设定超时时间settimeout30# 启动一个进程spawnsshroot@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代码:
#!/bin/bash# 功能:SSH免密码登录配置remote_host="$1"remote_pass="$2"if[-z"$remote_host"]||[-z"$remote_pass"];thenecho"用法:$0<远程主机> <密码>"exit1fi# 生成密钥对[-f~/.ssh/id_rsa]||ssh-keygen-trsa-P""-f~/.ssh/id_rsa# 使用expect自动发送公钥/usr/bin/expect<<EOF set timeout 30 spawn ssh-copy-id -i ~/.ssh/id_rsa.pub root@$remote_hostexpect { "yes/no" { send "yes\r"; exp_continue } "password:" { send "$remote_pass\r" } } expect eof EOFif[$?-eq0];thenecho"免密码配置成功"elseecho"免密码配置失败"fi4.5 实战:批量远程执行命令
#!/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"];thenecho"用法:$0<远程命令>"exit1fiforhostin"${host_list[@]}";doecho"==========$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 EOFecho""done五、实战综合案例
5.1 堡垒机脚本
把前面学到的知识综合起来,实现一个简单的堡垒机:
#!/bin/bash# 功能:简易堡垒机# 配置信息declare-Ahosts=([1]="10.0.0.12"[2]="10.0.0.13"[3]="10.0.0.14")declare-Ahost_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"forkeyin$(echo"${!hosts[@]}"|tr' ''\n'|sort);doecho"$key)${host_names[$key]}(${hosts[$key]})"doneecho" q) 退出"echo"========================================="}# SSH连接ssh_connect(){localhost="$1"expect<<EOF set timeout 30 spawn ssh$login_user@$hostexpect { "yes/no" { send "yes\r"; exp_continue } "password:" { send "$login_pass\r" } } interact EOF}# 主循环whiletrue;doshow_menuread-p"请选择主机编号: "choiceif["$choice"=="q"];thenecho"再见!"exit0elif[[-vhosts[$choice]]];thenecho"正在连接${host_names[$choice]}..."ssh_connect"${hosts[$choice]}"elseecho"无效选项,请重新选择"fidone5.2 日志分析脚本
#!/bin/bash# 功能:Nginx访问日志分析LOG_FILE="${1:-/var/log/nginx/access.log}"if[!-f"$LOG_FILE"];thenecho"日志文件不存在:$LOG_FILE"exit1fiecho"========== 日志分析报告 =========="echo"日志文件:$LOG_FILE"echo"日志行数:$(wc-l<"$LOG_FILE")"echo""# 访问量TOP10的IPecho"--- 访问量TOP10的IP ---"awk'{print $1}'"$LOG_FILE"|sort|uniq-c|sort-rn|head-10echo""# HTTP状态码统计echo"--- HTTP状态码统计 ---"awk'{print $9}'"$LOG_FILE"|sort|uniq-c|sort-rnecho""# 访问量TOP10的URLecho"--- 访问量TOP10的URL ---"awk'{print $7}'"$LOG_FILE"|sort|uniq-c|sort-rn|head-10echo""# 每小时访问量统计echo"--- 每小时访问量 ---"awk-F'[/:]''{print $2}'"$LOG_FILE"|sort|uniq-c|sort-k2necho"==================================="