在上次的课程中我们讲述了函数和case语句,case语句的语法格式为:

case $VARIABLE in
PAT1)
    分支1
    ;;
PAT2)
    分支2
    ;;
*)
    分支n
    ;;
esac

case是一个多分支的判断语句,与多个值进行比较时,case是比if语句要好用的多,在这里的PAT可使用GLOB通配符,以及|为或者之意,而如果做非字符串的等值比较时,则case语句就无法取代。

紧接着又讲到了函数,函数只能是被调用才能被执行,而给定函数名则即可进行调用,可接受参数,使用$1$2等位置变量进行声明,函数的组成目的是能够实现结构化编程,同时还能完成代码重用,函数的使用可引入了新的上下文,可使用局部变量,它的生命周期是当函数的生命周期结束,则会提前自动销毁,那么其函数的定义格式为:

function f_name {
    函数体
}

f_name() {
    函数体
}

局部变量:local VARIABLE

一、数组

接下来我们开始讲数组,我们的程序是由指令和数据组成,对于bash来讲,指令就是编程当中的语法关键字,加上各种能调用的系统命令及定义的函数等都可以理解为指令,而数据对于bash来讲,存放在变量和文件,当我们要存储多个类型相同的数据时,就可以使用一个叫数组的东西用来存储。

所以说数组和变量一样,只不过不同的是存储多个元素连续的内存空间,而变量是存储单个元素的内存空间。

数组也有数组名,且整个数组只有一个名字,它也是指向连续内存空间的开始处的起始位置,数组当中存储多个元素,如果想引用某个特定元素的话,就要使用另外一种机制找到该元素,那么这时候就要使用数组中的下标,也称为索引机制。

最开始,也就是第一个元素,我们用0来进行表示,而后我们就使用1,2,3,4分别来表示。

数组:
    程序=指令+数据
        指令:
        数据:变量、文件;

    变量:存储单个元素的内存空间;
    数组:存储多个元素的连续的内存空间;
        数组名:整个数组只有一个名字;
        数组索引:编号从0开始;
            数组名[索引]

以上数组名[索引]这种方式就可以引用数组中的元素,对于bash而言,其真正使用的格式为:${ARRAY_NAME[INDEX]},这里需要注意的是,花括号可不能够省略,因为去掉了花括号,$ARRAY_NAME就会被认为这是个变量,而并不会理解为这是一个数组的索引。所以对于数组以及数组的元素而言,则必须加上{}

对于bash-4.0以后的版本,对于数组的索引也可以支持自定义的索引格式,也称之为稀疏格式,不仅是0,1,2,...定义的数字格式。而这类数组我们就称之为关联数组

注意:bash-4及之后的版本,支持自定义索引格式,而不仅是0,1,2,... 所定义的数字格式。
    此类数组称之为"关联数组";

1.1 声明、赋值及引用数组的方式

对于数组来讲,是需要事先进行声明的,如果不声明,很可能会当作单个变量来使用。

声明数组:
    declare -a NAME: 声明索引数组;
    declare -A NAME: 声明关联数组;

以上就是数组声明的两种方式,那么数组就是在内存空间当中拥有多个连续的空间且分别能够存储多个连续的数据,每个数据都是独立的单元,只是没有独立的名称,需要用数组名称来来进行引用,但为了引用每一个元素,我们使用下标来进行,可理解为数组的标识,而下标可有两种方式,第一种是以数值的形式,我们称之为索引数组,第二种使用的是字符串来进行的下标,我们称之为关联数组。

而使用数组时,如何向数组中的元素进行赋值方法有很多种。

数组中元素的赋值方式:
    (1) 一次只赋值一个元素:
        ARRAY_NAME[INDEX]=VALUE

    (2) 一次赋值全部元素:
        ARRAY_NAME=("VAL1", "VAL2", "VAL3" ...)

    (3) 只赋值特定元素:
        ARRAY_NAME=([0]="VAL1", [3]="VAL5")

        注意:bash支持稀疏格式的数组;

    (4) read -a ARRAY_NAME

示例:

# animals[0]=pig
# animals[1]=bear
# echo ${animals[0]}
pig

