Shell编程——while循环和until循环

1、当型和直到型循环语法

1.1、while循环语句

while循环语句的基本语法为:

while <条件表达式>
do
    指令...
done

while循环语句会对紧跟在while命令后的条件表达式进行判断,如果该条件表达式成立,则执行while循环体里的命令或语句(即语法中do和done之间的指令)​,每一次执行到done时就会重新判断while条件表达式是否成立,直到条件表达式不成立时才会跳出while循环体。如果一开始条件表达式就不成立,那么程序就不会进入循环体(即语法中do和done之间的部分)中执行命令了。

while循环执行流程对应的逻辑图如图所示。
在这里插入图片描述

1.2、until循环语句

until循环语句的语法为:

until <条件表达式>
do
    指令...
done

until循环语句的用法与while循环语句的用法类似,区别是until会在条件表达式不成立时,进入循环执行指令;条件表达式成立时,终止循环。until的应用场景很罕见,读者了解下即可,本书不会深入探讨此循环语句。

2、当型和直到型循环的基本范例

首先来了解一下Shell中的两个休息命令:sleep 1表示休息1秒,usleep 1000000也表示休息1秒。

范例:每隔2秒输出一次系统负载(负载是系统性能的基础重要指标)情况。

参考答案1:每隔2秒在屏幕上输出一次负载值。

[root@oldboy scripts]# cat 10_1_1.sh
#! /bin/sh
while true         #<==提示:while  true  表示条件永远为真,因此会一直运行,像死循环一样,我们称之为守护进程。
do
    uptime
    sleep 2        #<==休息2秒后继续循环,在while  true循环中最好增加类似sleep的命令,目的是控制循环的频率,否则会消耗大量系统资源,那样就真成了名副其实的死循环了。
done

执行结果如下:

[root@oldboy scripts]# sh 10_1_1.sh
11:04:22 up 14:52,   1 user,   load average: 0.00, 0.00, 0.00
11:04:24 up 14:52,   1 user,   load average: 0.00, 0.00, 0.00
11:04:26 up 14:52,   1 user,   load average: 0.00, 0.00, 0.00
11:04:28 up 14:52,   1 user,   load average: 0.00, 0.00, 0.00

参考答案2:将负载值追加到log里,使用微秒单位。

[root@oldboy scripts]# cat 10_1_2.sh
#! /bin/sh
while [ 1 ]                    #==>这里的条件和上面的写法有区别,注意[]里面两端要有空格,true和1都表示条件永远成立。
do
    uptime >>/tmp/uptime.log  #==>将负载值输入到log文件里,注意,此处最好写绝对路径。
    usleep 2000000              #==>这里的单位是微秒,其实结果也是两秒。
done

通过在脚本的结尾使用&符号来在后台运行脚本:

[root@oldboy scripts]# sh 10_1_2.sh &
[1] 14318
[root@oldboy scripts]# tail -f /tmp/uptime.log     #==>使用tail命令实时观察输出结果。
11:08:35 up 14:57,   1 user,   load average: 0.00, 0.00, 0.00
11:08:37 up 14:57,   1 user,   load average: 0.00, 0.00, 0.00
11:08:39 up 14:57,   1 user,   load average: 0.00, 0.00, 0.00
11:08:41 up 14:57,   1 user,   load average: 0.00, 0.00, 0.00

在实际工作中,一般会通过客户端SSH连接服务器,因此可能就会有在脚本或命令执行期间不能中断的需求,若中断,则会前功尽弃,更要命的是会破坏系统数据。下面是防止脚本执行中断的几个可行方法:

  • 使用sh /server/scripts/while_01.sh &命令,即使用&在后台运行脚本。
  • 使用nohup /server/scripts/uptime.sh &命令,即使用nohup加&在后台运行脚本。
  • 利用screen保持会话,然后再执行命令或脚本,即使用screen保持当前会话状态。

3、让Shell脚本在后台运行的知识

有关脚本运行的相关用法和说明见下表。

