重头戏终于来了!
一、Shell脚本简介
shell script 是利用 shell 的功能所写的一个『程序 (program)』,这个程序是使用纯文本文件,将一些 shell 的语法与指令(含外部指令)写在里面, 搭配正规表示法、管线命令与数据流重导向等功能,以达到我们所想要的处理目的。
#!/bin/bash 在宣告这个 script 使用的 shell 名称。
用vi test.sh创建一个shell脚本:
#!/bin/bash echo "Hello World !"
执行方式:
(1)bash test.sh
(2)chmod a+x test.sh;./test.sh
二、撰写 shell script 的良好习惯
在每个 script 的文件头处记录好:
- script 的功能;
- script 的版本信息;
- script 的作者与联络方式;
- script 的版权宣告方式;
- script 的 History (历史纪录);
- script 内较特殊的指令,使用『绝对路径』的方式来下达;
- script 运作时需要的环境变量预先宣告与设定。
三、简单的 shell script 练习
1、请你以 read 指令的用途,撰写一个 script ,他可以让使用者输入: 1. first name 与 2. last name, 最后并且在屏幕上显示:
『Your full name is: 』的内容。
2、随日期变化:利用 date 进行文件的建立
假设我想要建立三个空的文件 (透过 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:
# 2018/06/13 yue 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命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。
touch "${file1}" # 底下三行在建立文件
touch "${file2}"
touch "${file3}"
3、数值运算:简单的加减乘除
用户输入两个变量, 然后将两个变量的内容相乘,最后输出相乘的结果。
#!/bin/bash # Program: # User inputs 2 integer numbers; program will cross these two numbers. # History: #2018/06/13 yue First Release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH echo -e "Your should inout 2 numbers.\n" read -p "first number: " firstnu read -p "second number: " secondnu total=$((${firstnu}*${secondnu})) echo -e "\nThe result is: ==>${total}"
4、数值运算:透过 bc 计算 pi
#!/bin/bash # Program: # User input a scale number to calculate pi number. # History: #2018/06/13 yue 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 calculate pi value." time echo "scale=${num}; 4*a(1)" | bc -lq
4*a(1) 是 bc 主动提供的一个计算 pi 的函数,至于 scale 就是要 bc 计算几个小数点下位数的意思。当 scale 的数值越大, 代表 pi 要被计算的越精确。
四、利用直接执行的方式来执行 script
不同的 script 执行方式会造成不一样的结果! 脚本的执行方式除了前面小节谈到的方式之外,还可以利用 source 或小数点 (.) 来执行。
1、利用直接执行的方式来执行 script——使用bash执行脚本时,echo script中的变量是无法显示内容的。
2、利用 source 来执行脚本:在父程序中执行——用source执行script时,可以用echo显示变量的内容。
五、善用判断式
1、利用 test 指令的测试功能例1——
首先,判断一下,让使用者输入一个档名,我们判断:
- 这个文件是否存在,若不存在则给予一个『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: #2018/06/18 yue 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" read -p "Input a filename : " filename test -z ${filename} && echo "You must input a filename." && exit 0 # 2. 判断文件是否存在?若不存在则显示讯息并结束脚本 test ! -e ${filename} && echo "The filename '${filenmae}' 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 for you are : ${perm}"
2、利用判断符号 [ ]
(1)想要知道 ${HOME} 这个变量是否为空——[ -z "${HOME}" ] ; echo $?
注:使用中括号必须要特别注意,因为中括号用在很多地方,包括通配符与正规表示法等等,所以如果要在 bash 的语法当中使用中括号作为 shell 的判断式时,必须要注意中括号的两端需要有空格符来分隔。
所以说,最好要注意:
- 在中括号 [] 内的每个组件都需要有空格键来分隔;
- 在中括号内的变数,最好都以双引号括号起来;
- 在中括号内的常数,最好都以单或双引号括号起来。
中括号比较常用在条件判断式 if ..... then ..... fi 的情况中。
例2——ans_yn.sh
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 』
#!/bin/bash # Program: # This program shows the user's choice # History: #2018/06/13 yue 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(3)Shell script 的默认参数($0, $1...)
指令可以带有选项与参数,例如 ls -la 可以察看包含隐藏文件的所有属性与权限。那么 shell script 能不能在脚本文件名后面带有参数呢?
举例来说,如果你想要重新启动系统的网络,可以这样做:
上面的指令可以『重新启动 /etc/init.d/network 这支程序』的意思
如果要依据程序的执行给予一些变量去进行不同的任务时,一开始使用的是 read 的功能!但 read 功能的问题是你得要手动由键盘输入一些判断式。如果透过指令后面接参数, 那么一个指令就能够处理完毕而不需要手动再次输入一些变量行为。
script 针对参数已经有设定好一些变量名称:
执行的脚本文件名为 $0 这个变量,第一个接的参数就是 $1 啊~ 所以,只要在 script 里面善用 $1 的话,就可以很简单的立即下达某些指令功能了。除了这些数字的变量之外,还有一些较为特殊的变量可以在 script 内使用来呼叫这些参数:
- $# :代表后接的参数『个数』,以上表为例这里显示为『 4 』;
- $@ :代表『 "$1" "$2" "$3" "$4" 』之意,每个变量是独立的(用双引号括起来);
- $* :代表『 "$1c$2c$3c$4" 』,其中 c 为分隔字符,默认为空格键, 所以本例中代表『 "$1 $2 $3 $4" 』之意。
例3——假设我要执行一个可以携带参数的 script ,执行该脚本后屏幕会显示如下的数据:
- 程序的文件名为何?
- 共有几个参数?
- 若参数的个数小于 2 则告知使用者参数数量太少
- 全部的参数内容为何?
- 第一个参数为何?
- 第二个参数为何
#!/bin/bash # Program: # Program shows the script name, parameters... # History: # 2015/07/16 yue 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}"
(4)shift:造成参数变量号码偏移
#!/bin/bash # Program: # Program shows the effect of shift function. # History: # 2018/06/13 yue 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 ==> '$@'"
六、条件判断式
1、利用 if .... then
(1)单层、简单条件判断式[ "${yn}" == "Y" -o "${yn}" == "y" ] 可替换为 [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]
例1:将 ans_yn.sh 这个脚本修改成为 if ... then 的样式
#!/bin/bash # Program: # This program shows the user's choice # History: #2018/06/13 yue 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(2)多重、复杂条件判断式
要注意的是, elif 也是个判断式,因此出现 elif 后面都要接 then 来处理!但是 else 已经是最后的没有成立的结果了, 所以 else 后面并没有 then!再次重写ans_yn.sh
#!/bin/bash # Program: # This program shows the user's choice # History: # 2018/06/13 yue 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
例2——
一般来说,如果不希望用户由键盘输入额外的数据时, 可以使用上一节提到的参数功能 ($1)!让用户在下达指令时就将参数带进去!
让用户输入『 hello 』这个关键词时,利用参数的方法可以这样依序设计:
1. 判断 $1 是否为 hello,如果是的话,就显示 "Hello, how are you ?";
2. 如果没有加任何参数,就提示使用者必须要使用的参数下达法;
3. 而如果加入的参数不是 hello ,就提醒使用者仅能使用 hello 为参数。
#!/bin/bash # Program: # Check $1 is equal to "hello" # History: # 2018/06/13 yue 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
例3——
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 去侦测主机是否有开启这四个主要的网络服务端口口呢?由于每个服务的关键词都是接在冒号『 : 』后面, 所以可以藉由撷取类似『 :80 』来侦测!那就可以简单的这样写这个程序:
#!/bin/bash # Program: # Using netstat and grep to detect WWW,SSH,FTP and Mail services. 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} # 先转存数据到内存当中!不用一直执行 netstat testing=$(grep ":80 " ${testfile}) # 侦测看 port 80 在否? if [ "${testing}" != "" ]; then echo "WWW is running in your system." fi testing=$(grep ":22 " ${testfile}) # 侦测看 port 22 在否? if [ "${testing}" != "" ]; then echo "SSH is running in your system." fi testing=$(grep ":21 " ${testfile}) # 侦测看 port 21 在否? if [ "${testing}" != "" ]; then echo "FTP is running in your system." fi testing=$(grep ":25 " ${testfile}) # 侦测看 port 25 在否? if [ "${testing}" != "" ]; then echo "Mail is running in your system." fi
例4——让用户输入他的退伍日期,让你去帮他计算还有几天才退伍?
由于日期是要用相减的方式来处置,所以我们可以透过使用 date 显示日期与时间,将他转为由1970-01-01 累积而来的秒数, 透过秒数相减来取得剩余的秒数后,再换算为日数即可。整个脚本的制作流程有点像这样:
1. 先让使用者输入他们的退伍日期;
2. 再由现在日期比对退伍日期;
3. 由两个日期的比较来显示『还需要几天』才能够退伍的字样。
利用『 date --date="YYYYMMDD" +%s 』转成秒数后
#!/bin/bash # Program: # You input your demobilization date, I calculate how many days before you demobilize. 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>20150716): " 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_d}*60*60*24))/60/60)) echo "You will demobilize after ${date_d} days and ${date_h} hours." fi
2、利用 case ..... esac 判断
3、利用 function 功能
因为 shell script 的执行方式是由上而下,由左而右, 因此在 shell script 当中的 function 的设定一定要在程序的最前面。
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
七、循环loop
1、while do done, until do done (不定循环)
(1)while——当 condition 条件成立时,就进行循环,直到condition 的条件不成立才停止
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."
(2)until——与 while 相反,当 condition 条件成立时,就终止循环, 否则就持续进行循环的程序段。
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——计算 1+2+3+....+100
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"
2、for...do...done (固定循环)
例2——
假设有三种动物,分别是 dog, cat, elephant 三种, 我想每一行都输出这样:『There are dogs...』之类的字样
for animal in dog cat elephant do echo "There are ${animal}s.... " done
例3——透过管线命令的 cut 捉出单纯的账号名称后,以 id 分别检查使用者的标识符与特殊参数
users=$(cut -d ':' -f1 /etc/passwd) # 撷取账号名称 for username in ${users} # 开始循环进行! do id ${username} done
例4——利用 ping这个可以判断网络状态的指令, 来进行网络状态的实际侦测时,我想要侦测的网域是本机所在的
192.168.1.1~192.168.1.100,由于有 100 台主机, 总不会要我在 for 后面输入 1 到 100 吧?此时可以这样做:
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
例5——让用户输入某个目录文件名, 然后我找出某目录内的文件名的权限。
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
3、for...do...done 的数值处理
这种语法适合于数值方式的运算当中,在 for 后面的括号内的三串内容意义为:
初始值:某个变量在循环当中的起始值,直接以类似 i=1 设定好;
限制值:当变量的值在这个限制值的范围内,就继续进行循环。例如 i<=100;
执行步阶:每作一次循环时,变量的变化量。例如 i=i+1。
值得注意的是,在『执行步阶』的设定上,如果每次增加 1 ,则可以使用类似『i++』的方式,亦即是 i 每次循环都会增加一的意思。好,我们以这种方式来进行 1 累加到使用者输入的循环吧!
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}"