Shell脚本:Linux Shell脚本学习指南(第二部分Shell编程)四

第二部分:Shell编程(四)

三十一、Shell test命令(Shell [])详解,附带所有选项及说明

test 是 Shell 内置命令,用来检测某个条件是否成立。test 通常和 if 语句一起使用,并且大部分 if 语句都依赖 test。

test 命令有很多选项,可以进行数值、字符串和文件三个方面的检测。

Shell test 命令的用法为:

test expression

当 test 判断 expression 成立时,退出状态为 0,否则为非 0 值。

test 命令也可以简写为[],它的用法为:

[ expression ]

注意[]expression之间的空格,这两个空格是必须的,否则会导致语法错误。[]的写法更加简洁,比 test 使用频率高。

test 和 [] 是等价的,后续我们会交替使用 test 和 [],以让读者尽快熟悉。

《二十九、Shell if else》一节中,我们使用 (()) 进行数值比较,这节我们就来看一下如何使用 test 命令进行数值比较。

#!/bin/bash

read age

if test $age -le 2; then

        echo "婴儿"

elif test $age -ge 3 && test $age -le 8; then

        echo "幼儿"

elif [ $age -ge 9 ] && [ $age -le 17 ]; then

        echo "少年"

elif [ $age -ge 18 ] && [ $age -le 25 ]; then

        echo "成年"

elif test $age -ge 26 && test $age -le 40; then

        echo "青年"

elif test $age -ge 41 && [ $age -le 60 ]; then

        echo "中年"

else

        echo "老年"

fi

其中,-le选项表示小于等于,-ge选项表示大于等于,&&是逻辑与运算符。

学习 test 命令,重点是学习它的各种选项,下面我们就逐一讲解。

1、与文件检测相关的 test 选项

表1:test 文件检测相关选项列表

文件类型判断
选 项作 用
-b filename判断文件是否存在,并且是否为块设备文件。
-c filename判断文件是否存在,并且是否为字符设备文件。
-d filename判断文件是否存在,并且是否为目录文件。
-e filename判断文件是否存在。
-f filename判断文件是否存在,井且是否为普通文件。
-L filename判断文件是否存在,并且是否为符号链接文件。
-p filename判断文件是否存在,并且是否为管道文件。
-s filename判断文件是否存在,并且是否为非空。
-S filename判断该文件是否存在,并且是否为套接字文件。
文件权限判断
选 项作 用
-r filename判断文件是否存在,并且是否拥有读权限。
-w filename判断文件是否存在,并且是否拥有写权限。
-x filename判断文件是否存在,并且是否拥有执行权限。
-u filename判断文件是否存在,并且是否拥有 SUID 权限。
-g filename判断文件是否存在,并且是否拥有 SGID 权限。
-k filename判断该文件是否存在,并且是否拥有 SBIT 权限。
文件比较
选 项作 用
filename1 -nt filename2判断 filename1 的修改时间是否比 filename2 的新。
filename -ot filename2判断 filename1 的修改时间是否比 filename2 的旧。
filename1 -ef filename2判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法

Shell test 文件检测举例:

#!/bin/bash

read filename

read url

if test -w $filename && test -n $url

then

        echo $url > $filename

        echo "写入成功"

else

        echo "写入失败"

fi

在 Shell 脚本文件所在的目录新建一个文本文件并命名为 urls.txt,然后运行 Shell 脚本,运行结果为:

urls.txt↙
http://c.biancheng.net/shell/↙
写入成功

2、与数值比较相关的 test 选​项

表2:test 数值比较相关选项列表

选 项作 用
num1 -eq num2判断 num1 是否和 num2 相等。
num1 -ne num2判断 num1 是否和 num2 不相等。
num1 -gt num2判断 num1 是否大于 num2 。
num1 -lt num2判断 num1 是否小于 num2。
num1 -ge num2判断 num1 是否大于等于 num2。
num1 -le num2判断 num1 是否小于等于 num2。

注意,test 只能用来比较整数,小数相关的比较还得依赖 《二十七、Linux bc命令》

Shell test 数值比较举例:

#!/bin/bash

read a b

if test $a -eq $b

then

        echo "两个数相等"

else

        echo "两个数不相等"

fi

运行结果1:
10 10
两个数相等

运行结果2:
10 20
两个数不相等

3、与字符串判断相关的 test 选项

表3:test 字符串判断相关选项列表

选 项作 用
-z str判断字符串 str 是否为空。
-n str判断宇符串 str 是否为非空。
str1 = str2
str1 == str2
===是等价的,都用来判断 str1 是否和 str2 相等。
str1 != str2判断 str1 是否和 str2 不相等。
str1 \> str2判断 str1 是否大于 str2。\>>的转义字符,这样写是为了防止>被误认为成重定向运算符。
str1 \< str2判断 str1 是否小于 str2。同样,\<也是转义字符。

有C语言、C++、Python、Java等编程经验的读者请注意,==、>、< 在大部分编程语言中都用来比较数字,而在 Shell 中,它们只能用来比较字符串,不能比较数字,这是非常奇葩的,大家要习惯。

其次,不管是比较数字还是字符串,Shell 都不支持 >= 和 <= 运算符,切记。


Shell test 字符串比较举例:

#!/bin/bash

read str1

read str2

#检测字符串是否为空

if [ -z "$str1" ] || [ -z "$str2" ]

then

        echo "字符串不能为空"

        exit 0

fi

#比较字符串

if [ $str1 = $str2 ]

then

        echo "两个字符串相等"

else

        echo "两个字符串不相等"

fi

运行结果:

http://c.biancheng.net/

http://c.biancheng.net/shell/

两个字符串不相等

细心的读者可能已经注意到,变量 $str1 和 $str2 都被双引号包围起来,这样做是为了防止 $str1 或者 $str2 是空字符串时出现错误,本文的后续部分将为你分析具体原因。

4、与逻辑运算相关的 test 选项

