原文地址: bash-tute

使用 Bash shell 编写脚本(script)的入门指南

一个简单的 shell script

一个 shell script 不只是将多个指令简单地排列在一起并运行。一般来说,shell script 应该在第一行作如下声明:

1
#!/bin/bash

这行声明会指定 script 运行在 bash shell 中,而不管用户当前使用的是哪一种交互式 shell 。不同的shell语法差异很大,所以这行声明非常重要。

一个简单的例子

以下是一个简单的shell script例子:

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 # list files

首先,注意第4行。在一个bash script中,跟在符号#后面的被视作注释(除了第一行的shell声明),shell 将会忽略注释。注释的作用是帮助人们阅读script。

\$USER 和 \$PWD 是变量。这两个变量属于被shell预定义的标准变量,他们不需要在script中特别定义。如果变量名在双引号内部,这个变量将会被被展开。展开表示shell将会先用变量的值替换变量,然后再执行指令。

接下来我们会详细讨论变量。

变量

所有编程语言都需要变量。定义变量的方法如下:

1
X="hello"

引用变量的方法如下:

1
$X

$X表示的是变量X的值。在语法上要注意以下几点:

  • 符号 = 两边不能有空格。这样写是错误的:
1
X = hello
  • 双引号并不是必需的,但当变量的值中包含空格时,就必须加双引号,如:
1
2
X=hello world # 错误
X="hello world" # 正确

这是因为shell会把命令行解析为一堆命令以及被空格分隔的命令参数。foo=baris 被解析为一个命令。但foo = bar会被错误地解析,foo因为被空格分隔导致它被解析成了一个单独的命令。同理,X=hello world也是错误的。

单引号 VS 双引号

一般情况下,将变量名包在双引号里可以使变量展开。单引号则不会展开变量。

例子

1
2
3
4
#!/bin/bash
echo -n '$USER=' # -n option stops echo from breaking the line
echo "$USER"
echo "\$USER=$USER" # this does the same thing as the first two lines

输出结果如下(假设你的用户名是elford)

1
2
$USER=elflord
$USER=elflord

综上,双引号更灵活,单引号更直观。所以,当两种引号都可以用的时候优先选择单引号。

使用引号保护你的变量

用引号保护变量很管用。当变量中包含空格或者变量是一个空的字符串时,更需要用引号保护变量。例如:

1
2
3
4
5
#!/bin/bash
X=""
if [ -n $X ]; then # -n tests to see if the argument is non empty
echo "the variable X is not the empty string"
fi

输出结果:

1
the variable X is not the empty string # 错误

这里出现的结果不符合预期是因为shell把$X展开成了空字符串,所以表达式[-n]返回true(此时-n没有参数,返回true)。正确的script应该这样写:

1
2
3
4
5
#!/bin/bash
X=""
if [ -n "$X" ]; then # -n tests to see if the argument is non empty
echo "the variable X is not the empty string"
fi

在这个例子中,表达式被展开为[ -n “” ],并返回false。因为-n被给予参数””(空字符串)。

变量在运行时会被展开

下面这个例子可以证明shell确实“展开”了变量(就像前文提到的一样):

1
2
3
4
5
#!/bin/bash
LS="ls"
LS_FLAGS="-al"
$LS $LS_FLAGS $HOME

这个script的最后一行实际上执行了下面这个指令:

1
ls -al /home/elflord # (假设你的home目录在/home/elford)

这个script的机制是:shell将变量替换为变量的值,然后执行指令。

使用大括号保护你的变量

假设你想输出变量X的值,然后紧接着输出字母”abc”。那么应该如何做?先试一下这样写:

1
2
3
#!/bin/bash
X=ABC
echo "$Xabc"

输出为空,因为shell认为我们在请求变量Xabc,而变量Xabc并未被定义。解决的办法是用大括号将变量X与其它字母隔开:

1
2
3
#!/bin/bash
X=ABC
echo "${X}abc"

条件语句,if/then/elif

条件语句if…的语法如下:

1
2
3
4
5
6
if condition
then
statement1
statement2
..........
fi

if…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

在实际使用中,任何指令都可以作为分支判断条件。当指令返回值为0时(也就是说指令运行的结果为成功),相应代码块中的指令就会开始执行。
在本文中,我们只使用”test”或”[ ]”执行判断。

