一、Shell编程简介
之前课程中已经介绍过,在操作系统的内核kernel外部有一个Shell层,Shell的作用是保护内核kernel不受损害。同时,Shell接收用户对Linux系统的操作指令并传递给内核,之后由内核执行。
之前学习过的Shell命令都是运行在Shell层上的,每次我们通过terminal输入一条命令,再通过Shell交给内核执行。有时,我们的需求过于复杂,需要执行多条命令,此时一条一条的在terminal内输入命令会十分的繁琐。Shell层提供给我们一个脚本工具,我们可以将想要依次执行的命令写成脚本档,再将脚本档发送给Shell。这样Shell就会按照脚本档内记载的命令依次执行,从而实现了自动化。
//Shell脚本在Windows系统内称为“批处理档”(.bat档),本质相同
也就是说,Shell脚本的实质就是Shell命令的有序集合。
1、编译型语言与解释性语言
本质上来说,。那么翻译语言肯定需要一定编程并让计算机执行实际上是一个将其中一种语言(编程语言)翻译成另外一种语言(机器语言)的过程的翻译策略。编程语言从源代码变成计算机识别的可执行程序有“编译”和“解释”两种方式。
1)编译
- -需要编译器
- -对代码进行整体检查
- -主要进行词法检查、语法分析、语义检查和中间代码生成、代码优化、目标代码生成5部分
- -若代码有错误,则停止编译并报错;若无错误,则会生成目标代码
- -执行效率较高
- -代表语言:C/C++
2)解释
- -需要解释器
- -依次执行代码内每条命令,每句执行一次
- -只会运行到当前语句时才会翻译该命令
- -若代码有错误,则在出错语句处停止,而该语句上面的语句已经被执行
- -执行效率较低
- -代表语言:Python、JavaScript、Shell脚本
我们在进行Shell编程的时候,本质上是将多条Shell命令写在一个档内(脚本),然后terminal按照该档内每条命令开始执行命令。因此Shell脚本是一种典型的解释性编程语言。
2、Shell编程的基本步骤
我们进行Shell编程有以下几个步骤:
1.建立Shell脚本档
2.给Shell脚本档可执行的权限
3.执行Shell脚本
示例:使用Shell编程
第一步:建立Shell脚本档,文件名为myshell.sh。档内容:
#!/bin/bash
date
cat myshell.sh
ping -c 3 114.114.114.114
echo "hello world"
脚本内的命令分别代表:
1.#!/bin/bash是Shell脚本档的固定开头,该命令说明了这个Shell脚本在哪个程序上执行。在这里表示使用/bin/bash来执行这个脚本。
注:在Shell脚本中,#开头的行表示注释。
2.打印当前系统时间
3.查看myshell.sh档内容
4.ping3次114.114.114.114检查网络通断
5.在终端打印hello world
第二步:给Shell脚本档可执行的权限
我们刚刚写的Shell脚本档只是一个普通的文本档,没有可执行的权限,因此我们需要给该档添加可执行权限。
chmod 0775 myshell.sh
这样myshell.sh就具有可执行权限了。我们可以使用ls -l命令查看该档的权限
第三步:执行该Shell脚本
./myshell.sh
执行该Shell脚本即可查看效果
二、Shell编程的变量
和C语言一样,Shell编程也有变量。在Shell脚本内,所有的变量都是字符串类型但是不支持数据类型(整型、字符型、浮点型等),并且无需对变量进行声明。
在sh/bash中有4种变量:
-用户自定义变量
-位置参数(命令行参数)
-预定义变量
-环境变量
1、用户自定义变量
Shell编程允许用户自定义变量来存储数据,但不支持数据类型(整型、字符型、浮点型等),任何变量的值都被解释成一个字符串。变量名命名规则如下:
1.首字符必须是字母或者下划线
2.中间不能有空格,可以使用下划线_表示空格
3.不能使用除下划线外其他的标点符号
我们可以使用=给变量赋值,格式为:
变量名=变量值
注意:等号两边没有空格。在Shell编程中变量名一般使用全大写字母,为了与Shell命令区分。
COUNT=1
若想调用一个变量的值,则需要在该变量名前面加上$。例如:
#!/bin/bash
COUNT=1
VAR="hello world"
echo $COUNT
echo COUNT
echo $VAR
注意第一个echo命令与第二个echo命令输出的区别。第一个echo命令会输出COUNT变量的值1,而第二个echo命令会将COUNT当做待输出的字符串处理,会输出COUNT。
Shell编程支持变量互相赋值,赋值方向为从右向左。例如:
Y=2
X=$Y
则变量X内就得到了变量Y的值2。
某些情况下,我们定义的变量会与其他文字混淆。例如:
num=2
echo "this is the $numnd"
此时执行该Shell脚本不会输出"this is the 2nd"而会输出"this is the",因为echo命令无法找到一个名为"numnd"的变量。我们可以使用花括号来包裹变量名,被花括号包裹的字符串会被Shell当做是一个变量来处理。
num=2
echo "this is the ${num}nd"
这样就可以输出"this is the 2nd"。
我们可以使用unset命令删除已定义过的变量。例如:
Z=hello
echo $Z
unset Z
echo $Z
则第二次执行的时候就不会输出变量Z的值。
2、位置变量(命令行参数)
由系统提供的参数称为位置参数,其作用等价于C语言中main函数传参的“命令行参数”。位置参数可以使用$+数字的形式获得。
$0 与键入命令一样
$1~$9 第一个到第九个命令行参数
例如:
#!/bin/bash
echo "this is 0" $0
echo "this is 1" $1
在执行该Shell脚本的时候我们携带一个命令行参数:
./myshell 123
则会输出:
this is 0 ./myshell.sh
this is 1 123
3、预定义变量
Shell编程内事先定义了一些变量,用户只能使用这些变量而不能重新定义它们。所有的预定义变量都由$符号和另一个符号构成,常用的预定义变量如下:
$# 命令行参数的个数
$@ 所有命令行参数(不计$0,同$*)
$? 前一个命令的退出状态
$* 所有命令行参数(不计$0,同$@)
$$ 正在执行的进程ID号
示例:演示各个位置变量的值
#!/bin/bash
echo "this is 0" $0
echo "this is 1" $1
echo "this is #" $#
echo "this is @" $@
echo "this is ?" $?
echo "this is *" $*
echo "this is $" $$
执行:./myshell.sh 123 456 789
输出:
this is 0 ./myshell.sh
this is 1 123
this is # 3
this is @ 123 456 789
this is ? 0
this is * 123 456 789
this is $ 2710
4、环境变量
环境变量适用于所有用户进程,环境变量均为大写。常用的环境变量如下:
- HOME 用户工作目录所在地址,在档/etc/passwd档存储器储
- IFS(Internal Field Separator) 内部字段分隔符,默认是空格、tab以及换行符
- PATH Shell搜索路径
- PS1 命令提示符格式($及$前的字符)(PS是Prompt Sign的缩写)
- PS2 换行提示符>
- TERM 终端类型,常见的值有vt100、vt200、ansi、xterm等
- HISTSIZE 保存历史记录的条目数
- LOGNAME 当前登录用户名
- HOSTNAME 主机名称
- SHELL 当前使用的Shell类型
- LANG/LANGUAGE与语言相关的环境变量
- MAIL 用户的邮件存放目录
- TMOUT 设置的脚本过期时间。例如TMOUT=3则表示该脚本3秒后过期
- UID 登录用户的ID
- USER 显示当前用户名
- SECONDS 记录脚本从开始运行到结束耗费的时间
示例:演示各个环境变量的值
#!/bin/bash
echo "HOME IS " $HOME
echo "IFS IS " $IFS
echo "PATH IS " $PATH
echo "TERM IS " $TERM
echo "LANGUAGE IS " $LANG
echo "LOGNAME IS " $LOGNAME
echo "HOSTNAME IS " $HOSTNAME
echo "SHELL IS " $SHELL
echo "MAIL IS " $MAIL
echo "UID IS " $UID
echo "USER IS " $USER
我们可以使用env命令查看更多的环境变量信息。
我们可以使用export命令来自定义环境变量,使用unset命令清除环境变量。例如:
export HELLO="Hello"
echo $HELLO
三、Shell编程语句
Shell脚本由0条或多条Shell语句构成,Shell语句可以分为三类:
1.说明性语句:以#开始到该行结束,对Shell语句说明,不会被执行。类似C语言的注释
2.功能性语句:任意的Shell命令、用户程序或其他
3.结构性语句:条件语句、分支语句、循环语句、循环控制语句等
1、说明性语句(注释行语句)
说明性语句可以出现在程序的任意位置,既可以独立一行,也可以接在其他语句后面。说明性语句都是以#开头的语句,在Shell脚本执行的时候不会被解释执行。
2、功能性语句
1)读入键盘的值read
read从标准输入内读数据,并赋值给后面的变量。语法格式为:
read 待赋值的变量
一个read命令会读取一行。例如:
read WORD#从输入读取一个字符串值赋值给变量WORD
read VAR1 VAR2 VAR3#从输入内读取三个字符串值,分别赋值给变量VAR1、VAR2和VAR3
思考:以下两种写法的操作一样吗?
写法1:
read VAR1 VAR2 VAR3
写法2:
read VAR1
read VAR2
read VAR3
示例1:用户输入一个目录,然后显示输入的目录内的档
#!/bin/bash
echo "please input a directory"
read VAR1
ls -l $VAR1
示例2:用户输入年、月、日,然后按指定格式输出。注意输入的数据要以空格或tab分隔,不要用回车
#!/bin/bash
echo "please input time with format: yyyy mm dd:"
read VAR1 VAR2 VAR3
echo "today is ${VAR1}/${VAR2}/${VAR3}"
2)算数运算命令expr
命令expr用于变量间简单的整数四则运算,包括加、减、乘、除、取余。注意如果使用乘法需要使用\*的写法来取消*(通配符)的元字符含义。
例如,在终端输入:
expr 12 + 5 \* 3 #注意乘号的写法
结果为27
示例:从键盘读入两个数字,分别计算这两个数字的加、减、乘、除、取余的结果并输出
#!/bin/bash
echo "please input 2 numbers:"
read VAR1
read VAR2
ADD=`expr $VAR1 + $VAR2` #反引号`的意思是使用后面命令的结果
SUB=`expr $VAR1 - $VAR2`
MUL=`expr $VAR1 \* $VAR2`
DIV=`expr $VAR1 / $VAR2`
MOD=`expr $VAR1 % $VAR2`
echo $VAR1 + $VAR2 = $ADD
echo $VAR1 - $VAR2 = $SUB
echo $VAR1 \* $VAR2 = $MUL #注意乘号写法,表示该符号是字符而不是通配符*
echo $VAR1 / $VAR2 = $DIV
echo $VAR1 % $VAR2 = $MOD
在示例脚本中,反引号`(位于键盘左上角)的作用是引用该命令的结果。ADD=`expr $VAR1 + $VAR2`的意思是首先执行expr命令得到计算结果,然后将该结果作为值赋值给变量ADD。
3)测试命令test
test命令可以测试三种对象:字符串、整数、档属性,每种测试对象都有若干测试操作符。
注意:在Shell编程中,0代表真,1代表假。这点和C语言相反。
1.测试字符串
用法:test 字
符串1 = 字符串2若两字符串相等则结果为0,若不相等则结果为1。注意两个字符串中间的等号左右分别有空格。若没有空格则会将输入作为一个字符串。
test 字符串1 != 字符串2
若两字符串不相等则结果为0,若相等则结果为1。注意两个字符串中间的不等号左右分别有空格。若没有空格则会将输入作为一个字符串。
test -z 字符串
测试该字符串长度是否为0,若为0则结果为0,若不为0则结果为1。
test -n 字符串
测试该字符串长度是否不为0,若不为0则结果为0,若为0则结果为1。(通常不需要-n,它代表默认操作,可以用-z选项来实现)
例如:
test "answer" = "yes"
echo $? #$?表示上一个命令的退出状态
test命令还有一种写法:使用方括号。例如:
test "answer" = "yes" 等价于 [ "answer" = "yes" ]。
test -z "hello" 等价于 [ -z "hello" ]。
注意左右方括号的两边都有空格。
示例:从键盘读入两个字符串,判断这两个字符串是否相同。并测试字符串1是否是空
#写法1
#!/bin/bash
echo "please input 2 strings:"
read VAR1
read VAR2
test "$VAR1" = "$VAR2"
echo $?
test -z "$VAR1"
echo $?
#写法2
#!/bin/bash
echo "please input 2 strings:"
read VAR1
read VAR2
[ "$VAR1" = "$VAR2" ]
echo $?
[ -z "$VAR1" ]
echo $?
2.测试整数关系
a -eq b 测试a与b是否相等
a -ne b 测试a与b是否不相等
a -gt b 测试a是否大于b
a -ge b 测试a是否大于等于b
a -lt b 测试a是否小于b
a -le b 测试a是否小于等于b
示例:从键盘读入两个数,并判断两个数字是否相等
#!/bin/bash
read X Y
if test $X -eq $Y
then
echo "X equal Y"
else
echo "X not equal Y"
fi
3.测试档属性
档测试操作表达式通常是为了测试档信息,一般由脚本来决定是否应该备份、复制、删除。test关于档测试的操作符很多,这里只做简单的介绍。
- -e name 测试一个档是否存在
- -d name 测试name是否为一个目录
- -f name 测试name是否为普通档
- -L/h name 测试name是否为符号链接
- -r name 测试name档是否存在且可读
- -w name 测试name档是否存在且可写
- -x name 测试name档是否存在且可执行
- -s name 测试档是否存在且档长度不为0
- f1 -nt f2 测试档f1是否比档f2更新
- f1 -ot f2 测试档f1是否比档f2更旧
例如:、
test -e /home/linux/myshell.sh(判别在目录/home/linux下是否存在myshell.sh文件)
echo $? //如果有值为零,无则为一
3、结构性语句
结构性语句主要根据程序的运行状态、输入数据、变量取值、控制信号、运行时间等因素来控制程序的流程。主要包括:条件测试语句(二路分支)、多路分支语句、循环语句、控制循环语句和后台执行语句。
1)条件测试语句
1.if……then……fi语句
类似C语言的if语句,格式为:
if 表达式
then 命令
fi
如果表达式为真,则执行命令表中的命令,否则退出if语句。fi表示if语句的结束,类似C语言的右括号}。fi和if必须成对出现。
2.if……then……else……fi语句
类似C语言的if-else语句,格式为:
if 表达式
then 命令1
else
命令2
fi
如果表达式为真,则执行命令表中的命令,否则执行else下命令。
3.if……then……elif……fi语句
类似C语言的if-else语句多重并列使用,格式为:
if 表达式1
then 命令1
elif 表达式2
then 命令2
……
else
命令n
fi
如果表达式1为真,则执行命令表中的命令1,否则判断表达式2;若表达式2为真则执行命令2……若所有表达式都不为真,则执行else下命令。
示例:从键盘读入一个档判断该档是否存在,并判断该档类型(普通档/目录/未知档)
#!/bin/bash
echo "please input a filename"
read FILE
if [ ! -e $FILE ] # 对test的结果取“非”
then
echo "file not exist"
elif [ -L $FILE ]
then
echo "file is a symboliclink"
elif [ -d $FILE ]
then
echo "file is a directory"
elif [ -f $FILE ]
then
echo "file is a regular file"
else
echo "Unknown"
fi
练习:将示例程序改成使用命令行传参的形式。注意考虑命令行参数未传参的情况。
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No arguments"
exit #直接退出脚本
fi
if [ ! -e $1 ]
then
echo "file not exist"
elif [ -L $1 ]
then
echo "file is a symbollink"
elif [ -d $1 ]
then
echo "file is a directory"
elif [ -f $1 ]
then
echo "file is a regular file"
else
echo "Unknown"
fi
2)多路分支语句
多路分支语句case可以用于实现多路分支,类似C语言内switch()语句下的case语句。格式为:
case 字符串变量 in #case语句只能检测字符串变量
模式1)
命令表1
;; #退出case语句用双分号
模式2|模式3) #若多个模式共享则使用|分隔
命令表2
;;
模式4)
命令表3
;;
……
模式n) #模式n常用通配符*表示所有其他模式
命令表n
;; #最后一个模式的双分号可以省略
esac
示例:从命令行传参file1或file2或file3,并输出传参结果
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No arguments"
exit
fi
case $1 in
file1)
echo "You choose file1"
;;
file2)
echo "You choose file2"
;;
file3|file4)
echo "You choose file3/4"
;;
*)
echo "You MUST choose a file"
;;
esac
3)循环语句
循环语句有两类:当循环次数已经确定时,则使用for循环语句;当循环次数未定的时候,则使用while循环语句。循环语句的语句括号用do和done来确定。
1.for循环语句
for循环语句一般用于循环次数确定的时候。格式为:
for 变量名 in 单词表
do
命令表
done
变量依次取出单词表内的所有数值,每取出一个数值,就执行一次循环,因此for循环语句的循环次数由单词表内数值个数决定
示例:从命令行传参一个目录名,然后将该目录内所有档复制到~/backup目录下
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No arguments"
exit
fi
if [ ! -d $1 ] #如果传参命令行不是目录
then
echo "$1 not a directory"
exit
fi
if [ ! -d $HOME/backup ] #如果backup目录不存在则需要创建
then
mkdir $HOME/backup
fi
if [ `ls $HOME/backup | wc -l` -ne 0 ] #如果backup目录非空则需要清空
then
echo "$HOME/backup is not empty, need clean……"
rm -rf $HOME/backup
mkdir $HOME/backup
echo "clean all files in $HOME/backup"
fi
FLIST=`ls $1` #获取参数
for FILE in $FLIST #让FILE变量从单词表中取数据
do
cp $1/$FILE $HOME/backup
echo "$FILE copied"
done
echo "Backup Completed"
2.while循环语句
若无法事先确定循环次数,则我们不推荐使用for循环语句因为无法事先设置好单词表。此时我们可以使用while循环语句。while循环语句适用于无法事先预估循环次数的情况。while语句的格式为:
while 命令或表达式
do
命令表
done
while语句首先会执行后面的命令或者判断表达式的值,如果为真则执行循环,然后再次判断;若为假则退出循环。
示例:批量生成名字为"文件名+数字"的空白档,其中文件名和数字都从命令行传参。例如输入
./proj.sh nihao 5
则会生成nihao1、nihao2、nihao3、nihao4、nihao5
#!/bin/bash
if [ $# -eq 0 ]
then
echo "No arguments"
exit
elif [ $# -lt 2 ]
then
echo "Arguments are too few"
exit
fi
echo "files will store in directory: $HOME/blankfile" #提示用户生成的档在~/blankfile内
if [ ! -d $HOME/blankfile ] #如果blankfile目录不存在则需要创建
then
mkdir $HOME/blankfile
fi
if [ `ls $HOME/blankfile | wc -l` -ne 0 ] #如果blankfile目录非空则需要清空
then
rm -rf $HOME/blankfile
mkdir $HOME/blankfile
fi
LOOP=$2 #循环次数
i=1 #控制循环状态变量
while [ $i -le $LOOP ]
do
touch $HOME/blankfile/$1$i
i=`expr $i + 1` #等价于i++
done
echo "CreateBlankFiles Completed"
4)循环控制语句
Shell脚本中可以使用break语句和continue语句来控制循环停止。continue语句的作用是结束本次循环进入下次循环。而break语句的作用则是跳出循环。
示例1:终端会输出10个数,在数字5处使用continue,则不会输出数字5
#!/bin/bash
for i in 1 2 3 4 5 6 7 8 9 10
do
if [ $i -eq 5 ]
then
continue
fi
echo "$i"
done
示例2:将示例1的脚本修改,将continue修改为break,则输出到数字4之后循环结束
#!/bin/bash
for i in 1 2 3 4 5 6 7 8 9 10
do
if [ $i -eq 5 ]
then
break
fi
echo "$i"
done
练习1:编写一个脚本,输出9*9乘法表
#!/bin/bash
for i in 1 2 3 4 5 6 7 8 9
do
for j in 1 2 3 4 5 6 7 8 9
do
MUL=`expr $i \* $j` #注意乘号写法
echo -e "$j*$i=$MUL\t\c"
#echo -e:开启转义字符
#t:制表符 c:不换行
#"echo -e"和'\c'连用可以关闭换行
if [ $i -eq $j ] #j>i的部分无需计算
then
break
fi
done
echo "" #换行
done
练习2(选做):编写一个脚本,测试当前子网内有多少主机可以连通(使用ping命令)
提示:IP地址范围为192.168.0.1~192.168.0.254 或 192.168.1.1~192.168.1.254(根据当前子网确定范围)
答案:
#!/bin/bash
i=1
COUNT=0
while [ $i -le 254 ]
do
echo "---------------------------"
echo "will ping host:$i"
ping -c 1 192.168.0.$i
if [ $? -eq 0 ]
then
echo "host$i can be connected"
COUNT=`expr $COUNT + 1`
else
echo "host$i cannot be connected"
fi
i=`expr $i + 1`
done
echo "---------------------------"
echo "There are $COUNT host can be connected"
四、Shell编程函数
Shell脚本支持自定义函数功能。我们可以将固定功能、需要多次调用的一组命令封装在一个函数内,这样我们需要使用该功能的时候只需调用该函数即可。
函数在调用前必须先定义,且在顺序上必须放在调用函数前面。
调用函数时可以使用参数传递,函数内使用return命令将运行结果返回给调用程序。
1、函数定义
函数定义的格式为:
格式1:
function_name(){
命令
……
}
格式2:
function function_name(){
命令
……
}
2、函数调用
//实际上,在Shell编程中,函数被看成“许多Shell命令整合成一个大的Shell命令”,因此调用函数与执行一个Shell命令无本质区别
1)无参数无返回值
若调用的函数无需返回值也无需传参,则像普通命令一样即可
示例:写一个打印hello world的子函数并调用
#!/bin/bash
hello(){
echo "hello world"
}
hello #调用函数
2)有参数有返回值
若函数需要传递参数,也需要得到返回值,则有两种调用方式:
方式1:
value=`function_name $arg1 $arg2……`
方式2:
function_name $arg1 $arg2……
echo $?
示例:编写一个函数add,计算两个数的和,两个数使用传参的形式获得
#!/bin/bash
add(){
a=$1
b=$2
z=`expr $a + $b`
echo "the num is $z"
}
add $1 $2