脚本文件格式:
第一行,顶格:#!/bin/bash
#!/bin/bash     前面不能有任何字符或空白字符,空白行更是不行。称之为shebang  标记为以bash来执行。让内核调用解释器来运行,而不是当作二进制来执行。


目录:

一、顺序执行

二、条件测试

三、算术运算

四、选择执行

五、循环执行

六、数组使用

七、函数


bash脚本,面向过程的编程中
顺序执行: 默认法则,逐条执行各语句。
选择执行: 有不同的分支,经过条件判断, 符合条件的分支就会执行。
循环执行: 将同一段代码代码反复执行多次, 有循环退出条件。


一、顺序执行

这个没有什么说的。我们来看一下基本的脚本格式。和一些杂七杂八的东西。

第一行顶格必须是#!/bin/bash。上面我们也说了。

#!/bin/bash
#
echo "Hello World"

可以用bash命令直接执行脚本:

star@sst-pp:/tmp$ bash test.sh
Hello World
star@sst-pp:/tmp$

也可以给予执行权限,再直接执行:

star@sst-pp:/tmp$ chmod +x test.sh
star@sst-pp:/tmp$ ./test.sh
Hello World
star@sst-pp:/tmp$

命令引用:

把命令替换成命令的执行结果

#!/bin/bash
#
echo "Hello World"
echo "This time is `date +%F-%T`"    #这里是键盘上tab键上面的那个`
echo "This time is $(date +%F-%T)"   #命令引用还可以用$()的方式。

执行结果:

star@sst-pp:/tmp$ ./test.sh
Hello World
This time is 2016-02-12-21:03:14
This time is 2016-02-12-21:03:14
star@sst-pp:/tmp$

注意:在当前的执行环境中,正在执行的命令和在命令引用里的命令是不一样的。如:

star@sst-pp:~/mydata/linux$ date +%F
2016-02-12
star@sst-pp:~/mydata/linux$ `date +%F`
2016-02-12:未找到命令
star@sst-pp:~/mydata/linux$

在命令引用出来以后就是命令的结果,当然如果结果碰巧也是可以执行的命令那就另一说了。了解一下,一会儿在if语句那里再提一下。

变量引用和命令引用都是用来获取数据的。$abc  获取abc的数据,$(cat /etc/passwd) 获取cat命令的执行结果。


双引号,单引号

而这里的双引号是用来把字符串括起来,不然中间有空格的话,传给echo的就只是一部分字符了。

在bash里面单双引号是不同的。
单引号:强引用,单引号里面的数据完全成了普通字符串,bash不会过多处理它。

双引号:弱引用,双引号里面的数据除了空格以外,其它的特殊字符,bash都会处理。

#!/bin/bash
#
echo 'This time is `date +%F-%T`'   #单引号
echo "This time is $(date +%F-%T)"  #双引号
echo "$$"       #显示当前shell的PID。 可以不加双引号。
echo '$$'       #这里用的单引号。

执行结果:

star@sst-pp:/tmp$ ./test.sh 
This time is `date +%F-%T`
This time is 2016-02-12-21:25:52
3014
$$
star@sst-pp:/tmp$

单引里面的变量引用和命令引用都不能使用,其它的特殊字符也一样。


位置参数:

#!/bin/bash
echo "$1-$2-$3"         #位置变量
echo "total:$*"         #总的参数
echo "count:$#"         #参数数量
echo "path:$0"          #执行路径
echo "script_name:`basename $0`"   #脚本名称

bash脚本之二(语法+测试)_第1张图片

执行结果:

star@sst-pp:/tmp$ ./test.sh 1 3 5 7 9  10
1-3-5
total:1 3 5 7 9 10
count:6
path:./test.sh
script_name:test.sh
star@sst-pp:/tmp$



二、条件测试:

如果变量有可能会为空,一定要加上双引号,在任何测试中,不然为报:需要一元表达式的错误。

测试方法

  1. test EXPRESSION

  2. [ EXPRESSION ]

  3. ` EXPRESSION `   

    EXPRESSION为测试表达式,把用于测试条件的表达式放在那个位置就可以,它会返回0或1。再由if或while之类的语句做出判断。在bash里面0为真,非0为假。

    注意:EXPRESSION两端必须有空白字符,否则为语法错误;

  4. 任何命令的执行结果都可以做为判断条件。用命令的状态返回值做判断。命令会返回0或非0。


常用判断类型:
整数测试: 比较大小。
字符测试: 比较字符串大小, 是否为空。
文件测试: 文件类型,文件是否存在。


(1)字符测试
双目测试符:

  1. >  大于

  2. <  小于

  3. ==  等于,等值比较。有时用一个=号也可以,但是为了规范,还是用两个。等号两边有空格

    [ "stringVarA" == "stringVarB" ]

  4. =~  左侧是字符串或变量,右侧是一个模式,判定左测的字符串能否被右侧的模式所匹配,通常只能在[[]]双中括号中使用。字符测试中的=~模式匹配要在[[]]里面。测试发现 \< \> 单词锚定不可用, 不会报错, 但返回值是不正确的。模式不要加引号。

    [[ "strinsA" =~ cd ]]  用来测试变量strinsA中是否有cd这个字符串。

    字符测试中的字符串或变量最好要加上引号,用来确定这个里面就是数据,避免如果是空变量的话,测试结果会不一样。

  5. !=,<>  都是不等于的意思。一般都用!=

在字符测试中有时候要用双中括号,不然会报类似的信息。