在这里插入图片描述
下面针对表中的知识进行实践演示,如下:

[root@oldboy scripts]# sh 10_1_2.sh &   #==>结尾使用&符号表示在后台运行脚本。
[1] 14318
[root@oldboy scripts]# fg             #==>执行fg命令将脚本放到前台执行, 如果有多个脚本任务,则可以使用fg加jobs输出中的任务编号调出对应编号的脚本到前台。
sh 10_1_2.sh
^Z                                    #==>执行Ctrl+z快捷键出现如下结果,临时暂停执行脚本。
[1]+   Stopped                      sh 10_1_2.sh
[root@oldboy scripts]# bg             #==>将当前执行的脚本放到后台运行。
[1]+ sh 10_1_2.sh &
[root@oldboy scripts]# jobs           #==>查看当前Shell下运行的脚本任务。
[1]+   Running                      sh 10_1_2.sh &
[root@oldboy scripts]# fg 1           #==>可以使用fg加jobs输出中的任务编号调出对应编号的脚本到前台来运行。
sh 10_1_2.sh
^C                                    #==>当脚本在前台运行时,可以执行Ctrl+c快捷键停止脚本运行。
[1]+   Stopped                      sh while-1-1.sh

以下是使用kill命令关闭jobs任务脚本的示例。

[root@oldboy ~]# jobs
[1]-   Running                      sh while1.sh &
[2]+   Running                      sh while1.sh &
[root@oldboy ~]# kill %2                            #<==注意任务编号的写法。
[root@oldboy ~]# jobs
[1]-   Running                      sh while1.sh &
[2]+   Terminated                   sh while1.sh     #<==脚本已关闭。

更多有关进程管理的Linux相关命令如下。

  • kill、killall、pkill:杀掉进程。
  • ps:查看进程。
  • pstree:显示进程状态树。
  • top:显示进程。
  • renice:改变优先权。
  • nohup:用户退出系统之后继续工作。
  • pgrep:查找匹配条件的进程。
  • strace:跟踪一个进程的系统调用情况。
  • ltrace:跟踪进程调用库函数的情况。

范例:使用while循环竖向打印54321。

参考答案1:

[root@oldboy scripts]# cat 10_2_1.sh
#! /bin/sh
i=5                          #<==因为是从大到小打印,所以初始化i的值为5。
while ((i>0))               #<==双小括号条件表达式的用法,当i大于0不成立时就跳出循环。
do
    echo  "$i"               #<==打印变量i的值。
    ((i--))                  #<==i的值自减,每次自减1。
done

执行结果如下:

[root@oldboy scripts]# sh 10_2_1.sh
5
4
3
2
1

参考答案2:使用双中括号条件表达式。

#! /bin/sh
i=5
while [[ $i0 ]]                          #<==双中括号条件表达式的用法。
do
    echo $i
    ((i--))
done

参考答案3:使用脚本传参需要打印的数字。

i="$1"                                        #<==使用i接收脚本传参。
while [ $i -gt 0 ]                            #<==单中括号条件表达式的用法。
do
    echo $i
    ((i--))
done

执行结果如下:

[root@oldboy scripts]# sh 10_2_3.sh 5   #<==也可以传入其他数字
5
4
3
2
1

参考答案4:使用until命令。

[root@oldboy scripts]# cat 10_2_4.sh
#! /bin/sh
i=5
until [[ $i1 ]]                          #<==当条件表达式不成立时,进入循环执行命令,因为5<1不成立,因此进入循环执行命令。
do
   echo $i
   ((i--))
done

执行结果如下:

[root@oldboy scripts]# sh 10_2_4.sh
5
4
3
2
1

范例:计算从1加到100之和(请用1+2+3+…+100的方法)​。

参考答案:

[root@oldboy scripts]# cat 10_3_1.sh
#! /bin/bash
# this script is created by oldboy.
i=1                 #<==i为自增的变量,从1到100,初始值为1。
sum=0               #<==总和变量初始值为0。
while ((i<=100))   #<==条件是i小于等于100,也就是从1加到100。
do
    ((sum=sum+i)) #<==把i的值和当前sum总和的值做加法并将结果重新赋值给sum。
    ((i++))         #<==i每次自增1。
