Unix指令集详解

UNIX 新手指南,第1部分: 文件维护工具
系统管理员可以在命令行中使用各种程序来维护 UNIX? 系统中的文件。在本教程中,您将试验使用一些命令,如 cd、cp 和 tar,以便在命令行中导航 UNIX 文件系统并处理文件和目录。cd 命令用来更改目录,cp 命令用来复制文件或目录,而 tar 可以快速地对文件进行存档。您还将了解如何处理文件权限并执行简单的输入/输出。
1.	开始之前
了解本教程中包含的内容以及如何最好地利用本教程。
1.1.	关于本系列
这个由四部分组成的文章系列将从用户的角度出发,介绍 UNIX? 的基本知识。对于很久没有使用类 UNIX 操作系统的用户来说,这个初始的教程是个很好的温习。对于具有 Windows? 使用经验的新的 UNIX 用户来说,它也很有价值,因为其中引用了 Windows,并将两者进行了比较。本系列中后续的教程将详细介绍一些特定的应用程序(例如,vi),并讨论 Shell 的诀窍与技巧。
1.2.	关于本教程
系统管理员可以在命令行中使用各种程序来维护 UNIX 系统中的文件。除了这里演示的基本程序外,还有更多的程序可供使用,但这里介绍的程序都是 UNIX 系统中的基本组成部分。您可以使用mv命令对文件和目录进行移动和重命名。cp 命令可以用来复制一个或多个文件或目录。有一个称为 tar 的应用程序,它可以快速地将文件聚集在一起形成一个存档。本教程还将讨论如何遍历文件系统、处理文件权限和简单的输入/输出,以便为新的 UNIX 用户顺利地使用系统而打下基础。
1.3.	目标
本教程的目标是让新的 UNIX 用户能够熟练地使用命令行遍历系统和处理文件。它重点关注于用来操作文件的一些常用命令行实用程序,但同时也讨论了关于文件权限和输入/输出的内容,以便向您完整地介绍日常工作中需要使用的命令。
1.4.	先决条件
要学习本教程,您需要对计算机和文件有基本的认识,但并不一定需要具备使用类 UNIX 操作系统的经验。您应该知道什么是目录(文件夹)和文件,并能够使用您的帐户登录到类 UNIX 操作系统中。如果您对 DOS 或 Microsoft? Windows 命令行比较熟悉,那么您将发现其中一些相似之处,但对于从未使用过这些命令行的用户来说,也可以顺利地学习本教程。 
1.5.	系统要求
在任何运行类 UNIX 操作系统的计算机上拥有您自己的帐户,这是完成本教程所需的要求。类 UNIX 操作系统包括 IBM AIX? 操作系统、Linux?、Berkeley Software Distribution (BSD) 和 Mac OS? X(通过终端来访问命令行),以及其他一些系统。
	在开始学习之前,有一点需要注意:目前可以获得许多不同的类 UNIX 分发版,从商业分发版(如 AIX)到免费的分发版(如 BSD 和 Linux)。本教程重点关注于所有分发版中都提供的一些命令和命令行选项,如果您发现与自己的分发版相比存在差异,那么请查看您的 man 页面(稍后将会进行描述)。
	请根据您的选择,登录到相应的类 UNIX 操作系统,以便开始学习。在登录之后,您应该自动地开始于您的用户 home 目录中。本教程中的示例使用 tuser(测试用户)用户名。 
	man
