Linux——Shell脚本

重头戏终于来了!

一、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: 』的内容。

Linux——Shell脚本_第1张图片



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}"

Linux——Shell脚本_第2张图片


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中的变量是无法显示内容的。

Linux——Shell脚本_第3张图片

2、利用 source 来执行脚本:在父程序中执行——用source执行script时,可以用echo显示变量的内容。

Linux——Shell脚本_第4张图片


五、善用判断式

1、利用 test 指令的测试功能

Linux——Shell脚本_第5张图片

Linux——Shell脚本_第6张图片

Linux——Shell脚本_第7张图片

Linux——Shell脚本_第8张图片

例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 的判断式时,必须要注意中括号的两端需要有空格符来分隔。

Linux——Shell脚本_第9张图片

所以说,最好要注意:

  • 在中括号 [] 内的每个组件都需要有空格键来分隔;
  • 在中括号内的变数,最好都以双引号括号起来;
  • 在中括号内的常数,最好都以单或双引号括号起来。

中括号比较常用在条件判断式 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 能不能在脚本文件名后面带有参数呢?

举例来说,如果你想要重新启动系统的网络,可以这样做:

Linux——Shell脚本_第10张图片

上面的指令可以『重新启动 /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   ==> '$@'"
Linux——Shell脚本_第11张图片

六、条件判断式

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)多重、复杂条件判断式

Linux——Shell脚本_第12张图片

        要注意的是, 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)

Linux——Shell脚本_第13张图片

上面的重点是『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
Linux——Shell脚本_第14张图片

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 的条件不成立才停止

Linux——Shell脚本_第15张图片

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 条件成立时,就终止循环, 否则就持续进行循环的程序段。

Linux——Shell脚本_第16张图片

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 (固定循环)

Linux——Shell脚本_第17张图片

例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 的数值处理

Linux——Shell脚本_第18张图片

这种语法适合于数值方式的运算当中,在 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}"

八、shell script 的追踪与 debug

Linux——Shell脚本_第19张图片

你可能感兴趣的:(测试开发)