七、使用Shell函数
1. 什么是函数?
在Shell脚本中,将一些需要重复使用的操作,定义为公共的语句块,即可称为函数
shell中允许将一组命令集合或语句形成一段可用代码,这些代码块称为shell函数
给这段代码起个名字称为函数名,后续可以直接调用该段代码的功能
函数的作用就是将程序里面多次被调用的代码组合起来,称为函数体,并取一个名字称为(函数名),当我们需要用到这段代码的时候,我们就可以直接来调用函数名。
2. 如何定义函数?
Shell函数的语法
在shell中 if语句有它的语法,for循环也有它的语法,那么shell中的函数,那肯定也有它的语法有以下三种:
function 函数名 () {
指令...
return -n
}
function 函数名 {
指令...
return -n
}
函数名 () {
指令...
return -n
}
提示:在以上的函数语法中,前面的funcation 表示声明一个函数!!! 可以不写
return -n 是指退出函数
注:定义函数名,不会被输出
函数中return说明:
return可以结束一个函数。类似于循环控制语句break(结束当前循环,执行循环体后面的代码)。 return默认返回函数中最后一个命令状态值,也可以给定参数值,范围是0-256之间。 如果没有return命令,函数将返回最后一个指令的退出状态值。
3. 函数如何调用?
㈠ 当前命令行调用
[root@MissHou shell04]# cat fun1.sh #!/bin/bash hello(){ echo "hello lilei $1" hostname } menu(){ cat <<-EOF 1. mysql 2. web 3. app 4. exit EOF } [root@MissHou shell04]# source fun1.sh [root@MissHou shell04]# . fun1.sh [root@MissHou shell04]# hello 888 hello lilei 888 MissHou.itcast.cc [root@MissHou shell04]# menu 1. mysql 2. web 3. app 4. exit
㈡ 定义到用户的环境变量中
[root@MissHou shell05]# vim ~/.bashrc 文件中增加如下内容: hello(){ echo "hello lilei $1" hostname } menu(){ cat <<-EOF 1. mysql 2. web 3. app 4. exit EOF } 注意: 当用户打开bash的时候会读取该文件
㈢ 脚本中调用
#!/bin/bash #打印菜单 source ./fun1.sh menu(){ cat <<-END h 显示命令帮助 f 显示磁盘分区 d 显示磁盘挂载 m 查看内存使用 u 查看系统负载 q 退出程序 END } menu //调用函数
使用函数时的规则:
先定义后调用;可以只定义不调用
在同一个脚本里,函数名相同时,后定义生效
函数名区分字母大小写
在函数体内定义的变量是局部变量,只能在当前函数内使用
在函数体外定义的变量是全局变量,全局变量所有函数都可以使用
函数之间可以互相调用
定义一次,可多次重用
Bash调用函数时不会开启新的子Shell,会在现有的shell环境中执行该函数
4. 应用案例
具体需求:
写一个脚本让用户输入基本信息(姓名,性别,年龄),如不输入一直提示输入,最后根据用户的信息输出相对应的内容
思路:
1.交互式定义多个变量来保存用户信息 姓名、性别、年龄 2.如果不输一直提示输入 循环直到输入字符串不为空 while 判断输入字符串是否为空 每个信息都必须不能为空,该功能可以定义为一个函数,方便下面脚本调用 3.根据用户输入信息做出匹配判断
代码实现:
#!/bin/bash #该函数实现用户如果不输入内容则一直循环直到用户输入为止,并且将用户输入的内容打印出来 input_fun() { input_var="" output_var=$1 while [ -z $input_var ] do read -p "$output_var" input_var done echo $input_var } input_fun 请输入你的姓名: 或者 #!/bin/bash fun() { read -p "$1" var if [ -z $var ];then fun $1 else echo $var fi } #调用函数并且获取用户的姓名、性别、年龄分别赋值给name、sex、age变量 name=$(input_fun 请输入你的姓名:) sex=$(input_fun 请输入你的性别:) age=$(input_fun 请输入你的年龄:) #根据用户输入的性别进行匹配判断 case $sex in man) if [ $age -gt 18 -a $age -le 35 ];then echo "中年大叔你油腻了吗?加油" elif [ $age -gt 35 ];then echo "保温杯里泡枸杞" else echo "年轻有为。。。" fi ;; woman) xxx ;; *) xxx ;; esac
扩展延伸:
描述以下代码含义: :() { :|:& } :
递归调用示例
Shell版的fork炸弹
仅13个字符:.(){.|.& };.
递归死循环,可迅速耗尽系统资源
无限的生成新的进程,造成死机
代码解析
.() #定义一个名为.的函数
{ #函数块的开始标记
.|.& #在后台递归调用函数.
; #函数块的结束标记
. #再次调用函数
三、综合案例
1. 任务背景
现有的跳板机虽然实现了统一入口来访问生产服务器,yunwei用户权限太大可以操作跳板机上的所有目录文件,存在数据被误删的安全隐患,所以希望你做一些安全策略来保证跳板机的正常使用。
2. 具体要求
1.只允许yunwei用户通过跳板机远程连接后台的应用服务器做一些维护操作
2.公司运维人员远程通过yunwei用户连接跳板机时,跳出以下菜单供选择:
欢迎使用Jumper-server,请选择你要操作的主机: 1. DB1-Master 2. DB2-Slave 3. Web1 4. Web2 h. help q. exit
3.当用户选择相应主机后,直接免密码登录成功
4.如果用户不输入一直提示用户输入,直到用户选择退出
3. 综合分析
1.将脚本放到yunwei用户家目录里的.bashrc文件里(/shell05/jumper-server.sh)
2.将菜单定义为一个函数[打印菜单],方便后面调用
3.用case语句来实现用户的选择【交互式定义变量】
4.当用户选择了某一台服务器后,进一步询问用户需要做的事情 case...esac 交互式定义变量
5.使用循环来实现用户不选择一直让其选择
6.限制用户退出后直接关闭终端 exit
4. 落地实现
#!/bin/bash # jumper-server # 定义菜单打印功能的函数 menu() { cat <<-EOF 欢迎使用Jumper-server,请选择你要操作的主机: 1. DB1-Master 2. DB2-Slave 3. Web1 4. Web2 h. help q. exit EOF } # 屏蔽以下信号 trap '' 1 2 3 19 # 调用函数来打印菜单 menu #循环等待用户选择 while true do # 菜单选择,case...esac语句 read -p "请选择你要访问的主机:" host case $host in 1) ssh root@10.1.1.1 ;; 2) ssh root@10.1.1.2 ;; 3) ssh root@10.1.1.3 ;; h) clear;menu ;; q) exit ;; esac done 将脚本放到yunwei用户家目录里的.bashrc里执行: bash ~/jumper-server.sh exit
进一步完善需求
为了进一步增强跳板机的安全性,工作人员通过跳板机访问生产环境,但是不能在跳板机上停留。
#!/bin/bash #公钥推送成功 trap '' 1 2 3 19 #打印菜单用户选择 menu(){ cat <<-EOF 欢迎使用Jumper-server,请选择你要操作的主机: 1. DB1-Master 2. DB2-Slave 3. Web1 4. Web2 h. help q. exit EOF } #调用函数来打印菜单 menu while true do read -p "请输入你要选择的主机[h for help]:" host #通过case语句来匹配用户所输入的主机 case $host in 1|DB1) ssh root@10.1.1.1 ;; 2|DB2) ssh root@10.1.1.2 ;; 3|web1) ssh root@10.1.1.250 ;; h|help) clear;menu ;; q|quit) exit ;; esac done 自己完善功能: 1. 用户选择主机后,需要事先推送公钥;如何判断公钥是否已推 2. 比如选择web1时,再次提示需要做的操作,比如: clean log 重启服务 kill某个进程
回顾信号:
1) SIGHUP 重新加载配置 2) SIGINT 键盘中断^C 3) SIGQUIT 键盘退出 9) SIGKILL 强制终止 15) SIGTERM 终止(正常结束),缺省信号 18) SIGCONT 继续 19) SIGSTOP 停止 20) SIGTSTP 暂停^Z
例1:Shell函数实例如下
function fj () { echo "我是风姐!" } function zhangsan () { echo "我是张三!" } fj zhangsan
剖析:
function fj () { # 前面的function是声明一个函数 名字叫 fj () { echo "我是风姐!" #下面呢 我们执行操作.echo 我是风姐 } #最后 我们以 } 为结束 function zhangsan () { # 前面的function是声明一个函数 名字叫 zhangsan () { echo "我是张三!" #下面呢 我们执行操作.echo 我是张三 } #最后 我们以 } 为结束 fj #调用fj 函数 zhangsan #调用zhangsan函数
那么定义了两个函数,我们最终目的是要调用它所以 在函数的下面输入了fj 和 zhangsan 意思是说 我要调用这两个函数,那么这两个函数中有两个命令 输出一个张三和凤姐,所以在我们执行脚本的时候结果就会是如下:
例2:分离函数体执行函数的脚本文件
cat >>/etc/init.d/functions<< EOF function zhangsan () { echo "我就是张三" } EOF
以上代码什么意思:我们可以看到第一行是cat >> /etc/....这个路径 我们完全可以理解为,把下面的内容 导入到/etc/init.d/functions这个文件中; 那么这个文件是Linux系统内置的脚本函数库
我们可以清楚的看到我们写的函数已经导入到了/etc/init.d/functions 这个文件中,那么接下来进行操作了 如下:
#!/bin/bash if [ -f /etc/init.d/functions ] then . /etc/init.d/functions fi zhangsan
解释:if 如果/etc/init.d/functions是一个普通文件,那么我们执行. /etc/init.d/functions 在这里这个"."是用来加载functions 中的命令或者变量参数等;最后因为我们在上面定义了zhangsan这个函数,那我们在最后一行可以调用这个zhangsan函数 输出如下:
那么在这里我们来一个总结:
当我们定义函数过多的情况下,我们可以把函数写在某一个文件中,当我们写脚本的时候需要用到这个函数中的某一个指令,那么我们就可以直接来调用文件中的函数名!
例3:编写带参数的shell函数实例
#!/bin/bash function lisi () { echo 我的名字叫: $1 } lisi $1
在这里我们定义了一个名字叫做lisi的函数,这里需要注意的是echo那行,正常的输出是没有问题的,但是在后面加了一个特殊的位置变量。$1 最后在我们调用函数的时候 后面也加了特殊位置变量 $1 执行效果如下:
3、利用Shell函数开发企业级URL检测脚本
这是书上的一部分案例,但是我还会在这里通过我的想法来解释整个脚本中的全部过程!
1)首先我们不用函数来写一个检测URL的脚本如下:
#!/bin/bash #no.1 if [ "$#" -ne 1 ] then echo "/root/sh/ $0" 请您输入一个网址 exit 1 fi #no.2 wget --spider -q -o /dev/null --tries=1 -T 3 $1 if [ "$?" -eq 0 ] then echo "$1 检测是成功的!" else echo "$1 检测是失败的!" exit 1 fi
剖析:
#!/bin/bash #no.1 if [ "$#" -ne 1 ] #如果用户输入的传参参数不是1 then # 那么 echo "/root/sh/ $0" 请您输入一个网址 #输出脚本路径和脚本名称 最后输出请您输入一个网址! exit 1 #退出脚本 fi #结束 #no.2 wget --spider -q -o /dev/null --tries=1 -T 3 $1 #如果用户输入传参数是对的,那么执行wget命令最后$1是用户输入的内容 if [ "$?" -eq 0 ] #如果以上wget测试成功了! then #那么 echo "$1 检测是成功的!" #输出用户输入网址并输出是成功的 else #否则 echo "$1 检测是失败的!" #输出用户输入网址并输出是失败的 exit 1 #退出脚本 fi #fi结束
感觉不够养眼?没有关系,如下图
最后运行的结果如下:
这不是重点,重点是我们使用shell函数来写整个过程,如下:
function TS () { echo "/root/sh/ $0" 请您输入一个网址 exit 1 } function Check_url () { wget --spider -q -o /dev/null --tries=1 -T 3 $1 if [ "$?" -eq 0 ] then echo "$1 检测成功!" else echo "$1 检测失败" exit 1 fi } function JG () { if [ "$#" -ne 1 ] then TS fi Check_url $1 } JG $*
剖析:
function TS () { #定义一个名字为TS的函数 (提示的意思) echo "/root/sh/ $0" 请您输入一个网址 #执行的指令是 输出一个:请您输入一个网址 exit 1 #退出 } function Check_url () { #定义一个名字为Check_url的函数 (检测的意思) wget --spider -q -o /dev/null --tries=1 -T 3 $1 #如果用户输入传参数是对的,那么执行wget命令最后$1是用户输 入的内容 if [ "$?" -eq 0 ] #如果以上wget去测试成功了! then #那么 echo "$1 检测成功!" #输出用户输入网址并输出是成功的 else #否则 echo "$1 检测失败" # 输出用户输入网址并输出是失败的 exit 1 #退出 fi } function JG () { #这是最后的函数JG(结果的意思) if [ "$#" -ne 1 ] ##如果用户输入的传参参数不是1 then #那么 TS #注意了!!! 调用TS fi Check_url $1 #如果上面传参是1 那么就调用Check_url函数 } JG $* #这里的$*就是把命令行接收的所有参数作为函数传给函数内部
最后我们再次执行脚本
最后为了能够让大家清晰的看到if语句的脚本和函数脚本的对比,一张图解释:
4、案例二 实现菜单脚本,使用函数编写如下:
function CDAN(){ cat << yankerp +------------------------------------------------+ | | | _o0o_ 1. 安装Nginx | | 08880 2. 安装Apache | | 88"."88 3. 安装MySQL | | (|-_-|) 4. 安装PHP | | 0\=/0 5. 部署LNMP环境 | | __/ \__ 6. 安装zabbix监控 | | ‘\ ///‘ 7. 退出此管理程序 | | / Linux一键 \ 8. 关闭计算机 | | || Server || ====================== | | \ //// 一键安装服务 | | ||| i i i ||| by Yankerp | | ___ ___ ====================== | |___‘. /--.--\ .‘___ | +------------------------------------------------+ yankerp } CDAN LOG_DIR=/usr/local/src read -p "请您输入1-8任意数值:" NUM if [ ${#NUM} -ne 1 ] then echo "请您输入1|2|3|4|5|6|7|8" exit 1 fi expr $NUM + 1 &>/dev/null if [ "$?" -ne 0 ] then echo "请您输入数值!" exit 1 fi if [ "$NUM" -gt 8 ];then echo "请您输入比8小的数值" exit 1 elif [ "$NUM" -eq 0 ];then echo "请您输入比0大的数值" exit 1 fi ###################### function Nginx_DIR() { yum install -y gcc gcc-c++ pcre-devel zlib-devel openssl-devel &>/dev/null if [ $? -eq 0 ] then cd $LOG_DIR && wget http://nginx.org/download/nginx-1.12.2.tar.gz &>/dev/null && useradd -M -s /sbin/nologin nginx && \ tar zxf nginx-1.12.2.tar.gz && cd nginx-1.12.2/ && \ ./configure --prefix=/usr/local/nginx --with-http_dav_module --with-http_stub_status_module --with-http_addition_module --with-http_sub_module --with-http_flv_module --with-http_mp4_module --with-pcre --with-http_ssl_module --with-http_gzip_static_module --user=nginx &>/dev/null && make &>/dev/null && make install &>/dev/null fi if [ -e /usr/local/nginx/sbin/nginx ];then ln -s /usr/local/nginx/sbin/nginx /usr/local/sbin/ && nginx && echo "Nginx安装并启动成功!!!" fi } if [ $NUM -eq 1 ] then echo "开始安装Nginx请稍等..." && Nginx_DIR fi
输出如下:
图2:
以上脚本首先通过了if语句进行各种判断, 判断用户输入的参数等等;在后面使用了函数 来定义了安装Nginx的操作,最后我们调用了函数使整个函数运行了起来!!!
呃,大家可以参考参考,若写的不好请谅解~ 我感觉对刚开始接触shell的人还是蛮有帮助的,只不过使用了多分支的if语句,感觉有点繁琐,接下来就会更新case语句了这样就可以解决多分支if语句很繁琐的问题!!!
函数可以递归调用(函数自己调用自己)
用function函数 +case分支结构 给源码apache写启动脚本,
apache指定安装目录/usr/local/httpd2,脚本名叫apached.sh
答:
写完脚本把apached.sh的权限改成x执行权限并拷贝到系统存储启动服务的目录/etc/init.d下并去掉后缀.sh 。
然后把/etc/init.d/apached脚本加入到chkconfig的开启自启动服务中。
1.进入到系统存储运行的目录