Shell 脚本中的循环语句

Shell 脚本中的循环语句

用对循环结构,事半功倍

结构化循环命令在编程中很常见。通常需要重复一组命令直到触及某个特定条件。比如处理某个目录下的所有文件、系统上的所有用户或某个文本文件中的所有内容。bash shell 提供了三个常用的循环命令 for、while 和 until,就来好好研究研究吧。

for 命令

for 命令允许你创建一个遍历一系列值的循环。每次迭代都使用其中一个值来执行已定义好的一组命令。

for var in list
do
  commands
done

遍历列表中的值

for 命令最基本的用法就是遍历值列表:

#!/bin/bash

for word in this is a just a sentence
do
  echo the next word is $word
done

---
the next word is this
the next word is is
the next word is a
the next word is sentence
-

echo $word

---
sentence

每次 for 命令遍历值列表,它都会将列表中的下个值赋给 $word 变量。在最后一次迭代后,$word 变量的值会在 shell 脚本的剩余部分一直保持最后迭代的值。

但是,如果要遍历的值列表中含有特殊符号,例如单引号,shell 会把它当作一个单独的数据值,有两种方法可以解决:

  • 使用转义字符(反斜线)来将单引号转义
  • 使用双引号来定义用到单引号的值

另外一点,for 命令假定每个值都是用空格分隔的,如果值列表中含有词组这样的,也需要用双引号将其圈起来。

当然,多数时候我们会将这个值列表存储在某个变量中,然后需要遍历变量中具体成员。

for item in $list

从命令读取值

任何能产生输出的命令,都可以使用 for 命令来遍历其输出:

#!/bin/bash

file="/path/to/file"

for item in $(cat $file)
do
  echo $item
done

cat 的输出包含 file 的内容,for 命令会遍历 file 中的内容,for 命令怎样分隔内容,就要继续看下一节咯。

更改内部字段分隔符

bash shell 中有一个特殊的环境变量,内部字段分隔符 IFS (internal field separator)。IFS 定义了 bash shell 用作字段分隔符的一系列字符。默认情况下,bash shell 会将以下字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

每当 bash shell 看到了这些字符中的任意一个,它就会假定遇到了列表中的新数据字段的开始。在处理含有空格的数据(例如文件路径)时,就会遇到麻烦了。要解决这个问题,可以在 shell 脚本中临时更改 IFS 环境变量的值来限制 bash shell 的分隔。例如,使其只能识别换行符:

#!/bin/bash

IFSOLD=$IFS
IFS=$'\n'
file="/path/to/file"

for line in $(cat $file)
do
  echo $line
done
IFS=$IFSOLD

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

IFS=$'\n':;"
#将换行符,冒号,分号和双引号设为IFS

用通配符读取目录

遍历目录中的文件,也可以用 for 命令实现。

#!/bin/bash