表4:test 逻辑运算相关选项列表

选 项作 用
expression1 -a expression逻辑与,表达式 expression1 和 expression2 都成立,最终的结果才是成立的。
expression1 -o expression2逻辑或,表达式 expression1 和 expression2 有一个成立,最终的结果就成立。
!expression逻辑非,对 expression 进行取反。

改写上面的代码,使用逻辑运算选项:

#!/bin/bash

read str1

read str2

#检测字符串是否为空

if [ -z "$str1" -o -z "$str2" ] #使用 -o 选项取代之前的 ||

then

        echo "字符串不能为空"

        exit 0

fi

#比较字符串

if [ $str1 = $str2 ]

then

        echo "两个字符串相等"

else

        echo "两个字符串不相等"

fi

前面的代码我们使用两个[]命令,并使用||运算符将它们连接起来,这里我们改成-o选项,只使用一个[]命令就可以了。

5、在 test 中使用变量建议用双引号包围起来

test 和 [] 都是命令,一个命令本质上对应一个程序或者一个函数。即使是一个程序,它也有入口函数,例如C语言程序的入口函数是 main(),运行C语言程序就从 main() 函数开始,所以也可以将一个程序等效为一个函数,这样我们就不用再区分函数和程序了,直接将一个命令和一个函数对应起来即可。

有了以上认知,就很容易看透命令的本质了:使用一个命令其实就是调用一个函数,命令后面附带的选项和参数最终都会作为实参传递给函数。

假设 test 命令对应的函数是 func(),使用test -z $str1命令时,会先将变量 $str1 替换成字符串:

  • 如果 $str1 是一个正常的字符串,比如 abc123,那么替换后的效果就是test -z abc123,调用 func() 函数的形式就是func("-z abc123")。test 命令后面附带的所有选项和参数会被看成一个整体,并作为实参传递进函数。
  • 如果 $str1 是一个空字符串,那么替换后的效果就是test -z,调用 func() 函数的形式就是func("-z "),这就比较奇怪了,因为-z选项没有和参数成对出现,func() 在分析时就会出错。

如果我们给 $str1 变量加上双引号,当 $str1 是空字符串时,test -z "$str1"就会被替换为test -z "",调用 func() 函数的形式就是func("-z \"\""),很显然,-z选项后面跟的是一个空字符串(\"表示转义字符),这样 func() 在分析时就不会出错了。

所以,当你在 test 命令中使用变量时,我强烈建议将变量用双引号""包围起来,这样能避免变量为空值时导致的很多奇葩问题。

6、总结

test 命令比较奇葩,>、<、== 只能用来比较字符串,不能用来比较数字,比较数字需要使用 -eq、-gt 等选项;不管是比较字符串还是数字,test 都不支持 >= 和 <=。有经验的程序员需要慢慢习惯 test 命令的这些奇葩用法。

对于整型数字的比较,我建议大家使用 (()),这在《二十九、Shell if else》一节中已经进行了演示。(()) 支持各种运算符,写法也符合数学规则,用起来更加方便,何乐而不为呢?

几乎完全兼容 test ,并且比 test 更加强大,比 test 更加灵活的是[[ ]][[ ]]不是命令,而是 Shell 关键字,下节将会讲解。

三十二、Shell [[]]详解:检测某个条件是否成立

[[ ]]Shell 内置关键字,它和test类似,也用来检测某个条件是否成立。

test 能做到的,[[ ]] 也能做到,而且 [[ ]] 做的更好;test 做不到的,[[ ]] 还能做到。可以认为 [[ ]] 是 test 的升级版,对细节进行了优化,并且扩展了一些功能。

[[ ]] 的用法为:

[[ expression ]]

当 [[ ]] 判断 expression 成立时,退出状态为 0,否则为非 0 值。注意[[ ]]expression之间的空格,这两个空格是必须的,否则会导致语法错误

1、[[ ]] 不需要注意某些细枝末节

[[ ]] 是 Shell 内置关键字,不是命令,在使用时没有给函数传递参数的过程,所以 test 命令的某些注意事项在 [[ ]] 中就不存在了,具体包括:

  • 不需要把变量名用双引号""包围起来,即使变量是空值,也不会出错。
  • 不需要、也不能对 >、< 进行转义,转义后会出错。

请看下面的演示代码:

#!/bin/bash

read str1

read str2

if [[ -z $str1 ]] || [[ -z $str2 ]] #不需要对变量名加双引号

then

        echo "字符串不能为空"

elif [[ $str1 < $str2 ]] #不需要也不能对 < 进行转义

then

        echo "str1 < str2"

else

        echo "str1 >= str2"

fi

运行结果:

http://c.biancheng.net/shell/
http://data.biancheng.net/
str1 < str2

2、[[ ]] 支持逻辑运算符

对多个表达式进行逻辑运算时,可以使用逻辑运算符将多个 test 命令连接起来,例如:

[ -z "$str1" ] || [ -z "$str2" ]

你也可以借助选项把多个表达式写在一个 test 命令中,例如:

[ -z "$str1" -o -z "$str2" ]

但是,这两种写法都有点“别扭”,完美的写法是在一个命令中使用逻辑运算符将多个表达式连接起来。我们的这个愿望在 [[ ]] 中实现了,[[ ]]  支持 &&、|| 和 ! 三种逻辑运算符

使用 [[ ]] 对上面的语句进行改进:

[[ -z $str1 || -z $str2 ]]

这种写法就比较简洁漂亮了。

注意,[[ ]] 剔除了 test 命令的-o-a选项,你只能使用 || 和 &&。这意味着,你不能写成下面的形式:

[[ -z $str1 -o -z $str2 ]]

当然,使用逻辑运算符将多个 [[ ]] 连接起来依然是可以的,因为这是 Shell 本身提供的功能,跟 [[ ]] 或者 test 没有关系,如下所示:

[[ -z $str1 ]] || [[ -z $str2 ]]

该表总结了各种写法的对错

test 或 [][[ ]]
[ -z "$str1" ] || [ -z "$str2" ][[ -z $str1 ]] || [[ -z $str2 ]]
[ -z "$str1" -o -z "$str2" ][[ -z $str1 -o -z $str2 ]]×
[ -z $str1 || -z $str2 ]×[[ -z $str1 || -z $str2 ]]

3、[[ ]] 支持正则表达式

在 Shell [[ ]] 中,可以使用=~来检测字符串是否符合某个正则表达式,它的用法为:

[[ str =~ regex ]]

str 表示字符串,regex 表示正则表达式。

下面的代码检测一个字符串是否是手机号:

#!/bin/bash

read tel

if [[ $tel =~ ^1[0-9]{10}$ ]]

then

        echo "你输入的是手机号码"

else

        echo "你输入的不是手机号码"

fi

运行结果1:
13203451100
你输入的是手机号码

运行结果2:
132034511009
你输入的不是手机号码

^1[0-9]{10}$的说明:

  • ^匹配字符串的开头(一个位置);
  • [0-9]{10}匹配连续的十个数字;
  • $匹配字符串的末尾(一个位置)。

本文并不打算讲解正则表达式的语法,不了解的读者请猛击正则表达式入门教程-CSDN博客

4、总结

有了 [[ ]],你还有什么理由使用 test 或者 [ ],[[ ]] 完全可以替代之,而且更加方便,更加强大。

但是 [[ ]] 对数字的比较仍然不友好,所以我建议,以后大家使用 if 判断条件时,用 (()) 来处理整型数字,用 [[ ]] 来处理字符串或者文件。

三十三、Shell case in语句详解

和其它编程语言类似,Shell 也支持两种分支结构(选择结构),分别是 if else 语句和 case in 语句。在《二十九、Shell if else》一节中我们讲解了 if else 语句的用法,这节我们就来讲解 case in 语句。

当分支较多,并且判断条件比较简单时,使用 case in 语句就比较方便了。

《二十九、Shell if else》一节的最后给出了一个例子,就是输入一个整数,输出该整数对应的星期几的英文表示,这节我们就用 case in 语句来重写代码,如下所示。

#!/bin/bash

printf "Input integer number: "

read num

case $num in

        1)

                echo "Monday"

                ;;

        2)

                echo "Tuesday"

                ;;

        3)

                echo "Wednesday"

                ;;

        4)

                echo "Thursday"

                ;;

        5)

                echo "Friday"

                ;;

        6)

                echo "Saturday"

                ;;

        7)

                echo "Sunday"

                ;;

        *)

                echo "error"