done
[ "$sum" -ne 0 ] && printf "totalsum is: $sum\n" #<==打印总和。

执行结果如下:

[root@oldboy scripts]# sh 10_3_1.sh
totalsum is: 5050

下面是通过数学公式计算的结果:

[root@oldboy scripts]# cat 10_3_2.sh
#! /bin/sh
i=100
((sum=i*(i+1)/2))         #<==利用数学公式进行计算,效率很高。
echo $sum
[root@oldboy scripts]# sh 10_3_2.sh
5050

4、企业生产实战:while循环语句实践

范例:使用while守护进程的方式监控网站,每隔10秒确定一次网站是否正常。

[root@oldboy scripts]# cat 10_6_1.sh
#! /bin/sh
if [ $# -ne 1 ]; then               #<==判断,若传参的个数不为1,
    echo $"usage $0 url"            #<==则打印正确使用提示。
    exit 1                          #<==以返回值1退出脚本。
fi
while true                          #<==永远为真,进入while循环。
do
    if [ `curl -o /dev/null --connect-timeout 5 -s -w "%{http_code}"   $1|
egrep -w "200|301|302"|wc -l` -ne 1 ]
    #<==对传入的URL参数获取状态码,过滤200、301、302任意之一转为数字,如果不等于1,则表示状态信息不对。
      then
        echo "$1 is error."         #<==提示URL访问错误。
        #echo "$1 is error."|mail -s "$1 is error." 31333741--@qq.com
                                    #<==发送邮件报警。
    else
        echo "$1 is ok"             #<==否则,提示URL访问OK。
    fi
    sleep 10   #<==休息10秒继续执行while循环,注意,当while后面有true等永远为真的条件时,一般在循环里要有退出循环的条件或类似sleep休息的命令,否则会大量消耗系统资源。
done

执行结果如下:

[root@oldboy scripts]# sh 10_6_1.sh http://www.oldboyedu.com
http://www.oldboyedu.com is ok    #<==每隔10秒检查一次,提示正常。
http://www.oldboyedu.com is ok    #<==每隔10秒检查一次,提示正常。
[root@oldboy scripts]# sh 10_6_1.sh http://www.abcdefg.com #<==换个不存在的地址。
http://www.abcdefg.com is error. #<==每隔10秒检查一次,提示异常。
http://www.abcdefg.com is error. #<==每隔10秒检查一次,提示异常。

范例:使用while守护进程的方式监控网站,每隔10秒确定一次网站是否正常。

参考答案1:引入函数库并且采用模拟用户访问的方式。

[root@oldboy scripts]# cat 10_7_1.sh
#! /bin/sh
. /etc/init.d/functions           #<==这里和上一个脚本不同,引入了函数库。
if [ $# -ne 1 ]; then
   echo $"usage $0 url"
   exit 1
fi
while true
do
   if [ `curl -o /dev/null --connect-timeout 5 -s -w "%{http_code}"   $1|
egrep -w "200|301|302"|wc -l` -ne 1 ]
     then
       action "$1 is error." /bin/false  #<==这里和上一个脚本不同,输出显示更专业了。
       #echo "$1 is error."|mail -s "$1 is error." 31333741--@qq.com
   else
       action "$1 is ok" /bin/true     #<==这里和上一个脚本不同,输出显示更专业了。
   fi
   sleep 10
done

执行结果见下图。
在这里插入图片描述
参考答案2:采用Shell数组法,同时检测多个URL是否正常,并给出专业的展示效果,这是实际工作中所用的脚本。

[root@oldboy scripts]# cat 10_7_2.sh
#! /bin/bash
# this script is created by oldboy.
# e_mail:31333741@qq.com
# function:case example
# version:1.3
. /etc/init.d/functions
check_count=0
url_list=(                  #<==定义检测的URL数组,包含多个URL地址。
http://blog.oldboyedu.com
http://blog.etiantian.org
http://oldboy.blog.51cto.com
http://10.0.0.7
)
function wait()             #<==定义3,2,1倒计时函数。
{
    echo -n '3秒后,执行检查URL操作.';
    for ((i=0; i<3; i++))
    do
        echo -n "."; sleep 1
    done
    echo
}
function check_url()        #<==定义检测URL的函数。
{
    wait                    #<==执行倒计时函数。
    for ((i=0; i<`echo ${#url_list[*]}`; i++)) #<==循环数组元素。
    do
        wget -o /dev/null -T 3 --tries=1 --spider ${url_list[$i]} >/dev/null 2&1
                                                       #<==检测是否可以访问数组元素的地址。
        if [ $? -eq 0 ]                                #<==如果返回值为0,则表示访问成功。
          then
            action "${url_list[$i]}" /bin/true #<==优雅地显示成功结果。
        else
            action "${url_list[$i]}" /bin/false #<==优雅地显示失败结果。
        fi
    done
    ((check_count++))                                  #<==检测次数加1。
}
main(){                                                #<==定义主函数。
    while true                                         #<==开启一个持续循环。
    do
        check_url                                      #<==加载检测url的函数。
        echo "-------check count:${check_count}---------"
        sleep 10                                       #<==间歇10秒。
    done
}
main                                                   #<==调用主函数运行程序。

执行结果见下图。
在这里插入图片描述

5、while循环按行读文件的方式总结

方式1:采用exec读取文件,然后进入while循环处理。

exec <FILE
sum=0
while read line
do
    cmd
done

方式2:使用cat读取文件内容,然后通过管道进入while循环处理。

cat FILE_PATH|while read line
do
    cmd
done

方式3:在while循环结尾done处通过输入重定向指定读取的文件。

while read line
do
    cmd
done<FILE

范例:开发一个Shell脚本实现Linux系统命令cat读文件的基本功能。

[root@oldboy scripts]# cat 10_9_1.sh
while read line
do
   echo $line
done<$1

执行结果如下:

[root@oldboy scripts]# sh 10_9_1.sh /etc/hosts
127.0.0.1  localhost  localhost.localdomain  localhost4  localhost4.
localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

6、企业级生产高级实战案例

范例:写一个Shell脚本解决类DDoS攻击的生产案例。请根据Web日志或系统网络连接数,监控某个IP的并发连接数,若短时内PV达到100,即调用防火墙命令封掉对应的IP。防火墙命令为:​“iptables -I INPUT -s IP地址-j DROP”​。

参考答案1:

先分析Web日志,可以每分钟或每小时分析一次,这里给出按小时处理的方法。可以将日志按小时进行分割,分成不同的文件,根据分析结果把PV数高的单IP封掉。例如,每小时单IP的PV数超过500,则即刻封掉,这里简单地把日志的每一行近似看作一个PV,实际工作中需要计算实际页面的数量,而不是请求页面元素的数量,另外,很多公司都是以NAT形式上网的,因此每小时单IP的PV数超过多少就会被封掉,还要根据具体的情况具体分析,本题仅给出一个实现的案例,读者使用时需要考虑自身网站的业务去使用。

解答如下:

[root@oldboy scripts]# cat 10_10_1.sh
file=$1                     #<==定义一个变量接收命令行传参,参数为日志文件类型。
while true
do
    awk '{print $1}' $1|grep -v "^$"|sort|uniq -c >/tmp/tmp.log
    #<==分析传入的日志文件,并在排序去重后追加到一个临时文件里。
    exec </tmp/tmp.log     #<==读取上述临时文件。
    while read line         #<==进入while循环处理。
    do
        ip=`echo $line|awk '{print $2}'`          #<==获取文件中的每一行的第二列。
        count=`echo $line|awk '{print $1}'`       #<==获取文件中的每一行的第一列。
        if [ $count -gt 500 ] && [ `iptables -L -n|grep "$ip"|wc -l` -lt 1 ]
        #<==如果PV数大于500,并且防火墙里没有封过此IP。
          then
            iptables -I INPUT -s $ip -j DROP      #<==则封掉PV数大于500的IP。
            echo "$line is dropped" >>/tmp/droplist_$(date +%F).log
                                                  #<==记录处理日志。
        fi
    done
    sleep 3600  #<==读者可以按分钟进行分析,不过日志的分割或过滤也得按分钟才行。
done

执行结果如下:

[root@oldboy scripts]# sh 10_10_1.sh access.log

单独打开窗口查看iptables的情况,结果如下:

[root@oldboy ~]# iptables -L -n
Chain INPUT (policy ACCEPT)
target       prot opt source                    destination
DROP         all  --  59.33.26.105           0.0.0.0/0      #<==这就是脚本封掉的IP。
DROP         all  --  124.115.4.18           0.0.0.0/0      #<==这就是脚本封掉的IP。

Chain FORWARD (policy ACCEPT)
target       prot opt source                    destination

Chain OUTPUT (policy ACCEPT)
target       prot opt source                    destination

参考答案2:

分析Linux系统的网络连接数,而不是分析Web日志。

设计思路:

首先要分析单IP占网络连接数的情况,即取当前网络连接状态为ESTABLISHED的行数,然后分析对应客户端列不同IP连接数量的排序,对排序比较高的IP进行封堵。

以下是待用的模拟数据:

 [root@oldboy scripts]# sed -n '20,30p' netstat.log
 tcp     0     0     115.29.49.213:80        120.237.97.10:54195       ESTABLISHED
 tcp     0     0     115.29.49.213:80        49.80.146.230:13453       FIN_WAIT2
 tcp     0     0     115.29.49.213:80        113.104.25.50:56714       FIN_WAIT2
 tcp     0     0     115.29.49.213:80        101.226.89.193:41639      ESTABLISHED
 tcp     0     0     115.29.49.213:80        119.147.225.185:58321     TIME_WAIT
 tcp     0     0     115.29.49.213:80        54.183.177.237:64129      TIME_WAIT
 tcp     0     0     115.29.49.213:80        120.198.202.48:41960      ESTABLISHED
 tcp     0     1     115.29.49.213:80        119.127.188.242:38843     FIN_WAIT1
 tcp     0     0     115.29.49.213:34081     223.4.9.70:80             TIME_WAIT
 tcp     0     0     115.29.49.213:80        58.223.4.14:46716         FIN_WAIT2
 tcp     0     0     115.29.49.213:80        122.90.74.255:12177       FIN_WAIT2

实现对应客户端列不同的IP连接数量排序的方法有:

[root@oldboy  scripts]#  grep  "ESTABLISHED"  netstat.log|awk  -F  "[  :]+"
'{print $(NF-3)}'|sort|uniq -c|sort -rn -k1|head -5
     4 118.242.18.177
     3 123.6.8.223
     3 114.250.252.127
     2 123.244.104.42
     2 121.204.108.160
[root@oldboy  scripts]#  grep  "ESTABLISHED"  netstat.log|awk  -F  "[  :]+"  '{
++S[$(NF-3)]}END {for(key in S) print S[key], key}'|sort -rn -k1|head -5
     4 118.242.18.177
     3 123.6.8.223
     3 114.250.252.127
     2 123.244.104.42
     2 121.204.108.160

在命令行中采用如下命令封掉IP:

iptables -I INPUT -s 121.204.108.160 -j DROP

让程序每3分钟执行一次,这里使用while守护进程的方式来实现。

参考答案1:

[root@oldboy scripts]# cat 10_10_2.sh
#! /bin/sh
file=$1             #<==定义一个变量以接收命令行传参,参数为日志文件类型。实际工作中可以先使用netstat命令将网络状态生成临时文件,然后再传参给$1。
if expr "$file" : ".*\.log" &>/dev/null #<==expr的用法,判断扩展名是否以.log结尾。
  then
    :                                #<==这个冒号表示什么都不做。
else
    echo $"usage:$0 xxx.log"         #<==对不符合扩展名类型的给出正确提示。
    exit 1                           #<==退出脚本。
fi

while true
do
    grep "ESTABLISHED" $1|awk -F "[ :]+" '{ ++S[$(NF-3)]}END {for(key in S)
print S[key], key}'|sort -rn -k1|head -5 >/tmp/tmp.log  #<==分析传入的网络状态日志,获取到客户端IP列的信息,并去重排序。
    while read line                              #<==读取去重后的/tmp/tmp.log文件。
    do
        ip=`echo $line|awk '{print $2}'`         #<==获取文件中的每一行的第二列。
        count=`echo $line|awk '{print $1}'` #<==获取文件中的每一行的第一列。
        if [ $count -gt 500 ] && [ `iptables -L -n|grep "$ip"|wc -l` -lt 1 ]
        #<==如果PV数大于500,并且防火墙里没有封过此IP,可通过设置小的值测试。
          then
            iptables -I INPUT -s $ip -j DROP  #<==则封掉PV数大于500的IP。
            echo "$line is dropped" >>/tmp/droplist_$(date +%F).log
        #<==记录处理日志。
        fi
    done</tmp/tmp.log     #<==读入临时日志文件,即使是放在结尾,也是在进入循环的时候就读入了。
    sleep 180
