第十六章 Shell编程

第十六章 Shell编程

16.1 shell脚本运行

所谓脚本,就是把众多命令写入一个文件中,让其按照一定的逻辑顺序执行,以完成一个具体的功能。而在Linux的shell编译环境下,shell编程与众多编程语言一样,也有其独立的语法。

16.1.1 脚本基本结构

首先,来看一下基本的shell语法格式:

vi /mnt/test.pl ---创建脚本文件,注:Linux中脚本一般为.pl或.sh后缀

#!/bin/bash ---指定编译本脚本的shell

echo hello ---输入多条命令

ls -l /var/

echo over

以上就是一个脚本的最简单的案例,其中第一句的#!/bin/bash一般必须书写,至于后面的命令,可以根据个人需求自定义编写。

我们可以看到,脚本编写其实与创建文本文档一样,使用vi即可,编写完毕,它也就是一个文档而已,需要给它增加执行权限才可以当做脚本被执行:

chmod a+x /mnt/test.pl

有了执行权限,可以直接使用绝对路径调用执行:

/mnt/test.pl ---执行脚本

16.1.2 脚本执行方式

关于脚本的执行,其实有三种方式可以实现:

方式一: 绝对路径

/mnt/test.pl ---指定绝对路径执行脚本

或者

cd /mnt ---进入脚本所在的目录

./test.pl --- .表示当前目录

注:本方式要求脚本必须有x权限才可被执行

方式二: 使用shell调用脚本

bash /mnt/test.pl ---指定使用bash编译执行脚本

注:本方式,允许脚本文件没有x权限

以上两种方式虽然可以正常调用脚本,但是当脚本中有对环境变量的配置时,会发现脚本的运行未能起作用,如下图:

图中,脚本中对PS1变量做了更改,执行后却未能生效。这是因为Linux中的编译器shell,是允许有多层嵌套的,即多个shell,一个shell的外层再套另一个shell。如下图案例:

图中可见,通过命令更改了PS1变量的值,立即生效了,但输入bash命令后,等于有重新打开了一个shell,该shell嵌套与于原shell之外,环境变量并未改变。当exit退出后,又恢复到了原shell中。

其实,每个shell都会有自己一套完整的、独立的环境变量配置,当打开一个新shell时,所有的环境变量将按系统的默认值初始化,所以新开shell不会受原shell的影响。

因此,前两种脚本的方式,都是新开一个shell执行脚本,脚本执行完毕,新开的shell自动关闭。若在脚本中对环境变量做设置,是不生效的,因为环境变量的设置,对新开的shell执行了,shell一关闭即失效。若要脚本中的环境变量设置立即生效,必须让脚本不开新shell,而在原shell上执行才可以,这就用到了第三种方式。

方式三:

source /mnt/test.pl

--- source表示使用当前shell编译执行脚本,不开启新shell

. /mnt/test.pl --- .表示当前shell,与source功能相同

16.1.3 自定义系统命令

若想让我们自己编写的脚本,像系统命令一样可以随时随地的执行,那么就要把脚本按照系统命令的原理操作。首先,我们知道,系统中的命令大多属于外部命令,执行时都是调用的其可执行程序,使用whereis和which可以查看得到。

那么,按照Linux的命令原理,手动输入的命令,都会去PATH环境变量指定的路径中去寻找命令对应的可执行程序(在环境变量一章中已讲过),可以用echo$PATH查看得到:

系统中所有命令的可执行程序,都是存放于这些路径内的。总结得到,我们可以借助于这种原理,把我们的脚本程序设置为系统命令。

假设有如下脚本:

vi /mnt/cpuTest.pl

#!/bin/bash

echo start

sar 1 1

echo end

chmod a+x /mnt/cpuTest.pl

将其设置为系统命令的方式有如下两种:

方式一:

cp /mnt/cpuTest.pl /usr/bin/ ---复制到系统命令的所在目录下