esac

运行结果:

Input integer number:3↙
Wednesday

看了这个例子,相信大家对 case in 语句有了一个大体上的认识,那么,接下来我们就正式开始讲解 case in 的用法,它的基本格式如下:

case expression in
    pattern1)
        statement1
        ;;
    pattern2)
        statement2
        ;;
    pattern3)
        statement3
        ;;
    ……
    *)
        statementn
esac

case、in 和 esac 都是 Shell 关键字,expression 表示表达式,pattern 表示匹配模式。

  • expression 既可以是一个变量、一个数字、一个字符串,还可以是一个数学计算表达式,或者是命令的执行结果,只要能够得到 expression 的值就可以。
  • pattern 可以是一个数字、一个字符串,甚至是一个简单的正则表达式。

case 会将 expression  的值与 pattern1、pattern2、pattern3 逐个进行匹配:

  • 如果 expression 和某个模式(比如 pattern2)匹配成功,就会执行这模式(比如 pattern2)后面对应的所有语句(该语句可以有一条,也可以有多条),直到遇见双分号;;才停止;然后整个 case 语句就执行完了,程序会跳出整个 case 语句,执行 esac 后面的其它语句。
  • 如果 expression 没有匹配到任何一个模式,那么就执行*)后面的语句(*表示其它所有值),直到遇见双分号;;或者esac才结束。*)相当于多个 if 分支语句中最后的 else 部分。
如果你有C语言、C++\Java等编程经验,这里的 ;;*)就相当于其它编程语言中的 break 和 default。

*)的几点说明:

  • Shell case in 语句中的*)用来“托底”,万一 expression 没有匹配到任何一个模式,*)部分可以做一些“善后”工作,或者给用户一些提示。
  • 可以没有*)部分。如果 expression 没有匹配到任何一个模式,那么就不执行任何操作。

除最后一个分支外(这个分支可以是普通分支,也可以是*)分支),其它的每个分支都必须以;;结尾,;;代表一个分支的结束,不写的话会有语法错误。最后一个分支可以写;;,也可以不写,因为无论如何,执行到 esac 都会结束整个 case in 语句。

上面的代码是 case in 最常见的用法,即 expression 部分是一个变量,pattern 部分是一个数字或者表达式。

case in 和正则表达式

case in 的 pattern 部分支持简单的正则表达式,具体来说,可以使用以下几种格式:

格式说明
*表示任意字符串。
[abc]表示 a、b、c 三个字符中的任意一个。比如,[15ZH] 表示 1、5、Z、H 四个字符中的任意一个。
[m-n]表示从 m 到 n 的任意一个字符。比如,[0-9] 表示任意一个数字,[0-9a-zA-Z] 表示字母或数字。
|表示多重选择,类似逻辑运算中的或运算。比如,abc | xyz 表示匹配字符串 "abc" 或者 "xyz"。

如果不加以说明,Shell 的值都是字符串,expression 和 pattern 也是按照字符串的方式来匹配的;本节第一段代码看起来是判断数字是否相等,其实是判断字符串是否相等。

最后一个分支*)并不是什么语法规定,它只是一个正则表达式,*表示任意字符串,所以不管 expression 的值是什么,*)总能匹配成功。

下面的例子演示了如何在 case in 中使用正则表达式:

#!/bin/bash

printf "Input a character: "

read -n 1 char

case $char in

        [a-zA-Z])

                printf "\nletter\n"

                ;;

        [0-9])

                printf "\nDigit\n"

                ;;

        [0-9])

                printf "\nDigit\n"

                ;;

        [,.?!])

                printf "\nPunctuation\n"

                ;;

        *)

                printf "\nerror\n"

esac

运行结果1:

Input integer number: S
letter

运行结果2:
Input integer number: ,
Punctuation

三十四、Shell while循环详解

while 循环是 Shell 脚本中最简单的一种循环,当条件满足时,while 重复地执行一组语句,当条件不满足时,就退出 while 循环。

Shell while 循环的用法如下:

while condition
do
    statements
done

condition表示判断条件,statements表示要执行的语句(可以只有一条,也可以有多条),dodone都是 Shell 中的关键字

while 循环的执行流程为:

  • 先对 condition 进行判断,如果该条件成立,就进入循环,执行 while 循环体中的语句,也就是 do 和 done 之间的语句。这样就完成了一次循环。
  • 每一次执行到 done 的时候都会重新判断 condition 是否成立,如果成立,就进入下一次循环,继续执行 do 和 done 之间的语句,如果不成立,就结束整个 while 循环,执行 done 后面的其它 Shell 代码。
  • 如果一开始 condition 就不成立,那么程序就不会进入循环体,do 和 done 之间的语句就没有执行的机会。

注意,在 while 循环体中必须有相应的语句使得 condition 越来越趋近于“不成立”,只有这样才能最终退出循环,否则 while 就成了死循环,会一直执行下去,永无休止。

while 语句和 if else 语句中的 condition 用法都是一样的,你可以使用 test 或 [] 命令,也可以使用 (()) 或 [[]],遗忘的读者请对应进行回顾:

  • 《二十九、Shell if else》
  • 《三十、Shell退出状态》
  • 《三十一、Shell test命令》
  • 《三十二、Shell [[]]》

while 循环举例

【实例1】计算从 1 加到 100 的和。

#!/bin/bash

i=1

sum=0

while ((i <= 100))

do

        ((sum += i))

        ((i++))

done

echo "The sum is: $sum"

运行结果:

The sum is: 5050

在 while 循环中,只要判断条件成立,循环就会执行。对于这段代码,只要变量 i 的值小于等于 100,循环就会继续。每次循环给变量 sum 加上变量 i 的值,然后再给变量 i 加 1,直到变量 i 的值大于 100,循环才会停止。

i++语句使得 i 的值逐步增大,让判断条件越来越趋近于“不成立”,最终退出循环。

对上面的例子进行改进,计算从 m 加到 n 的值。

#!/bin/bash

read m

read n

sum=0

while ((m <= n))

do

        ((sum += m))

        ((m++))

done

echo "The sum is: $sum"

运行结果:

1↙
100↙
The sum is: 5050

【实例2】实现一个简单的加法计算器,用户每行输入一个数字,计算所有数字的和。

#!/bin/bash

sum=0

echo "请输入您要计算的数字,按 Ctrl+D 组合键结束读取"

while read n

do

        ((sum += n))

done

echo "The sum is: $sum"

运行结果:

12↙
33↙
454↙
6767↙
1↙
2↙
The sum is: 7269

在终端中读取数据,可以等价为在文件中读取数据,按下 Ctrl+D 组合键表示读取到文件流的末尾,此时 read 就会读取失败,得到一个非 0 值的退出状态,从而导致判断条件不成立,结束循环。

三十五、Shell until循环用法详解

unti 循环和 while 循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立,就终止循环。

until 的使用场景很少,一般使用 while 即可。

Shell until 循环的用法如下:

until condition
do
    statements
done

condition表示判断条件,statements表示要执行的语句(可以只有一条,也可以有多条),dodone都是 Shell 中的关键字

until 循环的执行流程为:

  • 先对 condition 进行判断,如果该条件不成立,就进入循环,执行 until 循环体中的语句(do 和 done 之间的语句),这样就完成了一次循环。
  • 每一次执行到 done 的时候都会重新判断 condition 是否成立,如果不成立,就进入下一次循环,继续执行循环体中的语句,如果成立,就结束整个 until 循环,执行 done 后面的其它 Shell 代码。
  • 如果一开始 condition 就成立,那么程序就不会进入循环体,do 和 done 之间的语句就没有执行的机会。

注意,在 until 循环体中必须有相应的语句使得 condition 越来越趋近于“成立”,只有这样才能最终退出循环,否则 until 就成了死循环,会一直执行下去,永无休止。

上节《三十四、Shell while循环详解》演示了如何求从 1 加到 100 的值,这节我们改用 until 循环,请看下面的代码:

#!/bin/bash

i=1

sum=0

until ((i > 100))

do

        ((sum += i))

        ((i++))

done

echo "The sum is: $sum"

运行结果:

The sum is: 5050

在 while 循环中,判断条件为((i<=100)),这里将判断条件改为((i>100)),两者恰好相反,请读者注意区分。

三十六、Shell for循环和for int循环详解

除了 while 循环和 until 循环,Shell 脚本还提供了 for 循环,它更加灵活易用,更加简洁明了。Shell for 循环有两种使用形式,下面我们逐一讲解。

1、C语言风格的 for 循环

C语言风格的 for 循环的用法如下:

for((exp1; exp2; exp3))
do
    statements
done

几点说明:

  • exp1、exp2、exp3 是三个表达式,其中 exp2 是判断条件,for 循环根据 exp2 的结果来决定是否继续下一次循环;
  • statements 是循环体语句,可以有一条,也可以有多条;
  • do 和 done 是 Shell 中的关键字。

它的运行过程为:
1) 先执行 exp1。

2) 再执行 exp2,如果它的判断结果是成立的,则执行循环体中的语句,否则结束整个 for 循环。

