echo "[$(date '+%Y-%m-%d %H:%M:%S')] 信息:xxxx"。复制粘贴倒也行,但万一哪天想改日志格式——比如加个日志级别——你得满脚本去找,改几十个地方,漏一个就乱了。这不就跟家里东西乱放一个道理吗?如果每个工具都有固定的抽屉,想用的时候拉开就拿,用完放回去,多省心。函数就是你的"抽屉"——把常用操作封装起来,命名好,随叫随到。数组就是你的"收纳盒"——把一堆相关的数据放一起,统一处理。这篇学完,你写的脚本就会从"能用"升级到"好维护"。走起。—## 二、Shell 函数### 2.1 函数定义与调用Shell 函数的定义有两种写法,效果完全一样:bash#!/bin/bash# 方式一:function 关键字(可读性更好)function say_hello { echo "你好,世界!"}# 方式二:括号形式(更简洁,推荐)say_hi() { echo "嗨,你好!"}# 调用函数——直接写函数名就行say_hellosay_hi执行结果:你好,世界!嗨,你好!注意:调用函数时不要加括号!say_hello() 是函数定义,say_hello 才是函数调用。很多人刚开始搞混,写成 say_hello() 来调用,Shell 会报错。而且函数定义必须出现在调用之前——Shell 是顺序执行的,它还没读到函数定义的时候,根本不知道 say_hello 是个啥。### 2.2 函数参数($1, $2, $@, $*)函数也可以接受参数,方式和脚本参数一模一样——用 $1、$2……$@、$*、$#:bash#!/bin/bash# 带参数的函数greet() { echo "你好,$1!你今年 $2 岁了。" echo "你总共传了 $# 个参数"}# 调用并传参greet "小明" 25执行结果:你好,小明!你今年 25 岁了。你总共传了 2 个参数来,咱们写个实用点的——批量重命名打招呼:bash#!/bin/bash# 遍历传入的所有名字,挨个打招呼greet_all() { for name in "$@"; do echo "欢迎你,$name!" done}greet_all "张三" "李四" "王五" "赵六"执行结果:欢迎你,张三!欢迎你,李四!欢迎你,王五!欢迎你,赵六!这里 "$@" 会把每个参数都当作独立个体,而 "$*" 会合成一个字符串。在遍历时永远用 "$@",别问为什么,记住就行——我当年吃过这个亏。### 2.3 返回值与 returnShell 函数的 return 和大多数编程语言不一样:它只能返回 0~255 的整数,而且这个值本质上是退出状态码,不是通常意义上的"返回值"。bash#!/bin/bash# 判断数字是正是负check_number() { if [ "$1" -gt 0 ]; then return 0 # 正数,返回 0(成功) elif [ "$1" -lt 0 ]; then return 1 # 负数,返回 1(失败) else return 2 # 零 fi}check_number 5echo "退出码:$?" # $? 获取上一条命令的退出码check_number -3echo "退出码:$?"check_number 0echo "退出码:$?"执行结果:退出码:0退出码:1退出码:2看到了吗?$? 取到的是函数 return 的值,但这个值取值范围很小。假如你要回传一个数字怎么办? 比如你写了个加法函数想返回计算结果——return 可就尴尬了,因为 256 以上的值会溢出。正确的做法是输出到标准输出,用 $() 捕获:bash#!/bin/bash# 正确的"返回值"姿势——用 echo + 命令替换add() { local sum=$(( $1 + $2 )) echo "$sum" # 输出到标准输出}result=$(add 10 20) # $() 捕获函数输出echo "10 + 20 = $result"# 再算个大的result=$(add 1000 2000)echo "1000 + 2000 = $result"执行结果:10 + 20 = 301000 + 2000 = 3000总结一下:| 场景 | 用什么 ||------|--------|| 返回成功/失败状态 | return 0 / return 1,调用方用 $? 获取 || 返回数值/字符串结果 | echo "结果值",调用方用 $() 捕获 || 两者都需要 | echo 输出结果 + return 返回状态码 |### 2.4 局部变量 local这是初学者最容易忽略的一个点。默认情况下,函数里定义的变量是全局的——函数外面也能访问,函数执行完后变量仍然存在。这就带来了"变量污染"的风险——函数里不小心改了一个跟外面同名的变量,bug 就悄悄地来了。bash#!/bin/bash# 不加 local 的惨痛教训name="全局老王"change_name() { name="局部小李" # 没有 local!直接修改了全局变量! echo "函数内部:$name"}echo "调用前:$name"change_nameecho "调用后:$name" # 完了,全局变量被改了!执行结果:调用前:全局老王函数内部:局部小李调用后:局部小李 # 全局变量被污染了!加上 local 之后,变量只在函数内部生效:bash#!/bin/bashname="全局老王"change_name() { local name="局部小李" # 加 local,互不干扰 echo "函数内部:$name"}echo "调用前:$name"change_nameecho "调用后:$name" # 还是老王,稳如老狗执行结果:调用前:全局老王函数内部:局部小李调用后:全局老王规则很简单:函数内部用的变量,一律加 local。 养成这个习惯,能帮你避免无数莫名其妙的 bug。### 2.5 实战:日志函数 + 错误处理函数来看一个真实项目里几乎一定会用的东西——日志函数:bash#!/bin/bash# 文件名:logger_demo.sh# 日志函数:统一输出格式,方便以后调整log_info() { local msg="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $msg"}log_warn() { local msg="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $msg"}log_error() { local msg="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $msg" >&2 # >&2 输出到标准错误}# 错误处理函数:检查命令执行结果check_result() { if [ $? -ne 0 ]; then log_error "$1 失败!" exit 1 fi}# ---- 使用示例 ----log_info "开始部署应用..."# 模拟一个操作mkdir -p /tmp/test_appcheck_result "创建目录" # 如果 mkdir 成功了,$? 为 0,check_result 什么都不做log_info "正在复制文件..."cp -r ./src /tmp/test_app/check_result "复制文件"log_info "部署完成!"运行效果:[2026-06-26 22:00:00] [INFO] 开始部署应用...[2026-06-26 22:00:00] [INFO] 正在复制文件...[2026-06-26 22:00:00] [INFO] 部署完成!这段代码的价值在哪里?- 所有日志格式统一,想改时间格式只改一个地方- 错误处理集中到 check_result,不用每次写 if [ $? -ne 0 ]; then ...- 一旦某步失败,脚本自动退出,不会接着往下跑这就叫可维护性。—## 三、Shell 数组### 3.1 数组定义与访问Shell 数组用括号 () 定义,元素之间用空格分隔:bash#!/bin/bash# 数组定义fruits=("苹果" "香蕉" "橘子" "西瓜")# 访问单个元素——下标从 0 开始echo "第一个水果:${fruits[0]}"echo "第二个水果:${fruits[1]}"echo "第三个水果:${fruits[2]}"# 访问所有元素echo "所有水果:${fruits[@]}"# 访问下标越界——不会报错,返回空echo "第10个水果:${fruits[10]:-(不存在)}"执行结果:第一个水果:苹果第二个水果:香蕉第三个水果:橘子所有水果:苹果 香蕉 橘子 西瓜第10个水果:(不存在)注意:访问数组元素一定要用 ${} 花括号!写成 $fruits[0] 的话,Shell 会把 $fruits 展开成第一个元素,后面跟一个 [0]——完全不是你想要的。### 3.2 遍历数组遍历数组最常用的就是用 for 循环:bash#!/bin/bashservers=("web01" "web02" "db01" "cache01")echo "=== 服务器列表 ==="for server in "${servers[@]}"; do echo " - $server"done执行结果:=== 服务器列表 === - web01 - web02 - db01 - cache01还可以用下标遍历——适合需要知道索引的场景:bash#!/bin/bashscores=(85 92 78 95 88)for i in "${!scores[@]}"; do # "${!scores[@]}" 获取所有下标 echo "第 $((i + 1)) 个学生的成绩:${scores[$i]}"done执行结果:第 1 个学生的成绩:85第 2 个学生的成绩:92第 3 个学生的成绩:78第 4 个学生的成绩:95第 5 个学生的成绩:88注意:${!scores[@]} 的感叹号表示取下标列表,不是取反。这个感叹号在不同的上下文里意思不一样——这里就是"给我下标"。### 3.3 数组常用操作(长度/追加/删除/切片)获取数组长度:bash#!/bin/bashcolors=("红" "绿" "蓝")echo "数组长度:${#colors[@]}" # 输出:3echo "第一个元素长度:${#colors[0]}" # 输出:1("红"是一个字)追加元素:bash#!/bin/bashcolors=("红" "绿" "蓝")colors+=("黄" "紫") # 一次性追加多个echo "${colors[@]}" # 输出:红 绿 蓝 黄 紫删除元素(用 unset):bash#!/bin/bashnumbers=(10 20 30 40 50)unset numbers[2] # 删除第3个元素(下标 2)echo "${numbers[@]}" # 输出:10 20 40 50echo "下标列表:${!numbers[@]}" # 输出:0 1 3 4(注意下标 2 被删了)删掉中间的元素之后,数组中会留下一个"空洞"——后面元素的下标不会自动往前挪。遍历时用 "${!numbers[@]}" 拿有效下标就对了。切片(提取子数组):bash#!/bin/bashletters=("a" "b" "c" "d" "e" "f")# ${数组[@]:起始下标:个数}echo "${letters[@]:0:3}" # 输出:a b c(前3个)echo "${letters[@]:3:2}" # 输出:d e(第4个开始的2个)echo "${letters[@]:2}" # 输出:c d e f(第3个开始一直到尾)执行结果:a b cd ec d e f### 3.4 关联数组(declare -A)关联数组就是"键值对"——用字符串当下标,而不是数字:bash#!/bin/bash# 声明关联数组——必须用 declare -Adeclare -A user# 赋值user["name"]="张三"user["age"]=28user["city"]="北京"# 访问echo "姓名:${user["name"]}"echo "年龄:${user["age"]}"echo "城市:${user["city"]}"# 遍历所有键和值echo ""echo "=== 用户信息 ==="for key in "${!user[@]}"; do echo " $key:${user[$key]}"done执行结果:姓名:张三年龄:28城市:北京=== 用户信息 === name:张三 age:28 city:北京注意这个 declare -A 非常重要——不声明的话,Shell 会当普通数组处理,user["name"] 就变成了 user[0],全乱套了。关联数组在处理配置映射时特别有用:bash#!/bin/bash# 服务端口映射declare -A service_portsservice_ports["nginx"]=80service_ports["mysql"]=3306service_ports["redis"]=6379service_ports["ssh"]=22for svc in "${!service_ports[@]}"; do echo "$svc 运行在端口 ${service_ports[$svc]}"done执行结果:nginx 运行在端口 80mysql 运行在端口 3306redis 运行在端口 6379ssh 运行在端口 22—## 四、函数+数组实战### 4.1 批量文件处理脚本假设你有个目录,里面一堆 .log 文件,你想批量压缩超过 7 天的日志文件,然后删除压缩前的源文件。这在实际运维中再常见不过了:bash#!/bin/bash# 文件名:archive_old_logs.sh# 用途:压缩并归档7天前的日志文件# ---- 配置区 ----LOG_DIR="/var/log/myapp"ARCHIVE_DIR="/var/log/archives"RETENTION_DAYS=7# ---- 函数定义 ----log_info() { local msg="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $msg"}log_error() { local msg="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $msg" >&2}check_result() { if [ $? -ne 0 ]; then log_error "$1" exit 1 fi}# ---- 主逻辑 ----log_info "开始日志归档任务..."# 用数组收集所有 .log 文件log_files=("$LOG_DIR"/*.log)# 检查有没有日志文件if [ "${#log_files[@]}" -eq 0 ] || [ ! -f "${log_files[0]}" ]; then log_warn="没有找到 .log 文件" echo "$(date) [WARN] $log_warn" exit 0filog_info "找到 ${#log_files[@]} 个日志文件"# 确保归档目录存在mkdir -p "$ARCHIVE_DIR"# 遍历处理每个文件for file in "${log_files[@]}"; do # 获取文件的修改时间(Unix 时间戳) file_time=$(stat -c %Y "$file" 2>/dev/null) current_time=$(date +%s) file_days_old=$(( (current_time - file_time) / 86400 )) if [ "$file_days_old" -ge "$RETENTION_DAYS" ]; then filename=$(basename "$file") archive_name="$(basename "$filename" .log)_$(date +%Y%m%d).tar.gz" log_info "正在归档:$filename(已 $file_days_old 天)" # 压缩文件 tar -czf "$ARCHIVE_DIR/$archive_name" -C "$LOG_DIR" "$filename" check_result "压缩 $filename 失败" # 删除原文件 rm "$file" check_result "删除 $filename 失败" log_info "归档完成:$archive_name" else log_info "跳过:$(basename "$file")(仅 $file_days_old 天)" fidonelog_info "日志归档任务结束"这个脚本把前几篇学的知识点全都用上了——函数封装日志逻辑、数组收集文件列表、条件判断过滤文件、循环批量处理。写出来结构清晰,读完就知道每一步在干啥。### 4.2 菜单选择系统这是另一个很常见的场景——写一个交互式运维工具,让同事点点数字就能执行任务,不用记命令:bash#!/bin/bash# 文件名:ops_menu.sh# 用途:简单的运维菜单管理工具# ---- 菜单数据:用关联数组保存 ----declare -A menu_itemsmenu_items["1"]="查看系统信息"menu_items["2"]="检查磁盘空间"menu_items["3"]="查看当前连接数"menu_items["4"]="清理临时文件"menu_items["5"]="退出"# ---- 函数定义 ----show_menu() { echo "" echo "========== 运维工具箱 ==========" for key in $(echo "${!menu_items[@]}" | tr ' ' '\n' | sort); do echo " $key. ${menu_items[$key]}" done echo "================================"}do_task() { local choice="$1" case "$choice" in 1) echo "" echo "--- 系统信息 ---" echo "主机名:$(hostname)" echo "系统版本:$(cat /etc/os-release 2>/dev/null | head -1 || echo '未知')" echo "运行时间:$(uptime -p)" echo "内存总量:$(free -h | awk '/^Mem:/{print $2}')" echo "--- 系统信息 ---" ;; 2) echo "" echo "--- 磁盘使用情况 ---" df -h | grep -v tmpfs echo "--- 磁盘使用情况 ---" ;; 3) echo "" echo "--- 当前连接数 ---" ss -tun | tail -n +2 | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10 echo "--- 当前连接数 ---" ;; 4) echo "" echo "--- 清理 /tmp 下 7 天前的临时文件 ---" local count=0 for f in /tmp/*.tmp; do if [ -f "$f" ]; then local age=$(( ($(date +%s) - $(stat -c %Y "$f")) / 86400 )) if [ "$age" -ge 7 ]; then rm "$f" && count=$((count + 1)) fi fi done echo "已清理 $count 个临时文件" ;; 5) echo "再见!" exit 0 ;; *) echo "无效选项,请重新选择。" ;; esac}# ---- 主循环 ----while true; do show_menu read -p "请输入选项 [1-5]:" choice do_task "$choice"done用这种方式写的好处是:- 添加新功能只需要在 menu_items 加一项,在 do_task 里加一个分支- 菜单显示和任务执行完全分开,修改显示格式不影响功能- 任何人跑这个脚本都能操作,不用去记 df -h、ss -tun 这些命令—## 五、总结好,今天的内容就到这儿。来简单回顾一下:函数篇:- 定义函数:函数名() { ... } 或者 function 函数名 { ... }- 调用函数:直接写函数名,不要加括号- 参数传递:$1、$2……$@、$#,和脚本参数一模一样- return 只能返回 0~255 的退出码;要返回值用 echo + $() 命令替换- 所有函数内部变量都要加 local,避免污染全局变量数组篇:- 定义:数组=("元素1" "元素2" ...)- 访问:${数组[下标]}、${数组[@]}(全部元素)- 遍历:for v in "${数组[@]}" 或 for i in "${!数组[@]}"- 常用操作:${#数组[@]}(长度)、数组+=("新元素")(追加)、unset 数组[下标](删除)、${数组[@]:起始:个数}(切片)- 关联数组:declare -A 声明,用字符串当下标函数 + 数组的组合拳就能写出结构清晰、易于维护的脚本。再也不用把几千行代码全塞在 main 里了——把逻辑拆成函数,把数据装进数组,脚本的可读性和复用性瞬间提升一个档次。下期预告:我们会深入讲字符串处理和文件读写——sed、awk、grep 这些文本处理三剑客,以及怎么在 Shell 里读写配置文件。这些都是日常运维中最实用的技能,敬请期待。如果这篇对你有帮助,欢迎收藏转发,我们下篇见。
685

被折叠的 条评论
为什么被折叠?