缺点:自定义脚本与系统命令不分离,难以区分,扰乱原有的系统命令;不便于管理和查找。

方式二: 常用

PATH="$PATH:/mnt" ---在PATH变量后追加上脚本所在的目录

注:若想让对PATH的设置永久生效,则需要把该命令写入到环境变量配置文件中才可以;脚本名尽量不要与系统中已存在的命令名重复。

16.2 shell编程

以上介绍了脚本的运行,下面来讲解以下具体的编程

16.2.1 变量

关于变量,是所有开发语言必不可少的运行工具,shell编程也不例外。先来解释一下变量的定义:程序运行过程中,用于临时存放数据的一块内存空间即是变量,给这块空间起个名字,即变量名(此定义虽非官方,但很容易理解)。下面来看一下变量的声明、赋值。

shu=5

以此代码为例,是声明了一个变量叫shu,即会在内存中开辟一块空间,给shu专用。=5表示给变量存入数据,即存到内存中,称为 赋值。亦或:name=zhang 也是声明并赋值。

变量的使用也同样是用$符加以提取,如下例:

echo "my name is: $name" ---用$提取变量的值,加以使用

另外,当使用变量时,若变量名与之后文件接连书写,没有空格,会造成变量名的识别错误,如:echo $shua,则shell会认为要输出变量shua的值,但如果我们只声明了变量shu,且想要输出变量shu的值呢?可参看下例解决:

shu=5

echo ${shu}a ---用{}明确变量名,则输出结果:5a

再来看一下变量的计算,先看如下案例:

可见,代码运行的结果并非我们想象的求和的结果,而是shu3=5+3。这是因为变量赋值时,默认所有数据都当字符类型处理,所以shu1、shu2其实赋值的是字符形态的3、5,所以赋值给shu3时其实仅相当于让三个字符串联而已。

那么,若要想让计算式按数学运算的方式执行,需要使用let关键字,如下例:

shu1=3

shu2=5

let shu3=$shu1+$shu2 ---let开头的代码,将以数学计算的方式执行

echo shu3=$shu3 ---此时输出结果为:shu3=8

还有,变量的赋值,也有其他方式。之上的案例中我们可以看到都是在代码中直接给变量赋值的。其实,我们还可以要求人为的从键盘输入数据赋值给变量,如下:

read shu ---read表示:要求从键盘输入一个数据,赋值给变量

例如:

图中zhang是我们手动输入的姓名。

再者,我们还可以将命令的执行结果赋值给变量。案例格式如下:

shiJian=`date +"20%y-%m-%d%H:%M:%S"`

注:用反单引号把命令引起来,功能:把命令的执行结果赋值给变量

最后再来看一下变量值的截取,如下例:

shu=abc123

shu2=${shu%%1*} ---%%表示去除右侧字符,*还是通配符,结果: shu2=abc