3) 执行完循环体后再执行 exp3。

4) 重复执行步骤 2) 和 3),直到 exp2 的判断结果不成立,就结束循环。

上面的步骤中,2) 和 3) 合并在一起算作一次循环,会重复执行,for 语句的主要作用就是不断执行步骤 2) 和 3)。

exp1 仅在第一次循环时执行,以后都不会再执行,可以认为这是一个初始化语句。exp2 一般是一个关系表达式,决定了是否还要继续下次循环,称为“循环条件”。exp3 很多情况下是一个带有自增或自减运算的表达式,以使循环条件逐渐变得“不成立”。

for 循环的执行过程可用下图表示:

Shell for循环执行流程

​下面我们给出一个实际的例子,计算从 1 加到 100 的和。

#!/bin/bash

sum=0

for ((i=1; i<=100; i++))

do

        ((sum += i))

done

echo "The sum is: $sum"

运行结果:

The sum is: 5050

代码分析:

1) 执行到 for 语句时,先给变量 i 赋值为 1,然后判断 i<=100 是否成立;因为此时 i=1,所以 i<=100 成立。接下来会执行循环体中的语句,等循环体执行结束后(sum 的值为1),再计算 i++。

2) 第二次循环时,i 的值为2,i<=100 成立,继续执行循环体。循环体执行结束后(sum的值为3),再计算 i++。

3) 重复执行步骤 2),直到第 101 次循环,此时 i 的值为 101,i<=100 不再成立,所以结束循环。

由此我们可以总结出 for 循环的一般形式为:

for(( 初始化语句; 判断条件; 自增或自减 ))
do
    statements
done

for 循环中的三个表达式

for 循环中的 exp1(初始化语句)、exp2(判断条件)和 exp3(自增或自减)都是可选项,都可以省略(分号;必须保留)。

1) 修改“从 1 加到 100 的和”的代码,省略 exp1:

#!/bin/bash

sum=0

i=1

for ((; i<=100; i++))

do

        ((sum += i))

done

echo "The sum is: $sum"

可以看到,将i=1移到了 for 循环的外面。

2) 省略 exp2,就没有了判断条件,如果不作其他处理就会成为死循环,我们可以在循环体内部使用 break 关键字强制结束循环

#!/bin/bash

sum=0

for ((i=1; ; i++))

do

        if(( i>100 )); then

                break

        fi

        ((sum += i))

done

echo "The sum is: $sum"

break 是 Shell 中的关键字,专门用来结束循环,后续章节还会深入讲解。

3) 省略了 exp3,就不会修改 exp2 中的变量,这时可在循环体中加入修改变量的语句。例如:

#!/bin/bash

sum=0

for ((i=1; i<=100; ))

do

        ((sum += i))

        ((i++))

done

echo "The sum is: $sum"

4) 最后给大家看一个更加极端的例子,同时省略三个表达式:

#!/bin/bash

sum=0

i=0

for (( ; ; ))

do

        if(( i>100 )); then

                break

        fi

                ((sum += i))

                ((i++))

        done

echo "The sum is: $sum"

这种写法并没有什么实际意义,仅仅是为了给大家做演示。

2、Python风格的 for in 循环

Python 风格的 for in 循环的用法如下:

for variable in value_list
do
    statements
done

variable 表示变量,value_list 表示取值列表,in 是 Shell 中的关键字

in value_list 部分可以省略,省略后的效果相当于 in $@,本小节第三部分将会详细讲解。

每次循环都会从 value_list 中取出一个值赋给变量 variable,然后进入循环体(do 和 done 之间的部分),执行循环体中的 statements。直到取完 value_list 中的所有值,循环就结束了。

Shell for in 循环举例:

#!/bin/bash

sum=0

for n in 1 2 3 4 5 6

do

        echo $n

        ((sum+=n))

done

echo "The sum is "$sum

运行结果:

1
2
3
4
5
6
The sum is 21

3、对 value_list 的说明

取值列表 value_list 的形式有多种,你可以直接给出具体的值,也可以给出一个范围,还可以使用命令产生的结果,甚至使用通配符,下面我们一一讲解。

(1)直接给出具体的值

可以在 in 关键字后面直接给出具体的值,多个值之间以空格分隔,比如1 2 3 4 5"abc" "390" "tom"等。

上面的代码中用一组数字作为取值列表,下面我们再演示一下用一组字符串作为取值列表:

#!/bin/bash

for str in "C语言中文网" "http://c.biancheng.net/" "成立7年了" "日IP数万"

do

        echo $str

done

运行结果:

C语言中文网
http://c.biancheng.net/
成立7年了
日IP数万

(2) 给出一个取值范围

给出一个取值范围的具体格式为:

{start..end}

start 表示起始值,end 表示终止值;注意中间用两个点号相连,而不是三个点号。根据笔者的实测,这种形式只支持数字和字母。

例如,计算从 1 加到 100 的和:

#!/bin/bash

sum=0

for n in {1..100}

do

        ((sum+=n))

done

echo $sum

运行结果:

5050

再如,输出从 A 到 z 之间的所有字符:

#!/bin/bash

for c in {A..z}

do

        printf "%c" $c

done

输出结果:

ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz

可以发现,Shell 是根据 ASCII 码表来输出的。

(3) 使用命令的执行结果

使用反引号``或者$()都可以取得命令的执行结果,我们在《一、Shell变量》一节中已经进行了详细讲解,并对比了两者的优缺点。本节我们使用$()这种形式,因为它不容易产生混淆。

例如,计算从 1 到 100 之间所有偶数的和:

#!/bin/bash

sum=0

for n in $(seq 2 2 100)

do

        ((sum+=n))

done

echo $sum

运行结果:

2550

seq 是一个 Linux 命令,用来产生某个范围内的整数,并且可以设置步长,不了解的读者请自行百度。seq 2 2 100表示从 2 开始,每次增加 2,到 100 结束。