user.sh: 第 6 行: [: ==: 需要一元表达式


单目测试符:

  1. -n "$stringVar"   判断字符串或变量是否不空。这个其实只是判断后面是否有数据,就算是命令返回的数据也一样。只不过命令要放在命令引用里面。

    [ -n "$stringVar" ]    判断变量是否不空。不空为真,空为假。

  2. -z "$stringVar"   判断字符串或变量是否为空。空为真,不空为假。


注意: 变量一定要加上双引号, 因为变量如果为空,没有引号就是没有指定给判断符为空的值。这样的情况最后结果会是变量不空。



(2)数值测试:

  1. -gt  大于        greater than

  2. -lt  小于        less  than

  3. -ge  大于等于   

  4. -le  小于等于

  5. -ne  不等于

  6. -eq  等于         equal to


使用方式都一样。[ $num1 -eq $num2 ]


(3)文件测试:

单目测试: 

  1. -e file :   测试文件是否存在。  存在则为真,不存在则为假。

  2. -a file :   也是测试文件是否存在。   存在则为真,不存在则为假。

  3. -f file :   测试文件是否为普通文件 

  4. -d file :   测试文件是否为目录文件

  5. -b file :   测试文件是否为块设备

  6. -c file :  测试文件是否为字符设备

  7. -h file :   测试文件是否为符号链接文件。  -L 也是同样的功能

  8. -p file :   测试文件是否为管道文件。

  9. -S file :   测试文件是否为套接字文件。 大写的S

  10. -s file :   测试文件是否不空。 

  11. -r file :   测试执行用户是否对文件有读取权限。

  12. -w file :   测试执行用户是否对文件有写权限。

  13. -x file :   测试执行用户是否对文件有执行权限。


测试文件的时候  不要在文件右边加上斜杠,如 file/,这样会把file/ 当作一个文件,而不是file.  所以就总是假。

双目测试:

-nt  -ot 中第二个文件如果不存在,也会认为第一个文件更新或更老, 也就是结果为真。

  1. file1 -nt file2   测试是否第一个文件比第二个文件新。

  2. file1 -ot file2   测试是否第一个文件比第二个文件老。

  3. file1 -ef file2   测试两个文件是否在相同的设备并且相同的inode号。       


三、算术运算

算术运算,用来把字符变量进行算术运算。最终把字符的结果,如:23+3 变成数值的结果。不然的话:

[root@nfs ~]# a=5
[root@nfs ~]# b=4
[root@nfs ~]# c=a+b
[root@nfs ~]# echo $c
a+b
[root@nfs ~]# c=$a+$b
[root@nfs ~]# echo $c
5+4

都是字符相加了。


算术运算方式:

  1. declare
    -i  明确声明为整型变量,可以直接算术运算, a=$b+$c  
        只要把存储结果的变量声明为整型就可以。 上面的a.

  2. let  指明为算术运算  
    let varName=算术表达式 ,  let sum=$a+$b
    不能直接显示出结果,必须以这种把值赋给前面的变量的方式来显示结果。
    let 只是让后面的变量以×××计算。而至于结果给谁,怎么显示let都不会管。
    所以我们需要手动把结果给一个存储空间,在这里也就是需要赋给变量了。
    如果计算结果存在小数, 将会被圆整。

  3. varName=$[算术表达式]   也可以把中括号换成两个小括号 (())
    可以直接计算出数值,所以可以像下面这样直接用echo 显示出结果。
    如:   echo $[$a+$b]
    $[$a+$b] 其实就相当于 $[12+23]

    $[$a+$b]写成$[a+b]也是可以的。

  4. expr 来指定进行算术运算。 变量与中间的 运算符 必须要有空格。
    varName=`expr $num1 + $num2`  
    这里是直接用的命令引用,所以这个是直接计算出结果以后再给变量varName的。
    expr $num1 + $num2    就可以直接得出结果。
    因为expr是一个命令,后面的数值和运算符都是参数,所以才要空格分开。


算术运算操作符:  

+
-
*

/

%
取余
**
乘方

取余运算可以用来判断是否奇偶数, 与2 得0 也就是偶数,与2得1也就是奇数

注意:有些时候乘法符号可能需要转义,因为它是通配符啊。

增强型赋值:
变量做某种算术运算后回存至此变量中;#表示数字。
一般是这样: let i=$i+#
现在是这样: let i+=#
+=,-=,*=,/=,%=
自加,自减,自乘,。。。。。

自增:
        VAR=$[$VAR+1]
        let  VAR+=1
        let  VAR++

自减:
        VAR=$[$VAR-1]
        let  VAR-=1
        let  VAR--

如果总是在自身的基础上+=1 就可以用  ++ 来表示。
let varName++    用 let 算术运算++。


如果只是单纯的++,还可以用(())来替换let,注意不是$(())啊。

((varName++))


额外:
++的方式有两种,一是在变量后面++  一种是在变量前面++
++varName    在前面的是先加1再引用。 后面的是先引用再加1.
正常给变量赋值可能看不出来什么, 因为我们使用的只是变量。 但是:
看这个: 

[root@Test ~]# a=
[root@Test ~]# echo $[a++]
0
[root@Test ~]# echo $a
1
[root@Test ~]#

很明显是算术表达式 $[] 在运算之前,先给了echo  0 。
然后才给a加了1. 
那么如果把++放在前面:
[root@Test ~]# a=
[root@Test ~]# echo $[++a]
1
[root@Test ~]# echo $a
1
[root@Test ~]#
这就是先由算术表达式$[]给a进行了++ 运算,然后再把值给echo。
上面的$[]如果换成$(())也是一样的。 它们都是算术表达式。


脚本执行:

bash -x 显示脚本的执行过程,用于调试。bash -n 检测有没有语法错误。

四、选择执行:

(1) &&, ||

在逻辑运算里面。如果两个条件做与运算,那么只要第一个条件为假,整体的运算结果一定为假。
而做或运算,只要第一个条件为真,那么整体一定也为真。

&&为与运算符, || 为或运算符。

短路法则:
        ~]# COMMAND1 && COMMAND2
                COMMAND1为“假”,则COMMAND2不会再执行;
                否则,COMMAND1为“真”,则COMMAND2执行;

        ~]# COMMAND1 || COMMAND2
                COMMAND1为“真”,则COMMAND2不会再执行;
                否则,COMMAND1为“假”,则COMMAND2执行;

