简单shell脚本
!/bin/bash
这一行表明,不管用户选择的是那种交互式shell,该脚本需要使用bash shell来运行。由于每种shell的语法大不相同,所以这句非常重要。
简单实例
下面是一个非常简单的shell脚本。它只是运行了几条简单的命令
1
2
3
4
|
#!/bin/bash echo "hello, $USER. I wish to list some files of yours" echo "listing files in the current directory, $PWD" ls # 列出当前目录所有文件 |
首先,请注意第四行。在bash脚本中,跟在#符号之后的内容都被认为是注释(除了第一行)。Shell会忽略注释。这样有助于用户阅读理解脚本。 ?$USER和 $PWD都是变量。它们是bash脚本自定义的标准变量,无需在脚本中定义即可使用。请注意,在双引号中引用的变量会被展开(expanded)。“expanded”是一个非常合适的形容词:基本上,当shell执行命令并遇到$USER变量时,会将其替换为该变量对应的值。
变量
任何编程语言都会用到变量。你可以使用下面的语句来定义一个变量:
1
|
X= "hello" |
并按下面的格式来引用这个变量:
$X
更具体的说,$X表示变量X的值。关于语义方面有如下几点需要注意:
-
等于号两边不可以有空格!例如,下面的变量声明是错误的 :
1
|
X = hello |
-
在我所展示的例子中,引号并不都是必须的。只有当变量值包含空格时才需要加上引号。例如:
1
2
|
X = hello world # 错误 X = "hello world" # 正确 |
这是由于shell将每一行命令视为命令及其参数的集合,以空格分隔。 foo=bar就被视为一条命令。foo = bar 的问题就在于shell将空格分开的foo视为命令。同样,X=hello world的问题就在于shell将X=hello视为一条完整的命令,而”world”则被彻底无视(因为赋值命令不需其他参数)。
单引号 VS 双引号
基本上来说,变量名会在双引号中展开,单引号中则不会。如果你不需要引用变量值,那么使用单引号可以很直观的输出你期望的结果。 An example 示例
1
2
3
4
|
#!/bin/bash echo -n '$USER=' # -n选项表示阻止echo换行 echo "$USER" echo "$USER=$USER" # 该命令等价于上面的两行命令 |
输出如下(假设你的用户名为elflord)) $USER=elflord $USER=elflord
1
2
3
|
$USER=elflord $USER=elflord |
从例子中可以看出,在双引号中使用转义字符也是一种解决方案。虽然双引号的使用更灵活,但是其结果不可预见。如果要在单引号和双引号之间做出选择,最好选择单引号。
使用引号封装变量
有时候,使用双引号来保护变量名是个很好的点子。如果你的变量值存在空格或者变量值为空字符串,这点就显得尤其重要。看下面这个例子:
1
2
3
4
5
|
#!/bin/bash X= "" if [ -n $X ]; then # -n 用来检查变量是否非空 echo "the variable X is not the empty string" fi |
运行这个脚本,输出如下:
the variable X is not the empty string
为何?这是因为shell将$X展开为空字符串,表达式[-n]返回真值(因为改表达式没有提供参数)。再看这个脚本:
1
2
3
4
5
|
#!/bin/bash X= "" if [ -n "$X" ]; then # -n 用来检查变量是否非空 echo "the variable X is not the empty string" fi |
在这个例子中,表达式展开为[ -n ""],由于引号中内容为空,因此该表达式返回false值。
在执行时展开变量
为了证实shell就像我上面说的那样直接展开变量,请看下面的例子:
1
2
3
4
5
|
#!/bin/bash LS= "ls" LS_FLAGS= "-al" $LS $LS_FLAGS $HOME |
乍一看可能有点不好理解。其实最后一行就是执行这样一条命令:
Ls -al /home/elflord
(假设当前用户home目录为/home/elflord)。这就说明了shell仅仅只是将变量替换为对应的值再执行命令而已。
使用大括号保护变量
这里有一个潜在的问题。假设你想打印变量X的值,并在值后面紧跟着打印”abc”。那么问题来了:你该怎么做呢? 先试一试:
1
2
3
|
#!/bin/bash X=ABC echo "$Xabc" |
这个脚本没有任何输出。究竟哪里出了问题?这是由于shell以为我们想要打印变量Xabc的值,实际上却没有这个变量。为了解决这种问题可以用大括号将变量名包围起来,从而避免其他字符的影响。下面这个脚本可以正常工作:
!/bin/bashX=ABCecho “${X}abc”
1
2
3
|
#!/bin/bash X=ABC echo "${X}abc" |
条件语句, if/then/elif
在某些情况下,我们需要做条件判断。比如判断字符串长度是否为0?判断文件foo是否存在?它是一个链接文件还是实际文件?首先,我们需要if命令来执行检查。语法如下:
1
2
3
4
5
6
|
if condition then statement1 statement2 .......... fi |
当指定条件不满足时,可以通过else来指定其他执行动作。
1
2
3
4
5
6
7
8
|
if condition then statement1 statement2 .......... else statement3 fi |
当if条件不满足时,可以添加多个elif来检查其他条件是否满足。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
if condition1 then statement1 statement2 .......... elif condition2 then statement3 statement4 ........ elif condition3 then statement5 statement6 ........ fi |
当相关条件满足时,shell会执行在相应的if/elif与下个elif或fi之间的语句。事实上,判断条件可以是任意命令,当且只当命令返回并且退出状态为0时,才会执行该条件块中的语句(换句话说,就是当命令成功返回时)。不过在本文的学习中,我们只会关注“test”或“[]”形式的条件判断。
Test命令与操作符
条件判断中的命令几乎都是test命令。test根据测试条件通过或失败来返回true或false(更准确的说是返回0或非0值)。如下所示:
1
|
test operand1 operator operand2 |
对某些测试来说,只需要一个操作数(operand2)通常是下面这种情况的简写:
1
|
[ operand1 operator operand2 ] |
为了让我们的讨论更接地气一点,给出下面一些例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#!/bin/bash X=3 Y=4 empty_string= "" if [ $X -lt $Y ] # is $X less than $Y ? then echo "$X=${X}, which is smaller than $Y=${Y}" fi if [ -n "$empty_string" ]; then echo "empty string is non_empty" fi if [ -e "${HOME}/.fvwmrc" ]; then # test to see if ~/.fvwmrc exists echo "you have a .fvwmrc file" if [ -L "${HOME}/.fvwmrc" ]; then # is it a symlink ? echo "it is a symbolic link" elif [ -f "${HOME}/.fvwmrc" ]; then # is it a regular file ? echo "it is a regular file" fi else echo "you have no .fvwmrc file" fi |
需要注意的细节
Test命令的格式为“操作数< 空格 >操作符< 空格 >操作数”或者“操作符< 空格 >操作数”,这里特别说明必须要有这些空格,因为shell将没有空格的第一串字符视为一个操作符(以-开头)或者操作数。比如下面这个:
if [ 1=2 ]; then echo “hello”fi
它会打印出hello,这明显与预期结果是不一致的(因为shell只看到操作数1=2,没看到操作符)。
还有一种隐藏陷阱是未加引号的变量。像我们之前例子说的-n测试时变量须加引号的情形。其实,不管在什么情况下,加上引号总是没有坏处的,还有可能规避一些很奇葩的错误。因为有时候不加引号的变量扩展开的测试结果会让人非常困惑。例如:
1
2
3
4
5
6
|
#!/bin/bash X= "-n" Y= "" if [ $X = $Y ] ; then echo "X=Y" fi |
这个脚本打印出来的结果是错误的,因为shell将判断展开为 [ -n = ],但是”=”的长度不为0,所以条件判断通过从而导致输出结果为“X=Y”。
Test操作符简介
下图是test操作符的快速查询列表。当然这个列表并不全面,但记下这些就足够平常使用了(如果还需要了解其他操作符,可以查看man手册)。
operator | produces true if… | number of operands |
-n | operand non zero length | 1 |
-z | operand has zero length | 1 |
-d | there exists a directory whose name is operand | 1 |
-f | there exists a file whose name is operand | 1 |
-eq | the operands are integers and they are equal | 2 |
-neq | the opposite of -eq | 2 |
= | the operands are equal (as strings) | 2 |
!= | opposite of = | 2 |
-lt | operand1 is strictly less than operand2 (both operands should be integers) | 2 |
-gt | operand1 is strictly greater than operand2 (both operands should be integers) | 2 |
-ge | operand1 is greater than or equal to operand2 (both operands should be integers) | 2 |
-le | operand1 is less than or equal to operand2 (both operands should be integers) | 2 |
循环
循环结构允许我们执行重复的步骤或者在若干个不同条目上执行相同的程序。Bash中有下面两种循环
-
for 循环
-
while 循环
For 循环
直接来个例子,来直观地感受for循环的语法。
1
2
3
4
5
|
#!/bin/bash for X in red green blue do echo $X done |
For循环会遍历空格分开的条目。注意,如果某一项含有空格,必须要用引号引起来,例子如下:
1
2
3
4
5
6
7
8
|
#!/bin/bash colour1= "red" colour2= "light blue" colour3= "dark green" for X in "$colour1" $colour2 " $colour3" do echo $X done |
如果我们漏掉for循环中的引号,你能猜想出会发生什么吗?这个例子说明,除非你确认变量中不会包含空格,否则最好都用引号将变量保护起来。
在for循环中使用通配符
如果shell解析字符串时遇到*号,会将它展开为所有匹配的文件名。当且仅当目标文件与号展开后的字符串一致才会匹配成功。例如,单独的*号展开为当前目录的所有文件,中间以空格分开(包含隐藏文件)。
所以:
echo *
列出当前目录下的所有文件和目录。
echo *.jpg
列出所有的jpeg图片格式的文件。
echo ${HOME}/public_html/*.jpg
列出home目录中public_html目录下的所有jpeg文件。
正是由于这种特性,使得我们可以很方便的来操作目录和文件,尤其是和for循环结合使用时,更是便利。例子如下:
1
2
3
4
5
|
#!/bin/bash for X in *.html do grep -L '<UL>' "$X" done |
打印出当前目录下所有不包含<UL>字段的html文件。
While 循环
当给定条件为真值时,while循环会重复执行。例如:
1
2
3
4
5
6
7
|
#!/bin/bash X=0 while [ $X - le 20 ] do echo $X X=$((X+1)) done |
这样导致这样的疑问: 为什么bash不能使用C风格的for循环呢?
for (X=1,X<10; X++)
这也跟bash自身的特性有关,之所以不允许这种for循环是由于:bash是一种解释性语言,因此其运行效率比较低。也正是由于这个原因,高负荷迭代是不允许的。
命令替换
Bash shell有个非常好用的特性叫做命令替换。允许我们将一个命令的输出当做另一个命令的输入。比如你想要将命令的输出赋值给变量X,你可以通过变量替换来实现。
有两种命令替换的方式:大括号扩展和反撇号扩展。
大括号扩展: $(commands) 会展开为命令commands的输出结果。并且允许嵌套使用,所以commands中允许包含子大括号扩展。
反撇好扩展:将commands
扩展为命令commands的输出结果。不允许嵌套。
这里有一个例子:
1
2
3
4
5
6
7
|
#!/bin/bash files= "$(ls)" web_files=` ls public_html` echo "$files" # we need the quotes to preserve embedded newlines in $files echo "$web_files" # we need the quotes to preserve newlines X=` expr 3 * 2 + 4` # expr evaluate arithmatic expressions. man expr for details. echo "$X" |
$()替换方式的优点不言自明:非常易于嵌套。并且大多数bourne shell的衍生版本都支持(POSIX shell 或者更好的都支持)。不过,反撇号替换更简单明了,即使是最基本的shell它也提供了支持(任意版本的#!/bin/sh都可以)