再如,列出当前目录下的所有 Shell 脚本文件:

#!/bin/bash

for filename in $(ls *.sh)

do

        echo $filename

done

运行结果:

demo.sh
test.sh
abc.sh

ls 是一个 Linux 命令,用来列出当前目录下的所有文件,*.sh表示匹配后缀为.sh的文件,也就是 Shell 脚本文件。

(4) 使用 Shell 通配符

Shell 通配符可以认为是一种精简化的正则表达式通常用来匹配目录或者文件,而不是文本,不了解的读者请猛击Linux Shell 通配符 / glob 模式-CSDN博客

有了 Shell 通配符,不使用 ls 命令也能显示当前目录下的所有脚本文件,请看下面的代码:

#!/bin/bash

for filename in *.sh

do

        echo $filename

done

运行结果:

demo.sh
test.sh
abc.sh

(5)使用特殊变量

Shell 中有多个特殊的变量,例如 $#、$*、$@、$?、$$ 等(不了解的读者请猛击《五、Shell特殊变量》),在 value_list 中就可以使用它们。

#!/bin/bash

function func(){

        for str in $@

        do

                echo $str

        done

}

func C++ Java Python C#

运行结果:

C++
Java
Python
C#

其实,我们也可以省略 value_list省略后的效果和使用$@一样。请看下面的演示:

#!/bin/bash

function func(){

        for str

        do

                echo $str

        done

}

func C++ Java Python C#

运行结果:

C++
Java
Python
C#

三十七、Shell select in循环详解

select in 循环用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。

select in 是 Shell 独有的一种循环,非常适合终端(Terminal)这样的交互场景,C语言、C++、Java、Python、C#等其它编程语言中是没有的。

Shell select in 循环的用法如下:

select variable in value_list
do
    statements
done

variable 表示变量,value_list 表示取值列表,in 是 Shell 中的关键字。你看,select in 和for in 的语法是多么地相似。

我们先来看一个 select in 循环的例子:

#!/bin/bash

echo "What is your favourite OS?"

select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"

do

        echo $name

done

echo "You have selected $name"

运行结果:

What is your favourite OS?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#? 4↙
You have selected UNIX
#? 1↙
You have selected Linux
#? 9↙
You have selected

#? 2↙
You have selected Windows
#?^D

#?用来提示用户输入菜单编号;^D表示按下 Ctrl+D 组合键,它的作用是结束 select in 循环

运行到 select 语句后,取值列表 value_list 中的内容会以菜单的形式显示出来,用户输入菜单编号,就表示选中了某个值,这个值就会赋给变量 variable,然后再执行循环体中的 statements(do 和 done 之间的部分)。

每次循环时 select 都会要求用户输入菜单编号,并使用环境变量 PS3 的值作为提示符,PS3 的默认值为#?,修改 PS3 的值就可以修改提示符。

如果用户输入的菜单编号不在范围之内,例如上面我们输入的 9,那么就会给 variable 赋一个空值;如果用户输入一个空值(什么也不输入,直接回车),会重新显示一遍菜单。

注意,select 是无限循环(死循环),输入空值,或者输入的值无效,都不会结束循环,只有遇到 break 语句,或者按下 Ctrl+D 组合键才能结束循环。

完整实例

select in 通常和case in 一起使用,在用户输入不同的编号时可以做出不同的反应。

修改上面的代码,加入 case in 语句:

#!/bin/bash

echo "What is your favourite OS?"

select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"

do

        case $name in

                "Linux")

                        echo "Linux是一个类UNIX操作系统,它开源免费,运行在各种服务器设备和嵌入式设备。"

                break

                ;;

                "Windows")

                        echo "Windows是微软开发的个人电脑操作系统,它是闭源收费的。"

                break

                ;;

                "Mac OS")

                        echo "Mac OS是苹果公司基于UNIX开发的一款图形界面操作系统,只能运行与苹果提供的硬件之上。"

                break

                ;;

                "UNIX")

                        echo "UNIX是操作系统的开山鼻祖,现在已经逐渐退出历史舞台,只应用在特殊场合。"

                break

                ;;

                "Android")

                        echo "Android是由Google开发的手机操作系统,目前已经占据了70%的市场份额。"

                break

                ;;

        *)

                echo "输入错误,请重新输入"

        esac

done

用户只有输入正确的编号才会结束循环,如果输入错误,会要求重新输入。

运行结果1,输入正确选项:

What is your favourite OS?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#? 2
Windows是微软开发的个人电脑操作系统,它是闭源收费的。

运行结果2,输入错误选项:

What is your favourite OS?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#? 7
输入错误,请重新输入
#? 4
UNIX是操作系统的开山鼻祖,现在已经逐渐退出历史舞台,只应用在特殊场合。

运行结果3,输入空值:

What is your favourite OS?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#? 3
Mac OS是苹果公司基于UNIX开发的一款图形界面操作系统,只能运行与苹果提供的硬件之上。

三十八、Shell break和continue跳出循环详解

使用 while、until、for、select 循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用 break 或者 continue 关键字。

在C语言、C++、C#、Python、Java等大部分编程语言中,break 和 continue 只能跳出当前层次的循环,内层循环中的 break 和 continue 对外层循环不起作用;但是 Shell 中的 break 和 continue 却能够跳出多层循环,也就是说,内层循环中的 break 和 continue 能够跳出外层循环。

在实际开发中,break 和 continue 一般只用来跳出当前层次的循环,很少有需要跳出多层循环的情况。

1、break 关键字

Shell break 关键字的用法为:

break n

n 表示跳出循环的层数,如果省略 n,则表示跳出当前的整个循环。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。

Shell break关键字原理示意图

​图1:Shell break关键字原理示意图

【实例1】不断从终端读取用户输入的正数,求它们相加的和:

#!/bin/bash

sum=0

while read n; do

        if((n>0)); then

                ((sum+=n))

        else

                break

        fi

done