shu3=${shu##*c} ---##表示去除左侧字符,结果:shu3=123

16.2.2 判断语句

说到判断语句,与众多编程语言思路一样,无非是给定一个条件,如果条件满足则执行对应的代码。那么在shell中的格式如下:

if [ 条件 ]; then --- 格式要求:[ ]; 符号左右必须有空格

//代码

fi

运行逻辑:当条件满足、成立,则执行代码,否则不执行代码

虽然逻辑过程容易理解,但是关于条件的书写格式,是比较复杂的,常用格式如下:

[ $shu -gt 20 ]; --- 判断比较数字 -gt 大于 -lt小于 -eq等于 -ge大于等于 -le小于等于 –ne不等于

[ $name = "zhang" ]; ---用 = 做字符判断,注:=左右无空格,表示赋值功能,=左右有空格,表示判断,!=表示判断不等于

[ -f "/mnt/f1" ]; --- -f 判断给定的路径是否是一个文件,即判断文件是否存在 -d 判断目录 -l 判断软链接

[-x "/mnt/test.pl" ]; --- -x判断给定的文件是否有执行权限,-r 判断读权限 -w判断写权限

[-z $shu ]; ---判断变量是否为空,即未赋值。若为空,则判断成立

[ 条件1 -a 条件2 ]; --- -a表示逻辑与 -o 逻辑或 !逻辑反,即取反值

注:关于满足、不满足,成立、不成立这种对立的判断,称为布尔(bool)型数据,只有两个结果,成立满足叫true , 不成立不满足叫false。

除了这种简单的判断语句,if还有两种格式,如下:

格式2:

if [ 条件 ]; then ---如果条件满足,执行代码1,否则执行代码2

//代码1

else ---否则,若条件不满足,则执行代码2

//代码2

fi

格式3:

if [ 条件1 ]; then ---如果条件1满足,执行代码1

//代码1

elif [ 条件2 ]; then ---否则若条件1不满足,判断条件2 //代码2

elif [ 条件3 ]; then ---如果条件1、2都不满足,判断条件3

//代码3

else ---若前面条件都不满足

//代码4

fi

好了有了if的三种格式,我们在编程时,就可以依据需求完成不同条件的判断了,来看下面案例:

echo please input your age

read age

if [ $age -lt 16 ]; then

echo child

elif [ $age -lt 30 ]; then

echo younger

elif [ $age -lt 40 ]; then

echo stronger

elif [ $age -lt 50 ]; then

echo zhong nian

else

echo older

fi

以上案例中,根据年龄,逐级判断,输出年龄段状态。值得注意的是,我们排列的条件顺序是从年龄的小到大,那么当年龄大于16岁时回去判断是否小于30,一次类推。但如果我们把条件顺序反过了写,如下:

if [ $age -lt 50 ]; then

echo uncle

elif [ $age -lt 40 ]; then

echo stronger

elif [ $age -lt 30 ]; then

echo younger

elif [ $age -lt 16 ]; then

echo child

else

echo older

fi

则我们可以想象到,假设当age=15时,第一个条件小于50的判断是满足的,那么就会直接输出 uncle了,就与我们原先设想的结果完全不同。所以我们一定要先明确一点:只有在前面的条件不满足时,才会去判断后面的条件。在编写多级判断语句时一定要注意判断条件的先后顺序。

好了,下面我们来展示一个综合案例,是一个自制计算器的小程序,大家可以看明白思路后,自行编写试试:

#!/bin/bash

echo"------------------------------"

echo " welcome to my calc"

echo"------------------------------"

echo "start"

echo please input the first num:

read n1

echo please input the second num:

read n2

echo "please input the fu:+ - * / %"

read fu

if [ "$fu" = "+" ]; then

#注:因为fu可能会是*,*又表示通配符概念,所以用""还原回标准字符状态, #就不具备特殊符号的意义了

let res=$n1+$n2

elif [ "$fu" = "-"]; then

let res=$n1-$n2

elif [ "$fu" = "*"]; then

let res=$n1*$n2

elif [ "$fu" = "/"]; then

let res=$n1/$n2

elif [ "$fu" = "%"]; then

let res=$n1%$n2

fi

echo "$n1 $fu $n2 = $res" #输出最终的计算式,如:1 + 2 = 3

16.2.3 多分支语句

与if…elif…elif…else…fi 类似,shell中还有一个可以实现多层判断的语句,就是case多分支语句。下面是它的格式与思路

case $变量 in ---执行逻辑:根据变量的值,找到下面对应的项,执行代码

值1) 代码1 ;; --- ;; 两个分号,表示本项代码的结束

值2) 代码2 ;;

值3) 代码3 ;;

*) 代码4 ;; --- * 项表示,变量没有对应的值,则执行*这一项的代码

esac

例如:

echo "请输入考试名次:"

read mingCi

case $mingCi in

"1") echo "第一名奖励200元" ;;

"2") echo "第二名奖励100元" ;;

"3") echo "第三名奖励50元" ;;

