本文介绍一下 shell 的语法。
一、变量
在 shell 里,使用变量之前通常并不需要事先为他们做出声明,需要使用的时候直接创建就行了。默认情况下,所有变量都被看做字符串并以字符串来存储,即使它们被赋值为数值时也是如此。shell 和一些工具会在需要时把数值型字符串转换成对应的数值以对它们进行操作。
1.1 变量的命名
shell 变量的命名规则如下:开头是一个字母或下划线,后面可以接任意长度的字母、数字或下划线符号,变量名的字符长度并无限制(Bourne shell中)。不过为了兼容性(一些早期的shell里变量名是有长度限制的),一般还是不要超过255个字符。另外,Linux 区分大小写。当用户自己定义变量的时候,要注意变量名不能与 shell 中的关键字重名。
1.2 变量的赋值
shell 中变量的赋值方式如下:
变量名=值 # 注意 赋值语句两边不能有空格
注意,赋值语句两边不能有空格(即 “=” 号两边不能有空格)。等号右边若有空格的话,需要加上引号(单引号或双引号都是可以的)。shell 中可以在变量名前加上 $ 字符来取变量的值。用一个简单的例子演示一下:
#!/bin/bash name=tongye age=23 address="Hubei Wuhan" money='10$' echo "$name $age in $address" echo "I have $money!" exit 0
输出结果如下:
这里需要注意的是单引号和双引号的用法:在单引号中,所有特殊字符都没有特殊含义;在双引号中,"$"、" ` "(反引号)、"\" 有特殊含义,其余的没有特殊含义。至于反引号 " ` ",反引号中可以用来引用系统命令,其中的内容将会被优先执行,其功能与 $(...) 一样,详情后面再做叙述。
1.3 变量的类型
shell 中有四种类型的变量:用户自定义变量、环境变量、位置参数变量和预定义变量。
1) 用户自定义变量
用户自定义变量只会在当前 shell 中生效,也就是“局部变量”,上面程序中的 name、age、address、money 等都是用户自定义变量,只能在变量所在的那个 shell 脚本中生效。用户自定义变量一般用小写字母来命名。
2) 环境变量
当一个 shell 脚本程序开始执行时,一些变量会根据环境设置中的值进行初始化,这些变量通常用大写字母做名字,以便与用户自定义变量做区分,被称为环境变量。环境变量可以在当前 shell 和这个 shell 的所有子 shell 中生效。如果把环境变量写入相应的配置文件(如 /etc/profile ),那么这个环境变量就会在所有的 shell 中生效。系统自带的环境变量的名字不可更改,但是值可以按需更改。用户也可以使用 export 命令在 shell 中自己创建环境变量:
export 变量名=变量值 # 创建环境变量并赋值
一些主要的系统环境变量如下:
环境变量 | 描述 |
$HOME | 当前用户的家目录 |
$PATH | 以冒号分隔的用来搜索命令的目录列表,决定了 shell 将到哪些目录中去寻找命令或程序 |
$PS1 | 命令提示符,通常是 $ 字符,也可以自行设置 |
$PS2 | 二级提示符,用来提示后续的输入,通常是 > 字符 |
$IFS | 输入域分隔符。当 shell 读取输入时,它给出用来分隔单词的一组字符,通常是空格、制表符和换行符 |
$0 | shell 脚本的名字 |
$# | 传递给脚本的参数个数 |
$$ | shell 脚本的进程号(PID),脚本程序通常会用它来生成一个唯一的临时文件,如 /tmp/tmpfile_$$ |
3) 位置参数变量
位置参数变量主要用来向脚本中传递参数或数据,变量名不能自定义,变量作用也是固定的。主要有以下几种位置参数变量:
位置参数变量 | 描述 |
$1、$2、... | 脚本程序的参数,分别代表程序的第1个参数、第2个参数、... 程序第10个以上的参数需要用大括号包含,如 ${10} |
$* | 代表命令行中的所有参数。在一个变量中将所有参数列出,各参数之间用环境变量 IFS 中的第一个字符分隔开。 |
$@ | 和 $* 一样,也包含了命令行中的所有参数,但是不使用 IFS 环境变量,即使 IFS 为空,参数也是分开显示的 |
关于 $0 和 $#,在有些资料上,也把这两个归为位置参数变量,本文是把它们归为了环境变量。其中,$0 代表 shell 脚本本身(不算在参数行列),$# 代表传递给脚本的参数个数(不包括 $0)。
关于 $* 和 $@,这二者的区别就在 $* 使用 IFS 所定义的分隔符来分隔参数而 $@ 没有使用。$* 将所有的参数视为一个整体,而 $@ 将所有的参数分别视为单独的个体。一般来说,采用 $@ 来访问脚本程序的参数会比较好,不必担心 IFS 所设置的分隔符为空而导致各参数连在一起分不清楚。
4) 预定义变量
预定义变量是在 bash 中已经定义好了的变量,变量名不能自定义,变量作用也是固定的。实际上,位置参数变量就是预定义变量的一种。 除了上面介绍的一些外,这里再介绍两个:
$? :保存最后一次执行的命令的返回状态。如果 $? 的值为 0 ,则表明上一个命令成功执行;如果值非 0 ,则表明上一个命令没有成功执行。
$! :用于保存后运行的最后一个进程的 PID 号。
二、算术运算
shell 的算术运算符与 C 语言里的差不多,优先级与顺序也相同。但是,由于 shell 中所有变量都是被看做字符串来存储的,因此,要处理算术表达式,还需要使用一些特殊手段将数值型字符串转换成相应的数值。
2.1 使用 expr 命令对算术表达式求值
expr 命令将它的参数当做一个表达式来求值,可以用来进行数学运算。如下:
#!/bin/basha=2b=3c=`expr $a + $b`echo $c exit 0
这段代码的输出结果是:5 。注意使用 expr 命令的那一行,使用的是反引号 `` ,反引号中的内容会被优先执行,所以这一行代码的作用是将 expr $a + $b 这一表达式的执行结果赋给变量 c 。也可以使用 $(...) 来替代反引号: c=$(expr $a + $b)。
关于反引号和 $( .. ) 表达式,需要说明的一点是,反引号是一种比较老的语法形式,如果你希望自己写的脚本具备非常好的可移植性,那么可以使用反引号,新的脚本程序一般都使用 $(...) 来替代反引号了,以避免在反引号中处理一些特殊字符时需要应用的一些相对复杂的规则。比如,如果想在 ` ... ` 结构中使用 ` (反引号)字符,则需要使用转义符 \ 来进行转义,这样会使代码阅读起来较为困难。反引号和 $( ... ) 都可以用来引用系统命令。
expr 命令的功能十分强大,可以支持许多表达式求值运算:
表达式 | 说明 |
expr1 | expr2 | 若 expr1 非零,则等于 expr1 ,否则等于 expr2。 |
expr1 & expr2 | 只要有一个表达式为零,则等于零,否则等于 expr1。 |
expr1 = expr2 | 等于(与 == 是同义的),若两式相等则结果为1,不等结果为0 |
expr1 > expr2 | 大于 |
expr1 >= expr2 | 大于等于 |
expr1 < expr2 | 小于 |
expr1 <= expr2 | 小于等于 |
expr1 != expr2 | 不等于 |
expr1 + expr2 | 加 |
expr1 - expr2 | 减 |
expr1 * expr2 | 乘 |
expr1 / expr2 | 整除 |
expr1 % expr2 | 取余 |
注意:在 expr 命令所支持的操作符中,“ | 、 & 、< 、<= 、> 、 >= 、 * ” 这几个需要用 \ 符进行转义再使用。此外,表达式的各字符之间需要用空格隔开。 用一段代码演示一下 expr 命令的使用方法:
#!/bin/bash a=5;b=6;c=0 echo $(expr $a \| $c) # 输出 5 echo $(expr $b \& $c) # 输出 0 echo $(expr $a \& $b) # 输出 5 echo $(expr $a \<= $b) # 输出 1 echo $(expr $a \* $b) # 输出 30 echo $(expr $a = 2) # 输出 1 exit 0
expr 命令中的 | 和 & 操作符比较特殊,并不是我们常见的按位或和按位与,而是逻辑操作:
expr1 \| expr2 是逻辑或运算,结果为真(1 表示真,0表示假)则返回 expr1 的值,否则返回 expr2 的值,具有短路功能(expr1 为非零,则表达式一定非零,直接返回 expr1 的值,而不必在对 expr2 的值做判断);
expr1 \& expr2 是逻辑与运算,结果为真则返回 expr1 的值,否则返回 expr2 的值,具有短路功能(expr1 为零,则表达式一定为零,直接返回零,而不必再对 expr2 的值做判断)。
2.2 使用 $(( ... )) 的方式对算术表达式求值
expr 虽然功能强大,但是上面已经提到,在进行一些运算的时候,需要使用 \ 符来进行转义,这对于阅读代码的人来说并不友好。另一方面,expr 命令执行起来其实很慢,因为它需要调用一个新的 shell 来处理 expr 命令。更新更好的一种做法是使用 $((...)) 扩展的方式。只需要将准备求值的表达式放在 $((...)) 的括号中即可进行简单的算术求值。且,所有支持 $(( ... )) 的shell,都可以让用户在提供变量名称时,无须前置 $ 符。用一段代码演示一下用法:
#!/bin/bash a=5;b=6 echo $(($a + $b)) # 输出 11 。在变量名前加上 $,这在shell中一般是取变量值的意思 echo $((a + b)) # 输出 11 。可见,变量前不加 $ 也是可以的,为了简便,后面的代码就不加 $ 了 echo $((a | b)) # 输出 7 。这里的 | 是按位或操作符 echo $((a || b)) # 输出 1 。这里的 || 是逻辑或操作符 echo $((a & b)) # 输出 4 。这里的 & 是按位与操作符 echo $((a && b)) # 输出 1 。这里的 && 是逻辑与操作符 echo $((a * b)) # 输出 30 echo $((a == b)) # 输出 0 exit 0
可以看到, $(( ... )) 与 expr 命令还是有些不同之处的:
1)首先一些操作符的功能不同( | 和 & );
2)其次, expr 表达式在使用一些操作符时是需要使用转义操作的,而 $(( ... )) 结构不需要;
3)还有一点就是,$(( ... )) 结构中取变量的值可以不使用 $ 操作符。
一些更具体的使用方法,建议亲自动手去操作一下,这里就不再作更详细的叙述了。
三、使用 bash 计算器在shell脚本中进行浮点运算
可以发现,bash 中的基本算术运算只支持整数运算,要进行浮点运算的话,需要另寻方法,bash 计算器就是处理浮点运算的一个常见方案。
3.1 bash 计算器 bc
bash 计算器允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。可以使用 yum 安装 bc 命令:
sudo yum install bc
在命令行输入 bc 指令,即可进入 bash 计算器的界面:
如图所示,是使用 bc 进行简单的浮点运算。其中变量 scale 是 bc 命令的内建变量,用于控制计算结果保留到小数点后多少位,默认为0,故默认情况下,使用 5 / 4 得到的结果是1。在后续的程序中,将 scale 的值设置为了 4,即保计算结果留到小数点后 4 位,可以看到 5 / 4 的值为1.2500,保留到了小数点后 4 位。
实际上,bash 计算器是一种编程语言,除了识别数字外,还可以识别变量、表达式等许多东西,如下:
1)数字(包括整数和浮点数)
2)变量(简单变量和数组)
3)注释(# 或 /* */ 都能识别)
4)表达式
5)编程语句(if-then 等)
6)函数
举个例子:
$ bc -q # 进入 bc 命令行val1=5 # 新建变量 val1 并赋值val2=1.1 # 新建变量 val2 并赋值val3=val1 * val2 # 新建变量 val3 并赋值,val3 的值为 val1 和 val2 的乘积print val3 # 打印变量 val3 的值
最终输出结果为5.5
3.2 在脚本中使用 bc
既然已经知道了 bc 命令的用法,那么在脚本中使用 bc 也就很简单了,只需要使用反引号`` 或者 $() 将 bc 命令包含起来就行了,即:
value=$(echo "options;expression" | bc)
其中,options用于进行一些变量的设置(如 scale 变量的设置,或其他的一些自定义变量),如果需要设置多个变量,只需要在变量之间用分号进行隔开;expression 参数定义了通过 bc 执行的数学表达式。举个例子说明 bc 命令在 shell 脚本中的用法:
#!/bin/bashval1=$(echo "scale=4;5/4" | bc)echo the value of val1 is $val1
这个程序的输出结果为:the value of val1 is 1.2500
四、shell 中的条件判断命令 test 和 [
test 命令可以处理 shell 脚本中的各类工作。它产生的不是一般的输出,而是可使用的退出状态。test 命令通过接受各种不同的参数,来控制要执行哪种测试。在许多系统上,test 命令与 [ 命令的作用其实是一样的,使用 [ 命令的时候,一般在结尾加上 ] 符号,使代码更具可读性。另外,需要注意一点的是,在使用 [ 命令时,[ 符号与被检查的语句之间应该留有空格。shell 中通常使用 test 命令来产生控制结构所需要的条件,根据 test 命令的退出码决定是否需要执行后面的代码。
test 命令可以使用的条件类型有三类:字符串比较、算术比较和与文件有关的条件测试。
1)字符串比较
表达式 | 结果 |
string1 = string2 | 如果两个字符串相同则结果为真 |
string1 != string2 | 如果两个字符串不同则结果为真 |
-n string | 如果字符串不为空则结果为真 |
-z string | 如果字符串为空(null),则结果为真 |
使用方法如下:
str1="tongye"str2="ttyezi"# 用 test 命令,test 语句的结果将作为 if 的判断条件,结果为真即条件为真,则执行 if 下面的语句if test "$str1" = "$str2" ; then ....fi# 用 [ 命令的话,可以这样,注意 [ 与表达式之间要有空格if [ "$str1" != "$str2" ] ; then ....fi if [ -n "$str1" ] ; then .... fi
使用字符串比较的时候,必须给变量加上引号 " " ,避免因为空字符或字符串中的空格导致一些问题。实际上,对于条件测试语句里的变量,都建议加上双引号,能做字符串比较的时候,不要用数值比较。
2)算术比较
算术比较 | 结果 |
expr1 -eq expr2 | 如果两个表达式相等,则结果为真 |
expr1 -ne expr2 | 如果两个表达式不相等,则结果为真 |
expr1 -gt expr2 | 如果 expr1 > expr2 ,则结果为真 |
expr1 -ge expr2 | 如果 expr1 >= expr2 ,则结果为真 |
expr1 -lt expr2 | 如果 expr1 < expr2,则结果为真 |
expr1 -le expr2 | 如果 expr1 <= expr2,则结果为真 |
!expr | 如果表达式为假,则结果为真 |
使用方法如下:
num1=2num2=3if [ "$num1" -eq "$num2" ] ; then ...fiif [ "$num1" -le "$num2" ] ; then ....fi
注意算术比较和字符串比较之间的不同之处,字符串比较比较的是两个字符串,数字也是能组成字符串的,因此,当我们使用字符串比较的方式和数字比较的方式来比较两串数字的时候,结果会有些不同。说起来比较拗口,用一个例子来说明一下:
#!/bin/bash val1="1" val2="001" val3="1 " # 字符串 val3 在 1 的后面还有一个空格 [ "$val1" = "$val2" ] echo $? # 使用字符串比较,退出码为 1,说明两个字符串不相等 [ "$val1" -eq "$val2" ] echo $? # 使用数值比较,退出码为 0,说明两个数值相等 [ "$val1" = "$val3" ] echo $? # 退出码为 1 [ "$val1" -eq "$val3" ] echo $? # 退出码为 0 exit 0
需要注意的是,如果在编写代码时,变量没有加上双引号,上述程序的结果又会不同,仅对 val3 进行取值,将会忽略该字符串中的空格,则第三个表达式的退出码将为 0 。这也说明了在变量两边加上双引号的重要性。
3)文件条件测试
文件条件测试 | 结果 |
-d file | 如果文件是一个目录,则结果为真 |
-e file | 如果文件存在,则结果为真。注意,历史上 -e 选项不可移植,所以通常使用的是 -f 选项 |
-f file | 如果文件存在且为普通文件,则结果为真 |
-g file | 如果文件的 set-group-id 位被设置,则结果为真 |
-r file | 如果文件可读,则结果为真 |
-s file | 如果文件大小不为 0 ,则结果为真 |
-u file | 如果文件的 set-user-id 为被设置,则结果为真 |
-w file | 如果文件可写,则结果为真 |
-x file | 如果文件可执行,则结果为真 |
用一个例子演示一下:
#!/bin/bashif [ -f /bin/bash ] ; then echo "file /bin/bash exists"fiif [ -d /bin/bash ] ; then echo "/bin/bash is a directory"else echo "/bin/bash is not a directory"fi exit 0
五、控制结构
shell 中的控制结构与其他程序设计语言中的控制结构类似,也是由顺序结构、选择结构和循环结构组成。
5.1 if 语句
在上面的例子中,已经多次用到了 if 语句,这里再详细描述一下 if 语句的语法结构:
if condition1then statements1elif condition2then statements2else statements3fi
在 if 结构中,condition 就是我们第三节所说的条件判断语句。if 语句执行时,先执行 condition ,获得其退出状态,若退出状态为 0(这意味着条件满足),则执行 then 块中的语句,否则跳过 then,接下去执行。
如果需要对条件作更进一步的判断划分的话,可以使用 elif 语句(类似于 else if)。具体的例子上文有许多,就不再单独写了。
5.2 case 语句
与其他编程语言中的 case 语句类似, shell 中的 case 语句也可以用来进行模式匹配,语法如下:
case variable in pattern [ | pattern ] ... ) statements;; pattern [ | pattern ] ... ) statements;; ...esac
关于 case 的语法,有以下几点需要说明一下:
1)case 语句以 case 作为开头,以 esac 作为结尾;
2)case 语句的每个模式行都是以双分号 ;; 结尾的;
3)一个模式行可以合并匹配多个模式,使用 | 符作为分隔;
4)一个模式行可以执行多条语句,各语句之间可以使用单分号 ; 隔开,这也是为什么每行的结尾要使用双分号 ;; 作为结束标志的原因;
5)case 语句支持使用正则表达式作为匹配项,这使得 case 语句的功能更为强大。
#!/bin/bashread -p "please keyin a word:" -t 5 wordcase $word in [a-z] | [A-Z] ) echo "You have keyin a letter";; [1-9] ) echo "You have keyin a number";; * ) echo "Unknow input"esacexit 0
这段代码从键盘输入一个字符,然后进行匹配,判断这个字符是字母还是数字,都不是的话返回未知输入。
5.3 for 语句
shell 中的 for 语句与 C 语言等的 for 语句格式不一样,但都是用来循环处理一组值的。这组值可以是任意字符串的集合(shell 在默认情况下所有变量都是以字符串的形式存储的),它们可以在程序里被列出,更常见的做法是使用 shell 的文件名扩展结果。 for 循环将会重复整个对象列表,依次执行每一个独立对象的循环内容。对象可能是命令行参数、文件名或是任何可以以列表形式建立的东西。其语法如下:
for variable in valuesdo statementsdone
for 命令可以执行指定次数的一个或多个命令。在执行循环时,参数列表 values(可以有多个参数,如val1、val2、val3、...) 中的第一个参数将被赋给变量 variable,然后执行循环体(do 与 done 之间的命令);然后将列表中的第二个参数赋给 variable,依次循环,直到列表中的参数用完。举个简单的例子:
#!/bin/bash for name in tongye wuhen xiaodong wufei laowang do echo $name done exit 0
这段代码将依次打印参数列表中的参数。 关于 for 语句,还有许多其他用法,暂不细说。
5.4 while 与 until 语句
如果你需要进行循环操作而是先不知道需要循环的次数,可以使用 while 循环,while 循环的语法如下:
while conditiondo statementsdone
until 循环语句的功能与 while 一样,不同的是对于条件判断结果的处理上。until 循环的语法如下:
until conditiondo statementsdone
在 while 和 until 语句中,condition 是判断条件,不同的是,while 语句中,若判断条件为真,则执行循环体;until 语句中,若判断条件为真,则停止执行循环体。
#!/bin/bash i=1 while [ "$i" -le 10 ] do read -p "please keyin a number:" i done 9 10 echo "$i" 11 12 exit 0
这段代码从键盘中输入一个数字,直到输入数值大于 10,退出循环并打印最后输入的那个值。
参考书籍:
《Linux程序设计 第四版》
《Shell脚本学习指南》
《UNIX/Linux/OS X 中的 Shell 编程 第四版》