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 脚本获取对应位置的参数,获取参数的格式为:$nn 代表一个数字,在此需要注意与脚本传递参数不一样,$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 就是为了实现简化操作,通常将数据和程序分离,我们将一组实现具体业务的逻辑编写为一个函数,也可以称为一段代码块,将其模块化,赋予函数名称,利用函数可以达到代码复用,使得数据与逻辑分离,脚本更加便于维护和管理。