shell 是一个程序,让你可以输入命令,与操作系统交互。 shell 可以是图形化的;你用鼠标或触控板操作的桌面环境就是一种 shell。 我们关注的是命令行界面:你输入命令,结果(通常)会打印在终端里。
命令行 shell 有很多种。 Windows 提供 cmd 和 powershell。 Mac 和 Linux 提供 “Unix shell”,种类非常多。 Mac 的默认 shell 是 zsh。 csh 是一个老式的 shell,语法类似 C。 bash 是最流行的 shell 之一,尤其是在 Linux 上。
最早的 shell 之一是 Bourne shell,即 sh。 后来,这个 shell 被标准化,成为每个类 Unix 系统都必须提供的 shell。 许多 shell 都基于 Bourne shell,通常还额外提供交互或编程方面的功能。
Bash 基于 Bourne shell。 这就是它被称为 “Bourne-again shell” 的原因。 在许多 Linux 系统上,它是默认的 shell。 Bash 提供了许多功能,让它成为一个好用的交互式 shell,包括:
本学习路径讲的不是如何把 bash 当作交互式 shell 来使用。 而是学习用 bash 编写程序。 重点在于只用 bash 来解决问题,不过调用外部工具并没有任何限制。
Bash 从它的输入中读取命令(输入通常是终端或文件)。 它读取的每一行输入都被视为一条命令,也就是一条要执行的指令。 (有少数高级情况,比如跨多行的命令,后面会再讨论。)
Bash 把每一行拆分成单词,单词之间用空白字符(空格和制表符)分隔。 行首的第一个单词是要执行的命令的名字。 剩下的所有单词都成为该命令的实参(选项、文件名等)。
下面的例子里,我们会展示一些你可以运行的交互式命令。
# 字符会开始一段注释。
注释后面的所有单词都会被 shell 忽略;注释只是给人看的。$ 字符表示 shell 提示符。
如果你要复制命令,不要把提示符也复制进去。# Note: It is always a good idea to run `ls` before `rm`
# to make sure you know what you are deleting.
$ rm * # Remove all files in the current directory.
# Make sure you run this in a temp directory or you can lose files!
$ ls # List files in the current directory (no output, no files).
$ touch a b c # Create files 'a', 'b' and 'c'.
$ ls # List files again; this time the outputs: 'a', 'b' and 'c'.
a b c
rm 是一个应用程序,用来删除传给它的文件。
* 是一个 glob 字符,一种“通配符”模式,匹配当前目录下的所有文件。
后面我们会进一步讨论 glob。
你注意到 a 和 b 之间有好几个空格,而 b 和 c 之间只有一个了吗?
实参之间有多少空白并不重要!
例如:
$ echo This is a test.
This is a test.
$ echo This is a test.
This is a test.
echo 是一个命令,把它的实参打印到标准输出(在我们的例子里就是终端)。
Bash 把这条命令拆分成五个单词,并以四个实参调用 echo 命令。
第二个例子中多出来的空格没有任何影响。
echo 并不知道这些单词之间原本有多少空格。
如果我们想保留多出来的空白,就需要把整句话作为单个实参传进去。 可以用引号做到这一点:
$ echo "This is a test."
This is a test.
引号会把里面的所有内容组合成一个单独的实参。
这个实参是 This is a test.,包括那些具体的空格字符。
注意,引号本身并不属于实参,bash 在把这个单独的实参交给 echo 之前会把它们去掉。
实参与命令名之间、以及实参相互之间,都用空白分隔。 这一点要记住。 例如,下面这样是错的:
$ [-f file]
bash: [-f: command not found
这段代码的本意是测试名为 file 的文件是否存在。
人们很自然地会以为 [ 和 ] 周围的空白无关紧要,但 [ 其实是一条命令,而且它要求最后一个实参是 ]。
(后面我们会更详细地介绍 [ 命令。)
因此,我们必须把 [ 与 -f 分开,把 ] 与 file 分开。
否则 bash 会以为我们想执行一条名为 [-f 的命令,实参是 file]。
正确的写法是用空白分隔所有实参:
$ [ -f file ]
(我们见过很多人被这个行为弄糊涂;他们以为 [...] “只是语法”,可以省略空格;但 [ 是一条命令,空白是必须的。)
如果我们的文件名里包含空白或其他特殊字符,也需要加引号:
$ [ -f "my file" ]
Bash 能识别几种不同类型的命令:别名、函数、内置命令、关键字和可执行文件。 它们都是命令,行为方式也非常相似。
默认情况下,别名只在交互式 shell 中启用,在 shell 脚本里不启用。 这里不展开讲。
函数里包含 shell 命令,作用很像一个小脚本;它甚至可以接收实参、创建局部变量。 调用函数时,里面的命令就会执行。 函数后面会深入讲解。
Bash 内置了一些基本命令,比如:cd(切换目录)、echo(写出输出)等等。
可以把它们看作已经提供好的函数。
关键字和内置命令很像,主要区别在于关键字其实是 bash 语法的一部分,可能按照特殊规则来解析。
例如,[ 是 bash 的内置命令,而 [[ 是 bash 的关键字;两者都用来测试各种条件。
但 [[ 关键字还有一些额外的功能。
例如,它提供了 =~ 运算符,用来做正则表达式匹配。
$ [[ "hello world" =~ e.*o ]] # returns a "success" status
$ [ "hello world" =~ e.*o ]
bash: [: =~: binary operator expected
字符串比较运算符 < 和 > 是另一个例子,说明关键字会以不同方式解析代码。
在 [[...]] 里,这些运算符不需要特别处理。
$ if [[ "$USER" < "n" ]]; then
echo "$USER is in the first half of the alphabet"
fi
# => glennj is in the first half of the alphabet
但在 [...] 里,必须给它们加引号或转义,免得 shell 把它们解析成重定向运算符。
$ if [ "$USER" < "n" ]; then
echo "$USER is in the first half of the alphabet"
fi
# => bash: n: No such file or directory
$ if [ "$USER" \< "n" ]; then
echo "$USER is in the first half of the alphabet"
fi
# => glennj is in the first half of the alphabet
bash 能执行的最后一类命令是可执行文件。
它们是计算机文件系统上可以运行的可执行文件(程序)。
(可执行文件也叫外部命令或应用程序。)
通常只要输入可执行文件的名字就能调用它;一般不需要给出文件的完整路径。
能做到这一点,是因为 bash 使用了一个预定义的变量来帮助它找到常见可执行文件的位置。
这个变量叫做 PATH。
它是一组用冒号分隔的目录名(例如 /usr/bin:/bin)。
当指定一条命令(例如 myprogram 或 ls)却没有给出文件路径时(而且它不是别名、函数、内置命令或关键字),bash 会依次搜索 PATH 里的目录。
这些目录会从左到右依次搜索,看其中是否有与给定名字对应的可执行文件。
如果可执行文件不在已知路径中,就需要指定它的文件路径。
如果 myprogram 可执行文件在当前目录里,就用 ./myprogram;如果它在 /opt/somedirectory 目录里,就用 /opt/somedirectory/myprogram。
提示:可以用 type 命令来查看命令的详细信息:
$ type rm
rm is hashed (/bin/rm)
# or it might return: `rm is /bin/rm`
$ type cd
cd is a shell builtin
脚本是保存在文件里的一系列命令。 Bash 读取该文件,按顺序处理其中的命令。 只有当前一条命令结束后,它才会继续处理下一条。 例外情况是,某条命令被指定为异步运行(在后台运行)。 现在还不用太在意这种情况;后面我们会学习它是怎么工作的。
本指南中的任何命令行示例都可以用在脚本里。
写一个脚本很简单。 先新建一个文件,在第一行写上:
#!/bin/bash
这一行叫做解释器指令(也叫 hashbang 或 shebang)。
它指定当这个文件作为命令中的可执行文件使用时,用 /bin/bash 作为解释器。
当操作系统内核执行一个非二进制文件时,它会读取文件的第一行。
如果这一行以 #! 开头,内核就会根据它来决定把文件交给哪个解释器。
(也有其他可行的做法,见下文。)
#! 必须位于文件的最开头,前面不能有空格或空行。
我们脚本里的命令会写在它下面的各行。
提示:除了 #!/bin/bash,也可以写
#!/usr/bin/env bash
env 会在 $PATH 中查找由它的第一个实参指定的可执行文件(这里就是 “bash”)。
关于这个技巧以及它与普通的 #!/bin/bash 有何区别的更详细说明,请看 StackOverflow 上的这个问题。
请不要被网上那些用 /bin/sh 作为解释器的脚本或示例骗了。
sh 不是 bash!
Bash 本身是一种 “sh 兼容” 的 shell,也就是说它能运行大多数 sh 脚本,语法也有很多相同之处。
但反过来并不成立;bash 的一些功能在 sh 里会出错或产生意外的行为。
事实上,并不存在唯一的一个 sh shell。
有很多 shell 实现都支持 POSIX-sh 语法。
取决于你的操作系统,当你运行 sh 时,实际执行的可能是 bash、ash、dash 或其他某个 shell。
脚本文件创建好之后,可以这样执行:
$ bash myscript
在这个例子里,我们执行 bash 这条命令,让它读取我们作为实参给出的脚本。
这样执行时,#! 那一行只是注释。
另一种做法是给脚本加上可执行权限。 用这种方法,我们可以像执行其他可执行文件一样执行脚本,而不必手动调用 bash:
$ chmod +x myscript # Mark the file as executable.
$ ./myscript # Now, myscript can be executed directly
# instead of having to specify bash.
以这种方式运行命令时,操作系统(OS)会根据 #! 那一行来决定用哪个解释器来运行你的脚本。
本课内容取自 Bash Guide 上的 Commands and Arguments。