基元和组合表达式

<font style="color:rgb(31, 35, 40);">[[ ]]</font><font style="color:rgb(31, 35, 40);">sh</font>中是<font style="color:rgb(31, 35, 40);">[ ]</font>)包起来的表达式被称作 检测命令基元。这些表达式帮助我们检测一个条件的结果。在下面的表里,为了兼容sh,我们用的是[ ]。这里可以找到有关bash中单双中括号区别的答案。

跟文件系统相关:

基元 含义
[ -e FILE ] 如果FILE存在 (exists),为真
[ -f FILE ] 如果FILE存在且为一个普通文件(file),为真
[ -d FILE ] 如果FILE存在且为一个目录(directory),为真
[ -s FILE ] 如果FILE存在且非空(size 大于0),为真
[ -r FILE ] 如果FILE存在且有读权限(readable),为真
[ -w FILE ] 如果FILE存在且有写权限(writable),为真
[ -x FILE ] 如果FILE存在且有可执行权限(executable),为真
[ -L FILE ] 如果FILE存在且为一个符号链接(link),为真
[ FILE1 -nt FILE2 ] FILE1FILE2新(newer than)
[ FILE1 -ot FILE2 ] FILE1FILE2旧(older than)

跟字符串相关:

基元 含义
[ -z STR ] STR为空(长度为0,zero)
[ -n STR ] STR非空(长度非0,non-zero)
[ STR1 == STR2 ] STR1STR2相等
[ STR1 != STR2 ] STR1STR2不等

算数二元运算符:

基元 含义
[ ARG1 -eq ARG2 ] ARG1ARG2相等(equal)
[ ARG1 -ne ARG2 ] ARG1ARG2不等(not equal)
[ ARG1 -lt ARG2 ] ARG1小于ARG2less than)
[ ARG1 -le ARG2 ] ARG1小于等于ARG2less than or equal)
[ ARG1 -gt ARG2 ] ARG1大于ARG2greater than)
[ ARG1 -ge ARG2 ] ARG1大于等于ARG2greater than or equal)
条件语句可以跟 组合表达式 配合使用:
Operation Effect
[ ! EXPR ] 如果EXPR为假,为真
[ (EXPR) ] 返回EXPR的值
[ EXPR1 -a EXPR2 ] 逻辑 , 如果EXPR1和(and)EXPR2都为真,为真
[ EXPR1 -o EXPR2 ] 逻辑 , 如果EXPR1或(or)EXPR2为真,为真
当然,还有很多有用的基元,在Bash的man页面能很容易找到它们。

if else

  1. if condition
  2. then
  3. statements(s)
  4. fi
  • condition 是判断条件 - 如果 condition 成立(返回 true),那么 then 后边的语句将会被执行 - 如果 condition 不成立(返回 false),那么不会执行任何语句
注意,最后必须以 fi 来闭合,fi 就是 if 倒过来写。也正式有了 fi 来结尾,所以即使有神调语句也不需要用 {} 包围起来。

if else 语句

  1. #!/bin/bash
  2. read a
  3. read b
  4. if (($a == $b))
  5. then
  6. echo "a 和 b 相等"
  7. else
  8. echo "a 和 b 不想等,输入错误"
  9. fi

if elif else 语句

  1. if condition1
  2. then
  3. statement1
  4. elif condition2
  5. then
  6. statement2
  7. elif condition3
  8. then
  9. statement3
  10. ...
  11. else
  12. statementN
  13. fi
⚠️ 注意,if elif 后边都得跟着 then

嵌套 if 语句

if 条件测试中可以再嵌套 if 条件测试
  1. if [ 测试条件成立 ]
  2. then 执行相应命令
  3. if [ 测试条件成立 ]
  4. then 执行相应命令
  5. fi
  6. fi 结束

case in

和其它编程语言类似,Shell 也支持两种分支结构(选择结构),分别是 if else 语句和 case in 语句。 基本格式:
  1. case expression in
  2. pattern1) statement1;;
  3. pattern2) statement2;;
  4. pattern3 | pattern4) statement3;;
  5. *) default statement
  6. esac
case 命令会将指定的变量与不同模式进行比较。如果变量和模式是匹配的,那么 Shell 会执行该模式指定的命令。可以通过竖线操作符在一行中分隔出多个模式。星号会捕获所有与已知模式不匹配的值。 示例:
  1. $ cat case-in.sh
  2. #!/bin/bash
  3. # using the case command
  4. #
  5. case $USER in
  6. rich | barbara)
  7. echo "Welcom $USER"
  8. echo "Please enjoy your visit";;
  9. testing)
  10. echo "Special testing account";;
  11. jessica)
  12. echo "Do not forget to log off when you're done";;
  13. *)
  14. echo "Sorry, you are not allowed here";;
  15. esac
  16. $ ./case-in.sh

for

for 命令用于创建遍历系列值的循环。每次迭代都是用其中一个值来执行已定义好的一组命令。 基本格式:
  1. for var in list
  2. do
  3. commands
  4. done
