- 第13章 学习shell script
- !/bin/bash
- Program:
- This program shows "Hello World!" in your screen.
- History:
- 2005/08/23 VBird First release
- 13.2 简单的shell script 练习
- !/bin/bash
- Program:
- User inputs his first name and last name. Program shows his full name.
- History:
- 2005/08/23 VBird First release
- !/bin/bash
- Program:
- Program creates three files, which named by user's input
- and date command.
- History:
- 2005/08/23 VBird First release
- 1. 让用户输入文件名,并取得 fileuser 这个变量;
- 2. 为了避免用户随意按[Enter],利用变量功能分析文件名是否有设置
- 3. 开始利用 date 命令来取得所需要的文件名了
- 4. 创建文件名。
- !/bin/bash
- Program:
- User inputs 2 integer numbers; program will cross these two numbers.
- History:
- 2005/08/23 VBird First release
- 13.3 善用判断式
- !/bin/bash
- Program:
- User input a filename, program will check the flowing:
- 1.) exist? 2.) file/directory? 3.) file permissions
- History:
- 2005/08/25 VBird First release
- 1. 让用户输入文件名,并且判断用户是否真的有输入字符串
- 2. 判断文件是否存在,若不存在则显示信息并结束脚本
- 3. 开始判断文件类型与属性
- 4. 开始输出信息!
- !/bin/bash
- Program:
- This program shows the user's choice
- History:
- 2005/08/25 VBird First release
- 使用 file 来查询后,系统告知这个文件是个 bash 的可执行 script 。
- !/bin/bash
- Program:
- Program shows the script name, parameters…
- History:
- 2009/02/17 VBird First release
- !/bin/bash
- Program:
- Program shows the effect of shift function.
- History:
- 2009/02/17 VBird First release
- !/bin/bash
- Program:
- This program shows the user's choice
- History:
- 2005/08/25 VBird First release
- 一个条件判断
- 多个条件判断 (if…elif…else) 分多种不同情况执行
- !/bin/bash
- Program:
- This program shows the user's choice
- History:
- 2005/08/25 VBird First release
- !/bin/bash
- Program:
- Check $1 is equal to "hello"
- History:
- 2005/08/28 VBird First release
- 封包格式 本地IP:埠口 远程IP:埠口 是否监听
- !/bin/bash
- Program:
- Using netstat and grep to detect WWW,SSH,FTP and Mail services.
- History:
- 2005/08/28 VBird First release
- 1. 先做一些告知的操作而已~
- 2. 开始进行一些测试的工作,并且也输出一些信息。
- !/bin/bash
- Program:
- You input your demobilization date, I calculate how many days
- before you demobilize.
- History:
- 2005/08/29 VBird First release
- 1. 告知用户程序的用途,并且告知应该如何输入日期格式
- 2. 利用正则表达式测试一下这个输入的内容是否正确
- 3. 开始计算日期
- !/bin/bash
- Program:
- Show "Hello" from $1…. by using case …. esac
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program:
- This script only accepts the flowing parameter: one, two or three.
- History:
- 2005/08/29 VBird First release
- read -p "Input your choice: " choice # 暂时取消,可以替换!
- case $choice in # 暂时取消,可以替换!
- !/bin/bash
- Program:
- Use function to repeat information.
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program:
- Use function to repeat information.
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program:
- Repeat question until user input correct answer.
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program:
- Repeat question until user input correct answer.
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program:
- Use loop to calculate "1+2+3+…+100" result.
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program:
- Using for…loop to print 3 animals
- History:
- 2005/08/29 VBird First release
- !/bin/bash
- Program
- Use id, finger command to check system account's information.
- History
- 2009/02/18 VBird first release
- !/bin/bash
- Program
- Use ping command to check the network's PC state.
- History
- 2009/02/18 VBird first release
- 下面的语句取得 ping 的回传值是正确的还是失败的
- 开始显示结果是正确的启动 (UP) 还是错误的没有连通 (DOWN)
- !/bin/bash
- Program:
- User input dir name, I find the permission of files.
- History:
- 2005/08/29 VBird First release
- 1. 先看看这个目录是否存在啊?
- 2. 开始测试文件。
- !/bin/bash
- Program:
- Try do calculate 1+2+…+${your_input}
- History:
- 2005/08/29 VBird First release
- 若语法没有问题,则不会显示任何信息!
- description: Syslog is the facility by which many daemons use to log \
- messages to various system log files. It is a good idea to always \
- run syslog.
- Provides: $syslog
第13章 学习shell script
如果你想要管理好属于你的主机,那么,别说鸟哥不告诉你,可以自动管理系统的好工具—shell script!这家伙真的是得要好好学习学习的!基本上,shell script 有点像是早期的批处理文件,即是将一些命令汇整起来一次执行,但是 shell script 拥有更强大的功能,那就是它可以进行类似程序(program)的编写,并且不需要经过编译(compile)就能够执行,真的很方便。加上我们可通过 shell script 来简化我们日常的工作管理,而且,整个 Linux环境中,一些服务(services)的启动都是通过 shell script 进行的,如果你对于 script 不了解,发生问题时,可真是会求助无门喔!所以,好好学一学它吧!
13.1 什么是shell script
什么是 shell script(程序化脚本)呢?就字面上的意义,我们将它分为两部分。在“shell”部分,我们在第11章的BASH当中已经提过了,那是一个命令行界面下面让我们与系统沟通的一个工具接口。那么“script”是什么?字面上的意义,script 是“脚本”的意思。整句话是说,shell script是针对shell所写的“脚本”!
其实,shell script 是利用shell 的功能所写的一个“程序”(program),这个程序是使用纯文本文件,将一些 shell 的语法与命令(含外部命令)写在里面,搭配正则表达式、管道命令与数据流重定向等功能,以达到我们所想要的处理目的。
所以,简单地说,shell script 就像是早期 DOS 年代的批处理文件(.bat),最简单的功能就是将许多命令写在一起,让用户很轻易就能够一下子处理复杂的操作(执行一个文件"shell script",就能够一次执行多个命令)。而且 shell script 更提供数组、循环、条件与逻辑判断等重要功能,让用户也可以直接以shell来编写程序,而不必使用类似C程序语言等传统程序编写的语法。
这么说你可以了解了吗?shell script 可以简单被看成是批处理文件,也可以被说成是一个程序语言,且这个程序语言由于都是利用shell与相关工具命令,所以不需要编译即可执行,且拥有不错的排错(debug)工具,所以,它可以帮助系统管理员快速管理好主机。
13.1.1 为什么学习shell script
这是个好问题:“我又为什么一定要学 shell script?我又不是程序员,没有写程序的概念,那我为什么还要学 shell script 呢?不要学可不可以啊?”如果 Linux 对你而言,你只是想要“会用”而已,那么不需要学 shell script 也还无所谓,这部分先跳过去,等到有空的时候,再来好好看看。但是如果你是真的想要并清楚 Linux的来龙去脉,那么 shell script 就不可不知,为什么呢?因为:
自动化管理的重要依据
不用鸟哥说你也知道,管理一台主机真不是件简单的事情,每天要进行的任务就有:查询登录文件、追踪流量、监控用户使用主机状态、主机各项硬设备状态、主机软件更新查询等,更不要说得应付其他用户的突然要求了。而这些工作的进行可以分为自行手动处理和写个简单的程序来帮你每日自动处理分析这两种方式,你觉得哪种方式比较好?当然是让系统自动工作比较好,对吧!这就得需要良好的 shell script 来帮忙了。
追踪与管理系统的重要工作
虽然我们还没有提到服务启动的方法,不过,这里可以先提一下,我们 Linux 系统的服务(services)启动的接口是在/etc/init.d/这个目录下,目录下的所有文件都是script另外,包括启动(booting)过程也都是利用 shell script 来帮忙查找系统的相关设置数据,然后再代入各个服务的设置参数。举例来说,如果我们想要重新启动系统注册表文件,可以使用“/etc/init.d/syslogd restart”,那个 syslogd 文件就是 script。
另外,鸟哥曾经在某一代的Fedora上面发现,启动MySQL这个数据库服务时,确实是可以启动的,但是屏幕上却老是出现“failure”!后来才发现,原来是启动MySQL那个script会主动以“空的密码”去尝试登录 MySQL,但为了安全性,鸟哥修改过 MySQL 的密码,当然就登录失败。后来改了改script,就略去这个问题啦!如此说来,script确实是需要学习的。
简单入侵检测功能
当我们的系统有异常时,大多会将这些异常记录在系统记录器,也就是我们常提到的“系统注册表文件”,那么我们可以在固定的几分钟内主动去分析系统注册表文件,若察觉有问题,就立刻通报管理员,或者是立刻加强防火墙的设置规则,如此一来,你的主机可就能够达到“自我保护”的聪明学习功能。举例来说,我们可以通过 shell script 去分析当该数据包尝试几次还是连接失败之后就应该拒绝该 IP 之类的举动,例如鸟哥写过一个关于抵御攻击软件的 shell script,就是用这个想法去实现的。
连续命令单一化
其实,对于新手而言,script最简单的功能就是整合一些在命令行执行的连续命令,将它写入script当中,而由直接执行script来启动一连串的命令输入!例如:防火墙连续规则(iptables)、启动加载程序的项目(就是在/etc/rc.d/rc.local里头的数据)等都是相似的功能。其实,如果不考虑程序的部分,那么script也可以想成仅是帮我们把一大串的命令集合在一个文件里面,而直接执行该文件就可以执行那一串又臭又长的命令段!就是这么简单。
简易的数据处理
由前一章正则表达式的 awk 程序说明中,你可以发现,awk 可以用来处理简单的数据数据呢!例如薪资的处理等。shell script 的功能更强大,例如鸟哥曾经用 shell script 直接处理数据的比较、文字数据的处理等,编写方便,速度又快(因为在 Linux 中性能较佳),真的是很不错。
跨平台支持与学习历程较短
几乎所有的 UNIX Like 上面都可以跑 shell script,连 Windows 系列也有相关的 script 仿真器可以用,此外,shell script 的语法是相当亲和的,看都看得懂的文字(虽然是英文),而不是机器码,很容易学习。这些都是你可以加以考虑的学习点啊!
上面这些都是你考虑学习 shell script 的特点。此外,shell script 还可以简单的以 vim 来直接编写,实在是很方便的好东西。所以,还是建议你学习一下。
不过,虽然 shell script 号称是程序,但实际上,shell script 处理数据的速度上是不太够的。因为shell script 用的是外部的命令与 bash shell 的一些默认工具,所以,它常常会去调用外部的函数库,因此,命令周期上面当然比不上传统的程序语言。所以,shell script 用在系统管理上面是很好的一项工具,但是用在处理大量数值运算上,就不够好了,因为shell script 的速度较慢,且使用的CPU 资源较多,造成主机资源的分配不良。我们通常利用 shell script 来处理服务器的检测,倒是没有进行大量运算的需求。所以不必担心!
13.1.2 第一个script 的编写与执行
如同前面讲到的,shell script其实就是纯文本文件,我们可以编辑这个文件,然后让这个文件来帮我们一次执行多个命令,或者是利用一些运算与逻辑判断来帮我们达成某些功能。所以,要编辑这个文件的内容时,当然就需要具备有bash命令执行的相关认识。执行命令需要注意的事项在第5章的开始执行命令小节内已经提过,有疑问请自行回去翻阅。在shell script的编写中还需要用到下面的注意事项:
1.命令的执行是从上而下、从左而右地分析与执行;
2.命令的执行就如同第5章内提到的:命令、参数间的多个空白都会被忽略掉;
3.空白行也将被忽略掉,并且[tab]按键所得的空白同样视为空格键;
4.如果读取到一个Enter符号(CR),就尝试开始执行该行(或该串)命令;
5.至于如果一行的内容太多,则可以使用“[Enter]”来扩展至下一行;
6.“#”可作为批注。任何加在#后面的数据将全部被视为批注文字而被忽略。
如此一来,我们在script内所编写的程序就会被一行一行执行。现在我们假设你写的这个程序文件名是/home/dmtsai/shell.sh好了,那如何执行这个文件?很简单,可以有下面几个方法:
直接命令执行:shell.sh文件必须要具备可读与可执行(rx)的权限,然后:
绝对路径:使用/home/dmtsai/shell.sh来执行命令;
相对路径:假设工作目录在/home/dmtsai/,则使用./shell.sh来执行;
变量“PATH”功能:将shell.sh放在PATH指定的目录内,例如:~/bin/。
以bash 进程来执行:通过“bash shell.sh”或“sh shell.sh”来执行
反正重点就是要让那个 shell.sh 内的命令可以被执行的意思。那我为何需要使用“./shell.sh”来执行命令?回去第 11 章内的命令查找顺序查看一下,你就会知道原因了!同时,由于 CentOS 默认用户目录下的~/bin目录会被设置到$PATH内,所以你也可以将shell.sh创建在/home/dmtsai/bin/下面(~/bin目录需要自行设置)。此时,若shell.sh在~/bin内且具有rx的权限,那就直接输入shell.sh即可执行该脚本程序!
那为何“sh shell.sh”也可以执行呢?这是因为/bin/sh 其实就是/bin/bash(连接文件),使用shshell.sh即告诉系统,我想要直接以bash的功能来执行shell.sh这个文件内的相关命令的意思,所以此时你的shell.sh只要有r的权限即可被执行。而我们也可以利用sh的参数,如-n及-x来检查与追踪shell.sh的语法是否正确。
编写第一个script
在武侠世界中,不论是那个门派,要学武功要从扫地做起,那么要学程序呢?肯定是由“显示 Hello World!”开始的!那么鸟哥就先写一个 script 给大家瞧一瞧:
[root@www ~]# mkdir scripts; cd scripts
[root@www scripts]# vi sh01.sh
!/bin/bash
Program:
This program shows "Hello World!" in your screen.
History:
2005/08/23 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "Hello World! \a \n"
exit 0
在本章当中,请将所有编写的script放置到你主文件夹的~/scripts这个目录内,将来比较好管理。上面的写法当中,鸟哥主要将整个程序的编写分成数段,大致是这样:
1.第一行#!/bin/bash声明这个script使用的shell名称
因为我们使用的是bash,所以必须要以“#!/bin/bash”来声明这个文件内的语法使用bash的语法。那么当这个程序被执行时,它就能够加载bash的相关环境配置文件(一般来说就是 non-login shell 的~/.bashrc),并且执行 bash 来使我们下面的命令能够执行。这很重要的(在很多情况中,如果没有设置好这一行,那么该程序很可能会无法执行,因为系统可能无法判断该程序需要使用什么shell来执行)。
2.程序内容的说明
整个script当中,除了第一行的“#!”是用来声明shell的之外,其他的#都是“批注”用途。所以上面的程序当中,第二行以下就是用来说明整个程序的基本数据。一般来说,建议你一定要养成说明该script的内容与功能、版本信息、作者与联络方式、建立日期、历史记录等习惯。这将有助于将来程序的改写与调试呢!
3.主要环境变量的声明
建议务必要将一些重要的环境变量设置好,鸟哥个人认为,PATH与LANG(如果有使用到输出相关的信息时)是当中最重要的!如此一来,则可让我们这个程序在进行时可以直接执行一些外部命令,而不必写绝对路径。
4.主要程序部分
就将主要的程序写好即可!在这个例子当中,就是echo那一行。
5.告知执行结果
是否记得我们在第11章里面要讨论一个命令的执行成功与否,可以使用$?这个变量来查看。那么我们也可以利用exit这个命令来让程序中断,并且回传一个数值给系统。在我们这个例子当中,鸟哥使用exit 0,这代表离开script并且回传一个0给系统,所以我执行完这个script后,若接着执行 echo $?则可得到 0 的值。更聪明的读者应该也知道了,利用这个 exit n(n是数字)的功能,我们还可以自定义错误信息,让这个程序变得更加聪明呢!
接下来通过刚才上面介绍的执行方法来执行看看结果。
[root@www scripts]# sh sh01.sh
Hello World !
你会看到屏幕是这样,而且应该还会听到“咚”的一声,为什么呢?还记得前一章提到的 printf吧?用 echo 接着那些特殊的按键也可以发生同样的事情。不过,echo 必须要加上-e 的参数才行。在你写完这个小script之后,你就可以大声的说:“我也会写程序了!”很简单有趣吧?
另外,你也可以利用“chmod a+x sh01.sh; ./sh01.sh”来执行这个 script。
13.1.3 编写shell script 的良好习惯
一个良好习惯的养成是很重要的。大家在刚开始编写程序的时候,最容易忽略这部分,认为程序写出来就好了,其他的不重要。其实,如果程序的说明能够更清楚,那么对你自己是有很大的帮助的。
举例来说,鸟哥自己为了自己的需求,曾经编写了不少的script来帮我进行主机IP的检测、日志文件分析与管理啊、自动上传下载重要配置文件等,不过,早期就是因为太懒了,管理的主机又太多了,常常同一个程序在不同的主机上面进行更改,到最后,到底哪一个才是最新的都记不起来,而且,重点是我到底是改了哪里?为什么做那样的修改?都忘得一干二净了。
所以,后来鸟哥在写程序的时候,通常会比较仔细地将程序的设计过程记录下来,而且还会记录一些历史记录,如此一来,好多了。至少很容易知道我修改了哪些数据,以及程序修改的理念与逻辑概念等,在维护上面是轻松很多的。
另外,在一些环境的设置上面,毕竟每个人的环境都不相同,为了取得较佳的执行环境,我都会自行先定义好一些一定会被用到的环境变量,例如PATH设置。这样比较好。所以说,建议你一定要养成良好的script编写习惯,在每个script的文件头处记录好:
script的功能;
script的版本信息;
script的作者与联络方式;
script的版权声明方式;
script的History(历史记录);
script内较特殊的命令,使用“绝对路径”的方式来执行;
script执行时需要的环境变量预先声明与设置。
除了记录这些信息之外,在较为特殊的程序代码部分,个人建议务必要加上批注说明,可以帮助你非常多。此外,程序代码的编写最好使用嵌套方式,最好能以[tab]按键的空格缩排,这样你的程序代码会显得非常漂亮与有条理!在查阅与调试上较为轻松愉快。另外,使用编写 script 的工具最好使用vim而不是vi,因为vim会有额外的语法检验机制,能够在第一阶段编写时就发现语法方面的问题。
13.2 简单的shell script 练习
在第一个 shell script 编写完毕之后,相信你应该具有基本的编写能力了。接下来,在开始更深入的程序概念之前,我们先来玩一些简单的小范例好了。下面的范例中,实现结果的方式相当多,建议你先自行编写看看,写完之后再与鸟哥写的内容比较,这样才能加深概念。
13.2.1 简单范例
下面的范例在很多的脚本程序中都会用到,而下面的范例又都很简单,值得参考。
交互式脚本:变量内容由用户决定
很多时候我们需要用户输入一些内容,好让程序可以顺利运行。简单来说,大家应该都有安装过软件的经验,安装的时候,它不是会问你要安装到那个目录去吗?那个让用户输入数据的操作就是让用户输入变量内容。
你应该还记得在第11章bash的时候,我们有学到一个read命令吧?现在,请你以read命令的用途,编写一个 script,它可以让用户输入 first name 与 last name,最后并且在屏幕上显示“Your full name is:”的内容:
[root@www scripts]# vi sh02.sh
!/bin/bash
Program:
User inputs his first name and last name. Program shows his full name.
History:
2005/08/23 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
read -p "Please input your first name: " firstname # 提示用户输入
read -p "Please input your last name: " lastname # 提示用户输入
echo -e "\nYour full name is: $firstname $lastname" # 结果由屏幕输出
将上面这个sh02.sh执行一下,你就能够发现用户自己输入的变量可以让程序所使用,并且将它显示到屏幕上。接下来,如果想要制作一个每次执行都会依据不同的日期而变化结果的脚本呢?
随日期变化:利用日期进行文件的创建
想象一个状况,假设我的服务器内有数据库,数据库每天的数据都不太一样,因此当我备份时,希望将每天的数据都备份成不同的文件名,这样才能够让旧的数据也能够保存下来不被覆盖。要不同的文件名呢!这真困扰啊?难道要我每天去修改script?
不需要啊!考虑每天的“日期”并不相同,所以我可以将文件名取成类似 backup.2009-02-14.data,不就可以每天一个不同的文件名了吗?确实如此。那个 2009-02-14 怎么来的?那就是重点了。接下来出个相关的例子:假设我想要创建三个空的文件(通过touch),文件名最开头由用户输入决定,假设用户输入filename好了,那今天的日期是2009/02/14,我想要以前天、昨天、今天的日期来创建这些文件,即 filename_20090212,filename_20090213,filename_20090214,该如何是好?
[root@www scripts]# vi sh03.sh
!/bin/bash
Program:
Program creates three files, which named by user's input
and date command.
History:
2005/08/23 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
1. 让用户输入文件名,并取得 fileuser 这个变量;
echo -e "I will use 'touch' command to create 3 files." # 纯粹显示信息
read -p "Please input your filename: " fileuser # 提示用户输入
2. 为了避免用户随意按[Enter],利用变量功能分析文件名是否有设置
filename=${fileuser:-"filename"} # 开始判断有否配置文件名
3. 开始利用 date 命令来取得所需要的文件名了
date1=$(date —date='2 days ago' +%Y%m%d) # 前两天的日期
date2=$(date —date='1 days ago' +%Y%m%d) # 前一天的日期
date3=$(date +%Y%m%d) # 今天的日期
file1=${filename}${date1} # 下面三行在配置文件名
file2=${filename}${date2}
file3=${filename}${date3}
4. 创建文件名。
touch "$file1"
touch "$file2"
touch "$file3"
上面的范例鸟哥使用了很多在第11章介绍过的概念:包括命令“$(command)”的取得信息、变量的设置功能、变量的累加以及利用touch命令辅助!如果你开始执行这个sh03.sh之后,你可以进行两次执行:一次直接按[Enter]来查看文件名是什么,一次可以输入一些字符,这样可以判断你的脚本是否设计正确。
数值运算:简单的加减乘除
位应该还记得,我们可以使用 declare 来定义变量的类型吧?当变量定义成为整数后才能够进行加减运算。此外,我们也可以利用“$((计算式))”来进行数值运算的。可惜的是,bash shell里头默认仅支持到整数的数据而已。那我们来看看,如果我们要用户输入两个变量,然后将两个变量的内容相乘,最后输出相乘的结果,那可以怎么做?
[root@www scripts]# vi sh04.sh
!/bin/bash
Program:
User inputs 2 integer numbers; program will cross these two numbers.
History:
2005/08/23 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "You SHOULD input 2 numbers, I will cross them! \n"
read -p "first number: " firstnu
read -p "second number: " secnu
total=$(($firstnu*$secnu))
echo -e "\nThe result of $firstnu x $secnu is ==> $total"
在数值的运算上,我们可以使用“declare -i total=$firstnu*$secnu”,也可以使用上面的方式来进行!基本上,鸟哥比较建议使用这样的方式来进行运算:
var=$ ((运算内容))
不但容易记忆,而且也比较方便,因为两个小括号内可以加上空格符。将来你可以使用这种方式来计算的。至于数值运算上的处理,则有 +, -, *, /, % 等。那个%是取余数了。举例来说,13对3取余数,结果是13=4×3+1,所以余数是1。如下:
[root@www scripts]# echo $ (( 13 % 3 ))
1
13.2.2 script 的执行方式区别(source,shscript,./script)
不同的script执行方式会造成不一样的结果。尤其对bash的环境影响很大。脚本的执行方式除了前面小节谈到的方式之外,还可以利用 source 或小数点(.)来执行。那么这种执行方式有何不同呢?当然是不同的啦!让我们来介绍。
利用直接执行的方式来执行script
当使用前一小节提到的直接命令执行(不论是绝对路径/相对路径还是$PATH 内),或者是利用bash(或sh)来执行脚本时,该script都会使用一个新的bash环境来执行脚本内的命令。也就是说,使用这种执行方式时,其实script是在子进程的bash内执行的。我们在第11章BASH内谈到export的功能时,曾经就父进程/子进程谈过一些概念性的问题,重点在于:当子进程完成后,子进程内的各项变量或操作将会结束而不会传回到父进程中。这是什么意思呢?
我们举刚才提到过的 sh02.sh 这个脚本来说明好了,这个脚本可以让用户自行设置两个变量,分别是firstname与lastname,想一想,如果你直接执行该命令时,该命令帮你设置的firstname会不会生效?看一下下面的执行结果:
[root@www scripts]# echo $firstname $lastname
<==确认了,这两个变量并不存在。
[root@www scripts]# sh sh02.sh
Please input your first name: VBird <==这个名字是鸟哥自己输入的
Please input your last name: Tsai
Your full name is: VBird Tsai <==看吧!在 script 运行中,这两个变量有生效
[root@www scripts]# echo $firstname $lastname
<==事实上,这两个变量在父进程的 bash 中还是不存在的!
上面的结果你应该会觉得很奇怪,怎么我已经利用sh02.sh设置好的变量竟然在bash环境下面无效!怎么回事呢?如果将程序相关性绘制成图的话,我们以图 13-1 来说明。当你使用直接执行的方法来处理时,系统会给予一支新的 bash 让我们来执行 sh02.sh 里面的命令,因此你的firstname,lastname等变量其实是在下图中的子进程bash内执行的。当sh02.sh执行完毕后,子进程bash 内的所有数据便被删除,因此上面的练习中,在父进程下面 echo $firstname 时,就看不到任何东西了!这样可以理解吗?
利用source来执行脚本:在父进程中执行
如果你使用source来执行命令那就不一样了!同样的脚本我们来执行看看:
[root@www scripts]# source sh02.sh
Please input your first name: VBird
Please input your last name: Tsai
Your full name is: VBird Tsai
[root@www scripts]# echo $firstname $lastname
VBird Tsai <==有数据产生喔!
竟然生效了!因为source对script的执行方式可以使用下面的图13-2来说明!sh02.sh会在父进程中执行的,因此各项操作都会在原本的 bash 内生效!这也是为啥你不注销系统而要让某些写入~/.bashrc的设置生效时,需要使用“source ~/.bashrc”而不能使用“bash~/.bashrc”。
图13-1 sh02.sh 在子进程中运行
图13-2 sh02.sh 在父进程中运行
13.3 善用判断式
在第11章中,我们提到过$?这个变量所代表的意义,此外,也通过&&及||来作为前一个命令执行回传值对于后一个命令是否要进行的依据。在第 11 章的讨论中,如果想要判断一个目录是否存在,当时我们使用的是ls这个命令搭配数据流重定向,最后配合$?来决定后续的命令进行与否。但是否有更简单的方式可以来进行“条件判断”呢?有的,那就是“test”这个命令。
13.3.1 利用test 命令的测试功能
当我要检测系统上面某些文件或者是相关的属性时,利用test这个命令来工作真是好用得不得了,举例来说,我要检查/dmtsai是否存在时,使用:
[root@www ~]# test -e /dmtsai
执行结果并不会显示任何信息,但最后我们可以通过$?或&&及||来显示整个结果呢!例如我们将上面的例子改写成这样:
[root@www ~]# test -e /dmtsai && echo "exist" || echo "Not exist"
Not exist <==结果显示不存在。
最终的结果可以告知我们是“exist”还是“Not exist”呢?那我知道-e 是测试一个“东西”在不在,如果还想要测试一下该文件名是什么时,还有哪些标志可以来判断的呢?有下面这些,如表13-1所示。
表13-1
罉裄
现在我们就利用test来帮我们写几个简单的例子。首先,判断一下,让用户输入一个文件名,我们判断:
1.这个文件是否存在,若不存在则给予一个“Filename does not exist”的信息,并中断程序; 2.若这个文件存在,则判断它是个文件或目录,结果输出“Filename is regular file”或“Filename is directory”;
3.判断一下,执行者的身份对这个文件或目录所拥有的权限,并输出权限数据。
你可以先自行创作看看,然后再跟下面的结果比较。注意利用test与&&还有||等标志。
[root@www scripts]# vi sh05.sh
!/bin/bash
Program:
User input a filename, program will check the flowing:
1.) exist? 2.) file/directory? 3.) file permissions
History:
2005/08/25 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
1. 让用户输入文件名,并且判断用户是否真的有输入字符串
echo -e "Please input a filename, I will check the filename's type and \
permission. \n\n"
read -p "Input a filename : " filename
test -z $filename && echo "You MUST input a filename." && exit 0
2. 判断文件是否存在,若不存在则显示信息并结束脚本
test ! -e $filename && echo "The filename '$filename' DO NOT exist" && exit 0
3. 开始判断文件类型与属性
test -f $filename && filetype="regulare file"
test -d $filename && filetype="directory"
test -r $filename && perm="readable"
test -w $filename && perm="$perm writable"
test -x $filename && perm="$perm executable"
4. 开始输出信息!
echo "The filename: $filename is a $filetype"
echo "And the permissions are : $perm"
如果你执行这个脚本后,它会依据你输入的文件名来进行检查,先看是否存在,再看是文件或目录类型,最后判断权限。但是你必须要注意的是,由于root在很多权限的限制上面都是无效的,所以使用root 执行这个脚本时,常常会发现与ls -l 观察到的结果并不相同。所以,建议使用一般用户来执行这个脚本试看看。不过你必须要使用root的身份先将这个脚本转移给用户就是了,不然一般用户无法进入/root目录的。很有趣的例子吧!你可以自行再以其他的案例来编写一下可用的功能!
13.3.2 利用判断符号[]
除了我们很喜欢使用的test之外,其实,我们还可以利用判断符号“[]”(就是中括号啦)来进行数据的判断呢!举例来说,如果我想要知道$HOME这个变量是否为空的,可以这样做:
[root@www ~]# [ -z "$HOME" ] ; echo $?
使用中括号必须要特别注意,因为中括号用在很多地方,包括通配符与正则表达式等,所以如果要在bash的语法当中使用中括号作为shell的判断式时,必须要注意中括号的两端需要有空格符来分隔。假设我空格键使用“□”符号来表示,那么,在这些地方你都需要有空格键:
[□"$HOME"□==□"$MAIL"□]
↑ ↑ ↑ ↑
您会发现鸟哥在上面的判断式当中使用了两个等号“ == ”。其实在 bash 当中使用一个等号与两个等号的结果是一样的。不过在一般惯用程序的写法中,一个等号代表“变量的设置”,两个等号则是代表“逻辑判断 (是否之意)”。由于我们在中括号内重点在于“判断”而非“设置变量”,因此鸟哥建议您还是使用两个等号较佳!
上面的例子在于说明,两个字符串$HOME与$MAIL是否相同的意思,相当于test$HOME=$MAIL的意思。而如果没有空白分隔,例如[$HOME==$MAIL]时,我们的bash就会显示错误信息了。所以说,你最好要注意:
在中括号[]内的每个组件都需要有空格键来分隔;
在中括号内的变量,最好都以双引号括号起来;
在中括号内的常量,最好都以单或双引号括号起来。
为什么要这么麻烦啊?直接举例来说,假如我设置了 name="VBird Tsai",然后这样判定:
[root@www ~]# name="VBird Tsai"
[root@www ~]# [ $name == "VBird" ]
bash: [: too many arguments
怎么会发生错误啊?bash 还跟我说错误是由于“太多参数(arguments)”所致!为什么呢?因为$name如果没有使用双引号刮起来,那么上面的判定式会变成:
[ VBird Tsai == "VBird" ]
上面肯定不对嘛!因为一个判断式仅能有两个数据的比较,上面VBird与Tsai还有"VBird"就有三个数据。这不是我们要的。我们要的应该是下面这个样子:
[ "VBird Tsai" == "VBird" ]
这可是差很多的。另外,中括号的使用方法与 test 几乎一模一样。只是中括号比较常用在条件判断式if…then…fi的情况中就是了。好,那我们也使用中括号的判断来做一个小案例好了,案例设置如下:
1.当执行一个程序的时候,这个程序会让用户选择Y或N;
2.如果用户输入 Y 或 y 时,就显示“OK, continue ”;
3.如果用户输入 n 或 N 时,就显示“Oh, interrupt!”;
4.如果不是 Y/y/N/n 之内的其他字符,就显示“I don't know what your choice is”。
利用中括号、&&与||来继续吧!
[root@www scripts]# vi sh06.sh
!/bin/bash
Program:
This program shows the user's choice
History:
2005/08/25 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
read -p "Please input (Y/N): " yn
[ "$yn" == "Y" -o "$yn" == "y" ] && echo "OK, continue" && exit 0
[ "$yn" == "N" -o "$yn" == "n" ] && echo "Oh, interrupt!" && exit 0
echo "I don't know what your choice is" && exit 0
由于输入正确(Yes)的方法有大小写之分,不论输入大写Y或小写y都是可以的,此时判断式内就得要有两个判断才行!由于是任何一个成立即可(大小或小写的y),所以这里使用-o(或)连接两个判断。很有趣吧!利用这个字符串判别的方法,我们就可以很轻松地将用户想要进行的工作分类呢!接下来,我们再来谈一些其他有的没有的命令吧!
13.3.3 shell script 的默认变量($0, $1…)
我们知道命令可以带有参数,例如 ls -la 可以查看包含隐藏文件的所有属性与权限。那么 shellscript 能不能在脚本文件名后面带有参数呢?很有趣。举例来说,如果你想要重新启动系统注册表文件的功能,可以这样做:
[root@www ~]# file /etc/init.d/syslog
/etc/init.d/syslog: Bourne-Again shell script text executable
使用 file 来查询后,系统告知这个文件是个 bash 的可执行 script 。
[root@www ~]# /etc/init.d/syslog restart
restart 是重新启动的意思,上面的命令可以重新启动/etc/init.d/syslog 这个程序。那么如果你在/etc/init.d/syslog后面加上stop呢?没错!就可以直接关闭该服务了!如果你要依据程序的执行给予一些变量去进行不同的任务时,本章一开始是使用read的功能。但read功能的问题是你得要手动由键盘输入一些判断式。如果通过命令后面接参数,那么一个命令就能够处理完毕而不需要手动再次输入一些变量行为!这样执行命令会比较简单方便。
script是怎么实现这个功能的呢?其实script针对参数已经有设置好一些变量名称了!对应如下:
/path/to/scriptname opt1 opt2 opt3 opt4
$0 $1 $2 $3 $4
这样够清楚了吧?执行的脚本文件名为$0这个变量,第一个接的参数就是$1。所以,只要我们在script里面善用$1的话,就可以很简单地立即执行某些命令功能了!除了这些数字的变量之外,我们还有一些较为特殊的变量可以在script内使用来调用这些参数。
$#:代表后接的参数“个数”;
$@:代表"$1"、"$2"、"$3"、"$4"之意,每个变量是独立的(用双引号括起来);
$*:代表“"$1c$2c$3c$4" ”,其中c 为分隔字符,默认为空格键,所以本例中代表“"$1 $2 $3 $4"”之意。
那个$@与$*基本上还是有所不同。不过,一般使用情况下可以直接记忆$@即可!好了,来做个练习吧!假设我要执行一个可以携带参数的script,执行该脚本后屏幕会显示如下的数据:
程序的文件名;
共有几个参数;
若参数的个数小于 2 则告知用户参数数量太少;
全部的参数内容;
第一个参数;
第二个参数。
[root@www scripts]# vi sh07.sh
!/bin/bash
Program:
Program shows the script name, parameters…
History:
2009/02/17 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo "The script name is ==> $0"
echo "Total parameter number is ==> $#"
[ "$#" -lt 2 ] && echo "The number of parameter is less than 2. Stop here." \
&& exit 0
echo "Your whole parameter is ==> '$@'"
echo "The 1st parameter ==> $1"
echo "The 2nd parameter ==> $2"
执行结果如下:
[root@www scripts]# sh sh07.sh theone haha quot
The script name is ==> sh07.sh <==文件名
Total parameter number is ==> 3 <==果然有三个参数
Your whole parameter is ==> 'theone haha quot' <==参数的内容全部
The 1st parameter ==> theone <==第一个参数
The 2nd parameter ==> haha <==第二个参数
shift:造成参数变量号码偏移
除此之外,脚本后面所接的变量是否能够进行偏移(shift)呢?什么是偏移啊?我们直接以下面的范例来说明好了,用范例说明比较好解释!我们将sh07.sh的内容稍作变化一下,用来显示每次偏移后参数的变化情况:
[root@www scripts]# vi sh08.sh
!/bin/bash
Program:
Program shows the effect of shift function.
History:
2009/02/17 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo "Total parameter number is ==> $#"
echo "Your whole parameter is ==> '$@'"
shift # 进行第一次“一个变量的 shift ”
echo "Total parameter number is ==> $#"
echo "Your whole parameter is ==> '$@'"
shift 3 # 进行第二次“三个变量的 shift”
echo "Total parameter number is ==> $#"
echo "Your whole parameter is ==> '$@'"
执行成果如下:
[root@www scripts]# sh sh08.sh one two three four five six <==给予六个参数
Total parameter number is ==> 6 <==最原始的参数变量情况
Your whole parameter is ==> 'one two three four five six'
Total parameter number is ==> 5 <==第一次偏移,看下面发现第一个 one 不见了
Your whole parameter is ==> 'two three four five six'
Total parameter number is ==> 2 <==第二次偏移掉三个,“two three four”不见了
Your whole parameter is ==> 'five six'
光看结果你就可以知道啦,那个shift会移动变量,而且shift后面可以接数字,代表拿掉最前面的几个参数的意思。上面的执行结果中,第一次进行 shift 后它的显示情况是“one two three four five six”,所以就剩下五个啦!第二次直接拿掉三个,就变成“ two three four five six”。这样这个案例可以了解了吗?理解了shift的功能了吗?
上面这8个例子都很简单吧?几乎都是利用bash的相关功能而已。下面我们就要使用条件判断式来分别进行一些功能的设置了,好好瞧一瞧先。
13.4 条件判断式
只要讲到“程序”的话,那么条件判断式,即是if…then这种判别式肯定一定要学习的!因为很多时候,我们都必须要依据某些数据来判断程序该如何进行。举例来说,我们在上面的sh06.sh范例中不是有练习当用户输入Y/N时,必须要执行不同的信息输出吗?简单的方式可以利用&&与||,但如果我还想要执行一堆命令呢?那真的得要 if… then 来帮忙。下面我们就来聊一聊!
13.4.1 利用if…then
这个if…then是最常见的条件判断式了。简单地说,就是当符合某个条件判断的时候,就进行某项工作就是了。这个if…then的判断还有多层次的情况!我们分别介绍如下:
单层、简单条件判断式
如果你只有一个判断式要进行,那么我们可以简单地这样看:
if [ 条件判断式 ]; then
当条件判断式成立时,可以进行的命令工作内容;
fi <==将 if 反过来写,就成为 fi 。结束 if 之意!
至于条件判断式的判断方法,与前一小节的介绍相同!较特别的是,如果我有多个条件要判别时,除了sh06.sh那个案例所写的,也就是将多个条件写入一个中括号内的情况之外,我还可以有多个中括号来隔开。而括号与括号之间,则以&&或||来隔开,它们的意义是:
&&代表AND;
||代表or。
所以,在使用中括号的判断式中,&&及||就与命令执行的状态不同了。举例来说,sh06.sh里面的判断式可以这样修改:
["$yn"=="Y"-o"$yn"=="y"]
上式可替换为
["$yn"=="Y"]||["$yn"=="y"]
之所以这样改,很多人是习惯问题。很多人则是喜欢一个中括号仅有一个判别式的原因。好了,现在我们来将sh06.sh这个脚本修改成为if…then的样式来看看:
[root@www scripts]# cp sh06.sh sh06-2.sh <==用修改的比较快!
[root@www scripts]# vi sh06-2.sh
!/bin/bash
Program:
This program shows the user's choice
History:
2005/08/25 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
read -p "Please input (Y/N): " yn
if [ "$yn" == "Y" ] || [ "$yn" == "y" ]; then
echo "OK, continue"
exit 0
fi
if [ "$yn" == "N" ] || [ "$yn" == "n" ]; then
echo "Oh, interrupt!"
exit 0
fi
echo "I don't know what your choice is" && exit 0
不过,由这个例子看起来,似乎也没有什么了不起吧?sh06.sh还比较简单呢。但是如果以逻辑概念来看,其实上面的范例中,我们使用了两个条件判断。明明仅有一个$yn的变量,嗌为何需要进行两次比较呢?此时,多重条件判断就能够来测试 !
多重、复杂条件判断式
在同一个数据的判断中,如果该数据需要进行多种不同的判断时,应该怎么做?举例来说,上面的 sh06.sh 脚本中,我们只要进行一次$yn 的判断就好(仅进行一次 if),不想要做多次if的判断。此时你就得知道下面的语法了:
一个条件判断
if [ 条件判断式 ]; then
当条件判断式成立时,可以进行的命令工作内容;
else
当条件判断式不成立时,可以进行的命令工作内容;
fi
如果考虑更复杂的情况,则可以使用这个语法:
多个条件判断 (if…elif…else) 分多种不同情况执行
if [ 条件判断式一 ]; then
当条件判断式一成立时,可以进行的命令工作内容;
elif [ 条件判断式二 ]; then
当条件判断式二成立时,可以进行的命令工作内容;
else
当条件判断式一与二均不成立时,可以进行的命令工作内容;
fi
你得要注意的是,elif也是个判断式,因此出现elif后面都要接then来处理。但是else已经是最后的没有成立的结果了,所以else后面并没有then。我们来将sh06-2.sh改写成这样:
[root@www scripts]# cp sh06-2.sh sh06-3.sh
[root@www scripts]# vi sh06-3.sh
!/bin/bash
Program:
This program shows the user's choice
History:
2005/08/25 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
read -p "Please input (Y/N): " yn
if [ "$yn" == "Y" ] || [ "$yn" == "y" ]; then
echo "OK, continue"
elif [ "$yn" == "N" ] || [ "$yn" == "n" ]; then
echo "Oh, interrupt!"
else
echo "I don't know what your choice is"
fi
是否程序变得很简单,而且依序判断,可以避免掉重复判断的状况,这样真的很容易设计程序的啦。好了,让我们再来进行另外一个案例的设计。一般来说,如果你不希望用户由键盘输入额外的数据时,可以使用上一节提到的参数功能($1)!让用户在执行命令时就将参数代进去。现在我们想让用户输入“hello”这个关键字时,利用参数的方法可以这样依序设计:
1.判断$1 是否为 hello,如果是的话,就显示"Hello, how are you ?";
2.如果没有加任何参数,就提示用户必须要使用的参数;
3.而如果加入的参数不是hello,就提醒用户仅能使用hello为参数。
整个程序的编写可以是这样的:
[root@www scripts]# vi sh09.sh
!/bin/bash
Program:
Check $1 is equal to "hello"
History:
2005/08/28 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
if [ "$1" == "hello" ]; then
echo "Hello, how are you ?"
elif [ "$1" == "" ]; then
echo "You MUST input parameters, ex> {$0 someword}"
else
echo "The only parameter is 'hello', ex> {$0 hello}"
fi
然后你可以执行这支程序,分别在$1的位置输入hello,没有输入与随意输入,就可以看到不同的输出。是否还觉得挺简单的啊?事实上,学到这里,也真的很厉害了!好了,下面我们继续来玩一些比较大一点的计划。
我们在第11章已经学会了grep这个好用的玩意儿,那么多学一个叫做netstat的命令,这个命令可以查询到目前主机打开的网络服务端口(service ports),相关的功能我们会在服务器架设篇继续介绍,这里你只要知道,我可以利用“netstat -tuln”来取得目前主机有启动的服务,而且取得的信息有点像这样:
[root@www ~]# netstat -tuln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 :::22 :::* LISTEN
udp 0 0 0.0.0.0:111 0.0.0.0:*
udp 0 0 0.0.0.0:631 0.0.0.0:*
封包格式 本地IP:埠口 远程IP:埠口 是否监听
上面的重点是 Local Address(本地主机的 IP 与端口对应)那个字段,它代表的是本机所启动的网络服务。IP的部分说明的是该服务位于那个接口上,若为127.0.0.1则是仅针对本机开放,若是 0.0.0.0 或:::则代表对整个 Internet 开放(更多信息请参考服务器架设篇的介绍)。每个端口(port)都有其特定的网络服务,几个常见的 port 与相关网络服务的关系是:
80:WWW
22: ssh
21: ftp
25: mail
111: RPC(远程过程调用)
631: CUPS(打印服务功能)
假设我的主机有兴趣要检测的是比较常见的 port21,22,25 及 80 时,那我如何通过 netstat去检测我的主机是否有开启这四个主要的网络服务端端口呢?由于每个服务的关键字都是接在冒号“:”后面,所以可以通过选取类似“:80”来检测的!那我就可以简单地这样去写这个程序:
[root@www scripts]# vi sh10.sh
!/bin/bash
Program:
Using netstat and grep to detect WWW,SSH,FTP and Mail services.
History:
2005/08/28 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
1. 先做一些告知的操作而已~
echo "Now, I will detect your Linux server's services!"
echo -e "The www, ftp, ssh, and mail will be detect! \n"
2. 开始进行一些测试的工作,并且也输出一些信息。
testing=$(netstat -tuln | grep ":80 ") #检测 port 80在否
if [ "$testing" != "" ]; then
echo "WWW is running in your system."
fi
testing=$(netstat -tuln | grep ":22 ") #检测 port 22在否
if [ "$testing" != "" ]; then
echo "SSH is running in your system."
fi
testing=$(netstat -tuln | grep ":21 ") #检测 port 21在否
if [ "$testing" != "" ]; then
echo "FTP is running in your system."
fi
testing=$(netstat -tuln | grep ":25 ") #检测port 25在否
if [ "$testing" != "" ]; then
echo "Mail is running in your system."
Fi
实际执行这个程序你就可以看到你的主机有没有启动这些服务。是否很有趣呢?条件判断式还可以搞得更复杂。举例来说,在中国当兵是公民应尽的义务,不过,在当兵的时候总是很想要退伍的。那你能不能写个脚本程序,让用户输入他的退伍日期,让你去帮他计算还有几天才退伍。
由于日期是要用相减的方式来处置,所以我们可以通过使用date显示日期与时间,将它转为由 1970-01-01 累积而来的秒数,通过秒数相减来取得剩余的秒数后,再换算为日数即可。整个脚本的制作流程有点像这样:
1.先让用户输入他们的退伍日期;
2.再由现在日期对比退伍日期;
3.由两个日期的比较来显示“还需要几天”才能够退伍的字样。
似乎挺难的样子?其实也不会啦,利用“date—date="YYYYMMDD"+%s”转成秒数后,接下来的操作就容易得多了。如果你已经写完了程序,对照下面的写法看看:
[root@www scripts]# vi sh11.sh
!/bin/bash
Program:
You input your demobilization date, I calculate how many days
before you demobilize.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
1. 告知用户程序的用途,并且告知应该如何输入日期格式
echo "This program will try to calculate :"
echo "How many days before your demobilization date…"
read -p "Please input your demobilization date (YYYYMMDD ex>20090401): " date2
2. 利用正则表达式测试一下这个输入的内容是否正确
date_d=$(echo $date2 |grep '[0-9]{8}') # 看看是否有八个数字
if [ "$date_d" == "" ]; then
echo "You input the wrong date format…."
exit 1
fi
3. 开始计算日期
declare -i date_dem=date --date="$date2" +%s
# 退伍日期秒数
declare -i date_now=date +%s
# 现在日期秒数
declare -i date_total_s=$(($date_dem-$date_now)) # 剩余秒数统计
declare -i date_d=$(($date_total_s/60/60/24)) # 转为日数
if [ "$date_total_s" -lt "0" ]; then # 判断是否已退伍
echo "You had been demobilization before: " $((-1*$date_d)) " ago"
else
declare -i date_h=$(($(($date_total_s-$date_d6060*24))/60/60))
echo "You will demobilize after $date_d days and $date_h hours."
Fi
这个程序可以帮你计算退伍日期。如果是已经退伍的朋友,还可以知道已经退伍多久了。脚本中的date_d变量声明那个/60/60/24是来自于一天的总秒数(24小时×60分×60秒)。全部的操作都没有超出我们所学的范围吧?还能够避免用户输入错误的数字,所以多了一个正则表达式的判断式呢。这个例子比较难,有兴趣想要一探究竟的朋友,可以做一下课后练习题中关于计算生日的那一题。
13.4.2 利用case…esac 判断
上个小节提到的if…then…fi”对于变量的判断是以比较的方式来分辨的,如果符合状态就进行某些行为,并且通过较多层次(就是elif)的方式来进行多个变量的程序代码编写,譬如sh09.sh那个小程序,就是用这样的方式来编写的。那么万一我有多个既定的变量内容,例如sh09.sh当中,我所需要的变量就是"hello"及空字符串两个,那么我只要针对这两个变量来设置状况就好了,对吧?那么可以使用什么方式来设计呢?就用case…in…esac,它的语法如下:
case $变量名称 in <==关键字为 case ,还有变量前有$
"第一个变量内容") <==每个变量内容建议用双引号括起来,关键字则为小括号)
程序段
;; <==每个类型结尾使用两个连续的分号来处理!
"第二个变量内容")
程序段
;;
) <==最后一个变量内容都会用 来代表所有其他值
不包含第一个变量内容与第二个变量内容的其他程序执行段
exit 1
;;
esac <==最终的 case 结尾!“反过来写”思考一下!
要注意的是,这个语法以case(实际案例之意)为开头,结尾自然就是将case的英文反过来写。就成为esac。不会很难背啦!另外,每一个变量内容的程序段最后都需要两个分号(;;)来代表该程序段落的结束,这挺重要的。至于为何需要有*这个变量内容在最后呢?这是因为,如果用户不是输入第一个或第二个变量内容时,我们可以告知用户相关的信息。废话少说,我们拿sh09.sh的案例来修改一下,它应该会变成这样:
[root@www scripts]# vi sh09-2.sh
!/bin/bash
Program:
Show "Hello" from $1…. by using case …. esac
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
case $1 in
"hello")
echo "Hello, how are you ?"
;;
"")
echo "You MUST input parameters, ex> {$0 someword}"
;;
*) # 其实就相当于通配符,0~无穷多个任意字符之意!
echo "Usage $0 {hello}"
;;
esac
在上面这个 sh09-2.sh 的案例当中,如果你输入“sh sh09-2.sh test”来执行,那么屏幕上就会出现“Usage sh09-2.sh {hello}”的字样,告知执行者仅能够使用 hello。这样的方式对于需要某些固定字符串来执行的变量内容就显得更加方便呢!这种方式你真的要熟悉喔!这是因为系统的很多服务的启动script都是使用这种写法的,举例来说,我们Linux的服务启动放置目录是在/etc/init.d/当中,我已经知道里头有个syslog的服务,我想要重新启动这个服务,可以这样做:
/etc/init.d/syslog restart
重点是那个 restart。如果你使用“less /etc/init.d/syslog”去查阅一下,就会看到它使用的是 case语法,并且会规定某些既定的变量内容,你可以直接执行/etc/init.d/syslog,该script就会告知你有哪些后续接的变量可以使用。方便吧?
一般来说,使用“case $变量 in”这个语法中,当中的那个“$变量”大致有两种取得的方式:
直接执行式:例如上面提到的,利用“script.sh variable”的方式来直接给予$1 这个变量的内容,这也是在/etc/init.d目录下大多数程序的设计方式。
交互式:通过read这个命令来让用户输入变量的内容。
这么说或许你的感受性还不高,好,我们直接写个程序来练习:让用户能够输入 one, two, three ,并且将用户的变量显示到屏幕上,如果不是 one, two, three 时,就告知用户仅有这三种选择。
[root@www scripts]# vi sh12.sh
!/bin/bash
Program:
This script only accepts the flowing parameter: one, two or three.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo "This program will print your selection !"
read -p "Input your choice: " choice # 暂时取消,可以替换!
case $choice in # 暂时取消,可以替换!
case $1 in # 现在使用,可以用上面两行替换!
"one")
echo "Your choice is ONE"
;;
"two")
echo "Your choice is TWO"
;;
"three")
echo "Your choice is THREE"
;;
*)
echo "Usage $0 {one|two|three}"
;;
esac
此时,你可以使用“sh sh12.sh two”的方式来执行命令,就可以收到相对应的响应了。上面使用的是直接执行的方式,而如果使用的是交互式时,那么将上面第10,11行的"#"拿掉,并将12行加上批注(#),就可以让用户输入参数。这样是否很有趣啊?
13.4.3 利用function功能
什么是函数(function)功能啊?简单地说,其实,函数可以在 shell script 当中做出一个类似自定义执行命令的东西,最大的功能是,可以简化我们很多的程序代码。举例来说,上面的sh12.sh当中,每个输入结果 one, two, three 其实输出的内容都一样,那么我就可以使用 function 来简化了!function的语法是这样的:
function fname() {
程序段
}
那个fname就是我们的自定义的执行命令名称,而程序段就是我们要它执行的内容了。要注意的是,因为shell script 的执行方式是由上而下、由左而右,因此在shell script 当中的function 的设置一定要在程序的最前面,这样才能够在执行时被找到可用的程序段。好,我们将sh12.sh改写一下,自定义一个名为printit的函数来使用:
[root@www scripts]# vi sh12-2.sh
!/bin/bash
Program:
Use function to repeat information.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
function printit(){
echo -n "Your choice is " # 加上 -n可以不断行继续在同一行显示
}
echo "This program will print your selection !"
case $1 in
"one")
printit; echo $1 | tr 'a-z' 'A-Z' # 将参数做大小写转换!
;;
"two")
printit; echo $1 | tr 'a-z' 'A-Z'
;;
"three")
printit; echo $1 | tr 'a-z' 'A-Z'
;;
*)
echo "Usage $0 {one|two|three}"
;;
esac
以上面的例子来说,鸟哥做了一个函数名称为 printit,所以,当我在后续的程序段里面,只要执行 printit 的话,就表示我的 shell script 要去执行“function printit…”里面的那几个程序段落。当然,上面这个例子举得太简单了,所以你不会觉得function有什么好厉害的,不过,如果某些程序代码一再地在script当中重复时,这个function 嗌可就重要得多 !它不但可以简化程序代码,而且可以做成类似“模块”的玩意儿,真的很不错啦!
建议读者可以使用类似 vim 的编辑器到 /etc/init.d/ 目录下去查阅一下您所看到的文件,并且自行追踪一下每个文件的执行情况,相信会更有心得!
另外,function 也是拥有内置变量的。它的内置变量与shell script 很类似,函数名称代表示$0,而后续接的变量也是以$1, $2…来替代的。这里很容易搞错,因为“function fname() {程序段 } ”内的$0、$1等与shell script的$0是不同的。以上面sh12-2.sh来说,假如我执行“sh sh12-2.sh one ” 这表示在 shell script 内的$1 为 "one"这个字符串。但是在 printit()内的$1 则与这个 one 无关。我们将上面的例子再次改写一下,让你更清楚!
[root@www scripts]# vi sh12-3.sh
!/bin/bash
Program:
Use function to repeat information.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
function printit(){
echo "Your choice is $1" # 这个 $1 必须要参考下面命令的执行
}
echo "This program will print your selection !"
case $1 in
"one")
printit 1 # 请注意,printit 命令后面还有接参数!
;;
"two")
printit 2
;;
"three")
printit 3
;;
*)
echo "Usage $0 {one|two|three}";;
esac
在上面的例子当中,如果你输入“sh sh12-3.sh one”就会出现“Your choice is 1”的字样。为什么是 1 呢?因为在程序段落当中,我们是写了“printit 1”那个 1 就会成为 function 当中的$1。这样是否理解呢?function 本身其实比较困难一点,如果你还想要进行其他的编写的话得多加学习。不过,我们仅是想要更加了解 shell script 嗌而已,所以,这里看看即可,了解原理就好 !
13.5 循环(loop)
除了if…then…fi这种条件判断式之外,循环可能是程序当中最重要的一环了。循环可以不断地执行某个程序段落,直到用户设置的条件达成为止。所以,重点是那个“条件的完成”是什么。除了这种依据判断式达成与否的不定循环之外,还有另外一种已经固定要跑多少次的循环,可称为固定循环的状态。下面我们就来谈一谈。
13.5.1 while do done, until do done(不定循环)
一般来说,不定循环最常见的就是下面这两种状态了:
while [ condition ] <==中括号内的状态就是判断式
do <==do 是循环的开始!
程序段落
done <==done 是循环的结束
while 的中文是“当⋯⋯时”,所以,这种方式说的是当 condition 条件成立时,就进行循环,直到condition的条件不成立才停止的意思。还有另外一种不定循环的方式:
until [ condition ]
do
程序段落
done
这种方式恰恰与while相反,它说的是当condition条件成立时,就终止循环,否则就持续进行循环的程序段。是否刚好相反啊。我们以while来做个简单的练习好了。假设我要让用户输入yes或者是YES才结束程序的执行,否则就一直进行告知用户输入字符串。
[root@www scripts]# vi sh13.sh
!/bin/bash
Program:
Repeat question until user input correct answer.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
while [ "$yn" != "yes" -a "$yn" != "YES" ]
do
read -p "Please input yes/YES to stop this program: " yn
done
echo "OK! you input the correct answer."
上面这个例题说明的是当$yn这个变量不是"yes"且$yn也不是"YES"时,才进行循环内的程序,而如果$yn是"yes"或"YES"时,就会离开循环。那如果使用until呢?它的条件会变成这样:
[root@www scripts]# vi sh13-2.sh
!/bin/bash
Program:
Repeat question until user input correct answer.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
until [ "$yn" == "yes" -o "$yn" == "YES" ]
do
read -p "Please input yes/YES to stop this program: " yn
done
echo "OK! you input the correct answer."
仔细对比一下这两个结果有什么不同。再来,如果我想要计算1+2+3+…+100这个数据呢?利用循环是这样的:
[root@www scripts]# vi sh14.sh
!/bin/bash
Program:
Use loop to calculate "1+2+3+…+100" result.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
s=0 # 这是累加的数值变量
i=0 # 这是累计的数值,亦即是 1, 2, 3….
while [ "$i" != "100" ]
do
i=$(($i+1)) # 每次 i 都会增加 1
s=$(($s+$i)) # 每次都会累加一次!
done
echo "The result of '1+2+3+…+100' is ==> $s"
当你执行了“sh sh14.sh”之后,就可以得到 5050 这个数据才对。那么让你自行做一下,如果想要让用户自行输入一个数字,让程序由 1+2+…直到你输入的数字为止,该如何编写呢?应该很简单吧?答案可以参考一下习题练习。
13.5.2 for…do…done(固定循环)
相对于 while, until 的循环方式是必须要“符合某个条件”的状态,for 这种语法则是“已经知道要进行几次循环”的状态!它的语法是:
for var in con1 con2 con3 …
do
程序段
done
以上面的例子来说,这个$var的变量内容在循环工作时:
1.第一次循环时,$var的内容为con1;
2.第二次循环时,$var的内容为con2;
3.第三次循环时,$var的内容为con3;
……
我们可以做个简单的练习。假设我有三种动物,分别是 dog, cat, elephant 三种,我想每一行都输出这样:“There are dogs…”之类的字样,则可以:
[root@www scripts]# vi sh15.sh
!/bin/bash
Program:
Using for…loop to print 3 animals
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
for animal in dog cat elephant
do
echo "There are ${animal}s…"
done
等你执行之后就能够发现这个程序运行的情况啦!让我们想象另外一种状况,由于系统上面的各种账号都是写在/etc/passwd内的第一个字段,你能不能通过管道命令的cut找出单纯的账号名称后,以id及finger分别检查用户的标识符与特殊参数呢?由于不同的Linux系统上面的账号都不一样!此时实际去获取/etc/passwd并使用循环处理就是一个可行的方案了。程序可以如下:
[root@www scripts]# vi sh16.sh
!/bin/bash
Program
Use id, finger command to check system account's information.
History
2009/02/18 VBird first release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
users=$(cut -d ':' -f1 /etc/passwd) #获取账号名称
for username in $users #开始循环进行
do
id $username
finger $username
done
执行上面的脚本后,你的系统账号就会被获取出来检查。这个操作还可以用在每个账号的删除、更改上面呢!换个角度来看,如果我现在需要一连串的数字来进行循环呢?举例来说,我想要利用ping这个可以判断网络状态的命令。来进行网络状态的实际检测时,我想要检测的域是本机所在的192.168.1.1~192.168.1.100,由于有100台主机,总不会要我在for后面输入1到100吧?此时你可以这样做。
[root@www scripts]# vi sh17.sh
!/bin/bash
Program
Use ping command to check the network's PC state.
History
2009/02/18 VBird first release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
network="192.168.1" # 先定义一个域的前面部分
for sitenu in $(seq 1 100) # seq为 sequence(连续) 的缩写之意
do
下面的语句取得 ping 的回传值是正确的还是失败的
ping -c 1 -w 1 ${network}.${sitenu} &> /dev/null && result=0 || result=1
开始显示结果是正确的启动 (UP) 还是错误的没有连通 (DOWN)
if [ "$result" == 0 ]; then
echo "Server ${network}.${sitenu} is UP."
else
echo "Server ${network}.${sitenu} is DOWN."
fi
done
上面这一串命令执行之后就可以显示出192.168.1.1~192.168.1.100共100部主机目前是否能与你的机器连通!如果你的域与鸟哥所在的位置不同,则直接修改上面那个network的变量内容即可!其实这个范例的重点在$(seq..)那个位置!那个seq是连续(sequence)的缩写之意!代表后面接的两个数值是一直连续的!如此一来,就能够轻松地将连续数字写入程序中。
最后,让我们来玩判断式加上循环的功能!我想要让用户输入某个目录文件名,然后找出某目录内的文件名的权限,该如何是好?可以这样做。
[root@www scripts]# vi sh18.sh
!/bin/bash
Program:
User input dir name, I find the permission of files.
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
1. 先看看这个目录是否存在啊?
read -p "Please input a directory: " dir
if [ "$dir" == "" -o ! -d "$dir" ]; then
echo "The $dir is NOT exist in your system."
exit 1
fi
2. 开始测试文件。
filelist=$(ls $dir) # 列出所有在该目录下的文件名
for filename in $filelist
do
perm=""
test -r "$dir/$filename" && perm="$perm readable"
test -w "$dir/$filename" && perm="$perm writable"
test -x "$dir/$filename" && perm="$perm executable"
echo "The file $dir/$filename's permission is $perm "
done
很有趣的例子吧?利用这种方式,你可以很轻易地处理一些文件的特性。接下来,让我们来练习另一种for循环的功能吧!主要用在数值方面的处理。
13.5.3 for…do…done的数值处理
除了上述的方法之外,for循环还有另外一种写法!语法如下:
for (( 初始值; 限制值; 执行步长 ))
do
程序段
done
这种语法适合于数值方式的运算当中,在for后面的括号内的三串内容意义为:
初始值:某个变量在循环当中的初始值,直接以类似i=1设置好;
限制值:当变量的值在这个限制值的范围内,就继续进行循环,例如i<=100;
执行步长:每做一次循环时变量的变化量。例如i=i+1。
值得注意的是,在“执行步长”的设置上,如果每次增加1,则可以使用类似“i++”的方式,即i每次循环都会增加一的意思。好!我们以这种方式来进行1累加到用户输入的循环。
[root@www scripts]# vi sh19.sh
!/bin/bash
Program:
Try do calculate 1+2+…+${your_input}
History:
2005/08/29 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
read -p "Please input a number, I will count for 1+2+…+your_input: " nu
s=0
for (( i=1; i<=$nu; i=i+1 ))
do
s=$(($s+$i))
done
echo "The result of '1+2+3+…+$nu' is ==> $s"
一样也是很简单吧?利用这个for则可以直接限制循环要进行几次呢!
13.6 shell script的追踪与调试
script 在执行之前,最怕的就是出现语法错误的问题了!那么我们如何调试呢?有没有办法不需要通过直接执行该script就可以来判断是否有问题呢?当然是有的!我们就直接以bash的相关参数来进行判断吧!
[root@www ~]# sh [-nvx] scripts.sh
参数:
-n :不要执行 script,仅查询语法的问题;
-v :在执行 script 前,先将 script的内容输出到屏幕上;
-x :将使用到的 script 内容显示到屏幕上,这是很有用的参数!
范例一:测试 sh16.sh 有无语法的问题。
[root@www ~]# sh -n sh16.sh
若语法没有问题,则不会显示任何信息!
范例二:将 sh15.sh 的执行过程全部列出来。
[root@www ~]# sh -x sh15.sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/root/bin
export PATH
for animal in dog cat elephant
echo 'There are dogs…. '
There are dogs….
for animal in dog cat elephant
echo 'There are cats…. '
There are cats….
for animal in dog cat elephant
echo 'There are elephants…. '
There are elephants….
请注意,上面范例二中执行的结果并不会有颜色的显示!鸟哥为了方便说明所以在+号之后的数据都加上颜色了!在输出的信息中,在加号后面的数据其实都是命令串,由 sh -x 的方式来将命令执行过程也显示出来,如此用户可以判断程序代码执行到哪一段时会出现相关的信息!这个功能非常棒!通过显示完整的命令串,你就能够依据输出的错误信息来修改你的脚本了!
熟悉 sh 的用法,将可以使你在管理 Linux 的过程中得心应手。至于在 shell script 的学习方法上面,需要多看、多模仿并加以修改成自己的样式!这是最快的学习手段了!网络上有相当多的朋友在开发一些相当有用的 script,若是你可以将对方的 script 拿来,并且改成适合自己主机的样子。那么学习的效果会是最快的呢!
另外,我们Linux系统本来就有很多的服务启动脚本,如果你想要知道每个script所代表的功能是什么?可以直接以vim进入该script去查阅一下,通常立刻就知道该script的目的了。举例来说,我们之前一直提到的/etc/init.d/syslog,这个script是干嘛用的?利用vi去查阅最前面的几行字,它出现如下信息:
description: Syslog is the facility by which many daemons use to log \
messages to various system log files. It is a good idea to always \
run syslog.
BEGIN INIT INFO
Provides: $syslog
END INIT INFO
简单地说,这个脚本在启动一个名为syslog的常驻程序(daemon),这个常驻程序可以帮助很多系统服务记载它们的登录文件(log file),我们的 Linux建议你一直启动 syslog 是个好主意。
另外,本章所有的范例都可以在 http://linux.vbird.org/linux_basic/0340bashshell-scripts/ scripts- v3.tar.bz2 里头找到。
13.7 重点回顾
shell script 是利用 shell 的功能所写的一个“程序”(program),这个程序是使用纯文本文件,将一些shell的语法与命令(含外部命令)写在里面,搭配正则表达式、管道命令与数据流重定向等功能,以达到我们所想要的处理目的。
shell script 用在系统管理上面是很好的一项工具,但是用在处理大量数值运算上就不够好了,因为 shell script 的速度较慢,且使用的 CPU 资源较多,造成主机资源的分配不良。
在 shell script 的文件中,命令是从上而下、从左而右地分析与执行。
shell script的执行至少需要有r的权限,若需要直接命令执行,则需要拥有r与x的权限。
在良好的程序编写习惯中,第一行要声明 shell(#!/bin/bash),第二行以后则声明程序用途、版本、作者等。
对谈式脚本可用 read 命令达成。
要创建每次执行脚本都有不同结果的数据,可使用 date 命令利用日期达成。
script 的执行若以 source 来执行时,代表在父进程的 bash 内执行之意!
若需要进行判断式,可使用 test 或中括号([])来处理。
在 script 内,$0, $1, $2…, $@是有特殊意义的!
条件判断式可使用 if…then 来判断,若是固定变量内容的情况下,可使用 case $var in…esac来处理。
循环主要分为不定循环(while,until)以及固定循环(for),配合do、done来达成所需任务!
我们可使用 sh -x script.sh 来进行程序的调试。
13.8 本章习题
下面皆为实践题,请自行编写出程序。
请新建一个 script,当你执行该 script 的时候,该 script 可以显示你目前的身份(用 whoami)和你目前所在的目录(用pwd)。
请自行写一个程序,该程序可以用来计算你还有几天可以过生日。
让用户输入一个数字,程序可以由 1+2+3…一直累加到用户输入的数字为止。
编写一支程序,它的作用是先查看一下/root/test/logical 这个名称是否存在,若不存在,则创建一个文件,使用touch来创建,创建完成后离开;如果存在的话,判断该名称是否为文件,若为文件则将之删除后新建一个目录,文件名为 logical,之后离开;如果存在的话,而且该名称为目录,则删除此目录!
我们知道/etc/passwd 里面以:来分隔,第一列为账号名称。请写一个程序,可以将/etc/passwd 的第一列取出,而且每一列都以一行字符串“The 1 account is "root" ”来显示,那个 1 表示行数。
13.9 参考数据与扩展阅读
卧龙小三大师的文件:http://linux.tnc.edu.cn/techdoc/shell/book1.html
注 释
[2].Webmin 的官方网站:http://www.webmin.com/
[4].使用 man bash,再以 PS1 为关键字去查询,按下数次 n 往后查询后,可以得到 PS1 的变量说明。
[8].关于ASCII编码对照表可参考维基百科的介绍:维基百科(ASCII)条目:http://zh.wikipedia.org/zh-cn/Ascii