这就是数组元素赋值,无需事先声明,这种一次只是赋值一个元素,如果echo回显没有使用下标的话,默认显示为0元素的数值。

引用数组中的元素:${ARRAY_NAME[INDEX]}
    注意:引用时,只给数组名,表示引用下标为0的元素;

这是第一种赋值方式,那么第二种一次赋值多个元素的方式为:

# weekdays=("Monday" "Tuesday" "Wednesday" "Tourday")
# echo ${weekdays[1]}
Tuesday

第三种是是赋值特定的,就是可以在挑选元素赋值,就是稀疏格式的数组,所谓稀疏格式就是:每个元素的那个值都不一定会存在。

# sword=([0]="Yitian" [3]="Tudong" [5]="Gelu")
# echo ${sword[1]}

# echo ${sword[3]}
Tudong

最后就是使用read -a来给数组进行赋值,自动赋值给多个元素。

# read -a jianghu
Yuebuqun Dongfangbubai Linghuchong Qianchaotaijian
# echo ${jianghu[1]}
Dongfangbubai

如果我们想使用关联数组的话,示例如下:

# world[us]="america"
# echo "${world[us]}"
america
# world[uk]="United Kingdom"
# echo "${world[uk]}"
United Kingdom

我们自己直接给出字符串下标当作索引,来实现数组中的元素也是可以的,而这种就叫做关联数组。

那么引用数组中的元素我们也展示过了,为${ARRAY_NAME[INDEX]},需要注意的是,引用只给出数组名的话,会表示引用的为下标为0的元素。

引用数组中的元素:${ARRAY_NAME[INDEX]}
    注意:引用时,只给数组名,表示引用下标为0的元素;

另外我们也可以引用脚本时的参数一样,来引用数组中的所有元素及个数,说白了就是引用数组的长度。

