本教程基于 Bource shell(sh)和 Bource Again shell (bash),并假设你已经有如下经验:
- 使用过 Unix/Linux 的交互式 shell
- 最基础的编程知识(理解变量、函数,以及后台的概念)
文本格式约定如下:
- 首次出现的重要词汇,用斜体标出
- 命令行输入以
$
符号开头
如果你的提示符不一样,输入以下命令 PS1="$ "; export PS1
,之后你的交互就和所给例子一样了:
$ echo '#!/bin/sh' > my-script.sh
$ echo 'echo Hello World' >> my-script.sh
$ chmod 755 my-script.sh
$ ./my-script.sh
Hello World
$
为了使脚本文件可执行,需要设置文件权限的可执行位(eXecutable bit),对于 shell 脚本,可读位也是必须设置的:
$ chmod a+rx my-script.sh
$ ./my-script.sh
哲学
由于编写 shell 脚本很容易,所以网络上也充斥这许多质量很差的 shell 脚本。一份优秀、简洁、快速的 shell 脚本具备以下几点:
- 清晰易读的排版
- 避免不必要的命令
清晰的排版的 shell 脚本对比被称为“黑魔法”的 shell 脚本,主要的区别就是易于维护和理解。你可能觉得书写一条简单的脚本这不是什么大问题,但是有两点你得牢记:
- 一个简单的脚本随需求变化可能会发展成一个大型而复杂的脚本。
- 如果没有人懂得它如何工作,以后只有你自己能填坑。
有些脚本的问题在缩进,由于 shell 的主要控制结构是 if/then/else 和循环,缩进对代码可读性影响很大。
许多 shell 脚本有这种缺点:
cat /tmp/myfile | grep "mystring"
如果用下面的写法要更快:
grep "mystring" /tmp/myfile
第一种写法不仅仅要多加载一个 /bin/cat
程序到内存中,还要多开一个内存管道(pipe
)去传输文件内容。如果这段脚本在一个循环中,还要浪费时间在管道的建立和拆除流程上。
第一个脚本
按照传统,第一段脚本就是输出 “Hello World” 吧! 新建一个文件如下:
#!/bin/sh
# This is comment!
echo Hello World # This is a comment, too!
这里的第一行是告诉 Unix 该文件需要被 bin/sh 执行。这是 Bourne shell 在 Unix 系统中的标准位置。如果你使用 GNU/Linux ,那么 /bin/sh 通常是 bash (或者最近的 dash)的符号链接。
第二行开头是特殊符号 #
。这表示这一行是注释,它会被 shell 解释器完全忽略。
只有第一行是特例,操作系统会特别对待这条指令。这意味着即使你使用 csh、ksh 或者其他交互式 shell ,这一行之后的指令都是由 Bourne shell 执行。
类似的, Perl 脚本第一行可以是 #!/usr/bin/perl
用来告诉你的终端使用 perl 解释执行。
第三行运行了一条指令: echo
传入两个参数 “Hello” 和 “World” 。注意 echo
打印时会自动在每个参数之间添加一个空格。这里 #
仍然标注注释;#
之后的内容都会被 shell 忽略。
现在运行 chmod 755 first.sh
是文件可执行。接着运行 ./first.sh
:
$ chmod 755 first.sh
$ ./first.sh
Hello World
$
如果你需要打印的的两个单词之间有更多的空格该怎么做呢?试一试在上例中两个参数之间插入更多空格,能达到预期吗?如果按如下方式修改:
#!/bin/sh
# This is comment!
echo "Hello World" # This is a comment, too!
这次能成。因为 echo
这次只接受了一个参数。这里要理解的是, shell 在将参数传递给被调用的程序之前先解析了参数。
最后一个例子,尝试下面的脚本。预测它们的输出:
#!/bin/sh
# This is a comment!
echo "Hello World" # This is a comment, too!
echo "Hello World"
echo "Hello * World"
echo Hello * World
echo Hello World
echo "Hello" World
echo Hello " " World
echo "Hello "*" World"
echo `hello` world
echo 'hello' world
输出都和你想的一样么?如果不是,没关系!继续看就明白了。
变量
现存的几乎所有的编程语言都有变量的概念——一块内存的符号名称,我们可以为其分配值,读取和操作其内容。Bourne shell 也不例外,本章介绍该思想。
回看上一章节的例子,也可以用变量来完成。
注意符号 “=” 周围不能有空格: VAR=value
有效; VAR = value
无效。第一种情况下, shell 能识别到 =
并将命令是为变量分配。第二种情况下, shell 假定 VAR 是命令名称,并尝试执行它。
在 var.sh 中键入如下代码:
#!/bin/sh
MY_MESSAGE="Hello World"
echo $MY_MESSAGE
这段代码将 “Hello World” 赋值给 MY_MESSAGE
然后通过 echo
命令打印这个变量。
注意,我们需要引号包裹 Hello World 字符串,不像 echo
可以接收任意数量的变量,变量只能保存一个值。
shell 不在乎变量的类型,它们可以是字符串、整型,任何你想要的类型。
Perl 程序员应该对此感到熟悉,但是如果你从 C、Pascal 甚至 Ada 成长过来,会感到很奇怪。
说实话,这些都保存为字符串,但是期望数字类型的例程可以处理它们。
如果你给一个变量赋值字符串类型再加 1 ,是行不通的:
$ x="hello"
$ expr $x + 1
expr: not a decimal number: 'hello'
$
这是因为内部程序 expr
只期望得到数字。
还要注意,特殊字符必须正确转义以避免被 shell 解释,在之后的章节会讨论这个。
我们可以使用 read
命令以交互的方式设置变量名;以下脚本询问你的名字,并发出问候:
#!/bin/sh
echo What is your name?
read MY_NAME
echo "Hello $MY_NAME - hope you're well."
shell 内建命令 read
读取一行标准输入并赋值给变量。
变量作用域
Bourne shell 中的变量不需要定义(不像 C 语言)。但是如果你尝试读取一个未定义的变量,结果就得到一个空字符串。不会有警告或报错。这可能会导致一些错误,如果你这样赋值并打印:
MY_OBFUSCATED_VARIABLE=Hello
echo $MY_OSFUCATED_VARIABLE
最后什么都没有(第二个 OBFUSCATED 还拼写错了)。
有一个 export
命令,它对变量的作用范围有根本影响。为了理解你的变量会发生什么,你需要理解如何使用这个命令。新建一个脚本:
#!/bin/sh
echo "MYVAR is: $MYVAR"
MYVAR="hi there"
echo "MYVAR is: $MYVAR"
运行这段脚本:
$ ./myvar2.sh
MYVAR is:
MYVAR is: hi there
MYVAR 没有设置任何值,所以它是空的。然后我们给它赋值了,结果和预期的一样。
在试试下面的代码:
$ MYVAR=hello
$ ./myvar2.sh
MYVAR is:
MYVAR is: hi there
还是没有值,怎么回事呢?
当你调用 myvar2.sh
时,产生了一个新的 shell 来运行脚本。这部分由脚本开头的 #!/bin/sh
决定的,我们之前讨论过。
我们需要 export
这个变量,让它被其他程序继承 —— 包括 shell 脚本:
$ export MYVAR
$ ./myvar2.sh
MYVAR is: hello
MYVAR is: hi there
现在看第三行的脚本:改变了 MYVAR
的值。但是脚本里修改的值没办法传回交互式 shell ,再试试读取 MYVAR
:
$ echo $MYVAR
hello
$
一旦 shell 脚本退出,其运行环境就销毁了。但是 MYVAR 还在交互的 shell 中保留了值(hello) 。
为了在你的交互 shell 中接受脚本中的修改,我们必须用 source 命令 —— 这样就可以有效地在我们自己的交互式 shell 中运行脚本,而不是生成另一个 shell 来运行它。我们可以 .
(dot)命令获取(source)脚本:
$ MYVAR=hello
$ echo $MYVAR
hello
$ . ./myvar2.sh
MYVAR is: hello
MYVAR is: hi there
$ echo $MYVAR
hi there
修改在我们的 shell 中也起作用了!这也是你的 .profile
或者 .bash_profile
工作原理。注意在本例中,不需要 export MYVAR
。
还有一点需要注意,将变量名放在大括号中,避免 shell 不知道变量名的结尾位置。例子:
#!/bin/sh
echo "What is your name?"
read USER_NAME
echo "Hello $USER_NAME"
echo "I will create you a file called ${USER_NAME}_file"
touch "${USER_NAME}_file"
还要记得加引号,避免用户输入的 USER_NAME 是多个词。
通配符 (Wildcards)
如果你之前用过 Unix ,通配符就不是什么新鲜玩意。