使用多个命令
shell脚本的关键在于输入多个命令并处理每个命令的结果,甚至需要将一个命令的结果传给另一个命令。 shell可以让你将多个命令串起来,一次执行完成。如果要两个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开。
$ date ; who
使用这种办法就能将任意多个命令串连在一起使用了,只要不超过最大命令行字符数255就行。
创建 shell 脚本文件
在创建shell脚本文件时,必须在文件的第一行指定要使用的shell。其格式为:
#!/bin/bash
在通常的shell脚本中,井号(#)用作注释行。 shell并不会处理shell脚本中的注释行。然而,shell脚本文件的第一行是个例外, #后面的惊叹号会告诉shell用哪个shell来运行脚本。
在指定了shell之后,就可以在文件的每一行中输入命令,然后加一个回车符。之前提到过,注释可用#添加。例如:
#!/bin/bash
# This script displays the date and who's logged on
date
who
现在运行脚本,结果可能会叫你有点失望。你要跨过的第一个障碍是让bash shell能找到你的脚本文件。shell会通过PATH环境变量来查找命令。
PATH环境变量被设置成只在一组目录中查找命令。要让shell找到test1脚本,只需采取以下两种作法之一:
将shell脚本文件所处的目录添加到PATH环境变量中;
在提示符中用绝对或相对文件路径来引用shell脚本文件。
记住,为了引用当前目录下的文件,可以在shell中使用单点操作符。现在shell找到了脚本文件,但还有一个问题。 shell指明了你还没有执行文件的权限。下一步是通过chmod命令赋予文件属主执行文件的权限。
$ chmod u+x test1
显示消息
在echo命令后面加上了一个字符串,该命令就能显示出这个文本字符串。
$ echo This is a test
This is a test
注意,默认情况下,不需要使用引号将要显示的文本字符串划定出来。但有时在字符串中出现引号的话就比较麻烦了。
$ echo Let's see if this'll work
Lets see if thisll work
echo命令可用单引号或双引号来划定文本字符串。如果在字符串中用到了它们,你需要在文本中使用其中一种引号,而用另外一种来将字符串划定起来。
$ echo "This is a test to see if you're paying attention"
This is a test to see if you're paying attention
$ echo 'Rich says "scripting is easy".'
Rich says "scripting is easy".
如果想把文本字符串和命令输出显示在同一行中,该怎么办呢?可以用echo语句的-n参数。只要将第一个echo语句改成这样就行:
echo -n "The time and date are: "
你需要在字符串的两侧使用引号,保证要显示的字符串尾部有一个空格。命令输出将会在紧接着字符串结束的地方出现。
使用变量
通常你会需要在shell命令使用其他数据来处理信息。这可以通过变量来实现。变量允许你临时性地将信息存储在shell脚本中,以便和脚本中的其他命令一起使用。
环境变量
shell维护着一组环境变量,用来记录特定的系统信息。比如系统的名称、登录到系统上的用户名、用户的系统ID(也称为UID)、用户的默认主目录以及shell查找程序的搜索路径。可以用set命令来显示一份完整的当前环境变量列表。
$ set
BASH=/bin/bash
[...]
HOME=/home/Samantha
HOSTNAME=localhost.localdomain
HOSTTYPE=i386
IFS=$' \t\n'
IMSETTINGS_INTEGRATE_DESKTOP=yes
IMSETTINGS_MODULE=none
LANG=en_US.utf8
LESSOPEN='|/usr/bin/lesspipe.sh %s'
LINES=24
LOGNAME=Samantha
[...]
在脚本中,你可以在环境变量名称之前加上美元符($)来使用这些环境变量。下面的脚本演示了这种用法。
$ cat test2
#!/bin/bash
# display user information from the system.
echo "User info for userid: $USER"
echo UID: $UID
echo HOME: $HOME
$USER、$UID和$HOME环境变量用来显示已登录用户的有关信息。
注意, echo命令中的环境变量会在脚本运行时替换成当前值。另外,在第一个字符串中可以将$USER系统变量放置到双引号中,而shell依然能够知道我们的意图。但采用这种方法也有一个问题。看看下面这个例子会怎么样。
$ echo "The cost of the item is $15"
The cost of the item is 5
只要脚本在引号中出现美元符,它就会以为你在引用一个变量。在这个例子中,脚本会尝试显示变量$1(但并未定义),再显示数字5。要显示美元符,你必须在它前面放置一个反斜线。
$ echo "The cost of the item is \$15"
The cost of the item is $15
反斜线允许shell脚本将美元符解读为实际的美元符,而不是变量。
说明 你可能还见过通过${variable}形式引用的变量。变量名两侧额外的花括号通常用来帮助识别美元符后的变量名。
用户变量
除了环境变量, shell脚本还允许在脚本中定义和使用自己的变量。定义变量允许临时存储数据并在整个脚本中使用,从而使shell脚本看起来更像一个真正的计算机程序。
用户变量可以是任何由字母、数字或下划线组成的文本字符串,长度不超过20个。用户变量区分大小写,所以变量Var1和变量var1是不同的。这个小规矩经常让脚本编程初学者感到头疼。
使用等号将值赋给用户变量。在变量、等号和值之间不能出现空格。这里有一些给用户变量赋值的例子。
var1=10
var2=-57
var3=testing
var4="still more testing"
shell脚本会自动决定变量值的数据类型。在脚本的整个生命周期里,shell脚本中定义的变量会一直保持着它们的值,但在shell脚本结束时会被删除掉。
与系统变量类似,用户变量可通过美元符引用。变量每次被引用时,都会输出当前赋给它的值。重要的是要记住,引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用美元符。通过一个例子你就能明白我的意思。
$ cat test4
#!/bin/bash
# assigning a variable value to another variable
value1=10
value2=$value1
echo The resulting value is $value2
在赋值语句中使用value1变量的值时,仍然必须用美元符。这段代码产生如下输出。
The resulting value is 10
命令替换
shell脚本中最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。把输出赋给变量之后,就可以随意在脚本中使用了。这个特性在处理脚本数据时尤为方便。
有两种方法可以将命令输出赋给变量:
反引号字符(`)
$()格式
命令替换允许你将shell命令的输出赋给变量。尽管这看起来并不那么重要,但它却是脚本编程中的一个主要组成部分。
要么用一对反引号把整个命令行命令围起来:
testing=`date`
要么使用$()格式:
testing=$(date)
shell会运行命令替换符号中的命令,并将其输出赋给变量testing。注意,赋值等号和命令替换字符之间没有空格。
下面这个例子很常见,它在脚本中通过命令替换获得当前日期并用它来生成唯一文件名。
#!/bin/bash
# copy the /usr/bin directory listing to a log file
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today
today变量是被赋予格式化后的date命令的输出。这是提取日期信息来生成日志文件名常用的一种技术。 +%y%m%d格式告诉date命令将日期显示为两位数的年月日的组合。
$ date +%y%m%d
140131
这个脚本将日期值赋给一个变量,之后再将其作为文件名的一部分。文件自身含有目录列表的重定向输出。运行该脚本之后,应该能在目录中看到一个新文件。
警告 命令替换会创建一个子shell来运行对应的命令。子shell( subshell)是由运行该脚本的shell所创建出来的一个独立的子shell( child shell)。正因如此,由该子shell所执行命令是无法使用脚本中所创建的变量的。
在命令行提示符下使用路径./运行命令的话,也会创建出子shell;要是运行命令的时候不加入路径,就不会创建子shell。如果你使用的是内建的shell命令,并不会涉及子shell。
在命令行提示符下运行脚本时一定要留心!
重定向输入和输出
有些时候你想要保存某个命令的输出而不仅仅只是让它显示在显示器上。 bash shell提供了几个操作符,可以将命令的输出重定向到另一个位置(比如文件)。重定向可以用于输入,也可以用于输出,可以将文件重定向到命令输入。
输出重定向
最基本的重定向将命令的输出发送到一个文件中。 bash shell用大于号(>)来完成这项功能:
command > outputfile
之前显示器上出现的命令输出会被保存到指定的输出文件中。
$ date > test6
$ ls -l test6
-rw-r--r-- 1 user user 29 Feb 10 17:56 test6
$ cat test6
Thu Feb 10 17:56:58 EDT 2014
如果输出文件已经存在了,重定向操作符会用新的文件数据覆盖已有文件。
有时,你可能并不想覆盖文件原有内容,而是想要将命令的输出追加到已有文件中,比如你正在创建一个记录系统上某个操作的日志文件。在这种情况下,可以用双大于号(>>)来追加数据。
输入重定向
输入重定向和输出重定向正好相反。输入重定向将文件的内容重定向到命令,而非将命令的输出重定向到文件。
输入重定向符号是小于号(<):
command < inputfile
一个简单的记忆方法就是:在命令行上,命令总是在左侧,而重定向符号“指向”数据流动的方向。小于号说明数据正在从输入文件流向命令。
这里有个和wc命令一起使用输入重定向的例子。
$ wc < test6
2 11 60
wc命令可以对对数据中的文本进行计数。默认情况下,它会输出3个值:
文本的行数
文本的词数
文本的字节数
通过将文本文件重定向到wc命令,你立刻就可以得到文件中的行、词和字节的计数。
还有另外一种输入重定向的方法,称为内联输入重定向(inline input redirection)。这种方法无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据就可以了。乍看一眼,这可能有点奇怪,但有些应用会用到这种方式。
内联输入重定向符号是远小于号(<<)。除了这个符号,你必须指定一个文本标记来划分输入数据的开始和结尾。任何字符串都可作为文本标记,但在数据的开始和结尾文本标记必须一致。
command << marker
data
marker
在命令行上使用内联输入重定向时, shell会用PS2环境变量中定义的次提示符来提示输入数据。下面是它的使用情况。
$ wc << EOF
> test string 1
> test string 2
> test string 3
> EOF
次提示符会持续提示,以获取更多的输入数据,直到你输入了作为文本标记的那个字符串。
wc命令会对内联输入重定向提供的数据进行行、词和字节计数。
管道
有时需要将一个命令的输出作为另一个命令的输入。这可以用重定向来实现,只是有些笨拙。
$ rpm -qa > rpm.list
$ sort < rpm.list
这种方法的确管用,但仍然是一种比较繁琐的信息生成方式。我们用不着将命令输出重定向到文件中,可以将其直接重定向到另一个命令。这个过程叫作管道连接(piping)。
和命令替换所用的反引号( `)一样,管道符号在shell编程之外也很少用到。该符号由两个竖线构成,一个在另一个上面。然而管道符号的印刷体通常看起来更像是单个竖线(|)。管道被放在命令之间,将一个命令的输出重定向到另一个命令中:
command1 | command2
不要以为由管道串起的两个命令会依次执行。 Linux系统实际上会同时运行这两个命令,在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。
现在,可以利用管道将rpm命令的输出送入sort命令来产生结果。
$ rpm -qa | sort
执行数学运算
在shell脚本中有两种途径来进行数学运算。
expr 命令
最开始, Bourne shell提供了一个特别的命令用来处理数学表达式。 expr命令允许在命令行上处理数学表达式,但是特别笨拙。
$ expr 1 + 5
6
expr命令能够识别少数的数学和字符串操作符。
expr命令操作符
操 作 符 | 描 述 |
---|---|
ARG1 | ARG2 | 如果ARG1既不是null也不是零值,返回ARG1;否则返回ARG2 |
ARG1 & ARG2 | 如果没有参数是null或零值,返回ARG1;否则返回0 |
ARG1 < ARG2 | 如果ARG1小于ARG2,返回1;否则返回0 |
ARG1 <= ARG2 | 如果ARG1小于或等于ARG2,返回1;否则返回0 |
ARG1 = ARG2 | 如果ARG1等于ARG2,返回1;否则返回0 |
ARG1 != ARG2 | 如果ARG1不等于ARG2,返回1;否则返回0 |
ARG1 >= ARG2 | 如果ARG1大于或等于ARG2,返回1;否则返回0 |
ARG1 > ARG2 | 如果ARG1大于ARG2,返回1;否则返回0 |
ARG1 + ARG2 | 返回ARG1和ARG2的算术运算和 |
ARG1 - ARG2 | 返回ARG1和ARG2的算术运算差 |
ARG1 * ARG2 | 返回ARG1和ARG2的算术乘积 |
ARG1 / ARG2 | 返回ARG1被ARG2除的算术商 |
ARG1 % ARG2 | 返回ARG1被ARG2除的算术余数 |
STRING : REGEXP | 如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配 |
match STRING REGEXP | 如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配 |
substr STRING POS LENGTH | 返回起始位置为POS(从1开始计数)、长度为LENGTH个字符的子字符串 |
index STRING CHARS | 返回在STRING中找到CHARS字符串的位置;否则,返回0 |
length STRING | 返回字符串STRING的数值长度 |
+ TOKEN | 将TOKEN解释成字符串,即使是个关键字 |
(EXPRESSION) | 返回EXPRESSION的值 |
尽管标准操作符在expr命令中工作得很好,但在脚本或命令行上使用它们时仍有问题出现。许多expr命令操作符在shell中另有含义(比如星号)。当它们出现在在expr命令中时,会得到一些诡异的结果。
$ expr 5 * 2
expr: syntax error
要解决这个问题,对于那些容易被shell错误解释的字符,在它们传入expr命令之前,需要使用shell的转义字符(反斜线)将其标出来。
$ expr 5 \* 2
10
在shell脚本中使用expr命令也同样复杂:
$ cat test6
#!/bin/bash
# An example of using the expr command
var1=10
var2=20
var3=$(expr $var2 / $var1)
echo The result is $var3
要将一个数学算式的结果赋给一个变量,需要使用命令替换来获取expr命令的输出:
$ chmod u+x test6
$ ./test6
The result is 2
使用方括号
bash shell为了保持跟Bourne shell的兼容而包含了expr命令,但它同样也提供了一种更简单的方法来执行数学表达式。在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号($[ operation ])将数学表达式围起来。
$ var1=$[1 + 5]
$ echo $var1
6
$ var2=$[$var1 * 2]
$ echo $var2
12
用方括号执行shell数学运算比用expr命令方便很多。这种技术也适用于shell脚本。
$ cat test7
#!/bin/bash
var1=100
var2=50
var3=45
var4=$[$var1 * ($var2 - $var3)]
echo The final result is $var4
同样,注意在使用方括号来计算公式时,不用担心shell会误解乘号或其他符号。 shell知道它不是通配符,因为它在方括号内。
在bash shell脚本中进行算术运算会有一个主要的限制。请看下例:
$ cat test8
#!/bin/bash
var1=100
var2=45
var3=$[$var1 / $var2]
echo The final result is $var3
现在,运行一下,看看会发生什么:
The final result is 2
bash shell数学运算符只支持整数运算。若要进行任何实际的数学计算,这是一个巨大的限制。
说明 zshell( zsh)提供了完整的浮点数算术操作。如果需要在shell脚本中进行浮点数运算,可以考虑看看z shell。
浮点解决方案
有几种解决方案能够克服bash中数学运算的整数限制。最常见的方案是用内建的bash计算器,叫作bc。
bc的基本用法
bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。 bash计算器能够识别:
数字(整数和浮点数)
变量(简单变量和数组)
注释(以#或C语言中的/* */开始的行)
表达式
编程语句(例如if-then语句)
函数
可以在shell提示符下通过bc命令访问bash计算器:
$ bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type 'warranty'.
12 * 5.4
64.8
要退出bash计算器,你必须输入quit。
浮点运算是由内建变量scale控制的。必须将这个值设置为你希望在计算结果中保留的小数位数,否则无法得到期望的结果。
$ bc -q
3.44 / 5
0
scale=4
3.44 / 5
.6880
quit
$
scale变量的默认值是0。在scale值被设置前, bash计算器的计算结果不包含小数位。在将其值设置成4后, bash计算器显示的结果包含四位小数。 -q命令行选项可以不显示bash计算器冗长的欢迎信息。
除了普通数字, bash计算器还能支持变量。
$ bc -q
var1=10
var1 * 4
40
var2 = var1 / 5
print var2
2
quit
$
变量一旦被定义,你就可以在整个bash计算器会话中使用该变量了。 print语句允许你打印变量和数字。
在脚本中使用bc
现在你可能想问bash计算器是如何在shell脚本中帮助处理浮点运算的。还记得命令替换吗?
是的,可以用命令替换运行bc命令,并将输出赋给一个变量。基本格式如下:
variable=$(echo "options; expression" | bc)
第一部分options允许你设置变量。如果你需要不止一个变量,可以用分号将其分开。expression参数定义了通过bc执行的数学表达式。这里有个在脚本中这么做的例子。
#!/bin/bash
var1=$(echo "scale=4; 3.44 / 5" | bc)
echo The answer is $var1
也可以用shell脚本中定义好的变量。
#!/bin/bash
var1=100
var2=45
var3=$(echo "scale=4; $var1 / $var2" | bc)
echo The answer for this is $var3
这个方法适用于较短的运算,但有时你会涉及更多的数字。如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。
有一个方法可以解决这个问题。 bc命令能识别输入重定向,允许你将一个文件重定向到bc命令来处理。但这同样会叫人头疼,因为你还得将表达式存放到文件中。
最好的办法是使用内联输入重定向,它允许你直接在命令行中重定向数据。在shell脚本中,你可以将输出赋给一个变量。
variable=$(bc << EOF
options
statements
expressions
EOF
)
EOF文本字符串标识了内联重定向数据的起止。记住,仍然需要命令替换符号将bc命令的输出赋给变量。下面是在脚本中使用这种技术的例子。
#!/bin/bash
var1=10.46
var2=43.67
var3=33.2
var4=71
var5=$(bc << EOF
scale = 4
a1 = ( $var1 * $var2)
b1 = ($var3 * $var4)
a1 + b1
EOF
)
echo The final answer for this mess is $var5
退出脚本
迄今为止所有的示例脚本中,我们都是突然停下来的。运行完最后一条命令时,脚本就结束了。其实还有另外一种更优雅的方法可以为脚本划上一个句号。
shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕。退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。
查看退出状态码
Linux提供了一个专门的变量$?来保存上个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用$?变量。它的值会变成由shell所执行的最后一条命令
的退出状态码。
$ date
Sat Jan 15 10:01:30 EDT 2014
$ echo $?
0
按照惯例,一个成功结束的命令的退出状态码是0。如果一个命令结束时有错误,退出状态码就是一个正数值。
Linux错误退出状态码没有什么标准可循,但有一些可用的参考,如表所示。
Linux退出状态码
状 态 码 | 描 述 |
---|---|
0 | 命令成功结束 |
1 | 一般性未知错误 |
2 | 不适合的shell命令 |
126 | 命令不可执行 |
127 | 没找到命令 |
128 | 无效的退出参数 |
128+x | 与Linux信号x相关的严重错误 |
130 | 通过Ctrl+C终止的命令 |
255 | 正常范围之外的退出状态码 |
退出状态码126表明用户没有执行命令的正确权限。
exit 命令
默认情况下, shell脚本会以脚本中的最后一个命令的退出状态码退出。你可以改变这种默认行为,返回自己的退出状态码。 exit命令允许你在脚本结束时指定一个退出状态码。
你要注意这个功能,因为退出状态码最大只能是255。看下面例子中会怎样。
#!/bin/bash
# testing the exit status
var1=10
var2=30
var3=$[$var1 * $var2]
echo The value is $var3
exit $var3
现在运行它的话,会得到如下输出。
The value is 300
$ echo $?
44
退出状态码被缩减到了0~ 255的区间。 shell通过模运算得到这个结果。一个值的模就是被除后的余数。最终的结果是指定的数值除以256后得到的余数。在这个例子中,指定的值是300(返回值),余数是44,因此这个余数就成了最后的状态退出码。