侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

Shell脚本编程(6)之循环

zze
zze
2019-12-23 / 0 评论 / 0 点赞 / 516 阅读 / 13068 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

本部分内容参考自《Linux命令行与shell脚本编程大全 第3版》。

for命令

重复执行一系列命令在编程中很常见。通常你需要重复一组命令直至达到某个特定条件,比如处理某个目录下的所有文件、系统上的所有用户或是某个文本文件中的所有行。
bash shell 提供了 for 命令,允许你创建一个遍历一系列值的循环。每次迭代都使用其中一个值来执行已定义好的一组命令。下面是 bash shell 中 for 命令的基本格式。

for var in list
do
    commands
done

list 参数中,你需要提供迭代中要用到的一系列值。可以通过几种不同的方法指定列表中的值。
在每次迭代中,变量 var 会包含列表中的当前值。第一次迭代会使用列表中的第一个值,第二次迭代使用第二个值,以此类推,直到列表中的所有值都过一遍。
dodone 语句之间输入的命令可以是一条或多条标准的 bash shell 命令。在这些命令中,$var 变量包含着这次迭代对应的当前列表项中的值。

更改字段分隔符

现有包含如下内容的一个文本文件:

Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
Visit beautiful Arkansas
Visit beautiful Colorado
Visit beautiful Connecticut
Visit beautiful Delaware
Visit beautiful Florida
Visit beautiful Georgia
Visit beautiful New York
Visit beautiful New Hampshire
Visit beautiful North Carolina

我们可以使用 for 循环输出该文件的内容:

$ cat test18.sh 
#!/bin/bash
file="test1.txt"
for state in $(cat $file)
do
	echo $state
done

结果如下:

$ ./test18.sh 
Visit
beautiful
Alabama
Visit
beautiful
Alaska
Visit
...
North
Carolina

可以看到 for 循环默认以空格为分隔符,遍历输出了每一个单词,那如果我们想要每次输出一行的内容该怎么做?
这里有一个特殊的环境变量 IFS,叫作内部字段分隔符(internal field separator)。IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。默认情况下, bash shell 会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

如果 bash shell 在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。
要解决这个问题,可以在 Shell 脚本中临时更改 IFS 环境变量的值来限制被 bash shell 当作字段分隔符的字符。例如,如果你想修改 IFS 的值,使其只能识别换行符,那就必须这么做:

IFS=$'\n'

将这个语句加入到脚本中,告诉 bash shell 在数据值中忽略空格和制表符。对前一个脚本使用这种方法,将获得如下输出。

$ cat test18.sh 
#!/bin/bash
file="test1.txt"
IFS=$'\n'
for state in $(cat $file)
do
	echo $state
done
$ ./test18.sh 
Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
...
Visit beautiful New Hampshire
Visit beautiful North Carolina

现在, shell 脚本旧能够使用列表中含有空格的值了。
还有其他一些 IFS 环境变量的绝妙用法。假定你要遍历一个文件中用冒号分隔的值(比如在 /etc/passwd 文件中)。你要做的就是将 IFS 的值设为冒号。

IFS=:

如果要指定多个 IFS 字符,只要将它们在赋值行串起来就行。

IFS=$'\n':;"

这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。如何使用 IFS 字符解析数据没有任何限制。

C风格的for命令

如果你从事过 C 语言编程,可能会对 bash shell 中 for 命令的工作方式有点惊奇。在 C 语言中,for 循环通常定义一个变量,然后这个变量会在每次迭代时自动改变。通常程序员会将这个变量用作计数器,并在每次迭代中让计数器增一或减一。bash 的 for 命令也提供了这个功能。
bash shell 也支持一种 for 循环,它看起来跟 C 语言风格的 for 循环类似,但有一些细微的不同,
其中包括一些让 shell 脚本程序员困惑的东西。以下是 bash 中 C 语言风格的 for 循环的基本格式。

for (( variable assignment ; condition ; iteration process ))

C 语言风格的 for 循环的格式会让 bash shell 脚本程序员摸不着头脑,因为它使用了 C 语言风格的变量引用方式而不是 shell 风格的变量引用方式。 C 语言风格的 for 命令看起来如下。

for (( a = 1; a < 10; a++ ))

注意,有些部分并没有遵循 bash shell 标准的 for 命令:

  • 变量赋值可以有空格;
  • 条件中的变量不以美元符开头;
  • 迭代过程的算式未用expr命令格式;

看如下示例:

$ cat test1 
#!/bin/bash
for (( i=1; i <= 10; i++ ))
do
echo "The next number is $i"
done
$ ./test1 
The next number is 1
The next number is 2
The next number is 3
The next number is 4
The next number is 5
The next number is 6
The next number is 7
The next number is 8
The next number is 9
The next number is 10

for 循环通过定义好的变量(本例中是变量 i)来迭代执行这些命令。在每次迭代中,$i 变量包含了 for 循环中赋予的值。在每次迭代后,循环的迭代过程会作用在变量上,在本例中,变量增一。