例如:

#!/bin/bash
#
id $1 &> /dev/null && echo "$1 already exis" || useradd $1

执行过程:

star@sst-pp:/tmp$ sudo ./test.sh root
root already exis
star@sst-pp:/tmp$ sudo ./test.sh sst
star@sst-pp:/tmp$ sudo ./test.sh sst
sst already exis
star@sst-pp:/tmp$

如果id的结果为真,则会执行与运算符后面的操作。而这里只是echo数据,结果也为真。那么与运算结果为真, 或运算只要运算符前面为真,则不会再执行后面的操作。

而如果id为假,与运算符后面的操作也不会再执行。 与运算整体为假, 或运算符前面为假,开始执行或运算符后面的操作。


这里有个小坑:

#!/bin/bash
#
id $1 &> /dev/null && echo "$1 already exis" || useradd $1 && echo "$1 create success"

执行过程:

star@sst-pp:/tmp$ sudo ./test.sh root
root already exis
root create success
star@sst-pp:/tmp$ sudo ./test.sh user1
user1 create success
star@sst-pp:/tmp$ sudo ./test.sh user1
user1 already exis
user1 create success
star@sst-pp:/tmp$

因为最后一个与运算符前面的条件始终都会为真,所以后面的操作每次都会执行。

如果id为真,第一个与运算符后面的echo也为真,这个与运算结果为真。然后与后面的或运算符再运算,虽然或运算符后面的useradd不会执行,但或运算结果为真。

就算是没有最后一个与运算,而前面的与和或换一下位置也会发生这种情况。

id $1 &> /dev/null || useradd $1 && echo "$1 already exis"


最后再来一个:

#!/bin/bash
#
[ -r /tmp/var.sh ] && source /tmp/var.sh && echo -e "$user\n$file\n$shell"

执行结果:

star@sst-pp:/tmp$ ./test.sh 
root
ab.txt
/bin/bash
star@sst-pp:/tmp$

从另一个文件中读入了变量。


(2) if语句

if语句有三种格式:

  1. 单分支:
    if 条件; then
           分支1;
    fi

  2. 双分支:
    if 条件; then
            分支1;
    else                   #不满足条件,执行这个。
            分支2;
    fi

  3. 多分支:
    if 条件1; then
            分支1;
    elif 条件2; then
            分支2;
    elif 条件3; then
            分支3;
           .....
    else                    #上面都不满足的时候,执行这个分支。也可以不写。
            分支n;
    fi  

注意:即便多个条件可能都能同时满足,也只是会执行其中第一个满足条件的。


:通过命令行参数给定一个用户名,如果用户存在则输出用户名与shell。

#!/bin/bash
if id $1 &> /dev/null;then
        echo "User: $1"
        echo "Shell: `grep $1 /etc/passwd | cut -d : -f 7`"
fi

执行过程:

star@sst-pp:/tmp$ ./test.sh abc
star@sst-pp:/tmp$ ./test.sh root
User: root
Shell: /bin/bash
star@sst-pp:/tmp$ ./test.sh nobody
User: nobody
Shell: /usr/sbin/nologin
star@sst-pp:/tmp$

我们知道,测试一个条件也可以用命令的执行状态,上面用&>重定向把命令的所有输出结果重定向到/dev/null。if只要这个命令的状态码就够了。

注意: 不要加上命令引用,命令引用获取的是命令的输出结果,这样if判断的就是这个命令的输出的数据了。如: if `id root`  执行的时候就变成 if uid=0(root) gid=0(root) 组=0(root)  。


:通过命令行参数给定一个用户名,判断其ID号是偶数还是奇数;

#!/bin/bash
#
uid=`id -u $1 2> /dev/null`
[ -z "$uid" ] && echo "no user $1" && exit 1

if [ $[$uid%2] -eq 1 ];then
        echo "$1 uid is Odd number"
else
        echo "$1 uid is Even number"
fi

执行过程:

star@sst-pp:/tmp$ ./test.sh root
root uid is Even number
star@sst-pp:/tmp$ ./test.sh sst
sst uid is Odd number
star@sst-pp:/tmp$ ./test.sh eer
no user eer
star@sst-pp:/tmp$

上面的2>是错误重定向,把本来应该输出到屏幕的重定向到/dev/null。下面再细说重定向。

exit退出脚本,并且脚本的返回值为后面所指定的值。 一般情况下没有exit,脚本的返回值会是最后一个命令的状态值。

:通过命令行参数给定两个文本文件名,如果某文件不存在,则结束脚本执行;都存在时返回每个文件的行数,并说明其中行数较多的文件;