可以将 do 语句和 for 语句放在同一行,但必须用分号将其同列表中的值分开:for var in list; do
  1. #!/bin/bash
  2. # basic for command
  3. for item in Guangdong Sichuan Henan Heilongjiang Zhejiang Jiangsu
  4. do
  5. echo The next province is $item
  6. done
  • for 循环假定每个值都是用空格分割的
  • 当列表的值含有特殊字符,可以使用转义字符解决
  • 当在某个值的两边使用双引号时,Shell 并不会将双引号当成值的一部分

从命令中读取值

生成列表中所需值的另外一个途径就是使用命令的输出。可以用命令替换来执行任何能产生输出的命令,然后在 for 命令中使用该命令的输出。
  1. # for-read-values-from-a-file.sh
  2. # !/bin/bash
  3. # reading values from a file
  4. file="provinces"
  5. for province in $(cat $file)
  6. do
  7. echo "Visit beautiful $province"
  8. done
同目录下的 provinces.txt 文件中:
  1. Guangdong
  2. Sichuan
  3. Henan
  4. Heilongjiang
  5. Zhejiang
  6. Jiangsu
该例子在命令替换中使用了 cat 命令来输出文件 provinces 的内容。for 命令仍然以每次一行的方式遍历了 cat 命令的输出,假定每个值都是在单独的一行上。但这并没有解决数据中有空格的问题。如果你列出了一个名字中有空格的值,for 命令仍然会将每个单词当作单独的值。 上述示例文件执行路径为相同目录,如果是其他目录下,需要使用全路径名来引用文件位置。

更改字段分隔符

造成上述无法分隔含有空格的值原因是特殊的环境变量 IFS,叫作 内部字段分隔符(internal field separator)。IFS 环境变量定义了 Bash Shell 用作字段分隔符的一系列字符。默认情况下,Bash Shell 会将下列字符当作字符分隔符:
  • 空格
  • 制表符
  • 换行符
如果 Bash Shell 在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦,就像你上一个脚本示例中看到的。 在 Shell 脚本中临时更改 IFS 环境变量,可以限制被 Bash Shell 当作字段分隔符的字符。
  1. #/bin/bash
  2. # reading values from a file
  3. file="provinces"
  4. IFS=$'\n'
  5. for province in $(cat $file)
  6. do
  7. echo "Visit beautiful $province"
  8. done

用通配符读取目录