done

参考答案2:更专业的脚本(分模块实现)

[root@oldboy scripts]# cat 10_10_3.sh
#! /bin/sh
file=$1
JudgeExt(){                  #<==定义判断扩展名的函数,可以传入日志文件。
    if expr "$1" : ".*\.log" &>/dev/null
      then
        :
    else
        echo $"usage:$0 xxx.log"
        exit 1
    fi
}
IpCount(){                   #<==分析日志,对访问的IP去重排序。
grep  "ESTABLISHED"  $1|awk  -F  "[  :]+"  '{  ++S[$(NF-3)]}END  {for(key  in  S)
print S[key], key}'|sort -rn -k1|head -5 >/tmp/tmp.log
}
ipt(){                       #<==防火墙封堵函数。
    local ip=$1
    if [ `iptables -L -n|grep "$ip"|wc -l` -lt 1 ]
      then
        iptables -I INPUT -s $ip -j DROP
        echo "$line is dropped" >>/tmp/droplist_$(date +%F).log
    fi
}
main(){                              #<==主函数main。
    JudgeExt $file                   #<==传入日志判断扩展名。
    while true
    do
        IpCount $file                #<==对传入日志文件排序去重。
        while read line
        do
            ip=`echo $line|awk '{print $2}'`
            count=`echo $line|awk '{print $1}'`
            if [ $count -gt 3 ]      #<==去重后检测,若某IP超过3次,则封堵,实际工作中可以调整成你需要的阈值。
              then
                ipt $ip              #<==封掉对应的IP。
            fi
        done</tmp/tmp.log
        sleep 180
    done
}
main

7、小结

  • (1)While循环结构及相关语句综合实践小结
    • while循环的特长是执行守护进程,以及实现我们希望循环持续执行不退出的应用,适合用于频率小于1分钟的循环处理,其他的while循环几乎都可以被后面即将要讲到的for循环及定时任务crond功能所替代。
    • case语句可以用if语句来替换,而在系统启动脚本时传入少量固定规则字符串的情况下,多用case语句,其他普通判断多用if语句。
    • 一句话场景下,if语句、for语句最常用,其次是while(守护进程)​、case(服务启动脚本)​。
  • (2)Shell脚本中各个语句的使用场景
    • 条件表达式,用于简短的条件判断及输出(文件是否存在,字符串是否为空等)
    • if取值判断,多用于不同值数量较少的情况。
    • for最常用于正常的循环处理中。
    • while多用于守护进程、无限循环(要加sleep和usleep控制频率)场景。
    • case多用于服务启动脚本中,打印菜单可用select语句,不过很少见,一般用cat的here文档方法来替代。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值