4.2 编写Shell脚本

可以将Shell终端解释器当作人与计算机硬件之间的“翻译官”,它作为用户与Linux系统内部的通信媒介,除了能够支持各种变量与参数外,还提供了诸如循环、分支等高级编程语言才有的控制结构特性。要想正确使用Shell中的这些功能特性,准确下达命令尤为重要。Shell脚本命令的工作方式有两种:交互式和批处理。

交互式(Interactive):用户每输入一条命令就立即执行。

批处理(Batch):由用户事先编写好一个完整的Shell脚本,Shell会一次性执行脚本中诸多的命令。

在Shell脚本中不仅会用到前面学习过的很多Linux命令以及正则表达式、管道符、数据流重定向等语法规则,还需要把内部功能模块化后通过逻辑语句进行处理,最终形成日常所见的Shell脚本。

查看SHELL变量可以发现当前系统已经默认使用Bash作为命令行终端解释器了:

  1. [root@linuxprobe ~]# echo $SHELL
  2. /bin/bash

4.2.1 编写简单的脚本

估计读者在看完上文中有关Shell脚本的复杂描述后,会累觉不爱吧。但是,上文指的是一个高级Shell脚本的编写原则,其实使用Vim编辑器把Linux命令按照顺序依次写入到一个文件中,这就是一个简单的脚本了。

例如,如果想查看当前所在工作路径并列出当前目录下所有的文件及属性信息,实现这个功能的脚本应该类似于下面这样:

  1. [root@linuxprobe ~]# vim example.sh
  2. #!/bin/bash
  3. #For Example BY linuxprobe.com
  4. pwd
  5. ls -al