要用 for 命令来自动遍历目录中的文件,必须在文件名或路径名中使用通配符。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。 如果不知道所有文件名,这个特性在处理目录中的文件时就非常有用。
  1. #!/bin/bash
  2. # iterate through all the files in a directory
  3. for file in ./*
  4. do
  5. if [ -d "$file"]
  6. then
  7. echo "$file is a directory"
  8. elif [ -f "$file" ]
  9. then
  10. echo "$file is a file"
  11. fi
  12. done

C 语言风格的 for 命令

基本格式:
  1. for (( variable assignment ; condition ; iteration process ))
示例:
  1. for (( i=1; i<= 10; i++ ))
  2. do
  3. echo "The next number is $i"
  4. done

while

while 命令某种意义上是 if-then 语句和 for 循环的混合体。while 命令允许定义一个要测试的命令,然后循环执行一组命令,只要定义的测试命令返回的是退出状态码 0。它会在每次迭代的一开始测试 test 命令。在 test 命令返回非零退出状态码时,while 命令会停止执行那组命令。 基本格式:
  1. while test command
  2. do
  3. other commands
  4. done
常见的用法是方括号来检查循环命令中用到的 Shell 变量的值:
  1. #/bin/bash
  2. # while command test
  3. var1=10
  4. while [ $var1 -gt 0 ]
  5. do
  6. echo $var1
  7. var1=$[ $var1 - 1 ]
  8. done
只要测试条件成立,while 命令就会不停地循环执行定义好的命令。在这些命令中,测试条件中用到的变量必须修改,否则就会陷入无限循环。

until

util 命令和 while 命令工作的方式完全相反。util 命令要求你指定一个通常返回非零退出状态码的测试命令。只要测试命令的退出状态码不为 0,Bash Shell 才会执行循环中列出的命令。一旦测试命令返回了退出状态码 0,循环就结束了。 基本格式:
  1. util test commands
  2. do
  3. other commands
  4. done
示例:
  1. #!/bin/bash
  2. # using the until command
  3. num=100
  4. until [ $num -eq 0 ]
  5. do
  6. echo $num
  7. num=$[ $num - 25 ]
  8. done

break

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

跳出单体循环

  1. #!/bin/bash
  2. # breaking out of a for loop
  3. for num in 1 2 3 4 5 6 7 8 9 10
  4. do
  5. if [ $num -eq 5 ]
  6. then
  7. break
  8. fi
  9. echo "Iteration number: $num"
  10. done
  11. echo "The for loop is completed"
for 循环通常都会遍历列表中指定的所有值。但当满足 if-then 的条件时,Shell 会执行 break 命令,停止 for 循环。

跳出内部循环

在处理多个循环时,break 命令会自动终止你所在的最内层的循环。
  1. #!/bin/bash
  2. # breaking out of an inner loop
  3. for (( a = 1; a < 4; a++ ))
  4. do
  5. echo "Outer loop: $a"
  6. for (( b = 1; b < 100; b++ ))
  7. do
  8. if [ $b -eq 5 ]
  9. then
  10. break
  11. fi
  12. echo "Inner loop: $b"
  13. done
  14. done

跳出外部循环

有时你再内部循环,但需要停止外部循环。break 命令接受单个命令行参数值:
  1. break n
其中 n 指定了要跳出的循环层级。默认情况下,n 为 1,表明跳出的是当前的循环。如果你将 n 设为 2,break 命令就会停止下一级的外部循环。
  1. #!/bin/bash
  2. # breaking out of an outer loop
  3. for (( a = 1; a < 4; a++ ))
  4. do
  5. echo "Outer loop: $a"
  6. for (( b = 1; b < 100; b++ ))
  7. do
  8. if [ $b -gt 4 ]
  9. then
  10. break 2
  11. fi
  12. echo "Inner loop: $b"
  13. done
  14. done

continue

continue 命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。可以在循环内部设置 Shell 不执行命令的条件。这里有个在 for 循环中使用 continue 命令的简单例子。
  1. #!/bin/bash
  2. # using the continue command
  3. for (( num = 1; num < 15; num++ ))
  4. do
  5. if [ $num -gt 5 ] && [ $num -lt 10 ]
  6. then
  7. continue
  8. fi
  9. echo "Iteration number: $num"
  10. done
if-then 语句的条件被满足时,Shell 会执行 continue 命令,跳过此次循环中剩余的命令,但整个循环还会继续。当 if-then 语句不再被满足时,一切又回到正轨。 也可以在 whileuntil 循环中使用 continue 命令,但要特别小心。记住,当 Shell 执行 continue 命令时,它会跳过剩余的命令。如果你在其中某个条件里对测试条件变量进行增值,问题就会出现。
  1. #!/bin/bash
  2. # improperly using the continue command in a while loop
  3. num=0
  4. while echo "while iteration: $num"
  5. [ $num -lt 15 ]
  6. do
  7. if [ $num -gt 15 ] && [ $num -lt 10 ]
  8. then
  9. continue
  10. fi
  11. echo "Inside iteration number: $num"
  12. num=$[ $num + 1 ]
  13. done

嵌套循环

循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫作嵌套循环(nested loop)。注意,在使用嵌套循环时,你是在迭代中使用迭代,与命令运行的次数是乘积关系。不注意着点话,有可能会在脚本中造成问题。
  1. #!/bin/bash
  2. # nesting for loops
  3. for (( a = 1; a <= 3; a++ ))
  4. do
  5. echo "Starting loop $a"
  6. for (( b = 1; b <= 3; b++ ))
  7. do
  8. echo " Inside loop: $b"
  9. done
  10. done
while 循环内部放置一个 for 循环:
  1. #!/bin/bash
  2. # placing a for loop inside a while loop
  3. num1=5
  4. while [ $num1 -ge 0 ]
  5. do
  6. echo "Output loop: $num1"
  7. for (( num2 = 1; num2 < 3; num2++ ))
  8. do
  9. num3=$[ $num1 * $num2 ]
  10. echo " Inner loop: $num1 * $num2 = $num3"
  11. done
  12. $num1=$[ $num1 - 1 ]
  13. done

处理循环输出

如果需要对循环的输出使用管道或进行重定向,可以通过在 done 命令之后添加一个处理命令来实现。
  1. for file in /home/rich/*
  2. do
  3. if [ -d "$file" ]
  4. then
  5. echo "$file is a direcoty"
  6. elif
  7. echo "$file is a file"
  8. fi
  9. done > output.txt

实践应用

查找可执行文件

循环环境变量 $PATH 中的目录迭代。
  1. #!/bin/bash
  2. # finding files in the PATH
  3. IFS=:
  4. for folder in $PATH
  5. do
  6. echo "$folder:"
  7. for file in $folder/*
  8. do
  9. if [ -x $file ]
  10. then
  11. echo " $file"
  12. fi
  13. done
  14. done
运行这段代码时,你会得到一个可以在命令行中使用的可执行文件的列表。

创建多个用户账户

通过添加新用户账户放在一个文本文件中,然后创建一个简单的脚本处理。 文本文件的格式:
  1. userid,user name
read 命令会自动读取 .csv 文本的下一行内容,所以不需要专门再写一个循环来处理。当 read 命令返回 FALSE(也就是读取完整个文件时),while 命令就会退出。
  1. #!/bin/bash
  2. # process new user accounts
  3. input="users.csv"
  4. while IFS=',' read -r userid name
  5. do
  6. echo "adding $userid"
  7. useradd -c "$name" -m $userid
  8. done < "$input"
必须作为 root 用户才能运行这个脚本,因为 useradd 命令需要 root 权限。