#!/bin/bash
#
[ $# -lt 2 ] && echo "need more file path" && exit 1
#if all file not exis,elif single file not exis,
if ! [ -f $1 -o -f $2 ];then
        echo "$1 and $2  is not find"
        exit 1
elif ! [ -f $1 ];then
        echo "$1 is not find"
        exit 1
elif ! [ -f $2 ];then
        echo "$2 is not find"
        exit 1
fi

#line number
file1=`wc -l $1 | cut -d' ' -f1`
file2=`wc -l $2 | cut -d' ' -f1`

if [ $file1 -gt $file2 ];then
        more=$1
elif [ $file1 -lt $file2 ];then
        more=$2
else
        more="same"
fi

echo "`basename $1` line is: $file1"
echo "`basename $2` line is: $file2"
echo "more: $more"

执行:

star@sst-pp:/tmp$ ./test.sh /etc/passwd /etc/fsta
/etc/fsta is not find                                                                            
star@sst-pp:/tmp$ ./test.sh /etc/passwd /etc/fstab                                               
passwd line is: 45                                                                               
fstab line is: 21                                                                                
more: /etc/passwd                                                                                
star@sst-pp:/tmp$


I/O重定向:这里就只是简单描述一下了。

I/O 重定向(输入输出重定向)
输入重定向 : <
输出重定向:  > , >>
错误重定向:  2> , 2>>
所有输出      &> , &>>
                > * 2>&1 ,  >> * 2>&1
这里是把错误输出加到标准输出中。也可以把标准输出加入到错误输出中。
            2> * 1>&2           
&是专门用来引用文件描述符的, 也可以手动为某个文件设定自定义的文件描述符。
/dev/null  黑洞,常在重定向中使用。 重定向到些位置的数据都会清除。还有一个这里顺便提一下/dev/zero,泡泡机,常用于生成虚拟磁盘文件。以0填充生成文件,没有实际数据。

<< 可以用于在脚本中创建文件,或者生成菜单。 也算是输入重定向。
如:

star@sst-pp:/tmp$ cat > test << EOF
> Hello World                                                                                    
> +++++                                                                                          
> -----                                                                                          
> good luck
> EOF
star@sst-pp:/tmp$ cat test
Hello World
+++++
-----
good luck
star@sst-pp:/tmp$

这里的过程是<<先把数据给了cat,cat正常情况下是要输出到屏幕的,这里重定向到了文件。
cat > test这是一部分,<

上面说了,一般是用来在脚本中生成显示菜单的。如:

star@sst-pp:/tmp$ cat << EOF
> Hello
> World
> good luck
> EOF
Hello
World
good luck
star@sst-pp:/tmp$

输入的数据会由cat标准输出到屏幕上。
中间的数据也可以用变量表示,而完成自动向文件输入数据或输出到屏幕。

(3) case语句


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

case支持glob风格的通配符:
*:任意长度的任意字符;
?:任意单个字符;
[]:范围内任意单个字符;
a|b:a或b;


也支持一些特定的字符集:  
[:upper:]  大写字母        [:lower:]  小写字母
[:alpha:]  所有字母        [:digit:]  所有数字
[:alnum:]  所有字母和数字  [:space:]  空格
[:punct:]  标点符号 

这些只是代表一组特定的字符,使用的时候要在[]里面。如:[[:upper:]]。


命令补充:交互式脚本
read
        -p 可以添加提示符   如: read -p "Please Enter number" num
        -t 超时时间。
数据比变量多的话, 多出的数据会赋给最后一个变量。
变量比数据多的话, 多出的变量为空。  就算以前有值,也会变成空。


case不能像if一样做很复杂的判断,它只是简单的匹配,匹配成功则执行对应的分支。

适用于有很多选择,但不用做复杂的判断的情况。if也可以做很多选择判断,不过就有点臃肿了。

例:显示一个菜单让用户选择,通过不同的选择做出不同的操作。

#!/bin/bash
#
cat << EOF
1)show cpu info(c)
2)show disk partition info (d)
3)show disk use (s)
4)show memrofy info (m)
5)Quit(q)
EOF

read -p "Enter your choose:" option
#if option is null, default m
option=${option:-m}
#如果输入的是大写字母,把它改为小写。也可以用option=${option,}来实现,不过不常用。
option=`echo $option | tr [[:upper:]] [[:lower:]]`

case $option in
c)
        lscpu ;;
d)
        fdisk -l ;;
s)
        df -Th ;;
m)
        free -m ;;
*)
        exit 0
esac

执行:

star@sst-pp:/tmp$ sudo bash info.sh
1)show cpu info(c)
2)show disk partition info (d)
3)show disk use (s)
4)show memrofy info (m)
5)Quit(q)
Enter your choose:q
star@sst-pp:/tmp$

数据量太大了,就不贴了。 最后的*)相当于是替补了,因为*是匹配所有的,上面匹配不到的,就到这里执行来了。


五、循环执行

循环执行: 将一段代码重复执行0、1或多次;
进入条件:条件满足时才进入循环;
退出条件:每个循环都应该有退出条件,以有机会退出循环;


for循环
while循环
until循环


1、for循环

两种格式:
        (1) 遍历列表
        (2) 控制变量 
遍历列表:
        for  VARAIBLE  in  LIST; do
                循环体
        done
进入条件:只要列表有元素,即可进入循环;
退出条件:列表中的元素遍历完成;


列表的生成 

  1. {start..end}        如:{1..10}     生成1-10的列表。但是里面只能用数字,而不能用变量。

  2. seq [起始] [步长] 结束    起始默认为1  步长默认为1.
    如: seq 6 16    生成从6到16的数字列表,可以指定步长。 seq 1 2 6       得出:  1 3 5

  3. 列表不只是输入的数据,也可以是文件名,所以可以使用文件名通配符的方式代表目录下边的所有文件,这样每一个文件名都可以赋给变量。

  4. 使用命令生成, 如 ls 出来目录下的所有文件, 但是如果文件名有空格就麻烦了。

  5. 直接手动给出列表, 每个值用空格分开。

列表中的数据是以空格或tab或换行分开的。


例:求100以内所有正整数之和。

#!/bin/bash
#
sum=0
for i in {1..100};do
        let sum+=$i
done
echo $sum

执行:

star@sst-pp:/tmp$ bash sum.sh
5050
star@sst-pp:/tmp$

那如果是任意数之间的所有正整数之和呢,只要1和100用变量替换就可以啦。


例:计算当前系统上的所有用户的uid和gid之和。直接以命令取出/etc/passwd文件中的uid与gid。