while命令

while 命令某种意义上是 if-then 语句和 for 循环的混杂体。 while 命令允许定义一个要测试的命令,然后循环执行一组命令,只要定义的测试命令返回的是退出状态码 0。它会在每次迭代的一开始测试 test 命令。在 test 命令返回非零退出状态码时, while 命令会停止执行那组命令。
while 命令的格式如下:

while test command
do
    other commands
done

while 命令中定义的 test commandif-then 语句中的格式一模一样。可以使用任何普通的 bash shell 命令,或者用 test 命令进行条件测试,比如测试变量值。
while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中运行的命令而改变。如果退出状态码不发生变化, while 循环就将一直不停地进行下去。
最常见的 test command 的用法是用方括号来检查循环命令中用到的 shell 变量的值。

$ cat test1.sh 
#!/bin/bash
var1=10
while [ $var1 -gt 0 ]
do
	echo $var1
	var1=$[ $var1 - 1 ]
done
$ ./test1.sh 
10
9
8
7
6
5
4
3
2
1

while 命令定义了每次迭代时检查的测试条件:

while [ $var1 -gt 0 ]

只要测试条件成立,while 命令就会不停地循环执行定义好的命令。在这些命令中,测试条件中用到的变量必须修改,否则就会陷入无限循环。在本例中,我们用 shell 算术来将变量值减一:

var1=$[ $var1 - 1 ]

while 循环会在测试条件不再成立时停止。

while循环的特殊用法

while 循环可以很方便的遍历文件的每一行,语法如下:

while read line;do
    循环体
done < file

该语法示例的含义就是从 file 文件中每次读一行,每一行的内容保存到 line 变量。
例:输出/etc/passwd 文件中 id 为偶数的用户的 id 和用户名。

#!/bin/bash
IFS=":";
while read line;do
	echo $line;
done < /etc/passwd

until命令

until 命令和 while 命令工作的方式完全相反。 until 命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为 0,bash shell 才会执行循环中列出的命令。一旦测试命令返回了退出状态码 0,循环就结束了。
和你想的一样, until 命令的格式如下。

until test commands
do
    other commands
done

while 命令类似,你可以在 until 命令语句中放入多个测试命令。只有最后一个命令的退出状态码决定了 bash shell 是否执行已定义的 other commands
下面是使用 until 命令的一个例子。

$ cat test2.sh 
#!/bin/bash
var1=100
until [ $var1 -eq 0 ]
do
	echo $var1
	var1=$[ $var1 - 25 ]
done
$ ./test2.sh 
100
75
50
25

本例中会测试 var1 变量来决定 until 循环何时停止。只要该变量的值等于 0, until 命令就会停止循环。

控制循环

你可能会想,一旦启动了循环,就必须苦等到循环完成所有的迭代。并不是这样的。有两个命令能帮我们控制循环内部的情况:

  • break 命令;
  • continue 命令;

每个命令在如何控制循环的执行方面有不同的用法。

break命令

break 命令是退出循环的一个简单方法。可以用 break 命令来退出任意类型的循环,包括 whileuntil 循环。

跳出单个循环

在 shell 执行 break 命令时,它会尝试跳出当前正在执行的循环。

$ cat test3.sh 
#!/bin/bash
for var1 in 1 2 3 4 5 6 7 8 9 10
do
	if [ $var1 -eq 5 ]
	then
		break
	fi
	echo "Iteration number: $var1"
done
echo "The for loop is completed"
$ ./test3.sh 
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
The for loop is completed

for 循环通常都会遍历列表中指定的所有值。但当满足 if-then 的条件时, shell 会执行 break 命令,停止 for 循环。

这种方法同样适用于 whileuntil 循环。

跳出内部循环

在处理多个循环时, break 命令会自动终止你所在的最内层的循环。

$ cat test4.sh 
#!/bin/bash
# breaking out of an inner loop
for (( a = 1; a < 4; a++ ))
do
	echo "Outer loop: $a"
	for (( b = 1; b < 100; b++ ))
	do
		if [ $b -eq 5 ]
		then
			break
		fi
		echo " Inner loop: $b"
	done
done
$ ./test4.sh 
Outer loop: 1
 Inner loop: 1
 Inner loop: 2
 Inner loop: 3
 Inner loop: 4
Outer loop: 2
 Inner loop: 1
 Inner loop: 2
 Inner loop: 3
 Inner loop: 4
Outer loop: 3
 Inner loop: 1
 Inner loop: 2
 Inner loop: 3
 Inner loop: 4

内部循环里的 for 语句指明当变量 b 等于 100 时停止迭代。但内部循环的 if-then 语句指明当变量 b 的值等于 5 时执行 break 命令。注意,即使内部循环通过 break 命令终止了,外部循环依然继续执行。

跳出外部循环

有时你在内部循环,但需要停止外部循环。 break 命令接受单个命令行参数值:

