简介
本篇文章从函数的特点开始介绍 ,教会小白如何定义函数,学习函数中的各种方法,最后整理了一些实际的应用场景来帮助大家学会如何灵活应用。
文章目录如下:
1. 了解什么是shell函数
1.1. 函数的历史
1.2. 函数有哪些特点
2. 定义函数的方法
2.1. 定义函数的语法
2.2. 函数中的代码块怎么写
2.3. 调用函数的方法
3. 函数的用法
3.1. 声明局部变量
3.2. 函数传参的方法
3.3. 函数的返回值
4. 实际应用
4.1. 实时监控硬件资源
4.2. 随机输出小学生计算题
1. 了解什么是shell函数
1.1. 函数的历史
Bourne Shell 是一个最初由 Stephen Bourne 写成的 Unix shell,它于 1977 年首次发布,是 Unix 环境中最早也是最基本的 shell 之一。Bourne Shell 中的函数定义使用 function
关键字或只使用小括号 ()
来定义函数名。后来,Bourne Shell 的功能逐渐得到完善和增强,产生了多种 shell,如 C Shell、Korn Shell、Bash 等。这些新的 shell 每一种都支持函数,但有所不同。例如,C Shell 使用 ()
作为函数定义的符号,而 Bash 可以使用 function
或 ()
两种方式来定义函数名。
写法一:function 函数名(){}
写法二:函数名(){}
1.2. 函数有哪些特点
函数是一段在 Shell 脚本中定义的可执行代码块,它可以被多次调用并返回结果。通过定义函数,可以将一系列命令和操作组织到一个可重用的代码块中,以提高脚本的可读性和可维护性。
函数包含以下几个优点:
代码重用性:使用函数封装一组经常使用的命令后,可以使得这些代码可以在脚本中多次调用。这样可以提高代码的重用性,减少代码冗余。
模块化和可读性:将一段代码封装成函数,可以提高脚本的模块化程度,使得脚本更易于理解和维护。
参数传递:函数可以接受参数,并在调用时传递参数。这使得函数更加灵活,可以根据不同的参数执行不同的操作。
局部变量:函数中定义的变量默认是局部变量,只在函数内部可见。函数调用完成后自动销毁,减少内存使用量。
2. 定义函数的方法
- 当前目录2主要介绍最简单的定义方法、代码块写法、和调用方法。
2.1. 定义函数的语法
定义函数的语法有2种,关键字+函数名,或者直接写函数名。例如:可以这样写
function func1(){
代码块
}
也可以这样写
func1(){
代码块
}
当然了,像我们这种懒人,一般都使用第二种方式定义函数。知道如何定义函数后,那么函数的名称有没有什么特殊的要求呢?
- 函数名可以以字母、数字、下划线开头,名称自定义。
- 函数名不支持纯数字。因为使用纯数字作为函数名,可能到导致运算出错。
虽然函数的名称可以自定义,但是大家有没有发现一个问题,那就是变量名。我们平时写变量名一般是 "字母_字母" 的样式,比如:content_provider。如果我们函数也这么写,当代码量达到一定程度后,怎么区分这个是变量还是函数呢?所以在定义函数名称时,尽量使用驼峰方式(单词首字母大写),比如这样写
ContentProvider(){
代码块
}
2.2. 函数中的代码块怎么写
- 函数中的代码块与我们平时写的代码块一样,也支持变量、命令、函数调用等,所以在语法上没什么区别,直接干就行。但写的时候有几个要点需要注意一下:
1、缩进:函数中的代码块缩进一个tab或四个空格,有助于代码的可读性和可维护性。
Func(){
if xxx
代码块
fi
}
2、注释:在函数的顶部需要注释一行对这个函数的说明,这样能使代码更加清晰和易于理解。代码块中的注释就看自己的习惯呗。
Func(){
# 这个函数是用于举例子的
if xxx
代码块
fi
}
如果需要注释的字符比较多,那么我们可以使用 <<EOF
- 以<<EOF开头,注释写在下一行。
- 以EOF结尾,结尾要么放在最左边,要么使用 tab,不能使用空格。
- EOF不是固定的,也可以使用其他字符代替,但结尾字符必须与开头字符一致。
Func(){
<<EOF
这个函数是用于举例子的
函数包含了xxx
EOF # 注意:结尾的EOF不能使用空格,只能使用tab
if xxx
代码块
fi
}
3、占位符:如果该函数暂时不使用,但又担心删了容易忘,那么放个占位符吧( : 符号)
Func(){
:
}
如果不放占位符,那么会出现这样的错误
2.3. 调用函数的方法
调用函数的方法很简单,不需要什么关键字,直接写函数名就行。如下:
# 定义函数
Func(){
echo "我是一个函数!!!"
}
# 调用函数
Func
结果如下
注意:调用函数是区分大小写的,如果函数名是大写,调用使用小写就会出现找不到命令的错误
除了直接调用函数,我们还可以把函数当作命令,给某个变量赋值
Func(){
echo "我是一个函数!!!"
}
var="$(Func)"
echo "变量var的结果: ${var}"
结果如下
注意:将函数作为命令使用时,需要加上 $() 或者 `` 符号。
当然了,函数也可以被其他函数或自身调用
Func1(){
echo "我是一个函数1"
}
Func2(){
# 调用其他函数
Func1
echo "我是一个函数2"
}
Func2
结果如下
注意:使用函数调用自身时要检查好代码,否则一不小心就会一直递归。错误示例:
Func1(){
echo "我是一个函数1"
# 调用自身
Func1
}
Func1
3. 函数的用法
相信小伙伴们学习了《目录2:定义函数的方法》以后,都理解了如何去定义一个函数,那么接下来就进入正题:函数怎么用?有哪些用法?当前目录3将按正常的代码逻辑分别讲述如何去声明一个局部变量,它的作用什么,再去慢慢深入到传参的方法以及返回值的应用。
3.1. 声明局部变量
先了解一下什么是局部变量?
- 局部变量是在程序或代码块中定义的变量,其作用范围仅限于该代码块或函数内部。当函数或代码块执行完毕后,局部变量将被销毁,这意味着在其他部分无法访问该变量。
局部变量有什么优点?
在大型项目中,可能存在多个相同名称的变量。我们可以通过将变量限制在局部作用域内,可以避免因命名冲突而引起的错误。并且不同的函数可以使用相同的变量名,而不会相互干扰。
- 局部变量只在其所在的函数中存在,并且在函数执行完毕后会被销毁。这意味着,在程序执行期间不会占用额外的内存空间,提高了内存的利用效率。
局部变量通过关键字 local 来声明,示例
Func1(){
# 方法一
local var=1 # 将变量var声明为局部变量
}
Func2(){
# 方法二
local var1 var2 # 将变量var1和var2都声明为局部变量(这里可以声明多个变量)
var1=1
var2=2
}
变量可被赋值的对象与普通变量一致,所以不用担心语法问题。
在 shell 中,我们可以将普通变量当做成全局变量,因为只有在函数中通过 local 声明的变量才能成为局部变量,未声明的变量可以作用到全局。那么什么是局部变量,什么是全局变量呢 ?
我们将房子比喻成脚本、将人类比喻成全局变量、将宠物比喻为局部变量。
- 人类可以在猫窝和狗窝出入
- 猫只能在猫窝出入
- 狗只能在狗窝出入
所以,全局变量可以任何地方使用(整个脚本或函数中),但函数中的局部变量只能在函数中使用,无法作用到函数外。
举个例子,准备2个函数,两个函数中分别使用相同的变量,输出结果来看看
Func1(){
local var=10 # 函数1中的局部变量
echo ${var}
}
Func2(){
local var=20 # 函数2中的局部变量
echo ${var}
}
Func1 # 调用函数1
Func2 # 调用函数2
echo ${var} # 在全局输出局部变量
在函数1中声明局部变量为10,输出结果为10。
在函数2中声明局部变量为20,输出结果为20。
在全局没有声明变量 var,所以输出为空。
前面我们知道了函数中的局部变量无法作用到全局,那么在函数中调用函数呢?
Func1(){
# 函数1中声明一个变量var
local var=10
}
Func2(){
# 调用函数1,打印这个var变量
Func1
echo ${var}
}
Func2 # 调用函数2
结果如下
在函数2中调用函数1,仍然不能复用函数1声明的局部变量。
如果我们先在一个函数中声明了局部变量,再去调用另一个函数呢
Func1(){
# 打印变量var
echo ${var}
}
Func2(){
# 在函数2中声明一个局部变量
local var=20
# 调用函数1
Func1
}
Func2 # 调用函数2
结果如下
复用局部变量成功!!!
这是因为我们先在 Func2 中声明了一个局部变量,然后去调用 Func1。这时的 Func1 就属于 Func2 的一部分,所以 Func2 的变量可以作用到 Func1。
注意:Func1 中的局部变量依然只能作用到 Func1。
我们再来看看:当全局变量名称与局部变量相同时,变量的值是否会被修改
# 定义一个全局变量
var=10
Func1(){
# 定义一个局部变量
local var=20
echo "局部中的var: ${var}"
}
Func1
echo "全局中的var: ${var}"
结果如下
我们分别在全局定义一个变量 var,函数定义一个局部变量 var,大致流程如下:
全局的 var 代入函数,如果函数中没有 var 则使用全局变量,反之函数中有 var 则使用自身变量,不会修改全局。
那么在函数中不使用 local 声明局部,会修改全局变量吗?
# 定义一个全局变量
var=10
Func1(){
# 函数中不声明局部变量
var=20
}
Func1
# 打印这个变量
echo ${var}
可以看到全局变量被修改
总结
- 函数中声明的局部变量无法作用到函数以外,名称可以随便写。
- 函数中声明局部变量后再去调用另一个函数,该变量可以作用到被调用的函数身上。
- 函数中的局部变量与全局变量相同时,函数以自身局部变量为准,不会改变全局变量的值。
- 函数中不使用 local 声明变量,则该变量为全局变量,可以作用到全局。
3.2. 函数传参的方法
编写代码可以通过对函数传参的方式来提高代码灵活性,比如需要将某个文件夹下500多个文件中的空行删除,因为每个文件名字不相同,如果不使用函数则需要编写500行代码。使用函数将更加简单
# 定义一个删除文件空行的函数
DeleteBlankLine(){
sed -i '/^$/d' $1
}
# 查询这些文件并依次调用函数
for file in $(find ./file/ -type f);do
DeleteBlankLine "${file}" # 向函数传入一个文件路径的参数
done
我们封装一个删除空行的函数,使用 for 循环遍历文件,将其传递给函数逐个删除空行。可能有小伙伴会认为:我不使用函数,直接将 sed 命令放在 for 循环下面岂不是更简单。这样当然可以的,这里只是为了举一个例子。如果需求不只是删除空行,还有其他一大堆需求呢,那必然需要函数来使得代码更加整洁。当然了,是否使用函数还是看需求,我们的宗旨是代码简洁、易阅读就行。
对函数传参的方式很简单,在调用函数后面添加字符串即可
函数名 "参数1" "参数2" "参数n"
向函数传入参数后,代码块中通过 $1 $2 ... $n 等方式调用。$1 表示传入的第1个参数,$2 表示传入的第2个参数,以此类推。
Func1(){
echo $1 # 打印第1个参数值
echo $2 # 打印第2个参数值
}
Func1 "参数1" "参数2"
除了指定第n个参数外,还可以直接获取参数的个数($#)和全部参数($@、$*)。
Func1(){
echo $# # 打印参数个数
echo $@ # 打印全部参数
echo $* # 打印全部参数
}
Func1 "参数1" "参数2"
了解完如何传参后,来看一个例子:检查 IP192.168.1.7 和 192.168.1.8 是否能ping通
# 封装一个检查IP的方法
TestAddress(){
local ip="$1"
if ping -c 3 ${ip} &>/dev/null;then
echo "IP ${ip} 可以ping通!"
else
echo "IP ${ip} 无法ping通!"
fi
}
# 调用这个方法,分别检测两个IP
TestAddress "192.168.1.7"
TestAddress "192.168.1.8"
结果如下
我们封装了一个函数用于检测 IP 是否能 ping 通,在这个例子中固定只使用一个参数,这可能显得有些局限性。当需要检测的 IP 达到一定数量时,需要通过多次调用才能达到目的,显得代码比较繁琐。
我们来利用 $@ 来优化代码,将需要检测的IP放入数组中,直接将该数组的值全部传入函数。
# 需要测试的IP
test_ip=(192.168.1.7 192.168.1.8 192.168.1.9)
# 封装一个检查IP的方法
TestAddress(){
# 遍历所有IP
for ip in $@;do
if ping -c 3 ${ip} &>/dev/null;then
echo "IP ${ip} 可以ping通!"
else
echo "IP ${ip} 无法ping通!"
fi
done
}
# 调用这个方法(多个IP也只需要调用一次)
TestAddress ${test_ip[@]}
结果如下:
除了使用 $@ 方法,还可以使用 shift 来删除第一个的参数。先来看一个简单的示例
Func(){
# 删除第1个参数
shift
# 打印当前第一个参数
echo "第1个参数的值是:$1"
# 打印全部参数
echo "当前的全部参数包含:$@"
}
# 向函数中传入3个参数
Func "AAA" "BBB" "CCC"
将第一个参数删除,保留了剩下的参数,那么第2个参数就变成了第1个参数
也可以指定删除n个参数:shift [n]
Func(){
# 删除前2个参数
shift 2
# 打印当前第一个参数
echo "第1个参数的值是:$1"
# 打印全部参数
echo "当前的全部参数包含:$@"
}
# 向函数中传入3个参数
Func "AAA" "BBB" "CCC"
我们将这个方法代入测试IP中
# 需要测试的IP
test_ip=(192.168.1.7 192.168.1.8 192.168.1.9)
# 封装一个检查IP的方法
TestAddress(){
# 参数个数不等于0时,循环执行
while [ $# -ne 0 ];do
# 检测第1个参数
local ip="$1"
if ping -c 3 ${ip} &>/dev/null;then
echo "IP ${ip} 可以ping通!"
else
echo "IP ${ip} 无法ping通!"
fi
# 检测完后删除第一个参数
shift
done
}
TestAddress ${test_ip[@]}
结果如下
效果和 $@ 是一样的,根据不同的需求选择不同的方法吧。
3.3. 函数的返回值
什么是返回值?
函数的返回值提供了一种机制来向调用者传递信息,以便调用者根据函数的执行结果进行适当的处理。通过返回值,可以实现错误处理、结果传递和多状态返回等功能。这样可以增加程序的灵活性和可扩展性,并使代码更易于维护和调试。
函数的返回值是通过关键字 return 来返回一个 0~255 的整数。示例:
Func(){
return 10 # 指定一个返回值
}
Func # 调用函数
echo $? # 查看状态码
代码中,使用了 return 返回一个整数 10,这个10并不会输出到屏幕,而是返回到状态码中。所以我们可以利用返回值判断这个函数是否执行成功。
Func(){
return 10 # 指定一个返回值
}
Func # 调用函数
# 判断返回值是否为0,0表示正常,非0表示异常
if [ $? -eq 0 ];then
echo "函数没有出错"
else
echo "函数出错了"
fi
所以,函数是否出错可以手动指定。
需要注意的是,返回值是无法赋值给变量的
Func(){
return 10
}
var=$(Func) # 将函数结果赋值给变量
echo ${var} # 打印这个变量
但是,输出的结果是可以赋值的(将 return 改为 echo )
Func(){
echo 10 # 输出一个结果
}
var=$(Func) # 将函数结果赋值给变量
echo ${var} # 打印这个变量
使用 return 无法赋值给变量,而 echo 可以赋值。也就是说:当需要使用多个函数关联调度时,我们不需要使用 return,可直接使用 echo 返回,再利用 echo 的返回值来判断下一阶段应该如何调度。
当使用 return 后,函数将自动退出,return 下面的代码将不再执行
Func(){
return 0
echo "执行下一步"
}
Func
这种情况在什么地方可以用到呢?举一个例子:监控某个进程的物理内存、虚存的使用情况
# 封装一个监控进程资源的函数
MonitorProcess(){
local pid=$1
# 检查PID是否存在
if [ ! -d /proc/${pid} ];then
# 不存在则退出
return 1
else
# 存在则输出内存使用情况
local current_time="$(date '+%Y-%m-%d %H:%M:%S')"
local vm_rss=$(awk '/^VmRSS/ {print $2 $3}' /proc/${pid}/status)
local vm_size=$(awk '/^VmSize/ {print $2 $3}' /proc/${pid}/status)
echo "[${current_time}] 虚拟内存大小: ${vm_size}, 物理内存大小: ${vm_rss}"
fi
}
# 调用这个函数,传入一个正确的PID
MonitorProcess "进程ID"
如果该进程 ID 存在则查看内存使用情况,如果不存在则退出函数。这样写的好处就是:当整个脚本代码量较多时,发现某些地方出错时,我们希望退出某个函数而整个脚本的其他代码依然正常执行,那么 return 就是首选。
4. 实际应用
在《目录3基本用法》中列举了很多例子,相信大家基本已经学会如何去写一个函数了,这里再列举一些复杂点的例子,以便更快速的掌握函数的方法。
4.1. 实时监控硬件资源
编写一个按指定次数和间隔时间去监控硬件CPU使用率、内存使用情况、磁盘使用情况,通过表格的方式输出结果。执行语法如下
sh [脚本] [间隔时间] [监控次数]
# 封装一个输出表格的方法
OutputTable(){
local str_interval=20 # 设定每个单元格的长度
local symbol='—' # 设定表格的字符
local col=$# # 列数
local arr=($@) # 接收所有数组
local mode=${arr[0]} # 第1个参数表示模式
local data=(${arr[@]:1}) # 第1个参数后面的表示表格中的数据
# 计算输出的字符长度
local total_length=$(((col - 1) * str_interval + col ))
local table_symbol="$(perl -E "say '${symbol}' x ${total_length}")"
# 如果模式为0,输出表头
if [ ${mode} -eq 0 ];then
echo -e "${table_symbol}"
for ((i=0; i<${#data[@]}; i++));do
[ $[i+1] -ne ${#data[@]} ] && printf "|%-${str_interval}s" "${data[${i}]}" || printf "|%-${str_interval}s|\n" "${data[${i}]}"
done
echo -e "${table_symbol}"
# 如果模式为1,输出数据信息
elif [ ${mode} -eq 1 ];then
for ((i=0; i<${#data[@]}; i++));do
[ $[i+1] -ne ${#data[@]} ] && printf "|%-${str_interval}s" "${data[${i}]}" || printf "|%-${str_interval}s|\n" "${data[${i}]}"
done
echo -e "${table_symbol}"
fi
}
# 封装一个监控硬件资源的方法
MonitoringHardware(){
# 定义监控间隔时间和监控次数
local interval=$1
local count=$2
# 输出表格头
OutputTable 0 "CPU_used" "Memory_used" "Disk_used" "Monitoring_time"
for ((c=1; c<=count; c++));do
# 获取CPU利用率、内存使用大小、磁盘使用大小、时间
local current_time="$(date '+%Y-%m-%d_%H:%M:%S')"
local cpu_used=$(top -b -n 1 -p 1 |awk -F , '/^%Cpu/ {print $4}' |awk '{print 100 - $1 "%"}')
local mem_used=$(free -h |awk 'NR==2{print $3 "/" $2}')
local disk_used=$(df -h `pwd` |awk 'NR==2{print $3 "/" $2}')
# 使用表格输出数据
OutputTable 1 "${cpu_used}" "${mem_used}" "${disk_used}" "${current_time}"
sleep ${interval}
done
}
# 调用这个监控函数,使用外部传参来指定监控间隔时间和次数
MonitoringHardware $1 $2
这里封装了2个函数:
- OutputTable:以表格的形式输出结果。
- MonitoringHardware:按指定的监控次数获取CPU、内存、磁盘的使用情况,调用 OutputTable 以表格的形式输出。
结果如下
4.2. 随机输出小学生计算题
编写一个输出随机加法、减法、乘法、除法的计算题,并判断是否输出答案。执行语法如下
sh [脚本] [列数] [行数] [模式]
# 列数:一行输出多少列
# 行数:总共输出多少行
# 模式:5种模式
# add:加法
# sub:减法
# mul:乘法
# div:除法
# all:全部输出
# 是否输出答案
output_answer="yes"
# 设定列数
col=$1
# 设定行数
row=$2
# 设定运算符(加法:add, 减法:sub, 乘法:mul, 除法:div, 全部:all)
operator="$3"
# 设定数字在多少以内
integer_size=100
# 封装一个加法的函数
Calculations(){
local interval=20
for ((r=1; r<=row; r++));do
result=""
for ((c=1; c<=col; c++));do
int1=$((RANDOM % integer_size))
int2=$((RANDOM % integer_size))
# 输出加法
if [ "${operator}" == "add" ];then
result="${result} $(awk "BEGIN{print ${int1} + ${int2}}")"
str="${int1} + ${int2} = ?"
printf "%-${interval}s" "${str}"
# 输出减法
elif [ "${operator}" == "sub" ];then
result="${result} $(awk "BEGIN{print ${int1} - ${int2}}")"
str="${int1} - ${int2} = ?"
printf "%-${interval}s" "${str}"
# 输出乘法
elif [ "${operator}" == "mul" ];then
result="${result} $(awk "BEGIN{print ${int1} * ${int2}}")"
str="${int1} * ${int2} = ?"
printf "%-${interval}s" "${str}"
# 输出除法
elif [ "${operator}" == "div" ];then
[ ${int2} -eq 0 ] && int2=1
result="${result} $(awk -v "n1=${int1}" -v "n2=${int2}" 'BEGIN{printf "%.2f", n1 / n2}')"
str="${int1} / ${int2} = ?"
printf "%-${interval}s" "${str}"
fi
done
# 输出答案
[ "${output_answer}" == "yes" ] && printf "%-${interval}s\n" "answer:${result}" || echo
done
}
# 如果全部输出,则调用4次函数
if [ "${operator}" == "all" ];then
for i in add sub mul div;do
operator="${i}"
Calculations
done
# 如果只输出一种模式,按原方法调用
else
Calculations
fi
输出3列5行的加法,并输出答案
输出3列5行的加法,不输出答案(修改变量 output_answer="no" )
输出全部加、减、乘、除计算题,每种模式输出3列2行