在开始使用特定的命令之前,需要了解关于 man 的内容。man 表示手册 (manual),对于那些自信能够做到自给自足的 UNIX 用户来说,它是非常关键的工具。
输入man application-name可以查看关于需要了解的内容的解释。在本教程的每个页面中,我们鼓励您在执行其中指令的同时,查看相应的 man 页面。
尝试输入下面的命令(请注意,$ 在本教程中表示命令提示符,在本教程的示例中,您应该在美元符号后面输入您所看到的内容):  
$ man ls
2.	目录
您需要了解的第一件事情是,如何遍历和查看文件系统。在 UNIX 中,目录用来以一种层次结构对文件进行组织。您可以在命令行中使用一系列的命令和列表来查看和遍历 UNIX 文件系统,而不要单击目录进入其中并且用图标来表示每个文件。
2.1.	ls
如果您是第一次使用类 UNIX 操作系统,但是曾经使用过 DOS 或 Windows 命令行,那么 ls 基本上相当于 dir。它是列出目录 (List Directory) 的缩写。ls 可以与各种命令行选项一同使用,以便获得详细的列表、显示隐藏的文件、递归列出子目录,等等。请输入下面的示例:  
$ ls 
$ ls -l 
$ ls -a 
$ ls -R 
如果您正在命令行中考虑下一步应该进行什么操作,那么可以执行 ls,它可以帮助您了解现在所处的位置以及正在进行的操作。可以把它看作图形用户界面中的屏幕刷新操作,用来让您了解系统的当前状态。 
2.2.	cd
要遍历文件系统,可以使用 cd 来更改目录。输入 cd,然后输入您希望进入的目录的名称。如果在开头使用了 /,那么这个目录的名称是绝对 目录路径:它从文件系统的根开始。如果在开头没有使用 /,那么该目录是相对 路径:从您的当前工作 目录开始。例如,如果您位于自己的 home 目录 (/home/tuser),并且希望移动到某个子目录,可以在 cd 的后面输入希望进入的目录名称。例如,如果有一个 Documents 目录,那么您可以输入下面的命令:  
$ cd Documents
因为您开始于 /home/tuser/,所以该命令将使您进入 /home/tuser/Documents/。
与此相反,您可以使用绝对路径名来指定一个位置。例如,如下所示,您可以移动到 /tmp,然后返回到您的 home 目录:  
$ cd /tmp 
$ cd /home/tuser
2.3.	特殊目录名称
在 UNIX 中,使用一些特殊的目录名称可以使得对文件系统的遍历变得简单。三个最重要的特殊名称分别指向当前目录、当前目录的父目录和用户的 home 目录。当前目录可以由点号指定。例如,如果您输入 $ cd .,那么您依然位于当前目录中,即 /home/tuser/。当您运行当前工作目录中的可执行文件时,该字符变得特别重要。在缺省情况下,许多 UNIX Shell 会在应用程序目录中搜索应用程序,但不会搜索当前工作目录。通常可以在文件名前面使用 ./,以显式地引用当前工作目录中的文件和应用程序。父目录由两个点号来引用。要遍历到您的当前工作目录的父目录,可以输入下面的命令:  
$ cd .. 
如果您以前位于 /home/tuser,那么现在就在 /home 中。为了演示第三个特殊目录名称,可以使用快捷方法返回您的 home 目录(波浪符号)。可以输入下面的命令:  
$ cd ~ 
2.4.	pwd
要查看当前目录,您可以使用 pwd,它表示打印工作目录 (Print Working Directory)。它可以告诉您现在位于文件系统中的何处,这将帮助您在指定相对路径名称时确定应该使用什么样的名称。请尝试使用这三种特殊目录名称、绝对路径和相对路径,以遍历您的文件系统。在每一步骤中,可以使用 pwd 查看当前位置。 
查看当前目录(如果您执行了前面部分中的操作步骤,那么现在应该位于您的 home 目录):  
$ pwd 
2.5.	mkdir、rmdir
mkdir 和 rmdir 分别用来创建和删除目录。rmdir 仅当该目录为空有效(它并不删除文件)。
尝试使用下面的命令:  
$ mkdir TUTORIAL 
$ cd TUTORIAL 
$ pwd 
$ ls
您现在位于刚刚创建的 TUTORIAL 目录,而且该目录为空。 
您还可以使用 ~ 波浪号字符到达该目录。要进入 /home/tuser/TUTORIAL,可以输入: $ cd ~/TUTORIAL 
$ pwd
2.6.	目录布局
您已经了解了如何在目录中移动并获取清单,现在可以开始研究典型的 UNIX 分发版中的目录布局了。您可以通过几种不同的方式来组织 UNIX 文件系统。本教程讨论了一些在大多数类 UNIX 分发版中常见的根级目录。当然还有其他一些重要的根级目录,但下面这些是最常见的: /home (or /users)
/etc
/bin
/sbin
/usr
/car
/tmp
/home 用来存放用户目录。例如,tuser 用户位于 /home/tuser。 
/etc 是用来存储系统范围设置的目录,包括启动脚本和网络配置文件。该目录中的大多数文件都需要使用 root 用户进行编辑。
/bin 和 /sbin 是用来存储系统可执行文件(比如在本教程中学习的命令)的目录。/sbin 用于存放系统命令,如 shutdown,而 /bin 用于存放用户命令。 
应用程序通常安装在 /usr 中。/usr/local/ 中的子目录通常用来包含一些安装的应用程序,而这些应用程序并不属于基本分发版中的一部分。 
/var 是用来存放类似日志文件这样的内容的目录,需要不断地对其进行更新和存储。 
临时文件存储在 /tmp 中。系统中所有的用户都可以对该目录进行写入,并且在一些系统中,会周期性地删除其中的旧文件。
3.	文件
既然您已经知道了如何使用命令行在文件系统中移动,那么现在可以开始对文件进行处理了。本部分内容将介绍如何创建示例文件、复制文件、删除文件,以及查看和更改基本的文件权限。在像 UNIX 这样的多用户操作系统中,必须了解所有权和权限的概念。 
3.1.	Touch
首先,创建一个空文件,您将在本教程中使用到该文件。touch 命令可以用来创建一个空文件(对文件使用 touch 命令,通常用来更新文件的修改日期和访问日期)。 
返回到用户 home 目录中的 TUTORIAL 目录,并输入下面的命令创建一个文件: $ cd ~/TUTORIAL
$ touch example.txt
3.2.	cp
cp 命令用来复制文件。输入 cp 加上需要复制的文件的名称,再加上需要将该文件复制到的目录(您还可以使用相应的选项来指定新文件的名称)。例如,尝试将 example.txt 文件复制到 /tmp/: $ cp example.txt /tmp/
$ ls /tmp/
您应该可以在 /tmp/ 中看到 example.txt。现在,将 /tmp/ 中的文件复制回您的当前目录,但是为它取一个新的名称: $ cp /tmp/example.txt ./example2.txt
$ ls
请注意,这里使用一个点号来指定需要将这个新文件放到您的当前目录。在本示例中,不需要包括 ./(因为复制操作的缺省路径就是您的当前工作目录),但是它可以清楚地说明您要完成的任务。后面的 ls 命令则显示了在当前工作目录中有两个示例文件。 
3.3.	mv
移动(move)文件可以使用 mv 命令来完成。对于移动和复制操作来说,大多数语法和命令行选项都是相同的。如果您希望将新文件 example2.txt 移出当前目录并放入到 /tmp/,那么可以输入下面的命令: $ mv example2.txt /tmp/.
请注意,这里再次使用了点号显式地说明您正在进行的操作。 
3.4.	rm
要对系统进行清理,可以删除在 /tmp/ 中创建的文件。rm 命令用来从文件系统中删除文件。这与将文件移动到回收站或垃圾箱不同,该命令删除了文件指针,所以在使用 rm 命令时应当格外小心。输入以下命令: 
$ rm /tmp/example.txt
$ rm /tmp/example2.txt
$ ls /tmp/
/tmp/ 中的那两个示例文件都应该消失了。 
根据您所使用的类 UNIX 操作系统的不同,您还可以使用其他的删除命令,如 srm 或 can。尝试输入 man srm 和 man can 以查看是否存在这些命令。srm 用作安全版本的 rm,它对删除的文件使用随机数据进行覆盖,以防止对其进行恢复。can 在一些方法上与 srm 相反,can 可以保留文件,而将其移动到一个特殊的垃圾箱目录,类似于 Windows 的回收站。
4.	所有权和权限
在类 UNIX 操作系统中,文件所有权和权限是非常重要的概念。UNIX 从一开始就是多用户的操作系统。尽管现在新版本的 Windows 也可以用作多用户系统,但如果追溯到早期的个人计算机和 DOS 时期,Windows 的祖先是单用户的。即使在今天,有些 Windows 用户也很少考虑多用户系统中关于多个帐户和文件权限的问题。对于 UNIX 用户来说,理解文件所有权和权限是至关重要的。
4.1.	chown、chgrp
可以使用 ls -l 查看 UNIX 中的文件所有权,可以使用 chown 和 chgrp 对其进行更改。输入下面的命令: $ ls -l
使用 -l 指定需要查看长格式的文件清单。长格式包括关于权限、所有权、修改日期和文件大小等信息。您应该看到与下面所示类似的内容: tsystem:~/TUTORIAL tuser$ ls -l
total 0
-rw-r--r-- 1 tuser admin 0 Aug 13 15:35 example.txt
当详细显示目录清单时,您可以分别在第 3 和第 4 列中看到文件的用户所有权和组所有权。该文件由 tuser 用户和 admin 组所有。现在,暂时不管这个文件,输入下面的命令可以得到更多的信息: $ man chown
$ man chgrp
该命令的基本语法是使用用户名或组名作为第 1 个输入,后面是需要进行操作的文件或文件列表。例如: $ chown tuser example.txt
$ chgrp admin example.txt
4.2.	chmod
在 UNIX 中,使用与每个文件相关联的一组 9 个标志来处理基本文件权限。这些标志分别对应于不同用户类别(用户、组和其他用户)和三种文件操作(读取、写入和执行)。输入下面的命令: $ ls -l
您应该看到与下面所示类似的内容: -rw-r--r-- 1 tuser admin 0 Aug 13 15:35 example.txt
当您阅读目录的长格式清单时,其中的第 1 列显示了文件权限。请注意,该列中有 10 个字符(而不是 9 个)。第 1 个字符表示您正在处理的文件的类型。例如,短横线 -,告诉您这是一个常规文件。d 表示目录,而不是普通文件。现在,我们重点关注于其他的 9 个字符,每个字符可以为下列几种情况其中之一:-、r、w 或 x。如果关闭了某个文件所有的权限,那么它应该与下面所示类似: ---------- 1 tuser admin 0 Aug 13 15:35 example.txt
如果打开了某个文件所有的权限,那么它应该与下面所示类似: -rwxrwxrwx 1 tuser admin 0 Aug 13 15:35 example.txt
您可以看到 3 组 rwx。如前所述,有 3 种不同的用户类别(用户、组和其他用户)。每一组 rwx 对应于其中的一个类别: 
第 1 组 rwx 表示 user 的权限。换句话说,这可以告诉您,该文件的拥有者可以对其进行哪些操作。 
第 2 组对应于 group。这可以告诉您,组成员可以对该文件进行哪些操作。 
第 3 组对应于 other。这可以告诉您,系统中所有的用户可以对该文件进行哪些操作,无论该文件属于谁。 
让我们更仔细地研究一下 example.txt 文件当前的权限: -rw-r--r-- 1 tuser admin 0 Aug 13 15:35 example.txt
第 1 组三个标志表示该文件的所有者可以对其进行读取、写入操作,但是不能执行它(其中有 r 和 w,但没有 x)。从第 2 组三个标志中您可以看出,拥有该文件的组可以对其进行读取,但不能写入或执行(其中有 r,但没有 w 或 x)。第 3 组三个标志显示了,所有其他的用户可以读取该文件,但不能对其进行写入或执行操作(其中有 r,但没有 w 或 x)。这里给出了一个很好的示例,说明了详细的 man 页面非常重要。您可以停下来,并输入下面的命令: $ man chmod
现在,可以考虑一下这样的可能性,即该文件中保存了私有的信息,而您不希望任何其他的用户进行读取。您可能希望删除其他组和所有其他用户的读取权限。可以使用 chmod 更改这些权限。与 UNIX 中的许多操作一样,使用 chmod 的方法很多,本部分内容重点关注其中之一。使用三个字母(u、g 和 o)来表示三种类别(用户、组和其他用户)。使用三个字母(r、w 和 x)来表示三种权限(读取、写入和执行)。要更改这些权限,可以使用 chmod,加上需要更改的类别的字母,再加上一个加号或减号(分别表示打开或关闭),然后使用相应的字母给出需要更改的权限。最后,加上需要进行更改的文件的名称。最好通过一个示例来进行说明: $ chmod og-r example.txt 
$ ls -l 
您应该看到下面的结果:  
-rw------- 1 tuser admin 0 Aug 13 15:35 example.txt
在这个示例中,您指定了其他用户和组(o 和 g),并使用减号表示希望对这些类别关闭某些权限。然后,使用 (r) 表示需要关闭读取访问权限。现在,文件的所有者 tuser 仍然可以对该文件进行读取和写入操作,但系统中所有其他的用户(除了超级用户之外)都不能访问该文件。注意:超级用户 (root) 可以重写所有的文件权限。 
5.	处理多个文件
现在,您已经了解了如何遍历目录和处理单个文件。本教程中的下一个内容是,了解如何处理一组文件。几乎所有的 UNIX 命令除了可以处理单个文件外,还可以处理文件列表。通过显式地输入需要使用的每个文件的名称,或使用通配符表示需要使用的、具有共同名称特征的所有文件,您可以输入文件的列表。
5.1.	通配符
对于处理多个文件,最常用的方法就是使用 * 通配符。通过使用 * 来表示任何字符或任何数量的字符,您可以选择文件列表。为了创建更多的文件以进行演示,可以输入下列命令: $ cp example.txt example2.txt
$ cp example.txt script.sh
现在,输入下面的命令: $ ls *.txt 
您应该仅看到扩展名为 .txt 的文件。接下来,输入: $ ls exa*
这次,您应该看到两个示例文件,但不应该看到 script.sh。
任何能够处理多个文件的命令行应用程序都可以使用通配符。 
5.2.	递归
许多用来处理文件的命令行应用程序都具有一个 -R 选项。当使用了 -R 时,该应用程序将递归地进入一个目录及其所有的子目录,并且针对其中的每个文件执行所需的命令。例如,您可以返回您的 home 目录,然后复制整个 TUTORIAL 目录: $ cd ~ 
$ cp -R TUTORIAL /tmp/. 
$ ls /tmp/TUTORIAL/ 
现在,可以删除该目录,以便对系统进行清理:  
$ rm -R /tmp/TUTORIAL/ 
$ ls /tmp/
这就删除了整个目录,包括其中包含的所有文件。请注意:您可能会比原计划删除更多的数据,尤其是当通配符和 -R 一同使用时。
6.	存档和压缩
许多日常的文件操作都可以使用单个文件、目录和通配符来完成,但对于那些对备份、将文件集传输给其他用户、或仅为了节省空间感兴趣的用户来说,了解如何使用存档和压缩是非常重要的。UNIX 操作系统中有许多用于存档和压缩数据的实用程序。
6.1.	tar
要将多个文件聚集成单个文件(存档),最常用的方法是使用 tar 命令。tar 是磁盘存档程序 (Tape Archiver) 的缩写,这是因为它开始时使用备份磁带进行存档,但现在通常用来执行磁盘到磁盘的操作。尝试对 TUTORIAL 目录中的内容进行存档:
$ cd ~ 
$ tar cvf /tmp/tutorial.tar TUTORIAL 
$ ls /tmp/  
现在,您应该可以在 /tmp/ 中看到一个名为 tutorial.tar 的文件。该文件包含了目录 TUTORIAL 以及这个目录中所包含的文件。这个示例中使用了命令行选项 cvf: 
c 表示创建 (create),它用来告诉 tar 创建一个新的存档。 
v 表示详细 (verbose),它用来告诉 tar 显示加入到存档的每个文件的清单。 
f 表示文件 (file),它用来告诉 tar 将存档写入到文件而不是设备。 
然后,进入 /tmp 并提取该存档的内容:  
$ cd /tmp/ 
$ ls 
$ tar cvf tutorial 
$ ls
请注意,第 1 个 ls 命令显示了 tutorial.tar,但它并没有显示 TUTORIAL 目录。第 2 个 ls 命令(在 tar 命令之后),它显示出在 /tmp/ 中有一个 TUTORIAL 目录。删除 /tmp/TUTORIAL 目录,以便对系统进行清理:  
$ rm -R /tmp/TUTORIAL
6.2.	gzip
在有了 tar 文件之后,您可能希望对其进行压缩。在大多数 UNIX 分发版中,有大量的压缩选项可供使用,但本教程重点关注于使用 gzip 来创建所谓的 tarball。tarball 是一个经过压缩的 tar 文件。下面让我们来创建一个压缩版本的 tutorial.tar,以使其占用更少的空间:  
$ gzip tutorial.tar 
$ ls 
现在,已经对 tutorial.tar 存档进行了压缩,并将其重命名为 tutorial.tar.gz。这是一个 tarball。要解压该文件,可以输入:  
$ gzip -d tutorial.tar.gz 
所得的结果是没有压缩的原始 tar 文件 tutorial.tar。 
7.	文件系统和文件的大小
可以很容易地了解如何处理单独的文件并查看它们的大小和内容。您可以使用相同的方法来查看整个目录和文件系统中的内容。许多新版本的 UNIX 可以使用简单的数值格式来显示该信息,并使用字母来表示相应的单位。
7.1.	df
df 表示显示空闲 (Display Free) 磁盘空间。要使用该命令,可以简单地输入 df,您将获得关于计算机中每个文件系统的磁盘空间、已用空间和空闲空间的信息。在缺省情况下,大多数系统以 512KB 大小的块为单位进行显示,这将难以阅读。使用 -g 表示以千兆为单位显示信息,或使用 -m 表示以兆为单位显示信息。有些系统还提供了 -h 选项,它表示以人可读的 (human-readable) 的方式显示数据。这使得 df 使用像 G、M 和 K 这样的后缀,并且以 3 个或更少的位数来显示每个数值。输入下面的命令: $ df -h 
下面是在一台简单的服务器上可能看到的输出示例: 
$ df -h 
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda1             7.9G  3.7G  3.9G  50% /
none                  3.9G     0  3.9G   0% /dev/shm
/dev/sda3              24G   20G  1.9G  92% /export
ls -lh
如果您的系统为 df 提供了 -h 选项,那么您还可以将其与 ls 一起使用。输入下面的命令以查看详细的清单,这样更容易阅读文件的大小:  
$ ls -lh 
7.2.	du
du 是用来查看文件大小的第 3 种方法,但在汇总目录大小方面,它具有一定的优势。在有些系统中,它还可以与 -h 一同使用,否则可以尝试使用 -k,这将以 1024 字节大小的块为单位给出结果。您还可以使用 -s 和文件名或通配符,以指定需要查看哪个目录和文件。尝试下面的命令:  
$ cd ~ 
$ du -sk * 
$ du -sh * 
下面是在 home 目录中可能看到的输出示例:
$ du -sk * 
316468  OLD
637940  MyData1
571788  Code
12356364        Build
3224480 Hardened
$ du -sh * 
310M    OLD
623M    MyData1
559M    Code
12G     Build
3.1G    Hardened
7.3.	/dev
/dev 目录中保存了一些称为设备文件 的特殊文件,这些文件与其他的内容一起,用来访问系统中的磁盘驱动器。要了解关于 /dev 目录的更多内容,可以再次查看 df 的输出。这在每台计算机上都不一样,但是请注意 df 显示的您的计算机上每个文件系统的结果。与基于 Windows 的计算机不同,每个已装入的文件系统都必须从系统的根目录开始编址,使用斜杠 / 来表示根目录。这与使用字母(如 C、D、E 等等)对磁盘进行区分的系统有所不同。
在 UNIX 中,通常 SCSI(和 SATA)磁盘都使用相应的设备名,如 /dev/sda、/dev/sdb、/dev/sdc 等等。CD-ROM 驱动器常用的设备名为 /dev/cdrom。将这些设备装入 到相应的目录,以便可以对它们进行访问,而无需使用设备名。请参考您的 UNIX 版本中的文档,以找出系统中设备的标记方式。
7.4.	mount
任何设备都可以被装入到任何位置(任何目录)。例如,通常将 CD-ROM 装入到 /mnt/cdrom。有些类 UNIX 操作系统(如许多版本的 Linux 和 Mac OS)会自动地装入 CD-ROM,但最好还是了解一下如何使用 mount 命令。插入一张 CD-ROM,然后输入下面的命令:  
$ mount -t iso9660 /dev/cdrom /mnt/cdrom
$ df
$ ls /mnt/cdrom
注意:这种方法仅当您的系统中存在 /dev/cdrom 和 /mnt/cdrom 时有效。如果它们的确存在,那么您将在 df 命令的输出中看到,CD-ROM 已成为文件系统中的一部分。ls 命令应该显示刚刚装入的 CD-ROM 驱动器中的内容。
7.5.	umount
要卸载设备,大多数类 UNIX 操作系统使用 umount。其语法是 umount 加上装入点。如果以前成功地执行了 mount 命令,那么请输入下面的命令:  
$ umount /mnt/cdrom
$ df
$ ls /mnt/cdrom
注意:要正确地卸载设备,您不能位于 已装入的文件系统中,否则,系统将提示该文件系统处于繁忙 状态。在正确地执行了 umount 之后,df 命令将不再显示文件系统中的 CD-ROM 驱动器,并且 ls 命令显示 /mnt/cdrom 现在为空(因为没有将任何内容装入到其中,它只是一个普通的目录)。
8.	输入和输出
输入和输出通常被认为是简单的缺省终端:键盘/鼠标和显示器/扬声器。在 UNIX 中,通过访问系统的输入和输出流(以及错误流),使得用户和开发人员可以高效地对进出应用程序的输入和输出进行移动,这样做可以最大限度地减少人工交互,从而极大地降低处理过程的复杂程度。stdin、stdout 和 stderr 是 UNIX 中重要的组成部分,并使其成为了理想的脚本编写平台。
8.1.	stdin、stdout
大多数命令行应用程序可以从 stdin 接受输入,并输出到 stdout。stdin 表示标准输入。stdout 表示标准输出。在缺省情况下,stdin 的输入来自键盘(即您在终端中输入的内容),而向 stdout 输出的结果将显示在屏幕上(即您在屏幕上所看到的内容)。另外还有一种输出类型,即 stderr,它用来打印错误和调试信息,但本教程重点关注于 stdin 和 stdout。 
8.2.	重定向
重定向允许用户将通常发送到 stdout 的输出发送到另一个目标,例如一个文件。使用下面的命令,可以创建一个包含 TUTORIAL 目录清单的文本文件:  
$ cd ~/TUTORIAL 
$ ls > listing.txt 
$ ls
另一种形式的重定向是 >>,它将输出内容添加到文件的末尾,而不是创建一个新的文件。您还可以使用 2> 将 stderr 重定向到文件,或者使用 &> 将所有的输出(stdout 和 stderr)重定向到文件。您可以组合使用各种形式的重定向,例如,使用 2>> 将 stderr 添加到文件末尾。 
8.3.	cat
既然有个文件中包含了一些文本,那么让我们来看看其中的内容。查看文件内容的最快捷的方法是使用 cat 命令,它表示连接 (Concatenate),可以与重定向一起使用以连接文本。输入下面的命令:  
$ cat listing.txt 
如果您执行了本教程中的所有操作步骤,那么您应该看到与下面所示类似的内容:  
example.txt 
example2.txt
listing.txt 
script.sh 
8.4.	more
前面的 cat 命令通常用于内容较少的文件,这样一来,您可以在一页中看到所有的数据。如果您正在查看一个文件,而该文件的内容超过了一页,那么通常可以使用 more 命令,它将在 stdout 中完整显示一页数据的时候暂停输出。按下空格键可以继续输出下一页。尝试创建一个较长的文件,以便作为示例:  
$ ls /etc/ > listing2.txt
$ cat listing2.txt 
$ more listing2.txt 
如果您使用 cat,那么该文件的内容将很快地滚过屏幕,而无法进行阅读,但如果使用 more,您可以按空格键一页一页地浏览输出内容。 
8.5.	head 和 tail
日志文件中最后面的内容。当您在调试代码的时候,可以很方便地使用它们作为查看输出文件的方法。尝试输入下面的命令:  
$ head listing2.txt 
要查看文件结尾处若干行的内容,而不是起始处的若干行内容,可以尝试使用 tail:  
$ tail listing2.txt 
通常在缺省情况下,这两个命令将显示 10 行内容,但是您可以使用 -n 选项,以便显示任何行数的内容。例如,输入下面的命令:  
$ head -n 2 listing2.txt 
8.6.	grep
既然您正开始创建包含更多内容的文件,那么您可能希望从中搜索特定的文本。下面重要的示例中使用的 grep 是一种功能强大的搜索实用程序:  
$ grep host listing2.txt 
该命令输出 listing2.txt 中包含字符串 host 的所有行。 
8.7.	管道
要省略文件创建的步骤,您可以使用 pipe 字符 (|) 将一个命令的输出传递给另一个命令作为其输入。这是另一种形式的重定向,其功能非常强大,可用于连接一长串命令列表,以便高效地管理输入和输出。尝试这个简单的示例:  
$ ls /etc/ | grep host 
该命令与前面列出的两步操作得到同样的输出。这一行命令接受 ls 命令的输出,并将其作为 grep 命令的搜索输入。下面是另一个示例:  
$ du -sh /etc/* | more 
在这个示例中,查看了 /etc/ 中每个文件和目录的磁盘使用情况 (du)。输出结果超过一页,所以可以通过管道 (pipe) 将结果传递给 more,而后者每显示一页内容就会暂停输出。 
9.	总结
在学习完本教程后,您应该已经具备了在命令行中遍历 UNIX 文件系统和处理文件和目录的基本知识。您应该对本文列出的命令进行试验,而对于希望了解更多内容的项目,可以仔细地查看其 man 页面。
本系列中后续的教程将详细地讨论特定的应用程序(例如,vi),并介绍 Shell 的诀窍和技巧。同时,您可以使用测试用户进行各种试验。很快地,您就会喜欢上使用命令行选项进行工作,因为这比通过鼠标进行点击的速度更快!
UNIX 新手指南,第2部分: vi 文本编辑器
1.	开始之前
	了解本教程中包含的内容以及如何最好地利用本教程。
1.1.	关于本系列
	这个由四部分组成的文章系列将从用户的角度出发,介绍 UNIX? 的基本知识。对于很久没有使用类 UNIX 操作系统的用户来说,这个初始的教程是个很好的温习。对于具有 Windows? 使用经验的新的 UNIX 用户来说,它也很有价值,因为其中引用了 Windows,并将两者进行了比较。 本系列中的后续教程将详细地介绍一些特定的应用程序(比如 vi),并讨论关于 Shell 的诀窍与技巧。
1.2.	关于本教程
	vi 编辑器已有 30 多年的历史,并且到现在为止,仅对其进行了很少的更改。 它保持了不使用鼠标而通过键盘驱动的界面,这使得用户可以始终将手指放在标准键位处。 用户可以在两种模式(插入模式和命令模式)之间进行切换,以便分别插入文本或对文档进行操作和导航。命令模式为用户提供了各种各样的功能,而这些功能在鼠标驱动的界面中通常需要通过指向-点击 (point-and-click) 来实现。
1.3.	目标
	本教程的目标是让 vi 的新用户能够熟练地对文档进行创建、编辑和导航。 本文重点关注于常用的 vi 命令,并详细地讨论了 vi 中一些比较隐秘的特性。 在学习使用 vi 时需要记住的最重要的一点是,起初您可能会感觉到文本编辑的速度很慢并且很麻烦。 您可以回忆一下第一次使用鼠标或学习使用键盘进行输入的情形。 这个具有 30 多年历史的应用程序强迫用户以一种新的模式进行思考,但是花费大量的学习时间是值得的,最终能够实现快速的、不使用鼠标的文本编辑。
1.4.	先决条件
	要学习本教程,您需要对命令行有基本的认识。 您应该了解什么是文件和目录,并且能够使用自己的帐户登录到类 UNIX 操作系统。 
1.5.	系统要求
	在任何运行类 UNIX 操作系统的计算机上拥有您自己的帐户,这是完成本教程所需的要求。 类 UNIX 操作系统包括 IBM? AIX? 操作系统、Linux?、Berkeley Software Distribution (BSD)、Mac OS? X(使用终端来访问命令行),以及许多其他的操作系统。
2.	vi 简介
	vi 文本编辑器使用了两种主要的模式:命令模式和插入模式。 本教程的第一部分将重点关注于导航文件,这个任务可以在命令模式中完成。 当处于命令模式中时,普通的键盘操作用来执行命令,而不是创建文本。 当您进入到插入模式,可以使用键盘输入文本,例如在命令行中。 要退出命令模式,可以按 Esc 键。
	vi 中的命令有些是单键命令,有些是使用 Shift 或 Ctrl 或按键序列的命令。 在使用引用一个大写字母的命令时,您应该使用 Shift 键加上这个字母。 在使用引用两个字母或符号的命令时,您应该按顺序按下这些键,而不是同时按下。
	要开始练习,首先您将在命令行中使用 vi 命令加上新文件的名称,以创建一个空白文件。 在本教程中,您在 vi 中从头开始建立了一个文档,然后学习使用有用的 vi 命令对该文档进行编辑。 在完成本教程之后,您将了解所有主要的 vi 命令,这些命令可以用来完成日常的编辑任务,以及一些功能强大的命令,您可以在适当的时候使用它们。在学习本教程的过程中,随时可以使用参考资料部分中来自 Cal Tech 的参考手册。
	登录到您最喜欢的类 UNIX 操作系统,然后使用 vi 打开一个新的文件(请参见图 1)。
图 1. 使用 vi 打开一个新的文件
 
	打开了一个名为 tutorial.txt 的新文件(请参见图 2)。 您马上将看到奇怪的地方:文本编辑器最左边的一栏中填满了波浪符号。 不要担心,这是 vi 表示文档中未定义的部分的方式。 换句话说,因为该文件没有任何内容,所以这些行并不存在。 
图 2. vi 中的空白文件
 
	开始进行任何操作之前,您应该了解如何保存文件以及如何编辑文件。 要输入这些类型的命令,可以按冒号 (:) 键加上描述所需操作的字母序列。 要保存新的文件,可以按 : 键、w 键,然后按 Enter 键。要退出 vi,可以按 : 键、q 键,然后按 Enter 键。 现在,重新在命令行中打开 vi。 如果您希望退出 vi 而不保存所做的更改,那么它会发出警告并提示您按感叹号 (!) 以确认您的操作。 如果您希望忽略系统警告,可以在按键序列的后面追加一个感叹号,退出 vi 的按键序列是:: q !, Enter。可以将命令组合使用,如 wq,表示保存文件并退出 vi,或者x。
	需要完成的第一项任务是插入一些文本,以便能够学习如何编辑和操作文件。 在缺省情况下,进入 vi 后处于命令模式。 所以,如果您这时开始输入,那么可能会对得到的结果感到困惑。 稍后将对每种模式进行介绍,现在请按i键以进入到插入模式,然后输入一些文本行内容,这与在普通文本编辑器中的操作相同。 在完成输入后,按 Esc 键返回到命令模式。 请参见图 3 和 4。
图 3. 当您进入到插入模式时,编辑器中最下面的一行将会说明这一点
 
图 4. 在示例文档中输入一些内容,以便可以对其进行操作
 
	按了 Esc 键之后,光标仍然位于输入的最后一个字符处,并且返回到了命令模式。 下一部分将向您介绍如何导航文件。
3.	在 vi 中进行导航
3.1.	移动光标
	命令模式中,您的键盘变成了与界面的交互工具,而不是文本输入工具。vi 可以让那些需要使用所有常用命令的用户将手保持在标准键位(a-s-d-f 和 j-k-l-;)上,并延伸到附近的字母。 首先要学习的基本操作是移动光标。 大多数最新版本的 vi 允许您使用键盘上的箭头键,但是高级的 vi 用户更喜欢使用手指易于触及的按键 h-j-k-l: 
	H和L 分别表示向左和向右,这是非常直观的,因为将它们绑定到了四个方向键中最左边和最右边的按键上。K表示向上移动光标。J向下移动光标。将很快地记住这些按键。 要将光标移动到您所创建的包含三行内容的文件中的第一行,可以按两次 k 键。 光标现在位于第一行的末尾。 继续进行操作,并使用 h-j-k-l 将光标移动到文件中的其他位置,然后再次让它返回到第一行的末尾。 请参见图 5。
图 5. 使用方向键 h-j-k-l 让光标返回到文件的第一行
 
3.2.	使用快捷方式移动光标
3.1.1.	在一行中移动
	现在您的光标已经位于第一行的末尾,您可能希望将它移动到该行的开头,但却不希望反复地按 h 键以使光标移动到那里。 在命令模式中,vi 具有一些基于键盘的快捷方式,它们允许您快速地移动到文件中的各个位置,与您将手移动到鼠标并指向文件中的某个位置,或使用箭头键一次移动一个位置相比,这种方式的速度更快。这些快捷方式中的第一个是 0:移动到一行的开头,可以按 0,您的光标将跳转到该位置。 要移动到一行的末尾,可以按 $。 继续练习并尝试这种方法。
	现在,您可以一次移动一个字符。您也可以将光标移动到每一行的开头和末尾,但是这两种操作在移动粒度方面相差很大。 另一种选择是一次移动一个单词。 您可以使用 w 和 b 键来完成这项任务: 按 w 键将向前移动一个单词。 按 b 键将向后移动一个单词。您可以尝试这种方法,先将光标移动到第一行的开头(按 0),然后按三次 w 键将光标移动到单词 test 的开头。 接下来,按两次 b 键以返回到单词 is。
	您可能注意到了,w 键和 b 键将光标定位于每个单词的开头。 您还可以使用 e 键向前移动光标,导航到单词的末尾,或按 g 键。 按 g 向后移动。 请参见图 6。
图 6. 使用 w 和 b 键逐个单词地移动光标,这是一种快速导航到拼写错误或需要更改的单词的好方法
 
3.1.2.	从一行移动到另一行
	现在,您可以在一行中快速地进行移动,但是如果正在处理一个较大的文件,还需要快速地从一行移动到另一行。 在 vi 中,有许多方法可以用来完成这项任务。 您可以使用向下或向上的移动命令(k 和 j),或者您可以使用 page-up 和 page-down 命令。 vi 中的大多数命令不需要按 Ctrl 键,但是 page-up 和 page-down 这对命令是这条规则的例外: 
	按 Ctrl-u 向上翻一页。 
	按 Ctrl-d 向下翻一页。 
	要快速地导航到文件的开头或末尾,您可以按 gg 或 G: 按 gg 可以将光标移动到文档中的第一行。按 G 可以将光标移动到文档中的最后一行。 
	还可以在附加方法中指定行数,这些内容将在在 vi 命令前面加上数字部分中讨论。
4.	在 vi 中进行插入和编辑
4.1.	插入文本
4.1.1.	i前位插入
	在导航文档的过程中,您仍然处于命令模式,并且使用键盘作为界面交互工具来移动光标。 下一组命令模式按键提供了各种进入插入模式的方法,可以用来向文件中输入新的文本。 在本教程开始处输入初始文本时,您使用了最基本的方式进入到插入模式:按 i 键,它表示 insert。 按 i 可以在当前光标位置的前面进入到插入模式。 例如,如果您按 gg 导航到文件的开头,那么您可以按 i 键,这将使得您输入的任何文本都出现在当前行文本之前。 按 gg、i,然后输入 I am inserting new text with i。 在完成输入后,按 Esc 键返回到命令模式。 请记住,在输入完新的文本之后,您必须返回到命令模式,否则无法对文档进行导航。 请参见图 7。
图 7. 添加新文本的最简单的方法是使用 i 键进入到插入模式
 
4.1.2.	a后位插入
	添加新文本的另一个基本方法是使用 a 键,它表示 append。 使用 a 键可以进入到插入模式,但它将在光标当前位置之后添加文本,而不是在光标当前位置之前。 要测试该命令,定位到文档的最后一行,按 G 和 $ 键定位到该行的末尾。 然后按 a 键,输入 Pressing a appends text,并按 Esc 键返回到命令模式。 请参见图 8。
图 8. 另一种插入文本的方法是使用 a 键,它表示 append 
 
4.1.3.	I行前插入和A行后插入
	现在您的光标位于文件最后一行的句号处。 如果您现在按 i 键,那么会将文本插入到句号之前。 如果您按 a 键,则会将文本插入到句号之后。 通过按 I 键(大写字母),您可以在一行的开头进行输入,即使您的光标位于该行的末尾。 与之类似,如果您按大写 A 键,那么您可以在该行的末尾输入文本,无论光标当前处于什么位置。 要测试这个命令,可以按 I,输入 I think ,然后按 Esc 键。 请参见图 9。
图 9. 要在一行的开头插入文本(无论光标当前处于什么位置),可以按 I 键
 
4.1.4.	O前新行和o后新行	
	插入新文本的另一种有用的方法是,在进入到插入模式的同时,向您的文本文件添加新的一行。 与普通的文本插入一样,新的一行可以插入到光标位置之前或之后: 
	要在当前光标位置之前插入新的一行,可以按 O 键。 
	要在当前光标位置之后插入新的一行,可以按 o 键。 
	要尝试使用这个命令,可以按 O,输入 I inserted this line by pressing O,然后按 Esc 键以返回到命令模式。 请参见图 10。
图 10. 要在光标之前插入新的文本行,可以按 O 键 
 
	您现在已经使用了一些主要的方法来插入新的文本。 回顾一下,这些键分别是 a、i、A、I、o 和 O。您能记住其中每个字母表示什么操作吗? 即使现在没有记住,请不要担心,在使用一段时间之后您就会记住了。
4.2.	替换文本
	既然已经向 tutorial.txt 文件中输入了一些内容,您可能发现其中存在一些错误,或者可能希望更改其中的一些单词。 在学习如何删除文本之前,您应该学习如何替换文本。为什么呢? 因为如果您学习了如何添加新的文本和删除旧的文本,您可能会养成使用删除命令的习惯,而在某些情况下 replace 命令可能更加有效。 与先删除一个单词,然后在该处添加一个新单词的方法相比,使用单步处理对单词进行替换更加便捷。
4.2.1	r单字符替换
	vi 使用了两种重要的替换命令。 第一种是 r 键,它将删除光标位置的字符,并进入到插入模式以便输入单个替换的字符。 换句话说,您可以在按了 r 之后输入一个字符。 因此,vi 自动地返回到命令模式(不需要按 Esc 键)。 要尝试该命令,可以使用 k 键和 l 键导航到第二行的末尾。 您的光标应该位于句号处。 要将句号更改为感叹号,可以按 r 键,然后按 ! 键。 请参见图 11。
图 11. 要替换单个字符,可以按 r 键,然后输入文档中需要的字符 
 
4.2.2	cw单字替换
	还有一种更有价值的 replacement 命令,可以按 c 键,然后按 w 键,它们组合在一起表示 change word。 这个命令删除当前的单词,并进入到插入模式,以便您可以立即输入替换的单词。 在这种情况下,输入完新的单词后,您需要按 Esc 键以通知 vi 完成了替换任务。 将光标向下移动到单词 great,按 c 和 w 键,输入 cool,然后按 Esc 键。 请参见图 12。
图 12. 要替换单词,可以按 c 和 w 键,然后输入新的单词
 
4.3.	删除文本
4.3.1	x删除单字符	
	如果您希望删除文本,而不是对其进行替换,那么您需要使用 delete 命令。 与 vi 中其他的操作一样,您有多种方法可供选择,这取决于您希望一次删除多少数据。 最基本的 delete 命令是 x 键,它一次删除一个字符。 要尝试使用该命令,可以返回到文本文件的开头,按 Ctrl-u (page-up) 键。 按 $ 键定位到该行的末尾,然后按五次 x 键,以删除 test.。 请参见图 13。
图 13. 要逐个地删除字符,可以使用 x 键 
 
	dw删除单字
	按五次 x 键可以完成这项任务,但是您可能希望 vi 能够为各种操作提供更简单的方法。 按 d 键和 w 键,您可以一次删除一个单词,这两个键组合在一起表示 delete word。 按三次 b 键,返回到单词 This。 要删除这个单词,按 d 键和 w 键。 请参见图 14。
图 14. 要删除单词,可以使用 dw 命令
 
4.3.2	d$删除位后整句
	现在第一行中包含一个半截的、不完整的句子。 要删除这一行中光标位置之后所有的内容,您可以使用d键加上 $ 键,前面曾经介绍过,后者用来表示到一行末尾的操作。 按 d 键和 $ 键,以便删除该行末尾的 is a。 请参见图 15。
图 15. 要删除光标到一行末尾的文本,可以使用 d$
 
4.3.3	dd删除一行
	vi 中最后一个常用的删除命令是 delete-line 命令,可以按两次 d 键来完成该操作。 它会删除一行文本,并将下面一行内容上移,这样一来,文档中就不会出现空行。 要删除文件中的第一行,可以按 d 键,然后再按一次该键。 请参见图 16。
图 16. 要删除整行文本,可以按 dd
 
	谈到向上移动一行文本,您可以按 J 键以使用 join 命令,该命令将光标所在行的下面一行文本向上移动到光标所在行,而不会删除任何文本。 您的光标应该位于文档的新行(第一行)。 按 J 键将第二行上移至第一行。请参见图 17。
图 17. 要在 vi 中将两行文本合并在一起,可以按 J 键
 
5.	更好地完成其他操作
	现在,您应该已经掌握了在 vi 中创建、导航和编辑文本文件的一些基本技能。 在熟练掌握了这些基本的命令之后,您可以像在其他更加常规的文本编辑器中那样创建和编辑文件。 但是您的指令表中还缺少了一些内容。 本部分内容将向您介绍如何剪切、复制和粘贴。 您可以一次反复地执行命令、再次执行命令、搜索文档、并使用撤销 (undo) 和重做 (redo) 命令。 这个编辑器提供了其他文本编辑器中主要的功能,并且使用了快速访问的键盘命令方式。
5.1.	剪切、复制和粘贴
	在 vi 中删除文本的同时,会将这些文本自动地保存到缓冲区中(类似于 Windows 中的剪贴板)。 您已经了解了如何完成剪切命令,可以使用 x、dd、dw 和 d$。 可以使用类似的命令来复制数据,而不删除该数据,在 vi 中,这种操作称为 yank:
	按两次 y 键,可以复制整行文本。 
	按 y 键和 w 键,可以复制一个单词。 
	按 y 键和 $ 键,可以从当前光标位置开始复制该行文本。 
	只有了解了如何粘贴数据,对数据进行复制才能真正派上用场。 所以,在测试这些命令之前,您应该学习 paste 命令,可以使用 p 键来执行该命令。 与 vi 中其他的许多命令一样,小写的 p 键将数据粘贴到光标位置之后,而大写的 P 键将数据粘贴到光标位置之前。
	要进行复制和粘贴,导航到文本文件的第一行,按两次 y 键。 然后,将光标向下移动到第二行,按一次 p 键。 该操作将第一行的文本复制到了第三行。请参见图 18。
图 18. 要复制一行文本,可以使用 yy 命令,并使用 p 命令进行粘贴
 
	您可以尝试进行剪切和粘贴操作,将光标移动到第二行,按两次 d 键。 然后,按 p 键将该行文本粘贴到第二行的下面。请参见图 19。
图 19. 要剪切并粘贴一行文本,可以使用 dd 命令和 p 命令
 
5.2.	在 vi 命令前面加上数字
	此时,您可能为如何一次对多块数据执行这些命令而感到困惑。 例如,您可能经常需要复制和粘贴整个段落,而不是单独的行。vi 允许您在使用每个命令时,在它们的前面加上一个数字,这样就会多次执行该命令。 这种用法的功能非常强大,这也是使得 vi 能够成为专业用户手中优秀的编辑器的重要因素之一。 要尝试一次剪切并粘贴两行文本,可以导航到文件的第一行,按 2 键,按两次 d 键,然后按 p 键。 请参见图 20。
图 20. 可以在 dd 命令和 p 命令前面加上 2,一次剪切并粘贴两行文本
 
	在使用 paste 命令多次复制一行文本的过程中,也可以使用相同的概念。 要完成这样的任务,可以先返回到第一行,使用 yy 命令复制该行文本,然后按 10,再按 p 键。 现在将会增加 10 行关于 vi 的评价。 在继续进行其他操作之前,按 5 键,然后按两次 d 键以删除其中一些多余的行。 请参见图 21。
图 21. 在 p 命令前面加上数字 10,可以粘贴多行文本,然后在 dd 前面加上数字 5,删除其中的几行文本
 
	也可以尝试在导航命令的前面加上数字。 例如,按 30 键,然后按 l 键,这样可以将光标向右移动 30 个字符。 按 7 键,然后按 G 键,这样可以将光标移动到文件的第七行。 按 5 键,然后按 w 键,这样可以将光标移动到第五个单词。 在完成了这些实验之后,您可以继续进行下一步操作。
5.3.	再次执行 vi 命令
	vi 中另一个非常有用的命令是 .(句号)键。 . 键可以再次执行最后一条命令,这个重要的特性可以让您快速地完成工作。 例如,将光标导航到第一行的单词 cool,然后使用 cw 命令将这个单词更改为 fast。 在输入完单词 fast 后,不要忘记按 Esc 键。 将光标向下移动到另一个 cool 单词,然后按 . 键将这个单词更改为 fast。 您还可以移动到文件中最后两行的单词 line,并使用 . 键替换它们。 请参见图 22。
图 22. 使用句号键再次执行命令,这使得您可以快速地进行文档编辑
 
5.4.	在 vi 中搜索文本
	在 vi 中搜索文本也是非常快速且高效的。 要开始搜索一个字符串,可以按 / 键(斜杠键),再加上希望搜索的字符串,然后按 Enter 键。 要组合使用您的 vi 技能,可以按 / 键,输入 think,按 Enter,然后使用 cw 命令将这个单词更改为 know。 在完成操作之后,不要忘记按 Esc 键。
	如果您希望对第二行进行相同的操作,按 n 键以找到下一个 think,然后按 . 键将该单词更改为 know。 ? 键可以像 / 键那样进行搜索,但它将反向而不是正向搜索文档。 在将 think 替换为 know 之后,按 ? 键加上单词 fast 以便进行反向搜索。 请参见图 23 和 24。
图 23. 使用斜杠键加上希望搜索的字符串,可以搜索该字符串
 
图 24. 使用 ? 键加上希望搜索的字符串,可以反向搜索该字符串
 
5.5.	vi 中的撤销 (undo) 和重做 (redo)
	如果在操作中出现了错误,vi 为您提供了撤销和重做机制,以确保可以将文档恢复到正确的状态。 在命令模式中,按 u 键可以执行撤销命令,按 Ctrl-r 可以执行重做命令。 可以尝试撤销和重做操作,如下所示(请参见图 2): 
	定位到文本文件的第三行,并删除一些行。 
	按 3 键和 G 键定位到第三行。 
	按 2 键和 dd 命令以删除两行文本。 
	糟糕! 不应该进行删除操作,并且您希望恢复这两行文本。 要完成这个任务,可以按 u 键以撤销上一个命令。 
	如果您又改变了主意,想要删除这两行文本,可以按 Ctrl-r 以重新执行该命令。 
图 25. 要撤销一个命令,可以使用 u 键;要重新执行一个命令,可以按 Ctrl-r
 
6.	总结
	最后,让我们看看最后的一个命令序列,以便将您所学的一些命令组合在一起使用(请参见图 26):
	要定位到文档的开头,按两次 g 键。 
	要删除文档中的所有内容(因为其中的内容少于 100 行),可以输入 100 加上 dd 命令。 
	按 i 键进入到插入模式。 
	输入 I am done with this tutorial!。 
	按 Esc 键。 
	按两次 y 键,输入 100,然后按 p 键。 
	您现在告诉了计算机 100 次,已经完成了本教程的学习! 很好,现在可以休息一会儿,并喝杯咖啡了。
图 26. 完成了本教程的学习
 
	在学习完本教程之后,您应该具备了使用 vi 创建和编辑文件的知识。 对本教程中列出的命令进行实验,开始使用 vi 作为您日常的文本编辑器以便练习使用这些命令。 开始的时候,速度可能会有所降低,但是很快您将记住这些命令,并了解何时何处该使用它们,vi 可以显著地提高您的工作效率。
	本系列中后续的教程将介绍 Shell 的诀窍与技巧。 同时,您应该不断地使用命令行并练习 vi,您将很快会成为一名专业的 UNIX 用户!

UNIX 新手指南,第3部分: 正则表达式
---使用 grep、sed 和 awk
1.	开始之前
	了解 UNIX? 过滤器的强大功能。在本教程中,您将深入了解 grep 系列,包括许多 UNIX 实用程序中的正则表达式语法。您还将了解关于流编辑器 sed 的更多信息,并通过示例和解释来研究 awk 模式扫描语言。
开始之前
	了解本教程中包含的内容以及如何最好地利用本教程。
1.1.	关于本系列
	这个包括四个部分的系列教程从头开始介绍 UNIX?。对于很久没有使用类 UNIX 操作系统的用户来说,这个初始的教程是个很好的温习。对于具有 Windows? 使用经验的新的 UNIX 用户来说,它也很有价值,因为其中参考了 Windows,并将两者进行了比较。第二个教程重点讲述 vi 文本编辑器,该编辑器是功能最强大(也最神秘)的可用 UNIX 实用程序之一。本教程向您讲授有关使用正则表达式的 UNIX 命令行过滤器的知识,包括 grep、sed 和 awk。
1.2.	关于本教程
	要发挥 UNIX 命令行过滤器(如 grep、sed 和 awk)背后的强大功能,您需要非常熟悉正则表达式。本教程向新用户讲授其中每个实用程序的功能和如何使用正则表达式来操作文本。您将首先使用一个简单和好玩的 grep 示例,然后继续研究 sed 和 awk 的实际示例。
1.3.	目标
	本教程的目标是使 UNIX 和 Linux? 用户习惯于使用这三个可用于快速和高效地搜索和改数据的强大命令行工具。本教程开头将解释许多 UNIX 实用程序(及编程语言)基本框架中使用的正则表达式。随后的各个部分将给出与 grep、sed 和 awk 一起使用的正则表达式的示例。
1.4.	先决条件
	对于本教程,您需要对命令行有基本的了解。对于本教程的某些部分,了解如何在 UNIX 中使用 stdin、stdout 和 pipe 来处理输入和输出是有所帮助的。
1.5.	系统要求
	在任何运行类 UNIX 操作系统的计算机上拥有您自己的帐户,这是完成本教程所需的要求。类 UNIX 操作系统包括 IBM AIX? 操作系统、Linux?、Berkeley Software Distribution (BSD)、Mac OS? X(通过终端来访问命令行),以及其他许多系统。
2.	正则表达式
	正则表达式是一个字符串,旨在用于搜索或替换另一个字符串。初看起来,这似乎是一个相当基本的功能。大多数用户都熟悉几乎每个图形文本编辑器或字处理应用程序中都有的搜索和替换功能。如果将这个基本的搜索和替换功能比作计算器,则正则表达式可比作全功能的计算机。将正则表达式用于搜索条件的强大功能不应被低估。
2.1.	使用正则表达式的过滤器
	一些基于 UNIX 的最强大命令行工具使用了正则表达式,包括 grep、sed 和 awk(以及包括 Perl 在内的一些编程语言)。在从基础 UNIX 命令行用户转变为真正的超级用户时,学习如何使用正则表达式是一个必需步骤。存在一些不同版本的正则表达式语法和多个版本的 grep、sed 和 awk,因此本教程将集中于每种实现中都具有的非常标准的最常见构造。不要忘了参考您系统的 man 页,以获得有关语法和命令行选项的细节。
2.2.	基础
	在探索使用正则表达式的 UNIX 应用程序之前,了解基础知识是非常重要的。在本部分中,您只需继续往下阅读。稍后您将在 grep 中尝试一些示例。
2.2.1.	基本搜索
	正则表达式由一些普通字符和特殊字符组成,其中的特殊字符指示搜索条件。 在大多数基本情况下,正则表达式中也许根本就没有使用特殊字符。例如,如果您只是希望使用词条 golf 作为搜索条件,则可以输入以下命令:
	golf
	这就是一个正则表达式!它搜索单词 golf 的所有实例。正则表达式区分大小写,因此这将搜索 golf 的所有实例,但是不会查找 Golf 的实例。
2.2.2.	使用方括号
	若要同时搜索 golf 和 Golf,您可以使用方括号(它们是正则表达式中的特殊字符),并列出一串要搜索的各个字符。这类似于搜索中的搜索(这就是正则表达式背后的神奇之处)。
	[Gg]olf
	同样的概念也适用于任何字符列表——而不只是用于区分大小写。例如,您可能希望搜索 golf 和 gelf(您虚构的一种新体育运动):
	g[oe]lf
2.2.3.	句点
	现在假设您有第三种体育运动 gilf,您也希望对其进行检查。使用您到目前为止已学到的知识,一种方法是在您的搜索条件中使用 o、e 和 i。但是随着您的搜索的逐步发展,您可能希望查找以 g 开头、以 lf 结尾并且其间具有一个字符的所有字符串。为此,您可以使用另一个特殊字符,即句点 (.)。
	g.lf
	这将查找以 g 开头和以 lf 结尾并且其间具有一个字符的所有字符串。若要将您的搜索扩展到以 g 开头和以 f 结尾并且其间具有两个字符的所有字符串,您可以使用两个句点:
	g..f
3.	使用 grep 来搜索文件
	现在您已经对正则表达式背后的概念有了基本的了解,您可以开始使用实际的示例,以便能够看到它们的实际运用。您将试验的第一个命令行应用程序是 grep。grep 的名称实际上就来自于正则表达式:g/RE/p。grep 用于在一个或多个文件中搜索特定字符串的实例。缺省情况下,grep 输出其中出现了您的搜索字符串的每一行(而不是仅输出搜索字符串)。如果您在多个文件中执行搜索,则 grep 还会输出在其中找到该行的文件名。
	使用以下文本创建一个名为 grep.txt 的文件:
	I like golf.
	Golf is played on grass.
	I created gilf.
	grep 的基本语法如下:
	grep REGULAREXPRESSION FILENAME(S)
3.1.	基本搜索
	现在,返回到前面的第一个正则表达式示例:单独的单词 golf。若要与 grep 一起使用这个表达式,可输入:
	grep golf grep.txt
	此命令在 grep.txt 文件中搜索字符串 golf 的所有实例,并输出包含该字符串的行。您的输出应该类似如下:
	I like golf.
3.2.	使用方括号
	下一步,试验一些上面讨论过的特殊字符。您可以使用方括号(方括号表达式)来指示您想要搜索 golf 和 Golf:
	grep [gG]olf grep.txt
	输出应该类似如下:
	I like golf.
	Golf is played on grass.
3.3.	句点
	若要搜索 golf 和 gilf,您同样可以使用方括号。取而代之的是,可以尝试使用一个句点来指示您想要搜索 g 和 lf 之间的任何字符:
	$grep g.lf grep.txt
	输出应该类似如下:
	I like golf.
	I created gilf.
	搜索 golf、Golf 和 gilf
	您现在已经找到了获得每种 golf 变体的方法,但是还没有哪个搜索返回了所有三个实例:golf、Golf 和 gilf。花点时间考虑一下如何搜索所有三个实例。这可以通过多种方法来实现。下面是两个示例:
	grep ..lf grep.txt
	grep [gG][oi]lf grep.txt 
	这两种方法都返回所有三行:
	I like golf.
	Golf is played on grass.
	I created gilf.
3.4.	短横线
	您是否能够想出更多的方法来完成此任务呢?到目前为止,您仅学习了两个在正则表达式中使用的特殊字符。这只是开始!有些特殊字符在其他特殊字符之内使用。例如,当您将一组字符包括在方括号中时,您可以使用短横线 (-) 来搜索一系列字符。将以下行添加到您的文本文件:
	What is g2lf?
	使用您到目前为止已学到的知识,您知道如果使用类似于 g.lf 或 g[oi2]lf 的正则表达式,则这一行将包括在搜索结果中。使用句点将返回在该位置具有任何字符的结果;使用 [oi2] 将返回仅在该位置具有 o、 i 或 2 的结果。通过使用一个短横线,您可以实现第三种方法,其中不只包括少数字符,但并不是包括每个字符:
	grep g[a-z]lf
	此方法产生以下输出:
	I like golf.
	I created gilf.
	从输出中可以看到,此方法搜索落在 a 和 z 之间的任何字符(按字母顺序)。这排除了在 g 和 lf 之间具有数字或符号的字符串,这些字符串不是真正的单词,可能不属于您所需的搜索条件。
	通过在方括号中包括附加集合,您还可以搜索多个字符序列。例如,若要搜索 a-z 和 A-Z,可以使用以下搜索:
	grep g[a-zA-Z]lf
3.5.	脱字号 (^)
	当您的字符序列列表变得更长时,可能发现通过避免某些字符而不是指定想要查找的字符来进行搜索会更容易。这可以通过在搜索序列前在方括号中使用脱字符 (^) 来实现。这说起来挺复杂的,但是通过观察一个示例,应该是很容易理解的。通过使用以下 grep 命令来更改您的搜索,以避免数字但是包括所有其他字符:
	grep g[^0-9]lf
	此搜索类似于前面查找所有字母字符的搜索,但是此搜索还返回诸如数字符号 (#) 和美元符号 ($) 等不属于字母并且也不在您排除的数字序列中的字符。
3.6.	星号
	要试验的下一个特殊字符是星号 (*),它是若干个重复操作符之一。大多数人都非常熟悉在命令行上使用星号作为文件名搜索条件(通配符),但是在正则表达式中使用星号还是相当新鲜的。星号指示搜索项(前一个字符或方括号表达式)可以出现零次、一次或多次。若要对此进行尝试,请将以下行添加到您已经在使用的 grep.txt 文件:
	This time the o is missing in glf.
	Some people might say goolf.
	But they would not say goilf. 
	现在整个文件应该类似如下:
	I like golf.
	Golf is played on grass.
	I created gilf.
	What is g2lf?
	This time the o is missing in glf.
	Some people might say goolf.
	But they would not say goilf.
	尝试在 golf 中的 o 后面使用星号:
	grep go*lf grep.txt
	您的搜索将返回具有单词 golf、glf 和 goolf 的行:
	I like golf.
	This time the o is missing in glf.
	Some people might say goolf.
3.7.	问号
	另一个重复操作符是问号 (?)。问号的功能与星号类似,只不过搜索项可以出现零次或一次。多个实例将不匹配。使用问号取代星号来尝试您刚才执行的搜索:
	grep go?lf grep.tx
	可以看到,这次作为匹配结果返回了 golf 和 glf,但是没有返回 goolf,因为其中存在问号前的搜索项 o 的多个实例:
	I like golf.
	This time the o is missing in glf.
3.8.	加号
	最后一个常规重复操作符是加号 (+)。加号将查找某个搜索项出现一次或多次的情况。与星号不同,必须至少找到一个实例才会匹配。请尝试以下示例:
	grep go+lf grep.txt
	这次,该搜索返回 golf 和 goolf,但它不返回 glf,因为没有找到 o:
	I like golf.
	Some people might say goolf.
3.9.	行首和行尾定位点
	在转向 sed 之前,最后要学习的特殊字符是行首定位点(使用脱字符来实现)和行尾定位点(使用美元符号来实现)。您可能记得,您在本教程的前面使用过脱字符来对方括号表达式取反。当在方括号之外使用脱字符时,它执行完全不同的功能。将脱字符放在正则表达式开头将告诉该搜索仅操作行的开头。换句话说,正则表达式中的第一个字符(脱字符之后)必须与新行上的第一个字符匹配才能匹配该行。类似地,将美元符号放在正则表达式的结尾以指示您仅希望返回与行尾匹配的结果。换句话说,正则表达式中的最后一个字符(美元符号之前)必须与某行上的最后一个字符匹配才能匹配该行。若要对此进行测试,请将以下两行添加到 grep.txt:
	golf has been a fine example
	let's talk about something besides golf 
	请注意,对于此测试,您不应该对 golf 进行大写或加标点,因为它将演示一个针对同一单词的搜索,此搜索使用定位点在行尾或行首以不同的方式操作。若要测试行首定位点,请输入以下命令:
	grep ^golf grep.txt
	输出应该类似如下:
	golf has been a fine example
	若要测试行尾定位点,请使用同一个搜索,但是删除脱字符并在 golf 之后添加一个美元符号。
	grep golf$ grep.txt
	使用行尾定位点的输出类似如下:
	let's talk about something besides golf
3.10.	小结
	现在您已经通过在命令行上使用 grep 来学习了正则表达式的基础知识。下一步,您将学习使用 sed,此实用程序不仅搜索文本,而且还对搜索结果进行替换。首先,下面是对您到目前为止已学习过的内容的小结:
	.   句点表示任何单个字符
	[]  方括号包括一个字符序列
	-   短横线在字符之间使用以创建一个序列(在 [] 内)
	^   脱字符用于对序列(在 [] 内)取反
	*   星号搜索某个搜索项的零个、一个或多个实例
	?   问号搜索某个搜索项的零个或一个实例
	+   加号搜索某个搜索项的一个或多个实例
	$   美元符号搜索行尾
	^   脱字符搜索行首
	\   特殊字符前的反斜杠使该字符成为普通字符(请参见下一部分。)
4.	使用 sed 来编辑文件
	sed 是流编辑器 (stream editor) 的简写。文本编辑器的传统、现代定义是可用于创建和编辑文本文件的交互式应用程序。sed 也是一个文本编辑器,但它是一个命令行实用程序而不是交互式实用程序,从而使之成为一个极其强大的批处理编辑工具。sed 通常在 UNIX Shell 脚本中用于过滤较大的文本文件集。在本教程的第一部分中,您使用了一个讨论 golf 的小型测试文件。为了演示 sed 编辑器的高级功能,您将使用一个很小的代码片段,开发人员可能希望在批处理过程中更改该代码片段。
	请将以下文本复制并粘贴到一个名为 sed.txt 的文件中:
system "echo 'project:$project' >> logfile";
system "echo 'version:$version' >> logfile";
system "echo 'optionalid:$optionalid' >> logfile";
system "echo 'nodes:$nodes' >> logfile";
system "echo 'threads:$threads' >> logfile";
4.1.	正斜杠
	前面解释过的用于 grep 的所有特殊字符在 sed 中也有效。然而,若要使用 sed,您必须了解一些附加语法。sed 中的基本表达式由四个部分组成,各个部分之间用正斜杠 (/) 分隔。以下是用于基本 sed 命令的常见语法:
	sed s/REGULAREXPRESSION/REPLACEMENTSTRING/flags INPUT_FILE
	s-搜索和替换
	s 指示您希望执行索和替换。正斜杠用于绑定 sed 中的正则表达式。例如,如果您只希望将词条 logfile 替换为 logfile.txt,则可以运行以下命令:
	sed s/logfile/logfile.txt/ sed.txt
	输出应该类似如下:
	system "echo 'project:$project' >> logfile.txt";
	system "echo 'version:$version' >> logfile.txt";
	system "echo 'optionalid:$optionalid' >> logfile.txt";
	system "echo 'nodes:$nodes' >> logfile.txt";
	system "echo 'threads:$threads' >> logfile.txt";
	在此情况下要注意的一个要点在于,sed 不会实际更改 sed.txt 的内容。相反,它将输出发送到标准输出设备。对于这些示例,您将把输出发送到标准输出设备,以便能够立即看到操作结果。
	为便于将来参考,可以捕获输出或将其发送到某个新文件。例如,若要将输出发送到 sed_new.txt,可以运行以下命令:
	sed s/logfile/logfile.txt/ sed.txt > sed_new.txt
4.2.	反斜杠
	在学习使用斜杠的同时,还有另一个非常重要的特殊字符需要学习。反斜杠 (\) 称为转义字符,因为它对正则表达式解释中的下一个字符进行转义。更简单的是,将一个反斜杠放在特殊字符前,将使该字符成为普通项而不是命令项。这非常重要,因为许多文件(尤其是在编写代码的时候)广泛利用了与用于执行正则表达式的字符相同的字符。在您的 sed.txt 文件中,您会注意到美元符号的使用。如果您希望替换 $project 而不替换 project,则需要在搜索和替换中使用转义字符:
	sed s/\$project/\$project_name/ sed.txt
	您可以在输出中看到 $project 被更改了,但是 project 没有被更改。
	system "echo 'project:$project_name' >> logfile";
	system "echo 'version:$version' >> logfile";
	system "echo 'optionalid:$optionalid' >> logfile";
	system "echo 'nodes:$nodes' >> logfile";
	system "echo 'threads:$threads' >> logfile";
	更改某个项的多个实例
	这引入了 sed 中的另一个重要功能。如果您希望同时更改 project 的两个实例,该怎么办呢?通过到目前为止已学到的知识,合理的回答是只需使用 project 作为正则表达式,但是此回答并不是非常正确。下面将继续并进行尝试,以便能够演示和解释该过程:
	sed s/project/project_name/ sed.txt
	在输出中可以看到,project 的第一个实例被更改为 project_name:
system "echo 'project_name:$project' >> logfile";
system "echo 'version:$version' >> logfile";
system "echo 'optionalid:$optionalid' >> logfile";
system "echo 'nodes:$nodes' >> logfile";
system "echo 'threads:$threads' >> logfile";
	然而,第二个实例未更改,尽管它肯定匹配您的正则表达式。您从第一个示例中知道,sed 似乎更改其输入中的每个匹配字符串,而不是仅更改第一个匹配字符串,因为它更改 logfile 的每个实例。
	区别在于,logfile 的每个实例在单独的行上,而同一行上却有两个 project 实例。这为什么非常重要?因为 sed 被实现为一个行编辑器。它一次将一个单独的行放到内存中,并将其作为单个单元来操作。在运行 sed 时务必记住这点,因为所有命令行选项都是按这个设计原则来设计的(从而使大多数 sed 实现不会受到与系统内存有关的文件大小限制)。缺省情况下,每一行都视为 sed 命令的一次新的执行。尽管在第一个示例中似乎不是这样,但是其中 sed 命令仅替换匹配字符串的第一个实例。然而,您可以简单地使用一个 g 标志来改变此行为。
4.3.	g 标志
	执行同样的 sed 命令,但这次在结尾附加一个 g:
	sed s/project/project_name/g sed.txt
	这次,第一行上的两个 project 实例都被更改为 project_name:
system "echo 'project_name:$project_name' >> logfile";
system "echo 'version:$version' >> logfile";
system "echo 'optionalid:$optionalid' >> logfile";
system "echo 'nodes:$nodes' >> logfile";
system "echo 'threads:$threads' >> logfile";
	您可能记得,g 是 global 的简写。
4.4.	运行初步的搜索
	sed的另一个强大功能是在搜索和替换操作前运行初步搜索,以确定当前是否在您希望执行命令的行上。这差不多类似于在 sed 中执行 grep。在您的例子中,您可能希望更改 node 变量的日志文件,而不是将它与所有其他输出分组在一起。为此,您需要将字符串 logfile 更改为 logfile_nodes,但是仅在属于节点的行上执行更改。以下命令可以确切完成此任务:
	sed  /nodes/s/logfile/logfile_nodes/ sed.txt
	下面是其输出:
system "echo 'project:$project' >> logfile";
system "echo 'version:$version' >> logfile";
system "echo 'optionalid:$optionalid' >> logfile";
system "echo 'nodes:$nodes' >> logfile_nodes";
system "echo 'threads:$threads' >> logfile";
4.5.	更改以冒号结尾的每个字符串
	现在,尝试使用一些您在使用 grep 时学习到的正则表达式知识,不过这次是在 sed 命令中使用。通过在 sed 中使用以下正则表达式,您可以更改以冒号结尾的每个字符串:
	sed s/[a-z]*:/value:/g sed.txt
	输出应该类似如下:
system "echo 'value:$project' >> logfile";
system "echo 'value:$version' >> logfile";
system "echo 'value:$optionalid' >> logfile";
system "echo 'value:$nodes' >> logfile";
system "echo 'value:$threads' >> logfile"; 
	这相当酷,但不是非常合理。它不是非常合理的原因在于,您的所有变量前都有单词 value,没有办法对各个变量进行区分。然而,通过使用 sed 的另一个功能,您可以使这转变为一个实际的示例。 
4.6.	“&”号
	“和”号 (&) 表示与您的正则表达式匹配的字符串。换句话说,如果 [a-z]*: 在某个特定行上被证明为 project:,则“和”号将包含该值。这会非常有用。看一下以下这个示例:
	sed s/[a-z]*:/new_\&/g sed.txt
	这次,您修改了每个匹配字符串,但是保留了与每个变量关联的标识符:
system "echo 'new_project:$project' >> logfile";
system "echo 'new_version:$version' >> logfile";
system "echo 'new_optionalid:$optionalid' >> logfile";
system "echo 'new_nodes:$nodes' >> logfile";
system "echo 'new_threads:$threads' >> logfile";
4.7.	执行多个命令序列
	使用 sed,您还可以一次做多件事情。若要一次执行多个命令序列,您必须在每个表达式前使用 -e 标志。缺省情况下,sed 将第一个参数解释为一个表达式,但是在运行多个命令时,您需要作出更明确的指定,因此要使用 -e 标志。例如:
	sed -e s/[a-z]*:/value:/g -e s/logfile/name/g sed.txt
	可以在此例中看到,sed 在适当位置插入了 value: 和 name:
system "echo 'value:$project' >> name";
system "echo 'value:$version' >> name";
system "echo 'value:$optionalid' >> name";
system "echo 'value:$nodes' >> name";
system "echo 'value:$threads' >> name";
	正如您开始看到的,在大规模批处理过程中,sed 可以是个非常强大的文件编辑工具。在前一示例中,您是在对单个文件进行操作,就像在使用 grep 时所做的那样。不要忘了,这些实用程序的部分强大功能在于跨多个文件运行它们,这可以使用通配符或文件列表替换单个文件来实现,您已在本教程中这样使用过了。
5.	在命令行上使用 awk
	本教程首先对正则表达式进行了基本的解释,然后介绍了 grep 和 sed。grep 是一个强大的搜索实用程序,而 sed 则是一个更加强大的搜索和替换实用程序。awk 则更进一步,它在全功能的命令行编程语言中使用正则表达式。正如 sed 一样,当在命令行上使用 awk 时,它接受基于行的输入。awk 一次解释一行输入,但是与 sed 不同,它将该行上的每个部分作为变量来处理,这些变量可用作内联代码的输入和输出。
	应该指出的是,AWK(大写)是一个可用于编写脚本(而不只是在命令行上使用)的全功能编程语言,但本教程集中于 awk,后者是动态解释 AWK 命令的命令行实用程序。
	顺便提一下,任何人阅读到这里都会考虑如何实际运用所学到的知识,我刚才就使用 grep 在某些旧代码中搜索理想的 awk 示例:
	grep awk */*.pl
	大多数系统管理员或程序员每天都会看到这些工具的应用。下面是我的输出中的一些行:
	Edaemon/m_checkcurrentdisk.pl:$freespace = `awk '(NR==1) {print \$4 / 1024 / 1024}' grep.tmp`;
