[TOC]

0x09 shell编程函数

描述:Bash(Bourne Again shell)也跟其他编程语言一样也支持函数,一般在编写大型脚本中需要用到,函数可以让我们将一个复杂功能划分成若干模块,让程序结构更加清晰,代码重复利用率更高,像其他编程语言一样,Shell 也支持函数。但是bash作为一种解释性语言,bash 在编程能力方面提供的支持并不像其他编译性的语言(例如 C 语言)那样完善,执行效率也会低很多。

  • Shell 函数必须先定义后使用
  • Shell 函数与其他高级语言的函数有相似之处,也有返回值、删除函数、在终端调用函数(传参和递归)等等.

函数的定义格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#常用格式
funname () {
list of commands
[ return value ]
}

#在函数名前加上关键字 function:
function function_name () {
list of commands
[ return value ] # 函数返回值,可以显式增加return语句;如果不加,会将最后一条命令运行结果作为返回值。
}

#调用函数只需要给出函数名,不需要加括号
function_name

Shell 函数返回值只能是整数,一般用来表示函数执行成功与否[ 用 $? 接收返回得数值 ],0表示成功,其他值表示失败

  • 如果 return 其他数据比如一个字符串,往往会得到错误提示:“numeric argument required”。
  • 如果一定要让函数返回字符串,那么可以先定义一个变量,用来接收函数的计算结果,脚本在需要的时候访问这个变量来获得函数返回值

实际案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
#功能:使用函数Hello直接调用函数与带有return语句的函数

#定义函数
Hello () {
echo "Url is http://see.xidian.edu.cn/cpp/shell/"
}

funWithReturn(){
echo "The function is to get the sum of two numbers..."
echo -n "Input first number: "
read aNum
echo -n "Input another number: "
read anotherNum
echo "The two numbers are $aNum and $anotherNum !"
return $(($aNum+$anotherNum)) #返回值为整数
}

#调用函数
Hello
funWithReturn

ret=$? # Capture(捕获) value returnd by last command
echo "The sum of two numbers is $ret !"

#执行结果#
Url is http://see.xidian.edu.cn/cpp/shell/

WeiyiGeek.shell函数返回值

WeiyiGeek.shell函数返回值


全局与局部变量

描述:在shell函数中也存在局部和全局变量的说法,以下面的案例来看使用了local关键字;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
aa="this is aa" #全局变量 $aa 和 $bb
bb="this is bb"
function name() { #定义函数name
local cc="this is cc" #定义局部变量$cc
local dd="this is dd" #定义局部变量$dd
echo $aa, $bb #访问参数1和参数2
echo $cc #打印局部变量
return 10 #shell函数返回值是整形,并且在0~257之间。
}

echo $dd #这里将会打印不生效,因为dd是局部变量。
name #函数调用
echo "函数返回值为:$?"

#执行结果#
[返回空值]
this is aa, this is bb
this is cc
函数返回值为:0


函数参数

在Shell中,调用函数时可以向其传递参数,在函数体内部,通过 $n 的形式来获取参数的值,
例如,$1表示第一个参数,$2表示第二个参数(前面有说),这里与高级编程语言有所不同,就是参数没有使用括号包含,而是放在函数名后以空格分割即可;