#!/bin/bash
#
sum_uid=0
sum_gid=0
file=/etc/passwd

for i in `cut -d: -f3 $file`;do
        let sum_uid+=$i
done

for i in `cut -d: -f4 $file`;do
        let sum_gid+=$i
done

echo "uid sum is: $sum_uid"
echo "gid sum is: $sum_gid"

结果:

star@sst-pp:/tmp$ bash sum_id.sh 
uid sum is: 72346
gid sum is: 334031


例:再来个有意思的,输出99乘法表。

#!/bin/bash
for i in {1..9};do                            #这个是控制竖列的从1到9。 
    for y in `seq 1 $i`;do                #这个是控制一行从1到几的。这个几就是上面i的值。
        echo -n -e "${y}x${i}=$[$y*$i]\t"
    done
    echo
done

结果:

star@sst-pp:/tmp$ bash 9x9.sh 
1x1=1
1x2=2   2x2=4
1x3=3   2x3=6   3x3=9
1x4=4   2x4=8   3x4=12  4x4=16
1x5=5   2x5=10  3x5=15  4x5=20  5x5=25
1x6=6   2x6=12  3x6=18  4x6=24  5x6=30  6x6=36
1x7=7   2x7=14  3x7=21  4x7=28  5x7=35  6x7=42  7x7=49
1x8=8   2x8=16  3x8=24  4x8=32  5x8=40  6x8=48  7x8=56  8x8=64
1x9=9   2x9=18  3x9=27  4x9=36  5x9=45  6x9=54  7x9=63  8x9=72  9x9=81

如果再修改一下:

#!/bin/bash
for i in {1..9};do                                                #这里和上面一样。
    for y in `seq 1 $i`;do
        echo -n -e "${y}x${i}=$[$y*$i]\t"
    done
    echo
done
#下面是逆序的99乘法表。
for i in {1..9};do                                                #这里也是从1到9的循环。
    count=$[9-$i]                                                #因为是倒着来的,所以这里就用9减去i。
    for y in $(seq 1 $count);do                         #代表每一行的列表就是越来越小了。
        echo -n -e "${y}x$count=$[$y*$count]\t"
    done
    echo
done

结果:

star@sst-pp:/tmp$ bash 9x9.sh 
1x1=1
1x2=2   2x2=4
1x3=3   2x3=6   3x3=9
1x4=4   2x4=8   3x4=12  4x4=16
1x5=5   2x5=10  3x5=15  4x5=20  5x5=25
1x6=6   2x6=12  3x6=18  4x6=24  5x6=30  6x6=36
1x7=7   2x7=14  3x7=21  4x7=28  5x7=35  6x7=42  7x7=49
1x8=8   2x8=16  3x8=24  4x8=32  5x8=40  6x8=48  7x8=56  8x8=64
1x9=9   2x9=18  3x9=27  4x9=36  5x9=45  6x9=54  7x9=63  8x9=72  9x9=81
1x8=8   2x8=16  3x8=24  4x8=32  5x8=40  6x8=48  7x8=56  8x8=64
1x7=7   2x7=14  3x7=21  4x7=28  5x7=35  6x7=42  7x7=49
1x6=6   2x6=12  3x6=18  4x6=24  5x6=30  6x6=36
1x5=5   2x5=10  3x5=15  4x5=20  5x5=25
1x4=4   2x4=8   3x4=12  4x4=16
1x3=3   2x3=6   3x3=9
1x2=2   2x2=4
1x1=1


for循环的特殊用法:控制变量
for  ((控制变量初始化;条件判断表达式;控制变量的修正语句)); do
        循环体
done
控制变量初始化:仅在循环代码开始运行时执行一次;
控制变量的修正语句:每轮循环结束会先进行控制变量修正运算,而后再做条件判断;

例:还是小99:

#!/bin/bash
for ((i=1;i<=9;i++));do
    for ((y=1;y<=i;y++));do
        echo -n -e "${y}x${i}=$[$y*$i]\t"
    done
    echo
done

for ((i=8;i>=1;i--));do
    for ((y=1;y<=i;y++));do
        echo -n -e "${y}x${i}=$[$y*$i]\t"
    done
    echo
done

结果与上面一样。


不过我觉得这种方式一般还是用在数据从大减到小的情况时。

for ((y=$soft_num;y!=0;y--));do

而正常情况下的列表方式,是从小增到大。


while循环:


while  CONDITION; do
            循环体
             循环控制变量修正表达式
done
        
进入条件:CONDITION测试为”真“
退出条件:CONDITION测试为”假“

例:还是求100以内正整数之和。

#!/bin/bash
sum=0
num=1

while [ $num -le 100 ];do
        let sum+=$num
        ((num++))
done

echo $sum

结果就是5050,这里不贴了。

例:上面的显示信息的脚本加上循环,只有输入quit的时候才会退出循环。

#!/bin/bash
option=m
while [ $option != quit ];do
   cat << EOF
   1)show cpu info(c)
   2)show disk partition info (d)
   3)show disk use (s)
   4)show memrofy info (m)
   5)quit
EOF                #这个必须顶格
   
   read -p "Enter your choose:" option
   option=${option:-m}
   option=${option,}
   
   case $option in
   c)
        lscpu ;;
   d)
        fdisk -l ;;
   s)
        df -Th ;;
   m)
        free -m ;;
   esac
done

还要在while之前重复定义option,好像有点多余,但上面的条件中用的option又不能为空。

可以用true来表示条件,表示真。一直都是真。 而退出循环就要通过break来了。


循环控制语句:
continue:提前结束本轮循环,而直接进入下一轮循环判断;

break:提前跳出循环。

