写在前面(最重要)

本文部分资料和示例援引自以下书籍。在此,感谢原作者的创作,以及所有译者的付出,向他们致敬。

  1. Advanced Bash-Scripting Guide

  2. 《高级Bash脚本编程指南》Revision 10中文版

  3. Linux脚本编程执导

其中 《高级Bash脚本编程指南》Revision 10中文版 是 《Advanced Bash-Scripting Guide》 的中文翻译版,文档翻译正在进行中,再次感谢译者付出。

前言

在之前的文章中,我们已经详细的介绍了SHELL脚本编程的一些基础知识,运用这些基础已经能够帮助我们高效率的解决日常生产中的一些问题了,但还远远不够。实际生产中可能面临这大量的复杂的处理任务,需要我们编写脚本去完成。此时就需要一些更高级的内容,来帮助我们完成对脚本的编写。
今天将介绍的是SHELL脚本中的循环与分支

分支与循环

1. 分支

2. 循环

3. 循环控制

4. 特殊用法

分支

条件选择if语句

if 语句是脚本编写过程中基本分支语句。简单的if语句可以使用一些测试结构代替,但是if语句的可读性,却是测试结构无法替代。

单分支的if语句

单分支的 if 语句的语法结构如下。

	if 判断条件;then
		COMMANDS		
		...	
	fi

多分支的if语句

多分支的 if 语句的语法结构如下。

	if 判断条件;then
		COMMANDS		
		...	
	elif 判断条件;then
		COMMANDS		
		...	
	elif 判断条件;then
		COMMANDS		
		...	
	else		
	    COMMANDS		
		...	
	fi

if 语句其实还是比较好理解的,它是对某一个条件进行判断,然后根据判断的结构进行其他的操作。稍微有点编程经验的人,都会理解这种简短的分支结构。 
if 语句还可以嵌套,可以根据实际生产中的情况,合理的进行嵌套组合。

例子

  • 判断某个变量的值是否在0-5之间

# 这是一个嵌套的if语句
a=3
if [ "$a" -gt 0 ];then								#then 也可以写在下一行,这时就可以去掉 分号
  if [ "$a" -lt 5 ];then    
  echo "The value of \"a\" lies somewhere between 0 and 5."
  fi
fi

# 和下面的结果相同
if [ "$a" -gt 0 ] && [ "$a" -lt 5 ]
then  
    echo "The value of \"a\" lies somewhere between 0 and 5."
fi
  • 判断某个学员的学习成绩,并输出。不及格,及格,优秀等信息。

# 读取用户输入的成绩
read -p "Please input your score "  SCORE
if [ "$SCORE" -lt 60 ];then	
    echo "不及格"
elif [ "$SCORE" -ge 60   -a  "$SCORE" -lt 80 ];then	
     echo "及格"
elif [ "$SCORE" -ge 80   -a  "$SCORE" -le 100 ];then 
    echo "优秀"
fi

条件判断case语句

在一些比较流行的编程语言中也有多分支的条件判断语句,例如C/C++/JAVA中的switch语句,可以根据条件跳转到其中的任意一个分支,很适合用来创建菜单。

case 语句的语法如下

	case "$variable" in
	  "$condition1" )
	    command...	  	
	    ;;
	  "$condition2" )
	    command...	  	
	    ;;
	esac

注意

  • 对变量进行引用不是必须的,因为在这里不会进行字符分割

  • 条件测试语句必须以右括号 ) 结束。

  • 每一段代码块都必须以双分号 ;; 结束。

  • 如果测试条件为真,其对应的代码块将被执行,而后整个 case 代码段结束执行

  • case 代码段必须以 esac 结束(倒着拼写case)。

case支持glob风格的通配符

  • *: 任意长度任意字符

  • ?: 任意单个字符

  • []: 指定范围内的任意单个字符

  • a|b: a或b

示例

  • 判断用户输入的 是否是yes或者no(只要是这两个词就可以,忽略大小写)

	read -p "Please input [yes/no]" INPUT	
	case $INPUT in
        	[Yy]|[Yy][Ee][Ss])
        	    echo "Your word is yes"
        	    ;;
        	[Nn]|[Nn][Oo])
        	    echo "Your word is no"
        	    ;;
        	*)
        	    echo "Your word is wrong"
        	    ;;
	esac
  • 实现简单的通讯录,输入索引,能够查看详细的信息

#!/bin/bash#简易的通讯录数据库

#清屏

clear
echo -e  "ContactList
-----------
Chooseoneofthefollowingpersons:

[E]vans,Roland
[J]ones,Mildred
[S]mith,Julie
[Z]ane,Morris"echo


read -p "Please choose one menu [E|J|S|Z] "  person

case "$person" in
#注意变量是被引用的。

"E"|"e")
#同时接受大小写的输入。
echo -e "
RolandEvans
4321FlashDr.
Hardscrabble,CO80753
(303)734-9874
(303)734-9892fax
revans@zzy.net
Businesspartner&oldfriend"
;;
#注意用双分号结束这一个选项。

"J"|"j")
echo -e "
MildredJones
249E.7thSt.,Apt.19
NewYork,NY10009
(212)533-2814
(212)533-9972fax
milliej@loisaida.com
x-girlfriend
birthday:Feb.11"
;;

*)
#缺省设置。
#空输入(直接键入回车)也是执行这一部分。
echo
echo "Notyetindatabase."
;;
esac


echo

exit 0
  • 查看当前系统的设备架构 是 i386 还是i486 或者是X86_64

#!/bin/bash
#  使用命令替换生成 "case" 变量。
case $( arch ) in   # $( arch ) 返回设备架构。
# 等价于 'uname -m"。
i386 ) 
echo "80386-based machine"
;;

i486 ) 
echo "80486-based machine"
;;

i586 ) 
echo "Pentium-based machine"
;;

i686 ) 
echo "Pentium2+-based machine"
;;

X86_64 ) 
echo "X86_64-based machine"
;;

* ) 
echo "Other type of machine"
;;
esac


exit 0

select

select 语句严格来说不能算作循环,因为它们并没有反复执行代码块。但是和循环结构相似的是,它们会根据代码块顶部或尾部的条件控制程序流。
select 结构的语法如下

select variable [in list]
do 
    command... 
    break
done
  • 如果不使用break命令,select 语句将变成无限循环,会重复的执行那些代码段。

  • select 循环主要用于创建菜单,按数字顺序排列的菜单项将显示在标准输出上。

  • select 默认使用提示字串3(Prompt String 3,$PS3, 即#?),但同样可以被修改。

  • 用户输入菜单列表中的某个数字,执行相应的命令

  • 用户输入被保存在内置变量 REPLY 中

示例

  • 选出一个最喜欢吃的蔬菜

#!/bin/bash

PS3='Choose your favorite vegetable: '  # 设置提示字串。
					# 否则默认为 #?。
echo


select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
do    
    echo
    echo "Your favorite veggie is $vegetable."
    echo "Yuck!"
    echo
    break  # 如果没有 'break' ,整个代码会一直循环下去,直到按了Ctrl+C 为止。
done


exit

上面这段代码,会输出如下的效果

1) beans
2) carrots
3) potatoes
4) onions
5) rutabagas
Choose your favorite vegetable: 1

Your favorite veggie is beans.
Yuck!

如果省略了 [in list] 那么 select 将会使用传入脚本的命令行参数($@)或者传入函数的参数作为 list。可以与 for variable [in list] 中 in list 被省略的情况做比较。
下面将省略 这个列表来实现上面的例子。 运行效果与上面的结果是一致的。

#!/bin/bash

PS3='Choose your favorite vegetable: '

echo

choice_of(){
    select vegetable
    # [in list] 被省略,因此 'select' 将会使用传入函数的参数作为 list。
    do  
      echo
      echo "Your favorite veggie is $vegetable."
      echo "Yuck!"
      echo
      break
    done
}


choice_of beans rice carrorts radishes rutabaga spinach

#         $1    $2   $3      $4       $5       $6
#         传入了函数 choice_of()


exit 0

循环

循环,顾名思义就是重复性的执行某一基本操作。在Linux/Bash 中有三种基本循环,for,while,until循环。

for 循环 for arg in [list]

for 循环 可以有以下的列表生成方式

  • 直接给出列表

for var in item1 item2 ... itemN
do	
    command1
    command2
    ....
    ...
    commandN
done
  • 直接给出一个整数列表

# 直接给出整数列表  
for var in 1 2 3 4 ... 100
do	
    command1
    command2
    ....
    ...
    commandN
done


# 或者直接生成整数列表  
for var in {1..100..2}
do	
    command1
    command2
    ....
    ...
    commandN
done


# 或者直接生成整数列表  
for var in $(seq 0  2  100)   ##seq [start [step]] end 

do	
    command1
    command2
    ....
    ...
    commandN
    
done
  • 能够返回列表的命令

# 直接给出整数列表  
for var in $(ls *)
do	
    command1
    command2
    ....
    ...
    commandN