函数示例:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
funWithParam(){
    echo "The value of the first parameter is $1 !"
    echo "The value of the second parameter is $2 !"
    echo "The value of the tenth parameter is $10 !"
    echo "The value of the tenth parameter is ${10} !" #注意获取第10个参数时候的必须采用这样的形式;
    echo "The value of the eleventh parameter is ${11} !"
    echo "The amount of the parameters is $# !"  # 参数个数 (前面所说的特殊变量)
    echo "The string of the parameters is $* !"  # 传递给函数的所有参数
    echo "The string of the Single parameters is $@ !"  # 传递给函数的单个参数
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73 #注意这里传递给函数的参数

WeiyiGeek.函数参数传递

WeiyiGeek.函数参数传递


递归函数

bash也支持递归函数(能够调用自身的函数),那什么是递归函数?
答:说白了就是函数本身自我调用;

简单实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#实例1.脚本不断自我调用打印hello function,结束请按Ctrl+C结束。
function name() {
        echo $1
        sleep 1
        name "hello fucntion" #自己调用自己own
}
name

# 执行结果 ##
$ ./test.sh
hello fucntion
hello fucntion
hello fucntion


#实例2.再来看一个函数嵌套的例子
number_one () {
echo "Url_1 is http://see.edu.cn/cpp/shell/"
number_two
}
number_two () {
echo "Url_2 is http://see.edu.cn/cpp/"
}

number_one #调用需要放在number_two后

# 执行结果 ##
Url_1 is http://see.edu.cn/cpp/shell/
Url_2 is http://see.edu.cn/cpp/

补充知识点:
递归经典:可能很多人都曾经听说过 fork 炸弹,它实际上只是一个非常简单的递归程序,程序所做的事情只有一样:这个递归函数能够调用自身,不算的生成新的进程,这会导致这个简单的程序迅速耗尽系统里面的所有资源,造成拒绝服务攻击!(denial of service attack)

它定义了一个叫”.”的函数,调用了自己两次,一次是在前台,一次是在后台;

1
2
3
4
5
6
7
8
9
.()
{
.|.&
}
;
.

#或者
:( ){:|:&};: # :的函数

WeiyiGeek.递归函数炸弹

WeiyiGeek.递归函数炸弹


shell脚本函数常用脚本编写方法 (重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/bin/bash
# 函数实现输入格式效验与web应用监测

RETVAL=0
#包含脚本 让后面的函数聚可以使用里面的 action 函数与变量 (值得学习)
[ -f /etc/init.d/functions ] && . /etc/init.d/functions #由于ubuntu中没有所以下面直接简单的写了一个

# Run some action. Log its output.
# action() {
# local STRING rc
# STRING=$1;$2
# rc=$?
# if [ $rc -eq 0 ];then
# echo -n "$STRING [成功]"
# else
# echo -n "$STRING [失败]"
# fi
# echo .
# }

function usage(){
echo -e "Usage: $0 USERNAME URL \nUsername:只能是子母大小写数字以及下划线\nURL:支持https与http协议"
exit 1
}

function usernameFormat(){
[ 0 -lt $(echo "$1" | grep -E '[^a-zA-Z0-9_]'|wc -l) ] && return 1 || return 0
}

function urlFormat(){
format=$(echo "$1"|grep -oE "(^http|^https)\://\w.+\.\w+" | grep -oE "(^http|^https)") # -o 选项值得学习
if [ "$format" == "" ];then
echo "url Format Error!"
usage
elif [ "$format" == "https" ];then
return 1
elif [ "$format" == "http" ];then
return 2
else
return 3
fi
}

function checkweb(){
if [ $2 -eq 1 ];then
wget -T 10 --spider --no-check-certificate -t 2 $1 &>/dev/null #https

elif [ $2 -eq 2 ];then
wget -T 10 --spider -t 2 $1 &>/dev/null #http #值得学习的地方探测web服务
else
echo "checkweb Error!"
exit 1
fi

RETVAL=$?
if [ $RETVAL -eq 0 ];then
action "$1 url" /bin/true #值得学习的地方
else
action "$1 url" /bin/false
fi

return $RETVAL
}

function main(){
usernameFormat $1
check=$?
if [ 1 -eq "$check" ];then
echo "Username format Error!"
usage
fi
urlFormat "$2"
check=$?
checkweb "$2" "$check"
}

#关键点(这里是$*)
main $*

执行结果:

1
2
3
4
5
6
7
8
9
10
11
./webcheck.sh weiyegeek "https://www.baidu.com"
https://www.baidu.com url [成功].
./webcheck.sh weiyegeek "http://www.baidu.com"
http://www.baidu.com url [成功].

./webcheck.sh weiyegeek "http://www.baiducom.com"
http://www.baiducom.com url [失败].

#Centos7执行
./demo.sh weiyigeek http://www.baidu1.com
http://www.baidu1.com url [FAILED]

函数总结:

  • shell的位置参数($1,${n},$#,$*,$@,$?)都是函数的参数;
  • shell返回值是 exit 输出返回值(并且退出当前shell),函数里用 return 输出返回值(退出当前函数),都采用$?来获取执行的结果;
  • shell中函数传参与脚本传参是一致的,但是得注意 $0 代表任然是父脚本的名称;


删除函数

描述:像删除变量一样,删除函数也可以使用 unset 命令,不过要加上 .f 选项,如下所示:

1
unset -f function_name   #放在调用的前面!!!

如果你希望直接从终端调用函数可以将函数定义在主目录下的 .profile 文件,这样每次登录后在命令提示符后面输入函数名字就可以立即调用。

WeiyiGeek.函数嵌套删除

WeiyiGeek.函数嵌套删除


0x09 shell脚本包含

描述:Shell文件包含像其他语言一样,Shell 也可以包含外部脚本,将外部脚本的内容合并到当前脚本,也能在函数中进行调用外部变量

脚本包含:

1
2
3
#两种方式效果相同,一般常使用点号(.),但是注意号(.)与文件名中间有一空格
. filename
source filename

基础示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#例如,创建两个脚本,一个是被调用脚本 subscript.sh,内容如下:
echo "sub Script"
url="weiyigeek.github.io"

一个是主文件 main.sh,内容如下:
#!/bin/bash
. ./subscript.sh #注意:被包含脚本不需要有执行权限
echo "我的个人网站地址: ${url}" # 调用subscript里面的变量.

#执行脚本:
$chomd +x main.sh
./main.sh

#执行结果
http://see.edu.cn/2738.html

注意事项:

  • 如果subscript.sh没有权限main.sh将不会被执行;

0x10 补充知识

shell 脚本调试

描述:脚本调试功能是每一种编程语言具备得特性之一,出现一些始料未及得情况;使用调试功能可以弄清除是声明原因发生了错误或者异常;
shell脚本自身已经包含调试选项,能打印出脚本接收得参数和输入;

方法1:使用 _DEBUG环境变量:如果需要自定义格式显示调式信息可以通过_DEBUG环境变量来建立

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
#shell脚本调试
DEBUG() {
[ "$_DEBUG" = "on" ] && $@ || :
}

for i in {1..5}
do
DEBUG echo $i
done

将调试功能设置为“on”来运行脚本:_DEBUG=on ./script.sh
将需要调式的行前加上DEBUG,运行脚本前没有加_DEBUG=on就不会显示任何信息,脚本中“:”告诉shell不要进行任何操作。
WeiyiGeek.debug

WeiyiGeek.debug


方法2:使用shebang调式方法:
注释头从#!/bin/bash 修改成 #!/bin/bash -xv,其他就不用做任何操作了,这是最便捷的方法.


shell切分和提取

在进行切分文件名,提取文件名 与 提取文件扩展名,需要用到的几个操作符有:%、%%、#、##。

符号 说明
${VAR%[通配符]} % 属于非贪婪操作符,他是从右向左匹配最短结果
${VAR%%[通配符]} %% 属于贪婪操作符,会从右向左匹配符合条件的最长字符串
${VAR#[通配符]} # 属于非贪婪操作符,他是从左向右匹配最短结果
${VAR##[通配符]} %% 属于贪婪操作符,会从左向右匹配符合条件的最长字符串


实际案例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#示例1.从右向左匹配 :% 和 %% 操作符的示例(<<)

#!/bin/bash
#提取文件名或者删除后缀
file_name="text.gif"
#从$VAR中删除位于 % 右侧的通配符左右匹配的字符串,通配符从右向左进行匹配,现在给变量 name 赋值,name=text.gif,那么通配符从右向左就会匹配到 .gif,所有从 $VAR 中删除匹配结果
name=${file_name%.*}
echo file name is: $name #file name is: test

file_name="text.gif.bak.2012"
name=${file_name%.*}
name2=${file_name%%.*} # 操作符 %% 使用 .* 从右向左贪婪匹配到 .gif.bak.2012
echo file name is: $name #file name is: test.gif.bak #使用 %,匹配到right到left得第一个"." <<
echo file name is: $name2 #file name is: test  #使用 %%,匹配到从left到right得第一个"." >>


实际案例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#示例2.从左向右匹配:# 和 ## 操作符示例(<<)
#!/bin/bash
#提取后缀,删除文件名。
file_name="text.gif"
# ${VAR#*.} 含义:从 $VAR 中删除位于 # 右侧的通配符所匹配的字符串,通配符是左向右进行匹配。
suffix=${file_name#*.}
echo suffix is: $suffix #suffix is: gif

file_name="text.gif.bak.2012.txt"
suffix=${file_name#*.} # 与 %%.* 结果 相反
suffix2=${file_name##*.} # 与 %.* 结果 相反
echo suffix is: $suffix
echo suffix is: $suffix2 #操作符 ## 使用 *. 从左向右贪婪匹配到 text.gif.bak.2012
# suffix is: text.gif.bak.2012   使用 # ,取第一小数点开始到结尾的进行匹配 >>
# suffix2 is: txt  使用 ## ,取最后一个小数点的suffix(后缀名) <<

WeiyiGeek.名称切分案例

WeiyiGeek.名称切分案例


实际案例3:

1
2
3
4
5
// ## 取最后 一个 / 到末尾的字符串
// # 取第一个 / 到末尾的字符串
demo=1
for i in $(ls /nas_log/logs/student.log.2021-06-{27,28,29,30}.*.gz);do echo "${i##*/}";demo=$(($demo+1));cp ${i} /tmp/backup/${demo}-${i##*/};done
gzip -d student.log.2021-06-30-0.gz

输出说明:
1
2
3
4
5
6
/tmp/backup$ ls | more
1000-student.log.2021-06-30.6
1001-student.log.2021-06-30.7
1002-student.log.2021-06-30.8
1003-student.log.2021-06-30.9
1004-student.log.2021-06-27.0