"*") echo "无奖励" ;;

esac

需要介绍的是,case虽然书写简练,并且也具备多级判断的功能,但是只能做变量值的等值判断,但if…elif语句可以实现变量在区间值(如分数范围,年龄范围等)的判断,所以各有所长,在具体编程时应该在不同时机选择合适的语句。

16.2.4 循环语句

说到循环语句,各种开发语言中都有,shell中用的开发语句主要以while为主。循环语句的功能是:让计算机重复性多次执行某块代码。来看一下while的语法格式:

while [ 条件 ];

do

//代码

done

执行过程:条件判断=>执行代码=>条件判断=>执行代码=>...=>直到条件不满足,所以while语句是先判断,后执行的。

循环语句看似简单,但它的代码执行过程对初学者来说是需要逐步、逐次的思考清楚的,首先来分析一下如下案例:

例:输出100遍hello

shu=1

while [ $shu -le 100 ];

do

echo No.$shu hello

let shu=$shu+1

done

分析以上案例执行过程,变量shu的初始值为1,第一次进入while,先判断shu是否小于等于100,结果为true,那么执行代码,输出一次hell,然后shu自我增加一次(取出shu的值,加1后再赋值给shu)得到shu的值为2,到这里第一次循环结束.然后再次返回判断部分,shu值为2,小于等于100,判断成立,再次进入代码,以此类推。综上,我们可以总结到,循环中必备的有四个内容,我们称为循环四要素。

循环四要素:初值 条件 循环体(即代码) 自更新

有了四要素后,我们写完的代码,可以检查一下是否正确,要避免避免:无循环、死循环的现象。PS:无循环就是第一次条件不满足,直接跳过循环。死循环是循环内没有更新语句,造成判断条件永远成立,致使代码运行到循环后,不再停止、跳出。

好了,再来展示两个案例,以帮助大家理解循环:

例:计算1-100之间各数累加和

shu=1

sum=0

while [ $shu -le 100 ];

do

let sum=$sum+$shu

let shu=$shu+1

done

echo $sum

以上案例的思路是按照累加的过程:1+2=3,3+3=6,6+4=10…,所以每次循环累加后,把和存入sum变量,下次循环再次累加。

例:求1-100之间3的倍数之和

shu=1

sum=0

while [ $shu-le 100 ];

do

let yu=$shu%3

if [ $yu -eq 0 ]; then

let sum=$sum+$shu

fi

let shu=$shu+1

done

echo $sum

以上两个案例,读者可以逐一研究代码的执行过程,以理解循环的功能。

再有,循环中还有两个循环控制语句:continue和break,功能如下:

continue 停止本次循环,跳入下一次循环

break 停止、跳出整个循环

案例如下:

例:求1-100之间3的倍数之和

shu=1

sum=0

while [ $shu-le 100 ];

do

let yu=$shu%3 # %模运算,即求余数的运算

if [ $yu -ne 0 ]; then

let shu=$shu+1

continue

fi

let sum=$sum+$shu

let shu=$shu+1

done

echo $sum

案例中可看到,判断中当shu除以3的余数不为0时,即不是3的倍数,将会进入if语句,自加后执行continue语句,则跳出当前循环,直接进入到了下一次循环判断了,那么后面的累加和操作将不再执行。

例:计算1-100之间各数的累加和,求累加到哪个数时,和到达1000

shu=1

sum=0

while [ $shu =le 100 ];

do

letsum=$sum+$shu

if[ $sum -ge 1000 ]; then

echo $shu

break

fi

let shu=$shu+1

done

echo $shu

本案例中,当累加和到达1000时,就没有必要继续循环了,所以直接break停止了循环.

以上的所有案例,我们看到都是有固定循环次数的,其实while也可以支持没有固定次数的循环操作,如下例:

jiXu="y"; # 为了满足第一次循环,赋初值为y

while [ $jixu = "y" ];

do

echo "上午上课"

echo "下午实验"

