Shell 入门(译)

2020/06/20

本教程基于 Bource shell(sh)和 Bource Again shell (bash),并假设你已经有如下经验:

文本格式约定如下:

如果你的提示符不一样,输入以下命令 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 脚本,主要的区别就是易于维护和理解。你可能觉得书写一条简单的脚本这不是什么大问题,但是有两点你得牢记:

  1. 一个简单的脚本随需求变化可能会发展成一个大型而复杂的脚本。
  2. 如果没有人懂得它如何工作,以后只有你自己能填坑。

有些脚本的问题在缩进,由于 shell 的主要控制结构是 if/then/else 和循环,缩进对代码可读性影响很大。

许多 shell 脚本有这种缺点:

cat /tmp/myfile | grep "mystring"

如果用下面的写法要更快:

grep "mystring" /tmp/myfile

第一种写法不仅仅要多加载一个 /bin/cat 程序到内存中,还要多开一个内存管道(pipe)去传输文件内容。如果这段脚本在一个循环中,还要浪费时间在管道的建立和拆除流程上。

第一个脚本

按照传统,第一段脚本就是输出 “Hello World” 吧! 新建一个文件如下:

first.sh

#!/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
$

如果你需要打印的的两个单词之间有更多的空格该怎么做呢?试一试在上例中两个参数之间插入更多空格,能达到预期吗?如果按如下方式修改:

first1.sh

#!/bin/sh
# This is comment!
echo "Hello    World" # This is a comment, too!

这次能成。因为 echo 这次只接受了一个参数。这里要理解的是, shell 在将参数传递给被调用的程序之前先解析了参数。

最后一个例子,尝试下面的脚本。预测它们的输出:

first2.sh

#!/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 中键入如下代码:

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 命令以交互的方式设置变量名;以下脚本询问你的名字,并发出问候:

var2.sh

#!/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 命令,它对变量的作用范围有根本影响。为了理解你的变量会发生什么,你需要理解如何使用这个命令。新建一个脚本:

myvar2.sh

#!/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 不知道变量名的结尾位置。例子:

user.sh

#!/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 ,通配符就不是什么新鲜玩意。

参考