简介
? ? 本篇文章从函数的特点开始介绍 ,教会小白如何定义函数,学习函数中的各种方法,最后整理了一些实际的应用场景来帮助大家学会如何灵活应用。
????????
文章目录如下:
????????
????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 函数名(){}
写法二:函数名(){}
????????
函数是一段在 Shell 脚本中定义的可执行代码块,它可以被多次调用并返回结果。通过定义函数,可以将一系列命令和操作组织到一个可重用的代码块中,以提高脚本的可读性和可维护性。
函数包含以下几个优点:
代码重用性:使用函数封装一组经常使用的命令后,可以使得这些代码可以在脚本中多次调用。这样可以提高代码的重用性,减少代码冗余。
模块化和可读性:将一段代码封装成函数,可以提高脚本的模块化程度,使得脚本更易于理解和维护。
参数传递:函数可以接受参数,并在调用时传递参数。这使得函数更加灵活,可以根据不同的参数执行不同的操作。
局部变量:函数中定义的变量默认是局部变量,只在函数内部可见。函数调用完成后自动销毁,减少内存使用量。
????????
定义函数的语法有2种,关键字+函数名,或者直接写函数名。例如:可以这样写
function func1(){
代码块
}
也可以这样写
func1(){
代码块
}
????????
当然了,像我们这种懒人,一般都使用第二种方式定义函数。知道如何定义函数后,那么函数的名称有没有什么特殊的要求呢?
虽然函数的名称可以自定义,但是大家有没有发现一个问题,那就是变量名。我们平时写变量名一般是 "字母_字母" 的样式,比如:content_provider。如果我们函数也这么写,当代码量达到一定程度后,怎么区分这个是变量还是函数呢?所以在定义函数名称时,尽量使用驼峰方式(单词首字母大写),比如这样写
ContentProvider(){
代码块
}
????????
1、缩进:函数中的代码块缩进一个tab或四个空格,有助于代码的可读性和可维护性。
Func(){
if xxx
代码块
fi
}
????????
2、注释:在函数的顶部需要注释一行对这个函数的说明,这样能使代码更加清晰和易于理解。代码块中的注释就看自己的习惯呗。
Func(){
# 这个函数是用于举例子的
if xxx
代码块
fi
}
如果需要注释的字符比较多,那么我们可以使用 <<EOF
Func(){
<<EOF
这个函数是用于举例子的
函数包含了xxx
EOF # 注意:结尾的EOF不能使用空格,只能使用tab
if xxx
代码块
fi
}
????????
3、占位符:如果该函数暂时不使用,但又担心删了容易忘,那么放个占位符吧( : 符号)
Func(){
:
}
如果不放占位符,那么会出现这样的错误
????????
调用函数的方法很简单,不需要什么关键字,直接写函数名就行。如下:
# 定义函数
Func(){
echo "我是一个函数!!!"
}
# 调用函数
Func
结果如下
注意:调用函数是区分大小写的,如果函数名是大写,调用使用小写就会出现找不到命令的错误
????????
除了直接调用函数,我们还可以把函数当作命令,给某个变量赋值
Func(){
echo "我是一个函数!!!"
}
var="$(Func)"
echo "变量var的结果: ${var}"
结果如下
注意:将函数作为命令使用时,需要加上 $() 或者 `` 符号。
????????
当然了,函数也可以被其他函数或自身调用
Func1(){
echo "我是一个函数1"
}
Func2(){
# 调用其他函数
Func1
echo "我是一个函数2"
}
Func2
结果如下
注意:使用函数调用自身时要检查好代码,否则一不小心就会一直递归。错误示例:
Func1(){
echo "我是一个函数1"
# 调用自身
Func1
}
Func1
????????
相信小伙伴们学习了《目录2:定义函数的方法》以后,都理解了如何去定义一个函数,那么接下来就进入正题:函数怎么用?有哪些用法?当前目录3将按正常的代码逻辑分别讲述如何去声明一个局部变量,它的作用什么,再去慢慢深入到传参的方法以及返回值的应用。
先了解一下什么是局部变量?
- 局部变量是在程序或代码块中定义的变量,其作用范围仅限于该代码块或函数内部。当函数或代码块执行完毕后,局部变量将被销毁,这意味着在其他部分无法访问该变量。
局部变量有什么优点?
在大型项目中,可能存在多个相同名称的变量。我们可以通过将变量限制在局部作用域内,可以避免因命名冲突而引起的错误。并且不同的函数可以使用相同的变量名,而不会相互干扰。
- 局部变量只在其所在的函数中存在,并且在函数执行完毕后会被销毁。这意味着,在程序执行期间不会占用额外的内存空间,提高了内存的利用效率。
????????
局部变量通过关键字 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}
可以看到全局变量被修改
????????
总结
????????
编写代码可以通过对函数传参的方式来提高代码灵活性,比如需要将某个文件夹下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[@]}
结果如下
效果和 $@ 是一样的,根据不同的需求选择不同的方法吧。
????????
什么是返回值?
函数的返回值提供了一种机制来向调用者传递信息,以便调用者根据函数的执行结果进行适当的处理。通过返回值,可以实现错误处理、结果传递和多状态返回等功能。这样可以增加程序的灵活性和可扩展性,并使代码更易于维护和调试。
函数的返回值是通过关键字 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 就是首选。
????????
在《目录3基本用法》中列举了很多例子,相信大家基本已经学会如何去写一个函数了,这里再列举一些复杂点的例子,以便更快速的掌握函数的方法。
编写一个按指定次数和间隔时间去监控硬件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个函数:
结果如下
????????
编写一个输出随机加法、减法、乘法、除法的计算题,并判断是否输出答案。执行语法如下
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行