简介
本篇文章从函数的特点开始介绍 ,教会小白如何定义函数,学习函数中的各种方法,最后整理了一些实际的应用场景来帮助大家学会如何灵活应用。
文章目录如下:
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. 随机输出小学生计算题
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
}
如果需要注释的字符比较多,那么我们可以使用 < 3、占位符:如果该函数暂时不使用,但又担心删了容易忘,那么放个占位符吧( : 符号) 如果不放占位符,那么会出现这样的错误 调用函数的方法很简单,不需要什么关键字,直接写函数名就行。如下: 结果如下 注意:调用函数是区分大小写的,如果函数名是大写,调用使用小写就会出现找不到命令的错误 除了直接调用函数,我们还可以把函数当作命令,给某个变量赋值 结果如下 注意:将函数作为命令使用时,需要加上 $() 或者 `` 符号。 当然了,函数也可以被其他函数或自身调用 结果如下 注意:使用函数调用自身时要检查好代码,否则一不小心就会一直递归。错误示例: 相信小伙伴们学习了《目录2:定义函数的方法》以后,都理解了如何去定义一个函数,那么接下来就进入正题:函数怎么用?有哪些用法?当前目录3将按正常的代码逻辑分别讲述如何去声明一个局部变量,它的作用什么,再去慢慢深入到传参的方法以及返回值的应用。 先了解一下什么是局部变量? 局部变量有什么优点? 在大型项目中,可能存在多个相同名称的变量。我们可以通过将变量限制在局部作用域内,可以避免因命名冲突而引起的错误。并且不同的函数可以使用相同的变量名,而不会相互干扰。 局部变量通过关键字 local 来声明,示例 变量可被赋值的对象与普通变量一致,所以不用担心语法问题。 在 shell 中,我们可以将普通变量当做成全局变量,因为只有在函数中通过 local 声明的变量才能成为局部变量,未声明的变量可以作用到全局。那么什么是局部变量,什么是全局变量呢 ? 我们将房子比喻成脚本、将人类比喻成全局变量、将宠物比喻为局部变量。 所以,全局变量可以任何地方使用(整个脚本或函数中),但函数中的局部变量只能在函数中使用,无法作用到函数外。 举个例子,准备2个函数,两个函数中分别使用相同的变量,输出结果来看看 在函数1中声明局部变量为10,输出结果为10。 在函数2中声明局部变量为20,输出结果为20。 在全局没有声明变量 var,所以输出为空。 前面我们知道了函数中的局部变量无法作用到全局,那么在函数中调用函数呢? 结果如下 在函数2中调用函数1,仍然不能复用函数1声明的局部变量。 如果我们先在一个函数中声明了局部变量,再去调用另一个函数呢 结果如下 复用局部变量成功!!! 这是因为我们先在 Func2 中声明了一个局部变量,然后去调用 Func1。这时的 Func1 就属于 Func2 的一部分,所以 Func2 的变量可以作用到 Func1。 注意:Func1 中的局部变量依然只能作用到 Func1。 我们再来看看:当全局变量名称与局部变量相同时,变量的值是否会被修改 结果如下 我们分别在全局定义一个变量 var,函数定义一个局部变量 var,大致流程如下: 全局的 var 代入函数,如果函数中没有 var 则使用全局变量,反之函数中有 var 则使用自身变量,不会修改全局。 那么在函数中不使用 local 声明局部,会修改全局变量吗? 可以看到全局变量被修改 总结 编写代码可以通过对函数传参的方式来提高代码灵活性,比如需要将某个文件夹下500多个文件中的空行删除,因为每个文件名字不相同,如果不使用函数则需要编写500行代码。使用函数将更加简单 我们封装一个删除空行的函数,使用 for 循环遍历文件,将其传递给函数逐个删除空行。可能有小伙伴会认为:我不使用函数,直接将 sed 命令放在 for 循环下面岂不是更简单。这样当然可以的,这里只是为了举一个例子。如果需求不只是删除空行,还有其他一大堆需求呢,那必然需要函数来使得代码更加整洁。当然了,是否使用函数还是看需求,我们的宗旨是代码简洁、易阅读就行。 对函数传参的方式很简单,在调用函数后面添加字符串即可 向函数传入参数后,代码块中通过 $1 $2 ... $n 等方式调用。$1 表示传入的第1个参数,$2 表示传入的第2个参数,以此类推。 除了指定第n个参数外,还可以直接获取参数的个数($#)和全部参数($@、$*)。 了解完如何传参后,来看一个例子:检查 IP192.168.1.7 和 192.168.1.8 是否能ping通 结果如下 我们封装了一个函数用于检测 IP 是否能 ping 通,在这个例子中固定只使用一个参数,这可能显得有些局限性。当需要检测的 IP 达到一定数量时,需要通过多次调用才能达到目的,显得代码比较繁琐。 我们来利用 $@ 来优化代码,将需要检测的IP放入数组中,直接将该数组的值全部传入函数。 结果如下: 除了使用 $@ 方法,还可以使用 shift 来删除第一个的参数。先来看一个简单的示例 将第一个参数删除,保留了剩下的参数,那么第2个参数就变成了第1个参数 也可以指定删除n个参数:shift [n] 我们将这个方法代入测试IP中 结果如下 效果和 $@ 是一样的,根据不同的需求选择不同的方法吧。 什么是返回值? 函数的返回值提供了一种机制来向调用者传递信息,以便调用者根据函数的执行结果进行适当的处理。通过返回值,可以实现错误处理、结果传递和多状态返回等功能。这样可以增加程序的灵活性和可扩展性,并使代码更易于维护和调试。 函数的返回值是通过关键字 return 来返回一个 0~255 的整数。示例: 代码中,使用了 return 返回一个整数 10,这个10并不会输出到屏幕,而是返回到状态码中。所以我们可以利用返回值判断这个函数是否执行成功。 所以,函数是否出错可以手动指定。 需要注意的是,返回值是无法赋值给变量的 但是,输出的结果是可以赋值的(将 return 改为 echo ) 使用 return 无法赋值给变量,而 echo 可以赋值。也就是说:当需要使用多个函数关联调度时,我们不需要使用 return,可直接使用 echo 返回,再利用 echo 的返回值来判断下一阶段应该如何调度。 当使用 return 后,函数将自动退出,return 下面的代码将不再执行 这种情况在什么地方可以用到呢?举一个例子:监控某个进程的物理内存、虚存的使用情况 如果该进程 ID 存在则查看内存使用情况,如果不存在则退出函数。这样写的好处就是:当整个脚本代码量较多时,发现某些地方出错时,我们希望退出某个函数而整个脚本的其他代码依然正常执行,那么 return 就是首选。 在《目录3基本用法》中列举了很多例子,相信大家基本已经学会如何去写一个函数了,这里再列举一些复杂点的例子,以便更快速的掌握函数的方法。 编写一个按指定次数和间隔时间去监控硬件CPU使用率、内存使用情况、磁盘使用情况,通过表格的方式输出结果。执行语法如下 这里封装了2个函数: 结果如下 编写一个输出随机加法、减法、乘法、除法的计算题,并判断是否输出答案。执行语法如下 输出3列5行的加法,并输出答案 输出3列5行的加法,不输出答案(修改变量 output_answer="no" ) 输出全部加、减、乘、除计算题,每种模式输出3列2行
Func(){
<
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. 函数的用法
3.1. 声明局部变量
Func1(){
# 方法一
local var=1 # 将变量var声明为局部变量
}
Func2(){
# 方法二
local var1 var2 # 将变量var1和var2都声明为局部变量(这里可以声明多个变量)
var1=1
var2=2
}
Func1(){
local var=10 # 函数1中的局部变量
echo ${var}
}
Func2(){
local var=20 # 函数2中的局部变量
echo ${var}
}
Func1 # 调用函数1
Func2 # 调用函数2
echo ${var} # 在全局输出局部变量
Func1(){
# 函数1中声明一个变量var
local var=10
}
Func2(){
# 调用函数1,打印这个var变量
Func1
echo ${var}
}
Func2 # 调用函数2
Func1(){
# 打印变量var
echo ${var}
}
Func2(){
# 在函数2中声明一个局部变量
local var=20
# 调用函数1
Func1
}
Func2 # 调用函数2
# 定义一个全局变量
var=10
Func1(){
# 定义一个局部变量
local var=20
echo "局部中的var: ${var}"
}
Func1
echo "全局中的var: ${var}"
# 定义一个全局变量
var=10
Func1(){
# 函数中不声明局部变量
var=20
}
Func1
# 打印这个变量
echo ${var}
3.2. 函数传参的方法
# 定义一个删除文件空行的函数
DeleteBlankLine(){
sed -i '/^$/d' $1
}
# 查询这些文件并依次调用函数
for file in $(find ./file/ -type f);do
DeleteBlankLine "${file}" # 向函数传入一个文件路径的参数
done
函数名 "参数1" "参数2" "参数n"
Func1(){
echo $1 # 打印第1个参数值
echo $2 # 打印第2个参数值
}
Func1 "参数1" "参数2"
Func1(){
echo $# # 打印参数个数
echo $@ # 打印全部参数
echo $* # 打印全部参数
}
Func1 "参数1" "参数2"
# 封装一个检查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
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[@]}
Func(){
# 删除第1个参数
shift
# 打印当前第一个参数
echo "第1个参数的值是:$1"
# 打印全部参数
echo "当前的全部参数包含:$@"
}
# 向函数中传入3个参数
Func "AAA" "BBB" "CCC"
Func(){
# 删除前2个参数
shift 2
# 打印当前第一个参数
echo "第1个参数的值是:$1"
# 打印全部参数
echo "当前的全部参数包含:$@"
}
# 向函数中传入3个参数
Func "AAA" "BBB" "CCC"
# 需要测试的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. 函数的返回值
Func(){
return 10 # 指定一个返回值
}
Func # 调用函数
echo $? # 查看状态码
Func(){
return 10 # 指定一个返回值
}
Func # 调用函数
# 判断返回值是否为0,0表示正常,非0表示异常
if [ $? -eq 0 ];then
echo "函数没有出错"
else
echo "函数出错了"
fi
Func(){
return 10
}
var=$(Func) # 将函数结果赋值给变量
echo ${var} # 打印这个变量
Func(){
echo 10 # 输出一个结果
}
var=$(Func) # 将函数结果赋值给变量
echo ${var} # 打印这个变量
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"
4. 实际应用
4.1. 实时监控硬件资源
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
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