欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

Bash脚本编程学习笔记07:循环结构体

程序员文章站 2022-04-29 13:04:58
本篇中涉及到算术运算,使用了$[]这种我未在官方手册中见到的用法,但是确实可用的,在此前的博文《Bash脚本编程学习笔记03:算术运算》中我有说明不要使用,不过自己忘记了。大家还是尽量使用其他的方法进行算术运算。 简介 Bash具有三种循环结构: for循环。 while循环。 untile循环。 ......

本篇中涉及到算术运算,使用了$[]这种我未在官方手册中见到的用法,但是确实可用的,在此前的博文bash脚本编程学习笔记03:算术运算中我有说明不要使用,不过自己忘记了。大家还是尽量使用其他的方法进行算术运算。

简介

bash具有三种循环结构:

  • for循环。
  • while循环。
  • untile循环。

在使用循环结构体的时候,需要注意循环的进入条件和结束条件,避免出现死循环的情况。

 

for循环

for循环又分为两种格式:遍历列表和控制变量。

遍历列表

for var in list; do
    body
done

var:变量,在每次循环时,都会被list中的元素所赋值。

list:列表,生成列表的方式有多种,后面会详述。

body:循环体,由各种各样的命令所构成,在循环体中会引用变量var。

进入循环条件:只要list中有元素即可。

离开循环条件:list中的元素遍历完毕。

list的生成方式

这里的list生成方式的分类并非官方,甚至看起来蛮重复的,大家实际敲过之后就大概能明白列表是个什么东西了。

官方的list其实就是shell展开

1、直接给出。

[root@c7-server ~]# cat for_list.sh
#!/bin/bash
for i in 1 2 3 4 5; do
    echo $i
done
[root@c7-server ~]# bash for_list.sh
1
2
3
4
5

2、通过大括号或者seq命令生成的整数列表。

[root@c7-server ~]# cat for_list.sh
#!/bin/bash
for i in {1..5}; do
    echo $i
done
[root@c7-server ~]# bash for_list.sh 
1
2
3
4
5

seq是一个外部命令,一般用于生成一个整数序列。

seq [first [increment]] last

见名知意,大家试试以下几种命令生成的整数列表就懂了。

# seq 10
# seq 1 10
# seq 2 10
# seq 1 2 10
# seq 2 2 10

seq在使用的时候要结合bash的命令替换机制,即下面要说的就是了。

3、引用返回列表的命令。

像刚才的seq其实就类似于这种,其他的例如应用ls命令的结果的也是可以。

4、glob风格的展开。

----2020-01-13----

{x..y}:当x和y是正整数的时候,相比大家都会用,需要注意的是它可以加入一个增量。

{x..y[..incr]}

示例如下:

[root@c7-server ~]# echo {1..10}
1 2 3 4 5 6 7 8 9 10
[root@c7-server ~]# echo {1..10..2}
1 3 5 7 9

5、位置参数($@, $*)的引用。

练习

1、编写一个脚本,批量创建5个用户。

#!/bin/bash

for i in zwl0{1..5}; do
    if id $i &> /dev/null; then
        echo "user $i has already exists."
    else
        useradd $i
    fi
done

2、编写一个脚本,统计100以内的整数之和、奇数之和与偶数之和。

整数之和。

#!/bin/bash

declare -i sum=0
for i in $(seq 100); do
    echo "sum is $sum, i is $i."
    sum=$[$sum+$i]
done
echo "sum is $sum."

奇数之和:只要将seq替换为“seq 1 2 100”即可。

偶数之和:只要将seq替换为“seq 2 2 100”即可。

3、编写一个脚本,判断某目录下的所有文件的类型。

#!/bin/bash

