|
Harley Hahn 的
|
|
各章...
命令
教师 |
第 19 章...
过滤器:选择、排序、
在本章中,我们将以 Unix 工具箱里最有趣、也最强大的 那些过滤器来结束对过滤器的讨论:也就是挑选数据、 给数据排序、合并数据和修改数据的程序。这些程序 极为有用,值得我们花时间详细谈谈。 你已经知道,强大的程序往往要花很长时间才能学会, 本章要讨论的这些过滤器尤其如此。事实上,这些程序 如此强大,你大概永远无法把它们所有的细微之处 都掌握殆尽。 这没什么关系。我会确保你理解基础知识, 并且会给你展示大量例子。随着你的技能和需求 不断成长,你可以随时查阅在线手册,了解更高级的 细节,也可以利用 Web 和 Usenet 向其他人求助。 最重要的是:每当你有机会和一位 Unix 高手面对面 交流,一定要让他给你展示他们使用本章这些过滤器的 看家绝活。这是学习 Unix 最好的方式。 这是专门讲解过滤器的四章(第 16-19 章)中的最后一章。 在第 20 章中,我们将讨论正则表达式,它用来 指定模式。正则表达式能极大地增强过滤器的能力, 在第 20 章里,你会找到许多与本章过滤器 相关的例子,尤其是 grep——也许是所有 过滤器中最重要的那一个。
相关过滤器:look、strings grep 程序从标准输入或从一个或多个文件中 读取数据,提取出所有包含指定模式的行,并把这些行 写到标准输出。例如,你可以用 grep 在 10 个 很长的文件中找出所有包含 Harley 这个词的行。 或者,你可以用 sort 程序(本章后面会讲到) 给大量数据排序,然后通过管道把这些数据交给 grep,提取出所有包含 "note:" 这几个字符的行。 除了查找特定的字符串,你还可以让 grep 配合我们所说的"正则表达式"来查找模式。 一旦这样使用,grep 就成了一件非常 强大的工具。事实上,正则表达式重要到我们要在 第 20 章单独讨论它们,在那一章里你会看到 大量使用 grep 的例子。(正如你在本节末尾 会看到的,grep 这个名字里的 re, 代表的就是 "regular expression"(正则表达式)。) grep 的语法是: grep [-cilLnrsvwx] pattern [file...] 其中 pattern 是要查找的模式,file 是输入文件的文件名。 我们从一个简单的例子开始。在第 11 章中我 解释过,大多数 Unix 系统都把每个用户标识的基本信息 保存在一个名为 /etc/passwd 的文件里。每个 用户标识在这个文件中占一行。你可以用 grep 在这个文件中搜索相应的模式,从而显示自己用户标识的 信息。例如,要显示用户标识 harley 的信息, 使用这条命令: grep harley /etc/passwd 如果 grep 找不到任何与指定模式匹配的行, 它既不会有输出,也不会给出警告信息。和大多数 Unix 命令一样,grep 十分简洁。没什么可说的 时候,grep 就一个字也不说。(*) * 脚注 如果你认识的每个人都有同样的处世哲学,那该多好? 当你指定的模式里含有标点或特殊字符时,你应该把它们 引用起来,这样 Shell 才能正确解释这条命令。 (关于引用的讨论请见第 13 章。)例如,要在 一个名为 info 的文件中查找所有包含 "冒号后跟一个空格"的行,使用这条命令: grep ': ' info grep 查找单个文件固然好用,但它真正大显身手 的地方是在管道里。这是因为 grep 能够迅速把 大量原始数据压缩成少量有用的信息。这是一项极其 重要的能力,它使 grep 成为 Unix 工具箱里最 重要的程序之一。随便问一位有经验的 Unix 用户, 你会发现他们都离不开 grep。这个精彩程序的 强大之处,你需要花些时间才能真正体会,不过我们可以 先从几个简单的例子开始。 当你和其他人共用一台多用户系统时,你可以用 w 程序(第 8 章)显示所有用户以及 他们正在做什么的信息。下面是一段示例输出: |
|
8:44pm up 9 days, 7:02, 5 users, load: 0.11, 0.02, 0.00
|
|
假设你想显示所有在下午或晚上登录的用户。你可以搜索 输出中包含 "pm" 这个模式的行。使用这条管道: w -h | grep pm (注意我用的是带 -h 选项的 w。 这个选项会去掉表头,也就是前两行。)利用上面的数据, 前一条命令的输出是: |
|
harley ttyp1 5:47pm 15:11 w
|
|
Math: problems 12-10 to 12-33, due Monday
要列出所有还没有完成的作业,输入: grep -v DONE homework 输出是:
Math: problems 12-10 to 12-33, due Monday
|
|
如果你想看看还有多少份作业没完成,把 -c 和 -v 合起来用: grep -cv DONE homework 在这种情况下,输出是: 2 有时候,你可能想找出那些"搜索模式正好等于整行"的行。 这时就用 -x 选项。例如,假设 names 文件包含以下几行:
Harley
如果你想找出所有包含 "Harley" 的行,用: grep Harley names 如果你只想找出那些 "Harley" 恰好占满整行的行, 就用 ‑x 选项: grep -x Harley names 这种情况下,grep 只会选中第一行和最后一行。 要搜索整棵目录树(见第 23 章),使用 -r(recursive,递归)选项。例如,假设你想在 名为 admin 的目录中的所有文件里查找 "initialize" 这个词,包括所有子目录、这些子目录里的 所有文件,以此类推。你可以用: grep -r initialize admin 当你在很大的目录树上使用 -r 时,经常会看到 错误信息,告诉你 grep 无法读取某些文件, 原因要么是文件不存在,要么是你没有读取它们的权限。 (我们将在第 25 章讨论文件权限。)通常你会 看到下面两条信息中的一条:
No such file or directory
如果你不想看到这类信息,使用 -s(suppress, 屏蔽)选项。例如,假设你以超级用户身份登录,想在 系统的所有文件中查找 "shutdown now" 这几个词。 正如我们将在第 23 章讨论的,/ 指的是 整个文件系统的根(主要)目录。因此,如果我们从 / 目录出发并使用 -r(递归)选项, grep 就会搜索整个文件系统。命令是: grep -rs / 'shutdown now' 注意我给搜索模式加上了引号,因为它含有空格。 (引用在第 13 章中解释。)
在早年间(20 世纪 70 年代和 80 年代),人们常常 使用 grep 的另外两个版本:fgrep 和 egrep。 fgrep 程序是 grep 的快速版本,它只查找 "固定字符"字符串。(因此得名 fgrep。)也就是 说,fgrep 不允许使用正则表达式来匹配模式。 在计算机很慢、内存很有限的年代,只要你不需要正则 表达式,fgrep 就比 grep 更高效。如今 计算机很快,内存也很大,所以已经没必要使用 fgrep 了。我提到它只是出于历史原因。 egrep 程序是 grep 的扩展版本。(因此 得名 egrep。)最初的 grep 只支持 "基本正则表达式"。后来出现的 egrep 支持功能 更强的"扩展正则表达式"。二者的区别我们会在 第 20 章讨论。目前你只需要知道:扩展正则 表达式更好,只要有得选,你就应该始终使用它们。 现代的 Unix 系统允许你通过 egrep 或 grep -E 来使用扩展正则表达式。不过, 大多数有经验的 Unix 用户更愿意直接键入 grep。 解决办法是创建一个别名(见第 13 章),把 grep 改成 egrep 或 grep -E。 在 Bourne Shell 家族中,你会用下面两条命令之一:
alias grep='egrep'
在 C-Shell 家族中,你会用下面这些命令之一:
alias grep 'egrep'
一旦定义了这样的别名,你键入 grep 就能获得 扩展正则表达式的完整功能。要让这个改动永久生效, 你只需把相应的 alias 命令放进你的环境文件 (见第 14 章)。这个别名确实非常实用,所以我 建议你现在就花一点时间把它加到自己的环境文件里。 事实上,等你读到第 20 章时,我会假定你用的 就是扩展正则表达式。 注意:如果你用的是 Solaris(来自 Sun Microsystems), 你想要的 egrep 版本位于一个名为 /usr/xpg4/bin/(*)的特殊目录里,这意味着你 必须使用不同的别名。下面的例子只适用于 Solaris。 第一条是给 Bourne Shell 家族用的;第二条是给 C-Shell 家族用的:
alias grep='/usr/xpg4/bin/egrep'
* 脚注 xpg4 这个名字代表 "X/Open Portability Guide, Issue 4"(X/Open 可移植性指南第 4 版),那是一项关于 Unix 系统应如何表现的古老(1992 年)标准。这个目录里 的程序经过修改,以便按照 XPG4 标准行事。
相关过滤器:grep look 程序在按字母顺序排列的数据中搜索, 找出所有以指定模式开头的行。 使用 look 有两种方式。你可以使用来自一个或 多个文件的已排序数据,也可以让 look 搜索 字典文件(下一节会解释)。 当你用 look 搜索一个或多个文件时,语法是: look [-df] pattern [file...] 其中 pattern 是要查找的模式,file 是文件名。 这里举个例子。你是一所学校的学生,那里每学期所有 学生都要给自己的教授打分。这学期由你负责这件事。 你有一个名为 evaluations 的大文件,里面 汇总了一百多位教授的评分结果。数据是按字母顺序 排列的。文件的每一行包含一个等级(A、B、C、D 或 F), 后面跟两个空格,再后面是一位教授的姓名。例如:
A William Wisenheimer
你的任务是做出五个列表,贴到一个网站上去。这些 列表应该分别包含得到 A 等、B 等等等的教授姓名。 由于数据是按字母顺序排列的,你可以用 look 选出文件中所有以 A 开头的行,从而做出 第一个列表(A 等教授): look A evaluations 虽然这条命令能完成工作,我们还可以把它改进一下。 如我所说,数据文件中每一行都以一个单字母等级开头, 后面跟两个空格。拿到你想要的姓名之后,你可以用 colrm(第 16 章)去掉每行的前三个字符。 下面的例子针对每个等级都做了这件事:它们从数据文件 中选出相应的行,用 colrm 去掉每行的前三个 字符,然后把输出重定向到一个文件:
look A evaluations | colrm 1 3 > a-professors
与本章介绍的其他程序不同,look 不能从 标准输入读取:它的输入必须来自一个或多个文件。 这就是说,严格来讲,look 并不是过滤器。 这个限制的原因在于,对于标准输入,程序每次只能 读取一行。而 look 使用的是一种称为 "二分查找"(binary search)的搜索方法,它要求 一次性访问全部数据。因此,你不能把 look 用在管道中间,尽管你可以把它用在管道的开头。 当你有多个步骤时,最好的策略是先准备好数据,把它 存进一个文件,然后用 look 搜索这个文件。 例如,假设 frosh、soph、junior 和 senior 这四个文件包含前面描述的那种未经 排序的原始评分数据。在你用 look 搜索这些数据 之前,你必须把四个文件的内容合并并排序,把输出保存 到一个新文件里,例如:
sort frosh soph junior senior > evaluations
我们会在本章后面讨论 sort 程序。到那时你会 了解到与 look 相关的两个特定选项。-d (dictionary,字典)选项告诉 sort 只考虑字母 和数字。当你想让 look 忽略标点和其他特殊字符 时,就用 -d。-f(fold,折叠)选项 告诉 sort 忽略大小写的差别。例如,使用 -f 时,"Harley" 和 "harley" 被视为相同。 如果你用 sort 的这两个选项中任何一个来准备 数据,那么你对 look 也必须使用相同的选项, 这样 look 才知道该期待什么样的数据。例如:
sort -df frosh soph junior senior > evaluations
look 和 grep 都能根据指定模式从文本 文件中挑选行。因此,问一问"什么时候用 look, 什么时候用 grep?"是合乎情理的。 许多场合都会出现类似的问题,因为 Unix 往往为同一个 问题提供不止一种解决办法。正因如此,能够明智地分析 你手中的选项很重要,这样你才能为当前的任务挑到最合适 的工具。我们举个例子,把 look 和 grep 做个比较。 look 程序在三个重要方面受到限制。第一,它 要求输入是已排序的;第二,它只能从文件读取,不能从 标准输入读取;第三,它只能查找位于行首的模式。不过, 在这些限制范围内,look 有两个优点:用起来 简单,而且非常快。 grep 程序灵活得多:它不要求已排序的输入; 它既可以从文件读取,也可以从标准输入读取(这意味着 你可以把它用在管道中间);它可以在任意位置查找模式, 而不局限于行首。 此外,grep 支持"正则表达式",让你能够 指定泛化的模式,而不仅仅是简单的字符。例如,你可以 查找"字母 har,后面跟一个或多个字符,再后面 跟字母 ley,再后面跟零个或多个数字"。 (正则表达式非常强大,我们会在 20 章详细 讨论它们。) 通过使用正则表达式,你完全可以让 grep 做出 look 能做的一切。只不过 grep 会更慢, 语法也更别扭。 所以我给你的建议是:每当你需要从文件中挑选行时, 先问问自己 look 能不能胜任。如果能,就用它, 因为 look 既快又简单。如果 look 干不了这活(大多数时候都是如此),就用 grep。 作为一般规则,你应该始终用最简单的办法来解决问题。 那么速度呢?我提到过 look 比 grep 快。这有多重要?在 Unix 的早期,速度是一个重要的 考虑因素,因为 Unix 系统是与其他用户共享的,计算机 也相对较慢。当你从一个非常大的文件里挑选文本行时, 你确实能察觉 look 和 grep 之间的差别。 然而在今天,几乎所有 Unix 系统都运行在快得惊人的 计算机上,在实际使用中都是如此。因此,Unix 执行 一条命令的速度——至少对本章这些命令而言—— 是无关紧要的。例如,本章中的任何例子都会快得仿佛 瞬间完成。更具体地说,如果你把一条 look 命令和与之等价的 grep 命令相比,你根本 不可能察觉速度上的差异。 所以我的建议是:根据简洁性和易用来挑选工具,而不是 根据速度或效率上那点微乎其微的差别。当你在编写程序, 包括 shell 脚本时,这一点尤其重要。如果某个程序或 脚本太慢,通常都能找出一两个瓶颈并让它提速。但如果 一个程序复杂得没必要,或者难以使用,那么从长远看, 它会浪费大量你自己的时间,而你的时间远比 计算机的时间宝贵。 提示 只要工具有得选,就用能完成这件工作的最简单 的那个。
我前面提到,你可以用 look 搜索字典文件。 当你想找出所有以某个特定模式开头的词时,就这么用, 例如找出所有以 "simult" 这几个字母开头的词。这样使用 look 时,语法很简单: look pattern 其中 pattern 是要查找的模式。 所谓"字典文件"并不是一本真正的词典。它是一份很长、 很全面的词表,从 Unix 的早期版本起就存在了。 (当然,这些年来这份词表一直在更新。)字典文件里的 单词按字母顺序排列,每行一个词,这使得用 look 搜索该文件变得很容易。 字典文件最初是为一个名为 spell 的程序创建 的,spell 提供了一种粗糙的文档拼写检查方法。 spell 的工作是列出一份清单,显示文档中所有 没有收录在字典文件里的词。在早年间,spell 能通过找出可能的拼写错误为你省下大量时间。 如今有了好得多的拼写检查工具,spell 很少 用了:实际上,在大多数 Linux 或 Unix 系统上你甚至 找不到它。人们改用文字处理器内置的拼写检查功能, 或者在处理文本文件时,使用一个名为 aspell 的交互式程序,它是 GNU 工具集之一。如果你想试试 aspell,可以用: aspell -c file 其中 file 是一个包含纯文本的文件名。-c 选项表示你想检查文件中单词的拼写。 虽然 spell 已经不再使用,但字典文件依然还在, 你可以以各种方式利用它。特别是,你可以用 look 程序找出所有以特定模式开头的词。当你在为一个词的 拼写发愁时,这一点特别方便。例如,假设你想输入 "simultaneous" 这个词,但不确定怎么拼。输入: look simult 你会看到一份类似下面的列表:
simultaneity
现在,挑出正确的词,并且——如果你愿意—— 从一个窗口复制粘贴到另一个窗口,就是很简单的事了。 (关于如何复制粘贴的说明,请见第 6 章。) 我们会在第 20 章再次谈到字典文件,到时我会告诉 你它在系统上的位置,以及如何用它帮助解答文字谜题。 (顺带说一句,"simulty" 指的是私人之间的积怨或口角。) 提示 当你使用 vi 文本编辑器(见第 22 章) 时,你可以用 :r! 发出一次快速的 look 命令来显示一份单词列表。例如: :r !look simult 这条命令会把所有以 "simult" 开头的单词插入到你的 编辑缓冲区中。现在你可以挑出你想要的那个词, 把其他全部删掉。
相关过滤器:tsort、uniq sort 程序能完成两项相关的任务:给数据排序, 以及检查数据是否已经排好序。我们先从基础讲起。 给数据排序的语法是: sort [-dfnru] [-o outfile] [infile...] 其中 outfile 是存放输出的文件名,infile 是包含输入的文件名。 sort 程序非常灵活。你可以比较整行,也可以 只比较每行中选定的部分(字段)。使用 sort 最简单的方式,是对单个文件排序、比较整行,并把结果 显示在你的屏幕上。举个例子,假设你有一个名为 names 的文件,包含以下四行:
Barbara
要对这些数据排序并显示结果,输入: sort names 你会看到:
Al
要把排好序的数据保存到一个名为 masterfile 的文件中,你可以重定向标准输出: sort names > masterfile 最后这个例子把排好序的数据保存在一个新文件里。然而, 很多时候你会想把数据保存回同一个文件,也就是说,你想 用同一份数据的排序版本替换原文件。遗憾的是,你不能 把输出重定向到输入文件: sort names > names 你会记得,在第 15 章我解释过,当你重定向 标准输出时,Shell 会在运行命令之前先建立好输出文件。 在这个例子里,由于 names 是输出文件,Shell 会在运行 sort 命令之前把它清空。于是,等到 sort 准备读取输入时,names 已经是空的 了。因此,输入这条命令的结果,就是悄无声息地抹掉你 输入文件的内容。(*) * 脚注 除非你设置了 noclobber 这个 Shell 变量。 参见第 15 章。 正因为如此,sort 提供了一个专门的选项,让你 可以把输出保存到你想要的任何文件里。用 -o (output,输出),后面跟上输出文件名。如果输出文件 恰好是你的某个输入文件,sort 会确保保护好 你的数据。所以,要对一个文件排序并把输出保存在同一个 文件里,可以使用类似这样的命令: sort -o names names 在这种情况下,names 中的原始数据会被保留, 直到 sort 完成为止,然后输出才被写入该文件。 要对来自多个文件的数据排序,只要指定多个输入文件名 即可。例如,要把 oldnames、names 和 extranames 三个文件的合并内容排序,并把输出 保存到 masterfile 文件中,用: sort oldnames names extranames > masterfile 要对同样这几个文件排序,同时把输出保存在 names(其中一个输入文件)中,用: sort -o names oldnames names extranames sort 程序常常作为管道的一部分,用来处理由 另一个程序产生的数据。下面这个例子合并两个文件,只 提取包含 "Harley" 这几个字符的行,把这些行排序, 然后通过管道交给 less 显示: cat newnames oldnames | grep Harley | sort | less 默认情况下,sort 在排序时考察整行。不过如果 你愿意,你可以让 sort 只检查一个或多个字段, 也就是每行的某一部分。(我们在第 17 章讨论 cut 程序时讲过字段的概念。)允许你在 sort 中使用字段的这些选项提供了很强的控制 能力,但它们非常复杂,我这里就不细讲了。如果你哪天 真的需要按字段排序,你会在 Info 文件 (info sort)中找到细节。如果你的系统没有 Info 文件(见第 9 章),那么这些细节会在 man 页里(man sort)。
你可以用好几个选项来控制 sort 程序的行为: -d(dictionary,字典)只考察字母、数字和 空白字符(空格和制表符)。当你的数据中含有会妨碍排序 过程的字符(例如标点)时,使用这个选项。 -f(fold,折叠)选项把小写字母当作大写 字母处理。当你想忽略大小写区别时使用这个选项。例如, 使用 -f 时,harley 和 Harley 都被认为与 HARLEY 相同。("fold"这个术语的 含义在下面解释。) -n(numeric,数值)选项识别行或字段开头的 数字,并按数值大小排序。这样的数字可以包含前导空格、 负号和小数点。用这个选项告诉 sort 你用的 是数值数据。例如,假设你要排序:
11
如果你使用不带任何选项的 sort,输出是:
1
如果你使用 sort -n,你得到:
1
-r(reverse,反向)选项按相反的顺序排序。 例如,如果你对上一个例子中的数据用 sort -nr 排序,输出是:
20
以我的经验,你会发现自己使用 -r 选项的频率 比想象中高得多。这是因为按逆字母顺序或逆数值顺序 列出信息是很有用的。 最后一个选项 -u(unique,唯一)告诉 sort 检查完全相同的行,只保留其中一行。例如, 假设你用 sort -u 对以下数据排序:
Barbara
输出是:
Al
提示 除了 sort -u,你还可以用 uniq (本章后面会讲到)作为替代。uniq 程序更简单, 但与 sort 不同,必要时它不允许你针对特定 字段做处理。 名称的由来 Fold(折叠) 有不少 Unix 程序都带有一个忽略大小写差别的选项。 有时这个选项叫 -i,取自 "ignore"(忽略), 这很合情理。但更多时候,这个选项是 -f, 代表 FOLD:这是一个术语,表示小写字母要当作大写 字母处理(或反之),而并不改变原始数据。(这种用法 中的 "fold" 与 fold 程序毫无关系,所以别 搞混了。) "fold" 这个词最常用作形容词:"要让 sort 不区分大小写,请使用 fold 选项。"不过有时你也会看到 "fold" 被当作动词使用:"使用 -f 选项时, sort 会把小写字母折叠成大写字母。" 这里有个有意思的事:Unix 中 sort 程序的最初 版本是把大写字母折叠成小写字母。也就是说,当你使用 -f 时,sort 把所有字母都当作小写。 现代版本的 sort 则把小写字母折叠成大写字母, 也就是把所有字母都当作大写。这个区别重要吗?答案是: 有时重要,等我们讨论排序序列时你就会看到。 顺便说一下,谁也不知道 "fold" 这个词的来源,所以 你尽可以自己想一个比喻。
如我前面所说,sort 能完成两项相关的任务: 给数据排序,以及检查数据是否已经排好序。本节我们谈谈 检查数据。用这种方式调用 sort 时,语法是: sort -c[u] [file] 其中 file 是文件名。 -c(check,检查)选项告诉 sort, 你并不想排序数据,只想知道它是不是已经排好序了。 例如,要查看 names 文件里的数据是否已排序, 你可以用: sort -c names 如果数据已排序,sort 什么都不显示。 (没有消息就是好消息。)如果数据没有排序, sort 会显示一条消息,例如: sort: names:5: disorder: Polly Ester 这里,这条消息的意思是 names 中的数据没有 排好序(也就是有 "disorder",紊乱),紊乱从第 5 行 开始,那一行包含的数据是 Polly Ester。 你可以在管道中使用 sort -c,检查由 另一个程序写到标准输出的数据。例如,假设你有一个名为 poetry-generator 的程序,它会生成大量输出。 这些输出本应是排好序的,但你怀疑可能有问题,于是用 sort -c 检查一下: poetry-generator | sort -c 如果你把 -c 和 -u(unique,唯一) 选项结合起来,sort 会同时用两种方式检查你的 数据。它在寻找未排序数据的同时,也会寻找连续出现的 相同行。当你想要确保(1)数据已排序,并且(2)所有 行都是唯一的,就用 -cu。例如,文件 friends 包含以下数据:
Al Packa
你输入: sort -cu friends 虽然数据是排好序的,但 sort 发现了一个重复行: sort: friends:4: disorder: Patty Cake
假设你用 sort 程序对以下数据排序。输出会 是什么?
zzz
在某些系统上,你会得到:
AAA
在另一些系统上,你会得到:
AAA
这怎么可能?在 Unix 的早期,字符只有一种编排方式。 如今情形不同了,你运行 sort 时看到的结果, 取决于字符在你的系统上是如何编排的。 事情是这样的。 在 20 世纪 90 年代之前,Unix(以及大多数计算机系统) 使用的字符编码是 ASCII 码(ASCII CODE), 常简称 ASCII。这个名字代表 "American Standard Code for Information Interchange"(美国信息交换标准代码)。 ASCII 码创建于 1967 年。它为每个字符规定了一个 7 位的比特模式,总共 128 个。这些比特模式从 0000000(十进制的 0)到 1111111 (十进制的 127)。因此,128 个 ASCII 字符的编号 是 0 到 127。 构成 ASCII 码的这 128 个字符包括 33 个"控制字符" 和 95 个"可打印字符"。控制字符在第 7 章讨论过。 可打印字符列在下方,它们是字母表中的 52 个字母 (26 个大写、26 个小写)、10 个数字、32 个标点符号, 以及空格字符(在下面的列表中排在最前):
!"#$%&'()*+,-./0123456789:;<=>?
可打印字符的排列顺序就是我列出的顺序。它们从第 32 号 字符(空格)一直到第 126 号字符(波浪号)。(记住, 编号从 0 开始,所以空格实际上是第 33 个字符。) 作为参考,附录 D 收录了完整的 ASCII 码表。你现在就 可以花一点时间看看它。 就实际用途而言,把制表符视为可打印字符很方便,尽管 严格来说它其实是一个控制字符。制表符是第 9 号字符, 这使它排在其他可打印字符之前。因此,我给出如下定义: 96 个可打印字符包括制表符、空格、标点符号、数字和 字母。 为了方便起见,大多数 Unix 系统都有一页参考资料展示 ASCII 码,让你随时都能快速查看。遗憾的是,这页 ASCII 参考页并没有统一标准,所以显示它的办法取决于 你用的是哪个系统。细节见 Figure 19-1。 Figure 19-1: 显示 ASCII 码 你会在本书附录 D 中找到 ASCII 码的汇总。为了便于 联机查阅,大多数 Unix 系统都有一份方便实用的页面, 包含完整的 ASCII 码。传统上,这份页面存放在 /usr/pub/ 目录下一个名为 ascii 的文件里。 近年来,一些系统重新组织了 Unix 文件系统,ASCII 参考文件被移到了 /usr/share/misc。在另一些 系统上,这个文件被转换成了在线手册中的一页。因此, 显示 ASCII 参考页的办法取决于你所使用的系统。
就字符编码方案而言,字符的组织顺序称为排序序列 (COLLATING SEQUENCE)。凡是需要给字符排列顺序的 场合都会用到排序序列;例如你使用 sort 程序时, 或者在正则表达式中使用范围时(第 20 章讨论)。 在 ASCII 码中,排序序列就是字符在编码中出现的顺序。 Figure 19-2 做了概括。更详细的参考请见附录 D。 Figure 19-2: ASCII 码中字符的顺序 ASCII 码定义了 Unix 系统使用的 128 个基本字符。在 ASCII 码中,字符编号为 0 到 127。本图中的表格概括了 字符的顺序,当你使用 sort 这类程序时,这个 顺序很重要。例如,当你对文本排序时,空格排在 "%" (百分号)之前,"% "排在数字 "3" 之前,"3" 又排在 字母 "A" 之前,以此类推。 更详细的参考请见附录 D。
熟悉 ASCII 码的排序序列很重要,因为许多 Unix 系统 和编程语言都默认使用它。你不必把整个 ASCII 码都背 下来,但你确实需要记住三条基本原则:
• 空格排在数字之前。
这里举个例子。假设你的系统使用 ASCII 排序序列。 你用 sort 程序对以下数据排序(其中第三行 以一个空格开头):
hello
输出是:
hello
提示 至于 ASCII 码中字符的顺序,你需要记住的只是:空格、 数字、大写字母、小写字母,按这个顺序排列。 只要记住 "SNUL" 就行了。(*) * 脚注 如果你记不住 SNUL 这个首字母缩写,让我教你一个许多 聪明人都在用的记忆诀窍。你要做的,就是把你想记住的 东西和你的日常生活联系起来。 例如,假设你是一位专攻差分计算的数学家,恰好正在 处理四阶差分方程,而这些方程是由那些在特殊的 非均匀格(lattices)上正交的 Laguerre-Hahn 多项式 所满足的。要记住 SNUL,你只要想想 "special non- uniform lattices"(特殊非均匀格)就够了。 看到没,当个聪明人是多么容易?
在 Unix 的早期,人人都用 ASCII 码,事情就这么简单。 然而 ASCII 是以英语为基础的,随着 Unix、Linux 和 Internet 在世界范围内的普及,人们有必要设计一套能 支持众多语言和种种文化惯例的系统。 在 20 世纪 90 年代,人们开发出一套新系统,它建立在 "区域设置"(locale)这一概念之上,是 POSIX 1003.2 标准的一部分。(POSIX 在第 11 章和第 16 章讨论。) 一个区域设置(LOCALE)是一份技术规约,描述与来自 某种文化的用户交流时应使用的语言和各种惯例。其设想是: 用户可以选择他想要的任何区域设置,他所运行的程序就会 相应地与他交流。例如,如果一位用户选择了美式英语区域 设置,他的程序就应当用英语显示消息,按"月-日-年"的 格式书写日期,把 "$" 用作货币符号,如此等等。 在 Unix 内部,你的区域设置由一组环境变量定义,它们 标明你的语言、日期格式、时间格式、货币符号以及其他 文化惯例。每当程序需要了解你的偏好时,它只要查看相应 的环境变量即可。特别地,有一个名为 LC_COLLATE 的环境变量,用来指定你想使用哪一种排序序列。 (这些变量都有默认值,如果你愿意,可以修改它们。) 要在你的系统上显示所有区域设置变量的当前值—— 包括 LC_COLLATE——你使用 locale 命令: locale 如果你想知道自己的系统支持哪些区域设置,可以用 -a(all,全部)选项把它们全部显示出来: locale -a 在美国,Unix 系统默认使用两个区域设置中的一个。 这两个区域设置基本上相同,但排序序列不同,这就意味着 当你运行 sort 这类程序时,结果会因所用区域 设置的不同而不同。 由于很多人不知道区域设置的存在,即使是有经验的 程序员,在从一个 Unix 系统换到另一个 Unix 系统时, 也会一头雾水:sort 这类程序突然"不守规矩"了。 因此,我要花一点时间谈谈这两个美式区域设置,解释你 需要知道的要点。如果你住在美国之外,这些道理依然适用, 只是细节会有所不同。 第一个美式区域设置以 ASCII 码为基础。它有两个名字, 叫作 C 区域设置(以 C 编程语言命名),也叫 POSIX 区域设置:你想用哪个名字都行。第二个 美式区域设置以美式英语为基础,名为 en_US, 不过你会看到这个名字的各种变体。 C 区域设置是为兼容性而设计的,目的是保留 老派程序(以及老派程序员)所使用的惯例。en_US 区域设置则是为了融入一个现代化的国际框架,在这个框架 里,美式英语只是众多语言中的一种。 如我所说,这两个区域设置除了排序序列以外完全相同。 C 区域设置使用 ASCII 排序序列,其中大写 字母排在小写字母之前:ABC...XYZabc... z。这种排列称为 C 排序序列(C COLLATING SEQUENCE),因为 C 编程语言使用它。 en_US 区域设置使用另一种排序序列,其中 小写字母和大写字母成对归组:aAbBcCdD... zZ。这种排列更自然,因为它给单词和字符排列的 顺序,和你在词典里见到的顺序一致。因此这种排列称为 词典排序序列(DICTIONARY COLLATING SEQUENCE)。 直到 20 世纪 90 年代末,所有 Unix 系统都使用基于 ASCII 码的 C 排序序列;对于使用 C/POSIX 区域设置的系统,如今依然是这样。然而今天,某些 Unix 系统(包括少数 Linux 发行版)在设计时更偏向国际化, 因此它们使用 en_US 区域设置和词典排序序列。 你能看出可能的混乱之源吗?每当你运行一个依赖大小写 字母顺序的程序,输出都会受你的排序序列影响。于是, 根据你的系统默认使用哪个区域设置,你会得到不同的结果。 例如,当你使用 sort 程序,或者使用某些被称为 "字符类"的正则表达式时(见第 20 章),就可能 出现这种情况。 作为参考,Figure 19-3 展示了这两种排序序列。 请注意,差别相当明显:不仅字母的顺序不同,标点符号 的顺序也是如此。 Figure 19-3: C 与 en_US 区域设置的排序序列 在美国,Unix 和 Linux 系统使用两个区域设置之一: 基于 ASCII 码的 C/POSIX,或基于美式英语的 en_US。下面两张表展示了这两种区域设置各自的 排序序列。在 C 排序序列中(与 C 区域设置 一起使用),数字、小写字母和大写字母之间隔着符号。 在词典排序序列中(与 en_US 区域设置一起使用), 所有符号都在最前面,其后才是数字和字母。 注意:在两张表中,我都用一个点(•) 来表示空格字符。
为了说明区域设置的选择会造成什么影响,我们来看一个 例子:对以下数据排序时(其中第三行以空格开头)会发生 什么:
hello
在 C 区域设置(C 排序序列)下,输出是:
hello
在 en_US 区域设置(词典排序序列)下, 输出是:
hello
那么你应该用哪个区域设置?以我的经验,如果你使用 en_US 区域设置,迟早会遇到一些难以追查的 意外问题。例如,正如我们将在第 25 章讨论的, 你用 rm(remove,删除)程序删除文件。假设你 想删除所有文件名以大写字母开头的文件。传统上要用的 Unix 命令是: rm [A-Z]* 如果你用的是 C 区域设置,这条命令会正常 工作。然而,如果你用的是 en_US 区域设置, 结果你会把所有以任意大写或小写字母开头的文件都删掉, 除了以字母 a 开头的以外。(不必在意细节, 第 20 章会解释。)(*) * 脚注 还有很多场合,C 区域设置比 en_US 区域设置更好用。再举一例:你正在写一个 C 或 C++ 程序。在你的目录里,你有一些代码文件的文件名全部 是小写字母,例如 program1.c、program2.cpp、 data.h 等等。你还有一些其他文件,名字以大写 字母开头,例如 Makefile、RCS、 README。当你用 ls 程序(第 24 章) 列出目录内容时,所有"大写"开头的文件会排在最前面, 从而把其他文件和代码文件区分开。 我的建议是把默认值设为 C 区域设置,因为它使用 传统的 ASCII 排序序列。从长远看,比起使用 en_US 区域设置和词典排序序列,这带来的麻烦 更少。事实上,在你阅读本书的过程中,我假定你用的 就是 C 区域设置。 那么,如何指定你的区域设置?第一步是弄清你的系统 默认使用哪种排序序列。如果 C 区域设置已经 是你系统的默认值,那就很好。如果不是,你就需要改。 确定默认排序序列的一个办法,是输入 locale 命令并查看 LC_COLLATE 环境变量的值。它是 C 或 POSIX?还是 en_US 的 某个变体? 确定默认排序序列的另一个办法,是做下面这个简短的 测试。用这条命令创建一个名为 data 的小文件: cat > data 键入以下三行,然后按 ^D 结束命令:
AAA
现在对这个文件的内容排序: sort data 如果你使用的是 C/POSIX 区域设置, 输出将按 C(ASCII)排序序列排列:
AAA
如果你使用的是 en_US 区域设置,输出将按词典 排序序列排列:
[]
在你继续往下读之前,花一点时间看看 Figure 19-3 中的排序序列,确认这些例子你能想通。 如果你的 Unix 系统默认使用 C 或 POSIX 区域设置,你什么都不必做。(不过,请把本节剩下的 内容读完,因为总有一天,你会在别的系统上碰到这个 问题。) 如果你的系统使用 en_US 区域设置,你需要把 LC_COLLATE 环境变量改成 C 或 POSIX。在 Bourne Shell 家族中,下面两条命令 中的任何一条都能完成这件事:
export LC_COLLATE=C
在 C-Shell 家族中,你会用:
setenv LC_COLLATE C
要让这个改动永久生效,你只需把其中一条命令放进你的 登录文件。(环境变量在第 12 章讨论;登录文件在 第 14 章讨论。)在本书余下的部分,我会假定你 确实使用 C 排序序列;所以如果你现在不是,请立刻把 相应的命令写进你的登录文件。 提示 偶尔,你可能想用一种不同于默认的排序序列来运行单个 程序。为此,你可以在运行该程序时用一个子 Shell 临时 改变 LC_COLLATE 的值。(我们在第 15 章 讨论子 Shell。) 例如,假设你使用的是 C 区域设置,而你想用 en_US(词典)排序序列来运行 sort 程序。你可以用: (export LC_COLLATE=en_US; sort data) 当你以这种方式运行程序时,你对 LC_COLLATE 所做的改动是临时的,因为它只存在于子 Shell 内部。
相关过滤器:sort Unix 有一批专门处理已排序数据的过滤器。其中最有用的 一个是 uniq,它逐行检查数据,寻找连续出现的 重复行。 uniq 程序可以完成四种不同的任务:
• 去掉重复行
语法是: uniq [-cdu] [infile [outfile]] 其中 infile 是输入文件名,outfile 是输出文件名。 我们先看一个简单的例子。文件 data 包含 以下几行:
Al
(记住,由于 uniq 的输入必须是已排序的, 重复行会连续出现。)你想要一份文件中所有行的列表, 不含重复。要用的命令是: uniq data 输出很直截了当:
Al
如果你想把输出保存到另一个文件,比如 processed-data,你可以在命令中指定它的名字: uniq data processed-data 要只看重复行,使用 -d 选项: uniq -d data 用我们的示例文件,输出是:
Al
要只看唯一(不重复)的行,用 -u: uniq -u data 在我们的示例中,这样的行只有一行: Charles 问一问:如果你同时使用 -d 和 -u, 你猜会发生什么?(自己动手试试。) 要统计每行出现的次数,使用 -c 选项: uniq -c data 用我们的示例,输出是:
2 Al
到目前为止,我们的例子都很简单。uniq 的真正 威力,体现在把它用在管道里的时候。例如,把几个文件 合并排序,然后通过管道交给 uniq,是很常见的 做法,下面两个例子就是如此:
sort file1 file2 file3 | uniq
提示 如果你不带选项地使用 uniq,其实还有个替代 办法:你可以改用 sort -u。例如,下面 三条命令的效果完全相同:
sort -u file1 file2 file3
(参见本章前面关于 sort -u 的讨论。) 下面是一个真实的例子,让你看看这类构造有多强大。 Ashley 是南加州一所大学的学生。即将到来的寒假里, 她的表妹 Jessica 要从东海岸过来看她。Jessica 在 东海岸上是一所小而进步的文理学院。Jessica 想认识 一些男生,但她很挑剔:她只喜欢既聪明又爱运动的男生。 碰巧,Ashley 是她们女生联谊会伦理委员会的成员, 这让她能访问学生/学业数据库(别问细节)。Ashley 利用自己的特殊身份登录系统,建立了两个文件。第一个 文件 math237 包含所有选修数学 237(高等 微积分)的男生的姓名。第二个文件 pe35 包含 所有选修体育 35(冲浪鉴赏)的男生的姓名。 Ashley 的主意是,通过找出同时选修这两门课的男生, 为 Jessica 做一份可能的约会对象名单。因为文件太 大,没法手工比对,Ashley(她既漂亮又聪明) 于是动用 Unix。具体来说,她使用 uniq 程序 加上 -d 选项,并把输出保存到一个名为 possible-guys 的文件里: sort math237 pe35 | uniq -d > possible-guys 随后 Ashley 用邮件把这份名单发给 Jessica,Jessica 出发之前就能在 Instagram 上把这些男生的样子查个 清楚。 提示 你必须始终确保交给 uniq 的输入是已排序的。 否则 uniq 就无法发现重复。结果不会如你所愿, 但也不会有任何错误信息来警告你出了岔子。(*) * 脚注 这是难得的一次——哪怕对 Ashley 和 Jessica 来说也是如此——"排好序的"(sorted)风流韵事 会被当成一件好事。
相关过滤器:colrm、cut、paste 在所有专门处理已排序数据的 Unix 过滤器中,最有意思的 是 join,它根据某个特定字段的值来合并两个 已排序的文件。语法是: join [-i] [-a1|-v1] [-a2|-v2] [-1 field1] [-2 field2] file1 file2 其中 field1、field2 是表示特定字段的 数字;file1 和 file2 是包含已排序数据的 文件名。 在进入细节之前,我想先给你看一个例子。假设你有两个 已排序的文件,包含若干人的信息,每个人都有一个唯一的 识别号。在第一个名为 names 的文件中,每行 包含一个 ID 号,后面跟着名和姓:
111 Hugh Mungus
在第二个文件 phone 中,每行包含一个 ID 号, 后面跟着一个电话号码:
111 101-555-1111
join 程序允许你根据两个文件的共同值来合并 它们,在本例中就是 ID 号: join names phone 输出是:
111 Hugh Mungus 101-555-1111
join 读取输入时会忽略前导空白,也就是行首的 空格或制表符。例如,下面两行被视为相同:
111 Hugh Mungus 101-555-1111
在讨论 join 程序的细节之前,我想花一点时间 梳理一下某些术语。在第 17 章,我们谈过字段和 分隔符的概念。当你有一个文件,其中每一行都是一条数据 记录时,行内每个独立的项目就称为一个字段。在我们的 例子里,names 文件的每一行包含三个字段: 一个 ID 号、一个名、一个姓。phone 文件包含 两个字段:一个 ID 号和一个电话号码。 在每行内部,用来分隔字段的字符称为分隔符。在我们的 例子里,分隔符是空格,尽管你也会经常看到用制表符和 逗号来做这件事。默认情况下,join 假定每对 字段之间由空白分隔,也就是由一个或多个空格或制表符 分隔。 当我们根据相匹配的字段把两组数据合并起来时,这称为 连接(JOIN)。这个名字来自数据库理论。用于匹配的那个 特定字段称为连接字段(JOIN FIELD)。默认情况下, join 假定连接字段是每个文件的第一个字段, 不过正如你马上会看到的,这一点是可以改的。 为了建立连接,程序会寻找成对的行——每个文件各取 一行——它们的连接字段含有相同的值。对每一对, join 生成一行输出,由三部分组成:共同的连接 字段值、第一个文件中该行的其余部分、第二个文件中该行 的其余部分。 举个例子,看看上面两个文件的第一行。连接字段的值是 111。因此,第一行输出由 111、一个空格、 Hugh、一个空格、Mungus、一个空格和 101-555-1111 组成。(默认情况下,join 用一个空格分隔输出中的字段。) 在上面的例子中,第一个文件的每一行都能在第二个文件 中找到匹配的行。但情形未必总是如此。例如,考虑下面 这些文件。你在整理一份清单,记录朋友们的生日和他们 最喜爱的礼物。第一个文件 birthdays 包含两个 字段:名和生日:
Al May-10-1987
第二个文件 gifts 也包含两个字段:名和最喜爱 的礼物:
Al money
在这种情况下,你有 Al、Barbara 和 Dave 三个人的 生日信息和礼物信息。但你没有 Frances 和 George 的 礼物信息,也没有 Edward 的生日信息。看看使用 join 会发生什么: join birthdays gifts 因为只有三行的连接字段相匹配(Al、Barbara 和 Dave 那三行),所以输出只有三行:
Al May-10-1987 money
然而,假设你想看到所有有生日信息的人,即使他们没有 礼物信息。你可以使用 -a(all,全部)选项, 后面跟一个 1: join -a1 birthdays gifts 这是告诉 join:把 1 号文件中所有的名字都 输出,哪怕没有对应的礼物信息:
Al May-10-1987 money
同样地,如果你想看到所有有礼物信息的人(来自 2 号文件),即使他们没有生日信息,你可以用 -a2: join -a2 birthdays gifts 输出是:
Al May-10-1987 money
要列出两个文件中的所有名字,就把两个选项一起用: join -a1 -a2 birthdays gifts 输出是:
Al May-10-1987 money
当你像我们的第一个例子那样,以常规方式使用 join(不带 -a 选项)时,结果称为 内连接(INNER JOIN)。(这个术语也来自数据库理论。) 内连接的输出只来自连接字段匹配上的那些行。 当你使用 -a1 或 -a2 时,输出中会包含 连接字段没有匹配上的行。我们把这称为外连接 (OUTER JOIN)。 我不打算细讲,因为数据库理论尽管有趣,却超出了本书的 范围。我要你记住的只是:如果你打交道的是所谓的 "关系数据库",那么内连接和外连接的区别是很重要的。 接着说,如果你只想看那些匹配不上的行,你可以用 -v1 或 -v2(reverse,反向)选项。 使用 -v1 时,join 只输出 1 号文件中 没有匹配上的行,把所有匹配的都排除掉。例如: join -v1 birthdays gifts 输出是:
Frances Oct-15-1991
使用 -v2 时,你只得到 2 号文件中没有匹配上的 行: join -v2 birthdays gifts 输出是:
Charles music
当然,你可以把两个选项一起用,得到两个文件中所有 没有匹配上的行: join -v1 -v2 birthdays gifts 这时的输出是:
Charles music
由于 join 依赖数据的有序性,它有好几个选项 帮你控制排序方面的要求。首先,你可以用 -i (ignore,忽略)选项告诉 join 忽略大小写的 差别。例如,使用这个选项时,CHARLES 和 Charles 会被同样对待。 提示 你常常会用 sort 为 join 准备数据。 记住:在 sort 中,你用 -f(fold, 折叠)选项来忽略大小写的差别;而在 join 中, 你用 -i(ignore,忽略)选项。(参见本章 前面关于 "fold" 的讨论。) 我前面提到,join 假定连接字段是每个文件的 第一个字段。你可以用 -1 和 -2 选项 指定使用不同的连接字段。 要修改 1 号文件的连接字段,用 -1,后面跟上你 想使用的字段编号。例如,下面这条命令连接 data 和 statistics 两个文件,使用 1 号文件的第 3 个 字段和(默认的)2 号文件的第 1 个字段: join -1 3 data statistics 要修改 2 号文件的连接字段,使用 -2 选项。 例如,下面这条命令连接同样这两个文件,使用 1 号文件的第 3 个字段和 2 号文件的第 4 个字段: join -1 3 -2 4 data statistics 在结束关于 join 的讨论之前,我想提醒你: 由于 join 处理的是已排序数据,你得到的结果 可能取决于你的区域设置和排序序列,也就是取决于 LC_COLLATE 环境变量的值。(参见本章前面关于 区域设置的讨论。) 提示 使用 join 时最常见的错误,就是忘记给两个 输入文件排序。如果其中一个或两个文件没有相对于连接 字段正确排序,你会看到没有输出或者只有部分输出, 而且不会有任何错误信息警告你事情出了岔子。
相关过滤器:sort 考虑下面这个问题。你在安排自己晚上的活动,有好几项 约束条件:
• 你必须先洗碗,才能看电视。
如你所见,这有点让人眼花缭乱。你需要的是一份总清单, 指明每项活动该在什么时候做,使得所有约束条件都得到 满足。 用数学的语言说,这些约束条件每一个都称为偏序 (PARTIAL ORDERING),因为它们只规定了部分(是部分, 而非全部)活动的先后次序。在我们的例子里,每个偏序 规定了两项活动的次序。如果你能构造出一份总清单, 那它就是一个全序(TOTAL ORDERING),因为它规定了 所有活动的次序。 tsort 程序的工作是分析一组偏序——每个偏序 代表一条约束条件——并算出一个能满足所有约束条件的 全序。语法很简单: tsort [file] 其中 file 是文件名。 每一行输入必须是由空白(空格或制表符)分隔的一对 字符串,每一对表示一个偏序。例如,假设文件 activities 包含以下数据:
clean-dishes watch-TV
注意文件中每一行都是由空白分隔的两个字符串(在本例 中是一个空格)。每一行表示一个偏序,对应上面列出的 某一条约束。例如,第一行说你必须先洗碗才能看电视; 第二行说你必须先吃饭才能洗碗;以此类推。 tsort 程序会把这一组偏序变成一个全序。 使用这条命令: tsort activities 输出是:
shop
因此,这个问题的解决方案是:
• 采购
一般而言,只要没有环路,任何一组偏序都可以合并成 一个全序。例如,考虑下面这两个偏序:
study watch-TV
这里不可能有全序,因为如果你坚持要先看电视再学习, 你就没法做到先学习再看电视(尽管很多人都在尝试)。 如果你把这份数据交给 tsort,它会显示一条 错误信息,告诉你输入中含有环路。 名称的由来 tsort 在数学上,可以用一种称为"有向图"(directed graph) 的东西来表示一组偏序。如果图中没有环路,它就叫 "有向无环图"(directed acyclic graph),简称 DAG。 例如,树(见第 9 章)就是一个 DAG。 一旦有了 DAG,你就可以根据图中各元素的相对位置(而 不是它们的值)对这些元素排序,从而由偏序构造出全序。 事实上,tsort 就是这样完成工作的(虽然我们 不必操心其中的细节)。 在数学中,我们用 "topological"(拓扑的)一词来描述 那些依赖于相对位置的性质。因此,tsort 代表 "topological sort"(拓扑排序)。
相关过滤器:grep 要使用 strings 程序,你需要理解文本文件和 二进制文件的区别。请考虑下面三条定义: 1. 共有 96 个可打印字符:制表符、空格、标点符号、 数字和字母。任何可打印字符的序列都称为字符串 (CHARACTER STRING),或者更随便地称为串(STRING)。 例如,"Harley" 是一个长度为 6 的串。(我们在本章 前面讨论过可打印字符。) 2. 只包含可打印字符(并且每行末尾有一个换行符)的 文件称为文本文件(TEXT FILE)或 ASCII 文件 (ASCII FILE)。总体而言,Unix 过滤器是为处理文本 文件而设计的。事实上,本章里所有示例文件都是文本文件。 3. 任何非空的、又不是文本文件的文件都是二进制文件 (BINARY FILE),也就是说,任何至少包含一些非文本 数据的文件。常见的二进制文件例子有可执行程序、目标 文件、图像、声音文件、视频文件、文字处理文档、电子 表格和数据库。 如果你是程序员,你会和可执行程序与目标文件(程序的 "碎片")打交道,它们全都是二进制文件。如果你能窥视 一个可执行程序或目标文件的内部,你看到的大多是编码后 的机器指令,看起来就像毫无意义的乱码。不过,大多数 程序确实包含一些可辨认的字符串,比如错误消息、帮助 信息等等。 strings 程序是为程序员创建的工具,用来显示 嵌在可执行程序和目标文件中的字符串。例如,过去有过 一种惯例:程序员会在每个程序里插入一个字符串,标明 该程序的版本号。这样任何人都可以用 strings 从程序本身提取出它的版本。 如今,程序员和用户有了更完善的办法来跟踪这类信息(*) ,所以 strings 程序用得不多了。不过,你 不妨拿它来玩玩,看看各类二进制文件的"内部"。虽然这样 做很少有实际理由,但查一查二进制文件里藏了什么话, 是件很酷的事。语法是: strings [-length] [file...] 其中 length 是要显示的字符串的最小长度, file 是文件名,通常是一个路径名。 * 脚注 正如我们在第 10 章讨论的,大多数 GNU 工具 (Linux 和 FreeBSD 上使用的)都支持 --version 选项,用来显示版本信息。 举个例子,假设你想看看 sort 程序的内部。 首先,你用 whereis 程序找出包含该程序的文件 的路径名——也就是它的确切位置。(我们会在 第 24 章讨论路径名和 whereis,所以现在 不必在意细节。)要用的命令是: whereis sort 典型的输出是: sort: /usr/bin/sort /usr/share/man/man1/sort.1.gz 这条输出告诉我们程序和它的 man 页分别在哪里。我们 只对程序感兴趣,所以要用 strings 查看 sort 程序的内部,我们使用: strings /usr/bin/sort 这类命令通常会产生大量输出。不过,有三件事你可以做, 让输出更容易应付。 第一,默认情况下,strings 只提取长度至少为 4 个字符的字符串。这样做的目的是去掉那些短而无意义的 字符序列。即便如此,你很可能还是会看到大量并非真正 字符串的假串。不过,指定一个更长的最小长度,就能去掉 其中很多。为此,你可以使用一个由连字符(-) 后跟数字组成的选项。例如,要表明你只想看长度至少为 7 个字符的字符串(这是个不错的数字),你可以用: strings -7 /usr/bin/sort 其次,你可以把输出排序并去掉重复行。为此,只要把 输出通过管道交给 sort -iu(本章前面 讲过): strings -7 /usr/bin/sort | sort -iu 最后,如果输出多到还没读完就从屏幕上滚走了,你可以用 less(第 21 章)一屏一屏地显示输出: strings -7 /usr/bin/sort | sort -iu | less 如果你觉得"到程序内部寻找隐藏信息"这件事很吸引人, 这里有一个简便的办法,用 strings 探索各种 程序。最重要的 Unix 工具都存放在 /bin 和 /usr/bin 这两个目录里。(我们会在 第 23 章讨论这一点。)假设你想看看这些目录中 某些程序的内部。首先,输入下面两条 cd (change directory,切换目录)命令中的任意一条。这会把 你的"工作目录"改为你所选的那个目录:
cd /bin
现在用 ls(list,列出)程序显示该目录中所有 文件的列表: ls 这些文件全都是程序,你可以用 strings 查看 其中任何一个。而且,由于这些文件就在你的工作目录里, 你不必指定完整的路径名。在这种情况下,只写文件名就 够了。例如,如果你的工作目录是 /bin, date 程序就在这里,你可以用这条命令查看 date 程序的内部: strings -7 date | sort -iu | less 用这种方式,你就能在最常用的 Unix 工具里寻找隐藏的 字符串。等你试验够了,输入这条命令: cd 这会把你的工作目录改回你的主目录(第 23 章有解释)。
相关过滤器:sed tr(translate,转换)程序可以对字符执行三种 不同的操作。首先,它能把字符变成别的字符。例如,你 可以把小写字符变成大写字符,或者把制表符变成空格。 又或者,你可以把每一处数字 "0" 换成字母 "X"。 这样做时,我们说你"转换"(TRANSLATE)了这些字符。 其次,你可以规定:如果一个转换后的字符连续出现多次, 就把它们替换为单个字符。例如,你可以把连续的一个或 多个数字替换成字母 "X"。又或者,你可以把多个空格 替换成一个空格。这样做时,我们说你"压缩" (SQUEEZE)了这些字符。 最后,tr 还能删除指定的字符。例如,你可以 删除文件中所有的制表符。又或者,你可以删除所有既不是 字母也不是数字的字符。 在接下来的几节里,我们会逐一考察这些操作。不过在 开始之前,先看看语法: tr [-cds] [set1 [set2]] 其中 set1 和 set2 是字符集合。(*) * 脚注 如果你用的是 Solaris,你应当使用 Berkeley Unix 版本 的 tr。这类程序存放在 /usr/ucb 目录里, 所以你要做的只是确保这个目录排在你的搜索路径最前面。 (ucb 这个名字代表 University of California, Berkeley,加州大学伯克利分校。) 我们在第 2 章讨论 Berkeley Unix,在 第 13 章讨论搜索路径。 请注意,这个语法不允许你指定文件名,无论输入还是 输出都不行。这是因为 tr 是一个纯粹的过滤器: 它只从标准输入读取,只写到标准输出。如果你想从文件 读取,你必须重定向标准输入;如果你想写到文件里 (保存输出),你必须重定向标准输出。等你看到例子就 明白是怎么回事了。(重定向在第 15 章解释。) tr 程序执行的基本操作是转换。你指定两组字符。 tr 在读取数据时,会寻找属于第一组的字符。 每当 tr 找到这样的字符,它就用第二组中对应的 字符去替换。例如,假设你有一个名为 old 的 文件。你想把所有 "a" 字符变成 "A"。要这样做的 命令是: tr a A < old 要保存输出,只要把它重定向到一个文件,例如: tr a A < old > new 通过定义更长的字符组,你可以同时替换不止一个字符。 下面这条命令会查找并完成三种替换:"a" 替换为 "A"; "b" 替换为 "B";"c" 替换为 "C"。 tr abc ABC < old > new 如果第二组字符比第一组短,第二组的最后一个字符会被 重复使用。例如,下面两条命令是等价的:
tr abcde Ax < old > new
这两条命令都会把 "a" 换成 "A",把其余四个字符 ("b"、"c"、"d"、"e")换成 "x"。 当你指定的字符对 Shell 有特殊含义时,必须给它们加上 引号(见第 13 章),告诉 Shell 把这些字符当作 字面量处理。单引号和双引号都可以,不过在大多数情况下, 单引号效果最好。但是,如果你只引用一个字符,用反斜杠 更简单(同样见第 13 章)。 作为一般规则,把所有不是字母也不是数字的字符都加上引号 是个好主意。例如,假设你想把所有的冒号、分号和问号都 改成句点,你可以这样用: tr ':;?' \. < old > new tr 的强大之处,很大程度上来自它处理字符范围的 能力。举个例子,下面这条命令把所有大写字母改成小写: tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz <old >new 大写与小写字母之间的对应关系一目了然。可是,要把整个 字母表输入两遍实在麻烦。你可以改用连字符 (-)来定义字符范围,语法如下: start-end 其中 start 是范围的第一个字符,end 是范围的最后一个字符。 例如,前面那个例子可以改写成这样: tr A-Z a-z < old > new 范围可以是任何一组字符,只要它们在你所使用的排序序列中 构成连续的序列。(排序序列在本章前面讨论过。)例如, 下面这条命令实现了一种你可能用来给数字数据加密的密码。 数字 0 到 9 依次被替换成字母表的前九个字母,也就是 A 到 I。例如,375 会被替换成 CGE。 tr 0-9 A-I < old > new 为了方便,还有若干缩写可以代替范围使用。这些缩写称为 "预定义字符类"(predefined character class),我们在 第 20 章讨论正则表达式时会详细介绍。目前你只需要 知道:可以用 [:lower:] 代替 a-z; 用 [:upper:] 代替 A-Z;用 [:digit:] 代替 0-9。例如,下面两条命令 是等价的:
tr A-Z a-z < old > new
下面这两条命令也是等价的:
tr 0-9 A-I < old > new
(请注意,方括号和冒号是这个名称的一部分。) 就实用目的而言,这三个预定义字符类是你在 tr 程序中最可能用到的。不过,如果需要,还有 更多预定义字符类可用。完整清单见第 20 章的 Figure 20-3。 提示 与其他过滤器相比,tr 有个特别之处:它不允许你 直接指定输入文件或输出文件的名字。要从文件读取,你必须 重定向标准输入;要写入文件,你必须重定向标准输出。正因 如此,初学者使用 tr 时最常犯的错误就是忘记 重定向。例如,下面这些命令是行不通的:
tr abc ABC old
Linux 会显示一条含糊的消息,告诉你有多余的 "extra operand"(运算对象)。其他类型的 Unix 给出的 消息甚至更不说明问题。因为这个缘故,也许有一天,你会 发现自己花大量时间去琢磨 tr 命令为什么不工作。 解决办法就是永远记住:当你把 tr 用于文件时, 总是需要重定向:
tr abc ABC < old
到目前为止,我们所有的例子都很直截了当。不过,它们 多少有点刻意编排的味道。毕竟,你这一辈子有多少次需要 把冒号、分号和问号改成句点?有多少次需要把字母 "abc" 改成 "ABC"?或者用一种把数字变成字母的密码?传统上, tr 程序常用于更加冷僻的转换,而且往往涉及 不可打印字符。这里给一个典型的例子,让你有个概念。 在第 7 章中我解释过,在文本文件里,Unix 用换行 (^J)字符(*1)标记每一行的结尾,而 Windows 使用回车后跟换行(^M^J)。老版本的 Macintosh 操作系统(直到 OS 9)使用回车(^M) 字符(*2)。 * 脚注 正如我们在第 7 章讨论的,Unix 用户书写控制键名称 时,常常用 ^ 作为 "Ctrl" 的缩写。因此, ^J 指的是 <Ctrl-J>。 * 脚注 多年来,Macintosh 操作系统(Mac OS)都用 ^M 来标记一行的结尾。正如我提到的,这种情况 一直持续到 OS 9。2001 年,OS 9 被 OS X 取代,而 OS X 基于 Unix。和其他基于 Unix 的系统一样,OS X 用 ^J 来标记一行 文本的结尾。 假设你有一个来自老式 Macintosh 的文本文件。在这个文件 里,每行的结尾都用回车标记。在你把该文件用于 Unix 之前, 需要把所有回车改成换行。用 tr 来做这件事很 容易。不过,为了能这么做,你需要一种表示换行符和回车符 的办法。你有两个选择。 第一,你可以使用 tr 程序认识的特殊代码: \r 表示回车,\n 表示换行。或者,你 也可以用 \(反斜杠)后跟该字符的三位八进制 值。这样的话,\015 是回车,\012 是换行。作为参考,Figure 19-4 给出了你在 tr 中最可能用到的特殊代码和八进制值。 所谓八进制值,无非就是该字符在 ASCII 码中的 8 进制 (base 8)(*)编号。作为参考,附录 D 给出了整个 ASCII 码的八进制值。 * 脚注 一般而言,我们用 10 进制(十进制)计数,使用 0 到 9 这 10 个数字。然而,在使用计算机时,还有其他三种进位 制很重要:
• 2 进制(二进制):使用 2 个数字,0-1
我们将在第 21 章讨论这些计数系统。 Figure 19-4: tr 程序用来表示控制字符的代码 tr 程序用于转换(改变)特定字符,把它们变成 其他字符。要指定不可打印字符,你可以使用特殊代码, 也可以使用反斜杠(\)后跟该字符的三位八进制 (8 进制)值。(你可以在附录 D 中找到 ASCII 码中所有 字符的八进制值。) 本表给出四个最常用的控制字符的特殊代码和八进制值。 还有其他控制字符,但你在用 tr 时不太可能 需要它们。 注意:由于反斜杠被用作转义字符(见 第 13 章),如果你想指定一个真正的反斜杠, 必须连用两个。
现在我们来看看如何用 tr 把回车改成换行。假设 我们有一个名为 macfile 的文本文件,其中每行 都以回车结尾。我们想把所有回车改成换行,并把输出保存 到名为 unixfile 的文件里。下面两条命令中的 任何一条都能完成这项工作:
tr '\r' '\n' < macfile > unixfile
如你所见,一旦弄懂了这些代码的原理,用起来很简单。 例如,下面两条命令把文件 olddata 中所有的 制表符改成空格,并把输出保存到 newdata (*):
tr '\t' ' ' < olddata > newdata
* 脚注 当你要把制表符改成空格,或者把空格改成制表符时,通常 最好使用 expand 和 unexpand (第 18 章)。这两个程序就是专为这类改动 设计的,因此提供更灵活的功能。
到目前为止,我们讨论的是如何用 tr 做简单的 替换,也就是用一个字符替换另一个字符。现在我们要把 注意力转向更高级的话题。在此之前,先复习一下将要 使用的语法: tr [-cds] [set1 [set2]] 其中 set1 和 set2 是两组字符。 -s 选项告诉 tr:来自第一组的多个连续 相同字符应当被替换为单个字符。正如我前面提到的,这样 做时,我们说把字符"挤"(squeeze)起来了。举个例子。 下面两条命令把任何数字(0-9)替换为大写字母 "X"。 输入取自名为 olddata 的文件,输出写入名为 newdata 的文件:
tr [:digit:] X < olddata > newdata
这两条命令会把数字的每一次出现都替换成一个 "X"。例如,六位数字 120357 会被改成 XXXXXX。但是,假设你想把所有多位数字,不管 有多长,都变成单个 "X"。这时你要用 -s 选项:
tr -s [:digit:] X < olddata > newdata
这是告诉 tr 把所有多位数字挤压成单个字符。 例如,数字 120357 现在会被改成 X。 下面是一个有用的例子:我们挤压多个重复字符,但并不 真的改变字符本身。你想把连续的空格替换成单个空格。 解决办法是把空格换成空格,同时把多余的空格挤掉: tr -s ' ' ' ' < olddata > newdata 下一个选项 -d 会删除你指定的字符。因此, 使用 -d 时,你只定义一组字符。例如,要删除 所有的左括号和右括号,用: tr -d '()' < olddata > newdata 要删除所有数字,用下面任一条命令:
tr -d [:digit:] < olddata > newdata
最后一个选项 -c 最复杂,也最强大。这个选项 告诉 tr 匹配所有不在第一组中的字符 (*)。例如,下面这条命令把除空格和换行之外的所有字符 都替换成 "X": tr -c ' \n' X < olddata > newdata * 脚注 名称 -c 是 "complement"(补集)的缩写,这是 一个数学用语。在集合论中,一个集合的补集指的是所有 不属于该集合的元素。例如,就整数而言,所有 偶数构成的集合,其补集就是所有奇数构成的集合。就全部 大写字母而言,{ABCDWXYZ} 的补集是 {EFGHIJKLMNOPQRSTUV}。 这条命令的效果是保留文本的"外形",却丢掉它的含义。 比如说,假设文件 olddata 的内容是: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Do you really think you were designed to spend most of
前面那条命令会产生这样的输出:
XX XXX XXXXXX XXXXX XXX XXXX XXXXXXXX XX XXXXX XXXX XX
|
|
为了结束对 tr 的讨论,这里给出一个有趣的例子: 我们把 -c(补集)和 -s(挤压)两个选项 结合起来,统计不重复的单词。假设你写了两篇历史论文, 分别保存在名为 greek 和 roman 的文本 文件里。你想统计这两个文件中出现的不重复单词有多少。 策略如下:
• 用 cat 把文件合并起来
要让每个单词独占一行(第 2 步),我们只需要用 tr 把每一个不属于单词的字符替换成换行符。 例如,假设我们有这么几个词: As you can see 它们会变成这样:
As
为了简单起见,我们假定单词由一组 53 个不同的字符构成: 26 个大写字母、26 个小写字母,再加上撇号(也就是 单引号)。下面三条命令——随你挑一条——都能完成这项 工作:
tr -cs [:alpha:]\' "\n"
-c 选项处理不在第一组中的字符; -s 选项则把重复的字符挤掉。最终效果是:把所有 不是字母也不是撇号的字符都替换成换行符。 一旦你把单词分隔开、每行一个,给它们排序就很简单了。 只要用 sort 程序,加上 -u(unique, 唯一)去掉重复行,再加上 -f(fold,折叠)忽略 大小写的差别。然后你就可以用 wc -l 统计 行数。于是,完整的管道就是这样: |
|
cat greek roman | tr -cs [:alpha:]\' "\n" | sort -fu | wc -l 更一般地写: cat file1... | tr -cs [:alpha:]\' "\n" | sort -fu | wc -l |
|
就这样,一条 Unix 管道就能统计出一组输入文件里有多少 不重复的单词。如果你想把单词清单保存下来,只需要重定向 sort 程序的输出: cat file1... | tr -cs [:alpha:]\' "\n" | sort -fu > file
文本编辑器是一种让你能对文本行执行各种操作的程序。 通常你可以插入、删除、修改、查找等等。最重要的两个 Unix 文本编辑器是 vi(我们将在 第 22 章讨论它)和 Emacs。此外还有几个 重要性稍低、但更简单的文本编辑器,我们在 第 14 章讨论过:kedit、 gedit、Pico 和 Nano。 这些程序的共同特点是:它们都是交互式的。也就是说, 你使用它们时,先打开一个文件,然后一条接一条地输入 命令,直到做完为止。在本节中,我要向你介绍一个名为 sed 的文本编辑器,它是非交互式的。 使用非交互式文本编辑器时,你事先把命令编写好,然后把 命令交给程序,由它自动执行。非交互式文本编辑器能让你 把大量原本必须手工完成的任务自动化。 你可以用两种方式使用 sed。第一,你可以让 sed 从文件读取输入。这样你就能自动修改一个 已有的文件。例如,你可能想读入一个文件,把所有出现的 "harley" 改成 "Harley"。 第二,你可以把 sed 当作管道中的过滤器使用。 这样你就能编辑某个程序的输出。而且你还可以把 sed 的输出再管道给另一个程序做进一步处理。 开始之前,先说一点术语。想到数据在管道中从一个程序 送到另一个程序,会让人产生水沿管道流动的比喻。出于 这个原因,当数据从一个程序流向另一个程序时,我们把 它称为数据流(STREAM)。更确切地说,当数据被某个 程序读取时,我们把这些数据称为输入流(INPUT STREAM);当数据被某个程序写出时,我们把它称为输出流 (OUTPUT STREAM)。 在我们讨论过的所有过滤器中,sed 毫无争议是 最强大的。这是因为 sed 不是一个单一用途的 程序。它实际上是一门可移植、与 Shell 无关的语言的 解释器,这门语言专门用于对数据流做文本转换。名字由此 而来:sed 是 "stream editor"(流编辑器)的 缩写。 全面讨论 sed 能做的事情,超出了本书的范围。 不过,你能用 sed 完成的最有用的操作就是做 简单替换,所以我要教你的就是这个。即便如此,我略去的 内容仍然很多,所以等你有空的时候,上网找一个 sed 教程多学一些。如果你需要参考资料,可以查看 你系统上的 man 页(man sed)。 以这种方式使用 sed,语法是: sed [-i] command | -e command... [file...] 其中 command 是一条 sed 命令, file 是输入文件的名字。 为了让你看看使用 sed 是什么样子,这里给一个 典型的例子,我们把每一处出现的 "harley" 都改成 "Harley"。输入来自名为 names 的文本文件; 输出写入名为 newnames 的文件: sed 's/harley/Harley/g' names > newnames 这条命令的具体细节我马上会解释。不过首先,我们需要 谈谈输入文件和输出文件。 sed 程序每次从数据流中读取一行,按照下面三个 步骤,从头到尾处理全部数据:
默认情况下,sed 把输出写到标准输出,这意味着 sed 不会改动输入文件。有些情况下这样正好,因为 你本来就不想改原文件;你想把标准输出重定向到另一个 文件。上面那个例子就是这样:输入来自 names, 输出到 newnames,文件 names 原封未动。 然而大多数时候,你确实想修改原文件。要做到这一 点,你必须使用 -i(in-place,就地)选项。它让 sed 把输出保存到一个临时文件。等到所有数据都 成功处理完,sed 再把临时文件复制回原文件。最终 效果就是修改了原文件,但前提是 sed 顺利运行 结束、没有出错。这里是一个使用 ‑i 的典型 sed 命令: sed -i 's/harley/Harley/g' names 在这个例子里,sed 把所有出现的 "harley" 改成 "Harley",从而修改了输入文件 names。(*) * 脚注 -i 选项只在 GNU 版本的 sed 上可用。 如果你的系统不使用 GNU 工具集——例如你用的是 Solaris——你就不能用 -i。此时,要想用 sed 修改文件,你必须把输出保存到一个临时文件, 然后用 cp(复制)程序把临时文件复制到原文件, 再用 rm(删除)程序删掉临时文件。例如:
sed 's/harley/Harley/g' names > temp
换句话说,你必须手工完成 -i 选项自动替你做的 事情。 当你使用 sed -i 时要小心。你对输入文件做 的修改是永久性的,而且没有"撤销"命令。 提示 在用 sed 修改文件之前,先不带 -i 选项 运行一次程序,预览一下要做的修改,这是个好主意。例如: sed 's/xx/XXX/g' file | less 这样你可以查看输出,看看它是否如你所料。如果是,你可以 带上 -i 重新运行这条命令,真正做出修改(*): sed -i 's/xx/XXX/g' file * 脚注 Unix 有一条普遍原则:在做重要的、无法撤销的修改之前, 只要能预览,就先预览。 在第 13 章中,我们在历史列表和别名上用过 类似的策略。那两次我们都讨论过:如何先预览结果,再执行 真正的删除,从而避免误删不该删的文件。 这条原则非常重要,我要你把它记一辈子,或者记到你去世 为止(以先到者为准)。
相关过滤器:tr sed 的强大来自你能让它执行的各种操作。最重要 的操作是替换,你用 s 命令来做。语法是: [/address|pattern/]s/search/replacement/[g] 其中 address 是输入流中一或多行的地址; pattern 是一个字符串;search 是一个正则 表达式;replacement 是替换文本。 最简单的形式是:你给出一个查找串和一个替换串。例如: s/harley/Harley/ 这条命令告诉 sed:在输入流的每一行中查找字符 串 "harley";如果找到,就把它改成 "Harley"。默认 情况下,sed 每行只改第一个出现的查找串。例如, 假设输入流中有这样一行: I like harley. harley is smart. harley is great. 上面那条命令会把这一行改成: I like Harley. harley is smart. harley is great. 如果你想改掉查找串的所有出现,就在命令末尾加上后缀 g(global,全局): s/harley/Harley/g 在我们的例子里,加上 g 之后,原来那一行会变成: I like Harley. Harley is smart. Harley is great. 根据我的经验,用 sed 做替换时,你通常都希望 用 g 改掉查找串的所有出现,而不是只改每行的 第一个。这就是我在所有例子中都加上 g 后缀的 原因。 到目前为止,我们只查找了简单的字符串。不过,通过使用 所谓"正则表达式"(regular expression,常缩写为 "regex"),你可以让查找能力强得多。使用正则表达式可以 让你指定一个模式,从而获得更大的灵活性。但是正则表达式 可能很复杂,你要花一段时间才能学会用好它们。 现在我不打算讲使用正则表达式的细节。事实上,它们复杂 得——同时也强大得——让我专门用一整章来讨论,那就是 第 20 章。等你读完那一章,我希望你回到本节, 花些时间拿正则表达式和 sed 做做实验。(务必 参考 Figure 20-1、20-2 和 20-3 中那几张实用的 速查表。) 眼下,我只给你看两个把正则表达式和 sed 一起 使用的例子。先说一个:假设你有一个名为 calendar 的文件,里面记着你未来几个月的安排。 你想把所有出现的 "mon" 或 "Mon" 都改成单词 "Monday"。下面这条命令用正则表达式完成这个改动: sed -i 's/[mM]on/Monday/g' calendar 要理解这条命令,你只需要知道:在正则表达式中,记法 [...] 匹配方括号内的任意单个元素;这里 是 "m" 或 "M"。因此,查找串是 "mon" 或 "Mon"。 第二个例子稍微棘手些。本章前面讨论 tr 程序时, 我们谈过 Unix、Windows 和 Macintosh 用不同的字符来 标记一行文本的结尾:Unix 用换行(^J);Windows 用回车后跟换行(^M^J);Macintosh 用回车 (^M)。(这些字符在第 7 章有详细讨论。) 在讨论过程中,我示范过如何把 Macintosh 格式的文本文件 转换成 Unix 格式:你用 tr 把所有回车改成换行: tr '\r' '\n' < macfile > unixfile 可是,如果你有一个 Windows 格式的文本文件,又想把它 用在 Unix 上,该怎么办?换句话说,怎样把每行结尾的 "回车加换行"改成一个简单的换行?你不能用 tr, 因为你需要把两个字符(^M^J)变成一个 (^J);而 tr 只能把一个字符变成另一个 字符。 不过,我们可以用 sed,因为 sed 能把 任何东西变成任何东西。构造这条命令时,我们利用了回车符 (^M)位于行末、紧挨在换行符(^J)之前 这一事实。我们要做的只是找到并删除那个 ^M。 下面两条命令都能完成这个转换。第一条从名为 winfile 的文件读取输入,把输出写入名为 unixfile 的文件;第二条用 -i 直接修改 原文件:
sed 's/.$//' winfile > unixfile
这是怎么做到的?在正则表达式中,.(点)字符 匹配任意单个字符;$(美元符号)字符匹配行尾。 因此,查找串 .$ 指的就是换行符前面的那个字符。 仔细看看替换串。注意它是空的。这是告诉 sed 把查找串变成"什么都没有"。也就是说,我们让 sed 删除查找串。其效果就是把每行末尾那个多余 的回车字符去掉。 如果你以前从未用过正则表达式,我不指望你对最后这几条 命令觉得完全得心应手。但是我向你保证,等你读完 第 20 章,这些例子以及和它们类似的例子, 理解起来会非常容易。 提示 要用 sed 删除一个字符串,你就查找它,然后把它 替换成空。 这是一个值得记住的重要技巧,因为任何支持查找与替换操作 的程序都能用它。(实际上,你在文本编辑器里会常常用到 这个技巧。)
默认情况下,sed 对数据流中的每一行执行它的 操作。要改变这一点,你可以在命令前面加一个"地址"。 这是告诉 sed 只对该地址对应的行进行操作。地址 的语法如下: number[,number] | /regex/ 其中 number 是行号,regex 是一个正则 表达式。 最简单的形式是,地址就是一个单独的行号。例如,下面这条 命令只改数据流的第 5 行: sed '5s/harley/Harley/g' names 要指定一个行范围,就用逗号把两个行号分开。例如,下面 这条命令修改第 5 到第 10 行: sed '5,10s/harley/Harley/g' names 为了方便,你可以用 $(美元符号)字符表示数据流 的最后一行。例如,要只改数据流的最后一行,你可以用: sed '$s/harley/Harley/g' names 要修改第 5 行直到最后一行,你可以用: sed '5,$s/harley/Harley/g' names 除了指定行号,你还可以使用被 /(斜杠)字符包住 的正则表达式或字符串(*)。这是告诉 sed 只处理那些包含指定模式的行。例如,要只对 包含字符串 "OK" 的行做修改,你可以用: sed '/OK/s/harley/Harley/g' names * 脚注 正如我们将在第 20 章讨论的,字符串也被视为 正则表达式。 下面是一个更复杂的例子。这条命令只修改那些包含连续两个 数字的行: sed '/[0-9][0-9]/s/harley/Harley/g' names (记法 [0-9] 指的是 0 到 9 之间的单个数字。 细节见第 20 章。)
如我前面所说,sed 实际上是一门文本处理编程 语言的解释器。因此,你可以编写程序——包含任意多条 sed 命令——把它们存放在文件里,随时运行。 要做到这一点,你用 -f 命令来指明程序文件。 例如,要运行存放在名为 instructions 的文件中的 sed 程序,并使用名为 input 的文件中的 数据,你可以用: sed -f instructions input 遗憾的是,用 sed 编写程序超出了本书的范围。 本章我们专注于如何把 sed 当作过滤器使用。不过, 有时候你会希望 sed 执行好几项操作,实际上就是 运行一个小程序。当这种需求出现时,你可以指定任意多条 sed 命令,只要每条前面都加上 -e (editing command,编辑命令)选项。举个例子。 你有一个名为 calendar 的文件,用来记录你的日程。 文件里有一些你想展开的缩写。具体来说,你想把 "mon" 改成 "Monday"。要用的命令是: sed -i 's/mon/Monday/g' calendar 但是,你还想把 "tue" 改成 "Tuesday"。这就需要两条 独立的 sed 命令,而且每条前面都要加 -e 选项: sed -i -e 's/mon/Monday/g' -e 's/tue/Tuesday/g' calendar 到这里,你已经看出规律了:你需要七条独立的 sed 命令,一周七天每天一条。然而,这会需要一 条非常长的命令行。 正如我们在第 13 章讨论的,输入很长命令的最好 办法,是把它拆成多行。你只需要在按 <Return> 键 之前先输入一个 \(反斜杠)。反斜杠引用了换行符, 这就允许你把命令拆到不止一行上。 举个例子,下面这条很长的 sed 命令会改掉一周七 天的所有缩写。注意除最后一行以外,各行都是续行。你这里 看到的,实际上是一条非常长的命令行:
sed -i \
提示 当你输入 \<Return> 来续行时,大多数 Shell 会显示一个特殊的提示符,称为次级提示符(SECONDARY PROMPT),表示一条命令正在续写。 在 Bourne Shell 家族(Bash、Korn Shell)中,默认的 次级提示符是一个 >(大于号)字符。你可以 通过修改 PS2 Shell 变量来改变次级提示符(尽管 大多数人不会这么做)。 在 C-Shell 家族中,只有 Tcsh 有次级提示符。默认情况下 它是一个 ?(问号)。你可以通过修改 prompt2 Shell 变量来改变次级提示符。 (修改 Shell 变量的命令在第 12 章解释。把 这类命令放进你的初始化文件,在第 14 章讨论。)
复习问题 #1: 在所有过滤器中,grep 是最重要的。grep 做什么? 为什么它在管道中格外有用? 解释下列选项的含义:-c、-i、-l、 -L、-n、-r、-s、 -v、-w 和 -x。 复习问题 #2: sort 程序能完成哪两项任务? 解释下列选项的含义:-d、-f、-n、 -o、-r 和 -u。 为什么 -o 选项是必要的? 答案 sort 程序能够 (1) 给数据排序, (2) 复习问题 #3: 什么是排序序列?什么是区域设置?两者之间有什么联系? 复习问题 #4: uniq 程序能完成哪四项任务? 复习问题 #5: tr 程序能完成哪三项任务? 使用 tr 时,你用什么特殊代码来表示:退格、制表符、 换行/走纸、回车和反斜杠。 应用你的知识 #1: 正如我们将在第 23 章讨论的,/etc 目录用来存放 配置文件(在第 6 章解释)。请创建一条命令,查看 /etc 目录中的所有文件,搜索包含单词 "root" 的 行。输出应当一屏一屏地显示。提示:要指定文件名,请使用 模式 /etc/*。 在 /etc 目录的文件中搜索时,会产生一些多余的 错误消息。请创建该命令的第二个版本,把所有这类消息屏蔽 掉。 应用你的知识 #2: 有人跟你打赌,说如果你不使用词典,就找不出超过 5 个以 字母 "book" 开头的英文单词。不过允许你使用一条 Unix 命令。你该用哪条命令? 应用你的知识 #3: 你在自己的学校里经营一个在线婚恋服务。你有三个存放用户 注册信息的文件:reg1、reg2 和 reg3。在这些文件里,每一行包含一个人(单身也 算一个人,我可不是有意双关)的信息。 请创建一条管道,处理全部三个文件,只选出包含 "female" 或 "male" 这两个词(随你挑)的行。去掉所有重复之后, 结果应当保存到名为 prospects 的文件里。 做完之后,再创建第二条管道,显示所有登记过不止一次的人 (不论男女)的清单。提示:在文件中查找重复行。 应用你的知识 #4: 你有一个名为 data 的文本文件。请创建一条管道, 显示所有叠词(double word)实例,例如 "hello hello"。 (假设一个"词"由连续的大写或小写字母构成。) 提示:先创建一个所有单词的清单,每行一个词。然后把输出 管道给一个能查找连续相同行的程序。 进一步思考 #1: 在前面的一道题目里,我指出 grep 是最重要的 过滤器,并要求你解释它在管道中为什么格外有用。结合你对 那道题的回答想一想:人性的什么特点,让 grep 显得如此强大、如此有用? 进一步思考 #2: 最初,Unix 以美式英语和美国的数据处理标准(例如 ASCII 码)为基础。随着国际化工具和标准(例如区域设置)的发展, Unix 如今可以被来自各种文化背景的人使用。这些用户能够 用自己的语言、按照自己习惯的数据处理约定与 Unix 交互。 以这种方式扩展 Unix,有哪些取舍?请列出三条好处和三条 坏处。 进一步思考 #3: 本章我们详细讨论了 tr 和 sed 两个程序。 你可以看到,它们都非常有用。然而,它们都是复杂的工具, 要掌握得花不少时间和精力。 对某些人来说,这不是问题。但对许多其他人来说,花时间把 一个复杂工具用好,是一种令人不太舒服的体验。你认为这是 为什么? 所有的工具都应当设计得容易学会吗? 进一步思考 #4: 请就下面这句话发表看法:在整个 Unix 工具箱里,没有任何 一个程序,学会它所花的时间会比把钢琴弹好所需的时间更长。
© 本书全部内容,2026 年版权所有,Harley Hahn
|