break n

其中 n 指定了要跳出的循环层级。默认情况下, n1,表明跳出的是当前的循环。如果你将 n 设为 2break 命令就会停止下一级的外部循环。

$ cat test5.sh 
#!/bin/bash
for (( a = 1; a < 4; a++ ))
do
	echo "Outer loop: $a"
	for (( b = 1; b < 100; b++ ))
	do
		if [ $b -gt 4 ]
		then
			break 2
		fi
		echo " Inner loop: $b"
	done
done
$ ./test5.sh 
Outer loop: 1
 Inner loop: 1
 Inner loop: 2
 Inner loop: 3
 Inner loop: 4

注意,当 shell 执行了 break 命令后,外部循环就停止了。

continue命令

continue 命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。可以在循环内部设置 shell 不执行命令的条件。这里有个在 for 循环中使用 continue 命令的简单例子。

$ cat test5.sh
#!/bin/bash
for (( a = 1; a < 4; a++ ))
do
	echo "Outer loop: $a"
	for (( b = 1; b < 100; b++ ))
	do
		if [ $b -gt 4 ]
		then
			break 2
		fi
		echo " Inner loop: $b"
	done
done
$ ./test5.sh
Outer loop: 1
 Inner loop: 1
 Inner loop: 2
 Inner loop: 3
 Inner loop: 4

if-then 语句的条件被满足时(值大于 5 且小于 10), shell 会执行 continue 命令,跳过此次循环中剩余的命令,但整个循环还会继续。当 if-then 的条件不再被满足时,一切又回到正轨。
也可以在 whileuntil 循环中使用 continue 命令,但要特别小心。记住,当 shell 执行 continue 命令时,它会跳过剩余的命令。

处理循环的输出

最后,在 shell 脚本中,你可以对循环的输出使用管道或进行重定向。这可以通过在 done 命令之后添加一个处理命令来实现。

$ cat test6.sh 
#!/bin/bash
for file in /home/*
do
	if [ -d "$file" ]
	then
		echo "$file is a directory"
	else
		echo "$file is a file"
	fi
done > output.txt
$ ./test6.sh 
$ cat output.txt 
/home/docker is a directory
/home/user1 is a directory

shell 会将 for 命令的结果重定向到文件 output.txt 中,而不是显示在屏幕上。
这种方法同样适用于将循环的结果管接给另一个命令。

$ cat test7.sh 
#!/bin/bash
for state in "North Dakota" Connecticut Illinois Alabama Tennessee
do
	echo "$state is the next place to go"
done | sort
echo "This completes our travels"
$ ./test7.sh 
Alabama is the next place to go
Connecticut is the next place to go
Illinois is the next place to go
North Dakota is the next place to go
Tennessee is the next place to go
This completes our travels

state 值并没有在 for 命令列表中以特定次序列出。 for 命令的输出传给了sort 命令,该命令会改变 for 命令输出结果的顺序。运行这个脚本实际上说明了结果已经在脚本内部排好序了。

示例

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

while九九乘法表

#!/bin/bash
declare -i i=1;
declare -i j=1;
while [ $i -lt 10 ];do
	while [ $j -le $i ];do
		echo -ne "$j * $i = $[$j * $i]\t";
		let j++
	done
	echo
	let j=1;
	let i++
done

生成10个随机数取最大值和最小值

#!/bin/bash
min=0;
max=0;

loopCount=0

while [ $loopCount -lt 10 ];do
	random=`echo $RANDOM`
	if [ $loopCount -eq 0 ];then
		min=$random;
	else
		if [ $random -lt $min ];then
			min=$random
		elif [ $random -gt $max ];then
			max=$random
		fi
	fi
	let loopCount++
	echo "$[$loopCount]:$random"
done
echo "max:$max"
echo "min:$min"

until九九乘法表

#!/bin/bash
i=1
j=1
until [ $i -gt 9 ];do
	until [ $j -gt $i ];do
		echo -ne "$j X $i = $[$j * $i]\t"
		let j++
	done
	let j=1;
	let i++;
	echo
done

求1到100偶数的和

#!/bin/bash
sum=0
for i in {1..100};do
	if [ $[$i % 2] -eq 1 ];then
		continue;
	fi
	sum=$[$sum+$i];
done
echo $sum;

每隔3秒检测用户是否登录

使用 while 实现:

read -p '指定用户名:' username;

checkCount=0

while true; do
	let checkCount++;
	if who | grep "$username" &> /dev/null;then
		break;
	fi
	echo "检查第 $checkCount 次"
	sleep 3;
done
echo "用户 $username 已登录" >> /tmp/log.txt;

使用 until 实现:

#!/bin/bash
read -p '指定用户名:' username;

checkCount=0
until who | grep "$username" &> /dev/null; do
	let checkCount++;
	echo "检查第 $checkCount 次"
	sleep 3;
done
echo "用户 $username 已登录" >> /tmp/log.txt;
0

评论区