Shell 函数
1. Shell 函数概述
1.1 Shell 函数简介
Shell 和其他语言一样,也有函数,其本质就是一段可以复用的代码。将数据进行抽离处理,传递不同的值输出不同的结果,在指定的地方进行调用即可。
1.2 为什么要用函数
如果我们要重复执行一批相同的操作,不想重复去写,可以将这一系列操作抽象为一个函数,后期可以利用变量传值调用该函数,从而大大减少重复性劳动,提升效率减少代码量,使得 Shell 脚本更加的灵活和通用。
2. Shell 函数操作
2.1 函数语法
2.1.1 标准语法
function fnname() {
statements
return value
}
对各个部分的说明:
function
是 Shell 中的关键字,专门用来定义函数;fname
是函数名;statements
是函数要执行的代码,也就是一组语句;return value
表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值,这一部分可以写也可以不写。
由大括号包围的部分称为函数体,调用一个函数,实际上就是执行函数体中的代码。
例如:
function checkuser() {
echo "当前用户为:$USER"
return 0
}
如上就定义了一个 checkuser
函数,其输出当前登录系统的用户名,返回值为 0。
2.1.2 简化语法
- 函数名后无括号
#简化写法1
function fname{
statements
return n
}
- 函数不写 function
#简化写法2:
fname() {
statements
return n
}
上述两种定义函数的方法都可以,但是还是建议在编写 Shell 的时候,也不要浪费这点时间,建议大家都用完整函数定义,写 function 关键字,在为函数定义带上 (),这样更加规范,而且便于别人阅读修改你的脚本。
Tips:在函数定义中需要函数名称后可以有多个空格,括号内也可以有多个空格,如果函数体写在一行,需要在语句末尾加上
;
。
2.2 函数调用
当函数定义好了,在需要使用函数的地方,调用其函数名称即可,注意函数调用需要在函数定义之后。
2.2.1 无参数调用
当函数没有参数的时候,调用非常简单,直接写函数名称即可,调用函数就是在特定地方执行函数体内的操作。
// 定义函数
function fnname() {
statements
return value
}
// 调用函数
fname
2.2.2 传递参数调用
- 语法
我们之前在变量一章节介绍了 Shell 脚本的参数,知道了参数的重要性质及其各种类特征,与 Shell 脚本传递参数一样,函数也可以传递参数,例如:
// 定义函数
function fnname() {
statements
return value
}
// 调用函数
fname param1 param2 param3
如上所示,在调用函数 fname
的时候,我们传递了三个参数,参数之间利用空格分割,和 Shell 脚本传递参数一样,但需要注意 Shell 脚本中,函数在定义时候不能指定参数,在调用的时候传递参数即可,并且在调用函数时传递什么参数函数就接受什么参数。
2.3 函数参数
上述我们了解了函数的定义,在其中无参函数调用即调用函数名即可,对于有参函数,需要传递一定的参数来执行对应的操作,函数的参数和脚本的参数类型及用法一致,在此我们简单回顾下,看参数在函数中都有哪些分类,及该如何使用。
2.3.1 位置参数
位置参数顾名思义,就是传递给函数参数的位置,例如给一个函数传递一个参数,我们可以在执行 Shell 脚本获取对应位置的参数,获取参数的格式为:$n。n 代表一个数字,在此需要注意与脚本传递参数不一样,$0
为依旧为脚本的名称,在函数参数传递中,例如传递给函数的第一个参数获取就为 $1
,第 2 个参数就为 $2
, 以此类推……,需要其 $0
为该函数的名称。
例如:
[root@master func]# cat f1.sh
#!/bin/bash
function f1() {
echo "函数的第一个参数为: ${1}"
echo "函数的第二个参数为: ${2}"
echo "函数的第三个参数为: ${3}"
}
# 调用函数
f1 shell linux python go
[root@master func]# bash f1.sh
函数的第一个参数为: shell
函数的第二个参数为: linux
函数的第三个参数为: python
我们可以看到传递给 f1
函数共 4 个位置参数,在结果输出中可以看到由于函数体内部只对三个参数进行了处理,后续的参数也就不再处理了。
2.3.2 特殊参数
在 Shell 中也存在特殊含义的参数如下表:
变量 | 含义 |
---|---|
$# | 传递给函数的参数个数总和 |
$* | 传递给脚本或函数的所有参数,当被双引号 " " 包含时,所有的位置参数被看做一个字符串 |
$@ | 传递给脚本或函数的所有参数,当被双引号 " " 包含时,每个位置参数被看做独立的字符串 |
$? | $? 表示函数的退出状态,返回为 0 为执行成功,非 0 则为执行失败 |
示例:
[root@master func]# cat f1.sh
#!/bin/bash
function fsum() {
echo "函数第一个参数为: ${1}"
echo "函数第二个参数为: ${2}"
echo "函数第三个参数为: ${3}"
echo "函数的参数总数为: ${#}"
echo "函数的参数总数为: ${@}"
local sum=0
for num in ${@};
do
let sum=${sum}+${num}
done
echo "计算的总和为: ${sum}"
return 0
}
# 调用函数
fsum 10 20 1 2
echo $?
[root@master func]# bash f1.sh
函数第一个参数为: 10
函数第二个参数为: 20
函数第三个参数为: 1
函数的参数总数为: 4
函数的参数总数为: 10 20 1 2
计算的总和为: 33
0
如上可以看到特殊参数与 Shell 脚本传递参数一样。
Tips:局部变量需要特别声明在函数内部利用
local
关键字来声明。
2.4 函数返回值
函数返回值利用 $?
来接收,在上述示例中我们将计算的结果利用 echo 命令打印出来,如果我们在后续的脚本中需要利用此函数计算的结果,就需要得到这个返回值,此刻就需要将计算的结果不仅仅是打印而是返回了,函数中返回利用 return
关键字,在函数调用完成后,我们利用 $?
来接受函数的返回值,例如将我们上面的示例改造成返回结构的函数。
注意:shell 函数的返回值,只能是整形,并且在 0-257 之间,不能是字符串或其他形式。并且在调用方法和取得返回值之间,不能有任何操作,不然取不到 return 的值。
[root@master func]# cat f1.sh
#!/bin/bash
function fsum() {
echo "函数第一个参数为: ${1}"
echo "函数第二个参数为: ${2}"
echo "函数第三个参数为: ${3}"
echo "函数的参数总数为: ${#}"
echo "函数的参数总数为: ${@}"
local sum=0
for num in ${@};
do
let sum=${sum}+${num}
done
return $sum
}
fsum 10 20 1 2
echo $?
[root@master func]# bash f1.sh
函数第一个参数为: 10
函数第二个参数为: 20
函数第三个参数为: 1
函数的参数总数为: 4
函数的参数总数为: 10 20 1 2
33
可以看到我们将在函数内部计算的数组之和,利用 return 作为返回,此刻在函数调用的时候,利用 $?
就可以拿到函数返回的值进一步处理。
2.5 递归函数
Shell 支持递归函数,递归函数也就是自己调用自己,即在函数体内部又一次调用函数自己,例如:
[root@master func]# cat recursion.sh
#!/bin/bash
function myecho() {
echo "$(date)"
sleep 1
myecho inner
}
myecho
[root@master func]# bash recursion.sh
Sat Mar 28 13:14:38 CST 2020
Sat Mar 28 13:14:39 CST 2020
Sat Mar 28 13:14:40 CST 2020
Sat Mar 28 13:14:41 CST 2020
Sat Mar 28 13:14:42 CST 2020
...
如上就是一个递归函数,在函数体内部又调用了函数 myecho
,在执行的时候就会陷入无限循环。
3. 实例
3.1 需求
系统经常在执行定时脚本期间会将 Linux 系统 CPU 利用率跑满,导致其他服务受到影响,故查阅资料发现有大神写的 CPU 利用率限制程序。
地址:CPU Usage Limiter for Linux
利用此工具可以,配合定时任务放置在服务器上,达到限制程序 CPU 情况,可根据自己系统 CPU 核心数进行参数配置,会记录 CPU 超过阀值的日志,可供后期进行查看分析。
3.2 思路
利用函数编写安装 cpulimit 工具的函数,如果系统不存在该命令则安装并执行 CPU 限制,如果存在则执行 cpulimit 函数对超过指定 CPU 的进程进行限制,最后执行总体 main 函数。
3.3 实现
- 实现脚本
#!/bin/bash
# Description: count file scripts
# Auth: kaliarch
# Email: kaliarch@163.com
# function: count file
# Date: 2020-03-29 14:00
# Version: 1.0
[ $(id -u) -gt 0 ] && exit 1
# cpu使用超过百分之多少进行限制
PEC_CPU=80
# 限制进程使用百分之多少,如果程序为多线程,单个cpu限制为85,如果为多核心,就需要按照比例写,例如cpu为2c,像限制多线程占比80%,就写170
LIMIT_CPU=85
# 日志
LOG_DIR=/var/log/cpulimit/
# 超过阀值进程pid
PIDARG=$(ps -aux |awk -v CPU=${PEC_CPU} '{if($3 > CPU) print $2}')
# 安装cpulimit 函数
install_cpulimit() {
[ ! -d /tmp ] && mkdir /tmp || cd /tmp
wget -c https://github.com/opsengine/cpulimit/archive/v0.2.tar.gz
tar -zxf v0.2.tar.gz
cd cpulimit-0.2 && make
[ $? -eq 0 ] && cp src/cpulimit /usr/bin/
}
# 执行cpulimit
do_cpulimit() {
[ ! -d ${LOG_DIR} ] && mkdir -p ${LOG_DIR}
for i in ${PIDARG};
do
CPULIMITCMD=$(which cpulimit)
MSG=$(ps -aux |awk -v pid=$i '{if($2 == pid) print $0}')
echo ${MSG}
[ ! -d /tmp ] && mkdir /tmp || cd /tmp
nohup ${CPULIMITCMD} -p $i -l ${LIMIT_CPU} &
echo "$(date) -- ${MSG}" >> ${LOG_DIR}$(date +%F).log
done
}
# 主函数
main() {
hash cpulimit
if [ $? -eq 0 ];then
# 调用函数
do_cpulimit
else
install_cpulimit && do_cpulimit
fi
}
main
- 测试
需编写 CPU 压力测试 python 脚本,后期可以放入计划任务来监控并限制 CPU 使用率。
#!/bin/env python
import math
import random
a=10000
b=10000
c=10000
sum=0
for i in range(0,a):
for j in range(0,b):
randomfloat=random.uniform(1,10)
randompow=random.uniform(1,10)
sum+=math.pow(randomfloat, randompow)
print "sum is %s" % sum
当我们执行 python 的 CPU 压力测试脚本后,发现单核服务 CPU 跑到了 100%。
之后运行我们的 CPU 限制脚本,可以看到 CPU 被成功限制。
在此案例中,我们着重来看 Shell 函数,在实例中我们编写了安装与执行 CPU 限制两个函数,最后编写主函数,在主函数中调用其他函数,配合定时任务就能达到限制某进程的 CPU。
4. 注意事项
- 函数定义建议使用见名知意并需要有一定原则,不仅为了美观更是为了规范,使得其他人更好理解与阅读你的 Shell 脚本,增强脚本可维护性;
- 对于函数调用,必须在函数定义之后,Shell 运行为顺序运行,没有定义函数则调用函数会有异常;
- 函数定义不能指定参数,在调用的时候传递参数即可,并且调用函数传递什么参数,函数就接受什么参数;
- 函数传递参数与 Shell 脚本传递参数的类型及用法一致,在此可以融会贯通,对比理解记忆;
- shell 函数的返回值,只能是整形,并且在 0-257 之间,不能为字符串或其他形式。
5. 小结
我们编写 Shell 就是为了实现简化操作,通常将数据和程序分离,我们将一组实现具体业务的逻辑编写为一个函数,也可以称为一段代码块,将其模块化,赋予函数名称,利用函数可以达到代码复用,使得数据与逻辑分离,脚本更加便于维护和管理。