for i in /root/*; do
    file $i
done

其实file命令本身即可实现,主要是了解一下可以以通配符展开来生成list。

# file /root/*

4、计算当前所有用户的uid之和。

#!/bin/bash

declare -i sum=0
for i in $(cut -d : -f 3 /etc/passwd); do
    sum=$[$sum+$i]
done
echo "the sum of uids is $sum."

5、统计某个目录下的文本文件总数,以及文本文件的行数之和。注:无需递归,仅统计目录下第一层即可。

#!/bin/bash

if [ ! -d $1 ]; then
    echo "the file you input is not a directory,exit!"
    exit 1
fi

declare -i textcount=0
declare -i linecount=0

for i in $1/*; do
    if [ -f $i ]; then
        lines=$(wc -l $i | cut -d " " -f 1)
        textcount=$[$textcount+1]
        linecount=$[$linecount+$lines]
    fi
done

echo "the number of text file is $textcount."
echo "the number of line of text file is $linecount."

控制变量

for语句的控制变量,其实就是类似于c风格的for语句。

for ((expr1; expr2; expr3)); do
    body
done

此类for语句,只用于数值类的计算,写起来更像c语言,在(())不需要再使用$对变量进行展开,写起来更简洁方便。

expr1:只有在第一次循环时执行,一般用于对某个变量进行赋初值的操作。

expr2:每次都会执行,一般是对赋值的变量进行条件判断,为真执行body;为假的话,结束循环。

expr3:对赋值的变量进行数值调整,使其将来满足expr2为假的情况从而结束循环。

示例:计算100以内所有奇数之和。

#!/bin/bash

declare -i sum=0
for ((i=1;i<=100;i+=2)); do
    ((sum+=i))
done
echo "sum is $sum."

 

while循环

while condition; do
    cmd
done

当condition为真时,执行cmd,直到condition为假的时候才退出循环。

cmd中一般会包含一些可以在将来改变condition的判定结果的操作,否则会出现死循环。

while循环相比for循环的优势在于,for循环需要事先生成一个列表,如果列表元素比较大,例如{1..10000},那么就会占用比较多的内存空间;而while循环只需要占用一个变量的内存空间即可。(结论来自马哥,我觉得应该没错吧)

我们使用while循环来实现计算100以内的正整数之和。

[root@c7-server ~]# cat while_sum.sh
#!/bin/bash

declare -i sum=0
declare -i i=1
while [ $i -le 100 ]; do
    ((sum+=i))
    ((i++))
done

echo "sum is $sum."

几个注意事项:

后缀自增/减运算和赋值(如:+=, -=, *=, /=等等)时,涉及变量的时候不要将变量展开,否则会报错。

[root@c7-server ~]# declare -i i=1
[root@c7-server ~]# (($i++))
-bash: ((: 1++: syntax error: operand expected (error token is "+")
[root@c7-server ~]# (($i+=1))
-bash: ((: 1+=1: attempted assignment to non-variable (error token is "+=1")

同样的方式,在前缀自增/减时虽然不会报错,但是也不会达到预期的效果。

[root@c7-server ~]# ((++$i))
[root@c7-server ~]# echo $i
1

因为一旦使用了展开,bash会先进行展开,再进行算术运算。展开后就变成了。

((1++))
((++1))
((1+=1))

正确的用法是:

((i++))
((++i))
((i+=1))

赋值时,等于号右边的变量,可以不展开。以下是等效的。

((a=a+b))
((a=$a+$b))
((a+=b))
((a+=$b))

特殊用法:遍历文件内容

while循环有一种特殊的用法,可以遍历文件的内容(以行为单位),进行处理。文件内容遍历完毕后退出。

while read var; do
    body
done < /path/from/somefile

示例:打印uid为偶数的用户的名称、uid和默认shell。

#!/bin/bash

while read line; do
    username=$(echo $line | cut -d : -f 1)
    userid=$(echo $line | cut -d : -f 3)
    usershell=$(echo $line | cut -d : -f 7)
    if [ $((userid%2)) -eq 0 ]; then
        echo "user name is $username. user id is $userid. user shell is $usershell."
    fi
done < /etc/passwd

使用for+cat命令替换也可以实现类似的功能。

for i in $(cat /path/to/somefile); do
    echo $i
done

文件中的每一行也会被赋值给i,但是如果行内存在空格,那么那一行会被理解为多行。因此比较稳妥的方法还是使用while的特殊用法。

 

until循环

untile condition; do
    cmd
done

与while循环的进入循环和退出循环的逻辑正好相反。当condition为假时,执行cmd,直到condition为真的时候才退出循环。

同样,cmd中一般会包含一些可以在将来改变condition的判定结果的操作,否则会出现死循环。

until循环和while循环只是逻辑相反,因此用的比较少,while比较常用。

同样我们也使用until循环实现100以内的正整数之和的计算。

#!/bin/bash

declare -i sum=0
declare -i i=1
until [ $i -gt 100 ]; do
    ((sum+=i))
    ((i++))
done

echo "sum is $sum."

 

练习

1、使用四种循环结构体实现乘法口诀表的正向和反向打印。

for循环正向打印。

[root@c7-server ~]# cat for_jiujiu.sh 
#!/bin/bash

for i in {1..9}; do
    for j in $(seq $i); do
        echo -ne "$j*$i=$((i*j))\t"
    done
    echo
done
[root@c7-server ~]# bash for_jiujiu.sh
1*1=1    
1*2=2    2*2=4    
1*3=3    2*3=6    3*3=9    
1*4=4    2*4=8    3*4=12    4*4=16    
1*5=5    2*5=10    3*5=15    4*5=20    5*5=25    
1*6=6    2*6=12    3*6=18    4*6=24    5*6=30    6*6=36    
1*7=7    2*7=14    3*7=21    4*7=28    5*7=35    6*7=42    7*7=49    
1*8=8    2*8=16    3*8=24    4*8=32    5*8=40    6*8=48    7*8=56    8*8=64    
1*9=9    2*9=18    3*9=27    4*9=36    5*9=45    6*9=54    7*9=63    8*9=72    9*9=81

这里需要注意的一点,是:

for j in $(seq $i)

不可以换成

for j in {1..$i}

具体示例如下。

[root@c7-server ~]# echo {1..5}
1 2 3 4 5
[root@c7-server ~]# declare -i i=5
[root@c7-server ~]# echo {1..$i}
{1..5}

造成此结果的原因,在官方的关于大括号展开中有提及。

brace expansion is performed before any other expansions, and any characters special to other expansions are preserved in the result. it is strictly textual. bash does not apply any syntactic interpretation to the context of the expansion or the text between the braces.

for循环的反向打印,只要将{1..9}换成{9..1}即可。

[root@c7-server ~]# bash for_jiujiu.sh
1*9=9    2*9=18    3*9=27    4*9=36    5*9=45    6*9=54    7*9=63    8*9=72    9*9=81    
1*8=8    2*8=16    3*8=24    4*8=32    5*8=40    6*8=48    7*8=56    8*8=64    
1*7=7    2*7=14    3*7=21    4*7=28    5*7=35    6*7=42    7*7=49    
1*6=6    2*6=12    3*6=18    4*6=24    5*6=30    6*6=36    
1*5=5    2*5=10    3*5=15    4*5=20    5*5=25    
1*4=4    2*4=8    3*4=12    4*4=16    
1*3=3    2*3=6    3*3=9    
1*2=2    2*2=4    
1*1=1

c风格的for循环的正向打印。

#!/bin/bash

for ((i=1;i<=9;i++)); do
    for ((j=1;j<=i;j++)); do
        echo -ne "$j*$i=$((i*j))\t"
    done
    echo
done

c风格的for循环的反向打印。

#!/bin/bash

for ((i=9;i>=1;i--)); do
    for ((j=1;j<=i;j++)); do
        echo -ne "$j*$i=$((i*j))\t"
    done
    echo
done

while循环正向打印。

#!/bin/bash

declare -i i=1
while [ $i -le 9 ]; do
    declare -i j=1
    while [ $j -le $i ]; do
        echo -ne "$j*$i=$((i*j))\t"
        ((j++))
    done
    ((i++))
    echo
done

while循环反向打印。

#!/bin/bash

declare -i i=9
while [ $i -gt 0 ]; do
    declare -i j=1
    while [ $j -le $i ]; do
        echo -ne "$j*$i=$((i*j))\t"
        ((j++))
    done
    ((i--))
    echo
done

until循环正向打印。

#!/bin/bash

declare -i sum=0
declare -i i=1
until [ $i -gt 9 ]; do
    declare -i j=1
    until [ $j -gt $i ]; do
        echo -ne "$j*$i=$((i*j))\t"
        ((j++))
    done
    ((i++))
    echo
done

until循环反向打印。

#!/bin/bash

declare -i sum=0
declare -i i=9
until [ $i -le 0 ]; do
    declare -i j=1
    until [ $j -gt $i ]; do
        echo -ne "$j*$i=$((i*j))\t"
        ((j++))
    done
    ((i--))
    echo
done

 

循环控制指令

此前讲解循环时,循环的每一轮,都是要执行的,直到循环退出条件满足时才退出。而循环控制指令continue和break,可以改变这种默认的机制。

continue:结束本轮循环,进入下一轮循环。大致结构如下。

for i in list; do
    cmd1
    ...
    if test; then
        continue
    fi
    cmdn
    ...
done

进入下一轮循环的条件是本轮循环的body部分全部执行完,因此continue不应该放在整个body的末尾,否则就有点画蛇添足了。

continue一般放在body的中间,结合某些判断跳出本轮循环。例如,当list是100以内的所有正整数时,求所有偶数之和。

#!/bin/bash

declare -i sum=0
for i in {1..100}; do
    if [ $((i%2)) -eq 1 ]; then
        continue
    fi
    ((sum+=i))
done
echo "sum is $sum."

break:直接结束所有的循环,继续执行脚本剩余的部分。这里要注意和exit区分,如果break出现的位置换成了exit的话,那么exit结束的是整个脚本,而不仅仅是循环而已,脚本直接退出了,不会执行脚本剩余部分。

break一般会结合死循环一起使用,死循环一般会结合sleep命令一起使用。

sleep命令基本语法。

sleep n[suffix]

n:具体的数值,默认单位是秒。

suffix:后缀,表示单位。s秒、n分钟、h小时、d天数。

大致的语法就是这个样子,整个循环是一个死循环。sleep控制了死循环的循环间隔,防止消耗资源过多;if+break实现了对死循环的控制,达到某个条件就退出。

while true; do
    cmd1
    ...
    if test; then
        break
    fi
    [cmdn
    ...]
    sleep ...
done

使用死循环+break求100以内所有奇数之和。

#!/bin/bash

declare -i i=1
declare -i sum=0
while true; do
    if [ $i -gt 100 ]; then
        break
    fi
    ((sum+=i))
    ((i+=2))
done
echo "sum is $sum."

练习:每隔3秒监控系统中已登录的用户,如果发现alongdidi则记录于日志中并退出脚本。

思路一:死循环监控

[root@c7-server ~]# cat user_monitor.sh
#!/bin/bash

while true; do
    if who | grep "^alongdidi\>" &> /dev/null; then
        echo "$(date +"%f %t") alongdidi logined." >> /var/log/user_monitor.log
        break
    else
        sleep 3
    fi
done
[root@c7-server ~]# cat /var/log/user_monitor.log 
2020-01-14 15:52:32 alongdidi logined.

思路二:直到alongdidi登录,否则继续循环。

#!/bin/bash

until who | grep "^alongdidi\>" &> /dev/null; do
    sleep 3
done
echo "$(date +"%f %t") alongdidi logined." >> /var/log/user_monitor.log

continue和break在使用的时候可以带上参数n。

continue n:跳出n轮循环。

break n:结束几个循环体。这种在嵌套循环的情况下才会遇到。

while ...; do
    while ...; do
        break 2
    done
done