SHELL脚本编程进阶(二)

时间:2021-11-16 21:45:15

个人博客地址:http://www.pojun.tech/ 欢迎访问

写在前面(最重要)

本文部分资料和示例援引自以下书籍。在此,感谢原作者的创作,以及所有译者的付出,向他们致敬。

  1. Advanced Bash-Scripting Guide

  2. 《高级Bash脚本编程指南》Revision 10中文版

  3. Linux脚本编程执导

其中 《高级Bash脚本编程指南》Revision 10中文版《Advanced Bash-Scripting Guide》 的中文翻译版,文档翻译正在进行中,再次感谢译者付出。

前言

  在之前的文章 Linux 基础命令(十)—— SHELL脚本编程进阶(一) 中,我们已经详细的介绍了SHELL脚本编程的循环与分支的相关内容,这些循环与分支在bash编程中有着至关重要的地位。今天我们将要介绍bash编程进阶的第二部分,也是实际使用中很重要的一部分,函数和数组。

函数和数组

  第一部分 函数

    1. 函数定义

    2. 函数使用

    3. 函数返回值

    4. 函数参数

    5. 函数变量

    6. 函数的递归调用

  第二部分 数组

    1. 数组声明与赋值

    2. 数组引用

    3. 数组的数据处理

第一部分 函数

函数定义

   函数,任何一门高级编程语言中都具备的一种代码结构。其实是借用了工程上模块化的思想。函数的作用能够将简化代码的编写,使得程序结构更加的清晰。同时函数能够复用重复的代码,实现代码重用和模块化。
   bash中函数是由若干条shell命令组成的语句块,与shell程序形式上是相似的,不同的是它不是一个单独的进程,不能独立运行,而是shell程序的一部分。
   函数由两部分组成:函数名和函数体 ,bash中函数的定义如下所示。


#语法一:
function fun_name
{
...函数体...
}

#语法二:
function fun_name ()
{
...函数体...
}

#语法三:
fun_name ()
{
...函数体...
}

实际使用过程中, 三种方式没有什么区别,根据自己的喜好去使用就可以。

函数使用

  函数使用场景一般是在bash脚本中,定义函数,然后进行调用。

#!/bin/bash
#

#函数定义
hello (){
echo "Hello World!"
}
#函数调用
hello

  同时也推荐,bash 脚本中需要使用的函数全部定义到一个单独的文件中,然后在bash脚本中进行引用。这样做的好处就是不光这一个bash脚本可以引用该文件里面的函数,其他的bash脚本也可以引用里面的某些函数。CentOS典型的例子有很多,例如/etc/init.d/functions文件。

#单独定义一个函数文件
#执行vim /app/funcs

#函数定义
hello (){
echo "Hello World!"
}
#函数调用
hello

然后在bash脚本中进行调用


#!/bin/bash
#引用函数文件
. /app/funcs

#调用函数名称
hello

函数返回值

bash中有两种返回值

  • 函数执行结果返回值,使用echo命令返回,相当于Java 中的return关键字。
  • 函数的退出状态码,默认是函数体最后一条命令的退出状态码。当然也可以自定义,使用return关键字。自定义退出状态码,其格式为:
    • return 从函数中返回,用最后状态命令决定返回值。
    • return 0 无错误返回
    • return 1-255 有错误返回

#!/bin/bash
#
#定义一个能够返回字符串的函数
hello_echo (){
echo "Hello"
}