echo "sum=$sum"

运行结果:

10↙
20↙
30↙
0↙
sum=60

while 循环通过 read 命令的退出状态来判断循环条件是否成立,只有当按下 Ctrl+D 组合键(表示输入结束)时,read n才会判断失败,此时 while 循环终止。

除了按下 Ctrl+D 组合键,你还可以输入一个小于等于零的整数,这样会执行 break 语句来终止循环(跳出循环)。

【实例2】使用 break 跳出双层循环。

如果 break 后面不跟数字的话,表示跳出当前循环,对于有两层嵌套的循环,就得使用两个 break 关键字。例如,输出一个 4*4 的矩阵:

#!/bin/bash

i=0

while ((++i)); do #外层循环

        if((i>4)); then

                break #跳出外层循环

        fi

        j=0;

        while ((++j)); do #内层循环

                if((j>4)); then

                        break #跳出内层循环

                fi

                printf "%-4d" $((i*j))

        done

        printf "\n"

done

运行结果:

1 2 3 4

2 4 6 8

3 6 9 12

4 8 12 16

当 j>4 成立时,执行第二个 break,跳出内层循环;外层循环依然执行,直到 i>4 成立,跳出外层循环。内层循环共执行了 4 次,外层循环共执行了 1 次。

我们也可以在 break 后面跟一个数字,让它一次性地跳出两层循环,请看下面的代码:

#!/bin/bash

i=0

while ((++i)); do #外层循环

        j=0;

        while ((++j)); do #内层循环

                if((i>4)); then

                        break 2 #跳出内外两层循环

                fi

                if((j>4)); then

                        break #跳出内层循环

                fi

                printf "%-4d" $((i*j))

        done

        printf "\n"

done

修改后的代码将所有 break 都移到了内层循环里面。读者需要重点关注break 2这条语句,它使得程序可以一次性跳出两层循环,也就是先跳出内层循环,再跳出外层循环。

2、continue 关键字

Shell continue 关键字的用法为:

continue n

n 表示循环的层数:

  • 如果省略 n,则表示 continue 只对当前层次的循环语句有效,遇到 continue 会跳过本次循环,忽略本次循环的剩余代码,直接进入下一次循环。
  • 如果带上 n,比如 n 的值为 2,那么 continue 对内层和外层循环语句都有效,不但内层会跳过本次循环,外层也会跳过本次循环,其效果相当于内层循环和外层循环同时执行了不带 n 的 continue。这么说可能有点难以理解,稍后我们通过代码来演示。

continue 关键字也通常和 if 语句一起使用,即满足条件时便跳出循环。 

Shell continue关键字原理示意图

图2:Shell continue关键字原理示意图

【实例1】不断从终端读取用户输入的 100 以内的正数,求它们的和:

#!/bin/bash

sum=0

while read n; do

        if((n<1 || n>100)); then

                continue

        fi

        ((sum+=n))

done

echo "sum=$sum"

运行结果:

10↙
20↙
-1000↙
5↙
9999↙
25↙
sum=60

变量 sum 最终的值为 60,-1000 和 9999 并没有计算在内,这是因为 -1000 和 9999 不在 1~100 的范围内,if 判断条件成立,所以执行了 continue 语句,跳过了当次循环,也就是跳过了((sum+=n))这条语句。

注意,只有按下 Ctrl+D 组合键输入才会结束,read n才会判断失败,while 循环才会终止。

【实例2】使用 continue 跳出多层循环,请看下面的代码:

#!/bin/bash

for((i=1; i<=5; i++)); do

        for((j=1; j<=5; j++)); do

                if((i*j==12)); then

                        continue 2

                fi

                printf "%d*%d=%-4d" $i $j $((i*j))

        done

        printf "\n"

done

运行结果:

1*1=1 1*2=2 1*3=3 1*4=4 1*5=5

2*1=2 2*2=4 2*3=6 2*4=8 2*5=10

3*1=3 3*2=6 3*3=9 4*1=4 4*2=8 5*1=5 5*2=10 5*3=15 5*4=20 5*5=25

从运行结果可以看出,遇到continue 2时,不但跳过了内层 for 循环,也跳过了外层 for 循环。

3、break 和 continue 的区别

break 用来结束所有循环,循环语句不再有执行的机会;continue 用来结束本次循环,直接跳到下一次循环,如果循环条件成立,还会继续循环。

三十九、Shell函数详解(函数定义、函数调用)

Shell 函数的本质是一段可以重复使用的脚本代码,这段代码被提前编写好了,放在了指定的位置,使用时直接调取即可。

Shell 中的函数和C++、C#、Python、Java 等其它编程语言中的函数类似,只是在语法细节有所差别。

Shell 函数定义的语法格式如下:

function name() {
    statements
    [return value]

}

对各个部分的说明:

  • function是 Shell 中的关键字,专门用来定义函数;
  • name是函数名;
  • statements是函数要执行的代码,也就是一组语句;
  • return value表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写。

{ }包围的部分称为函数体,调用一个函数,实际上就是执行函数体中的代码。

1、函数定义的简化写法

如果你嫌麻烦,函数定义时也可以不写 function 关键字:

name() {
    statements
    [return value]
}

如果写了 function 关键字,也可以省略函数名后面的小括号:

function name {
    statements
    [return value]
}

我建议使用标准的写法,这样能够做到“见名知意”,一看就懂。

2、函数调用

调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:

name

如果传递参数,那么多个参数之间以空格分隔

name param1 param2 param3

不管是哪种形式,函数名字后面都不需要带括号。

和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。

Shell 也不限制定义和调用的顺序,你可以将定义放在调用的前面,也可以反过来,将定义放在调用的后面。

3、实例演示

1) 定义一个函数,输出 Shell 教程的地址:

#!/bin/bash

#函数定义

function url(){

        echo "http://c.biancheng.net/shell/"

}

#函数调用

url

运行结果:

http://c.biancheng.net/shell/