Test指令与操作

判断条件中的指令大多数时候都是test指令。Test根据各操作的成功或失败来返回真或假(更准确地说,以0或非0状态退出),例如:

1
test operand1 operator operand2

如果在test中只需要一个操作数,那么test指令可以简写为:

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 # 测试 ~/.fvwmrc 是否存在
echo "you have a .fvwmrc file"
if [ -L "${HOME}/.fvwmrc" ]; then # 测试 ~/.fvwmrc 是否是符号链接(symbolic link)
echo "it's a symbolic link
elif [ -f "${HOME}/.fvwmrc" ]; then # 测试 ~/.fvwmrc 是否是常规文件(regular file)
echo "it's a regular file"
fi
else
echo "you have no .fvwmrc file"
fi

一些需要注意的地方

test指令的正确格式是“操作数<空格>操作符<空格>操作数”或者“操作符<空格>操作数”,空格是必需的。如果第一个连续字符串以’-‘开头,它会被解析为操作符,否则被解析为操作数。例如:

1
2
3
if [ 1=2 ]; then
echo "hello"
fi

将会输出hello,因为shell找到了一个操作数,但没有找到任何操作符。

另一个需要注意的是引号,不用引号保护变量有可能造成bug。如:

1
2
3
4
5
6
#!/bin/bash
X="-n"
Y=""
if [ $X = $Y ] ; then
echo "X=Y"
fi

这个script不会输出预期的结果,因为shell将表达式展开为:

1
[ -n = ]

然而字符串”=”的长度不为0。

test操作符的简短总结

常用的test操作符:

操作符 返回值为真的情况 操作数的个数
-n 操作数不为0(或长度不为0) 1
-z 操作数为0(或长度为0) 1
-d 操作的目录存在 1
-f 操作的文件存在 1
-eq 两个操作数为整数且相等 2
-neq 两个操作数为整数且不相等 2
= 两个操作数相等(可比较字符串) 2
!= 两个操作数不相等(可比较字符串) 2
-lt 操作数1小于操作数2(且都为整数) 2
-gt 操作数1大于操作数2(且都为整数) 2
-ge 操作数1大于等于操作数2(且都为整数) 2
-le 操作数1小于等于操作数2(且都为整数) 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循环中的通配符

符号 * 可以匹配任意字符串。如:

1
echo *

会输出当前目录下的所有文件和文件夹。

1
echo *.jpg

会输出所有jpeg文件。

1
echo ${HOME}/public_html/*.jpg

会输出public_html目录下的所有jpeg文件。

如上所示,* 通配符在操作文件时十分管用,尤其是用在for循环中,如:

1
2
3
4
5
#!/bin/bash
for X in *.html
do
grep -L '<UL>' "$X" # 通过正则匹配查找目录下所有html文件中的ul标签
done

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循环:

1
for (X=1,X<10; X++)

不允许for循环的其中一个原因是:bash是一种解释型语言,所以它的执行效率很低,正因如此,重型循环(heavy iteration)不被允许。

指令替换

在bash中,指令替换是一个很有用的功能,它帮助你使用某条指令的输出去执行另一条指令。例如:把一条指令的输出设为变量X的值。

指令替换有两种方式:

  • 括号展开
  • 反引号展开(``)

括号展开的格式为:$(commands) ,它将会被展开为commands的输出。括号展开允许嵌套,所以commands中可以包含括号展开。

反引号展开则将 commands 展开为 commands 的输出。

例如:

1
2
3
4
5
6
7
#!/bin/bash
files="$(ls)"
web_files=`ls public_html`
echo "$files" # 用双引号保护内部的空格
echo "$web_files" # 用双引号保护内部的空格
X=`expr 3 \* 2 + 4` # 利用 expr 进行运算
echo "$X"

括号展开的优势在于容易嵌套。而且大多数 bourne shell 的变种都支持括号展开 (如:POSIX)。然而反引号展开的可读性更强,而且最基础的 shell 都能支持它(如:#!/bin/sh 的所有版本)

注:上例中echo表达式中的字符串必须用引号保护。

本文地址: http://www.cuixiaochen.com/2016/06/15/Bash-script-快速入门指南(译)/