数组的长度(数组中元素的个数)
    ${#ARRAY_NAME[*]}
    ${#ARRAY_NAME[@]}

示例:生成10个随机数,并找出其中的最大值和最小值。

#!/bin/bash
#

declare -a rand
declare -i max=0

for i in {0..9}; do
    rand[$i]=$RANDOM
    echo "${rand[$i]}"
    [ ${rand[$i]} -gt $max ] && max=${rand[$i]}
done

echo "Max:$max"

示例:定义一个数组,数组中元素是/var/log/目录下所有以.log结尾的文件;统计其下标为偶数的文件中的行数之和。

#!/bin/bash
#

declare -a files
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."

引用数组中的元素个数我们刚才讲过了,但如果要引用数组中的所有元素。

${ARRAY_NAME[*]}
${ARRAY_NAME[@]}

那么我们接下来开始讲述一下数组元素切片,那个切片的目的就是可以返回有限的几个元素。
示例:

# files=(/etc/[Pp]*)
# echo "${files[*]}"
/etc/PackageKit /etc/pam.d /etc/passwd /etc/passwd- /etc/pbm2ppa.conf /etc/pinforc /etc/pkcs11 /etc/pki /etc/plymouth /etc/pm /etc/pnm2ppa.conf /etc/polkit-1 /etc/popt.d /etc/postfix /etc/ppp /etc/prelink.conf.d /etc/printcap /etc/profile /etc/profile.d /etc/protocols /etc/pulse /etc/purple /etc/python

我们可以保留某些个元素,而切片是用偏移量offset,意味这偏移过去几个元素并取几个元素number

数组元素切片:${ARRAY_NAME[@]:offset:number}
    offset:要路过的元素个数;
    number:要取出的元素个数;省略number时,表示偏移量之后的所有元素;

示例:

# echo ${files[@]:3:4}
/etc/passwd- /etc/pbm2ppa.conf /etc/pinforc /etc/pkcs11
# echo ${files[@]:3}
/etc/passwd- /etc/pbm2ppa.conf /etc/pinforc /etc/pkcs11 /etc/pki /etc/plymouth /etc/pm /etc/pnm2ppa.conf /etc/polkit-1 /etc/popt.d /etc/postfix /etc/ppp /etc/prelink.conf.d /etc/printcap /etc/profile /etc/profile.d /etc/protocols /etc/pulse /etc/purple /etc/python

对于数组而言,我们还可以向数组中追加元素。给明元素下标,但是如果不知道数组有多少元素的话,就要引用数组的长度。

向非稀疏格式数组中追加元素:
    ARRAY_NAME[${#ARRAY_NAME[*]}]=
# animals[${#animals[*]}]=admin
# echo "${animals[@]}"
pig dog admin

那么删除数组中的某个元素的话我们使用unset指令,与之前的撤销变量差不多,只不过不同的是撤销变量的话需要使用$,并不是撤销元素中的值而已,而在数组中就直接输入那个数组中的那个元素就可以。

删除数组中的某元素:
    unset ARRAY[INDEX]
# unset animals[0]
# echo "${animals[@]}"
dog  admin

刚才讲到过关联数组是什么,关联数组是以用字符为下标的形式来进行,需先声明其关联数组而后可进行赋值,那么它的声明方式及格式为:

关联数组:
    declare -A ARRAY_NAME
        ARRAY_NAME=([index_name1]="value1" [index_name2]="value" ...)

二、bash的内置字符串处理工具

接下来我们就说一下常用的字符串处理工具,首先第一个就是字符串切片工具,与数组切片差不多,偏移过去取出那几个字符。

字符串切片:
    ${var:offset:number}
        取字符串的子串;
        取字符串的最右侧的几个字符:${var: -length}
            注意:冒号后必须有一个空白字符;
# name=Jerry
# echo ${name: 1}
erry
# echo ${name: 2}
rry
# echo ${name:2:2}
rr
# echo ${name: -4}
erry

还有一种就是基于模式去字符串的子串。

基于模式取子串:
    ${var#*word}:其中word是指定的分隔符;功能:自左而右,查找var变量所存储的字符串中,第一次出现的word分隔符,删除字符串开头至此分隔符之间的所有字符;
    ${var##word}:其中word是指定的分隔符;功能:自左而右,查找var变量所存储的字符串中,最后一次出现的word分隔符,删除字符串开头至此分隔符之间的所有字符;
# mypath="/etc/init.d/functions"
# echo "${mypath#*/}"
etc/init.d/functions

# echo "${mypath##*/}"
functions

从以上两个实例看得出,第一个以/为分隔符,表示自左而右,查找第一次的分隔符/,把整个开头的分隔符到第一次出现的/将其删除。

而第二个实例就是自左而右找出最后一个/分隔符,将第一次开头的分隔符到最后一个开头的分隔符之间的范围将其删除,可作为取路径基名。

除了自左而右,我们还可以自右而左。

${var%word}:其中word是指定的分隔符;功能:自右而左,查找var变量所存储的字符串中,第一次出现的word分隔符,删除此分隔符至字符串尾部之间所有的字符;
${var%%word}:其中word是指定的分隔符;功能:自右而左,查找var变量所存储的字符串中,最后一次出现的word分隔符,删除此分隔符至字符串尾部之间的所有字符;
mypath="/etc/init.d/functions"
${mypath%/*}:/etc/init.d

url=http://www.china.org:80
    ${url##:*}
    ${url%%:*}

那么接下来介绍的是如何进行查找及替换。

查找替换:
    ${var/PATTERN/SUBSTI}:查找var所表示的字符串中,第一次被PATTERN所匹配到的字符串,将其替换为SUBSTI所表示的字符串;
    ${var//PATTERN/SUBSTI}:查找var所表示的字符串中,所有被PATTERN所匹配到的字符串,并将其全部其替换为SUBSTI所表示的字符串;

    ${var/#PATTERN/SUBSTI}:查找var所表示的字符中,行首被PATTERN所匹配到的字符串,将其替换为SUBSTI所表示的字符串;
    ${var/%PATTERN/SUBSTI}:查找var所表示的字符中,行尾被PATTERN所匹配到的字符串,将其替换为SUBSTI所表示的字符串;

    注意:PATTERN中使用的glob风格和通配符;
# userinfo="root:x:0:0:root admin:/root:/bin/chroot"
# echo "${userinfo/root/ROOT}"
ROOT:x:0:0:root admin:/root:/bin/chroot
# echo "${userinfo//root/ROOT}"
ROOT:x:0:0:ROOT admin:/ROOT:/bin/chROOT
# echo "${userinfo/#r??t/ROOT}"
ROOT:x:0:0:root admin:/root:/bin/chroot
# echo "${userinfo/%r??t/ROOT}"
root:x:0:0:root admin:/root:/bin/chROOT

除了查找及替换外,还有一个就是查找及删除。

${var/PATTERN}:以PATTERN为模式查找var字符串中第一次匹配,并删除之;
${var//PATTERN}:
${var/#PATTERN}:
${var/%PATTERN}:

那么下一个介绍的就是字符大小写转换。

字符大小写转换:
    ${var^^}:把var中的所有小写字符转换为大写;
    ${var,,}:把var中的所有大写字符转换为小写;

最后一个介绍的是变量赋值。

变量赋值:
    ${var:-VALUE}:如果var变量为空,或未设置,那么返回VALUE;否则,则返回var变量的值;
    ${var:=VALUE}:如果var变量为空,或未设置,那么返回VALUE,并将VALUE赋值给var变量;否则,则返回var变量的值;
    ${var:+VALUE}:如果var变量为空,则返回VALUE;
    ${var:?ERROR_INFO}:如果var为空,或未设置,那么返回ERROR_INFO为错误提示;否则,返回var的值;

第一个就是如果这个变量本身有值,就返回自己本身的值,而如果这个变量没值,就返回后面所定义的值。

# echo ${hi:-world}
world
# hi="america"
# echo ${hi:-world}
america

那么第二个就是如果这个变量的值为空的话,就将这个值赋予给这个变量,不然的话就返回这个变量的定义的值。

# echo $hello

# echo "${hello:=world}"
world
# echo $hello
world

第三个就是如果var不空,则返回VALUE

# unset hello
# echo $hello

# echo "${hello:+worlds}"

# hello=world
# echo "${hello:+worlds}"
worlds

以上就是bash内置的字符串处理工具,我们现在示例以下脚本案例,来完成如下功能。

练习:写一个脚本,完成如下功能:
    (1) 提示用户输入一个可执行命令的名称;
    (2) 获取命令所依赖的所有库文件列表;
    (3) 复制命令至某个目标目录(例如/mnt/sysroot)下的对应路径中;
        bash, /bin/bash ==> /mnt/sysroot/bin/bash
    (4) 复制命令依赖到的所有库文件至目标目录下的所对应的路径下;
        /lib64/ld-linux-x6464.so.2 ==> /mnt/sysroot/lib64/ld-linux-x8664.so.2

    进一步:
        每次复制完成一个命令后,不要退出,而是提示用户继续输入要复制的其它命令,并重复完成如上所描述的功能;直到用户输入"quit"退出脚本;
写一个脚本:
    ping命令去查看192.168.1.1-192.168.67.1范围内的所有主机是否在线;在线显示为up,不在线显示为down,分别统计在线主机,及不在线的主机数;

    分别使用for, while, until循环实现;
#!/bin/bash
#
declare -i uphosts=0
declare -i downhosts=0

for i in {1..67}; do
    if ping -W1 -c1 192.168.$i.1 &> /dev/null;then
        echo "192.168.$i.1 is up."
        let uphosts+=1
    else
        echo "192.168.$i.1 is down."
        let downhosts+=1
    fi
done

echo "uphosts is $uphosts."
echo "downhosts is $downhosts."
#!/bin/bash
#
declare -i uphosts=0
declare -i downhosts=0
declare -i i=1

hostping() {
    if ping -W1 -c1 $1 &> /dev/null;then
        echo "$1 is up."
        return 0
    else
        echo "$1 is down."
        return 1
    fi
}

while [ $i -le 67 ]; do
    hostping 192.168.$i.1
    [ $? -eq 0 ] && let uphosts+=1 || let downhosts+=1
    let i++
done

echo "uphosts is $uphosts."
echo "downhosts is $downhosts."