你可以将调用放在定义的前面,也就是写成下面的形式:

#!/bin/bash

#函数调用

url

#函数定义

function url(){

        echo "http://c.biancheng.net/shell/"

}

2) 定义一个函数,计算所有参数的和:

#!/bin/bash

function getsum(){

        local sum=0

        for n in $@

        do

                ((sum+=n))

        done

        return $sum

}

getsum 10 20 55 15 #调用函数并传递参数

echo $?

运行结果:

100

$@表示函数的所有参数,$?表示函数的退出状态(返回值)。关于如何获取函数的参数,后边会详细讲解。

此处我们借助 return 关键字将所有数字的和返回,并使用$?得到这个值,这种处理方案在其它编程语言中没有任何问题,但是在 Shell 中是非常错误的,Shell 函数的返回值和其它编程语言大有不同,我们将在《四十一、Shell函数返回值》中展开讨论。

四十、Shell函数参数

和 C++、C#、Python、Java 等大部分编程语言不同,Shell 中的函数在定义时不能指明参数,但是在调用时却可以传递参数。

函数参数是《四、Shell位置参数(命令行参数)》 的一种,在函数内部可以使用$n来接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。

除了$n,还有另外三个比较重要的变量:

  • $#可以获取传递的参数的个数;
  • $@或者$*可以一次性获取所有的参数(通过《六、Shell $*和$@之间的区别》可以了解更多内容)。

$n、$#、$@、$* 都属于特殊变量,不了解的读者请看《五、Shell特殊变量:Shell $#、$*、$@、$?、$$》

【实例1】使用 $n 来接收函数参数

#!/bin/bash

#定义函数

function show(){

        echo "Tutorial: $1"

        echo "URL: $2"

        echo "Author: "$3

        echo "Total $# parameters"

}

#调用函数

show C# http://c.biancheng.net/csharp/ Tom

运行结果:

Tutorial: C#
URL: http://c.biancheng.net/csharp/
Author: Tom
Total 3 parameters

注意,第 7 行代码的写法有点不同,这里使用了 《九、Shell字符串拼接(连接、合并)》技巧。

【实例2】使用 $@ 来遍历函数参数。

定义一个函数,计算所有参数的和:

#!/bin/bash

function getsum(){

        local sum=0

        for n in $@

        do

                ((sum+=n))

        done

        echo $sum

        return 0

}

#调用函数并传递参数,最后将结果赋值给一个变量

total=$(getsum 10 20 55 15)

echo $total

#也可以将变量省略

echo $(getsum 10 20 55 15)

运行结果:

100

100

四十一、Shell函数返回值精讲

在 C++、Java、C#、Python 等大部分编程语言中,返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果就通过 return 语句返回

但是 Shell 中的返回值表示的是函数的退出状态:返回值为 0 表示函数执行成功了,返回值为非 0 表示函数执行失败(出错)了。if、while、for 等语句都是根据函数的退出状态来判断条件是否成立。

Shell 函数的返回值只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。

函数执行失败时,可以根据返回值(退出状态)来判断具体出现了什么错误,比如一个打开文件的函数,我们可以指定 1 表示文件不存在,2 表示文件没有读取权限,3 表示文件类型不对。

如果函数体中没有 return 语句,那么使用默认的退出状态,也就是最后一条命令的退出状态。如果这就是你想要的,那么更加严谨的写法为:

return $?

$?是一个特殊变量,用来获取上一个命令的退出状态,或者上一个函数的返回值,请看《七、Shell $?:获取函数返回值或者上一个命令的退出状态》了解更多。

如何得到函数的处理结果?

有人可能会疑惑,既然 return 表示退出状态,那么该如何得到函数的处理结果呢?比如,我定义了一个函数,计算从 m 加到 n 的和,最终得到的结果该如何返回呢?

这个问题有两种解决方案:

  • 一种是借助全局变量,将得到的结果赋值给全局变量;
  • 一种是在函数内部使用 echo、printf 命令将结果输出,在函数外部使用$()或者``捕获结果。

下面我们具体来定义一个函数 getsum,计算从 m 加到 n 的和,并使用以上两种解决方案。

【实例1】将函数处理结果赋值给一个全局变量

#!/bin/bash

sum=0 #全局变量

function getsum(){

        for((i=$1; i<=$2; i++)); do

                ((sum+=i)) #改变全局变量

        done

        return $? #返回上一条命令的退出状态

}

read m

read n

if getsum $m $n; then

        echo "The sum is $sum" #输出全局变量

else

        echo "Error!"

fi

运行结果:

1
100
The sum is 5050

这种方案的弊端是:定义函数的同时还得额外定义一个全局变量,如果我们仅仅知道函数的名字,但是不知道全局变量的名字,那么也是无法获取结果的。

【实例2】在函数内部使用 echo 输出结果

#!/bin/bash

function getsum(){

        local sum=0 #局部变量

        for((i=$1; i<=$2; i++)); do

                ((sum+=i))

        done

        echo $sum

        return $?

}

read m

read n

total=$(getsum $m $n)

echo "The sum is $total"

#也可以省略 total 变量,直接写成下面的形式

#echo "The sum is "$(getsum $m $n)

运行结果:

1↙
100↙
The sum is 5050

代码中总共执行了两次 echo 命令,但是却只输出一次,这是因为$()捕获了第一个 echo 的输出结果,它并没有真正输出到终端上。除了$(),你也可以使用``来捕获 echo 的输出结果,请在《一、Shell变量》了解两者的区别。

这种方案的弊端是:如果不使用$(),而是直接调用函数,那么就会将结果直接输出到终端上,不过这貌似也无所谓,所以我推荐这种方案。

总起来说,虽然C语言、C++、Java 等其它编程语言中的返回值用起来更加方便,但是 Shell 中的返回值有它独特的用途,所以不要带着传统的编程思维来看待 Shell 函数的返回值。

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值