for file in /home/ubuntu/workspace/*
do
  if [ -d "$file" ]
  then
    echo "$file is a directory"
  elif [ -f "$file" ]
  then
    echo "$file is a file"
  fi
done

---
/home/ubuntu/workspace/README.md is a file
/home/ubuntu/workspace/bash is a directory
/home/ubuntu/workspace/finance is a directory
/home/ubuntu/workspace/letsencrypt is a directory

在 test 语句中,我们将 $file 用双引号圈起来是为了适应含有空格的目录或文件名。

要遍历多个目录,可以将多个目录合并到同一个 for 语句中:

for file in /home/ubuntu/workspace/* /home/ubuntu/lib/*

即使文件或目录不存在,for 命令也会尝试处理列表中的内容,所以,在执行该命令之前测试一下文件或目录还是有必要的。

C 语言风格的 for 命令

在 C 语言中,常用的 for 循环是这样的;

for (int i = 0; i <= 10; i++)
{
  printf("The next number is %d\n", i);
}

bash shell 也支持这种风格的 for 循环,其基本格式为:

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

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

在这种格式中,有些部分没有遵循 bash shell 标准的命令:

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

来举个🌰:

#!/bin/bash

for (( i = 1, j = 10; i <= 10; i++, j-- ))
do
  echo "$i ~ $j"
done

---
1 ~ 10
2 ~ 9
3 ~ 8
4 ~ 7
5 ~ 6
6 ~ 5
7 ~ 4
8 ~ 3
9 ~ 2
10 ~ 1

while 命令

while 命令允许定义一个要测试的命令,只要测试命令返回的退出状态码为 0 就循环执行一组命令。基本格式为:

while test command
do
  other commands
done

while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中运行的命令而改变,不然 while 命令将一直运行下去。

#!/bin/bash
var=10
while [ $var -gt 0 ]
do
  echo $var
  var=$[ $var-1 ]
done

while 命令允许你在 while 语句行定义多个测试命令,只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。例如:

#!/bin/bash

var=5

while echo $var
      [ $var -ge 0 ]
do
  echo "this is inside the loop"
  var=$[ $var - 1 ]
done

---
5
this is inside the loop
4
this is inside the loop
3
this is inside the loop
2
this is inside the loop
1
this is inside the loop
0
this is inside the loop
-1

注意,把每个测试条件放在单独的一行上。这里如果是 while echo $var [$var -ge 0],会当作是一条 echo 语句执行,返回非 0 的退出状态码,循环将一直执行。

until 命令

until 命令和 while 命令的工作方式完全相反。until 只有在测试命令返回非零的退出状态码时,循环中的命令才会执行。

#!/bin/bash
var=100

until [ $var -eq 0 ]
do
 echo $var
 var=$[ $var - 25 ]
done

---
100
75
50
25

嵌套循环

在循环语句的循环体内可以使用任意类型的命令,当然也包括循环命令,这时候就是执行的一种嵌套循环(nested loop)。这种情况下,命令的运行次数是乘积关系,所以要十分注意多层嵌套以防消耗过多系统资源。

#!/bin/bash

var1=3

until [ $var1 -eq 0 ]
do
  echo "outer loop: $var1"
  var2=1
  while [ $var2 -lt 5 ]
  do
    var3=$(echo "scale=2; $var1 / $var2" | bc)
    echo "  inner loop: $var1/$var2 = $var3"
    var2=$[ $var2 + 1 ]
  done
  var1=$[ $var1 - 1 ]
done

---
outer loop: 3
  inner loop: 3/1 = 3.00
  inner loop: 3/2 = 1.50
  inner loop: 3/3 = 1.00
  inner loop: 3/4 = .75
outer loop: 2
  inner loop: 2/1 = 2.00
  inner loop: 2/2 = 1.00
  inner loop: 2/3 = .66
  inner loop: 2/4 = .50
outer loop: 1
  inner loop: 1/1 = 1.00
  inner loop: 1/2 = .50
  inner loop: 1/3 = .33
  inner loop: 1/4 = .25

跳出循环

bash shell 有两个命令可在循环中控制循环内部情况:

  1. break

  2. continue

使用这两个命令,就可以不用等到循环完成所有迭代,而是达到指定条件即可跳出循环。

break 命令

1. 跳出单个循环

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

#!/bin/bash

for var in 1 2 3 4 5 6 7
do
  if [ $var -eq 3 ]
  then
    break
  fi
  echo "iteration number: $var"
done
echo "the for loop finished"

---
iteration number: 1
iteration number: 2
the for loop finished

2. 跳出内部循环

在处理嵌套循环时,break 命令终止内层循环继续执行外层循环。

#!/bin/bash

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

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

3. 跳出外部循环

当处在内层循环而需要跳出外层循环时,可为 break 命令指定单个参数 break n。默认情况下,n 为 1,表明跳出的是当前循环。如果 n 设为 2,break 命令会停止外一层的循环。

#!/bin/bash

for (( a = 1; a < 4; a++ ))
do
  echo "outer loop: $a"
  for (( b = 1; b < 100; b++ ))
  do
    if [ $b -gt 5 ]
    then
      break 2
    fi
    echo "  inner loop: $b"
  done
done

---
outer loop: 1
  inner loop: 1
  inner loop: 2
  inner loop: 3
  inner loop: 4
  inner loop: 5

continue 命令

continue 会提前终止某次循环,但不会完全终止整个循环。

#!/bin/bash

for (( a = 1; a < 15; a++ ))
do
  if [ $a -gt 5 ] && [ $a -lt 10 ]
  then
    continue
  fi
  echo "iteration number: $a"
done

---
iteration number: 1
iteration number: 2
iteration number: 3
iteration number: 4
iteration number: 5
iteration number: 10
iteration number: 11
iteration number: 12
iteration number: 13
iteration number: 14

在 while 或 until 命令中当然也可以使用 continue 命令。但是,如果在 continue 命令后改变测试条件变量的值就要非常小心了:

#!/bin/bash

var1=0

while echo "while iteration: $var1"
      [ $var1 -lt 15 ]
do
  if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
  then
    continue
  fi
  echo "  inner iteration number: $var1"
  var1=$[ $var1 + 1 ]
done

---
while iteration: 0
  inner iteration number: 0
while iteration: 1
  inner iteration number: 1
while iteration: 2
  inner iteration number: 2
while iteration: 3
  inner iteration number: 3
while iteration: 4
  inner iteration number: 4
while iteration: 5
  inner iteration number: 5
while iteration: 6
while iteration: 6
while iteration: 6
while iteration: 6
while iteration: 6
while iteration: 6
while iteration: 6
...

这个示例中,当执行到 continue 命令时,会跳过当次循环,也就跳过了 var1=$[ $var1 + 1 ] 这个重要的命令,使 while 命令一直执行下去。

处理循环输出

在 shell 中,可以对循环的输出使用管道或进行重定向,在 done 命令后添加一个处理命令即可。

for file /home/frank/*
do
  if [ -d "$file" ]
  then
    echo "$file is a directory"
  elif [ -f "$file" ]
    echo "$file is a file"
  fi
done > output.txt

shell 会将 for 命令的输出重定向到 output.txt 文件中,而不显示在屏幕上。

好了,搞定 shell 中的循环会让我们在操作数据时事半功倍!多使用多练习就能体会到了。下次继续吧!

Ads by Google

林宏

Frank Lin

Hey, there! This is Frank Lin (@flinhong), one of the 1.41 billion . This 'inDev. Journal' site holds the exploration of my quirky thoughts and random adventures through life. Hope you enjoy reading and perusing my posts.

YOU MAY ALSO LIKE

Using Liquid in Jekyll - Live with Demos

Web Notes

2016.08.20

Using Liquid in Jekyll - Live with Demos

Liquid is a simple template language that Jekyll uses to process pages for your site. With Liquid you can output complex contents without additional plugins.

Practising closures in JavaScript

JavaScript Notes

2018.12.17

Practising closures in JavaScript

JavaScript is a very function-oriented language. As we know, functions are first class objects and can be easily assigned to variables, passed as arguments, returned from another function invocation, or stored into data structures. A function can access variable outside of it. But what happens when an outer variable changes? Does a function get the most recent value or the one that existed when the function was created? Also, what happens when a function invoked in another place - does it get access to the outer variables of the new place?