done
  • 使用glob 如 *.sh ,便是当前目录下的.sh文件列表

for var in *.sh
do	
    command1
    command2
    ....
    ...
    commandN
done
  • 直接使用某个变量引用 如$@,$* ,或者某个变量

fileNames="HELLO1  HELLO2  HELLO3  HELLO4"

for var in $fileNames
do    
    command1
    command2
    ....
    ...
    commandN
done
  • 也可以直接遍历某个数组

ArrayName=(/etc/*.conf)

for var in ${ArrayName[@]}
do    
    command1
    command2
    ....
    ...
    commandN
done

示例

检查某些指定的文件是否存在

#!/bin/bash
# fileinfo.sh


FILES="/usr/sbin/accept
/usr/sbin/pwck
/usr/sbin/chroot
/usr/bin/fakefile
/sbin/badblocks
/sbin/ypbind"     # 你可能会感兴趣的一系列文件。
                  # 包含一个不存在的文件,/usr/bin/fakefile。
                  
 echo
 
 for file in $FILESdo

     if [ ! -e "$file" ]       
     # 检查文件是否存在。
     then    
         echo "$file does not exist."; 
         echo    
         continue                # 继续判断下一个文件。
      fi  
      
      echo
      
      
 done
 
 
 
 exit 0

while 循环

Bash中的while循环结构与其他编程语言的while循环结构一致的,在循环的开始就判断条件是否满足,如果循环条件为真,就继续执行循环,如果为假则跳出循环。
其语法结构如下

while [ condition ]
do  	
    command1
    ...
    commandN
done
  • CONDITION:循环控制条件;进入循环之前,先做一次判断;每一次循环之后会再次做判断;条件为“true”,则执行一次循环;直到条件测试状态为“false”终止循环。

  • 因此:CONDTION一般应该有循环控制变量;而此变量的值会在循环体不断地被修正。

  • 进入条件:CONDITION为true

  • 退出条件:CONDITION为false

  • while循环的括号结构不是必须存在的

示例:简单的while循环

#!/bin/bash

var0=0

LIMIT=10

while [ "$var0" -lt "$LIMIT" ]
do  
    echo -n "$var0 "        # -n 不会另起一行

    var0=`expr $var0 + 1`   # var0=$(($var0+1))  效果相同。
                          # var0=$((var0 + 1)) 效果相同。
                          # let "var0 += 1"    效果相同。
done                      # 还有许多其他的方法也可以达到相同的效果。

示例:多测试条件的while循环

一个 while 循环可以有多个测试条件,但只有最后的那一个条件决定了循环是否终止。这是一种你需要注意到的不同于其他循环的语法。

#!/bin/bash

var1=unset
previous=$var1

while echo "previous-varialbe = $previous"
      echo      
      previous=$var1 		 # 记录下 $var1 之前的值。
      [ "$var1" != end ]  
    # 在 while 循环中有4个条件,但只有最后的那个控制循环。
     	# 最后一个条件的退出状态才会被记录。
do

echo "Input variable #1 (end to exit)" 
read var1

echo "variable #1 = $var1"

done

exit 0

示例:在 while 循环中结合 read 命令,我们就得到了一个非常易于使用的 while read 结构。它可以用来读取和解析文件 。

while read value   # 一次读入一个数据。
do    
    echo "The value is $value"
done

until

与while循环相反,until循环是其测试条件为真时,跳出循环。也就是说,测试条件为假,进入循环,测试条件为真退出循环。 而且,与其他编程语言不一样的地方在于,until循环的测试条件在循环的顶部。 语法如下所示

until [ condition-is-true ]do  commands(s)...done

示例:读取用户输入,并输出到标准输出上,如果是end结束

#!/bin/bash

END_CONDITION=end

until [ "$var1" = "$END_CONDITION" ]
# 在循环顶部测试条件。
do  
    echo "Input variable #1 "
    echo "($END_CONDITION to exit)"
    read var1  
    echo "variable #1 = $var1"
    echo
done

循环控制 break,continue,shift

break [N],continue [N]

break 和 continue 命令的作用和其他编程语言中的作用一样。break 是跳出结束循环,continue是跳出本次循环,进入到下一次循环。
但是 ,在bash 中,break 和 continue 有一种特殊用法,能够指定跳出其上 N 层的循环,也就是 continue [N]

接下来我们使用一个示例来演break和continue的使用。

#!/bin/bash

# "continue N" 命令可以影响其上 N 层循环。
for outer in I II III IV V           # 外层循环
do 
    echo; 
    echo -n "Group $outer: "

  # --------------------------------------------------------------------
  for inner in 1 2 3 4 5 6 7 8 9 10  # 内层循环
  do

    if [[ "$inner" -eq 7 && "$outer" = "III" ]]
    then
      continue 2  # 影响两层循环,包括“外层循环”。
                  # 将其替换为普通的 "continue",那么只会影响内层循环。
    fi    
    
    echo -n "$inner "  # 7 8 9 10 将不会出现在 "Group III."中。
  done
  # --------------------------------------------------------------------
done

exit 0

shift [N]

用于将参量列表左移指定次数,默认是左移1次。 
参量列表 list 一旦被移动,最左端的那个参数就从列表中删除。while 循环遍历位置参量列表时,常用到 shift。

示例:批量添加用户,并能够根据输入的选项,进行不同的输出

# 使用示例

# ./user.sh  --add MAGE,WANG,HELLO -v
# ./user.sh  -h

#!/bin/bash

DEBUG=0
ADD=0
DEL=0

for I in `seq  $#`; do
case $1 in
-v|--verbose)
    DEBUG=1			#是否用来输出详情
    shift			# 参数列表向左 移动 1 个
    ;;

-h|--help)
    echo "Usage:`basename $0` --add USER_LIST --del USER_LIST -v|--verbose -h|--help"
    exit 0    
    ;;
    
--add)
    ADD=1    
    ADDUSERS=$2
    shift 2    
    ;;
 
 
--del)
    DEL=1    
    DELUSERS=$2
    shift 2    
    ;;
    
esac

done


if [ $ADD -eq 1 ]; then
    for USER in `echo $ADDUSERS | sed 's@,@ @g'`; do	#将用户列表分割
        if id $USER &> /dev/null; then
            [ $DEBUG -eq 1 ] && echo "$USER exists"
        else            useradd $USER
            [ $DEBUG -eq 1 ] && echo "Add user $USER finished."
        fi
     done      
fi

if [ $DEL -eq 1 ]; then
    for USER in `echo $DELUSERS | sed 's@,@ @g'`; do
        if id $USER &> /dev/null; then            userdel -r $USER
            [ $DEBUG -eq 1 ] && echo "Delete user $USER finished "
        else
            [ $DEBUG -eq 1 ] && echo "user $USER not exists."
        fi
    done      
fi

特殊用法

  • 双小括号方法,即((…))格式,也可以用于算术运算,双小括号方法也可以使bash Shell实现C语言风格的变量操作。for循环和while循环都可以使用这种((…))格式

** for 循环 的语法格式如下 **

for ((EXPR1;EXPR2;EXPR3));do	
    COMMAND
    ....
    COMMAND
done
# 例如 输出 1-10

LIMIT=10
for ((a=1; a <= LIMIT ; a++)) # 双圆括号语法,不带 $ 的 LIMIT
do  
    echo -n "$a "
done

until循环的语法如下

until (( EXPR ));do	
    COMMAND
    ....
    COMMAND
done

#例输出 0-10 

LIMIT=10
var=0

until (( var > LIMIT ))
do  # ^^ ^     ^     ^^   没有方括号,没有 $ 前缀。
  echo -n "$var "
  (( var++ ))
done    # 0 1 2 3 4 5 6 7 8 9 10

while循环的语法如下

while (( EXPR1 ))   #  双圆括号结构,
do                       
 
done

#例如 实现1-10 的和


LIMIT=10

while (( a <= LIMIT ))   #  双圆括号结构,
do                       #+ 并且没有使用 "$"。
  echo -n "$a "
  ((a += 1))             # let "a+=1"
  # 是的,就是这样。
  # 双圆括号结构允许像C语言一样自增一个变量。
done
  • while 循环的特殊用法(遍历文件的每一行)

#依次读取/PATH/FROM/SOMEFILE文件中的每一行,且将行赋值给变量line
while read line; do	
    循环体
    
done < /PATH/FROM/SOMEFILE

在实际的使用过程中,可以实现多种循环的嵌套,以便实现复杂任务,应当具体情况具体分析。

如何在 for,while 和 until 之间做出选择?我们知道在C语言中,在已知循环次数的情况下更加倾向于使用 for 循环。但是在Bash中情况可能更加复杂一些。Bash中的 for 循环相比起其他语言来说,结构更加松散,使用更加灵活。因此使用你认为最简单的就好。


个人博客地址:http://www.pojun.tech/ 欢迎访问