一、函数介绍
1.1 什么是函数
函数就是用来盛放一组代码的容器,函数内的一组代码完成一个特定的功能,称之为一组代码块,调用函数便可触发函数内代码块的运行,这可以实现代码的复用,所以函数又可以称之为一个工具
1.2 为何要有函数
- 减少代码冗余
- 提升代码的组织结构性、可读性
- 增强扩展性
1.3 语法
#语法:
[ function ] funname [()]
{
命令1;
命令2;
命令3;
...
[return int;]
}
# 示例1:完整写法
function 函数名() {
函数要实现的功能代码
}
# 示例2:省略关键字(),注意此时不能省略关键字function
function 函数名 {
函数要实现的功能代码
}
# 示例3:省略关键字function
函数名() {
函数要实现的功能代码
}
1.4 调用函数
# 语法:
函数名 # 无参调用
函数名 参数1 参数2 # 有参调用
# 示例
function test1(){
echo "执行第一个函数"
}
function test2 {
echo "执行第二个函数"
}
test3(){
echo "执行第三个函数"
}
# 调用函数:直接引用函数名字即调用函数,会触发函数内代码的运行
test1
test2
test3
1.5 总结
具备某一功能的工具=>函数
事先准备好工具=>函数的定义
遇到应用场景,拿来就用=>函数的调用
所以函数的使用原则:先定义,后调用
二、函数参数
如果把函数当成一座工厂,函数的参数就是为工厂运送的原材料
- 调用函数时可以向其传递参数 (类似于python中的位置实参)
# 调用函数test1,在其后以空格为分隔符依次罗列参数
test1 aa bb cc dd
- 在函数体内部,通过
$n 的形式来获取参数的值,例如,$1 表示第一个参数,$2 表示第二个参数…当n>=10时,需要使用${n} 来获取参数
[root@manager function]# cat test1.sh
#!/bin/bash
function test1(){
echo "...........start........"
echo $1
echo $2
echo $3
echo "...........stop..........."
}
# 调用函数
test1 狄仁杰 程咬金 甄姬
[root@manager function]# sh test1.sh
...........start........
狄仁杰
程咬金
甄姬
...........stop...........
在脚本内获取脚本调用者在命令行里为脚本传入的参数,同样使用的是$n,不要搞混,命令行后的参数是给脚本传递的,函数体内的参数是需要函数名后传递的位置参数
[root@manager function]# cat test2.sh
function test2(){
echo "...start..."
echo "这是函数内的参数:$1"
echo "这是函数内的参数:$2"
echo "这是函数内的参数:$3"
echo "...end..."
}
# 调用函数,给函数传递位置参数
test2 狄仁杰 程咬金 甄姬
echo "这是脚本级的参数:$1"
echo "这是脚本级的参数:$2"
echo "这是脚本级的参数:$3"
# 运行,给脚本传参
[root@manager function]# sh test2.sh 苹果 香蕉 葡萄
...start...
这是函数内的参数:狄仁杰
这是函数内的参数:程咬金
这是函数内的参数:甄姬
...end...
这是脚本级的参数:苹果
这是脚本级的参数:香蕉
这是脚本级的参数:葡萄
2.1 元字符补充
参数处理 | 说明 |
---|
$* | 所有参数 | $# | 传递到脚本或函数的参数个数 | $@ | 所有参数,与$*类似 | $$ | 当前脚本进程的ID号 | $? | 获取上一条命令执行完毕后的退出状态。0表示正确,非0代表错误, 如果执行的是函数那么$?取的是函数体内return后的值 |
1.当$*和$@没有被引号引用起来的时候,它们确实没有什么区别,都会把位置参数当成一个个体。
2."$*" 会把所有位置参数当成一个整体(或者说当成一个单词),如果没有位置参数,则"$*"为空,
如果有两个位置参数并且分隔符为空格时,"$*"相当于"$1 $2"
3."$@" 会把所有位置参数当成一个单独的字段,如果没有位置参数,
则"$@"展开为空(不是空字符串,而是空列表),如果存在一个位置参数,
则"$@"相当于"$1",如果有两个参数,则"$@"相当于"$1" "$2"等等
[root@manager function]# cat test3.sh
function test3(){
echo "\$*: $*"
echo "\$@: $@"
echo $# # 5
echo $$ # 87380
echo $? # 0
}
[root@manager function]# sh test3.sh
$*: 111 222 333 444 555 # 其实是一个参数 "111 222 333 444 555"
$@: 111 222 333 444 555 # 是多个独立的参数"111" "222" "333" "444" "555"
5
3840
0
[root@manager function]# cat test4.sh
function test4(){
for i in "$@"
do
echo $i
done
}
# 无论传递多少参数,"$@" 想当于存放这些独立参数的列表
test4 11 22 33 "44 55" # 注意"44 55"被引号引成了一个参数
[root@manager function]# sh test4.sh
11
22
33
44 55
[root@manager function]# cat test5.sh
function test5(){
for i in "$*"
do
echo $i
done
}
# 无论传递多少参数,"$*" 只想当于一个参数
test5 11 22 33 "44 55" # 注意"44 55"被引号引成了一个参数
[root@manager function]# sh test5.sh
11 22 33 44 55
2.2 nginx 启停脚本
函数式编程的核心是,尽量把函数的功能写的独立,高内聚低耦合,业务逻辑判断尽量在主程序中执行,这样函数就可以重复调用,增强扩展性
[root@manager function]# cat nginx.sh
#!/bin/bash
. /etc/init.d/functions
function nginx_state(){
netstat -lntp | grep nginx &>/dev/null
if [ $? -eq 0 ];then
return 0 # 表示nginx已经启动
else
return 1 # 表示nginx未启动
fi
}
function nginx_control(){
if [ $? -eq 0 ];then
action "nginx $1 is ok!" /bin/true
else
action "nginx $1 is error!" /bin/false
fi
}
# 启动函数
function nginx_start(){
/usr/sbin/nginx
}
# 停止函数
function nginx_stop(){
/usr/sbin/nginx -s stop
}
# 重载函数
function nginx_reload(){
/usr/sbin/nginx -s reload
}
case $1 in
start)
nginx_state
if [ $? -eq 0 ];then
action "nginx 已经是启动状态,请不要重复启动" /bin/false
else
nginx_start
nginx_control $1
fi
;;
stop)
nginx_state
if [ $? -ne 0 ];then
action "nginx 已经是停止状态,请不要重复停止" /bin/false
else
nginx_stop
nginx_control $1
fi
;;
reload)
nginx_state
if [ $? -ne 0 ];then
echo "nginx 目前是停止状态,需要启动后才能reload,正在尝试启动"
nginx_start
nginx_control start
sleep 1
fi
/usr/sbin/nginx -s reload
nginx_control $1
;;
*)
echo "USAGE: $0 [ start | stop | relload ]"
esac
三、函数的返回值
如果把函数当成一座工厂,函数的返回值就是工厂的产品,在函数内使用return关键字返回值,函数内可以有多个return,但只要执行一个,整个函数就会立刻结束。
需要注意的是shell语言的函数中,通常用return返回函数运行是否成功的状态,0代表成功,非零代表失败,需要用$?获取函数的返回值。 return可以返回0-255的整数。
- 如果函数内没有return,那么将以最后一条命令运行结果(命令运行成功结果为0,否则为非0)作为返回值
[root@manager return]# cat 1.sh
function test1(){
echo 111
return
echo 222
return
echo 333
}
test1
[root@manager return]# sh 1.sh
111
- 如果函数内有return,那么return后跟的只能是整型值并且范围为0-255,用于标识函数的运行结果是否正确, 与C语言和Python正好相反,shell 语言中 0 代表 true,0 以外的值代表 false
[root@manager return]# cat 2.sh
function test1(){
echo 111
echo 222
echo 333
return 5
}
test1
[root@manager return]# sh 2.sh
111
222
333
[root@manager return]# echo $?
5
可以看出 $? 为return的返回值,前提是值必须在(0-255)之间
- echo返回值:使用echo可以返回任何字符串结果,通常用于返回数据,比如一个字符串值或者列
表值。
[root@manager return]# cat get_user.sh
#!/bin/bash
function get_user(){
users=$(cat /etc/passwd | awk -F ":" '{print $1}')
echo $users
}
user_list=$(get_user)
for user in $user_list
do
echo $user
done
3.1 实现跳板机
需求:使用case、循环、函数、实现JumpServer跳板机功能。 1.用户登陆该服务器则自动执行该脚本。 2.脚本提示可连接主机列表。 3.该脚本无法直接退出。
【trap信号列表】
下面列出一些比较重要的信号(括号里面的数字是传统的信号编号)
信号 说明
HUP(1) 挂起,通常因终端掉线或用户退出而引发
INT(2) 中断,通常因按下Crtl+C组合健而引发
QUIT(3) 退出,通常因某些严重的执行错误而引发
ABRT(6) 中止,通常因某些严重的执行错误而引发
ALRM(14) 报警,通常用来处理超时 |
TERM(15) 终止,通常在系统关机时发送
TSTP(20) 停止进程的运行,但该信号可以被处理和忽略,用户健入SUSP字符时(通常是Ctrl-Z)发出这个信号
[root@manager return]# cat jumpserver.sh
#/bin/bash
function inventory_info(){
cat << EOF
****************开源Jumpserver堡垒机*************************
1) 172.16.1.3
2) 172.16.1.4
3) 172.16.1.5
4) 172.16.1.6
5) 172.16.1.7
6) 172.16.1.8
*************************************************************
EOF
}
# 先打印菜单
inventory_info
trap "" HUP INT TSTP
while :
do
read -p "请输入要登录的主机:" action
case $action in
1)
ssh root@172.16.1.3
;;
2)
ssh root@172.16.1.4
;;
3)
ssh root@172.16.1.5
;;
4)
ssh root@172.16.1.6
;;
5)
ssh root@172.16.1.7
;;
6)
ssh root@172.16.1.8
;;
h)
clear
inventory
;;
exec)
exit
;;
*)
continue
esac
done
将脚本添加/etc/bashrc,登陆ssh后则会执行该脚本
3.2 实现多服务一键安装
目前只实现了nginx多版本的源码安装,其他两个服务等后续完成。
1.目录结构
[root@manager nginx_install]# tree
.
├── install_nginx # 安装nginx脚本
├── main.sh # 主函数入口
└── menuinfo # 菜单
2.主菜单文件
[root@manager nginx_install]# cat menuinfo
#一级菜单
function main_menu(){
cat << EOF
---------主菜单----------
| 1) 安装 nginx |
| 2) 安装 mysql |
| 3) 安装 php |
| 4) 退出 |
--------------------------
EOF
}
function nginx_menu(){
cat << EOF
---------nginx version----
| 1) 安装nginx 1.16 |
| 2) 安装nginx 1.18 |
| 3) 安装nginx 1.20 |
| 4) 返回上级菜单 |
-------------------------
EOF
}
function mysql_menu(){
cat << EOF
-------mysql version-----
| 1) 安装mysql 5.6 |
| 2) 安装mysql 5.7 |
| 3) 安装mysql 8.0 |
| 4) 返回上级菜单 |
-------------------------
EOF
}
function php_menu(){
cat << EOF
---------php version------
| 1) 安装php 5.5 |
| 2) 安装php 5.6 |
| 3) 安装php 7.1 |
| 4) 返回上级菜单 |
-------------------------
EOF
}
3.主程序入口
[root@manager nginx_install]# cat main.sh
. ./menuinfo
. ./install_nginx
main_menu
while :
do
read -p "请输入功能编号:" num
case $num in
1)
# 打印nginx安装菜单
nginx_menu
while true
do
read -p "请输入需要安装的nginx版本号:" num_nginx
case $num_nginx in
1)
var_nginx 1.16.1
wget_nginx
uncompress_nginx
make_nginx
start_nginx
;;
2)
var_nginx 1.18.0
wget_nginx
uncompress_nginx
make_nginx
start_nginx
;;
3)
var_nginx 1.20.1
wget_nginx
uncompress_nginx
make_nginx
start_nginx
;;
4)
clear
main_menu
break
;;
5)
continue
esac
done
;;
2)
# 打印mysql安装菜单
mysql_menu
;;
3)
# 打印php安装菜单
php_menu
;;
4)
exit
;;
*)
continue
esac
done
4.nginx安装脚本
[root@manager nginx_install]# cat install_nginx
. /etc/init.d/functions
. ./menuinfo
var_nginx(){
nginx_version=$1
nginx_install_dir=/usr/local/nginx
nginx_download_dir=/opt
ip=$(ifconfig eth0 | grep "inet " | awk '{print $2}')
}
function wget_nginx(){
# 安装nginx依赖包
yum install -y gcc gcc-c++ autoconf pcre pcre-devel make automake httpd-tools zlib-devel openssl-devel &>/dev/null
echo "正在安装相关依赖"
# 下载软件
if [ -f $nginx_download_dir/nginx-$nginx_version.tar.gz ];then
action "nginx-$nginx_version.tar.gz 已经存在,无需重复下载" /bin/false
else
echo "正在下载nginx-$nginx_version,请稍后..."
wget -O $nginx_download_dir/nginx-$nginx_version.tar.gz https:
if [ -f $nginx_download_dir/nginx-$nginx_version.tar.gz ];then
action "nginx-$nginx_version.tar.gz 下载成功" /bin/true
else
action "nginx-$nginx_version.tar.gz 下载失败" /bin/false
exit
fi
fi
}
# 先检查压是否已解压,后决定是否解压
function uncompress_nginx(){
if [ -d $nginx_download_dir/nginx-$nginx_version ];then
action "nginx-$nginx_version 已经解压过了,无需重复解压" /bin/false
else
cd $nginx_download_dir && tar -xf $nginx_download_dir/nginx-$nginx_version.tar.gz &>/dev/null
if [ -d $nginx_download_dir/nginx-$nginx_version ];then
action "nginx-$nginx_version 解压成功" /bin/true
else
action "nginx-$nginx_version 解压失败" /bin/false
exit
fi
fi
}
#先检查是否被编译过,后决定是否编译
function make_nginx(){
if [ -d $nginx_install_dir-$nginx_version ];then
action "$nginx_download_dir/nginx-$nginx_version已经被编译过,尝试重新创建软连接"
# 判断是否已存在之前的软连接
if [ -L $nginx_install_dir ];then
rm -rf $nginx_install_dir
fi
# 创建新的软连接
ln -s $nginx_install_dir-$nginx_version $nginx_install_dir
if [ -L $nginx_install_dir ];then
action "nginx重新创建软连接成功" /bin/true
else
action "nginx重新创建软连接失败" /bin/false
exit
fi
else
# 执行编译
echo "正在编译和安装nginx-$nginx_version,请稍后...."
cd $nginx_download_dir/nginx-$nginx_version && ./configure --prefix=$nginx_install_dir-$nginx_version &>/dev/null && \
make &>/dev/null && make install &>/dev/null
if [ $? -eq 0 ];then
if [ -L $nginx_install_dir ];then
rm -rf $nginx_install_dir
fi
ln -s $nginx_install_dir-$nginx_version $nginx_install_dir
if [ -L $nginx_install_dir ];then
action "nginx-$nginx_version编译成功,创建软连接成功" /bin/true
else
action "nginx-$nginx_version编译成功,创建软连接失败" /bin/false
exit
fi
else
action "nginx-$nginx_version编译失败" /bin/false
exit
fi
fi
}
# 检查nginx状态
function check_nginx(){
netstat -lntp | grep nginx &>/dev/null
if [ $? -eq 0 ];then
return 0
else
return 1
fi
}
# 启动
function start_nginx(){
check_nginx
# 说明nginx已经启动了需要先杀死
if [ $? -eq 0 ];then
# 过滤nginx master 进程号
id_nginx=$(ps -ef |grep nginx | grep " 1 " | awk '{print $2}')
# 杀死进程
kill $id_nginx
sleep 2
#判断是否杀死成功
check_nginx
if [ $? -eq 0 ];then
action "已有nginx进程正在运行,尝试杀死,但未杀死" /bin/false
exit
else
action "已有nginx进程正在运行,尝试杀死,已杀死" /bin/true
fi
fi
# 直接启动nginx
${nginx_install_dir}/sbin/nginx
# 检查是否启动成功
check_nginx
if [ $? -eq 0 ];then
action "nginx启动成功,可以通过 http://$ip:80 进行访问" /bin/true
else
action "未知错误,nginx启动失败,需要手动启动" /bin/fasle
fi
# 重新调用菜单
main_menu
break # 结束循环,进入上级菜单,也可以返回 exit直接退出脚本
}
#var_nginx 1.16.1
#wget_nginx
#uncompress_nginx
#make_nginx
#start_nginx
测试1.重复安装已经装过的版本
测试2.安装全新版本
四、 变量的作用域
Shell 变量的作用域(Scope),就是 Shell 变量的有效范围(可以使用的范围)。
4.1 局部变量:只能在函数内访问
使用local关键字定义在函数内的变量属于局部范围,只能在函数内使用,如下所示
[root@manager shell]# cat hello.sh
#!/bin/bash
# 定义函数
function test(){
local x=111
echo "函数内访问x:$x"
}
# 调用函数
test
echo "在函数外即全局访问x:$x" # 无法访问函数内的局部变量
执行结果:
[root@manager shell]# ./hello.sh
函数内访问x:111
在函数外即全局访问x:
4.2 全局变量:可以在当前shell进程中使用
所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。在 Shell 中定义的变量,默认就都是全局变量。
[root@manager shell]# cat hello.sh
#!/bin/bash
x=2222
function test(){
echo "函数内访问x:$x"
}
test
echo "在函数外即全局访问x:$x"
执行结果:
[root@manager shell]# ./hello.sh
函数内访问x:2222
在函数外即全局访问x:2222
需要注意:
- 在函数内定义的变量,如果没有用local声明,那么默认也是全局变量,shell变量语法的该特性与js的变量是类似的(在js函数内部定义的变量,默认也是全局变量,除非加上关键字var)
[root@manager shell]# cat hello.sh
#!/bin/bash
function test(){
x=2222 # 全局变量
}
test
echo "在函数外即全局访问x:$x"
[root@manager shell]# ./hello.sh
在函数外即全局访问x:2222
- 每执行一个解释器,都会开启一个解释器的shell进程,每个shell进程都有自己的作用域彼此互不干扰
[root@manager shell]# x=111 # 该变量仅仅只在当前shell进程中有效,对新的shell进程无影响
[root@manager shell]# echo $x
111
[root@manager shell]# bash # 执行bash解释器,则开启一个新的进程,或者干脆直接打开一个新的终端
[root@manager shell]# echo $x
[root@manager shell]#
- 全局变量的作用范围是当前的 Shell 进程,而不是当前的 Shell 脚本文件,它们是不同的概念。打开一个 Shell 窗口就创建了一个 Shell 进程,打开多个 Shell 窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效。
[root@manager shell]# echo $x
[root@manager shell]# cat hello.sh
#!/bin/bash
function test(){
x=2222 # 全局变量
}
test
[root@manager shell]# source hello.sh # 在当前shell进程中执行,产生一个全局变量x
[root@manager shell]# echo $x # 在当前shell进程中访问全局变量x,可以看到
2222
[root@manager shell]#
[root@manager shell]#
[root@manager shell]# cat aaa.sh
#!/bin/bash
echo $x
[root@manager shell]# source aaa.sh # 在当前shell进程中访问全局变量x,同样可以看到
2222
结论:函数test内的全局变量x早已超越了文件,即全局变量是超越文件的,作用范围是整个当前bash进程
4.3 环境变量:在当前进程的子进程中都可以使用
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效。如果使用export 命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为“环境变量”。
环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程。当 Shell 子进程产生时,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程。不难理解,环境变量还可以传递给孙进程。
[root@manager shell]# export y=333 # 爷爷
[root@manager shell]# bash # 爸爸
[root@manager shell]# echo $y
333
[root@manager shell]# bash # 孙子
[root@manager shell]# echo $y
333
ps:通过exit命令可以一层一层地退出 Shell。
ps: set env 和export 参考
set 显示当前shell的变量(本地变量),包括当前用户的变量( 环境变量)
env 显示当前用户的变量 (环境变量)
export 显示当前导出成用户变量的shell变量 (环境变量)
注意:
- 环境变量只能向下传递而不能向上传递,即“传子不传父”。
- 两个没有父子关系的 Shell 进程是不能传递环境变量的
- 我们一直强调的是环境变量在 Shell 子进程中有效,并没有说它在所有的 Shell 进程中都有效;如果你通过终端创建了一个新的 Shell 窗口,那它就不是当前 Shell 的子进程,环境变量对这个新的 Shell 进程仍然是无效的。
- 环境变量也是临时的
[root@manager ~]# ps aux | grep [b]ash$
root 2757 0.0 0.2 116188 2844 pts/0 Ss 09:40 0:01 -bash
root 22407 0.0 0.2 115944 2452 pts/0 S 23:33 0:00 bash
root 22420 0.0 0.2 115944 2456 pts/0 S 23:33 0:00 bash
注意:
# -开头的bash代表是在登录终端登录的顶级shell进程
# 非-开头的bash代表的是子shell进程
一旦退出了在终端登录的顶级shell,那么该终端下开启的所有子shell都会被回收,export设置的环境变量随即消失
所以说环境变量也是临时的,如果想设置成永久的,需要将变量写入shell配置文件中才可以,Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件中,那么每次启动进程都会定义这个变量。
五、登录shell与非登录shell
- 登录shell:就是通过输入用户名 密码后 或 su - 用户名 获得的shell
- 非登录shell:则是通过bash命令和脚本开启的shell环境
那么他们有什么区别呢?和我们永久设定环境变量又有什么关系呢?
我们知道在linux里一切皆为文件,同样,shell的属性加载也是写到文件里的
在登陆时就会加载对应文件的内容来初始化shell环境,
非登录与登录区别就在于加载的文件不同 从而导致获得的shell环境不同
我们看看登录shell都加载了那些文件
--> /etc/profile
--> /etc/profile.d
通常,我们会将环境变量设置在 $HOME/.bash_profile 中
但如果不管哪种登录shell都想使用的变量 可以考虑设置在$HOME/.bashrc 或者/etc/bashrc 中,因为它们都属于无论如何都会执行的文件,但是,如果我们真的在这类文件中添加了变量,那么意味着每次执行shell都会重新定义一遍该变量,而定义变量是要耗费内存资源的,这非常不可取,所以我们通常会结合export在/etc/profile文件中声明一个全局变量,这样在每次登录用户时产生的顶级shell里会有一个全局变量,所有的子shell都可以看到了,无需重复定义
[root@manager ~]# vim /etc/profile
[root@manager ~]# head -2 /etc/profile
# /etc/profile
name="bertwu"
[root@manager ~]# echo $name
[root@manager ~]# source /etc/profile # 可以在当前shell中立即生效,也可以退出后重新登录终端
[root@manager ~]# echo $name
bertwu
|