注意: break是跳出循环的,后面可以加上数字,表示跳出几层循环(在循环嵌套中)。而exit是退出脚本的。


如写成这样就可以了。

#!/bin/bash
while true;do
   cat << EOF
   1)show cpu info(c)
   2)show disk partition info (d)
   3)show disk use (s)
   4)show memrofy info (m)
   5)quit
EOF
   read -p "Enter your choose:" option
   option=${option:-m}
   option=${option,,}
   [ $option == quit ] && break            ##
   case $option in
   c)
        lscpu ;;
   d)
        fdisk -l ;;
   s)
        df -Th ;;
   m)
        free -m ;;
   esac
done


while循环的特殊用法(遍历文件的行):

while  read  VARIABLE; do
        循环体;
done  <  /PATH/FROM/SOMEFILE
依次读取/PATH/FROM/SOMEFILE文件中的每一行,且将值赋值给VARIABLE变量;

例:输出/etc/passwd文件中shell为/bin/bash的用户。

#!/bin/bash
#
while read line;do
        user=`echo $line | cut -d: -f1`
        shell=`echo $line | cut -d: -f7`
        if [[ $shell == /bin/bash ]];then
                echo -n "$user   "
        fi
done < /etc/passwd        ##
echo
star@sst-pp:/tmp$ bash user.sh 
root   star   sstc


命令的输出也可以经过管道传给while。如把上面脚本中done后面的

用管道传递的方式,效果也是一样的。

cat /etc/passwd | while read line;do
.....


而在执行脚本的时候,也是可以直接传递的。

star@sst-pp:/tmp$ cat /etc/passwd | bash user.sh 
root   star   sstc


until循环:

until  CONDITION; do
                循环体
                循环控制变量修正表达式
done

进入条件:CONDITION测试为”假“
退出条件:CONDITION测试为”真“


until跟while结构一样。只不过这个是为假才执行,为真就退出。



六、数组使用:

直接来例子:想从一些学生中随机的挑出几个。

#!/bin/bash
#discrition: this is random print student name list
modu=10    #要与人数相同
count=4       #挑出几个
student=('liMing' 'xisoHui' 'Hua' 'Mals' 'Bili' 'youo' 'dAn' 'zhangsan' 'lisi' 'xiaoli')      #学生列表
for i in `seq 1 $count`;do
    random=$[$RANDOM%$modu]         #生成随机的数字。$RANDOM是会生成随机数的变量。 与总人数取余,就可以得出从0到总人数减1之间的随机数。
    echo "${student[$random]}"               #显示数组中对应的随机索引的学生。
    student[$random]=${student[$modu-1]}     #把已经显示过的学生直接用最后一个学生覆盖。
    let modu--                        #总人数减一。
done

最后两句再解释一下:比如上面是要挑出4个学生,也就要执行四次循环,结果很有可能会在4次循环中显示出相同的学生。 而要让显示过的学生下次循环肯定不会再显示,我这里就是用最后一个学生把显示过的学生覆盖,然后下次循环的时候,随机索引就不再包含最后一个学生。

执行结果:

star@sst-pp:~/script$ bash array1.sh 
zhangsan
lisi
Hua
xiaoli


这种情况如果用每个变量保存一个学生名称就有点费劲了,而且也也太不灵活了。在数组里面可以直接在后面加学生,并把总人数改成对应值即可。


:有时候在网上下载了一个软件,在系统上计算出效验码以后与软件所提供的做对比,费眼啊。

#!/bin/bash
# describe: compare file md5/sha1 and appoint md5/sha1 same.
# default -c sha1
# exit 1 par error
# exit 3 -k error
# exit 4 can't find md5sum or sha1sum
# exit 5 can't find -f file
[ $# -lt 2 ] && echo "apr too little" && echo "Usage:`basename $0` [-c md5|sha1] -k key_code -f path_file"  && exit 

#取参数
while [ $# -ge 2 ];do
   case $1 in
   -c)
    soft=$2
    shift 2
    ;;
   -k)
    key=$2
    shift 2
    ;;
   -f)
    file=$2
    shift 2
    ;;
    *)
    echo "Usage:`basename $0` [-c md5|sha1] -k key_code -f path_file"
    exit 1
   esac
done

soft=${soft:-sha1}

case $soft in
  md5)
    soft=md5sum  ;;
  sha1)
    soft=sha1sum ;;
    *)
    echo "-c md5|sha1"
    exit 3
esac

[ ! which $soft &> /dev/null ] && echo "Can't find software $soft command" && exit 4
[ ! -e "$file" ] && echo "Can't find file $file" && exit 5
#取出变量中字符的长度
key_len=`echo ${#key}`

#把-k带入变量中的字符依次赋值给数组。其实可以直接用变量中的字符依次比较,而不用数组。
for i in `seq 0 $[$key_len-1]`;do
    key_arr[$i]=${key:$i:1}
done
#计算文件的校验码.
file_key=`$soft $file | cut -d' ' -f1`
file_key_len=`echo ${#file_key}`

#把文件的校验码依次赋值给数组。其实可以直接用变量中的字符依次比较,而不用数组。
for i in `seq 0 $[$file_key_len-1]`;do
    file_key_arr[$i]=${file_key:$i:1}
done

[ $file_key_len -ge $key_len ] && count=$file_key_len || count=$key_len

#确定输出颜色,并先打印出-k带来的校验码。
for i in `seq 0 $[$count-1]`;do
    if [ "${key_arr[$i]}" == "${file_key_arr[$i]}" ];then
        color[$i]=2
    else
        color[$i]=1
    fi
    
    echo -n -e "\033[3${color[$i]}m${key_arr[$i]}\033[0m"
done
echo

for i in `seq 0 $[$count-1]`;do
    echo -n -e "\033[3${color[$i]}m${file_key_arr[$i]}\033[0m"