echo "晚上自习"

echo "明天继续吗?y/n"

read jiXu

done

另外,shell编程还有for语句结构的循环,它的语法如下:

for 变量 in 值1 值2 值3 ... do

//代码

done

执行思路:用给定的值,逐一赋值给变量,带入代码执行

缺点:不支持数据范围的指定,如:1-100。PS:若要设定范围需要内嵌特殊代码。

案例:例:计算1-10之间各数累加和

sum=0

for shu in 1 2 3 4 5 6 7 8 9 10

do

letsum=$sum+$shu

done

echo $sum

16.2.5 选择语句结构

shell中,还有一个独特的语句结构,就是选择结构,这个结构在java、C语言中是没有的,下面来看一下它的语法格式:

select 变量 in 值1 值2 值3 ...

do

//代码

break ---停止,跳出select结构,若不加break句,会循环重复选择

done

执行思路:把列举的值当做菜单以供选择,根据用户选择,把对应的值赋值给变量,带入代码。

例:

select xuan in aaa bbb ccc ddd

do

echo your choice is : $xuan

break

done

执行过程如下图:

若没有break语句,则执行过程如下:

如上图,我们只能通过ctrl+c组合键关闭shell进程。

16.3 组合应用

首先,先来看一下变量赋值的一个应用:

图中可见,显示f1中第三列文字,赋值给变量words后,显示变量值时是不分行的,也就说明:当命令结果是多行状态时,赋值给变量后,将变为一行数据,即变量的值中不支持回行。

注:若想在输入命令时,让系统以shell程序的方式执行,则把多行代码用;分隔开即可。

然后,我们再来看一下read读取文档的使用:

read hang < /mnt/f1 ---读取文档中的第一行文字,赋值给变量

但是这个read命令只能读取第一行文字,再次执行还是第一行。原因是因为访问文件时会打开文件,创建文件流,会有指针读取文件的第一行文字,若再次读取,则指针会下移一行,做读取。但是用这个命令时,打开文件,读取一行后立即关闭了文件。再次执行命令,又重新打开了文件,又从第一行开始读取了,所以无法实现多行读取功能。PS:以上原因有过开发经验的读者会比较好理解,虽然不甚准确,但思路接近,比较容易理解,适合于初学者。

那么如果想要读取文件中的每一行文字呢?则需配合while循环来使用,看下例:

shu=1

while read hang

do

echo No.$shu: $hang

let shu=$shu+1

done < /mnt/f1

案例功能:逐行读取文档内容,每次读取出一行,赋值给变量,带入代码。

用while配合read使用,则读取完一行后不会关闭文件,进而就可以使指针下移一行,再次读取第二行了。需要解释的是,当read读取成功后,即等于读取操作结果为true,正适合于while的判断;而当读取完文件的最后一行后,再次读取将读取失败,则视为false的结果,所以while循环将停止。运行结果如下图:

好了,在案例中我们也可以看到文件f1原有内容类似于表格,是多行多列的内容,那么我们也可以对每行内容中的每列文件加以单独提取,案例如下:

shu=1

while read c1 c2 c3

do

echo No.$shu: $c3

let shu=$shu+1

done < f1

代码功能:逐行读取文档内容,每次读取出一行,把该行各列的文字,赋值给对应的变量,带入代码,代码中c1 c2 c3是三个变量,对应文件中每行的各列。

16.4 函数调用

16.4.1 函数的定义、调用

当我们需要以一段代码需要多次使用时,如果每次使用都要写一遍代码的话,那么又麻烦,代码又繁琐,那么可以使用函数来实现一次定义,多次使用。

函数,即是一段完整的代码,能够实现一个较小的功能,可以被shell程序所调用。格式如下:

定义格式:

function 函数名() {

代码

}

函数名 (){ ---不写function关键字也可以

代码

}

调用函数:

shell代码中,直接写函数名,即可调用。

案例:

vi test.sh