|-------- XML error:  The previous line is longer than the max of 90 characters ---------|
Edaemon/m_getdatetime.pl:$month = `awk '(NR==1) {print \$2}' datetime.txt`;
Odaemon/odaemon.beowulf.dvd.pl:$filesize = `awk '(NR==1) {print \$1}' temp.txt`;
	这些是非常好的示例,因为它们说明了非常基础的 awk 应用。对于您的第一次尝试,甚至可以使它更简单。对于您的 awk 测试,请在一个空目录中创建以下文件(每个文件的内容无关紧要,并且它们可以是空的)。
Screenshot_1.jpg
Screenshot_2.jpg
Screenshot_3.jpg
awk.txt
regular.txt
sed.txt
5.1.	使用 ls 的输出作为 awk 的输入
	缺省情况下,awk 读取输入文件中的每一行,并将内容分离为由空格确定的变量。在非常简单的示例中,您可以使用 ls 的输出作为 awk 的输入并打印结果。此示例结合使用管道字符 (|) 和 ls 来将输出发送到 awk:
	ls | awk ' { print $1 } '
	awk 随后打印每行上的第一项,在此例中为每行上的唯一项:
Screenshot_1.jpg
Screenshot_2.jpg
Screenshot_3.jpg
awk.txt
regular.txt
sed.txt
5.2.	使用 ls -l 来为 awk 生成多列输入
	这确实是非常基本的功能。对于下一个示例,请使用 ls -l 来为 awk 生成多列输入:
	ls -l
	不同系统的 ls 实现稍有差别,下面是一些示例输出:
	total 432
	-rw-rw-rw-   1 guest  guest  169074 Oct 15 14:51 Screenshot_1.jpg
	-rw-rw-rw-   1 guest  guest   23956 Oct 15 20:56 Screenshot_2.jpg
	-rw-rw-rw-   1 guest  guest   12066 Oct 15 20:57 Screenshot_3.jpg
	-rw-r--r--   1 tuser  tuser     227 Oct 15 20:16 awk.txt
	-rw-r--r--   1 tuser  tuser     233 Oct 15 19:35 regular.txt
	-rw-r--r--   1 tuser  tuser     227 Oct 15 23:16 sed.txt
	请注意,文件所有者是每行上的第三个项,文件名是每行上的第九个项(缺省情况下,awk 中的项之间用空格分隔)。通过打印每行上的第三和第九个变量,您可以使用 awk 来从该列表提取文件所有者和文件名。下面是完成这项工作的命令:
	ls -l | awk ' { print $3 " " $9 } '
	您将注意到,awk 中的 print 命令有两个引号,而且引号中有一个空格。这只是为了在输出的文件所有者和文件名之间打印一个空格:
guest Screenshot_1.jpg
guest Screenshot_2.jpg
guest Screenshot_3.jpg
tuser awk.txt
tuser regular.txt
tuser sed.txt
	您可以在 awk 打印语句中的变量之间的引号中放置任何文本。
5.3.	使用正则表达式来指定行
	现在您已经学习了如何使用 awk 的基础知识,但本教程不是关于正则表达式的吗?awk 中大量使用了正则表达式。最常见的示例是在 awk 命令前附加一个正则表达式,用于指定您想要操作的行。与 sed 一样,awk 中的正则表达式位于两个正斜杠之间。例如,如果您只希望操作 tuser 所拥有的文件,则可以使用以下命令:
	ls -l | awk ' /tuser/ { print $3 " " $9 } '
	该命令产生以下输出:
	tuser awk.txt
	tuser regular.txt
	tuser sed.txt
5.4.	更改文件扩展名
	在另一个示例中,您可能希望更改每个文本文件的文件扩展名而不更改图像文件。为此,您将需要使用句点而不是空格来分隔输入变量,然后使用正则表达式来指示您仅希望搜索文本文件。若要基于句点来分隔变量,可以使用 -F 标志,后跟后跟您希望使用的字符(用引号引起来)。尝试此示例,通过管道将 awk 输出发送到某个 Shell(此 Shell 将执行 awk 生成的命令):
	ls –l  | awk -F"." ' /txt/ { print "mv " $1 "." $2 " " $1 ".doc" } ' | bash
	后续的 ls -l 将显示新的文件名:
-rw-rw-rw-   1 guest  guest  169074 Oct 15 14:51 Screenshot_1.jpg
-rw-rw-rw-   1 guest  guest   23956 Oct 15 20:56 Screenshot_2.jpg
-rw-rw-rw-   1 guest  guest   12066 Oct 15 20:57 Screenshot_3.jpg
-rw-r--r--   1 tuser  tuser     227 Oct 15 20:16 awk.doc
-rw-r--r--   1 tuser  tuser     233 Oct 15 19:35 regular.doc
-rw-r--r--   1 tuser  tuser     227 Oct 15 23:16 sed.doc
	记住,这些只是 awk 的入门基础知识,但是 AWK 是一种全功能的编程语言,所具有的功能远远超出了本教程所介绍的内容。请查看一下 awk man 页。如果您希望学习更多的知识,花钱买一本优秀的图书是明智的。
5.5.	总结
	本教程中的示例应该足以使您基本了解使用正则表达式的 UNIX 过滤器以及如何在命令行上使用它们。所使用的三个实用程序 grep、sed 和 awk 都具有众多的内置选项和功能,这些内容大大超出了本教程讨论的入门课程的范围。存在一些专门探讨 sed 和 awk 的书籍。请仔细搜索一遍关于 grep 的 man 页以了解有关其强大功能的更多信息。
	如果您觉得自己已掌握了正则表达式的基础知识,并希望进行下一步的学习,可以考虑学习 Perl,它是另一种充分利用了正则表达式的卓越语言。不熟悉的用户在掌握 Perl 以后,原本似乎没有意义的字符串竟变成了紧凑而高效的代码行,真的很有意思!
	如果您学习了本系列中的每个教程,那么您现在已经学会了如何在命令行上操作基本文件、如何使用 vi 文本编辑器和如何使用命令行过滤器。 
	请密切关注本系列中的下一个教程,其中将介绍 Shell 诀窍和技巧。与此同时,您还应该竭尽所能地学习正则表达式和本教程中的实用程序。它们使您可以将很长的复杂任务转换为值得骄傲的快捷、一流的解决方案!