#定义一个返回命令执行结果的函数
hello_return (){
ls /etc/* &> /dev/null
return
}

#定义一个自定义状态结果的函数
hello_return_error (){
echo "test" &> /dev/null

return 1
}

#调用函数的返回结果
echo "result 1 is $(hello_echo)"

#调用函数的结果状态值
hello_return
echo "result 2 is $?"
#调用函数的结果状态值
hello_return_error
echo "result 3 is $?"

函数参数

bash 中函数支持参数的传递。但是与其他高级编程语言不同的是,bash中并不会显示的指定参数的类型和个数,而是直接在函数的最后的传入参数。与bash脚本的参数传递是一致的。
在函数体中当中,可使用 1, 2, …调用这些参数;还可以使用 @, *, $#等特殊变量。这一点与bash脚本几乎一致。


#!/bin/bash
#
hello (){
echo "参数的个数是$#"
echo "输出所有的参数$@"
echo "输出所有的参数$*"
echo "第一个参数是$1"

for arg in $@;do
echo "$arg"
done
}

#调用函数,并在调用的时候传入参数。
hello python java c#

因为bash的写法太过于灵活,以至于让人感觉bash并不是很严谨,在使用的时候可能要抛弃以往那些高级编程语言的思维。上面示例的结果如下图所示。

[root@localhost function]#./f4.sh 
参数的个数是3
输出所有的参数python java c#
输出所有的参数python java c#
第一个参数是python
python
java
c#

函数变量

bash中的变量的作用域有三种类型:

  • 环境变量:在当前shell和子shell中有效。
  • 本地变量:只在当前shell进程中有效。
  • 局部变量:只在函数的生命周期中有效。函数运行结束,变量失效。

在函数中定义局部变量的的方式是 local VARIABLE_NAME=VALUE

#!/bin/bash
#
hello (){
#定义一个函数体内的局部变量
local name="HelloBash"
echo $name
}

echo $name #没有值 因为脚本中并没有定义这样一个本地变量
hello #会输出HelloBash

函数的递归调用

函数的递归调用指的是,函数间接地或者直接地调用自身。但是在递归地同时一定要注意什么时候结束递归,避免死循环。 这是编程地一种基本能力。

实验一 实现斐波那契数列

  斐波那契数列又称黄金分割数列,因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2)利用函数,求n阶斐波那契数列

#!/bin/bash
#

#实现了斐波那契数列
read -p "请输入阶数" N

fibonacci (){
if (( $1==0 ));then
echo 0
elif (( $1==1 ));then
echo 1
else
#echo $(($[fibonacci $[$1-1]]+$[fibonacci $[$1-2]]))
let num=$(fibonacci $[$1-1])+$(fibonacci $[$1-2])
echo $num
fi
}
if [ $N -ge 0 ] &> /dev/null ;then
for I in `seq 0 $N`;do
fibonacci $I
done
else
echo "$N 不是数字或者$N 小于0"
exit
fi

实验二 实现汉诺塔

  汉诺塔(又称河内塔)问题是源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘,利用函数,实现N片盘的汉诺塔的移动步骤。

#!/bin/bash


read -p "please input the number of plates:" NUM
#执行的步骤数
STEPS=0

move () {
let STEPS++
echo "$STEPS: move plate $1 $2---------->$3"
}
#传入的参数 NUM A B C
hanoi () {

if [ $1 -eq 1 ];then
move 1 $2 $4
else
hanoi $[$1-1] $2 $4 $3
move $1 $2 $4
hanoi $[$1-1] $3 $2 $4
fi

}
hanoi $NUM A B C

实验三 fork炸弹

fork 炸弹是一种恶意地程序。实质上是一个简单地递归程序,由于程序是递归的,如果没有任何限制,这会导致这个简单的程序迅速耗尽系统里面的所有资源。

#在bash命令提示符后输入以下内容,系统很快就会崩溃。
:(){ :|:& };:
#下面这段代码是fork炸弹地形象展示。
bomb() { bomb | bomb & }; bomb

顺便附上前几年比较火的让浏览器瞬间崩溃的12行代码,其实原理与fork炸弹是类似的。


<html>
<body>
<script>
var total="";
for (var i=0;i<1000000;i++)
{
total= total+i.toString();
history.pushState(0,0,total);
}
</script>
</body>
</html>

第二部分 数组

  bash 中没有像其他高级编程语言那么多的基本数据类型(int double)和引用数据类型(List ArrayList)。bash是一种弱类型的编程语言。
  数组是存储多个元素的连续的内存空间,相当于多个变量的集合。

数组声明与赋值

数组的声明有两种方式

  • 显示声明:declare -a ARRAY_NAME
  • 直接赋值:ARRAY_NAME[INDEX]=VALUE ,如果使用这种方式,Bash会自动创建数组。

数组的赋值有多种方式

  • 一次只赋值一个元素 ARRAY_NAME[INDEX]=VALUE
  • 一次赋值全部元素 ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...)
  • 只赋值特定元素 ARRAY_NAME=([0]="VAL1" [3]="VAL2" ...)

在介绍了数组的引用之后,一并来举例说明数组的使用。

注意:索引可支持使用自定义的格式,而不仅是数值格式,即为关联索引,bash4.0版本之后开始支持,这就是关联数组,有点类似于高级编程语言中的字典

数组引用

数组的引用有下面几种方式

  • ${ARRAY_NAME[INDEX]}省略[INDEX]表示引用下标为0的元素
  • 引用所有的元素 ${ARRAY_NAME[*]},${ARRAY_NAME[@]}
  • 数组的长度 ${#ARRAY_NAME[*]} ,${#ARRAY_NAME[@]}
  • 删除数组中的某个元素:导致稀疏格式 ,unset ARRAY[INDEX]
  • 删除整个数组:unset ARRAY

数组的数据处理

数组数据的处理涉及到了数组数据的读取和追加。

  • 数组切片 ${ARRAY[@]:offset:number}
    • offset :要跳过的元素个数
    • number:要取出的元素个数
    • 区偏移量之后的所有元素 ${ARRAY[@]:offset}
  • 向数组中追加元素 ARRAY[${#ARRAY[*]}]=value

实验一 实现矩阵转置

# 编写脚本实现矩阵转置 matrix.sh
# 1 2 3 1 4 7
# 4 5 6 ==>> 2 5 8
# 7 8 9 3 6 9

#!/bin/bash
declare -A matrix
num_rows=3
num_columns=3

matrix=([1,1]="1" [1,2]="2" [1,3]="3" [2,1]="4" [2,2]="5" [2,3]="6" [3,1]="7" [3,2]="8" [3,3]="9" )

f1="%9s"
for ((i=1;i<=num_rows;i++)) do
for ((j=1;j<=num_columns;j++)) do
printf "$f1" ${matrix[$i,$j]}
done
echo
done

echo
echo

for ((j=1;j<=num_columns;j++)) do
for ((i=1;i<=num_rows;i++)) do
printf "$f1" ${matrix[$i,$j]}
done
echo
done

这里我们需要注意的是,在bash中并没有二维数组的这个概念。所以我们只能够使用自定义下标的方式来模拟二维数组,原理上是相似的。
实现效果如下图所示。

SHELL脚本编程进阶(二)

实验二 编写脚本,定义一个数组,数组中的元素是/var/log目录下所有以.log结尾的文件;要统计其下标为偶数的文件中的行数之和


#!/bin/bash
#
#bash中数组支持直接使用通配符来进行赋值
declare -a files=(/var/log/*.log)
declare -i lines=0
for i in $(seq 0 $[${#files[*]}-1]);do
#判断下标是不是偶数
if [ $[$i%2] -eq 0 ];then
let lines+=$(wc -l ${files[$i]} | cut -d' ' -f1)
fi
done

echo "Lines:$lines"

迄今为止,bash编程的大部分知识都已经介绍的差不多了,但是介绍的内容比较浅显,并没有深入的介绍。同时bash编程由于具有巨大的灵活性,导致使用方式多种多样,实际生产中应该根据自己的实际情况来灵活使用。