Shell函数与自动化让脚本从能用进化到好用前面几篇我们已经能写出带判断、有循环的脚本了。但随着脚本越来越长你可能会发现一个问题同样的代码在好几个地方重复出现改一处漏一处维护起来很头疼。这时候就需要函数来帮忙了。今天我们聊聊Shell函数、数组、信号处理以及如何用expect实现自动化交互。一、函数代码复用的利器1.1 函数的定义和调用Shell函数有两种定义方式# 方式1function关键字functionsay_hello{echoHello,$1!}# 方式2函数名括号推荐say_hello(){echoHello,$1!}# 调用函数say_helloWorld注意函数必须先定义后调用否则会报command not found。1.2 函数参数和返回值Shell函数的参数传递方式和脚本参数一样用$1、$2接收#!/bin/bash# 计算两数之和add(){localnum1$1# local声明局部变量localnum2$2echo$((num1num2))}result$(add35)echo3 5 $result关于返回值Shell函数有两种返回方式return返回状态码0-255通过$?获取echo返回任意值通过$(函数名)获取#!/bin/bash# 演示两种返回方式# 方式1return返回状态码is_root(){if[$(whoami)root];thenreturn0# 成功elsereturn1# 失败fi}ifis_root;thenecho当前是root用户elseecho当前不是root用户fi# 方式2echo返回数据get_ip(){localip$(hostname-I|awk{print $1})echo$ip}my_ip$(get_ip)echo本机IP:$my_ip1.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_commanddockerensure_dir/opt/app/logslog_info部署完成1.5 实战系统信息采集函数#!/bin/bash# 功能系统信息采集# CPU使用率get_cpu_usage(){top-bn1|grepCpu(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-Fload average:{print $2}|tr-d }# 采集信息echo 系统状态 echoCPU使用率:$(get_cpu_usage)%echo内存使用率:$(get_mem_usage)%echo磁盘使用率:$(get_disk_usage)echo系统负载:$(get_load_average)echo二、数组批量数据处理2.1 索引数组# 定义数组fruits(applebananacherrydate)# 访问元素echo${fruits[0]}# apple下标从0开始echo${fruits[]}# 所有元素echo${#fruits[]}# 数组长度# 遍历数组forfruitin${fruits[]};doecho水果:$fruitdone# 按下标遍历foriin${!fruits[]};doecho索引$i:${fruits[$i]}done# 添加元素fruits(elderberry)# 删除元素unsetfruits[1]# 删除banana2.2 关联数组字典Shell 4.0支持关联数组可以用字符串作为下标# 声明关联数组必须用declare -Adeclare-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(nginxmysqlredis)# 显示菜单echo 服务管理 echo服务列表:${services[*]}echo操作类型:${!service_ops[]}echoread-p请输入服务名: svc_nameread-p请输入操作: action# 检查服务是否在列表中if[[${services[*]}~$svc_name]];thenif[[-vservice_ops[$action]]];thenecho执行:${service_ops[$action]}$svc_name${service_ops[$action]}$svc_nameelseecho无效操作:$actionfielseecho未知服务:$svc_namefi三、信号处理与脚本健壮性3.1 Linux信号基础Linux通过信号与进程通信。常见的信号有信号编号说明SIGHUP1挂起进程SIGINT2中断进程CtrlCSIGQUIT3退出进程SIGKILL9强制终止不可捕获SIGTERM15优雅终止SIGTSTP20暂停进程CtrlZ3.2 trap捕获信号trap命令可以让你自定义脚本收到信号时的行为#!/bin/bash# 演示trap信号捕获# 定义清理函数cleanup(){echoecho收到退出信号正在清理...rm-f/tmp/lock_fileecho清理完成退出脚本exit0}# 捕获SIGINT和SIGTERM信号trapcleanup SIGINT SIGTERM# 创建锁文件touch/tmp/lock_fileecho脚本运行中按CtrlC退出...whiletrue;doecho$(date%T)- 运行中...sleep2done3.3 脚本退出时的清理#!/bin/bash# 确保脚本退出时一定执行清理temp_dir$(mktemp-d)# 捕获EXIT信号脚本退出时触发traprm -rf$temp_dir; echo 临时目录已清理EXIT# 使用临时目录echo数据$temp_dir/data.txtls$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# 启动一个进程spawnsshroot10.0.0.12# 等待匹配关键字expect{yes/no{sendyes\r;exp_continue}password:{send123456\r}}# 交还控制权给用户interact核心命令spawn启动一个新进程expect等待匹配指定字符串send发送字符串到进程interact交还控制权给用户exp_continue继续匹配后续的expect4.4 Shell中嵌入expect实际工作中我们通常在Shell脚本中嵌入expect代码#!/bin/bash# 功能SSH免密码登录配置remote_host$1remote_pass$2if[-z$remote_host]||[-z$remote_pass];thenecho用法:$0远程主机 密码exit1fi# 生成密钥对[-f~/.ssh/id_rsa]||ssh-keygen-trsa-P-f~/.ssh/id_rsa# 使用expect自动发送公钥/usr/bin/expectEOF 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.1210.0.0.1310.0.0.14)login_pass123456remote_cmd$1if[-z$remote_cmd];thenecho用法:$0远程命令exit1fiforhostin${host_list[]};doecho$host/usr/bin/expectEOF set timeout 30 spawn ssh root$host$remote_cmd expect { yes/no { send yes\r; exp_continue } password: { send $login_pass\r } } expect eof EOFechodone五、实战综合案例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_userrootlogin_pass123456# 显示菜单show_menu(){echo-e\033[31mechoecho 欢迎使用堡垒机系统echoecho-e\033[0mforkeyin$(echo${!hosts[]}|tr \n|sort);doecho$key)${host_names[$key]}(${hosts[$key]})doneecho q) 退出echo}# SSH连接ssh_connect(){localhost$1expectEOF 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[$choiceq];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_FILEexit1fiecho 日志分析报告 echo日志文件:$LOG_FILEecho日志行数:$(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
Shell函数与自动化:让脚本从“能用“进化到“好用“
发布时间:2026/6/14 1:31:41
Shell函数与自动化让脚本从能用进化到好用前面几篇我们已经能写出带判断、有循环的脚本了。但随着脚本越来越长你可能会发现一个问题同样的代码在好几个地方重复出现改一处漏一处维护起来很头疼。这时候就需要函数来帮忙了。今天我们聊聊Shell函数、数组、信号处理以及如何用expect实现自动化交互。一、函数代码复用的利器1.1 函数的定义和调用Shell函数有两种定义方式# 方式1function关键字functionsay_hello{echoHello,$1!}# 方式2函数名括号推荐say_hello(){echoHello,$1!}# 调用函数say_helloWorld注意函数必须先定义后调用否则会报command not found。1.2 函数参数和返回值Shell函数的参数传递方式和脚本参数一样用$1、$2接收#!/bin/bash# 计算两数之和add(){localnum1$1# local声明局部变量localnum2$2echo$((num1num2))}result$(add35)echo3 5 $result关于返回值Shell函数有两种返回方式return返回状态码0-255通过$?获取echo返回任意值通过$(函数名)获取#!/bin/bash# 演示两种返回方式# 方式1return返回状态码is_root(){if[$(whoami)root];thenreturn0# 成功elsereturn1# 失败fi}ifis_root;thenecho当前是root用户elseecho当前不是root用户fi# 方式2echo返回数据get_ip(){localip$(hostname-I|awk{print $1})echo$ip}my_ip$(get_ip)echo本机IP:$my_ip1.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_commanddockerensure_dir/opt/app/logslog_info部署完成1.5 实战系统信息采集函数#!/bin/bash# 功能系统信息采集# CPU使用率get_cpu_usage(){top-bn1|grepCpu(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-Fload average:{print $2}|tr-d }# 采集信息echo 系统状态 echoCPU使用率:$(get_cpu_usage)%echo内存使用率:$(get_mem_usage)%echo磁盘使用率:$(get_disk_usage)echo系统负载:$(get_load_average)echo二、数组批量数据处理2.1 索引数组# 定义数组fruits(applebananacherrydate)# 访问元素echo${fruits[0]}# apple下标从0开始echo${fruits[]}# 所有元素echo${#fruits[]}# 数组长度# 遍历数组forfruitin${fruits[]};doecho水果:$fruitdone# 按下标遍历foriin${!fruits[]};doecho索引$i:${fruits[$i]}done# 添加元素fruits(elderberry)# 删除元素unsetfruits[1]# 删除banana2.2 关联数组字典Shell 4.0支持关联数组可以用字符串作为下标# 声明关联数组必须用declare -Adeclare-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(nginxmysqlredis)# 显示菜单echo 服务管理 echo服务列表:${services[*]}echo操作类型:${!service_ops[]}echoread-p请输入服务名: svc_nameread-p请输入操作: action# 检查服务是否在列表中if[[${services[*]}~$svc_name]];thenif[[-vservice_ops[$action]]];thenecho执行:${service_ops[$action]}$svc_name${service_ops[$action]}$svc_nameelseecho无效操作:$actionfielseecho未知服务:$svc_namefi三、信号处理与脚本健壮性3.1 Linux信号基础Linux通过信号与进程通信。常见的信号有信号编号说明SIGHUP1挂起进程SIGINT2中断进程CtrlCSIGQUIT3退出进程SIGKILL9强制终止不可捕获SIGTERM15优雅终止SIGTSTP20暂停进程CtrlZ3.2 trap捕获信号trap命令可以让你自定义脚本收到信号时的行为#!/bin/bash# 演示trap信号捕获# 定义清理函数cleanup(){echoecho收到退出信号正在清理...rm-f/tmp/lock_fileecho清理完成退出脚本exit0}# 捕获SIGINT和SIGTERM信号trapcleanup SIGINT SIGTERM# 创建锁文件touch/tmp/lock_fileecho脚本运行中按CtrlC退出...whiletrue;doecho$(date%T)- 运行中...sleep2done3.3 脚本退出时的清理#!/bin/bash# 确保脚本退出时一定执行清理temp_dir$(mktemp-d)# 捕获EXIT信号脚本退出时触发traprm -rf$temp_dir; echo 临时目录已清理EXIT# 使用临时目录echo数据$temp_dir/data.txtls$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# 启动一个进程spawnsshroot10.0.0.12# 等待匹配关键字expect{yes/no{sendyes\r;exp_continue}password:{send123456\r}}# 交还控制权给用户interact核心命令spawn启动一个新进程expect等待匹配指定字符串send发送字符串到进程interact交还控制权给用户exp_continue继续匹配后续的expect4.4 Shell中嵌入expect实际工作中我们通常在Shell脚本中嵌入expect代码#!/bin/bash# 功能SSH免密码登录配置remote_host$1remote_pass$2if[-z$remote_host]||[-z$remote_pass];thenecho用法:$0远程主机 密码exit1fi# 生成密钥对[-f~/.ssh/id_rsa]||ssh-keygen-trsa-P-f~/.ssh/id_rsa# 使用expect自动发送公钥/usr/bin/expectEOF 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.1210.0.0.1310.0.0.14)login_pass123456remote_cmd$1if[-z$remote_cmd];thenecho用法:$0远程命令exit1fiforhostin${host_list[]};doecho$host/usr/bin/expectEOF set timeout 30 spawn ssh root$host$remote_cmd expect { yes/no { send yes\r; exp_continue } password: { send $login_pass\r } } expect eof EOFechodone五、实战综合案例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_userrootlogin_pass123456# 显示菜单show_menu(){echo-e\033[31mechoecho 欢迎使用堡垒机系统echoecho-e\033[0mforkeyin$(echo${!hosts[]}|tr \n|sort);doecho$key)${host_names[$key]}(${hosts[$key]})doneecho q) 退出echo}# SSH连接ssh_connect(){localhost$1expectEOF 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[$choiceq];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_FILEexit1fiecho 日志分析报告 echo日志文件:$LOG_FILEecho日志行数:$(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