UNIX 新手指南,第4部分: Shell 诀窍
利用这些强大的技术来学习编写脚本的基础知识
	当编写 Shell 程序时,您通常会遇到一些特殊的情况,希望采用自动方式处理。本教程包括一些关于此类情况的 Bourne Shell 脚本示例。这些情况包括字符串的进制转换(十进制到十六进制、十六进制到十进制、十进制到八进制,等等)、在管道循环中读取键盘、Subshell 执行、内联输入、为目录中的每个文件执行一次命令,以及使用多种方法构造连续循环。本系列文章的第 4 部分总结了一批执行有用功能的 Shell 单命令行程序。
开始之前
了解本教程中包含的内容以及如何最好地利用本教程。
关于本系列
本系列教程主要针对新用户撰写,简要介绍 UNIX? 基本概念。本系列教程的前三篇文章站在拥有 Microsoft? Windows? 背景的新用户的角度重温了一遍 UNIX 系统,讲述了文件系统和常用命令,介绍了 vi(最常见的 UNIX 编辑器),并且通过使用 grep、sed 和 awk 工具简要介绍了筛选器和正则表达式。
关于本教程
本教程介绍了一套新用户易于掌握的诀窍和技巧。说明在特定情况下,如何使用在 Bourne Shell 中编写的小脚本自动执行操作,包括自动执行进制转换、读取键盘输入、在 Subshell 中执行命令、为目录中的所有文件执行相同命令,以及多种形式的循环。本教程最后以一套实用的 Shell 单命令行程序作为结束。
目标
本教程的目标是向新用户介绍如何使用和实现许多在各种级别上提供自动化操作的Shell 方法。本教程通过提供针对特定情况的诀窍和技巧来说明这些方法,并且提供适用于常见任务的 Shell 单命令行程序的概要性介绍。
先决条件
本教程面向相对不熟悉 UNIX 的用户。唯一的先决条件是了解 UNIX 文件系统的基本知识和操作命令、命令行本身,以及能够使用类似 vi 的编辑器编写文本文件。本系列教程的前面部分对这些概念作了全面说明。
系统要求
您需要在带有 Bourne 兼容 Shell 环境(例如 bash)的 UNIX 系统上拥有用户级访问权限。这是本教程唯一的系统要求。
Shell 命令执行
学习 Shell 脚本的最佳方法是通过示例。对于您要在脚本中执行的任何命令都可以在命令行上立即尝试,这也是本教程通篇提供大量实践示例的原因所在。例如,echo 命令将一行文本写入到标准输出。(许多 Shell 以内置命令形式提供其自己版本的 echo 命令,包括 IBM AIX? 的 Bourne Shell 实现。如果这也是您的现实情况,那么当您运行 echo 时,实际上正在运行您的 Shell 版本的命令。)
引用
尝试在使用 echo 输出短消息时加引号:
$ echo "Hello, world"
Hello, world
Shell 引用(无论在命令行还是在脚本中加注)是一种将字符串传递给 Shell 的方法,可以避免对字符串中可能包含的任何特殊元字符产生混淆。当字符串包含一个以上的单词或者段落包含空格字符时使用引用。如果单个字符恰好是 Shell 元字符,并且您想去除它的特殊含义,就可以在两边加上引号,例如,当您要传递一个美元符号 ($) 作为字面上的美元符号字符而不是作为变量名前的特殊元字符时。
在引用的文本内部发生各种扩展。例如,在双引号括起来的文本中,变量被展开为它们的值,而单引号括起来的文本内部引用的变量名则不展开。
有三种重要的引用类型需要了解:
1、通过在前面加反斜杠 (\) 引用单个字符。这样只会传替字符的字面含义,而非它可能包含的任何特殊含义,比如空格符或 Shell 元字符。例如,使用 \* 引用一个星号 (*),它是 Shell 元字符。要引用真正的反斜杠字符,可以使用 \\。
2、通过在文本字符串两边加双引号 (") 来传递扩展的引用。美元符号 ($) 和单引号 (') 字符会保留其自身含义。因此,和其他字符一起在引用中出现的任何变量名都会被它们的值所替代。新行或特定字符 ($`"\) 前的反斜杠被移除,但是引用的字符会被传递。
3、使用单引号 (') 将文本括起来以传递文本字符串的字面引用,所有的变量名、元字符等都作为文字字符,而不它们的含义或值来传递。
请注意在不同的 Shell 中引用的确切规则会有所区别。参考您所使用的特殊 Shell 的 man 页面来了解准确规则。
分配一个变量,然后尝试使用各种引用格式输出该变量,如清单 1 中所示。
清单 1. 使用 echo 演示 Shell 变量引用格式
$ myvar = "Hello, world"
$ echo $myvar
Hello, world
$ echo "$myvar"
Hello, world
$ echo '$myvar'
$myvar
$ echo \$myvar
$myvar
$ echo \'$myvar\'
'Hello, world'
$ echo "'$myvar'"
'Hello, world'
$ echo '"$myvar"'
"$myvar"
$ echo \"$myvar\"
"Hello, world"
注意解释变量的方式取决于所使用的引用格式。
注释
在 Shell 中,以井号 (#) 开始一个注释行。井号及其后面跟随的同一行的所有内容都被忽略。尝试输入几行夹杂注释的文本,如清单 2 中所示:
清单 2. 在 Shell 中使用注释
$ # a comment does nothing
$ echo "Hello, world" # This text is ignored
Hello, world
$ echo # This will not output

$ echo 'But a hash (#) can be quoted'
But a hash (#) can be quoted
$ echo "# Even in double quotes"
# Even in double quotes
$
创建 Shell 脚本
正如您所看到的,您可以直接在命令行测试这些 Shell 编程结构。但是,当您完成了单行命令的学习并且真正开始构建更长的程序时,您需要将程序写入称为脚本的文件。脚本 是一个设置了可执行位的文本文件,并且包含由 Shell 语言命令组成的程序。UNIX Shell是一种解释性语言,这意味着它的程序不经过编译,而是由解释器读取,解释器本身是Shell可执行程序,比如 /bin/sh、/bin/bsh 或 /bin/bash。
Shell 脚本的第一行通常都是相同的:
#!/bin/sh
这是 Shell 自己使用的一种特殊注释,用于确定文件的语言或目录。感叹号在 UNIX 和排版术语中常常被称为 bang,后面跟随的路径名告诉 Shell 应该使用来执行该文件的解释器。在本例中是 /bin/sh,它在许多系统中代表 Bourne Shell 可执行程序本身。举例来说,特别为 Korn Shell 编写的脚本应该以 #!/usr/bin/ksh 开始,正如 Ruby 脚本将以 #!/usr/bin/ruby 开始。安装 bash 之后,/bin/sh 通常是到 bash 二进制程序的符号链接。并且考虑到兼容性,使用 /bin/sh 比使用 /bin/bash 更可取。在一些系统中,比如 IBM AIX 5L?,Bourne Shell 可执行程序的名称是 bsh,并且位于 /usr/bin/bsh。清单 3 提供了 Shell 脚本的简短示例。
清单 3. Shell 脚本示例
#!/bin/sh
# This is a shell script
message = "Hello, world!"
echo "The message is '"$message"'"
按照本系列教程前面文章中的说明,使用 vi 编辑器键入该脚本并保存到名为 myscript 的文件中(请参见参考资料部分)。然后使用 chmod 设置该文件的执行权限,使该文件可以执行:
$ chmod u+x myscript
此命令使该文件只能由您执行。如果希望系统中的所有用户都能执行该文件,那么您还可以为所有用户设置执行权限:
$ chmod a+x myscript
现在您可以运行该脚本。给出该文件的名称和相对于当前工作目录的路径,在路径中使用一个点字符 (.) 来表示:
$ ./myscript
The message is 'Hello, world!'
$
Shell 变量 PATH 包含一组以冒号分隔的目录。它就是您的路径,Shell 总是会“看到”这些目录中的所有文件。UNIX Path 的目的是为了便于运行二进制文件。这就是为什么您只需要键入命令的基本文件名,比如 ls 和 echo,而不用提供它们的完整或相对路径名。如果您将脚本移动到 Path 中的目录,那么只需键入它的名字就可以运行。具体的 Path 取决于您的 UNIX 实现和本地设置,但 Path 中的目录通常包括 /bin、/usr/bin 和 /usr/local/bin。
一些用户对它们的 Shell 进行配置,从而使 PATH 变量包括当前的工作目录,这在 Path 中以点字符 (".") 表示。如此一来,要在当前目录下运行脚本,只需要键入它的名称,不需要指出相对目录。,Shell 按给定的顺序搜索 Path中的目录,从而避免中木马或发生异常情况,一种极其不明智的做法是把当前工作目录放在 Path 的末尾。
要查看您的 Path,可以使用 echo 显示 PATH 变量的内容,如清单 4 所示。
清单 4. 更改 PATH
                    
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11
$ myscript
myscript: command not found
$ PATH = $PATH":."
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:.
$ myscript
The message is 'Hello, world!'
$
在解释器名称的后面可以附加特殊选项或标志,比如 /usr/bin/bsh -n,这用于调试目的。连字符关闭选项,加号则打开选项。特殊的内置环境变量 -(一个连字符)包含当前 Shell 的完整选项列表。
尝试在您当前的交互式 Shell 中设置了哪些选项。通过使用 echo 显示 - 变量的内容来完成这项任务:
$ echo $-
himBH
$
参考您使用的 Shell 的 man 页面来获取当前的标志和选项列表。表 1 提供了 AIX? 上的 Bourne Shell 的常用标志列表,以及对每种标志作用的简要说明。
表 1. AIX Bourne Shell 的常用选项
标志 描述 
-a  导出所有已分配值的变量。 
-c Variable  执行从变量 中读取的命令。 
-e  当命令满足以下条件之一时立即退出:命令退出时返回比 0 大的值;命令不是 while、until 或 if 结构的一部分;命令不经过 AND 或 OR 检测;或者命令不是管道前加感叹号。 
-f  禁用所有文件名替换。 
-h  定义函数时,定位和记住函数内部调用的所有命令。 
-i  指定交互式 Shell。 
-k  将所有关键字 都放入命令的环境。 
-n  读取命令,但是不执行它们。 
-r  调用受限制的 Shell。 
-s  从标准输入读取命令,然后将输出写入标准错误(不包括 Shell 内置命令的输出)。 
-t  读取并执行单个命令,然后退出。 
-u  在脚本中,将所有未定义 变量视为错误。当尝试变量替换时退出。 
-v  当读取输入行时将其显示出来。 
-x  在执行命令之前显示其完整命令(包括所有的参数和选项)。 
Shell 运算和进制转换
Shell 提供大量的基本运算操作,在脚本中非常有用。Shell 对您提供的算术表达式求值,执行运算展开式,此时使用得出的结果替换表达式。以下面的格式提供运算表达式:
$(( expression ))
您可以使用 echo 在命令行显示运算展开式的结果,了解其工作情况。现在尝试清单 5 所显示的结果。
清单 5. Bourne Shell 中的运算展开式
$ echo $((10+40))
50
$ echo $((5*(3+3)))
30
您还可以将展开式分配给变量。尝试清单 6 所显示的结果。
清单 6. 将运算展开式分配给 Shell 变量                    
$ myvar = 10
$ echo $myvar
10
$ echo $(($myvar-2))
8
$ myvar = $(($myvar+5))
$ echo $myvar
15
$ result = $(($myvar-10))
$ echo $result
5
$

表 2 列出了在大多数 Bourne 以及与 Bourne 兼容的 Shell中可以使用的运算符。正如上面第二个示例,使用圆括号括起来的语句有更高的优先级。实际上,Shell 算术优先级通常根据 C 语言的规则来确定。

表 2. Shell 条件表达式
运算符 描述 
+  加 
-  减 
*  乘 
/  除 
%  求余 
<  小于(1 代表真,0 代表假) 
<=  小于等于(1 代表真,0 代表假) 
>  大于(1 代表真,0 代表假) 
>=  大于等于(1 代表真,0 代表假) 
<<  按位向左移位:将给定的整数或第一个表达式向左移动第二个表达式表示的位数 
>>  按位向右移位:将给定的整数或第一个表达式向右移动第二个表达式表示的位数 
使用 Shell 运算进行进制转换
假定在您的脚本中有一些数字,您需要以另外的进制处理这些数字。使用 Shell 运算可以很容易地自动实现这类转换。一种情况是使用 Shell 运算把一个数字从给定的进制转换位十进制。如果数字以运算展开式的形式提供,那么假定它带有十进制符号,除非它前面带有 0(这种情况假定是八进制)或 0x(这种情况假定是十六进制)。键入以下内容以得到一些八进制和十六进制值的十进制输出:

$ echo $((013))
$ echo $((0xA4))
您还可以使用以下格式指定 2 到 64 之间的任意进制:

$((BASE#NUMBER))
通过在 Shell 提示符后键入清单 7 中所示的行,尝试将二进制、八进制、十六进制以及其他进制的数转换为十进制。
清单 7. 在 Shell 中将任意进制的数以十进制输出
echo $((2#1101010))
echo $((8#377))
echo $((16#D8))
echo $((12#10))
echo $((36#ZZYY))

使用 bc 进行进制转换 
在 Shell 中进行进制转换的另一个诀窍是使用 bc,它是一种任意精度运算语言,大多数 UNIX 安装程序都提供。因为它允许您指定输出进制,所以当您需要以十进制以外的进制输出时,这是一种很好的技术。
bc 的特殊变量 ibase 和 obase 分别包含用于输入和输出的进制的值。缺省情况下,都被设置为 10。要执行进制转换,需要改变其中的一个或两个值,然后提供一个数字。立即尝试,如清单 8 中所示。

清单 8. 使用 bc 执行进制转换
                    
$ bc -ql
10
10
obase=16
10
A
ibase=2
10
2
Control-D
$
要快速执行进制转换,可以联合使用 bc 和 echo形成快捷的单命令行程序,将给定的值通过管道传输给 bc。键入清单 9 中显示的内容。
清单 9. Shell 单命令行 bc 程序
$ echo 'obase=16; 47' | bc
2F
$ echo 'obase=10; ibase=16; A03' | bc
2563
$
警告:当您设置 bc 的输入进制以后,输入 bc 的所有数字都使用该进制,包括您提供用于设置输出进制的数字。因此最好先设置输出进制,否则可能会产生意想不到的结果,如清单 10 中所示。
清单 10. 设置输入和输出进制的先后顺序的重要性
                    
$ echo 'ibase=16; obase=10; A' | bc
A
$ echo 'ibase=16; obase=A; A' | bc
10
$
内联输入
尽管 echo 通过管道将内容传递给交互式命令(比如 bc)可以生成快捷的单命令行程序,但是它对于多行输入并不适用,比如可能用到实际文件中的内容。但是另外一种有用的方法可以完成这个任务。Shell 有一种工具称为 here documents 或内联输入,这是一种动态构建文件的非常好的方法,比如用于脚本内部,并且将该文件的内容重定向到一个命令。
使用 Shell << 操作符来指定一个 here document,然后在同一行的后面跟上一个限定字符串,该字符串标记输入的结束,并且您可以选择任何文本,只要是不包含空格字符的单个词都可以。其后跟随构成您的输入文件的行,然后以独占一行的限定字符串结束输入,在它的前面或后面不能有任何文本,否则该行将被视为输入的一部分。使用 cat 进行尝试,如清单 11 中所示。
清单 11. 编写 here document
$ cat << END
>  END of input text
> ENDspace
                    
> This is still not the END
> ENDING SOON
> THE END
> END
 END of input text
END 
This is still not the END
ENDING SOON
THE END
$
限定字符串(本例中是 END)可以出现在输入的任何地方,只有当它以独占一行并且不含空格或其他字符的形式出现时,才表示输入的结束。
脚本中的内联输入
在脚本中经常使用内联输入将使用信息输出到标准输出。这通常通过将 here document 发送给 cat 来完成,如清单 12 中的脚本所示。使用 vi 输入该脚本并保存到名为 baseconv 的文件中,并且将该文件设置为可执行文件(请参见创建 Shell 脚本部分)。
清单 12. 使用 here document 提供 Shell 脚本使用信息
#!/bin/sh
cat << EOF
baseconv is a program to convert a number from one base to another.

Usage: baseconv [options]

Options:

-i	BASE input base
-o	BASE output base
-h	display this message

For more information, consult the baseconv man page.
EOF
 
当执行该脚本时,here document 的内容被发送到(使用 cat)标准输出。立即尝试,如清单 13 中所示。
清单 13. 从 here document 输出 Shell 脚本使用信息
$ baseconv
baseconv is a program to convert a number from one base to another.

Usage: baseconv [options]

Options:

-i	BASE input base
-o	BASE output base
-h	display this message

For more information, consult the baseconv man page.
$
此外,Bourne Shell 的大多数实现允许出现使用可选的连字符重定向的内联输入。可选的连字符将所有的前导 Tab 字符从所有输入行的前面去掉,也包括包含限定字符串的行。这对于您希望让编写的脚本保持当前缩进时会有帮助。由于内联输入通常逐字读取,并且限定字符串必须在行的开始处给出,因此输入将打乱您的当前缩进并使脚本看起来不雅观。因此,您可以重写清单 12 中的脚本,使其与清单 14 一致,而输出不会改变。
清单 14. 带前导缩进的 Shell 脚本 here document
#!/bin/sh

	cat <<- EOF
		baseconv is a program to convert a number from one base to another.

		Usage: baseconv [options]

		Options:

		-i	BASE input base
		-o	BASE output base
		-h	display this message

		For more information, consult the baseconv man page.
		EOF
在命令行使用内联输入
在命令行中,使用调用交互式程序的单命令行程序进行内联输入,比如在使用 bc 进制转换部分讨论的 bc 计算程序。在任意交互式命令中,您可以使用 here document 代替实际文件,或代替任意行的实际输入。尝试使用 here document 将多行输入发送到 bc。键入清单 15 中显示的内容。
清单 15. 将内联输入发送到交互式程序
$ bc << EOF
> ibase=16
> A
> EOF
10
$
通常使用内联输入来扩展变量。尝试清单 16 中显示的内容。
清单 16. 内联输入如何扩展变量
$ BASECON=16
$ bc << EOF
> ibase=16
> $BASECON
> EOF
22
$
Subshell 执行
可以在一个名为 subshell 的新 Shell 中执行一个或一组命令,当前 Shell 是 SubShell 的父 Shell。Subshell 继承父亲的环境。I/O 重定向可以出现在子 Shell 和父 Shell 之间,但是 Subshell 永远不能修改父环境。当您为了执行这些命令(比如设置变量)要更改 Shell 的环境,并且不想更改脚本自身运行所在的环境时,这就是您所期望的技术。当您想要同时在后台启动多个长时间运行的进程时也最好使用 Subshell。一个 Shell 可以生成多个 Subshell,而 Subshell 又可以循环生成属于它们自身的任意数量的 Subshell。图 1 说明了这个过程。
图 1. Subshell 如何与它的父 Shell 交互
 
Shell 有时自动生成自身的 Subshell,比如在管道中使用内置命令时。在 Subshell 中,Shell $ 参数扩展到父Shell 而不是 Subshell 的进程 ID (PID)。
在 Subshell 中运行命令
要在 Subshell 中运行一组命令,可以使用括号将其括起来。您可以使用重定向将输入发送到 Subshell 的标准输入,或将 Subshell 的集合输出发送到文件或管道。
尝试在您的 home 目录键入清单 17 中显示的内容。该示例创建一个 example 目录和一些测试文件,前提是原来不存在 example 目录。
清单 17. 在 Subshell 中创建一组文件
$ pwd
/home/user
$ (mkdir example; cd example; touch A B C)
$ pwd
/home/user
$ cd example; ls
A B C
$ pwd
/home/user/example
$
在本例中,Shell 生成一个在后台运行的 Subshell,建立 example 目录,然后使用 touch 在该目录中生成三个虚拟文件。同时,Shell 返回 home 目录的命令行。
当您有一组执行时间长的命令时,在命令行和脚本中使用 Subshell 都很方便。为了让 Shell 保持空闲,您可以在后台运行 Subshell,或者在后台运行许多个 Subshell。
( group-of-long-running-commands ) &
( another-group-of-long-running-commands ) &
( yet-another-group-of-long-running-commands ) &
Subshell 和变量
理解变量与 Subshell 的交互方式非常重要。因为 Subshell 环境是其父亲的副本,所以它继承了父亲的所有变量。但是父 Shell 从不会看到 Subshell 环境发生的任何变化,同样,Subshell 生成以后,再也不会看到父亲发生的任何变化。
作为示例,使用 vi 编辑器将清单 18 中的脚本保存到 home 目录的 vartest 文件中,然后将其设置为可执行(请参见编写 shell 脚本部分)。
清单 18. 演示 Subshell 中变量行为的 Shell 脚本
#!/bin/sh
# Demonstrates variable behavior in a subshell environment

VAR=10

echo "VAR is" $VAR

(
echo "In the subshell, VAR is still" $VAR
VAR=$(($VAR+5))
echo "The new value of VAR in the subshell is" $VAR
)
echo "Outside of the subshell, VAR is" $VAR
现在尝试通过键入脚本的名称来执行它,如清单 19 中所示。
清单 19. vartest 脚本的输出
$ vartest
VAR is 10
In the subshell, VAR is still 10
The new value of VAR in the subshell is 15
Outside of the subshell, VAR is 10
$
连续循环
现在来看循环,它允许您执行重复任务,比如对一组文件执行一些操作或命令。Shell 有几种构造循环的方法。
构造 for 循环
最常见的循环结构是 for 循环。首先定义一个变量作为循环的名称,提供一组成员,可以是包括整数和文件名在内的任何单词,然后提供每次重复执行的命令。每个命令都以分号结束 (;),整个命令组以位于单词 do 和 done 之间。清单 20 描述了它的结构。
清单 20. Shell 中循环的结构
for loopname in members
do
	command;
	command;
	...
	command;
done
在循环的第一次重复中,loopname 变量获取第一个成员的值。然后 loopname 的值被清单中下一个成员的值替代,接下来它继续重复直到遍历所有成员。
在大多数 Shell 中,do 和 done 都可以被大括号所替代,如清单 21 中所示。
清单 21. Shell 循环的替代结构
for loopname in members
{
	command;
	command;
	...
	command;
}
键入清单 22 中的文本来运行包含三个成员的简单循环:
清单 22. 使用循环来改变变量的值
                    
$ for i in 1 2 3
> {
> VAR = $(($VAR+$i))
> echo $i:$VAR
> }
1:1
2:3
3:6
$
针对目录中的每个文件执行命令
您可以使用循环针对给定的一组文件执行一个或一组命令。如果您提供文件的名称作为 for 循环的成员,那么循环按您提供名称的顺序在每个文件上执行操作。您可以两次提供同一个文件,循环将依次对该文件执行操作。在您的 example 目录中尝试使用清单 23 中的文本执行上述操作。
清单 23. 利用一组文件构造循环
$ cd ~/example
$ ls
A B C
$ for file in C B B C
> {
> echo $file
> }
C
B
B
C
$
要对同一目录下的所有文件执行操作,可以使用星号 (*) 作为循环的唯一成员,如清单 24 中所示。Shell 将星号扩展为目录中的所有文件。然后,对于循环中您要对所有文件执行的命令,使用 loopname 变量作为合适的参数或选项。
清单 24. 针对目录中的所有文件执行同一命令
$ ls
A B C
$ for file in *
> {
> mv $file $((0x$file))
> }
$
如果您正在运行本教程中的所有示例,那么您的 example 目录中的内容应该已改变:
$ ls
10 11 12
$
发生的情况是循环中的 mv 命令将文件的名称从十六进制值(通过在名称的前面插入 0x 构成)更改为与它相等的十进制值。
构造 while 循环
您可以构造一种当满足某些条件就一直运行的循环。使用 while 条件语句来实现这一目标,其格式如清单 25 所示。
清单 25. Shell while 循环的结构
while [ condition ]; do
	command;
	command;
	...
	command;
done
在循环中,condition 可以是使用操作符(请参见表 3)构建的语句,或者可以像一个变量名那样简单。只要值是非 0 的,就代表真。
表 3. 常用 Shell 操作符
操作符 描述 
-eq  等于 
-ne  不等于 
-lt  小于 
-le  小于等于 
-gt  大于 
-ge  大于等于 
构造 while 循环时,有一些注意事项需要牢记在心。首先,在条件与将它括起来的括号之间必须留有空白字符。其次,如果在条件中将变量用于数字比较,那么在 while 语句之前必须首先定义该变量。
键入清单 26 中的文本以执行一个简短的 while 循环:
清单 26. 使用 while 循环更改变量
$ VAR=0
$ while [ $VAR -lt 10 ]; do
>   echo $VAR;
>   VAR=$(($VAR+1));
> done
0
1
2
3
4
5
6
7
8
9
$
构造 until 循环
until 条件语句与 while 相似并使用相同的操作符,但是它们的行为相反。它只有当条件为假时才执行循环,并且循环持续重复直到 给定的条件为真。它的格式在清单 27 中说明。
清单 27. Shell until 循环的结构
until [ condition ] ; do
	command;
	command;
	...
	command;
done
通过键入清单 28 中所示的内容尝试运行一个简短的 until 循环:
清单 28. 使用 until 循环更改变量
                    
$ VAR=10
$ until [ $VAR -eq 0 ]; do
>   echo $VAR;
>   VAR=$(($VAR-1));
> done
10
9
8
7
6
5
4
3
2
1
$
嵌套多重循环
您可以嵌套循环和组合多种类型的循环来执行各种类型的复杂操作。由于 for 循环的成员不必是数字或以任意类型的顺序排列,因此您可以使用稍后在某个内部循环中作为命令执行的命令名称作为其成员,比如 printf、echo、stop、resume,等等。
尝试运行清单 29 中的示例。这是一个执行算术替换的 until 循环,同时嵌套在循环词未按数字顺序排列的 for 循环内部。
清单 29. 使用嵌套循环进行算术替换
$ for i in 250 100 2136 875
>  {
>    VAR=10;
>    until [ $VAR -eq 0 ]; do
>      echo "$i / $VAR = $(($i/$VAR))  $i * $VAR = $(($i*$VAR))\
             $i + $VAR = $(($i+$VAR))  $i - $VAR = $(($i-$VAR))";
>      VAR=$(($VAR-1);
>    done;
>  }
250 / 10 = 25  250 * 10 = 2500  250 + 10 = 260  250 - 10 = 240
250 / 9 = 27  250 * 9 = 2250  250 + 9 = 259  250 - 9 = 241
250 / 8 = 31  250 * 8 = 2000  250 + 8 = 258  250 - 8 = 242
250 / 7 = 35  250 * 7 = 1750  250 + 7 = 257  250 - 7 = 243
250 / 6 = 41  250 * 6 = 1500  250 + 6 = 256  250 - 6 = 244
250 / 5 = 50  250 * 5 = 1250  250 + 5 = 255  250 - 5 = 245
250 / 4 = 62  250 * 4 = 1000  250 + 4 = 254  250 - 4 = 246
250 / 3 = 83  250 * 3 = 750  250 + 3 = 253  250 - 3 = 247
250 / 2 = 125  250 * 2 = 500  250 + 2 = 252  250 - 2 = 248
250 / 1 = 250  250 * 1 = 250  250 + 1 = 251  250 - 1 = 249
100 / 10 = 10  100 * 10 = 1000  100 + 10 = 110  100 - 10 = 90
100 / 9 = 11  100 * 9 = 900  100 + 9 = 109  100 - 9 = 91
100 / 8 = 12  100 * 8 = 800  100 + 8 = 108  100 - 8 = 92
100 / 7 = 14  100 * 7 = 700  100 + 7 = 107  100 - 7 = 93
100 / 6 = 16  100 * 6 = 600  100 + 6 = 106  100 - 6 = 94
100 / 5 = 20  100 * 5 = 500  100 + 5 = 105  100 - 5 = 95
100 / 4 = 25  100 * 4 = 400  100 + 4 = 104  100 - 4 = 96
100 / 3 = 33  100 * 3 = 300  100 + 3 = 103  100 - 3 = 97
100 / 2 = 50  100 * 2 = 200  100 + 2 = 102  100 - 2 = 98
100 / 1 = 100  100 * 1 = 100  100 + 1 = 101  100 - 1 = 99
2136 / 10 = 213  2136 * 10 = 21360  2136 + 10 = 2146  2136 - 10 = 2126
2136 / 9 = 237  2136 * 9 = 19224  2136 + 9 = 2145  2136 - 9 = 2127
2136 / 8 = 267  2136 * 8 = 17088  2136 + 8 = 2144  2136 - 8 = 2128
2136 / 7 = 305  2136 * 7 = 14952  2136 + 7 = 2143  2136 - 7 = 2129
2136 / 6 = 356  2136 * 6 = 12816  2136 + 6 = 2142  2136 - 6 = 2130
2136 / 5 = 427  2136 * 5 = 10680  2136 + 5 = 2141  2136 - 5 = 2131
2136 / 4 = 534  2136 * 4 = 8544  2136 + 4 = 2140  2136 - 4 = 2132
2136 / 3 = 712  2136 * 3 = 6408  2136 + 3 = 2139  2136 - 3 = 2133
2136 / 2 = 1068  2136 * 2 = 4272  2136 + 2 = 2138  2136 - 2 = 2134
2136 / 1 = 2136  2136 * 1 = 2136  2136 + 1 = 2137  2136 - 1 = 2135
875 / 10 = 87  875 * 10 = 8750  875 + 10 = 885  875 - 10 = 865
875 / 9 = 97  875 * 9 = 7875  875 + 9 = 884  875 - 9 = 866
875 / 8 = 109  875 * 8 = 7000  875 + 8 = 883  875 - 8 = 867
875 / 7 = 125  875 * 7 = 6125  875 + 7 = 882  875 - 7 = 868
875 / 6 = 145  875 * 6 = 5250  875 + 6 = 881  875 - 6 = 869
875 / 5 = 175  875 * 5 = 4375  875 + 5 = 880  875 - 5 = 870
875 / 4 = 218  875 * 4 = 3500  875 + 4 = 879  875 - 4 = 871
875 / 3 = 291  875 * 3 = 2625  875 + 3 = 878  875 - 3 = 872
875 / 2 = 437  875 * 2 = 1750  875 + 2 = 877  875 - 2 = 873
875 / 1 = 875  875 * 1 = 875  875 + 1 = 876  875 - 1 = 874
$
读取键盘输入
您还可以在脚本中或从命令行本身读取键盘输入。使用 read 命令可以实现这一功能,这是一个内置函数,将任意数量的变量名作为参数。它从标准输入读取变量的值,读入单行输入并将各个输入词分配给各个变量。
尝试读取一个变量,如清单 30 中所示:
清单 30. 使用 read 读取一个变量
$ read VAR
23
$ echo $VAR
23
$
使用 -p 选项为每次 read 提供提示。使用以引号括起来的字符串提供提示,如清单 31 中所示。发生变量扩展。
清单 31. 在变量读取时使用提示
$ read -p "Instead of $VAR, what number would you like? " VAR
Instead of 23, what number would you like? 17
$ echo $VAR
17
$
如果键盘输入的词比变量个数多,那么依次为变量分配输入的词,到最后一个变量时,为其分配输入行余下的部分。(如果输入的词比变量个数少,那么为变量分配值直到所有的输入都已分配,然后为所有剩余的变量分配空值。)
在循环中读取
您可以在循环中使用 read 作为条件表达式。现在使用清单 32 中的内容尝试这一操作:
清单 32. 在循环中读取一组文件名
$ while read -p "File? " file; do ls $file; done
File? 10
10
File? 12
12
File? 42
42: no such file or directory
File? *
10 11 12
File? Control-C
$
从管道读取
此技术通常在对循环的输入使用管道时使用。尝试键入清单 33 中的文本,该文本使用循环替代 ls 命令的输出:
清单 33. 从管道读取
$ ls | while read file; do ls $file; done
10
11
12
$
您还可以跨多行操作变量,比如将一条消息发送到标准输出,然后对 loopname 变量执行 Shell 运算(请参见 Shell 运算和进制转换部分)。尝试清单 34 中提供的示例:
清单 34. 使用管道读取的较长循环
$ ls | while read file; do echo "The file is " `ls -i $file`; \
echo "If the number were in hex, the value would be $((16#$file))"; done
The file is  100267120 10
If the number were in hex, the value would be 16
The file is  100267121 11
If the number were in hex, the value would be 17
The file is  100267122 12
If the number were in hex, the value would be 18
$
您可以在一个管道输入的 read 中读取多个值,如清单 35 中所示。
清单 35. 从一个管道读取多个变量
$ ls -i | while read inode file; do \
                    echo "File $file has inode $inode"; done
File 10 has inode 100267120
File 11 has inode 100267121
File 12 has inode 100267122
$
实际运用
此结束部分将您在前面学到的诀窍和技术加以组合来实现在实际中有用的单命令行程序。它还包括一个简单的 Shell 脚本——执行任意进制的转换。
有用的单命令行程序
以下示例是执行有用功能的 Shell 单命令行程序样本。它们全部由本教程中描述的各种结构组成。
从当前目录中获取一组文件名恰好为两个字符长的文件,并使用 .ppm 扩展名为其重新命名:
for i in ??; { mv $i $i.ppm; }

使用 tar 和 Subshell 复制整个目录树,同时保持相同的文件权限:
( cd source ; tar pcf - * ) | ( cd target ; tar pxvf - )

读取二进制数并以十进制输出值:
read BINLOC;echo $((2#$BINLOC))

在 /usr/local 目录树中找到所有带 .mp3 扩展名的文件(这些文件的名称中可能包含空格字符),然后使用 bzip2 实用程序压缩这些文件:
find /usr/local -name "*.mp3" | while read name ; do bzip2 $name; done

将给定文件中所有十进制数的值以十六进制输出:
cat file | while read number ; do echo $((0x$number)); done

将给定文件中所有十进制数转换为十六进制的值,并将值输出到带有 .hex 扩展名的新文件中:
cat file | while read number ; do echo $((0x$number)) >> file.hex; done

构造重复十次的循环,以数字(从 0 到 90 以 10 递增)作为传递的参数运行 command:
i=0; while [ $i -ne 100 ]; do command $i; i=$(($i+10)); done
实力脚本
示例脚本:将数字转换为其他进制
本教程中讨论的一些诀窍在清单 36 中被组合在一起。它是一个示例脚本——baseconv,将数字从给定的输入进制转换为输出进制。为它提供输入进制和输出进制的值作为参数,然后它从键盘输入读取数字,直到读取了数字 0。
清单 36. 转换进制的简单脚本
#!/bin/sh
# baseconv, convert numbers from one base to another.
#

NUMBER=1
while [ $NUMBER ]; do
    read -p "Input base: " IN
    read -p "Output base: " OUT
    read -p "Number: " NUMBER
    bc -ql <<- EOF
		obase=$OUT
		ibase=$IN
		$NUMBER
		EOF
done

当您把它保存到可执行文件后(请参见创建 Shell 脚本部分),尝试运行该文件,如清单 37 中所示
清单 37. baseconv 脚本的输出
$ ./baseconv
Input base: 10
Output base: 16
Number: 33
21
Input base: 2
Output base: 1100
Number: 101
5
Input base: 16
Output base: A
Number: ACA
2762
Input base: 10
Output base: 10
Number: 
Carriage return
$
结束语
噢!本教程确实涵盖了许多内容,带您快速浏览了基本的 Shell 编程概念。在学习本教程的过程中,您了解了有关 Shell 编程的许多核心概念:连续循环、内联输入、读取键盘输入、进制转换及 Subshell 执行。您还了解到 Shell 代码片段如何能够作为单命令行程序直接从 Shell 提示符上运行,以及如何将它们放在同一个文件中作为可执行脚本。您可以从中学习一些最重要的脚本编程概念。您如果能综合运用在本教程以及本系列教程的前面部分学到的知识,那么您已成功地迈上 UNIX 专家之路。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值