Shell脚本文件的名称可以任意,但为了避免被误以为是普通文件,建议将.sh后缀加上,以表示是一个脚本文件。在上面的这个example.sh脚本中实际上出现了三种不同的元素:第一行的脚本声明(#!)用来告诉系统使用哪种Shell解释器来执行该脚本;第二行的注释信息(#)是对脚本功能和某些命令的介绍信息,使得自己或他人在日后看到这个脚本内容时,可以快速知道该脚本的作用或一些警告信息;第三、四行的可执行语句也就是我们平时执行的Linux命令了。什么?!你们不相信这么简单就编写出来了一个脚本程序,那我们来执行一下看看结果:

  1. [root@linuxprobe ~]# bash example.sh
  2. /root/Desktop
  3. total 8
  4. drwxr-xr-x. 2 root root 23 Jul 23 17:31 .
  5. dr-xr-x---. 14 root root 4096 Jul 23 17:31 ..
  6. -rwxr--r--. 1 root root 55 Jul 23 17:31 example.sh

除了上面用bash解释器命令直接运行Shell脚本文件外,第二种运行脚本程序的方法是通过输入完整路径的方式来执行。但默认会因为权限不足而提示报错信息,此时只需要为脚本文件增加执行权限即可(详见第5章)。初次学习Linux系统的读者不用心急,等下一章学完用户身份和权限后再来做这个实验也不迟:

  1. [root@linuxprobe ~]# ./example.sh
  2. bash: ./Example.sh: Permission denied
  3. [root@linuxprobe ~]# chmod u+x example.sh
  4. [root@linuxprobe ~]# ./example.sh
  5. /root/Desktop
  6. total 8
  7. drwxr-xr-x. 2 root root 23 Jul 23 17:31 .
  8. dr-xr-x---. 14 root root 4096 Jul 23 17:31 ..
  9. -rwxr--r--. 1 root root 55 Jul 23 17:31 example.sh

4.2.2 接收用户的参数

但是,像上面这样的脚本程序只能执行一些预先定义好的功能,未免太过死板了。为了让Shell脚本程序更好地满足用户的一些实时需求,以便灵活完成工作,必须要让脚本程序能够像之前执行命令时那样,接收用户输入的参数。

其实,Linux系统中的Shell脚本语言早就考虑到了这些,已经内设了用于接收参数的变量,变量之间可以使用空格间隔。例如$0对应的是当前Shell脚本程序的名称,$#对应的是总共有几个参数,$*对应的是所有位置的参数值,$?对应的是显示上一次命令的执行返回值,而$1、$2、$3……则分别对应着第N个位置的参数值,如图4-15所示。

4.2 编写Shell脚本 - 图1

图4-15 Shell脚本程序中的参数位置变量

理论过后我们来练习一下。尝试编写一个脚本程序示例,通过引用上面的变量参数来看下真实效果:

  1. [root@linuxprobe ~]# vim example.sh
  2. #!/bin/bash
  3. echo "当前脚本名称为$0"
  4. echo "总共有$#个参数,分别是$*。"
  5. echo "第1个参数为$1,第5个为$5。"
  6. [root@linuxprobe ~]# sh example.sh one two three four five six
  7. 当前脚本名称为example.sh
  8. 总共有6个参数,分别是one two three four five six
  9. 1个参数为one,第5个为five

4.2.3 判断用户的参数

学习是一个登堂入室、由浅入深的过程。在学习完Linux命令、掌握Shell脚本语法变量和接收用户输入的信息之后,就要踏上新的高度—能够进一步处理接收到的用户参数。

在本书前面章节中讲到,系统在执行mkdir命令时会判断用户输入的信息,即判断用户指定的文件夹名称是否已经存在,如果存在则提示报错;反之则自动创建。Shell脚本中的条件测试语法可以判断表达式是否成立,若条件成立则返回数字0,否则便返回其他随机数值。条件测试语法的执行格式如图4-16所示。切记,条件表达式两边均应有一个空格。

测试语句格式

图4-16 条件测试语句的执行格式


按照测试对象来划分,条件测试语句可以分为4种:

文件测试语句;

逻辑测试语句;

整数值比较语句;

字符串比较语句。

文件测试即使用指定条件来判断文件是否存在或权限是否满足等情况的运算符,具体的参数如表4-3所示。

表4-3 文件测试所用的参数

操作符作用
-d测试文件是否为目录类型
-e测试文件是否存在
-f判断是否为一般文件
-r测试当前用户是否有权限读取
-w测试当前用户是否有权限写入
-x测试当前用户是否有权限执行

下面使用文件测试语句来判断/etc/fstab是否为一个目录类型的文件,然后通过Shell解释器的内设$?变量显示上一条命令执行后的返回值。如果返回值为0,则目录存在;如果返回值为非零的值,则意味着目录不存在:

  1. [root@linuxprobe ~]# [ -d /etc/fstab ]
  2. [root@linuxprobe ~]# echo $?
  3. 1

再使用文件测试语句来判断/etc/fstab是否为一般文件,如果返回值为0,则代表文件存在,且为一般文件:

  1. [root@linuxprobe ~]# [ -f /etc/fstab ]
  2. [root@linuxprobe ~]# echo $?
  3. 0

逻辑语句用于对测试结果进行逻辑分析,根据测试结果可实现不同的效果。例如在Shell终端中逻辑“与”的运算符号是&&,它表示当前面的命令执行成功后才会执行它后面的命令,因此可以用来判断/dev/cdrom文件是否存在,若存在则输出Exist字样。

  1. [root@linuxprobe ~]# [ -e /dev/cdrom ] && echo "Exist"
  2. Exist

除了逻辑“与”外,还有逻辑“或”,它在Linux系统中的运算符号为||,表示当前面的命令执行失败后才会执行它后面的命令,因此可以用来结合系统环境变量USER来判断当前登录的用户是否为非管理员身份:

  1. [root@linuxprobe ~]# echo $USER
  2. root
  3. [root@linuxprobe ~]# [ $USER = root ] || echo "user"
  4. [root@linuxprobe ~]# su - linuxprobe
  5. [linuxprobe@linuxprobe ~]$ [ $USER = root ] || echo "user"
  6. user

第三种逻辑语句是“非”,在Linux系统中的运算符号是一个叹号(!),它表示把条件测试中的判断结果取相反值。也就是说,如果原本测试的结果是正确的,则将其变成错误的;原本测试错误的结果则将其变成正确的。

我们现在切换回到root管理员身份,再判断当前用户是否为一个非管理员的用户。由于判断结果因为两次否定而变成正确,因此会正常地输出预设信息:

  1. [linuxprobe@linuxprobe ~]$ exit
  2. logout
  3. [root@linuxprobe root]# [ $USER != root ] || echo "administrator"
  4. administrator

就技术图书的写作来讲,一般有两种套路:让读者真正搞懂技术了;让读者觉得自己搞懂技术了。因此市面上很多浅显的图书会让读者在学完之后感觉进步特别快,这基本上是作者有意为之,目的就是让您觉得“图书很有料,自己收获很大”,但是在步入工作岗位后就露出短板吃大亏。所以刘遄老师决定继续提高难度,为读者增加一个综合的示例,一方面作为前述知识的总结,另一方面帮助读者夯实基础,能够在今后工作中更灵活地使用逻辑符号。

当前我们正在登录的即为管理员用户—root。下面这个示例的执行顺序是,先判断当前登录用户的USER变量名称是否等于root,然后用逻辑运算符“非”进行取反操作,效果就变成了判断当前登录的用户是否为非管理员用户了。最后若条件成立则会根据逻辑“与”运算符输出user字样;或条件不满足则会通过逻辑“或”运算符输出root字样,而如果前面的&&不成立才会执行后面的||符号。

  1. [root@linuxprobe ~]# [ $USER != root ] && echo "user" || echo "root"
  2. root

整数比较运算符仅是对数字的操作,不能将数字与字符串、文件等内容一起操作,而且不能想当然地使用日常生活中的等号、大于号、小于号等来判断。因为等号与赋值命令符冲突,大于号和小于号分别与输出重定向命令符和输入重定向命令符冲突。因此一定要使用规范的整数比较运算符来进行操作。可用的整数比较运算符如表4-4所示。

表4-4 可用的整数比较运算符

操作符作用
-eq是否等于
-ne是否不等于
-gt是否大于
-lt是否小于
-le是否等于或小于
-ge是否大于或等于

接下来小试牛刀。我们先测试一下10是否大于10以及10是否等于10(通过输出的返回值内容来判断):

  1. [root@linuxprobe ~]# [ 10 -gt 10 ]
  2. [root@linuxprobe ~]# echo $?
  3. 1
  4. [root@linuxprobe ~]# [ 10 -eq 10 ]
  5. [root@linuxprobe ~]# echo $?
  6. 0

在2.4节曾经讲过free命令,它可以用来获取当前系统正在使用及可用的内存量信息。接下来先使用free -m命令查看内存使用量情况(单位为MB),然后通过grep Mem:命令过滤出剩余内存量的行,再用awk '{print $4}'命令只保留第四列,最后用FreeMem=语句的方式把语句内执行的结果赋值给变量。

这个演示确实有些难度,但看懂后会觉得很有意思,没准在运维工作中也会用得上。

  1. [root@linuxprobe ~]# free -m
  2. total used free shared buffers cached
  3. Mem: 1826 1244 582 9 1 413
  4. -/+ buffers/cache: 830 996
  5. Swap: 2047 0 2047
  6. [root@linuxprobe ~]# free -m | grep Mem:
  7. Mem: 1826 1244 582 9
  8. [root@linuxprobe ~]# free -m | grep Mem: | awk '{print $4}'
  9. 582
  10. [root@linuxprobe ~]# FreeMem=`free -m | grep Mem: | awk '{print $4}'`
  11. [root@linuxprobe ~]# echo $FreeMem
  12. 582

上面用于获取内存可用量的命令以及步骤可能有些“超纲”了,如果不能理解领会也不用担心,接下来才是重点。我们使用整数运算符来判断内存可用量的值是否小于1024,若小于则会提示“Insufficient Memory”(内存不足)的字样:

  1. [root@linuxprobe ~]# [ $FreeMem -lt 1024 ] && echo "Insufficient Memory"
  2. Insufficient Memory

字符串比较语句用于判断测试字符串是否为空值,或两个字符串是否相同。它经常用来判断某个变量是否未被定义(即内容为空值),理解起来也比较简单。字符串比较中常见的运算符如表4-5所示。

表4-5 常见的字符串比较运算符

操作符作用
=比较字符串内容是否相同
!=比较字符串内容是否不同
-z判断字符串内容是否为空

接下来通过判断String变量是否为空值,进而判断是否定义了这个变量:

  1. [root@linuxprobe ~]# [ -z $String ]
  2. [root@linuxprobe ~]# echo $?
  3. 0

再尝试引入逻辑运算符来试一下。当用于保存当前语系的环境变量值LANG不是英语(en.US)时,则会满足逻辑测试条件并输出“Not en.US”(非英语)的字样:

  1. [root@linuxprobe ~]# echo $LANG
  2. en_US.UTF-8
  3. [root@linuxprobe ~]# [ $LANG != "en.US" ] && echo "Not en.US"
  4. Not en.US

出现问题?大胆提问!

因读者们硬件不同或操作错误都可能导致实验配置出错,请耐心再仔细看看操作步骤吧,不要气馁~

Linux技术交流请加A群:560843(),B群:340829(推荐),C群:463590(推荐),点此查看全国群

*本群特色:通过口令验证确保每一个群员都是《Linux就该这么学》的读者,答疑更有针对性,不定期免费领取定制礼品。