done
echo

执行结果:

bash脚本之二(语法+测试)_第2张图片

这里用到了shift。

位置参数轮替(shift)
轮替的意思就是 所有位置参数向后移,连$#也是如此,
轮替完以后 位置参数相当于就不再计算前面的参数了。把前面的忽略了。

shift 默认为1个位置轮替,
可以指定 如:  shift 2


echo颜色与相关:

echo -e  用来支持控制字符  如, \n  换行    \b 退格   \t 制表符
          还可以控制特殊的输出方式。
          "\0NNN[##m数据\0NNN[0m"   

          最后的\033[0m 是用来取消属性的。 不然这个属性会应用到echo以外去。

          里面的##  
                第一个是表示颜色前景或是背景,或是字体格式,如闪烁。
                如果第一个是表示的颜色的话,第二个就是具体什么颜色。
        如: echo -e "\033[32mHello\033[0m"

如果只有一个#号位:
            [5 是跳跃
            [7 是前景背景调换
            [1 是加粗
            [4 是加下划线

如果有两个#号:
            [3 是前景色
            [4 是背景色
第二个#号:
            0黑,1红,2绿,3黄,4蓝,5紫,6天蓝,7灰。

如果有多个参数要用,可以用 ; 隔开。
echo -e "\033[32;5;1mHello\033[0m"
前景绿色;跳跃;加粗

reset 可以重启终端, 不小心把参数用到外面来了,用这个恢复默认。
其它的可以控制颜色的命令也可以恢复,如再颜色选项的ls。


echo -n 不换行。



:实现冒泡算法。随机生成几个数。从小到大排序。

#!/bin/bash
#description: 
# 
total_num=8
index_num=$[$total_num-1]
soft_num=$index_num
liushi=0

for i in `seq 0 $index_num`;do
        num_arr[$i]=`echo $RANDOM`
done

while [ $soft_num -ne 0 ];do
        for i in `seq 0 $[$soft_num-1]`;do
                if [ ${num_arr[$i]} -gt ${num_arr[$i+1]} ];then
                        linshi=${num_arr[$i+1]}
                        num_arr[$i+1]=${num_arr[$i]}
                        num_arr[$i]=$linshi
                fi
        done
        ((soft_num--))
done

for i in `seq 0 $index_num`;do
        echo ${num_arr[$i]}
done

结果:

star@sst-pp:~$ bash ooooo.sh
467
2321
2831
7015
9663
15704
29946
31867
star@sst-pp:~$

最外层的while循环是控制整体循环次数,里层的for是负责把大的数字移到数组后边的索引位置。而循环次数不用每次都一样,因为最大的数已经在最上边了,很明显它也不用再移动了。


七、函数:
函数:function
过程式编程:代码重用

注意:定义函数的代码段不会自动执行,在调用时执行;所谓调用函数,在代码中给定函数名即可;函数名出现的任何位置,在代码执行时,都会被自动替换为函数代码;

函数名也是指向内存的地址,只不过这个地址存储的是代码。


语法:两种格式

  1. FuncName(){      
           函数体
    }

    函数名和括号之间,括号和大括号之间有没有空格都可以。

  2. function FuncName {
             函数体
    }


函数可以接受参数:

传递参数给函数:
在函数体中当中,可以使用$1,$2, ...引用传递给函数的参数;还可以函数中使用$*或$@引用所有参数,$#引用传递的参数的个数;
在调用函数时,在函数名后面以空白符分隔给定参数列表即可,例如,testfunc  arg1 arg2 arg3 ...


函数返回值:

函数的执行结果返回值:
        (1) 使用echo或printf命令进行输出;
        (2) 函数体中调用的命令的执行结果;
函数的退出状态码:
        (1) 默认取决于函数体中执行的最后一条命令的退出状态码;
        (2) 自定义:return

return #    指定状态返回值,退出函数。
跟exit # 功能类似,只不过是退出函数而已。
如果在函数中用exit 也是直接退出脚本。


函数变量作用域:

  1. 所有的变量默认情况下都是属于主程序的,并且都是字符型的,虽然没有声明过,虽然没有赋值。这个是因为bash的原因。这种的变量都是本地变量。会向下传递变量,所以会把变量传递给函数,函数可以使用也可以修改。这样的情况下,在函数中给同一个变量赋值会修改变量在主程序中的值,所以在函数中最好使用local来指定局部变量。

    local    来指定是局部变量。把变量名和数据放到自己的内存空间存放,不把变量和数据放到主程序所拥有的内存空间。这样不管变量在主程序中有没有值,都是不会变的。


  2. 在函数中声明的本地变量,在主程序中不可以使用,因为在函数中声明的变量,是属于这个函数的,只可以向下传递,但是不可以向上把变量传递给主脚本, 可以向下传递给子函数。也不会修改主脚本变量的已有的值。只要声明这个变量,那么它们的所指定的内存空间地址就不一样。不能传递给子shell
               
    如果只是在函数中给变量赋值,那么主程序可以使用这个变量。因为只要是没有指定在函数中声明,这个变量就是属于被主程序声明过的, 而且还是字符型的。谁声明变量,变量就在谁的内存空间中,就算是都声明了同样的变量,那也是会在各自的内存空间,互不干拢。

    环境变量只有跨shell才会失效,子shell有效。所以在脚本中声明的环境变量在脚本之外就不能用了


  3. 位置变量。 位置变量不属于脚本, 只是简单的给脚本传递参数,所以,脚本的位置变量,在函数中是不可以使用的。不过函数也是有位置参数的,只不过是脚本调用函数的时候指定传递给函数的。
    如:  functionName Parameters1 Parameters2 ....
    并且可以间接的把脚本的位置参数传递给函数。
    functionName $1 $2
    functionName $*
    把脚本的位置变量的数据传递给函数
                             
    注意上边是把脚本位置变量的数据,而不是把位置变量直接传递


例:

#!/bin/bash

test () {
echo $1 $2
echo $*
echo $#
}

test a b c d e

结果:

star@sst-pp:/tmp$ bash fun.sh 
a b
a b c d e
5
star@sst-pp:/tmp$

例:n*n表带上颜色。指定从几乘到几,并且加上颜色。

#!/bin/bash
#
first=$2
first=${first:-0}
#这个函数就是为了生成一个1-7之间随机的返回值,让后面的echo来使用。从而也就有了随机的颜色了。
color () {
    col=0
    while [ $col -eq 0 ];do        #因为0是黑色,黑色的窗口显示不出来。所以只要是0就重新生成随机值。
        col=$[$RANDOM%8]
    done
    return $col
}

#need multiply number(second number)
nxn () {
    number=$1
    for i in $(seq $first $number);do
        color                    
        echo -e -n "\033[3${?}m${i}x${number}=$[$i*$number]\033[0m\t"
    done
}

for i in $(seq $first $1);do
    nxn $i
    echo
done

co=$[$1-1]

while [ $co -ge $first ];do
    nxn $co
    echo
    ((co--))
done

结果:

bash脚本之二(语法+测试)_第3张图片

bash脚本之二(语法+测试)_第4张图片

前一个参数是乘到几结束,后面的参数是从几开始。


例:ping 172.16.网段*.1的主机是否在线。

#!/bin/bash
#
#
trap "echo 'quit';exit" INT                #信号捕捉,不然的话,我们的ctrl+c的信号很可能是关不了脚本的。
address="172.16"
up_count=0
down_count=0

opration () {                   #这个函数只是负责接受IP地址,然后PING,再根据状态用不同的颜色显示出来。
        ping -W 1 -c 1  $1 &> /dev/null  && state=0 || state=1
        if [ $state -eq 0 ];then
                echo -e "\033[32m$1\tis up\033[0m"
                ((up_count++))
        else
                echo -e "\033[31m$1\tis down\033[0m"
                ((down_count++))
        fi
}
for i in {0..255};do
        opration "$address.$i.1"
done

opration "$address.67.1"
echo "$up_count is up"
echo "$down_count is down"

结果:

bash脚本之二(语法+测试)_第5张图片


信号捕捉:只能在脚本的最前面。
列出信号:
trap  -l
kill  -l
trap  'COMMAND'  SIGNALS
常可以进行捕捉的信号:HUP, INT


man 7 signal   查看信号帮助。
不能捕捉kill(9) 和 term(15) 信号。
trap在脚本开始就开始运行,等待信号。现在一些信号由trap来处理,也就不会再发生关不掉脚本的情况了。如像上面的脚本,如果没有trap的话,关闭信号每可能会被ping捕获。
捕捉到信号以后 执行trap定义的操作。



复制命令的脚本,在做小linux系统的时间可能要复制命令之类的,对应的库当然也要复制啦:

默认复制到/mnt/sysroot

#!/bin/bash
# Author: xingxing
# Version: 0.1
# Email:
# Description: Move software(include library)
# soft is software name
# outPath is base output dirctory
# softPath is software dirctory

# Target root(/) dir. Target desk / partition have to mount here.  
outPath=/mnt/sysroot
PATH=/bin:/sbin:/usr/bin:/usr/sbin

createDir () {
    dirname=`dirname $1`
    if ! [ -d "$outPath$dirname" ];then
        mkdir -p $outPath$dirname
    fi
}

moveSoft () {
    createDir $softPath
    cp $softPath $outPath$softPath
}

moveLib () {

    ldd $softPath | while read linkData;do
                libPath=`echo $linkData | grep -o "/.*[[:space:]]"`
                [ -z "$libPath" ] && continue
                    createDir $libPath
                    cp $libPath $outPath$libPath
            done
            
}

while true;do
    read -p "Enter software name(quit): " soft

    if [ -z "$soft" ];then
        continue
    elif [ "$soft" == "quit" ];then
        break
    elif ! which $soft &> /dev/null;then
    
        if type $soft &> /dev/null;then
            echo -e "\033[35m$soft is build software\033[0m"
        else
            echo -e "\033[35m$$soft is not exist\033[0m"
        fi

        continue
    fi

    softPath=`\which $soft`
    
    if [ -e "$outPath$softPath" ];then
        echo -e "\033[35m$soft is already exist\033[0m"
        continue
    fi

    moveSoft
    retu1=$?
    moveLib
    retu2=$?

    [ $retu1 -eq 0 -a $retu2 -eq 0 ] && \
    echo -e "\033[33m$soft move was success\033[0m"  || \
    echo -e "\033[35m$soft move was failed\033[0m"

done

结果

bash脚本之二(语法+测试)_第6张图片


就这样吧。自己现在也不熟练,想起来什么再来补充吧。希望能帮助到朋友们。

j_0022.gif


其它:

多进程:把执行放入后台。

#!/bin/bash
i=1
while [ $i -lt 10 ];do

        echo "$i"&
        ((i++))
done
wait
echo "dkfjdlkf"

结果:

bash脚本之二(语法+测试)_第7张图片


多的语句可以用大括号:

#!/bin/bash
i=1
while [ $i -lt 10 ];do
        {
        echo "$i"
        echo "$i"
        }&
        ((i++))
done
wait
echo "dkfjdlkf"

wait 等待前面的后台任务全部完成以后才往下执行。   不然结果会是这样:

bash脚本之二(语法+测试)_第8张图片

因为都在后台,所以不会顺序输出,甚至脚本都退出了,后台的程序还在输出。

上面敲回车后进入正常的终端。