#!/bin/bash

function qiuHe(){

shu=1

sum=0

while [ $shu -le 100 ];

do

let sum=$sum+$shu

let shu=$shu+1

done

echo $sum

}

echo "我们将要计算1-100之间各数的累加和,结果如下:"

qiuHe #调用函数qiuHe

值得注意的是:(1)在shell脚本中,程序的开始运行点,并不会从函数开始,而是从函数之外的第一行代码开始执行,所以上例中运行的第一条代码是echo "我们将…"句。(2)还有shell的代码执行过程是由上往下读取到一条语句,即编译一条,所以在函数的编写时,函数的定义语句必须写在调用语句之前,否则函数将无法使用。(3)与其他开发语言不同,shell中的变量并没有严格的生存期概念,只要在之前代码出现使用过的变量,在之后代码中都可以直接使用。

16.4.2 函数的参数传递

当我们调用函数时,如果函数要用到某些数据而自己无法得到,则需要调用方为它提供,这就可以使用参数传递实现。所以参数传递的功能是:调用方,给函数传递素材性数据,让函数使用该素材数据做运算,该素材数据称为参数。

函数中参数定义的格式是:在函数代码中用 $数字 的格式来指定参数的编号、个数,如:$1 $2,若达到10个以上的参数时需用{}明确,如:${10}。调用函数时,只需要在函数名后面列举出要传递进去的数据即可,如下例:

vi test.sh

#!/bin/bash

jiaFa(){

letres=$1+$2 #使用参数,进行计算,参数与调用方给定的一一对应

echores=$res

}

shu1=5

shu2=10

jiaFa shu1 shu2 #调用函数,并在后面列举出传给它的参数

16.4.3 函数的返回值

反过来想,当函数执行完毕后,如果需要携带数据回到调用方,让调用方使用该数据继续运行,则使用函数的返回值实现。

函数中的书写格式是:在函数代码中用 return 关键字指定带回的返回值,调用方使用 $? 的格式接收返回值。案例如下:

vi test.sh

#!/bin/bash

jiaFa(){

letres=$1+$2

return $res

}

shu1=5

shu2=10

jiaFa shu1 shu2 #调用函数,并在后面列举出传给它的参数

he=$? #$?代表之前代码中离的最近的一个函数的返回值

echo $shu1 + $shu2 = $he

16.4.4 小结

通过以上的几个案例可以想到,当一段代码会经常被使用到时,我们可以提前把代码写到一个函数中,那么在之后的shell程序中,如果用到,只需要直接调用就可以了,无需再把代码编写一般,这样就实现了一次定义,多次调用的效果,既节约了代码,又清晰了思路。

另外,关于shell编程部分,初学者可能会感觉有些难度,那么首先要确保能够先理解本章中各案例的每行代码的功能,理解每个案例的执行思路。然后按照每个案例的功能,给自己设计一个类似的案例编写下试试,慢慢积累编程的思路和感觉。关于编程能力的锻炼是需要较多案例演习才能够掌握熟练的。这里为大家提供一个系统用户管理的完整案例以供大家借鉴。

vi /mnt/userManage.sh

#!/bin/bash

echo"-------------------------------------"

echo " welcome to user manage system"

echo"-------------------------------------"

echo ""

run=true

while $run

do

select xuan in "show all users" "add a new user" "change a user's password" "delete a user" "Exit"

do

case $xuan in

"showall users")

allUsers=`awk-F ":" '{print $1}' /etc/passwd`

echo $allUsers ;;

"adda new user")

echo please input a new username:

read name

useradd $name

passwd $name ;;

"changea user's password")

echo please input a username forchange:

read name

passwd $name ;;

"deletea user")

echo please input a username fordelete:

read name

userdel -r $name ;;

"Exit")

echo byebye

run=false ;;

esac

break

done

done

原文发布于微信公众号 - 教主小筑(gh_e0879483602d)

原文发表时间:2019-04-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券