在过去几十年中所出现的UNIX和类UNIX操作系统家族已经成为如今最为流行、使用最广泛的操作系统之一,这都算不上什么秘密了。对于使用了多年UNIX的程序员而言,一切都顺理成章:UNIX系统为程序开发提供了既优雅又高效的环境。这正是Dennis Ritchie和Ken Thompson在20世纪60年代晚期在贝尔实验室开发UNIX时的初衷。
在本书中,我们使用的术语UNIX泛指基于UNIX的操作系统大家族,其中包括像Solaris这样真正的UNIX操作系统以及像Linux和Mac OS X这样的类UNIX操作系统。
UNIX系统最重要的特性之一就是各式各样的程序。超过200个基本命令会随着标准操作系统发行,Linux还对标准命令数量做了扩充,通常能达到700~1000个!这些命令(也称为工具)从统计文件行数、发送电子邮件到显示特定年份的日历,可谓无所不能。
不过UNIX真正的威力并非来自数量庞大的命令,而在于你可以非常轻松、优雅地将这些命令组合在一起完成非常复杂的任务。
UNIX的标准用户界面是命令行,其实就是Shell,它的角色是作为用户和系统最底层之间(内核)的缓冲带。Shell就是一个程序,读入用户输入的命令,将其转换成系统更易于理解的形式。它还包括了一些核心编程构件,可以做出判断、执行循环以及为变量储值。
从AT&T发行版(源自Stephen Bourne在贝尔实验室编写的初版)开始,标准Shell就是同UNIX系统捆绑在一起的。自那时起,IEEE根据Bourne Shell以及后续的一些其他Shell制订了标准。该标准目前的(本书写作之时)版本是Shell and Utilities volume of IEEE Std 1003.1-2001,也称为POSIX标准。本书余下的内容都离不开Shell。
在本章中,你将学习到什么是UNIX的Shell,Shell能够做什么,以及为什么说它是每个高级用户工具箱中不可或缺的一部分。20分钟轻松学会shell很容易,不过如果想要全面掌握还需要专业的书籍来深度学习。
本文摘自《UNIX/Linux/OS X中的Shell编程(第4版)》
试读地址:
www.epubit.com.cn/book/online…
2.1 内核和实用工具
UNIX系统在逻辑上被划分为两个不同的部分:内核和实用工具(Utility),如图2.1所示。或者你也可以认为是内核和其他部分,通常来说,所有的访问都要经由Shell。
图2.1 UNIX系统
内核是UNIX系统的核心所在,当打开计算机并启动(booted)之后,内核就位于计算机的内存中,直到关机为止。
组成完整的UNIX系统的各种实用工具位于计算机磁盘中,在需要的时候会被加载到内存中并执行。实际上你所知道的所有UNIX命令都是实用工具,因此这些命令所对应的程序也都在磁盘上,仅在需要时才会被载入内存。举例来说,当你执行date
命令时,UNIX系统会将名为date
的程序从磁盘上载入到内存中,读取其代码来执行特定的操作。
Shell也是一个实用工具程序,它作为登录过程的一部分被载入到内存中执行。实际上,有必要了解当终端或终端窗口中的第一个Shell启动时所发生的一系列事件。
2.2 登录Shell
在早期,终端是一个物理设备,通过线缆连接到安装了UNIX系统的硬件上。而如今,终端程序能够让你停留在Linux、Mac或Windows环境内部,在受控窗口(managed window)中同网络上的设备交互。通常来说,你会启动如Terminal或xterm这类程序,然后在需要的时候利用ssh
、telnet
或rlogin
连接到远程系统。
对于系统上的每个物理终端,都会激活一个叫作getty
的程序,如图2.2所示。
图2.2 getty
进程
只要系统允许用户登录,UNIX系统(更准确地说,应该是叫作init
的程序)就会在每个终端端口自动启动一个getty
程序。getty
是一个设备驱动程序,能够让login
程序在其所分配的终端上显示login:
,等待用户输入内容。
如果你是通过ssh
这类程序来连接的,会分配到一个伪终端或伪tty
。这就是为什么在输入who
命令时会看到有类似于ptty3
或pty1
这样的条目。
在这两种情况下,会有程序读取账户和密码信息,对这些信息进行验证,如果没有问题的话,就调用登录所需的登录程序。
只要输入相应字符并敲下Enter键,login
程序就完成了登录过程(见图2.3)。
当login
开始执行时,它会在终端上显示字符串Password:
,然后等待用户输入密码。完成输入并按下Enter键后(出于安全性的考虑,你在屏幕上看不到输入的内容),login
会比对文件/etc/passwd
中相应的条目来验证登录名和密码。每个用户在该文件中都有对应的条目,其中包括了登录名、主目录以及用户登录后要启动的程序。最后一部分信息(登录Shell)存储在每行最后一个冒号之后。如果这个冒号后面没有内容,则默认使用标准Shell,即/bin/sh
。
图2.3 用户sue
终端上启动的login
如果是通过终端程序登录,数据交换也许会涉及系统上的程序(如ssh
)和服务器上的程序(如sshd
),要是你在自己的UNIX计算机上打开了窗口,可能不需要再次输入密码就能够立刻登入。非常方便!
把话题转回密码文件。下面3行展示了/etc/passwd
文件内容的典型形式,对应着系统用户:sue
、pat
和bob
。
sue:*:15:47::/users/sue:
pat:*:99:7::/users/pat:/bin/ksh
bob:*:13:100::/users/data:/users/data/bin/data_entry复制代码
待login
将所输入密码的加密形式与特定账户保存在/etc/shadow
中的加密形式进行比对之后,如果没有问题,它会检查要执行的登录程序的名称。在绝大多数情况下,这个登录程序会是/bin/sh
、/bin/ksh
或/bin/bash
。在少数情况下,可能会是一个特殊的定制程序或者/bin/nologin
,后者用于不能进行交互式访问的账户(常用于文件所有权管理)。其背后的理念就是你可以为登录账户进行设置,使其登录到系统之后能够自动运行指定的程序。大多数时候指定的程序都是Shell,毕竟它是一种通用的实用工具,不过这并非是唯一的选择。
来看用户sue。一旦该用户通过验证,login
会结束掉自身,将控制权转交给sue的终端连接,该连接与标准Shell相连,然后login就从内存中消失了(见图2.4)。
按照之前/etc/passwd
文件中显示的其他条目,pat
得到的是存储在/bin
下的ksh
(这是Korn Shell),bob得到的是一个名为data_entry
的指定程序(见图2.5)。
图2.4 login
执行/usr/bin/sh
图2.5 3个登录的用户
之前提到过,init
程序会针对网络连接运行类似于getty
的程序。例如,sshd
、telnetd
和rlogind
会响应来自ssh
、telnet
和rlogin
的连接请求。这些程序并没有直接和特定的物理终端或调制解调器线路联系在一起,而是将用户的Shell连接到伪终端上。你可以在X Window系统的窗口中或使用who
命令查看是否已经通过网络或联网的终端连接登录到了系统中:
$ who
phw pts/0 Jul 20 17:37 使用rlogin登录
$复制代码
2.3 在Shell中输入命令
当Shell启动时,它会在终端中显示出一个命令行提示符,通常是美元符$
,然后等待用户输入命令(图2.6中的第1步和第2步)。每次输入命令并按Enter键(第3步),Shell就会分析输入的内容,然后执行所请求的操作(第4步)。
如果你要求Shell调用某个程序,Shell会搜索磁盘,查找环境变量PATH中指定的所有目录,直到找到指定的程序。找到了该程序后,Shell会将自己复制一份(称为子Shell),让内核使用指定的程序替换这个子Shell,接着登录Shell就会“休眠”,等待被调用的程序执行完毕(第5步)。内核将指定程序复制到内存中并开始执行。这个复制过来的程序称为进程。程序和进程之间是有区别的,前者是保存在磁盘上的文件,而后者位于内存中并被逐行执行。
如果程序将输出写入到标准输出中,那么输出内容会出现在终端里,除非你将其重定向或通过管道导向其他命令。与此类似,如果程序从标准输入中读取输入,那么它会等着你输入内容,除非输入被重定向到了另一个文件或通过管道从其他命令导入(第6步)。
当命令执行完毕后,就会从内存中消失,控制权再次交给登录Shell,它会提示你输入下一条命令(第7步和第8步)。
图2.6 命令执行周期
注意,只要你没有登出系统,这个周期就会周而复始下去。如果登出系统,Shell就会终止执行,系统将会启动一个新的getty
(或者rlogind
等)并等待其他用户登入,如图2.7所示。
重要的是要认识到Shell就是一个程序而已。它在系统中没有什么特权,也就是说,只要有足够的专业技术和热情,任何人都可以创建自己的Shell。这就是为什么如今会有这么多不同风格的Shell,其中包括由Stephen Bourne开发的古老的Bourne Shell、由David Korn开发的KornShell、主要用于Linux系统的Bourne again Shell以及由Bill Joy开发的C Shell。这些Shell都旨在应对特定的需求,各自都有自己独特的功能和特色。
图2.7 登录周期
2.4 Shell的职责
现在你知道了Shell会分析(用计算机行话来说,就是解析)输入的每一行命令,然后执行指定的程序。在解析期间,文件名中的特殊字符(如*
)会被扩展,就像第一章讲到的那样。
Shell还有其他的职责,如图2.8所示。
图2.8 Shell的职责
2.4.1 程序执行
Shell负责执行你在终端中指定的所有程序。
每次输入一行内容,Shell就会分析该行,然后决定执行什么操作。就Shell而言,每一行都遵循以下基本格式:
program-name arguments复制代码
说得更正式些,输入的这一行叫做命令行。Shell会扫描该命令行,确定要执行的程序名称及所传入的程序参数。
Shell使用一些特殊字符来确定程序名称及每个参数的起止。这些字符统称为空白字符(whitespace characters),它们包括空格符、水平制表符和行尾符(更正式的叫法是换行符)。连续的多个空白字符会被Shell忽略。如果你输入命令
mv tmp/mazewars games复制代码
Shell会扫描该命令行,提取行首到第一个空白字符之间的所有内容作为待执行的程序名称:mv
。随后的空白字符(多余的空格)会被忽略,直到下一个空白字符之间的字符作为mv
的第一个参数:tmp/mazewars
。再到下一个空白字符(在本例中是换行符)之间的字符作为mv
的第二个参数:games
。解析完命令行之后,Shell就开始执行mv
命令,其中包括两个指定的参数:tmp/mazewars
和games
(见图2.9)。
图2.9 执行带有两个参数的mv
命令
刚才提到过,多个空白字符会被Shell忽略。这意味着当Shell处理下面的命令行时:
echo when do we eat? 复制代码
会向echo
程序传递4个参数:when
、do
、we
和eat?
(见图2.10)。
图2.10 执行带有4个参数的echo
命令
echo
会提取命令参数并将其显示在终端中,因此在输出的参数之间加上一个空格会使得命令输出变得更易读:
$ echo when do we eat?
when do we eat?
$复制代码
结果证明echo
命令完全看不到这些空白字符,它们都被Shell给“没收”了。等到第5章讲引用的时候,你就知道该如何把空白字符包含到程序参数中了,不过,通常来说,去掉这些多余的空白字符正是我们想要的做法。
我们之前讲到过,Shell会搜索磁盘,直到找到需要执行的程序为止,然后由UNIX内核负责程序的执行。在大多数时候,的确如此。但有些命令实际上是内建于Shell自身中的。这些内建命令包括cd
、pwd
和echo
。Shell在磁盘中搜索命令之前,它首先会判断该命令是否为内建命令,如果是的话,就直接执行。
不过在调用命令之前,Shell还有点事需要处理,因此,让我们先来讨论一下这方面的内容。
2.4.2 变量及文件名替换
和比较正式的编程语言一样,Shell允许将值赋给变量。只要你在命令行中将某个变量放在美元符号$之后,Shell就会将该变量替换成对应的变量值。我们会在第4章中详细讨论这个话题。
除此之外,Shell还会在命令行中执行文件名替换。实际上Shell,在确定要执行的程序及其参数之前,会扫描命令行,从中查找文件名替换字符*
、?
或[...]
。
假设当前目录下包含这些文件:
$ ls
mrs.todd
prog1
shortcut
sweeney
$复制代码
现在让我们在echo
命令中使用文件名替换(*
):
$ echo `*``` 列出所有文件
mrs.todd prog1 shortcut Sweeney
$复制代码
我们给echo
程序传入了几个参数?1个还是4个?因为Shell会执行文件名替换,所以答案是4个。当Shell分析下列命令行时
echo *复制代码
它识别出了特殊字符*
,将其替换成当前目录下的所有文件名(甚至还会将这些文件名依字母顺序排列):
echo mrs.todd prog1 shortcut sweeney复制代码
然后Shell决定将哪些参数传给实际的命令。因此,echo
根本不知道星号*
的存在,它只知道命令行上有4个参数(见图2.11)。
图2.11 执行echo
2.4.3 I/O重定向
Shell还要负责处理输入/输出重定向。它会扫描每一个命令行,从中查找特殊的重定向字符<
、>
或>>
(如果你觉得好奇的话,还有一个重定向序列<<
,你会在第12章中学到相关的内容)。
如果你输入命令
echo Remember to record The Walking Dead > reminder复制代码
Shell会识别出特殊的输出重定向字符>
,然后将命令行中的下一个单词作为输出重定向所指向的文件名。在本例中,这个文件名为reminder
。如果reminder
已经存在且用户具有写权限,那么文件中已有的内容会被覆盖掉。如果没有该文件或其所在目录的写权限,Shell会产生错误信息。
在Shell执行程序之前,它会将程序的标准输出重定向到指定的文件。在大多数情况下,程序根本不知道自己的输出被重定向了。它仍照旧向标准输出中写入(这通常是终端),意识不到Shell已经将信息重定向到了文件中。
让我们来看两个几乎一样的命令:
$ wc -l users
5 users
$ wc -l < users
5
$复制代码
在第一个例子中,Shell解析命令行,确定要执行的程序名称是wc
并为其传入两个参数:-l
和users
(见图2.12)。
图2.12 执行wc -l users
当wc
执行时,会看到传入的两个参数。第一个参数是-l
,告诉它需要统计行数。第二个参数指定了待统计行数的文件。因此wc
会打开文件users
,统计行数,然后打印出结果及对应的文件名。
第二个例子中的wc
操作略有不同。Shell在扫描命令行时发现了输入重定向字符<
,其后的单词就被解释成从中重定向输入的文件名。从命令行中提取出了“< users
”之后,Shell就开始执行wc
程序,将其标准输入重定向为文件users
并传入单个参数-l
(见图2.13)。
图2.13 执行wc -l < users
这次当wc
执行时,它会看到传入的单个参数-l
。因为没有指定文件名,wc
会转而去统计标准输入中内容的行数。因此wc -l
在统计行数时,并不知道它实际上是在对文件users
进行统计。最后的显示结果和平时一样,但是缺少了文件名,因为我们并没有为wc
指定。
要理解两条命令在执行上的不同,这一点非常重要。如果还不太清楚,那么在继续阅读之前复习一下上面的内容。
2.4.4 管道
Shell在扫描命令行时,除了重定向符号之外还会查找管道字符|。每找到一个,就会将之前命令的标准输出连接到之后命令的标准输入,然后执行这两个命令。
如果你输入
who | wc -l复制代码
Shell会查找分隔了命令who
和wc
的管道符号。它将上一个命令的标准输出连接到下一个命令的标准输入,然后执行两者。who
命令执行时会生成已登录用户列表并将结果写入标准输出,它并不知道输出内容并没有出现在终端而是进入了另一个命令。
当wc
命令执行时,它发现并没有指定文件名,因此就对标准输入内容进行统计,并没有意识到标准输入并非来自终端,而是来自于who
命令的输出。
随着本书内容的深入,你会看到管道中并不仅限于有两条命令,你可以在复杂的管道中将3条、4条、5条甚至更多的命令串联在一起。这多少有点不好理解,但却是UNIX系统强大威力的所在。
2.4.5 环境控制
Shell提供了一些能够定制个人环境的命令。个人环境包括主目录、命令行提示符以及用于搜索待执行程序的目录列表。我们会在第10章中对此展开详述。
2.4.6 解释型编程语言
Shell有自己内建的编程语言。这种语言是解释型的,也就是说,Shell会分析所遇到的每一条语句,然后执行所发现的有效的命令。这与C++及Swift这类编程语言不同,在这些语言中,程序语句在执行之前通常会被编译成可由机器执行的形式。
相较于编译型语言,由解释型语言所编写的程序一般要更易于调试和修改。然而,所花费的时间要比实现相同功能的编译型语言程序更长。
Shell编程语言提供了可在大多数其他编程语言中找到的其他特性。它有循环结构、决策语句、变量、函数,而且是面向过程的。基于IEEE POSIX标准的现代Shell还有许多其他特性,包括数组、