书名:UNIX/Linux/OS X中的Shell编程(第4版)
ISBN:978-7-115-47041-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [美]Stephen G. Kochan Patrick Wood
译 门 佳
责任编辑 傅道坤
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Stephen G. Kochan and Patrick Wood: Shell Programming in Unix, Linux and OS X (Fourth Edition)
Copyright © 2017 Pearson Education, Inc.
ISBN: 9780134496009
All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise without the prior consent of Addison Wesley.
版权所有。未经出版者书面许可,对本书任何部分不得以任何方式或任何手段复制和传播。 本书中文简体字版由人民邮电出版社经Pearson Education, Inc.授权出版。版权所有,侵权必究。
本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。
本书是经典图书Unix Shell Programming时隔15年之后的全新升级版本,全面讲解了如何在POSIX标准Shell环境中开发程序,以充分发挥UNIX和类UNIX操作系统的潜在功能。
本书共分为14章,其内容涵盖了Linux/UNIX的基础知识,Shell的概念、工作原理和运行机制,编写Shell程序时使用的一些工具,Shell中的脚本与变量,在Shell中如何解释引用,传递参数,条件语句,循环,数据的读取及打印,Shell环境,交互式以及非标准Shell的特性等。本书后面的两个附录还提供了POSIX标准Shell的特性汇总信息,以及有助于进一步学习掌握Shell编程的资源。
本书坚持以“实例教学”为理念,旨在鼓励读者动手实践,从而彻底掌握Shell编程。本书实例丰富,内容易懂,特别适合有志于掌握Shell编程的Linux/UNIX初级用户阅读。
Stephen Kochan是多本UNIX和C语言畅销书的作者与合著者,其中包括Programming in C、Programming in Objective-C、Topics in C Programming和Exploring the Unix System。他之前是AT&T贝尔实验室的软件顾问,负责开发和讲授UNIX和C语言编程相关的课程。
Patrick Wood是Electronics for Imaging公司(坐落于新泽西)的CTO(首席技术官)。他之前曾经是贝尔实验室的一名技术人员,并在1985年遇到了Kochan先生。随后他们俩共同创建了Pipeline Associates, Inc. 公司,提供UNIX咨询服务,当时他是公司的副总裁。他们共同写作了Exploring the Unix System、Unix System Security、Topics in C Programming和Unix Shell Programming等图书。
在过去几十年中所出现的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。
书中的例子在运行着Mac OS X 10.11的Mac计算机、Ubuntu Linux 14.0以及运行着SunOS 5.7旧版的Sparcstation Ultra-30下均通过了测试。除了第14章中的一些Bash示例,其他所有的例子都是用Korn Shell运行的,当然,在Bash下也没有问题。
因为Shell提供了一种解释型编程语言,所以能够快速方便地编写、修改和调试程序。因此,我们使用Shell作为首选编程语言,等你熟练掌握了Shell编程之后,相信你也会做出同样的选择。
本书假设你熟悉系统和命令行相关的基础知识,也就是说,知道怎么登录,知道如何创建、编辑、删除文件,也知道如何使用目录。如果你对于Linux或UNIX已经手生了,我们在第1章“基础概述”中会复习一些基础知识。除此之外,这一章中也会讲到文件名替换、I/O重定向及管道。
在第2章“什么是Shell”中,解答了Shell究竟是什么、它的工作原理,以及如何利用Shell作为与操作系统交互的主要方式。你将了解每次登录时所发生的事情、Shell程序是如何启动的、如何解析命令行以及如何为你执行其他程序。第2章的关键点在于要明白Shell不过是另一个程序罢了,没什么特别的地方。
第3章“常备工具”讲解了一些有助于编写Shell程序的工具,其中包括cut、paste、sed、grep、sort、tr和uniq。在这些工具的选择上的确比较主观,但它们为书中接下来要开发的程序打下了基础。另外,本章还详细讨论了正则表达式,在很多UNIX命令中都对其有所涉及,如sed、grep和ed。
第4章~第9章继续为编写Shell程序做铺垫。你将学习到如何编写自己的命令、变量的用法、编写可接受参数的程序、条件判断、循环命令(for、while和until)以及使用read命令从终端或文件中读取数据。第5章“引用”专门讨论了Shell中最有意思(通常也会令人困惑)的话题之一:如何解释引用。
到这里,所有基本的Shell编程构件已经全部讲完,你已经有能力编写Shell程序,并解决特定的问题了。
第10章“环境”所讲述的主题(环境)对于真正理解Shell的运作方式非常重要。你会在本章中学到局部变量和导出变量、子Shell、特殊的Shell变量(如HOME、PATH和CDPATH)以及如何设置.profile文件。
第11章“再谈参数”和第12章“拓展内容”讲述了一些之前没有提及的知识点,在第13章“再谈rolo”中给出了名为rolo的电话簿程序的最终版,该程序的开发过程贯穿全书。
第14章“交互式与非标准Shell特性”讨论了多种Shell特性,这些特性要么并非IEEE POSIX标准Shell的正式组成部分(不过在大部分UNIX和Linux Shell中都可以使用),要么主要是以交互方式使用,而非用于程序中。
附录A“Shell总结”中总结出了IEEE POSIX标准Shell的各种特性。
附录B“更多的相关信息”中列出了参考资料和资源,包括不同Shell的下载站点。
本书所秉持的哲学是实例教学法。我们坚信:在演示某种特性的具体用法时,恰当选择的实例所带来的效果要远胜于干巴巴的陈述。“一图胜……”的那句老话看起来也适用于编码。
我们鼓励你在自己的系统中敲入并测试每个例子,只有这样你才能掌握Shell编程。不要畏惧尝试。试着修改例子中的命令来观察效果,或是加入不同的选项和特性,使程序变得更加实用或强健。
本章将会对UNIX系统进行简要讲述,其中包括文件系统、基本命令、文件名替换、I/O重定向及管道。
date
命令可以显示出日期和时间:
$ date
Thu Dec 3 11:04:09 MST 2015
$
date
会打印出星期、月份、日期、时间(24小时制,依据系统时区设置)及年份。在本书的所有例子中,我们使用加粗字体,来表示用户输入的内容,使用正常字体表示UNIX系统显示的内容,使用楷体表示交互过程中的注释。
按Enter(回车)键就可以将UNIX命令提交给系统。Enter键表示你已经完成了输入,剩下的事情就交给UNIX系统了。
who
命令可以用来获取当前已登录到系统中的所有用户的信息:
$ who
pat tty29 Jul 19 14:40
ruth tty37 Jul 19 10:54
steve tty25 Jul 19 15:52
$
目前,已登录的用户有3名:pat
、ruth
和steve
。除了每个用户的ID之外,还列出了用户所在的tty编号以及用户登录的日期和时间。当用户登录系统时,UNIX系统会为用户所在的终端或网络设备分配一个唯一的标识数字,这个数字就是tty编号。
也可以使用who
命令来获取本人的信息:
$ who am i
pat tty29 Jul 19 14:40
$
who
和who am i
其实都是同一个命令:who
。在后一种用法中,am
和i
是who
命令的参数(这并不是一个展示命令行参数用法的好例子;只是出于对who
命令的好奇心而已)。
echo命令会在终端打印出(或者说回显)你在行中输入的所有内容(这里有一些例外情况,随后你就会知道):
$ echo this is a test
this is a test
$ echo why not print out a longer line with echo?
why not print out a longer line with echo?
$ echo
显示空行
$ echo one two three four five
one two three four five
$
在上面的例子中,你会注意到echo
将单词间多余的空白字符(blank)压缩了。这是因为在UNIX系统中,单词(word)非常重要,而空白字符就是用来分隔单词的。UNIX系统通常会忽略多余的空白字符(下一章中会详细讲述相关内容)。
UNIX系统只识别3种基本类型的文件:普通文件、目录文件和特殊文件。普通文件就是系统中包含数据、文本、程序指令或其他内容的那些文件。目录,或者说是文件夹,会在本章后续部分讲述。最后,和名字一样,特殊文件是对UNIX系统有特殊意义的文件,通常和某种形式的I/O相关联。
文件名可以由键盘上能够直接输入的任意字符(有些字符甚至可以是无法直接输入的)组成,总数量不能超过255个。如果文件名中的字符多于255个,UNIX系统会忽略多余的字符。
UNIX系统提供了很多便于文件处理的工具。接下来我们简单地介绍几个相关的文件操作命令。
要查看目录下的文件,可以使用ls
命令:
$ ls
READ_ME
names
tmp
$
命令输出表明当前目录下包含READ_ME
、names
和tmp
这3个文件(注意,ls
的输出随系统而异。例如,在很多UNIX系统中,当ls
向终端输出时,其输出内容会分为多列;在另一些系统中,不同类型的文件会用不同的颜色表示。你可以使用-1
选项[数字1]强制单列输出)。
你可以使用cat
命令来检查文件的内容(这个命令是concatenate的简写,可不是指猫科动物)。cat
的参数是待检查的文件名:
$ cat names
Susan
Jeff
Henry
Allan
Ken
$
你可以使用wc
命令获得文件中的行数、单词数和字符数。仍需要将待统计的文件名作为该命令的参数:
$ wc names
5 7 27 names
$
wc
命令在文件名前列出了3个数字,第一个数字表示文件行数(5),第二个数字表示单词数(7),第三个数字表示字符数(27)。
大多数UNIX命令允许在命令执行时指定选项。选项通常采用如下形式:
-letter
也就是说,命令选项是减号(-
)后面直接跟上单个字母。例如,要计算文件中包含的行数,可以使用wc
命令的-l
选项(字母l):
$ wc -l names
5 names
$
要统计文件中包含的字符数,可以指定-c
选项:
$ wc -c names
27 names
$
最后,-w
选项可以用来统计文件中包含的单词数:
$ wc -w names
7 names
$
有些命令要求选项应该出现在文件名参数之前。例如,sort names -r
没有问题,但wc names -l
就不行了。不过前一种形式并不多见,大多数UNIX命令的设计是让你先指定命令行选项,就像wc -l names
那样。
可以使用cp
命令来复制文件。该命令的第一个参数是要复制的文件名(称为源文件),第二个参数是要复制为的文件名(称为目标文件)。你可以像下面这样将文件names复制为saved_names
:
$ cp names saved_names
$
执行过该命令之后,文件names
的内容会被复制到一个名为saved_names
的新文件中。和很多UNIX命令一样,cp
命令在执行后没有任何输出(除了命令行提示符)表明该命令执行成功。
可以使用mv
(move)命令重命名文件。mv
命令的参数形式和cp
命令一样。第一个参数是待重命名的文件,第二个参数是文件的新名字。因此,如果要想将文件saved_names
更名为hold_it
,可以使用下列命令:
$ mv saved_names hold_it
$
注意,在执行mv
或cp
命令时,UNIX系统可不管命令行中第二个参数指定的文件是否存在。如果存在,文件内容会被覆盖。举例来说,如果有个名为old_names
的文件已经存在,执行命令:
cp names old_names
会将文件names
复制为old_names
,同名文件之前的内容就丢失了。与此类似,下面的命令:
mv names old_names
会将names
更名为old_names
,即使在命令执行前文件old_names
已经存在了。
rm
命令可以从系统中删除文件。rm
命令的参数就是要删除的文件:
$ rm hold_it
$
你可以使用rm
命令一次删除多个文件,只需要将这些文件在命令行上列出就可以了。例如,下列命令将删除文件wb
、collect
和mon
:
$ rm wb collect mon
$
假设你有一组文件,里面包含了各种备忘录、建议书和信件。再假设你还有另一组文件,里面都是计算机程序。合理的做法是把第一组文件放到名为documents
的目录中,把后一组文件放到名为programs
的目录中。图1.1演示了这种目录组织方式。
图1.1 目录结构示例
documents
目录中包含了文件plan
、dact
、sys.A
、new.hire
、no.JSK
和AMG.reply
。目录programs
中包含了文件wb
、collect
和mon
。随后你可能想在目录中进一步组织文件。这可以通过创建子目录并将文件放置到相应的子目录中来实现。例如,你可能想在documents
目录下创建名为memos
、proposals
和letters
的子目录,如图1.2所示。
图1.2 包含子目录的目录
documents
中包含了子目录memos
、proposals
和letters
。每个子目录分别又包含了两个文件:memos
中包含了plan
和dact
;proposals
中包含了sys.A
和new.hire
;letters
中包含了no.JSK
和AMG.reply
。
尽管特定目录中的每个文件的名字都不能重复,但包含在不同目录中的文件则没有此要求。因此,在programs
目录中可以有一个叫做dact
的文件,哪怕是在memos
子目录下已经有了同名的文件。
UNIX系统将系统中每个用户与一个特定的目录关联起来。当你登录系统后,会自动处于你所属的目录中(这称为个人的主目录)。
用户主目录的位置视系统而异,假设你的主目录叫做steve
,它是users
目录下的一个子目录。因此,如果你还拥有documents
和programs
目录,那么整个目录结构如图1.3所示。在目录树的顶部有一个名为/
(读作slash)的特殊目录,该目录称为根目录(root)。
图1.3 层次化目录结构
当你处于某个特定目录内时(这叫做当前工作目录),该目录中所包含的文件可以直接访问,无须指定路径。如果想访问其他目录中的文件,要么先使用命令“切换”到对应的目录,然后访问;要么通过路径名来指定要访问的文件。
路径名允许你唯一地标识出UNIX系统中某个特定文件。在路径名的写法中,路径中连续的目录之间用字符“/”分隔。以字符“/”起始的路径名称为完整路径名或绝对路径名,因为它指定了从根目录开始的完整路径信息。例如,/users/steve
指明了目录steve
包含在users
目录中。类似地,/users/steve/documents
引用了users
目录下的steve
子目录中的documents
目录。作为最后一个例子,/users/steve/documents/letters/AMG.reply
指定了包含在对应路径下的AMG.reply
文件。
为了帮助减少所需要的输入,UNIX提供了一些惯用写法。不是以“/”开头的路径名称为相对路径名:这种路径相对的是当前工作目录。例如,如果你登录系统,进入了主目录/users/steve
,你只需要输入documents
就可以引用该目录。与此类似,相对路径名programs/mon
可以访问programs
目录下的文件mon
。
按照惯例,..
指向当前目录的上一级目录,也称为父目录。例如,你现在位于主目录/users/steve
,路径名..
引用的是users
目录。如果你通过命令将工作目录更改到documents/letters
,那么路径名..
引用的就是documents
目录,../..
引用的则是steve
目录,../proposals/new.hire
引用的是包含在proposals
目录中的new.hire
文件。指向特定文件的路径通常不止一个,这非常符合UNIX的特点。
另一种惯用写法是单点号.
,它总是引用当前目录。在本书随后的部分中,当你想指定未在PATH
中的当前目录下的Shell脚本时,这种写法就变得很重要了。我们很快会详细解释这一点。
pwd
命令可以告诉你当前工作目录的名字,帮助你确定自己所处的位置。
回想图1.3中的目录结构。登录系统后所处的目录叫做主目录。你可以假定用户steve
的主目录是/users/steve
。因此,无论steve
什么时候登录到系统,他都会自动进入该目录中。要验证这一点,可以使用pwd
(print working directory)命令:
$ pwd
/users/steve
$
该命令的输出证实了steve
的当前工作目录就是/users/steve
。
你可以使用cd
命令更改当前工作目录。该命令使用目标目录名作为参数。
假设你登录到系统后进入到了主目录/user/steve
中。图1.4中用箭头指出了这个位置。
有两个目录:documents
和programs
,正处于steve
的主目录之下。这一点很容易验证,只需要在终端中输入ls
命令:
$ ls
documents
programs
$
和之前列出普通文件的例子一样,ls
命令列出了documents
和programs
这两个目录。
图1.4 当前工作目录steve
要想更改当前工作目录,使用cd
命令,后面跟上新的目录名:
$ cd documents
$
执行完该命令后,你就进入了documents
目录,如图1.5所示。
图1.5 cd documents
你可以在终端中使用pwd
命令来验证工作目录是否已经改变:
$ pwd
/users/steve/documents
$
移动到上一级目录最简单的方法就是将..
用在命令中:
cd ..
因为按照惯例,..
总是指向上一级目录(见图1.6)。
$ cd ..
$ pwd
/users/steve
$
图1.6 cd ..
如果你想更改到letters
目录,可以使用cd
命令,同时指定相对路径documents/letters
(见图1.7):
$ cd documents/letters
$ pwd
/users/steve/documents/letters
$
如果要返回到主目录中,可以使用cd
命令,向上移动两级目录:
$ cd ../..
$ pwd
/users/steve
$
或者也可以不通过相对路径,而是使用完整的路径名来返回主目录:
$ cd /users/steve
$ pwd
/users/steve
$
最后,返回主目录的第3种方法,也是最简单的方法,就是输入不带有任何参数的cd
命令。无论你当前处于文件系统中的什么位置,这种用法都可以将你直接带回主目录中:
$ cd
$ pwd
/users/steve
$
图1.7 cd documents/letters
输入ls
命令后,当前工作目录下的文件都会被列出。但你也可以使用ls
来列出其他目录中的文件,只需要将目录名作为命令参数就行了。先返回主目录:
$ cd
$ pwd
/users/steve
$
看一下当前工作目录中的文件:
$ ls
documents
programs
$
如果你将其中一个目录名称提供给ls
命令,就可以得到该目录中的内容列表。输入ls documents
可以查看documents
目录下的内容:
$ ls documents
letters
memos
proposals
$
要查看子目录memos
,操作方法类似:
$ ls documents/memos
dact
plan
$
如果你指定的参数不是目录,那么ls
只会在终端上显示出该文件的名字:
$ ls documents/memos/plan
documents/memos/plan
$
不明白了?ls
命令有一个选项,可以用来确定某个特定文件是否为目录。-l
选项(字母l)可以给出目录下文件更详细的描述信息。假设你现在处于steve
的主目录中,下面是ls
命令的-l
选项的输出:
$ ls –l
total 2
drwxr-xr-x 5 steve DP3725 80 Jun 25 13:27 documents
drwxr-xr-x 2 steve DP3725 96 Jun 25 13:31 programs
$
第一行显示出了所列出文件占用的存储块(1024字节)数。后续的每一行都包含了目录中某个文件的详细信息。每行第一个字符指明了文件类型:目录是d
,文件是-
,特殊文件是b
、c
、l
或p
。
接下来的9个字符定义了文件或目录的访问权限。访问模式(access mode)应用于文件所有者(前3个字符)、与文件所有者同组的其他用户(接下来的3个字符)以及系统中的其他用户(最后3个字符)。访问模式通常指明了某类用户是否能够读取文件、写入文件或执行文件(如果是程序或Shell脚本)。
ls -l
命令然后会显示出链接数(参见本章随后的“文件链接:ln
命令”一节)、文件所有者、文件所属组、文件大小(其中包含了多少个字符)以及文件最后的修改时间。最后一部分信息是文件名。
注意
很多现代UNIX系统已经不再使用组了,因此尽管相关的权限信息仍旧会显示,但是文件和目录的所属组在ls命令的输出中通常都被忽略了。
现在你应该就能够利用ls -l
的输出获得目录中文件的详细信息了:
$ ls -l programs
total 4
-rwxr-xr-x 1 steve DP3725 358 Jun 25 13:31 collect
-rwxr-xr-x 1 steve DP3725 1219 Jun 25 13:31 mon
-rwxr-xr-x 1 steve DP3725 89 Jun 25 13:30 wb
$
每一行的第一列中的连接号(-
)指明了collent
、mon
和wb
这3个文件是普通类型的文件,每一行的并非目录。那么你能不能看出这些文件有多大?
mkdir
命令可用于创建目录。该命令的参数就是你要创建的目录名。举例来说,假设当前的目录结构仍和图1.7中的一样,你希望创建一个和目录documents
和programs
处于同一层级的新目录misc
。如果你处于主目录中,输入mkdir misc
就可以实现想要的结果:
$ mkdir misc
$
如果你现在执行ls
,就会看到新创建的目录:
$ ls
documents
misc
programs
$
现在的目录结构如图1.8所示。
图1.8 包含新目录misc的目录结构
cp
命令可以用来在目录间复制文件。例如,你可以将programs
目录下的文件wb
复制到misc
目录下的文件wbx
:
$ cp programs/wb misc/wbx
$
因为两个文件在不同的目录中,就算名字相同也没有问题:
$ cp programs/wb misc/wb
$
如果目标文件打算采用和源文件相同的名字(显然是在不同的目录中),只需要指定目标目录作为第二个参数就行了:
$ cp programs/wb misc
$
在执行这个命令时,UNIX系统会发现第二个参数只是一个目录,于是就会将源文件复制到该目录中。新的文件和源文件采用一样的名字。
你可以一次向目录中复制多个文件,只需要将多个文件名放在目标目录之前就可以了。假设你当前在programs
目录中,执行下列命令:
$ cp wb collect mon ../misc
$
会将文件wb
、collect
和mon
以相同的名字复制到misc
目录中。
要想将文件从其他目录中复制到你当前所处的位置上并采用相同的名字,可以使用“.”作为当前目录的简写:
$ pwd
/users/steve/misc
$ cp ../programs/collect .
$
上面的命令将文件collect
从目录../programs
复制到当前目录中(/users/steve/misc
)。
回忆一下用来给文件更名的mv
命令。的确,在UNIX系统中其实并没有rename
命令。如果mv
命令的两个参数指向的是不同的目录,那么文件会从第一个目录移动到第二个目录。
从主目录进入documents
目录:
$ cd documents
$
假设memos
目录中包含的文件plan
是一份提议,你要把它从该目录移动到proposals
目录中。命令如下:
$ mv memos/plan proposals/plan
$
和cp
命令一样,如果源文件和目标文件同名,那么只需要给出目标目录即可,因此还有一种更简单的实现方式:
$ mv memos/plan proposals
$
另外也可以像cp
命令那样把多个文件一块移动到其他目录中,只需要把待移动的文件放在目标目录之前就可以了:
$ pwd
/users/steve/programs
$ mv wb collect mon ../misc
$
这可以将文件wb
、collect
和mon
移动到目录misc
中。
你也可以使用mv
命令来更改目录名。下面的命令可以将目录programs
更名为bin
:
$ mv programs bin
$
到目前为止,我们在讨论文件管理相关话题的时候都是假设无论在文件系统中的任何位置,特定的一组数据有且只有一个对应的文件名。UNIX实际上要更复杂一些,它可以给相同的一组数据赋予多个文件名。
为特定文件创建多个名字的命令是ln
。
该命令的一般形式为:
ln from to
这样可以将文件from
链接到文件to
。
回想一下图1.8中steve
的programs
目录的结构。在这个目录中,有一个叫做wb
的程序。假设steve
还想以writeback
的名字调用该程序,显而易见的做法就是创建名为writeback
的wb
的副本:
$ cp wb writeback
$
这种方法的缺点在于需要占用两倍的磁盘空间。而且,如果steve
修改了wb
,他有可能会忘记对writeback
做出同样的修改,这样就会导致原以为相同的程序出现两个不一样的副本。这可就麻烦了,Steve!
通过将文件wb
链接到一个新的名字上,就可以避免这些问题:
$ ln wb writeback
$
现在就不再是一个文件的两个副本了,而是一个文件,两个不同的名字:wb
和writeback
。两者在逻辑上被UNIX系统链接在了一起。
就你所见,看起来似乎是拥有了两个不同的文件。执行ls
命令的话,会显示出两个独立的文件:
$ ls
collect
mon
wb
writeback
$
当使用ls -l
的时候,事情就变得有意思了:
$ ls -l
total 5
-rwxr-xr-x 1 steve DP3725 358 Jun 25 13:31 collect
-rwxr-xr-x 1 steve DP3725 1219 Jun 25 13:31 mon
-rwxr-xr-x 2 steve DP3725 89 Jun 25 13:30 wb
-rwxr-xr-x 2 steve DP3725 89 Jun 25 13:30 writeback
$
仔细看输出信息的第二列:collect
和mon
显示的是数字1,而wb
和writeback
显示的是数 2。这个数字表示的是文件的链接数,对于没有链接的非目录文件,这个数字通常是1。因为wb
和writeback
链接在了一起,所以这个数字是2(或者更准确地说,有两个名字的文件)。
你可以随时删除这两个链接文件中的某一个,另一个并不会随之消失:
$ rm writeback
$ ls -l
total 4
-rwxr-xr-x 1 steve DP3725 358 Jun 25 13:31 collect
-rwxr-xr-x 1 steve DP3725 1219 Jun 25 13:31 mon
-rwxr-xr-x 1 steve DP3725 89 Jun 25 13:30 wb
$
注意,wb
的链接数从2变成了1,这是因为其中一个链接已经被删除了。
ln
命令在大多数时候都是用来使某个文件同时出现在多个目录中。举例来说,假设pat想要访问steve的wb
程序。这并不需要为pat
制作一份该程序的副本(会导致先前提到过的数据同步问题),也不用将steve
的programs
目录纳入pat
的PATH
环境变量中(这样做存在安全风险,我们会在第10章讨论这个话题),只需简单地将该文件链接到自己的程序目录中就行了:
$ pwd
/users/pat/bin pat用来存放程序的目录
$ ls -l
total 4
-rwxr-xr-x 1 pat DP3822 1358 Jan 15 11:01 lcat
-rwxr-xr-x 1 pat DP3822 504 Apr 21 18:30 xtr
$ ln /users/steve/wb . 将wb链接到pat的bin目录
$ ls -l
total 5
-rwxr-xr-x 1 pat DP3822 1358 Jan 15 11:01 lcat
-rwxr-xr-x 2 steve DP3725 89 Jun 25 13:30 wb
-rwxr-xr-x 1 pat DP3822 504 Apr 21 18:30 xtr
$
注意,steve
仍然是文件wb
的属主,就算是查看pat
的目录内容也是如此。这不是没道理的,因为该文件的确只有一份,其属主就是steve
。
在链接文件的过程中,唯一的要求就是:对于普通的链接,被链接的文件必须与链接文件处在同一个文件系统中。如果不是这样的话,ln
命令在进行链接的时候会报错(可以使用df
命令来确定系统中都有哪些不同的文件系统。输出的每一行的第一个字段就是文件系统的名称)。
要想在不同文件系统(或是不同的网络互联系统)的文件之间创建链接,可以使用ln
命令的-s
选项。这样创建出来的叫做符号链接。符号链接用起来和普通链接差不多,除了符号链接指向的是原始文件。因此,如果原始文件被删除的话,符号链接就无效了。
让我们来看看在上个例子中使用符号链接的话会怎样:
$ rm wb
$ ls -l
total 4
-rwxr-xr-x 1 pat DP3822 1358 Jan 15 11:01 lcat
-rwxr-xr-x 1 pat DP3822 504 Apr 21 18:30 xtr
$ ln -s /users/steve/wb ./symwb wb的符号链接
$ ls -l
total 5
-rwxr-xr-x 1 pat DP3822 1358 Jan 15 11:01 lcat
lrwxr-xr-x 1 pat DP3822 15 Jul 20 15:22 symwb -> /users/steve/wb
-rwxr-xr-x 1 pat DP3822 504 Apr 21 18:30 xtr
$
注意,文件symwb
的属主是pat
,文件类型是ls
输出的第一个字符,也就是l,这表明该文件是一个符号链接。这个符号链接的大小是15(文件内容其实就是字符串/users/steve/wb
),但如果我们访问文件内容的话,看到的会是所链接到的那个文件的内容,也就是/users/steve/wb
:
$ wc symwb
5 9 89 symwb
$
可以使用ls
命令的-L
和-l
选项获得符号链接所指向文件的详细信息:
$ ls -Ll
total 5
-rwxr-xr-x 1 pat DP3822 1358 Jan 15 11:01 lcat
-rwxr-xr-x 2 steve DP3725 89 Jun 25 13:30 wb
-rwxr-xr-x 1 pat DP3822 504 Apr 21 18:30 xtr
$
删除符号链接所指向的文件会使得符号链接失效(因为符号链接是通过文件名来维护的),但符号链接本身不会被删除:
$ rm /users/steve/wb 假设pat能够删除该文件
$ ls -l
total 5
-rwxr-xr-x 1 pat DP3822 1358 Jan 15 11:01 lcat
lrwxr-xr-x 1 pat DP3822 15 Jul 20 15:22 wb -> /users/steve/wb
-rwxr-xr-x 1 pat DP3822 504 Apr 21 18:30 xtr
$ wc wb
Cannot open wb: No such file or directory
$
这种类型的文件叫做悬挂符号链接(dangling symbolic link),应该将其删除,除非你有什么特别的理由保留这类文件(例如,你打算替换被删除的文件)。
最后要留意的一件事:ln
命令采用的格式和cp
及mv
一样,这意味着你可以为特定目标目录中的多个文件创建链接。
ln files directory
rmdir
命令可以用来删除目录。如果指定的目录中包含任何文件和子目录,rmdir
不会继续进行处理,这样就避免了误删文件的可能。
要删除目录/users/pat
,可以这样做:
$ rmdir /users/pat
rmdir: pat: Directory not empty
$
操作错误!让我们来删除之前创建的misc
目录:
$ rmdir /users/steve/misc
$
还是老样子,上面的命令只有在misc
目录中不包含文件或其他子目录的时候才执行,否则的话,还是会出现和刚才相同的错误:
$ rmdir /users/steve/misc
rmdir: /users/steve/misc: Directory not empty
$
如果你还是想删除misc
目录,那么在重新使用rmdir
命令之前必须删除目录中包含的所有文件。
还有另外一种删除目录及其内容的方法:使用rm
命令的-r
选项。命令格式很简单:
rm -r dir
dir
是要删除的目录名称。rm
命令会删除指定的目录以及其中的所有文件(包括目录),因此在使用这条强力命令的时候可得小心。
想试试全速操作?-f
选项能够强制执行操作,不再逐条命令发出提示。如果粗心大意的话,这会把你的系统彻底搞砸,因此很多管理员干脆根本不用rm -rf
!
在UNIX系统中,Shell拥有一个强大的特性:文件名替换。假设你的当前目录下有以下文件:
$ ls
chaptl
chapt2
chapt3
chapt4
$
如果你想同时显示这些文件的内容的话,很简单:cat
命令能够显示出在命令行中所指定的多个文件的内容。就像这样:
$ cat chaptl chapt2 chapt3 chapt4
...
$
但是这种方法太麻烦了。你可以借助于文件名替换,只需要简单地输入:
$ cat *
...
$
Shell会自动将模式 *
替换成当前目录下能够匹配到的所有文件名。如果你在其他命令中使用 *
,相同的替换过程一样会发生。那么echo
命令呢?
$ echo *
chaptl chapt2 chapt3 chapt4
$
在这里,*
又一次被替换成当前目录中的所有文件名,然后用echo
命令显示出了这些文件名。
命令行中只要是 *
出现的地方,Shell都会进行替换:
$ echo * : *
chaptl chapt2 chapt3 chapt4 : chaptl chapt2 chapt3 chapt4
$
*
能够实现部分文件替换功能,它实际上还可以与其他字符配合使用,以限制所能够匹配到的文件名范围。
举例来说,假设在当前目录下不仅有chapt1
~chapt4
这些文件,还包括文件a
、b
和c
:
$ ls
a
b
c
chaptl
chapt2
chapt3
chapt4
$
要想只显示出以chap
开头的文件,可以输入:
$ cat chap*
.
.
.
$
chap *
能够匹配以chap
开头的所有文件。在指定的命令被调用之前,这些文件名替换就已经完成了。
*
并不仅限于放在文件名尾部,它还可以出现在开头或中间的位置:
$ echo *t1
chaptl
$ echo *t*
chaptl chapt2 chapt3 chapt4
$ echo *x
*x
$
在第一个echo
中,*t1
指定了所有以字符t1
作为结尾的文件名。在第二个echo
中,首个*
能够匹配t
字符之前的任意多个字符,另一个*
匹配t
之后的任意多个字符,因此,只要包含t
的文件名,就会被打印出来。因为没有以x
作为结尾的文件名,所以最后一个例子中并没有发生替换,echo
命令也就只是显示出了*x
。
星号(*
)能够匹配零个或多个字符,也就是说,x*
能够匹配文件x
,也能够匹配x1
、x2
、xabc
等。问号(?
)仅能够匹配单个字符。因此cat ?
能够显示出所有文件名中只有单个字符的文件,而cat x?
则会显示出文件名长度为两个字符且第一个字符是x
的所有文件。我们再一次用echo
来演示这种匹配行为:
$ ls
a
aa
aax
alice
b
bb
c
cc
report1
report2
report3
$ echo ?
a b c
$ echo a?
aa
$ echo ??
aa bb cc
$ echo ??*
aa aax alice bb cc report1 report2 report3
$
在上面的例子中,??
匹配两个字符,*
匹配余下的零个或多个字符,其效果就是找出所有文件名长度至少为两个字符的文件。
另一种匹配单个字符的方法是在中括号[]
中给出待匹配的字符列表。例如,[abc]
能够匹配字符a
、b
或c
。这类似于?
,但是允许你选择具体要匹配哪些字符。
你可以使用破折号指定一个字符的逻辑范围,这可是太方便了!例如,[0-9]
能够匹配字符0~9。在指定字符范围的时候,唯一的限制就是第一个字符在字母表上必须位于最后一个字符之前,因此[z-f]
并不是一个有效的字符范围,而[f-z]
就没有问题。
可以通过配合使用字符范围以及字符列表来实现复杂的替换。例如,[a–np–z]*
能够匹配以字母a
~n
或者p
~z
开头的所有文件(或者说得再简单些,就是不以小写字母o
开头的文件)。
如果[
之后的第一个字符是!
,那么所匹配的内容正好相反。也就是说,匹配中括号内容之外的任意字符。因此:
[!a–z]
能够匹配小写字母以外的任意字符,另外:
*[!o]
能够匹配不以小写字母o
结尾的那些文件。
表1.1给出了文件名替换的另外一些例子。
表1.1 文件名替换示例
命令 |
描述 |
---|---|
|
打印出以 |
|
打印出以 |
|
删除包含点号的所有文件 |
|
列出以 |
|
删除当前目录下的所有文件(执行该命令的时候务必小心) |
|
打印出以 |
|
将 |
|
列出以小写字母开头且不以数字结尾的所有文件 |
我们当前对于命令行和文件名的讨论并不完整,因为尚未谈及UNIX老用户的困扰以及Linux、Windows及Mac用户天天都要碰到的东西:文件名中的空格。
因为Shell使用空格作为单词之间的分隔符,这样一来,问题就出现了。也就是说,echo hi mom
会被解析成调用echo
命令,命令参数为hi
和mom
。
现在想象你有一个叫做my test document
的文件。你该如何在命令行中引用该文件?该如何使用cat
命令查看或显示?
$ cat my test document
cat: my: No such file or directory
cat: test: No such file or directory
cat: document: No such file or directory
这样子肯定不行。为什么?因为cat
需要指定文件名,而在这里它看到的不是一个文件名,而是3个:my
、test
和document
。
有两种标准的解决方法:使用反斜杠将所有的空格进行转义,或者将整个文件名放在引号中,让Shell知道这是一个包含了空格的单词,并非多个单词。
$ cat "my test document"
This is a test document and is full
of scintillating information to edify
and amaze.
$ cat my\ test\ document
This is a test document and is full
of scintillating information to edify
and amaze.
这样就没问题了,了解这一点非常重要,因为你所打交道的文件系统中很可能有很多包含了空格的目录和文件名称。
尽管空格可能是文件名中最麻烦、也是最烦人的特殊字符,但偶尔你会发现还有其他一些字符也能把你在命令行上的工作搞砸。
例如,你该如何处理包含问号的文件名?在下一节中,你会知道字符“?”
对于Shell具有特殊含义。大多数的现代Shell都能够聪明地避免歧义,但仍需要引用文件名或使用反斜杠指明文件名中的特殊字符:
$ ls -l who\ me\?
-rw-r--r-- 1 taylor staff 0 Dec 4 10:18 who me?
当文件名中包含反斜杠和问号的时候,事情才变得真正有意思起来,这种情况是不可避免的,尤其是对于那些由Liunx或Mac系统中的图像化工具所创建的文件。有什么技巧吗?将包含双引号的文件名放到单引号中,反之亦然。就像这样:
$ ls -l "don't quote me" 'She said "yes"'
-rw-r--r-- 1 taylor staff 0 Dec 4 10:18 don't quote me
-rw-r--r-- 1 taylor staff 0 Dec 4 10:19 She said "yes"
这个话题我们随后还会谈及,但对于那些由包含空格或其他非标准字符的目录或文件所引发的问题,你现在已经知道了该如何避免。
大多数UNIX系统命令都是从屏幕中获得输入,然后将输出结果发送回屏幕。在UNIX行话中,屏幕通常叫做终端,这一称谓可以追溯到计算机时代的初期。如今,它更多指的是运行在图形化环境(Linux窗口管理器、Windows系统或Mac系统)中的终端程序。
命令通常从标准输入(默认是计算机键盘)中读取输入。这是一种表明键入信息的不错方法。与此类似,命令通常将输出写入标准输出,这可以是终端或者终端程序(默认)。图1.9中描述了这种概念。
图1.9 典型的UNIX命令
举一个例子,回忆一下,在执行who
命令的时候会显示出当前已登录的所有用户。说得更正式一些,who
命令会将已登录的用户列表写入标准输出,如图1.10所示。
图1.10 who
命令
UNIX命令可以使用文件或上一条命令的输出作为其输入,也可以将其输出发送给另一条命令或其他程序。这个概念极其重要,它不仅有助于理解命令行的威力,另外还解释了为什么即便在有图形化界面可用的情况下还要去记忆各种命令。
不过在这之前,先考虑一种情况:如果在调用sort
命令的时候没有指定文件名参数,那么该命令会从标准输入中获得命令输入。和标准输出一样,默认对应的是终端(或键盘)。
如果采用这种方式输入,必须在完成最后一行输入后指定文件结尾序列(end-of-file sequence),按照UNIX的惯例,这指的是Ctrl+d,即同时按下Control键(视所使用的键盘不同,也可以是Ctrl键)和d键所产生的序列。
让我们使用sort命令对下面4个名字进行排序:Tony、Barbara、Harry和Dirk。用不着先把名字放到文件中,我们可以直接在终端中输入:
$ sort
Tony
Barbara
Harry
Dirk
Ctrl+d
Barbara
Dirk
Harry
Tony
$
因为没有在sort
命令指定文件名,所以就直接从标准输入(终端)中获得输入了。键入第4个名字之后,按下Ctrl键和d键表明数据输入完毕。这时,sort
命令就会对这4个名字进行排序,将排序结果显示在标准输出设备(也就是终端)上,如图1.11所示。
图1.11 sort
命令
wc
命令是能够从标准输入中获得输入的另一个例子(如果没有在命令行中指定文件名)。在下面的例子中统计了从终端中所输入文本的行数:
$ wc -l
This is text that
is typed on the
standard input device.
Ctrl+d
3
$
注意,wc
命令不会将用于结束输入的Ctrl+d
视为单独的一行,因为这并不是由它处理的,而是Shell负责解释。由于指定了wc
命令的-l
选项,因此该命令只输出了总行数(3)。
很容易就可以将发送到标准输出的命令输出转移到文件中。这种能力叫做输出重定向,也是理解UNIX强大功能必不可少的一环。
如果将>
file
放置在能够将输出写入到标准输出上的命令之后,那么该命令的输出就会被写入到文件file
中:
$ who > users
$
上面的命令使得who的输出结果被写入到文件users中。注意,这不会有任何输出内容出现,因为输出已经从默认的标准输出设备(终端)重定向到了指定的文件中。我们不妨验证一下:
$ cat users
oko tty01 Sep 12 07:30
ai tty15 Sep 12 13:32
ruth tty21 Sep 12 10:10
pat tty24 Sep 12 13:07
steve tty25 Sep 12 13:03
$
如果命令的输出被重定向到某个文件,而这个文件中之前已经有内容存在,那么这些已有内容会被重写。
$ echo line 1 > users
$ cat users
line 1
$
现在考虑下面的例子,别忘了users
中包含着之前who
命令的输出:
$ echo line 2 >> users
$ cat users
line 1
line 2
$
如果你留心的话,会注意到本例中的echo
命令使用了由字符>>
表示的另一种类型的输出重定向。这组字符使得命令的标准输出内容被追加到指定文件的现有内容之后。文件中先前的内容并不会丢失,新的输出只不过被添加到了尾部而已。
借助于重定向符号>>
,你可以使用cat
将一个文件的内容追加到另一个文件之后:
$ cat file1
This is in file1.
$ cat file2
This is in file2.
$ cat file1 >> file2 将file1的内容追加到file2之后
$ cat file2
This is in file2.
This is in file1.
$
之前说过,如果给cat
命令指定多个文件名,那么这些文件的内容会先后被显示出来。这意味着还有另外一种做法可以实现同样的效果:
$ cat file1
This is in file1.
$ cat file2
This is in file2.
$ cat file1 file2
This is in file1.
This is in file2.
$ cat file1 file2 > file3 使用重定向
$ cat file3
This is in filel.
This is in file2.
$
实际上,cat
命令的名字正是得自于此:当用于多个文件时,其效果就是将这些文件连接(concatenate)在一起。
正如命令的输出可以被重定向到文件中,文件也可以被重定向到命令的输入中。大于号>
作为输出重定向符号,而小于号<
则作为输入重定向符号。当然,只有那些从标准输入中接收输入的命令才能够使用这种方法将文件重定向到其输入中。
要想重定向输入,需要将作为输入内容的文件名放在<
之后。举例来说,命令wc -l users
可以统计出文件users
中的行数:
$ wc -l users
2 users
$
你也可以通过重定向wc
命令的标准输入来完成同样的任务:
$ wc -l < users
2
$
注意,wc
命令的这两种不同的形式所产生的输出并不一样。在前一个输出中,文件users
的名字是和文件行数一同出现的,而在后一个输出中,并没有出现文件名。
这一点反映出了两个命令在执行上的细微差异。在第一个例子中,wc
知道自己是从文件users
中读取输入。在第二个例子中,它只看到了通过标准输入传来的原始数据。Shell
将输入从终端重定向到了文件users
(下一章还有更多的相关讨论)。就wc
而言,因为它不知道自己的输入到底是来自终端还是文件,所以也就没办法输出文件名了!
之前创建的文件users
中包含了当前已登录系统的用户列表。因为每一行对应着一个已登录的用户,所以可以通过统计文件行数很容易知道有多少个登录会话:
$ who > users
$ wc -l < users
5
$
输出表明当前有5位用户已经登录,或者说有5个登录会话,其区别在于用户(尤其是管理员)经常多次登录。以后只要你想知道有多少用户登录,都可以使用上面的命令。
另一种确定登录用户的方法可以避免使用中间文件。之前提到过,UNIX能够将两个命令“连接”在一起。这种连接叫做管道,它可以将一个命令的输出直接作为另一个命令的输入。管道使用字符|表示,被放置在两个命令之间。要想在who
和wc -l
之间创建一个管道,可以输入who | wc -l
:
$ who | wc -l 5 $
创建出的管道如图1.12所示。
图1.12 管道:who | wc -l
在命令之间建立好管道之后,第一个命令的标准输出就被直接连接到第二个命令的标准输入。who
命令会将已登录用户的列表写入标准输出。而且,如果没有为wc
命令指定文件名参数的话,该命令会从标准输入中接收输入。因此,作为who
命令输出的已登录用户列表就自动成为了wc
命令的输入。要注意的是,在终端上是绝对看不到who
命令的输出的,因为它直接通过管道进入了wc
命令。管道的处理过程如图1.13所示。
图1.13 管道的处理过程
管道可以在任意两个程序之间创建,前提是第一个程序会将输出写入到标准输出,第二个程序会从标准输入中读取输入。
再看另一个例子,假设你想统计目录中的文件个数。考虑到ls
命令输出的每行都对应一个文件,这样就可以借用之前的方法:
$ ls | wc -l
10
$
输出表明当前目录中包含了10个文件。
也可以创建出由不止两个程序所组成的更为复杂的管道,一个程序的输出依次作为下一个命令的输入。在你成长为命令行“老手”的路上,你会发现很多展现了管道强大威力的地方。
在UNIX术语中,过滤器常指的是这样的程序:可以从标准输入中接收输入,对输入数据进行处理,然后将结果写入标准输出。说得再简洁些,过滤器就是能够用来在管道中修改其他程序输出的程序。因此,在上个例子的管道里,wc
就是过滤器。因为ls
并没有从标准输入中读取输入,所以并不是过滤器。另外,如cat
和sort
都可以作为过滤器,而who
、date
、cd
、pwd
、echo
、rm
、mv
及cp
就算不上了。
除了标准输入和标准输出,还有第3种虚拟设备:标准错误。绝大多数UNIX命令会将其错误信息写入到这里。和其他两个“标准”位置一样,标准错误默认是同终端或终端应用程序相关联的。在绝大多数情况下,你无法分辨标准输出和标准错误之间的差别:
$ ls n* 列出所有以n开头的文件
n* not found
$
这里的“not found”消息实际上就是由ls
命令写入到标准错误的。你可以通过重定向ls
命令的输出来验证该消息的确没有输出到标准输出:
$ ls n* > foo
n* not found
$
你可以看到,即便是做了标准输出重定向,这条消息依然出现在了终端,并没有被添加到文件foo
中。
上面的例子揭示了标准错误存在的原因:即便是标准输出被重定向到了文件中或通过管道导向了其他命令,错误消息依然能够显示在终端中。
你也可以使用一种略微复杂的形式将标准错误重定向到文件中(假如你想在长期的操作过程中记录程序可能出现的错误):
command 2> file
注意,2
和>
之间可没有空格。所有正常情况下应该进入标准错误的错误信息都会被转入file
所指定的文件中,类似于标准输出重定向。
$ ls n* 2> errors
$ cat errors
n* not found
$
如果想在一行中输入多个命令,只需要使用分号作为命令之间的分隔符就行了。举例来说,你可以在同一行中输入date
和pwd
命令来显示出当前时间及当前工作目录:
$ date; pwd
Sat Jul 20 14:43:25 EDT 2002
/users/pat/bin
$
你可以在一行中写入任意数量的命令,只要每个命令之间使用分号分隔就可以了。
在正常情况下,输入命令后需要等待命令结果显示在终端中。就目前碰到的所有例子而言,等待的时间都很短,连一秒钟都不到。
但有时候,你需要运行的多个命令得花上几分钟甚至更长的时间才能结束。在这种情况下,除非你将命令放入后台执行,否则在继续往下处理之前,你只能等着这些命令执行完毕。
结果看起来就好像UNIX或Linux系统将注意力完全放在了当前的操作上,但这些系统实际上具备多任务能力,可以同时运行多个命令。如果你用的是Ubuntu系统,其中的窗口管理器、时钟、状态监视器及终端窗口都是同时在运行的。你同样可以在命令行上同时运行多个命令。这就是将命令“放入后台”的含义,让你可以同其他任务一同工作。
将命令或命令序列放入后台运行的写法是在命令尾部加上字符&
。这表示该命令不再和终端绑定在一起,你可以继续其他工作。后台命令的标准输出仍会被导向终端,不过在大多数情况下,标准输入不会再和终端相关联。如果命令试图从标准输入中读取,它将停止运行,等待被带回前台(我们会在第14章“交互式与非标准Shell特性”中对此详述)。
下面是一个例子:
$ sort bigdata > out & 将sort命令放入后台
[1] 1258 进程id
$ date 终端随即就可以供其他工作使用了
Sat Jul 20 14:45:09 EDT 2002
$
当命令被放入后台后,UNIX系统会自动显示出两个数字。第一个是命令的作业号(job number),第二个是进程ID(process ID),也称为PID。在刚才的例子中,作业号是1,进程ID是1258。作业号可以供某些Shell命令作为一种引用特定后台作业的便捷方式(你会在第14章学到更多的相关内容)。进程ID唯一地标识了后台命令,可用于获取该命令的状态信息。这些信息可以通过ps
命令来得到。
ps
命令能够给出系统中所运行进程的信息。如果不使用任何选项的话,该命令只会打印出你所拥有的进程状态。在终端中输入ps
,会得到几行描述运行进程的信息:
$ ps
PID TTY TIME CMD
13463 pts/16 00:00:09 bash
19880 pts/16 00:00:00 ps
$
ps
命令会打印出4列信息(视系统而定):PID
(进程ID);TTY
(进程所在的终端号);TIME
(以分秒计算的进程所使用的计算机时间);CMD(进程名称)。(上例中的bash进程是登录时所启动的Shell,它使用了9秒钟的计算机时间。)在该命令结束之前,它在输出中都显示为一个运行的进程,因此上例中的进程19880就是ps
命令本身。
如果配合-f
选项,ps
会打印出更多的进程信息,包括父进程ID(PPID
)、进程开始时间(STIME
)及其他命令参数:
$ ps -f
UID PID PPID C STIME TTY TIME CMD
Steve 13463 13355 0 12:12 pts/16 00:00:09 bash
Steve 19884 13463 0 13:39 pts/16 00:00:00 ps -f
$
表1.2总结了本章介绍过的命令。在表1.2中,file
指的是单个文件,file(s)
指的是一个或多个文件,dir
指的是单个目录,dir(s)
指的是一个或多个目录。
表1.2 命令总结
命令 |
描述 |
---|---|
|
显示一个或多个文件的内容,如果没有提供参数的话,则显示标准输入内容 |
|
将当前工作目录更改为 |
|
将 |
|
将一个或多个文件复制到 |
|
显示日期和时间 |
|
显示给出的一个或多个参数 |
|
将 |
|
将一个或多个文件链接到 |
|
列出一个或多个文件 |
|
列出一个或多个目录中的文件,如果没有指定目录,则列出当前目录中的文件 |
|
创建一个或多个指定的目录 |
|
移动 |
|
将一个或多个文件移动到指定目录 |
|
列出活动进程的信息 |
|
显示出当前工作目录的路径 |
|
删除一个或多个文件 |
|
删除一个或多个空目录 |
|
将一个或多个文件中的行进行排序,如果没有指定文件,则对标准输入内容进行排序 |
|
统计一个或多个文件中的行数、单词数和字符数,如果没有指定文件的话,则统计标准输入中的内容 |
|
显示出已登录的用户 |
在本章中,你将学习到什么是UNIX的Shell,Shell能够做什么,以及为什么说它是每个高级用户工具箱中不可或缺的一部分。
UNIX系统在逻辑上被划分为两个不同的部分:内核和实用工具(Utility),如图2.1所示。或者你也可以认为是内核和其他部分,通常来说,所有的访问都要经由Shell。
图2.1 UNIX系统
内核是UNIX系统的核心所在,当打开计算机并启动(booted)之后,内核就位于计算机的内存中,直到关机为止。
组成完整的UNIX系统的各种实用工具位于计算机磁盘中,在需要的时候会被加载到内存中并执行。实际上你所知道的所有UNIX命令都是实用工具,因此这些命令所对应的程序也都在磁盘上,仅在需要时才会被载入内存。举例来说,当你执行date
命令时,UNIX系统会将名为date
的程序从磁盘上载入到内存中,读取其代码来执行特定的操作。
Shell也是一个实用工具程序,它作为登录过程的一部分被载入到内存中执行。实际上,有必要了解当终端或终端窗口中的第一个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登录
$
当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 登录周期
现在你知道了Shell会分析(用计算机行话来说,就是解析)输入的每一行命令,然后执行指定的程序。在解析期间,文件名中的特殊字符(如*
)会被扩展,就像第一章讲到的那样。
Shell还有其他的职责,如图2.8所示。
图2.8 Shell的职责
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还有点事需要处理,因此,让我们先来讨论一下这方面的内容。
和比较正式的编程语言一样,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
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
指定。
要理解两条命令在执行上的不同,这一点非常重要。如果还不太清楚,那么在继续阅读之前复习一下上面的内容。
Shell在扫描命令行时,除了重定向符号之外还会查找管道字符|。每找到一个,就会将之前命令的标准输出连接到之后命令的标准输入,然后执行这两个命令。
如果你输入
who | wc -l
Shell会查找分隔了命令who
和wc
的管道符号。它将上一个命令的标准输出连接到下一个命令的标准输入,然后执行两者。who
命令执行时会生成已登录用户列表并将结果写入标准输出,它并不知道输出内容并没有出现在终端而是进入了另一个命令。
当wc
命令执行时,它发现并没有指定文件名,因此就对标准输入内容进行统计,并没有意识到标准输入并非来自终端,而是来自于who
命令的输出。
随着本书内容的深入,你会看到管道中并不仅限于有两条命令,你可以在复杂的管道中将3条、4条、5条甚至更多的命令串联在一起。这多少有点不好理解,但却是UNIX系统强大威力的所在。
Shell提供了一些能够定制个人环境的命令。个人环境包括主目录、命令行提示符以及用于搜索待执行程序的目录列表。我们会在第10章中对此展开详述。
Shell有自己内建的编程语言。这种语言是解释型的,也就是说,Shell会分析所遇到的每一条语句,然后执行所发现的有效的命令。这与C++及Swift这类编程语言不同,在这些语言中,程序语句在执行之前通常会被编译成可由机器执行的形式。
相较于编译型语言,由解释型语言所编写的程序一般要更易于调试和修改。然而,所花费的时间要比实现相同功能的编译型语言程序更长。
Shell编程语言提供了可在大多数其他编程语言中找到的其他特性。它有循环结构、决策语句、变量、函数,而且是面向过程的。基于IEEE POSIX标准的现代Shell还有许多其他特性,包括数组、数据类型和内置的算术运算。