Shell编程之函数与数组
目录
一:Shell函数
1:函数的用法
(1)两个数求和
(2)系统资源监控并报警函数
2:函数变量的作用范围
1.变量作用范围类型
2.作用范围规则表格
3:函数的参数
1.函数参数基础
2.参数传递模式对比
4:递归函数
1.递归函数关键要素
2.递归优化技巧
二:Shell数组
1. 索引数组(普通数组)
2. 关联数组(键值对)
(1)获取数组长度
(2) 读取某下标赋值
(3)数组遍历
(4)数组切片
(5)数组替换
(6)数组删除
三: Shell脚本调试
1.基础调试方法
2.调试工具
3.常见错误排查表
4.性能调试
本章总结
一:Shell函数
1:函数的用法
Shell函数可用于存放一系列的指令。在 Shell脚本执行的过程中,函数被置于内存中,每次调用函数时不需要从硬盘读取,因此运行的速度比较快。在 Shell编程中函数并非是必须的元素,但使用函数可以对程序进行更好的组织。将一些相对独立的代码变成函数,可以提高程序可读性与重用性,避免编写大量重复代码。
Shell函数定义的方法如下所示:
[function] 函数名(){
命令序列
[return x]
}
1. “function”关键字表示定义一个函数,可以省略;
2. “{”符号表示函数执行命令的入口,该符号可以与函数名同行也可以在函数名下一行的句首;
3. “}”符号表示函数体结束,两个大括号之间{}是函数体;
4. “命令序列”部分可以是任意的 Shell命令,也可以调用其他函数;
5. “return”表示退出函数返回一个退出值,通过返回值判断执行是否成功,也可以使用 exit 终止整个 Shell脚本。
Shell函数调用的方法为:函数名·[参数1] [参数2]。 下面通过具体的示例学习函数的定义与调用。
(1)两个数求和
使用 Shell脚本实现两个数相加求和,通过定义函数的方式来完成。sum 函数内部通过 read 命令接收用户分别输入的两个数,然后做加法运算,最后通过调用函数的方式来输出两个数的和。
功能 | 语法 | 示例 | 说明 |
---|---|---|---|
定义函数 | function_name() { commands; } 或 function function_name { commands; } | greet() { echo "Hello $1"; } | 两种定义方式等效,第一种更常用 |
调用函数 | function_name [arguments] | greet "World" | 像普通命令一样调用,可以传参数 |
返回值 | return [n] | return 0 | 返回状态码(0-255),0表示成功 |
获取返回值 | $? | echo $? | 获取上一条命令/函数的返回值 |
参数传递 | $1, $2, ..., $n | echo "First arg: $1" | 函数内使用位置参数获取传入的参数 |
参数个数 | $# | echo "Args count: $#" | 获取传递给函数的参数个数 |
所有参数 | $@ 或 $* | for arg in "$@"; do ... | 获取所有参数,"$@"保留各参数独立性 |
局部变量 | local var=value | local name=$1 | 函数内使用local声明局部变量 |
导出函数 | export -f function_name | export -f greet | 使函数在子shell中可用 |
(2)系统资源监控并报警函数
该函数会定期监控系统的 CPU 和内存使用率,当使用率超过设定的阈值时,会发送报警信息,这里简单模拟为输出到控制台,实际应用中可扩展为发送邮件、短信等。
功能 | 函数实现 | 报警方式 | 示例调用 | 说明 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
CPU使用率监控 | `check_cpu() { local threshold=1;localusage=1;localusage=(top -bn1 | grep "Cpu(s)" | sed "s/., [0−9.]∗[0−9.]∗% id./\1/" | awk '{print 100 - (echo "usage>usage>threshold" | bc) -eq 1 ] && return 1 | return 0; }` | 返回值 | `check_cpu 90 && echo "CPU正常" | echo "CPU超过阈值"` | 监控CPU使用率是否超过阈值 | ||
内存使用监控 | `check_mem() { local threshold=1;localfree=1;localfree=(free -m | awk '/Mem:/{print (free -m | awk '/Mem:/{print ((100 - free*100/total)); [ usage−gtusage−gtthreshold ] && return 1 | return 0; }` | 返回值 | `check_mem 85 && echo "内存正常" | echo "内存超过阈值"` | 监控内存使用百分比 | ||||
磁盘空间监控 | `check_disk() { local path=1;localthreshold=1;localthreshold=2; local usage=(df−h(df−hpath | awk 'NR==2{print $5}' | tr -d '%'); [ usage−gtusage−gtthreshold ] && return 1 | return 0; }` | 返回值 | `check_disk "/" 90 && echo "磁盘正常" | echo "磁盘空间不足"` | 监控指定挂载点磁盘使用率 | ||||
进程存在性检查 | `check_process() { local process=1;pgrep−x1;pgrep−xprocess >/dev/null && return 0 | return 1; }` | 返回值 | `check_process "nginx" && echo "Nginx运行中" | echo "Nginx未运行"` | 检查指定进程是否运行 | ||||||
端口监听检查 | `check_port() { local port=$1; netstat -tuln | grep -q ":$port " && return 0 | return 1; }` | 返回值 | `check_port 80 && echo "80端口已监听" | echo "80端口未监听"` | 检查指定端口是否被监听 | |||||
发送邮件报警 | `send_alert() { local subject=1;localbody=1;localbody=2; echo "$body" | mail -s "$subject" admin@example.com; }` | 邮件 | send_alert "CPU警报" "CPU使用率超过90%" | 使用mail命令发送报警邮件 | |||||||
发送HTTP通知 | send_webhook() { local url=$1; local message=$2; curl -X POST -H 'Content-Type: application/json' -d "{\"text\":\"$message\"}" $url; } | Webhook | send_webhook "https://hooks.example.com" "服务器异常" | 通过HTTP请求发送报警通知 |
1. 参数说明:函数接收三个参数,分别是PU使用率阈值、内存使用率阈值以及监控间隔时间(秒)。
2. 循环监控:使用 while true 循环不断监控系统资源。通过 top 和 free 命令获取 CPU 和内存使用率,使用 bc 命令进行浮点数比较。
3. 报警处理:当使用率超过阈值时,调用send alert 函数发送报警信息。
2:函数变量的作用范围
在 Shell脚本中函数的执行并不会开启一个新的子 Shell,而是仅在当前定义的 shell环境中有效。如果Shell脚本中的变量没有经过特殊设定,默认在整个脚本中都是有效的。
1.变量作用范围类型
变量类型 | 声明方式 | 作用范围 | 生命周期 | 示例 |
---|---|---|---|---|
全局变量 | 默认声明(无修饰符) | 整个脚本(包括函数内外) | 从声明到脚本结束 | var="value" |
局部变量 | 使用local 关键字 | 仅在当前函数内有效 | 函数执行期间 | local var="value" |
环境变量 | 使用export 命令 | 当前脚本及其子进程 | 直到进程结束 | export VAR="value" |
位置参数 | $1 , $2 , ..., $n | 函数内部(会覆盖脚本参数) | 函数执行期间 | echo "First arg: |
2.作用范围规则表格
规则 | 说明 | 示例 |
---|---|---|
1. 函数内未声明直接使用的变量 | 会访问或修改全局变量 | function f1() { var=10; } var=5; f1; echo $var # 输出10 |
2. 使用local 声明的变量 | 只在函数内有效,不影响外部同名变量 | function f2() { local var=20; } var=5; f2; echo $var # 输出5 |
3. 函数参数变量 | $1-$n 只在函数内有效,不影响脚本参数 | function f3() { echo $1; } f3 "hello" # 输出hello |
4. 函数内修改环境变量 | 需要使用export 或declare -x | function f4() { export VAR=30; } |
5. 子shell中的变量 | 不会影响父shell的变量 | var=5; (var=10); echo $var # 输出5 |
6. 函数返回值 | 通过return 返回状态码,通过echo 返回数据 | function f5() { echo "data"; return 0; } |
在编写脚本时,有时需要将变量的值限定在函数内部,可以通过内置命令 local 来实现。函数内部变量的使用,可以避免函数内外同时出现同名变量对脚本结果的影响。local 命令的使用如下所示。
上述脚本中,myfun 函数内部使用了 local 命令设置变量 i,其作用是将变量 i 限定在函数内部。myfun 函数外部同样定义了变量 i,内部变量 i 和全局变量 i 互不影响。脚本执行时先调用了函数myfun,函数内部变量i为 8,所以输出结果是 8。调用完函数之后,给变量i 赋值为 9,再打印外部变量 i,所以又输出 9。
3:函数的参数
函数的参数的用法如下。
函数名称 参数 1 参数 2 参数 3......
1.函数参数基础
参数变量 | 描述 | 示例 |
---|---|---|
$1 - $n | 位置参数,表示第1到第n个参数 | echo "第一个参数: $1" |
$0 | 函数名(在大多数Shell中),脚本名(在某些Shell中) | echo "函数名: $0" |
$# | 传递给函数的参数个数 | echo "参数个数: $#" |
$@ | 所有参数的列表,每个参数作为独立字符串 | for arg in "$@"; do echo "$arg"; done |
$* | 所有参数的列表,合并为单个字符串 | echo "所有参数: $*" |
2.参数传递模式对比
传递方式 | 语法 | 特点 | 适用场景 |
---|---|---|---|
位置参数 | func arg1 arg2 | 简单直接 | 少量简单参数 |
选项参数 | func -a val1 -b val2 | 可读性强,顺序灵活 | 复杂参数设置 |
数组展开 | func "${array[@]}" | 传递数组内容 | 需要处理数组时 |
Here String | func <<< "$data" | 传递字符串 | 简单字符串输入 |
文件描述符 | func <(command) | 传递命令输出 | 需要处理命令输出时 |
在使用函数参数时,函数名称在前参数在后,函数名和参数之间用空格分隔,可以有多个参数,参数使用$1、$2、$3.的方式表示。以此类推,从第 10 个参数开始,调用方法为${10},不加大括号无法调用成功。下面是函数参数的一个简单应用。
第一个参数是写日志的目标文件
第二个参数是日志信息
整个脚本实现将日志信息写入目标文件内的目的。
4:递归函数
Shell也可以实现递归函数,就是可以调用自己本身的函数。在 Linux 系统上编写 shell脚本的时候,经常需要递归遍历系统的目录,列出目录下的文件和目录,逐层递归列出,并对这些层级关系进行展示。具体的实现过程如下所示。
函数 list_files 的第一个参数是列举的目录名,第二个参数是调整的空间。
递归函数是指调用自身的函数,在 Shell 脚本中也可以实现递归功能。
1.递归函数关键要素
要素 | 说明 | 示例 |
---|---|---|
终止条件 | 必须存在明确的递归结束条件 | if [ $n -eq 0 ]; then return; fi |
参数变化 | 每次递归调用参数必须向终止条件靠近 | factorial $((n-1)) |
局部变量 | 必须使用local 声明变量,避免递归间干扰 | local result=$(recursive_func) |
返回值 | 可通过return 返回状态码或echo 返回数据 | echo $result 或 return 0 |
栈深度 | Shell有递归深度限制(通常几百层) | ulimit -s 查看栈大小 |
2.递归优化技巧
技巧 | 说明 | 示例 |
---|---|---|
尾递归 | 将递归调用放在函数最后,减少栈使用 | tail_recursive $((n-1)) |
记忆化 | 缓存已计算结果,避免重复计算 | 使用关联数组存储中间结果 |
迭代替代 | 对深度递归考虑用循环改写 | for/while 循环替代递归 |
尾调用优化 | 确保递归调用是最后一步操作 | return $(recursive_call) |
二:Shell数组
在 shell脚本中,数组是一种常见的数据结构,主要的应用场景包括:获取数组长度、获取元素长度、遍历元素、元素切片、元素替换、元素删除等等。shell中的数组与 Java、c、Python 不同,只有一维数组,没有二维数组。数组元素的大小与限制,也不需要事先定义。shell数组用括号()来表示,元素用空格分隔,元素的下标与大部分编程语言类似从0开始。
数组常用定义方法包括以下几种。
方法一:
数组名=(value0 value1 value2...)
方法二:
数组名=([0]=value[1]=value[2]=value ...)
方法三:
列表名=”value0 value1 value2 ...数组名=($列表名)
方法四:
数组名[0]=”value”数组名[2]=”value’数组名[1]=”value”
1. 索引数组(普通数组)
操作 | 语法 | 示例 |
---|---|---|
声明数组 | array=(value1 value2 value3) | fruits=("apple" "banana" "cherry") |
设置元素 | array[index]=value | fruits[0]="apricot" |
获取元素 | ${array[index]} | echo ${fruits[1]} # 输出 banana |
获取所有元素 | ${array[@]} 或 ${array[*]} | echo ${fruits[@]} |
数组长度 | ${#array[@]} | echo ${#fruits[@]} # 输出元素个数 |
获取索引 | ${!array[@]} | echo ${!fruits[@]} # 输出 0 1 2 |
2. 关联数组(键值对)
操作 | 语法 | 示例 |
---|---|---|
声明关联数组 | declare -A array | declare -A user |
设置元素 | array[key]=value | user[name]="Alice" |
获取元素 | ${array[key]} | echo ${user[name]} |
获取所有值 | ${array[@]} | echo ${user[@]} |
获取所有键 | ${!array[@]} | echo ${!user[@]} |
数组长度 | ${#array[@]} | echo ${#user[@]} |
下面通过具体的示例掌握数组的基本使用方法。
(1)获取数组长度
在 Shell编程中,数组是一种用于存储多个值的数据结构。获取数组长度(即数组中元素的个数)在很多场景下都非常有用,比如进行数组遍历、数据处理等。普通数组是 shell中最常见的数组类型,其下标是从 0开始的连续整数。获取普通数组长度可以使用 ${#数组名[@]}或 ${#数组名[*]} 这两种语法,它们的效果是一样的。
(2) 读取某下标赋值
在 shell编程里,你可以读取数组中指定下标的元素,也可以给指定下标的数组元素赋值。普通数组是 Shell中最常用的数组类型,其下标是从0开始的整数。
(3)数组遍历
在 Shell编程里,数组遍历指的是按顺序访问数组中的每一个元素,并对这些元素执行特定操作的过程。这在处理一组相关数据时非常有用,比如批量处理文件、统计数据等。
(4)数组切片
数组切片是一种操作,用于从数组中提取出一部分连续的元素,形成一个新的数组。这个操作在很多编程语言中都有支持,不过不同语言实现数组切片的语法和方式会有所差异。在Bash 脚本里,也可以进行数组切片操作。语法是 ${array[@]:start:length},其中 start 是开始的索引,length 是要提取的元素个数。
将数组切片之后,返回的是字符串,以空格作为分隔符。
(5)数组替换
(6)数组删除
三: Shell脚本调试
在 shell脚本开发中,经常碰到一些规范方面的问题,例如忘了使用引号或在 if 语句末尾处忘记加 fi 结束。要注意把复杂的脚本简单化,要思路清晰,并且分段实现。当执行脚本时出现错误后,不要只看那些提示的错误行,而是要观察整个相关的代码段。
为避免编写的脚本出错,除了在编写脚本时注意书写规范,排除语法错误,更重要的是利用调试脚本工具来调试脚本。echo 命令是最有用的调试脚本工具之一,一般在可能出现问题的脚本中加入 echo 命令,采用的是分段排查的方式。
除了 echo 命令之外,bash shell也有相应参数可以调试脚本。使用 bash 命令参数调试,命令的语法为:
sh [-nvx] 脚本名
常用参数的具体含义为:
参数 | 含义 |
-n | 不会执行该脚本,仅查询脚本语法是否有问题,如果没有语法问题就不显示任何内容,如果有问题会提示报错。 |
-v | 在执行脚本时,先将脚本的内容输出到屏幕上然后执行脚本,如果有错误,也会给出错误提示。 |
-x | 将执行的脚本内容输出到屏幕上,这个是对调试很有用的参数。 |
1.基础调试方法
方法 | 命令/选项 | 说明 | 示例 |
---|---|---|---|
打印调试 | echo /printf | 输出变量和状态信息 | echo "变量值: $var" |
执行跟踪 | set -x /set +x | 显示执行的每条命令及其参数 | set -x your_script.sh set +x |
详细模式 | set -v /set +v | 显示读取的输入行 | set -v your_script.sh set +v |
严格模式 | set -euo pipefail | 组合严格检查选项 | set -euo pipefail |
语法检查 | bash -n | 只检查语法不执行 | bash -n script.sh |
2.调试工具
工具 | 用途 | 示例 |
---|---|---|
shellcheck | 静态分析工具 | shellcheck script.sh |
bashdb | Bash调试器 | bashdb script.sh |
vscode/bash插件 | 集成开发环境调试 | 配置launch.json |
xtrace | 增强的跟踪输出 | PS4='+${BASH_SOURCE}:${LINENO}: ' bash -x script.sh |
3.常见错误排查表
错误类型 | 排查方法 | 解决方案 |
---|---|---|
语法错误 | bash -n script.sh | 检查括号、引号匹配 |
变量未定义 | set -u | 确保变量已初始化 |
命令失败 | echo $? | 检查命令返回值 |
权限问题 | ls -l | 添加执行权限 chmod +x |
文件不存在 | test -f file | 检查文件路径 |
管道错误 | set -o pipefail | 检查管道中所有命令 |
参数错误 | echo "$@" | 验证参数数量 ${#@} |
4.性能调试
方法 | 说明 | 示例 |
---|---|---|
time | 测量命令执行时间 | time your_script.sh |
pv | 监控管道数据流 | cat bigfile.log | pv | grep "error" |
strace | 跟踪系统调用 | strace -f -o trace.log bash script.sh |
perf | 性能分析工具 | perf stat bash script.sh |
当脚本文件较长时,可以使用 set 命令指定调试一段脚本。
本章总结
函数:函数是 shell脚本中组织代码、提高复用性的重要手段。通过将一系列指令封装成函数,可以避免代码重复,使脚本更加简洁易读。函数的定义与调用简单直观,支持参数传递和递归调用,为模块化开发提供了便利。
数组:作为 Shell脚本中的一种重要数据结构,数组支持存储多个值,并提供了丰富的操作方法,如获取数组长度、读取和赋值、遍历、切片、替换和删除元素等。这些功能在处理一组相关数据时非常有用。
调试与测试:在 Shell脚本编写过程中,调试与测试是确保脚本正确运行的关键环节。通过使用 echo命令和 bash 的调试参数(-n、-v、-x),可以方便地定位和解决问题,提高脚本的稳健性。