学习shell与shell script
# 学习 shell script
# 什么是 shell script
shell script (程序化脚本),就字面意义上,其分为两部分。在shell部分,在BASH章节已经提过,那是一个命令行界面下面让我们与系统沟通的一个工具接口;script是脚本的意思,shell script就是针对shell所写的脚本。
其实,shell script是利用shell的功能所写的一个程序(program),这个程序是使用纯文本文件,将一些shell的语法与命令(含外部命令)写在里面,搭配正则表达式、管道命令与数据流重定向等功能,以达到我们所想要的处理目的。
# 为什么学习 shell script
- 自动化管理的重要依据
管理服务器不是简单的事情,每天要进行的任务就有:查询登录档、追踪流量、监控用户使用主机状态、主机各项硬设备状态、主机软件更新查询、更不用说应付其他使用者的突然要求了。这些工作可分为:自行手动处理或写个简单的程序来自动处理分析。
- 追踪与管理系统的重要工作
在CentOS 6.x以前的版本中,系统的服务(services)启动的接口是在/etc/init.d/
这个目录下,目录下的所有文件都是script;另外,包括开机(booting)过程也都是利用shell script来帮忙搜寻系统的相关设定数据,然后再代入各个服务的设定参数。例如,如果想要重新启动系统注册表档,可以:/etc/init.d/rsyslogd restart
,这个rsyslogd
文件就是script。
时至今日,虽然/etc/init.d/*
这个脚本启动的方式(systemV)已经被新一代的systemd
所取代(从CentOS 7开始),但是很多的个别服务在管理他们的服务启动方面,还是使用shell script的机制。
- 简单入侵检测功能
当系统有异状时,大多会将这些记录在系统记录器,也就是系统注册表档,那么运维人员可以在固定的几分钟内主动去分析系统注册表档,若有问题,就立刻通过管理员,或者立刻加强防火墙规则。
- 连续指令单一化
对于新手而言,script最简单的功能就是:**汇整一些在command line下达的连续指令,将他写入scripts当中,而由直接执行scripts来启动一连串的command line指令输入!**其实,如果不考虑program部分,那么scripts也可以想成仅是把一大串的指令汇整在一个文件里面,而直接执行该文件就可以执行那一串又臭又长的指令段!
- 简易的数据处理
- 跨平台支持与学习历程较短
不过,虽然shell script号称是程序(program),但实际上,shell script处理数据的速度上是不太够的。因为shell script用的是外部的指令与bash shell的一些默认工具,所以,它常常会调用外部的函数库,因此,指令周期上面当然比不上传统的程序语言。所以,shell script用在系统管理上面是很好的一项工具,但是用在处理大量数值运算上,就不够好了,因为shell scripts的速度较慢,且使用的CPU资源较多,造成主机资源的分配不良。
# 第一个script的编写与执行
在shell script的编写中需要注意以下事项:
- 指令的执行是从上而下、从左而右的分析与执行;
- 指令的下达就如同之前提到的:指令、选项与参数间的多个空白都会被忽略掉;
- 空白行也将被忽略掉,并且
tab
按键所推开的空白同样视为空格键; - 如果读取到一个
Enter
符号(CR
),就尝试开始执行该行(或该串)命令; - 至于如果一行的内容太多,则可以使用
\Enter
来延伸至下一行; #
可作为批注,任何加在#
后面的资料将全部被视为批注文字而被忽略。
如此一来,在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
来下达指令?之前有介绍过指令搜寻顺序,同时,由于CentOS默认用户家目录下的~/bin
目录会被设定到${PATH}
内,所以也可以将shell.sh建立在/home/dmtsai/bin
目录下,(~/bin
目录需要自行创建),此时,若shell.sh在~/bin内具有rx的权限,那直接输入shell.sh即可执行该脚本程序!
那为何sh shell.sh
也可执行呢?这是因为/bin/sh
其实就是/bin/bash
(连接档),使用sh shell.sh
即告诉系统,如果想要直接以bash
的功能来执行shell.sh
这个文件内的相关指令的意思,所以此时shell.sh只要有r的权限即可被执行!,也可以利用sh
的参数,如-n
及-x
来检查与追踪shell.sh的语法是否正确。
#!/bin/bash
# Program
# This program shows "Hello World!" in your screen.
# History
# 2023/12/07 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
2
3
4
5
6
7
8
9
针对上面程序的解释:
#!/bin/bash
在宣告这个script使用的shell名称:
因为我们使用的是bash,所以,必须以#!/bin/bash
来宣告这个文件内的语法使用bash的语法,那么当程序执行时,就能加载bash的相关环境配置文件(一般是non-login shell的 ~/.bashrc),并且执行bash来使底下的指令能够执行,这很重要(如果没有设定号这一行,程序可能无法执行,因系统无法判断程序需要什么shell来执行)- 程序内容说明:
程序中,除第一行#!
用来宣告shell,其他的#
都是注释用途,上面程序第二行以下就是用来说明整个程序的基本数据。一般建议内容为:- 内容与功能;
- 版本信息;
- 作者与联络方式
- 建档日期
- 历史记录等等 这有助于未来程序的改写。
- 主要环境变量的宣告:
建议务必将一些重要的环境变量设定好,PATH与LANG(如果有使用到输出相关的信息时)是当中最重要的,如此一来,则可让我们这支程序在进行时,可以直接下达一些外部指令,而不必写绝对路径。 - 主要程序部分:例子中,就是
echo
那一行。 - 执行成果告知(定义回传值):
之前章节提到过一个指令的成功与否,可以使用$?
这个变量来观察,在程序中可以利用exit这个指令来让程序中断,并且回传一个数值给系统。在上述例子中,使用exit 0
代表离开script并且回传一个0给系统,所以执行完这个程序后,若接着下达echo $?
则可得到0的值。因此利用这个exit n(n是数字)
的功能,可以自定义错误信息。
# 编写shell script的良好习惯
建议养成良好的script编写习惯,在每个script的文件头处记录好:
- script 的功能
- script 的版本信息
- script 的作者与联络方式
- script 的版权宣告方式
- script 的History(历史记录)
- script 内较特殊的指令,使用绝对路径的方式来下达
- script 运作时需要的环境变量预先宣告与设定
除了记录这些信息外,在较为特殊的程序代码部分,建议务必要加上注释说明。此外,程序代码的编写最好使用巢状方式,在包覆的内部程序代码最好以tab按键的空格向后推,这样程序代码会显得非常的漂亮与有条理。另外,编写script的工具最好使用vim而不是vi,因为vim会有额外的语法检验机制,能够在第一阶段编写时就发现语法方面的问题。
# 简单的 shell script 练习
# shell script简单范例
- 对谈式脚本:变量内容由用户决定
很多时候我们需要使用者输入一些内容,好让程序可以顺利运行,简单的说,大家应该都有安装过软件的经验,安装的时候,会提示要安装到哪个目录去,那个让用户输入数据的动作,就是让用户输入变量内容。
之前有介绍过read
指令,限制,以read
指令的用途,编写一个script,可以让用户输入:1. first name 2. last name,最后在屏幕上显示:Your full name is:
的内容
#!/bin/bash
# Program:
# User inputs his first name and last name. Program shows his full name.
# History:
# 2015/07/16 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}"
2
3
4
5
6
7
8
9
10
11
- 随日期变化:利用
date
进行文件的建立
假设有这样一种情况,服务器内有数据库,数据库每天的数据需要进行备份,而且希望每天的数据都备份成不同的档名,这样才能让旧的数据也能保存下来不被覆盖。考虑每天的日期不同,可以将档名取成类似:backup.2015-07-16.data
,不就可以每天一个不同档名了。下面看下这个例子:假设要建立三个空的文件(通过touch
),档名最开头由用户输入决定,假设用户输入filename
,那今天的日期是2015/07/16
,以前天、昨天、今天的日期来建立这些文件,亦filename_20150714, filename_20150715, filename_20150716
。
#!/bin/bash
# Program:
# Program creates three files, which named by user's input and date command.
# History:
# 2015/07/16 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}"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
上面的范例使用了很多之前介绍过的概念:包括小指令$(command)
取得信息、变量的设定、变量的累加以及利用touch
指令辅助。执行这个create_3_filename.sh
后,可以执行两次:一次直接按Enter
来查阅生成的文档名,一次可以输入一些字符,这样可以判断脚本是否设计正确。
- 数值运算:简单的加减乘除
前面介绍过可以使用declare
来定义变量的类型,当变量定义称为整数后才能进行加减运算,此外,也可以利用$((计算式))
来进行数值运算。不过,bash shell里头预设仅支持到整数的数据而已。
#!/bin/bash
# Program:
# User inputs 2 integer numbers; program will cross these two numbers.
# History:
# 2015/07/16 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 multiplying them! \n"
read -p "first number: " firstnum
read -p "second number: " secnum
total=$((${firstnum}*${secnum}))
echo -e "\nThe result of ${firstnum} * ${secnum} is ==> ${total}"
2
3
4
5
6
7
8
9
10
11
12
在数值的运算上,可以使用declare -i total=${firstnum}*${secnum}
,也可以使用上面的方式来进行,不过比较建议使用这样的方式来进行运算:var=$((运算内容))
,不但容易记忆,也比较方便,因为两个小括号内可以加上空格。数值运算上的处理有:+, -, *, /, %
等。
另外,如果想要计算含有小数点的数据,可以通过bc
这个指令,例如:
- 数值运算:通过
bc
计算pi
其实计算pi
时,小数点以下位数可以无限制的延伸下去,而bc
提供了一个计算pi
的函数,只是想要使用该函数必须要使用bc -l
来调用才行,因为这个小数点可以无限延伸运算的特性存在,所以我们可以通过下面的脚本来让用户输入一个小数点为数值,以让pi
能够更准确。
#!/bin/bash
# Program:
# User input a scale number to calculate pi number.
# History:
# 2015/07/06 VBird First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "This program will calculate pi value. \n"
echo -e "You should input a float number to calculate pi value. \n"
read -p "The scale number (10~10000)?" checking
num=${checking:-"10"} # 开始判断是否有输入数值
echo -e "Starting calcuate pi value. Be patient."
time echo "scale=${num};4*a(1)" | bc -lq
2
3
4
5
6
7
8
9
10
11
12
13
上述数据中,4*a(1)
是bc
主动提供的一个计算pi
的函数,至于scale
就是要bc
计算几个小数位数的意思。当scale
的数值越大,代表pi
要被计算的越精确,当然用到的时间就会越多,不过最好不要超过5000,因为算越久,耗费资源越多。
# script的执行方式差异(source, sh script, ./script)
不同的script执行方式会造成不一样的结果,脚本的执行方式除了前面谈到的方式之外,还可以利用source
或小数点(.
)来执行,那么不同的执行方式有什么区别呢?
- 利用直接执行的方式来执行script
当使用前一小节提到的直接指令下达(不论是绝对路径/相对路径还是${PATH}
内),或者是利用bash
(或sh
)来执行脚本时,该script都会使用一个新的bash环境来执行脚本内的指令。也就是,使用这种执行方式,其实script是在子程序的bash
内执行的。前面章节谈到的export
功能,曾经就父程序/子程序谈过一些概念性是问题,重点在于:当子程序完成后,在子程序内的各项变量或动作将会结束而不会传回到父程序中。
以上面的showname.sh
这个脚本来说,这个脚本可以让用户自行设定两个变量,分别是firstname
和lastname
,如果直接执行该脚本时,该脚本会设定firstname
,那么在父程序中会不会生效呢?
上面的结果就比较意外,showname.sh
设定好的变量竟然在bash
环境下无效,为什么?如果将程序相关性绘制成图的话,可以以下图说明。当使用直接执行的方法来执行时,系统会给予一个新的bash
来执行showname.sh
里面的指令,因此firstname
、lastname
等变量其实是在下图中的子程序内执行,当showname.sh
执行完后,子程序bash
内的所有数据便被移除,因此上面的练习中,在父程序下,echo ${firstname}
就看不到任何东西。
- 利用source来执行脚本:在父程序中执行
如果使用source
来执行那就不一样了
# 善用判断式
前面提到过$?
这个变量所代表的意义,此外,也通过&&
及||
来作为前一个指令执行回传值对于后一个指令是否要进行的依据。在前面的章节中,如果想要判断一个目录是否存在,当时使用的是ls
这个指令搭配数据流重定向,最后配合$?
来决定后续的指令进行与否,那么是否有更简单的方式呢?
# 利用test
指令的测试功能
当要检测系统上面某些文件或者是相关的属性时,利用test
这个指令来工作就很方便,比如,检查/test
是否存在,使用:
test -e /test
执行结果并不会显示任何信息,但可以通过$?
或&&
及||
来展现整个结果,如:
test -e /test && echo "exist" || echo "Not exist"
- 关于文件类型判断,如
test -e filename
表示是否存在
测试的标志 | 代表意义 |
---|---|
-e | 判断目标是否存在?(常用) |
-f | 判断目标是否存在且为文件(file )(常用) |
-d | 判断目标是否存在且为目录(directory )(常用) |
-b | 判断目标是否存在且为一个block device 装置 |
-c | 判断目标是否存在且为一个character device 装置 |
-S | 判断目标是否存在且为一个Socket 文件 |
-p | 判断目标是否存在且为一个FIFO(pipe) 文件 |
-L | 判断目标是否存在且为一个连接文件 |
- 关于文件的权限监测,如
test -r filename
表示可读否(但root
权限常有例外)
测试的标志 | 代表意义 |
---|---|
-r | 测试目标是否存在且具有可读的权限 |
-w | 测试目标是否存在且具有可写的权限 |
-x | 测试目标是否存在且具有可执行的权限 |
-u | 测试目标是否存在且具有SUID的属性 |
-g | 测试目标是否存在且具有SGID的属性 |
-k | 测试目标是否存在且具有Sticky bit的属性 |
-s | 测试目标是否存在且为非空白文件 |
- 两个文件之间的比较,如:
test file1 -nt file2
测试的标志 | 代表意义 |
---|---|
-nt | (newer than )判断file1 是否比file2 新 |
-ot | (older than )判断file1 是否比file2 旧 |
-ef | 判断file1 与file2 是否为同一文件,可用在判断hard link 的判定上,主要意义在判定,两个文件是否均指向同一个inode |
- 关于两个整数之间的判定,如:
test num1 -eq num2
测试的标志 | 代表意义 |
---|---|
-eq | 测试两数值是否相等(equal ) |
-ne | 测试两数值是否不等(not equal ) |
-gt | 测试num1 是否大于num2 (greater than ) |
-lt | 测试num1 是否小于num2 (less than ) |
-ge | 测试num1 是否大于等于num2 (greater than or equal ) |
-le | 测试num1 是否小于等于num2 (less than or equal ) |
- 判定字符串的数据
测试的标志 | 代表意义 |
---|---|
test -z string | 判断字符串是否为空,若string 为空字符串,则为true |
test -n string | 判断字符串是否非空,若string 为空字符串,则为false |
test str1 == str2 | 判定str1 是否等于str2 ,若相等,则回传true |
test str1 != str2 | 判断str1 是否不等于str2 ,若相等,则回传false |
- 多重条件判定,如:
test -r filename -a -x filename
测试的标志 | 代表意义 |
---|---|
-a | (and )两种情况同时成立,如test -r file -a -x file ,则file 同时具有r 与x 权限,才回传true |
-o | (or )两种情况任何一个成立,如test -r file -o -x file ,则file 具有r 或x 权限时,则回传true |
! | 反状态,如test ! -x file ,当file 不具有x 时,则回传true |
下面就利用test
来写几个简单的例子,首先,让用户输入一个文档名,使用程序进行判断:
- 文档是否存在,若不存在则提示
Filename does not exist
,并中断程序; - 若文档存在,则判断其是文件还是目录,根据判断结果输出
Filename is regular file
或Filename is directory
; - 然后再判断以下,用户对这个文件或目录所拥有的权限,并输出权限数据。
#!/bin/bash
# Program:
# User input a filename, program will check the flowing:
# 1. exist?
# 2. file/directory?
# 3. file permissions
# History:
# 2015/07/16 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}' don't 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 for you are: ${perm}"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
如果执行这个脚本,其会根据输入的文件名进行检查,首先判断是否存在,再看文件或目录类型,最后判断权限。但需要注意的是:由于root在很多权限的限制上面都无效,所以使用root执行这个脚本时,常常会发现与ls -l
观察到的结果并不相同。因此,建议使用一般使用者来执行这个脚本试试看。
# 利用判断符号[]
除了使用test
外,其实,还可以利用判断符号[]
来进行数据判断,如,判断${HOME}
这个变量是否为空,可以这样写:
[ -z "${HOME}" ]; echo $?
使用中括号必须要特别注意,因为中括号用在很多地方,包括通配符与正则表达式等等,所以如果要在bash
的语法当中使用中括号作为shell
的判断式,必须要注意中括号的两端需要有空格符来分隔,假设空格键用□
符号来表示,这些地方都需要空格:
[ "${HOME}" == "${MAIL}" ]
[□"${HOME}"□==□"${MAIL}"□]
2
提示
上面的判断中使用了两个等号==
,其实在bash
当中使用一个等号与两个等号结果是一样的,不过在一般惯用程序的写法中,一个等号代表变量的设定,两个等号则是代表逻辑判断(是与否之意),由于此处在中括号内重点在于判断而非设定变量,因此建议使用两个等号。
上面的例子,两个字符串${HOME}
与${MAIL}
是否相同的意思,相当于test ${HOME} == ${MAIL}
,但是如果没有空白分隔,如[${HOME}==${MAIL}]
时,bash
就会显示错误信息。因此需要注意:
- 在中括号
[]
内的每个组件都需要有空格来分隔; - 在中括号内的变量,最好都以双引号括号起来;
- 在中括号内的常量,最好都以单或双引号括起来。
比如下面的例子,name="VBird Tsai"
,如果这样判定:
name="VBird Tsai"
[ ${name} == "VBird" ]
2
这样就会报错,原因就在于${name}
如果没有使用双引号括起来,那么上面的判定就会变成:
[ VBird Tsai == "VBird" ]
那这样肯定不对,因为一个判断式仅能有两个数据的比对,我们需要的应该是这个样子:
[ "VBird Tsai" == "VBird" ]
另外,中括号的使用方法与test
几乎一模一样,只是中括号比较常用在条件判断式if ... then ... fi
中,接下来使用中括号来做一个小案例,如下:
- 当执行一个程序的时候,这个程序让用户输入
Y
或N
- 如果用户输入
Y
或y
时,就显示OK, continue.
- 如果用户输入
N
或n
时,就显示Oh, interrupt!
- 如果不是
Y/y/N/n
之内的其他字符,就显示I don't know what your choice is.
#!/bin/bash
# Program:
# This program shows the user's choice
# History:
# 2015/07/16 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 your choice is" && exit 0
2
3
4
5
6
7
8
9
10
11
12
# Shell script 的默认变数($0, $1...)
指令可以带有选项与参数,如ls -la
可以查看包含隐藏文件的所有属性与权限,那么shell script
能否在脚本后面带有参数呢?比如,重启系统的网络可以这样:
file /etc/init.d/network
/etc/init.d/network: Bourne-Again shell script, ASCII text executable
/etc/init.d/network restart
2
3
restart
是重新启动,上面的指令可以重新启动/etc/init.d/network
,如果在/etc/init.d/network
后面加上stop
,就可以直接关闭该服务。本章开始使用read
功能,但read
的问题是需要手动由键盘输入一些变量,如果通过指令后面接参数,那么一个指令就能够处理完毕而不需要手动输入一些变量。其实script
针对参数已经设定好一些变量名称了,对应如下:
执行的脚本名为$0
这个变量,第一个接的参数就是$1
,因此,只要在脚本中善用$1
,就可以简单的下达某些指令功能,同时还有一些比较特殊的变量可以在脚本内来使用这些参数。
$#
: 代表后接的参数个数,以上表为例这里显示为4$@
: 代表"$1" "$2" "$3" "$4"
之意,每个变量是独立的(用双引号括起来)$*
: 代表"$1c$2c$3c$4
,其中c
为分隔字符,默认为空格,所以本例中代表"$1 $2 $3 $4"
之意
$@
与$*
还是有所不同的,一般情况用$@
即可,下面来看一个例子,执行脚本后会显示如下数据:
- 程序的文件名为何?
- 共有几个参数
- 若参数的个数小于2则告知参数数量太少
- 全部的参数内容为何?
- 第一个参数为何?
#!/bin/bash
# Program:
# Program shows the script name, parameters...
# History:
# 2015/07/06 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 "The 1st parameter is ==> ${1}"
echo "The 2nd parameter is ==> ${2}"
2
3
4
5
6
7
8
9
10
11
12
13
shift
: 使参数变量号码偏移
接下来通过一个例子来解释参数变量号码的偏移,如下:
#!/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 ===> '$@'"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过结果可知,shift
会移动变量,而且shift
后可以接数字,代表拿掉最前面几个参数的意思。
# 条件判断式
条件判断式,亦if then
,很多时候,都要依据某些数据来判断程序该如何进行,比如,在ans_yn.sh
讨论输入响应的范例中由用户输入Y/N
,然后根据用户的输入返回不同的消息。
# 利用if ... then
if ... then
是最常见的条件判断,当符合某个条件判断的时候,就执行相关的业务逻辑。
- 单层、简单条件判断
if [ 条件判断 ]; then
# 当条件判断成立时,可以执行的指令
fi # 将 if 反过来写
2
3
至于条件判断的写法,前面已经介绍过,不过,如果有多个条件要判别,除了ans_yn.sh
案例所写,就是将多个条件写入一个中括号内的情况,还可以用多个中括号来隔开,不过括号与括号之间,需要以&&
或||
来隔开:
&&
代表and
||
代表or
所以,使用中括号的判断中,&&
及||
就与指令下达的状态不同了,例如,ans_yn.sh
里面的判断可以这样修改:
[ "${yn}" == "Y" -o "${yn}" == "y" ]
可以替换为 [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]
之所以这样改,很多是习惯问题,很多人喜欢一个中括号仅有一个判别式,下面将ans_yn.sh
这个脚本修改为if ... then
的样式:
#!/bin/bash
# Program:
# This program shows the user's choice
# History:
# 2015/07/16 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 your choice is" && exit 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
由这个例子看,似乎也没有什么,原本的ans_yn.sh
还比较简单,但改写后以逻辑来看,使用了两个两件判断,命名仅有一个${yn}
的变量,却需要进行两次比对,此时多重条件判断就派上用场了。
- 多重、复杂条件判断
在同一个数据的判断中,如果数据需要进行多种不同的判断,可以这样写:
# 一个条件判断,分成功与失败
if [ 条件判断 ]; then
# 当条件判断为true,执行
else
# 当条件判断为false,执行
fi
2
3
4
5
6
# 多个条件判断 (if ... elif ... elif ... else)分多种不同情况执行
if [ 条件判断一 ]; then
# 当条件判断一成立时,执行
elif [ 条件判断二 ]; then
# 当条件判断二成立时,执行
else
# 当条件判断一与二都不成立时,执行
fi
2
3
4
5
6
7
8
需要注意的是,elif
也是个判断,因此出现elif
后面都要接then
来处理,但是else
已经是最后的,所以else
后面不用跟then
。
#!/bin/bash
# Program:
# This program shows the user's choice
# History:
# 2015/07/16 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
elif [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then
echo "Oh, interrupt!"
exit 0
else
echo "I don't know your choice is" && exit 0
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
下面看下这个例子,一般如果不希望用户由键盘输入额外的数据,可以让用户在执行脚本时就将参数带进去,现在让用户输入hello
这个关键词,利用参数的方法可以这样设计:
- 判断
$1
是否为hello
,如果是,就输出Hello, how are you?
- 如果没有加任何参数,就提示用户必须要使用的参数下达法
- 而如果加入的参数不是
hello
,就提醒用户仅能使用hello
为参数
#!/bin/bash
# Program:
# Check $1 is equal to "hello"
# History:
# 2015/07/16 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
之前章节已经学了grep
指令,现在介绍一个新指令netstat
,这个指令可以查询目前主机开启的网络服务端端口(service ports
)。
上面的重点是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(打印服务功能)
假设要侦测比较常见的port: 21, 22, 25及80,可以通过netstat
侦测主机是否有开启这四个主要的网络服务端端口,每个服务的关键词都接在冒号:
后,那么程序可以这样写:
#!/bin/bash
# Program:
# Using netstat and grep to detect WWW, SSH, FTP and Mail services.
# History:
# 2015/07/16 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(smtp) will be detect! \n"
# 2. 开始进行一些测试的工作,并且输出一些信息
testfile=/dev/shm/netstat_checking.txt
netstat -tuln > ${testfile} # 先转存数据到内存中,不用一直执行
testing=$(grep ":80 " ${testfile}) # 监测80端口
if [ "${testing}" != "" ]; then
echo "WWW is running in your system."
fi
testing=$(grep ":22 " ${testfile}) # 监测22端口
if [ "${testing}" != "" ]; then
echo "SSH is running in your system."
fi
testing=$(grep ":21 " ${testfile}) # 监测21端口
if [ "${testing}" != "" ]; then
echo "FTP is running in your system."
fi
testing=$(grep ":25 " ${testfile}) # 监测25端口
if [ "${testing}" != "" ]; then
echo "Mail is running in your system."
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
再看下面的例子:
- 让用户输入一个日期
- 再由现在日期比对输入的日期
- 比较两个日期相差的天数
#!/bin/bash
# Program:
# You input your date, I calculate how many days before today
# History:
# 2015/07/16 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 today ..."
read -p "please input your demobilization date (YYYYMMDD ex>20150716): " date2
# 2. 利用正则表达式测试用户输入是否正确
date_d=$(echo ${date2} | grep '[0-9]\{8\}') # 测试是否有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 before: " $((-1*${date_d})) " age"
else
declare -i date_h=$(($((${date_total_s}-${date_d}*60*60*24))/60/60))
echo "You will after ${date_d} days and ${date_h} hours."
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 利用case ... esac
判断
上小节提到的if ... then ... fi
对于变量的判断是以比对的方式来分辨,如果符合状态就进行某些行为,并且透过较多层次(elif
)的方式进行多个变量的程序代码编写,那么如果有多个既定的变量内容,比如就是hello
及空字符串两个,只需要针对这两个变量来设定情况,该怎么写呢?可以使用case ... in ... esac
case $param in # 关键词为case,注意变量前有$
"value1") # 每个变量内容建议用双引号括起来,关键词为小括号)
program1
;; # 每个类别结尾使用两个连续的分号处理
"value2")
program2
;;
*) # 最后一个变量内容都会用*来代表所有其他值
default program
exit 1
;;
esac # 最终的case结尾,反过来写
2
3
4
5
6
7
8
9
10
11
12
例子如下:
#!/bin/bash
# Program:
# Show "Hello" from $1 ... by using case ... esac
# History:
# 2015/07/16 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}"
;;
*)
echo "Usage ${0} {hello}"
;;
esac
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在上面的案例中,如果输入sh hello-3.sh test
,屏幕上会输出Usage hello-3.sh {hello}
的字样,告知执行者仅能使用hello
,这些方式对于需要某些固定字符串来执行的变量内容就显得比较方便。早期系统很多服务的启动脚本都采用这种写法(CentOS 6.x以前),虽然CentOS 7已经使用systemd
,不过仍有数个服务是放在/etc/init.d
目录下,如netconsole
的服务就在该目录下,如果想重启该服务,就可以:
/etc/init.d/netconsole restart
重点是restart
,如果查看netconsole
的源码,就会发现其使用的是case
语法,并且会规定某些既定的变量内容。
一般来说,使用case $param in
这个语法,当中的$param
大致有两种取得方式:
- 直接下达式: 如上面用到的,
script.sh variable
的方式来直接给予$1
这个变量内容 - 交互式: 通过
read
这个指令让用户输入变量内容
#!/bin/bash
# Program:
# This script only accepts the flowing parameter: one, two or three.
# History:
# 2015/07/17 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25