捐赠?

Harley Hahn
个人主页

给 Harley
发送留言


来自 Harley Hahn 的
个人寄语

Unix 之书
主页

搜索

章节列表

目录

插图列表

各章...
   1   2   3
   4   5   6
   7   8   9
  10  11  12
  13  14  15
  16  17  18
  19  20  21
  22  23  24
  25  26

术语表

各附录...
  A  B  C
  D  E  F
  G  H

命令
摘要...

• 按字母顺序
• 按类别

Unix-Linux
年表

互联网
资源

错误与
勘误

推荐语


教师
与学生
资料...

主页
与概述

练习
与答案

Unix 模范
课程与
课程大纲

教师用
PowerPoint 文件

第 19 章...

过滤器:选择、排序、
合并与修改 (Filters: Selecting, Sorting, Combining, and Changing)

在本章中,我们将以 Unix 工具箱里最有趣、也最强大的 那些过滤器来结束对过滤器的讨论:也就是挑选数据、 给数据排序、合并数据和修改数据的程序。这些程序 极为有用,值得我们花时间详细谈谈。

你已经知道,强大的程序往往要花很长时间才能学会, 本章要讨论的这些过滤器尤其如此。事实上,这些程序 如此强大,你大概永远无法把它们所有的细微之处 都掌握殆尽。

这没什么关系。我会确保你理解基础知识, 并且会给你展示大量例子。随着你的技能和需求 不断成长,你可以随时查阅在线手册,了解更高级的 细节,也可以利用 Web 和 Usenet 向其他人求助。 最重要的是:每当你有机会和一位 Unix 高手面对面 交流,一定要让他给你展示他们使用本章这些过滤器的 看家绝活。这是学习 Unix 最好的方式。

这是专门讲解过滤器的四章(第 16-19 章)中的最后一章。 在第 20 章中,我们将讨论正则表达式,它用来 指定模式。正则表达式能极大地增强过滤器的能力, 在第 20 章里,你会找到许多与本章过滤器 相关的例子,尤其是 grep——也许是所有 过滤器中最重要的那一个。

跳至页首

选择包含特定模式的行:grep
(Selecting Lines That Contain
a Specific Pattern: grep)

相关过滤器:lookstrings

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
User     tty      login@  idle   JCPU   PCPU  what
tammy    ttyp0   Wed10am 4days  42:41  37:56  -bash
harley   ttyp1    5:47pm        15:11         w
linda    ttyp3    5:41pm    10   2:16     13  -tcsh
casey    ttyp4    4:45pm         1:40   0:36  vi dogstuff
weedly   ttyp5    9:22am  1:40     20      1  gcc catprog.c

假设你想显示所有在下午或晚上登录的用户。你可以搜索 输出中包含 "pm" 这个模式的行。使用这条管道:

w -h | grep pm

(注意我用的是带 -h 选项的 w。 这个选项会去掉表头,也就是前两行。)利用上面的数据, 前一条命令的输出是:

harley   ttyp1    5:47pm        15:11         w
linda    ttyp3    5:41pm    10   2:16     13  -tcsh
casey    ttyp4    4:45pm         1:40   0:36  vi dogstuff

假设我们只想显示下午和晚上登录的那些用户的用户标识。 我们要做的,就是把 grep 的输出通过管道交给 cut(第 17 章),提取数据的第 1 到第 8 列:

w -h | grep pm | cut -c1-8

输出是:

harley
linda
casey

那要不要把输出排序呢?只要通过管道交给 sort(本章后面会讲到):

w -h | grep pm | cut -c1-8 | sort

输出是:

casey
harley
linda

名称的由来

grep


在 20 世纪 70 年代初,最早几版 Unix 所用的文本 编辑器名叫 ed。在 ed 内部有一条命令, 它能在文件中搜索所有包含指定模式的行,然后把 这些行打印到终端上。(在那个年代,Unix 用户用的是 把输出打印在纸上的终端。)

这条命令名叫 g,取自 global(全局), 因为它能搜索整个文件。当你用 g 打印所有 包含某个模式的行时,语法是:

g/re/p

其中 g 代表 "global";re 是一个 正则表达式,用来描述你想搜索的模式;p 代表 "print"(打印)。

grep 这个名字,正是从这个歪打正着的缩写里 来的。换句话说,grep 代表的是:

Global(全局):表明 grep 会搜索全部输入数据。

Regular Expression(正则表达式):表明 grep 可以搜索任何能用正则表达式表达的模式(详见第 20 章)。

Print(打印):一旦 grep 找到了你想要的东西,它就会替你打印(显示)出来。正如我们在第 7 章讨论的,出于历史原因,我们常常用 "print" 表示 "display"(显示)。

在 Unix 圈子里,人们常把 "grep" 当动词用,既有 技术含义,也有日常含义。所以你可能会听人说: "我把你的邮件地址弄丢了,只好把自己的文件全都 grep 一遍,才找到你的电话号码。"或者:"我在客厅里 翻了好几遍(grepped),还是没找到借给我的那本书。"

跳至页首

grep 最重要的几个选项
(The Most Important grep Options)

grep 程序有很多选项,我只讨论其中最重要的 几个。先说 -c(count,计数)选项,它显示 被提取出的行数,而不是行本身。下面举个例子。

正如我们将在第 23 章讨论的,Unix 文件系统 使用目录,目录与 Windows 和 Macintosh 上使用的 文件夹类似(但并不相同)。一个目录既可以包含普通文件, 也可以包含其他目录,后者称为子目录。例如,某个目录 可能包含 20 个文件和 3 个子目录。

你在第 24 章会看到,可以用 ls 命令 显示某个目录中包含的文件和子目录的名称。例如, 下面这条命令显示名为 /etc 的目录的内容。 (读过第 23 章之后,你就会明白 /etc 这个名字的含义了。)

ls /etc

如果你在自己的系统上运行这条命令,就会发现 /etc 里有很多条目。要想知道哪些条目是 子目录,请使用 -F 选项:

ls -F /etc

使用这个选项时,ls 会在所有子目录名的末尾 加上一个 /(斜杠)字符。例如,假设你在输出中 看到:

motd
rc.d/

这就意味着 motd 是一个普通文件,而 rc.d 是一个子目录。

假设你想统计 /etc 目录里子目录的个数。 你要做的,就是把 ls -F(*)的输出通过 管道交给 grep -c,数一数斜杠的个数:

ls -F /etc | grep -c "/"

* 脚注

默认情况下,ls 会在每一行列出多个名称, 让输出更紧凑。不过,当你把输出通过管道交给另一个 程序时,ls 会把每个名称单独放在一行。 如果你想模拟这种行为,请使用 -1 (数字 1)选项,例如:

ls -1 /etc

在我的系统上,输出是:

92

顺便说一下,如果你想统计一个目录里所有条目的总数, 只要把 ls 的输出通过管道交给 wc -l(第 18 章)即可,例如:

ls /etc | wc -l

在我的系统上,/etc 目录里有 242 个条目。

下一个选项 -i 告诉 grep,在比较时 忽略大小写的差别。例如,假设一个名为 food-costs 的文件包含下面五行:

pizza $25.00
tuna $3.50
Pizza $23.50
PIZZA $21.00
vegetables $18.30

下面这条命令找出所有包含 "pizza" 的行。注意, 按照 grep 的语法,模式要写在文件名之前:

grep pizza food-costs

输出只有一行:

pizza $25.00

要忽略大小写的差别,就用 -i:

grep -i pizza food-costs

这一次,输出包含三行:

pizza $25.00
Pizza $23.50
PIZZA $21.00

— 提示 —

-i(ignore,忽略)选项让 grep 忽略大小写的 差别。本章后面我们还会讨论另外两个程序,looksort,它们也有类似的选项。只不过在这 两个程序里,你用的不是 -i,而是 -f (fold,折叠)。别搞混了。

("fold"这个词是一个术语,表示应当把大写和小写 字母同等看待。我们后面会讲到它。)

接下来,有时候你会想知道被选中的行在数据流中的位置。 这时就用 -n 选项。它告诉 grep 在每行输出前面写上一个相对行号。(你的数据本身 不必含有这些编号;grep 在处理输入时会替你 数行数。)举个例子,考虑下面这条命令,它针对前面 列出的 food-costs 文件同时使用了 -i-n 选项:

grep -in pizza food-costs

输出是:

1:pizza $25.00
3:Pizza $23.50
4:PIZZA $21.00

当你需要确定某些行在一个大文件中的确切位置时, -n 选项很有用。例如,假设你想修改所有包含 某个特定模式的行。一旦你用 grep -n 找到这些行的位置,就可以用文本编辑器直接跳到你 想改动地方。

下一个选项 -l(list filenames,列出文件名) 在你想要在多个文件中查找某个模式时很有用。使用 -l 时,grep 不显示包含该模式的行, 而是写出找到这些行的文件名。

例如,假设你有三个文件:namesoldnamesnewnames。恰好 names 里含有 "harley";newnames 里 含有 "Harley";而 oldnames 里两者都没有。 要查看哪些文件包含模式 "Harley",你可以用:

grep -l Harley names oldnames newnames

输出是:

newnames

再加上 -i 选项以忽略大小写的差别:

grep -il Harley names oldnames newnames

这时的输出是:

names
newnames

-L(大写 "L")选项的作用与 -l 相反。它列出包含匹配内容的文件。在我们的 例子里,要列出不含模式 "Harley" 的文件,你可以用:

grep -L Harley names oldnames newnames

输出是:

names
oldnames

下一个选项 -w 表示你只想查找完整的单词。 例如,假设你有一个名为 memo 的文件, 包含以下几行:

We must, of course, make sure that all the
data is now correct before we publish it.
I thought you would know this.

你想显示所有包含单词 "now" 的行。如果你输入:

grep now memo

你会看到:

data is now correct before we publish it.
I thought you would know this.

这是因为 grep 把 "now" 和 "know" 都选中了。 但如果你输入:

grep -w now memo

你就只会看到你想要的那行输出:

data is now correct before we publish it.

-v(reverse,反向)选项选出所有 包含指定模式的行。这是一个特别有用的选项,你会发现自己 频繁地使用它。举个例子,假设你是个学生,有一个名为 homework 的文件用来记录你的作业。这个文件 里每份作业占一行。一旦你完成了某份作业,就在后面 标上 "DONE"。例如:

Math: problems 12-10 to 12-33, due Monday
Basket Weaving: make a 6-inch basket, DONE
Psychology: essay on Animal Existentialism, due end of term
Surfing: catch at least 10 waves, DONE

要列出所有还没有完成的作业,输入:

grep -v DONE homework

输出是:

Math: problems 12-10 to 12-33, due Monday
Psychology: essay on Animal Existentialism, due end of term

如果你想看看还有多少份作业没完成,把 -c-v 合起来用:

grep -cv DONE homework

在这种情况下,输出是:

2

有时候,你可能想找出那些"搜索模式正好等于整行"的行。 这时就用 -x 选项。例如,假设 names 文件包含以下几行:

Harley
Harley Hahn
My friend is Harley.
My other friend is Linda.
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
Permission denied

如果你不想看到这类信息,使用 -s(suppress, 屏蔽)选项。例如,假设你以超级用户身份登录,想在 系统的所有文件中查找 "shutdown now" 这几个词。

正如我们将在第 23 章讨论的,/ 指的是 整个文件系统的根(主要)目录。因此,如果我们从 / 目录出发并使用 -r(递归)选项, grep 就会搜索整个文件系统。命令是:

grep -rs / 'shutdown now'

注意我给搜索模式加上了引号,因为它含有空格。 (引用在第 13 章中解释。)

跳至页首

grep 的变体:fgrepegrep
(Variations of grep: fgrep egrep)

在早年间(20 世纪 70 年代和 80 年代),人们常常 使用 grep 的另外两个版本:fgrepegrep

fgrep 程序是 grep 的快速版本,它只查找 "固定字符"字符串。(因此得名 fgrep。)也就是 说,fgrep 不允许使用正则表达式来匹配模式。 在计算机很慢、内存很有限的年代,只要你不需要正则 表达式,fgrep 就比 grep 更高效。如今 计算机很快,内存也很大,所以已经没必要使用 fgrep 了。我提到它只是出于历史原因。

egrep 程序是 grep 的扩展版本。(因此 得名 egrep。)最初的 grep 只支持 "基本正则表达式"。后来出现的 egrep 支持功能 更强的"扩展正则表达式"。二者的区别我们会在 第 20 章讨论。目前你只需要知道:扩展正则 表达式更好,只要有得选,你就应该始终使用它们。

现代的 Unix 系统允许你通过 egrepgrep -E 来使用扩展正则表达式。不过, 大多数有经验的 Unix 用户更愿意直接键入 grep。 解决办法是创建一个别名(见第 13 章),把 grep 改成 egrepgrep -E。 在 Bourne Shell 家族中,你会用下面两条命令之一:

alias grep='egrep'
alias grep='grep -E'

在 C-Shell 家族中,你会用下面这些命令之一:

alias grep 'egrep'
alias grep 'grep -E'

一旦定义了这样的别名,你键入 grep 就能获得 扩展正则表达式的完整功能。要让这个改动永久生效, 你只需把相应的 alias 命令放进你的环境文件 (见第 14 章)。这个别名确实非常实用,所以我 建议你现在就花一点时间把它加到自己的环境文件里。 事实上,等你读到第 20 章时,我会假定你用的 就是扩展正则表达式。

注意:如果你用的是 Solaris(来自 Sun Microsystems), 你想要的 egrep 版本位于一个名为 /usr/xpg4/bin/(*)的特殊目录里,这意味着你 必须使用不同的别名。下面的例子只适用于 Solaris。 第一条是给 Bourne Shell 家族用的;第二条是给 C-Shell 家族用的:

alias grep='/usr/xpg4/bin/egrep'
alias grep '/usr/xpg4/bin/egrep'

* 脚注

xpg4 这个名字代表 "X/Open Portability Guide, Issue 4"(X/Open 可移植性指南第 4 版),那是一项关于 Unix 系统应如何表现的古老(1992 年)标准。这个目录里 的程序经过修改,以便按照 XPG4 标准行事。

跳至页首

选择以特定模式开头的行:
look
(Selecting Lines Beginning With a Specific Pattern: look)

相关过滤器:grep

look 程序在按字母顺序排列的数据中搜索, 找出所有以指定模式开头的行。

使用 look 有两种方式。你可以使用来自一个或 多个文件的已排序数据,也可以让 look 搜索 字典文件(下一节会解释)。

当你用 look 搜索一个或多个文件时,语法是:

look [-df] pattern [file...]

其中 pattern 是要查找的模式,file 是文件名。

这里举个例子。你是一所学校的学生,那里每学期所有 学生都要给自己的教授打分。这学期由你负责这件事。 你有一个名为 evaluations 的大文件,里面 汇总了一百多位教授的评分结果。数据是按字母顺序 排列的。文件的每一行包含一个等级(A、B、C、D 或 F), 后面跟两个空格,再后面是一位教授的姓名。例如:

A  William Wisenheimer
C  Peter Pedant
F  Norman Knowitall

你的任务是做出五个列表,贴到一个网站上去。这些 列表应该分别包含得到 A 等、B 等等等的教授姓名。 由于数据是按字母顺序排列的,你可以用 look 选出文件中所有以 A 开头的行,从而做出 第一个列表(A 等教授):

look A evaluations

虽然这条命令能完成工作,我们还可以把它改进一下。 如我所说,数据文件中每一行都以一个单字母等级开头, 后面跟两个空格。拿到你想要的姓名之后,你可以用 colrm(第 16 章)去掉每行的前三个字符。 下面的例子针对每个等级都做了这件事:它们从数据文件 中选出相应的行,用 colrm 去掉每行的前三个 字符,然后把输出重定向到一个文件:

look A evaluations | colrm 1 3 > a-professors
look B evaluations | colrm 1 3 > b-professors
look C evaluations | colrm 1 3 > c-professors
look D evaluations | colrm 1 3 > d-professors
look F evaluations | colrm 1 3 > f-professors

与本章介绍的其他程序不同,look 不能从 标准输入读取:它的输入必须来自一个或多个文件。 这就是说,严格来讲,look 并不是过滤器。

这个限制的原因在于,对于标准输入,程序每次只能 读取一行。而 look 使用的是一种称为 "二分查找"(binary search)的搜索方法,它要求 一次性访问全部数据。因此,你不能把 look 用在管道中间,尽管你可以把它用在管道的开头。

当你有多个步骤时,最好的策略是先准备好数据,把它 存进一个文件,然后用 look 搜索这个文件。 例如,假设 froshsophjuniorsenior 这四个文件包含前面描述的那种未经 排序的原始评分数据。在你用 look 搜索这些数据 之前,你必须把四个文件的内容合并并排序,把输出保存 到一个新文件里,例如:

sort frosh soph junior senior > evaluations
look A 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 -df A evaluations

跳至页首

什么时候用 look,
什么时候用 grep?
(When Do You Use look and When Do You Use grep?)

lookgrep 都能根据指定模式从文本 文件中挑选行。因此,问一问"什么时候用 look, 什么时候用 grep?"是合乎情理的。

许多场合都会出现类似的问题,因为 Unix 往往为同一个 问题提供不止一种解决办法。正因如此,能够明智地分析 你手中的选项很重要,这样你才能为当前的任务挑到最合适 的工具。我们举个例子,把 lookgrep 做个比较。

look 程序在三个重要方面受到限制。第一,它 要求输入是已排序的;第二,它只能从文件读取,不能从 标准输入读取;第三,它只能查找位于行首的模式。不过, 在这些限制范围内,look 有两个优点:用起来 简单,而且非常快。

grep 程序灵活得多:它不要求已排序的输入; 它既可以从文件读取,也可以从标准输入读取(这意味着 你可以把它用在管道中间);它可以在任意位置查找模式, 而不局限于行首。

此外,grep 支持"正则表达式",让你能够 指定泛化的模式,而不仅仅是简单的字符。例如,你可以 查找"字母 har,后面跟一个或多个字符,再后面 跟字母 ley,再后面跟零个或多个数字"。 (正则表达式非常强大,我们会在 20 章详细 讨论它们。)

通过使用正则表达式,你完全可以让 grep 做出 look 能做的一切。只不过 grep 会更慢, 语法也更别扭。

所以我给你的建议是:每当你需要从文件中挑选行时, 先问问自己 look 能不能胜任。如果能,就用它, 因为 look 既快又简单。如果 look 干不了这活(大多数时候都是如此),就用 grep。 作为一般规则,你应该始终用最简单的办法来解决问题。

那么速度呢?我提到过 lookgrep 快。这有多重要?在 Unix 的早期,速度是一个重要的 考虑因素,因为 Unix 系统是与其他用户共享的,计算机 也相对较慢。当你从一个非常大的文件里挑选文本行时, 你确实能察觉 lookgrep 之间的差别。

然而在今天,几乎所有 Unix 系统都运行在快得惊人的 计算机上,在实际使用中都是如此。因此,Unix 执行 一条命令的速度——至少对本章这些命令而言—— 是无关紧要的。例如,本章中的任何例子都会快得仿佛 瞬间完成。更具体地说,如果你把一条 look 命令和与之等价的 grep 命令相比,你根本 不可能察觉速度上的差异。

所以我的建议是:根据简洁性和易用来挑选工具,而不是 根据速度或效率上那点微乎其微的差别。当你在编写程序, 包括 shell 脚本时,这一点尤其重要。如果某个程序或 脚本太慢,通常都能找出一两个瓶颈并让它提速。但如果 一个程序复杂得没必要,或者难以使用,那么从长远看, 它会浪费大量你自己的时间,而你的时间远比 计算机的时间宝贵。

— 提示 —

只要工具有得选,就用能完成这件工作的最简单 的那个。

跳至页首

找出所有以特定模式开头的词:
look
(Finding All the Words That Begin With a Specific Pattern: look)

我前面提到,你可以用 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
simultaneous
simultaneously
simultaneousness
simulty

现在,挑出正确的词,并且——如果你愿意—— 从一个窗口复制粘贴到另一个窗口,就是很简单的事了。 (关于如何复制粘贴的说明,请见第 6 章。)

我们会在第 20 章再次谈到字典文件,到时我会告诉 你它在系统上的位置,以及如何用它帮助解答文字谜题。

(顺带说一句,"simulty" 指的是私人之间的积怨或口角。)

— 提示 —

当你使用 vi 文本编辑器(见第 22 章) 时,你可以用 :r! 发出一次快速的 look 命令来显示一份单词列表。例如:

:r !look simult

这条命令会把所有以 "simult" 开头的单词插入到你的 编辑缓冲区中。现在你可以挑出你想要的那个词, 把其他全部删掉。

跳至页首

给数据排序:sort
(Sorting Data: sort)

相关过滤器:tsortuniq

sort 程序能完成两项相关的任务:给数据排序, 以及检查数据是否已经排好序。我们先从基础讲起。 给数据排序的语法是:

sort [-dfnru] [-o outfile] [infile...]

其中 outfile 是存放输出的文件名,infile 是包含输入的文件名。

sort 程序非常灵活。你可以比较整行,也可以 只比较每行中选定的部分(字段)。使用 sort 最简单的方式,是对单个文件排序、比较整行,并把结果 显示在你的屏幕上。举个例子,假设你有一个名为 names 的文件,包含以下四行:

Barbara
Al
Dave
Charles

要对这些数据排序并显示结果,输入:

sort names

你会看到:

Al
Barbara
Charles
Dave

要把排好序的数据保存到一个名为 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 完成为止,然后输出才被写入该文件。

要对来自多个文件的数据排序,只要指定多个输入文件名 即可。例如,要把 oldnamesnamesextranames 三个文件的合并内容排序,并把输出 保存到 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 -dfn
(Controlling the Order in Which Data Is Sorted: sort -dfn)

你可以用好几个选项来控制 sort 程序的行为:

-d(dictionary,字典)只考察字母、数字和 空白字符(空格和制表符)。当你的数据中含有会妨碍排序 过程的字符(例如标点)时,使用这个选项。

-f(fold,折叠)选项把小写字母当作大写 字母处理。当你想忽略大小写区别时使用这个选项。例如, 使用 -f 时,harleyHarley 都被认为与 HARLEY 相同。("fold"这个术语的 含义在下面解释。)

-n(numeric,数值)选项识别行或字段开头的 数字,并按数值大小排序。这样的数字可以包含前导空格、 负号和小数点。用这个选项告诉 sort 你用的 是数值数据。例如,假设你要排序:

11
2
1
20
10

如果你使用不带任何选项的 sort,输出是:

1
10
11
2
20

如果你使用 sort -n,你得到:

1
2
10
11
20

-r(reverse,反向)选项按相反的顺序排序。 例如,如果你对上一个例子中的数据用 sort -nr 排序,输出是:

20
11
10
2
1

以我的经验,你会发现自己使用 -r 选项的频率 比想象中高得多。这是因为按逆字母顺序或逆数值顺序 列出信息是很有用的。

最后一个选项 -u(unique,唯一)告诉 sort 检查完全相同的行,只保留其中一行。例如, 假设你用 sort -u 对以下数据排序:

Barbara
Al
Barbara
Barbara
Dave

输出是:

Al
Barbara
Dave

— 提示 —

除了 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 -c
(Checking If Data Is Sorted: sort -c)

如我前面所说,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
Max Out
Patty Cake
Patty Cake
Shirley U. Jest

你输入:

sort -cu friends

虽然数据是排好序的,但 sort 发现了一个重复行:

sort: friends:4: disorder: Patty Cake

跳至页首

ASCII 码;
排序序列
(The ASCII Code; Collating Sequences)

假设你用 sort 程序对以下数据排序。输出会 是什么?

zzz
ZZZ
bbb
BBB
aaa
AAA

在某些系统上,你会得到:

AAA
BBB
ZZZ
aaa
bbb
zzz

在另一些系统上,你会得到:

AAA
aaa
BBB
bbb
ZZZ
zzz

这怎么可能?在 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:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
`abcdefghijklmnopqrstuvwxyz{|}~

可打印字符的排列顺序就是我列出的顺序。它们从第 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 参考页的办法取决于你所使用的系统。

Unix 类型 显示 ASCII 码页的命令
Linuxman ascii
FreeBSDless /usr/share/misc/ascii
Solarisless /usr/pub/asciiman 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。

编号 字符
0-31控制字符(包括制表符)
32空格字符
33-47符号: ! " # $ % & ' ( ) * + , - . /
48-57数字: 0 1 2 3 4 5 6 7 8 9
58-64更多符号: : ; < = > ? @
65-90大写字母: A B C ... Z
91-96更多符号: [ \ ] ^ _ `
97-122小写字母: a b c ... z
123-126更多符号: { | } ~
127空控制字符(del)

熟悉 ASCII 码的排序序列很重要,因为许多 Unix 系统 和编程语言都默认使用它。你不必把整个 ASCII 码都背 下来,但你确实需要记住三条基本原则:

• 空格排在数字之前。
• 数字排在大写字母之前。
• 大写字母排在小写字母之前。

这里举个例子。假设你的系统使用 ASCII 排序序列。 你用 sort 程序对以下数据排序(其中第三行 以一个空格开头):

hello
Hello
 hello
1hello
:hello

输出是:

 hello
1hello
:hello
Hello
hello

— 提示 —

至于 ASCII 码中字符的顺序,你需要记住的只是:空格、 数字、大写字母、小写字母,按这个顺序排列。

只要记住 "SNUL" 就行了。(*)

* 脚注

如果你记不住 SNUL 这个首字母缩写,让我教你一个许多 聪明人都在用的记忆诀窍。你要做的,就是把你想记住的 东西和你的日常生活联系起来。

例如,假设你是一位专攻差分计算的数学家,恰好正在 处理四阶差分方程,而这些方程是由那些在特殊的 非均匀格(lattices)上正交的 Laguerre-Hahn 多项式 所满足的。要记住 SNUL,你只要想想 "special non- uniform lattices"(特殊非均匀格)就够了。

看到没,当个聪明人是多么容易?

跳至页首

区域设置与排序序列
(Locales and Collating Sequences)

在 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: Cen_US 区域设置的排序序列

在美国,Unix 和 Linux 系统使用两个区域设置之一: 基于 ASCII 码的 C/POSIX,或基于美式英语的 en_US。下面两张表展示了这两种区域设置各自的 排序序列。在 C 排序序列中(与 C 区域设置 一起使用),数字、小写字母和大写字母之间隔着符号。 在词典排序序列中(与 en_US 区域设置一起使用), 所有符号都在最前面,其后才是数字和字母。

注意:在两张表中,我都用一个点() 来表示空格字符。

C 区域设置:C 排序序列
空格字符
符号! " # $ % & ' ( ) * + , - . /
数字0 1 2 3 4 5 6 7 8 9
更多符号: ; < = > ? @
大写字母A B C D E F G H I J K L M
N O P Q R S T U V W X Y Z
更多符号[ \ ] ^ _ `
小写字母a b c d e f g h i j k l m
n o p q r s t u v w x y z
更多符号{ | } ~
en_US 区域设置:词典排序序列
符号` ^ ~ < = > |
空格字符
更多符号_ - , ; : ! ? / . ' " ( )
[ ] { } @ $ * \ & # % +
数字0 1 2 3 4 5 6 7 8 9
字母a A b B c C d D e E f F g G
h H i I j J k K l L m M n N
o O p P q Q r R s S t T u U
v V w W x X y Y z Z

为了说明区域设置的选择会造成什么影响,我们来看一个 例子:对以下数据排序时(其中第三行以空格开头)会发生 什么:

hello
Hello
 hello
1hello
:hello

C 区域设置(C 排序序列)下,输出是:

 hello
1hello
:hello
Hello
hello

en_US 区域设置(词典排序序列)下, 输出是:

 hello
:hello
1hello
hello
Hello

那么你应该用哪个区域设置?以我的经验,如果你使用 en_US 区域设置,迟早会遇到一些难以追查的 意外问题。例如,正如我们将在第 25 章讨论的, 你用 rm(remove,删除)程序删除文件。假设你 想删除所有文件名以大写字母开头的文件。传统上要用的 Unix 命令是:

rm [A-Z]*

如果你用的是 C 区域设置,这条命令会正常 工作。然而,如果你用的是 en_US 区域设置, 结果你会把所有以任意大写或小写字母开头的文件都删掉, 除了以字母 a 开头的以外。(不必在意细节, 第 20 章会解释。)(*)

* 脚注

还有很多场合,C 区域设置比 en_US 区域设置更好用。再举一例:你正在写一个 C 或 C++ 程序。在你的目录里,你有一些代码文件的文件名全部 是小写字母,例如 program1.cprogram2.cppdata.h 等等。你还有一些其他文件,名字以大写 字母开头,例如 MakefileRCSREADME。当你用 ls 程序(第 24 章) 列出目录内容时,所有"大写"开头的文件会排在最前面, 从而把其他文件和代码文件区分开。

我的建议是把默认值设为 C 区域设置,因为它使用 传统的 ASCII 排序序列。从长远看,比起使用 en_US 区域设置和词典排序序列,这带来的麻烦 更少。事实上,在你阅读本书的过程中,我假定你用的 就是 C 区域设置。

那么,如何指定你的区域设置?第一步是弄清你的系统 默认使用哪种排序序列。如果 C 区域设置已经 是你系统的默认值,那就很好。如果不是,你就需要改。

确定默认排序序列的一个办法,是输入 locale 命令并查看 LC_COLLATE 环境变量的值。它是 CPOSIX?还是 en_US 的 某个变体?

确定默认排序序列的另一个办法,是做下面这个简短的 测试。用这条命令创建一个名为 data 的小文件:

cat > data

键入以下三行,然后按 ^D 结束命令:

AAA
[]
aaa

现在对这个文件的内容排序:

sort data

如果你使用的是 C/POSIX 区域设置, 输出将按 C(ASCII)排序序列排列:

AAA
[]
aaa

如果你使用的是 en_US 区域设置,输出将按词典 排序序列排列:

[]
aaa
AAA

在你继续往下读之前,花一点时间看看 Figure 19-3 中的排序序列,确认这些例子你能想通。

如果你的 Unix 系统默认使用 CPOSIX 区域设置,你什么都不必做。(不过,请把本节剩下的 内容读完,因为总有一天,你会在别的系统上碰到这个 问题。)

如果你的系统使用 en_US 区域设置,你需要把 LC_COLLATE 环境变量改成 CPOSIX。在 Bourne Shell 家族中,下面两条命令 中的任何一条都能完成这件事:

export LC_COLLATE=C
export LC_COLLATE=POSIX

在 C-Shell 家族中,你会用:

setenv LC_COLLATE C
setenv LC_COLLATE POSIX

要让这个改动永久生效,你只需把其中一条命令放进你的 登录文件。(环境变量在第 12 章讨论;登录文件在 第 14 章讨论。)在本书余下的部分,我会假定你 确实使用 C 排序序列;所以如果你现在不是,请立刻把 相应的命令写进你的登录文件。

— 提示 —

偶尔,你可能想用一种不同于默认的排序序列来运行单个 程序。为此,你可以在运行该程序时用一个子 Shell 临时 改变 LC_COLLATE 的值。(我们在第 15 章 讨论子 Shell。)

例如,假设你使用的是 C 区域设置,而你想用 en_US(词典)排序序列来运行 sort 程序。你可以用:

(export LC_COLLATE=en_US; sort data)

当你以这种方式运行程序时,你对 LC_COLLATE 所做的改动是临时的,因为它只存在于子 Shell 内部。

跳至页首

查找重复行:uniq
(Finding Duplicate Lines: uniq)

相关过滤器:sort

Unix 有一批专门处理已排序数据的过滤器。其中最有用的 一个是 uniq,它逐行检查数据,寻找连续出现的 重复行。

uniq 程序可以完成四种不同的任务:

• 去掉重复行
• 选出重复行
• 选出唯一行
• 统计重复行的个数

语法是:

uniq [-cdu] [infile [outfile]]

其中 infile 是输入文件名,outfile 是输出文件名。

我们先看一个简单的例子。文件 data 包含 以下几行:

Al
Al
Barbara
Barbara
Charles

(记住,由于 uniq 的输入必须是已排序的, 重复行会连续出现。)你想要一份文件中所有行的列表, 不含重复。要用的命令是:

uniq data

输出很直截了当:

Al
Barbara
Charles

如果你想把输出保存到另一个文件,比如 processed-data,你可以在命令中指定它的名字:

uniq data processed-data

要只看重复行,使用 -d 选项:

uniq -d data

用我们的示例文件,输出是:

Al
Barbara

要只看唯一(不重复)的行,用 -u:

uniq -u data

在我们的示例中,这样的行只有一行:

Charles

问一问:如果你同时使用 -d-u, 你猜会发生什么?(自己动手试试。)

要统计每行出现的次数,使用 -c 选项:

uniq -c data

用我们的示例,输出是:

2 Al
2 Barbara
1 Charles

到目前为止,我们的例子都很简单。uniq 的真正 威力,体现在把它用在管道里的时候。例如,把几个文件 合并排序,然后通过管道交给 uniq,是很常见的 做法,下面两个例子就是如此:

sort file1 file2 file3 | uniq
cat file1 file2 file3 | sort | uniq

— 提示 —

如果你不带选项地使用 uniq,其实还有个替代 办法:你可以改用 sort -u。例如,下面 三条命令的效果完全相同:

sort -u file1 file2 file3
sort file1 file2 file3 | uniq
cat file1 file2 file3 | sort | uniq

(参见本章前面关于 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)风流韵事 会被当成一件好事。

跳至页首

合并两个文件中已排序的数据:
join
(Merging Sorted Data From Two Files: join)

相关过滤器:colrmcutpaste

在所有专门处理已排序数据的 Unix 过滤器中,最有意思的 是 join,它根据某个特定字段的值来合并两个 已排序的文件。语法是:

join [-i] [-a1|-v1] [-a2|-v2] [-1 field1] [-2 field2] file1 file2

其中 field1field2 是表示特定字段的 数字;file1file2 是包含已排序数据的 文件名。

在进入细节之前,我想先给你看一个例子。假设你有两个 已排序的文件,包含若干人的信息,每个人都有一个唯一的 识别号。在第一个名为 names 的文件中,每行 包含一个 ID 号,后面跟着名和姓:

111 Hugh Mungus
222 Stew Pendous
333 Mick Stup
444 Melon Collie

在第二个文件 phone 中,每行包含一个 ID 号, 后面跟着一个电话号码:

111 101-555-1111
222 202-555-2222
333 303-555-3333
444 404-555-4444

join 程序允许你根据两个文件的共同值来合并 它们,在本例中就是 ID 号:

join names phone

输出是:

111 Hugh Mungus 101-555-1111
222 Stew Pendous 202-555-2222
333 Mick Stup 303-555-3333
444 Melon Collie 404-555-4444

join 读取输入时会忽略前导空白,也就是行首的 空格或制表符。例如,下面两行被视为相同:

111 Hugh Mungus 101-555-1111
    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
Barbara  Feb-2-1992
Dave     Apr-8-1990
Frances  Oct-15-1991
George   Jan-17-1992

第二个文件 gifts 也包含两个字段:名和最喜爱 的礼物:

Al        money
Barbara   chocolate
Charles   music
Dave      books
Edward    camera

在这种情况下,你有 Al、Barbara 和 Dave 三个人的 生日信息和礼物信息。但你没有 Frances 和 George 的 礼物信息,也没有 Edward 的生日信息。看看使用 join 会发生什么:

join birthdays gifts

因为只有三行的连接字段相匹配(Al、Barbara 和 Dave 那三行),所以输出只有三行:

Al May-10-1987 money
Barbara Feb-2-1992 chocolate
Dave Apr-8-1990 books

然而,假设你想看到所有有生日信息的人,即使他们没有 礼物信息。你可以使用 -a(all,全部)选项, 后面跟一个 1:

join -a1 birthdays gifts

这是告诉 join:把 1 号文件中所有的名字都 输出,哪怕没有对应的礼物信息:

Al May-10-1987 money
Barbara Feb-2-1992 chocolate
Dave Apr-8-1990 books
Frances Oct-15-1991
George Jan-17-1992

同样地,如果你想看到所有有礼物信息的人(来自 2 号文件),即使他们没有生日信息,你可以用 -a2:

join -a2 birthdays gifts

输出是:

Al May-10-1987 money
Barbara Feb-2-1992 chocolate
Charles music
Dave Apr-8-1990 books
Edward camera

要列出两个文件中的所有名字,就把两个选项一起用:

join -a1 -a2 birthdays gifts

输出是:

Al May-10-1987 money
Barbara Feb-2-1992 chocolate
Charles music
Dave Apr-8-1990 books
Edward camera
Frances Oct-15-1991
George Jan-17-1992

当你像我们的第一个例子那样,以常规方式使用 join(不带 -a 选项)时,结果称为 内连接(INNER JOIN)。(这个术语也来自数据库理论。) 内连接的输出只来自连接字段匹配上的那些行。

当你使用 -a1-a2 时,输出中会包含 连接字段没有匹配上的行。我们把这称为外连接 (OUTER JOIN)。

我不打算细讲,因为数据库理论尽管有趣,却超出了本书的 范围。我要你记住的只是:如果你打交道的是所谓的 "关系数据库",那么内连接和外连接的区别是很重要的。

接着说,如果你只想看那些匹配不上的行,你可以用 -v1-v2(reverse,反向)选项。 使用 -v1 时,join 只输出 1 号文件中 没有匹配上的行,把所有匹配的都排除掉。例如:

join -v1 birthdays gifts

输出是:

Frances Oct-15-1991
George Jan-17-1992

使用 -v2 时,你只得到 2 号文件中没有匹配上的 行:

join -v2 birthdays gifts

输出是:

Charles music
Edward camera

当然,你可以把两个选项一起用,得到两个文件中所有 没有匹配上的行:

join -v1 -v2 birthdays gifts

这时的输出是:

Charles music
Edward camera
Frances Oct-15-1991
George Jan-17-1992

由于 join 依赖数据的有序性,它有好几个选项 帮你控制排序方面的要求。首先,你可以用 -i (ignore,忽略)选项告诉 join 忽略大小写的 差别。例如,使用这个选项时,CHARLESCharles 会被同样对待。

— 提示 —

你常常会用 sortjoin 准备数据。 记住:在 sort 中,你用 -f(fold, 折叠)选项来忽略大小写的差别;而在 join 中, 你用 -i(ignore,忽略)选项。(参见本章 前面关于 "fold" 的讨论。)

我前面提到,join 假定连接字段是每个文件的 第一个字段。你可以用 -1-2 选项 指定使用不同的连接字段。

要修改 1 号文件的连接字段,用 -1,后面跟上你 想使用的字段编号。例如,下面这条命令连接 datastatistics 两个文件,使用 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 时最常见的错误,就是忘记给两个 输入文件排序。如果其中一个或两个文件没有相对于连接 字段正确排序,你会看到没有输出或者只有部分输出, 而且不会有任何错误信息警告你事情出了岔子。

跳至页首

从偏序关系推出全序关系:
tsort
(Creating a Total Ordering From Partial Orderings: tsort)

相关过滤器:sort

考虑下面这个问题。你在安排自己晚上的活动,有好几项 约束条件:

• 你必须先洗碗,才能看电视。
• 你必须先吃饭,才能洗碗。
• 你必须先采购,才能做晚饭。
• 你必须先采购,才能把食物收好。
• 你必须先把食物收好,才能做晚饭。
• 你必须先做好晚饭,才能吃。
• 你必须先把食物收好,才能看电视。

如你所见,这有点让人眼花缭乱。你需要的是一份总清单, 指明每项活动该在什么时候做,使得所有约束条件都得到 满足。

用数学的语言说,这些约束条件每一个都称为偏序 (PARTIAL ORDERING),因为它们只规定了部分(是部分, 而非全部)活动的先后次序。在我们的例子里,每个偏序 规定了两项活动的次序。如果你能构造出一份总清单, 那它就是一个全序(TOTAL ORDERING),因为它规定了 所有活动的次序。

tsort 程序的工作是分析一组偏序——每个偏序 代表一条约束条件——并算出一个能满足所有约束条件的 全序。语法很简单:

tsort [file]

其中 file 是文件名。

每一行输入必须是由空白(空格或制表符)分隔的一对 字符串,每一对表示一个偏序。例如,假设文件 activities 包含以下数据:

clean-dishes watch-TV
eat clean-dishes
shop cook
shop put-away-food
put-away-food cook
cook eat
put-away-food watch-TV

注意文件中每一行都是由空白分隔的两个字符串(在本例 中是一个空格)。每一行表示一个偏序,对应上面列出的 某一条约束。例如,第一行说你必须先洗碗才能看电视; 第二行说你必须先吃饭才能洗碗;以此类推。

tsort 程序会把这一组偏序变成一个全序。 使用这条命令:

tsort activities

输出是:

shop
put-away-food
cook
eat
clean-dishes
watch-TV

因此,这个问题的解决方案是:

• 采购
• 把食物收好
• 做晚饭
• 吃晚饭
• 洗碗
• 看电视

一般而言,只要没有环路,任何一组偏序都可以合并成 一个全序。例如,考虑下面这两个偏序:

study watch-TV
watch-TV study

这里不可能有全序,因为如果你坚持要先看电视再学习, 你就没法做到先学习再看电视(尽管很多人都在尝试)。 如果你把这份数据交给 tsort,它会显示一条 错误信息,告诉你输入中含有环路。

名称的由来

tsort


在数学上,可以用一种称为"有向图"(directed graph) 的东西来表示一组偏序。如果图中没有环路,它就叫 "有向无环图"(directed acyclic graph),简称 DAG。 例如,树(见第 9 章)就是一个 DAG。

一旦有了 DAG,你就可以根据图中各元素的相对位置(而 不是它们的值)对这些元素排序,从而由偏序构造出全序。 事实上,tsort 就是这样完成工作的(虽然我们 不必操心其中的细节)。

在数学中,我们用 "topological"(拓扑的)一词来描述 那些依赖于相对位置的性质。因此,tsort 代表 "topological sort"(拓扑排序)。

跳至页首

在二进制文件中查找字符
字符串:strings
(Searching for Character Strings in Binary Files: strings)

相关过滤器: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
cd /usr/bin

现在用 ls(list,列出)程序显示该目录中所有 文件的列表:

ls

这些文件全都是程序,你可以用 strings 查看 其中任何一个。而且,由于这些文件就在你的工作目录里, 你不必指定完整的路径名。在这种情况下,只写文件名就 够了。例如,如果你的工作目录是 /bin, date 程序就在这里,你可以用这条命令查看 date 程序的内部:

strings -7 date | sort -iu | less

用这种方式,你就能在最常用的 Unix 工具里寻找隐藏的 字符串。等你试验够了,输入这条命令:

cd

这会把你的工作目录改回你的主目录(第 23 章有解释)。

跳至页首

转换字符:tr
(Translating Characters: tr)

相关过滤器:sed

tr(translate,转换)程序可以对字符执行三种 不同的操作。首先,它能把字符变成别的字符。例如,你 可以把小写字符变成大写字符,或者把制表符变成空格。 又或者,你可以把每一处数字 "0" 换成字母 "X"。 这样做时,我们说你"转换"(TRANSLATE)了这些字符。

其次,你可以规定:如果一个转换后的字符连续出现多次, 就把它们替换为单个字符。例如,你可以把连续的一个或 多个数字替换成字母 "X"。又或者,你可以把多个空格 替换成一个空格。这样做时,我们说你"压缩" (SQUEEZE)了这些字符。

最后,tr 还能删除指定的字符。例如,你可以 删除文件中所有的制表符。又或者,你可以删除所有既不是 字母也不是数字的字符。

在接下来的几节里,我们会逐一考察这些操作。不过在 开始之前,先看看语法:

tr [-cds] [set1 [set2]]

其中 set1set2 是字符集合。(*)

* 脚注

如果你用的是 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
tr abcde Axxxx < 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 [:upper:] [:lower:] < old > new

下面这两条命令也是等价的:

tr 0-9 A-I < old > new
tr [:digit:] A-I < old > new

(请注意,方括号和冒号是这个名称的一部分。)

就实用目的而言,这三个预定义字符类是你在 tr 程序中最可能用到的。不过,如果需要,还有 更多预定义字符类可用。完整清单见第 20 章的 Figure 20-3。

— 提示 —

与其他过滤器相比,tr 有个特别之处:它不允许你 直接指定输入文件或输出文件的名字。要从文件读取,你必须 重定向标准输入;要写入文件,你必须重定向标准输出。正因 如此,初学者使用 tr 时最常犯的错误就是忘记 重定向。例如,下面这些命令是行不通的:

tr abc ABC old
tr abc ABC old new

Linux 会显示一条含糊的消息,告诉你有多余的 "extra operand"(运算对象)。其他类型的 Unix 给出的 消息甚至更不说明问题。因为这个缘故,也许有一天,你会 发现自己花大量时间去琢磨 tr 命令为什么不工作。

解决办法就是永远记住:当你把 tr 用于文件时, 总是需要重定向:

tr abc ABC < old
tr abc ABC < old > new

跳至页首

转换不可打印的字符
(Translating Unprintable Characters)

到目前为止,我们所有的例子都很直截了当。不过,它们 多少有点刻意编排的味道。毕竟,你这一辈子有多少次需要 把冒号、分号和问号改成句点?有多少次需要把字母 "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
• 8 进制(八进制):使用 8 个数字,0-7
• 16 进制(十六进制):使用 16 个数字,0-9 A-F

我们将在第 21 章讨论这些计数系统。

Figure 19-4: tr 程序用来表示控制字符的代码

tr 程序用于转换(改变)特定字符,把它们变成 其他字符。要指定不可打印字符,你可以使用特殊代码, 也可以使用反斜杠(\)后跟该字符的三位八进制 (8 进制)值。(你可以在附录 D 中找到 ASCII 码中所有 字符的八进制值。)

本表给出四个最常用的控制字符的特殊代码和八进制值。 还有其他控制字符,但你在用 tr 时不太可能 需要它们。

注意:由于反斜杠被用作转义字符(见 第 13 章),如果你想指定一个真正的反斜杠, 必须连用两个。

代码 Ctrl 键 八进制代码 名称
\b^H\010退格 (backspace)
\t^I\011制表符 (tab)
\n^J\012换行 / 走纸 (newline / linefeed)
\r^M\015回车 (return)
\\反斜杠 (backslash)

现在我们来看看如何用 tr 把回车改成换行。假设 我们有一个名为 macfile 的文本文件,其中每行 都以回车结尾。我们想把所有回车改成换行,并把输出保存 到名为 unixfile 的文件里。下面两条命令中的 任何一条都能完成这项工作:

tr '\r' '\n' < macfile > unixfile
tr '\015' '\012' < macfile > unixfile

如你所见,一旦弄懂了这些代码的原理,用起来很简单。 例如,下面两条命令把文件 olddata 中所有的 制表符改成空格,并把输出保存到 newdata (*):

tr '\t' ' ' < olddata > newdata
tr '\011' ' ' < olddata > newdata

* 脚注

当你要把制表符改成空格,或者把空格改成制表符时,通常 最好使用 expandunexpand (第 18 章)。这两个程序就是专为这类改动 设计的,因此提供更灵活的功能。

跳至页首

转换字符:高级话题
(Translating Characters: Advanced Topics)

到目前为止,我们讨论的是如何用 tr 做简单的 替换,也就是用一个字符替换另一个字符。现在我们要把 注意力转向更高级的话题。在此之前,先复习一下将要 使用的语法:

tr [-cds] [set1 [set2]]

其中 set1set2 是两组字符。

-s 选项告诉 tr:来自第一组的多个连续 相同字符应当被替换为单个字符。正如我前面提到的,这样 做时,我们说把字符"挤"(squeeze)起来了。举个例子。

下面两条命令把任何数字(0-9)替换为大写字母 "X"。 输入取自名为 olddata 的文件,输出写入名为 newdata 的文件:

tr [:digit:] X < olddata > newdata
tr 0-9 X < olddata > newdata

这两条命令会把数字的每一次出现都替换成一个 "X"。例如,六位数字 120357 会被改成 XXXXXX。但是,假设你想把所有多位数字,不管 有多长,都变成单个 "X"。这时你要用 -s 选项:

tr -s [:digit:] X < olddata > newdata
tr -s 0-9 X < olddata > newdata

这是告诉 tr 把所有多位数字挤压成单个字符。 例如,数字 120357 现在会被改成 X

下面是一个有用的例子:我们挤压多个重复字符,但并不 真的改变字符本身。你想把连续的空格替换成单个空格。 解决办法是把空格换成空格,同时把多余的空格挤掉:

tr -s  ' '  ' '  < olddata > newdata

下一个选项 -d 会删除你指定的字符。因此, 使用 -d 时,你只定义一组字符。例如,要删除 所有的左括号和右括号,用:

tr -d '()' < olddata > newdata

要删除所有数字,用下面任一条命令:

tr -d [:digit:] < olddata > newdata
tr -d 0-9 < 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
your waking hours working in an office and going to
meetings? — Harley Hahn

前面那条命令会产生这样的输出:

XX XXX XXXXXX XXXXX XXX XXXX XXXXXXXX XX XXXXX XXXX XX
XXXX XXXXXX XXXXX XXXXXXX XX XX XXXXXX XXX XXXXX XX
XXXXXXXXX X XXXXXX XXXX

为了结束对 tr 的讨论,这里给出一个有趣的例子: 我们把 -c(补集)和 -s(挤压)两个选项 结合起来,统计不重复的单词。假设你写了两篇历史论文, 分别保存在名为 greekroman 的文本 文件里。你想统计这两个文件中出现的不重复单词有多少。 策略如下:

• 用 cat 把文件合并起来
• 用 tr 让每个单词独占一行
• 用 sort 给各行排序并去掉重复
• 用 wc 统计剩下的行数

要让每个单词独占一行(第 2 步),我们只需要用 tr 把每一个不属于单词的字符替换成换行符。 例如,假设我们有这么几个词:

As you can see

它们会变成这样:

As
you
can
see

为了简单起见,我们假定单词由一组 53 个不同的字符构成: 26 个大写字母、26 个小写字母,再加上撇号(也就是 单引号)。下面三条命令——随你挑一条——都能完成这项 工作:

tr -cs [:alpha:]\' "\n"
tr -cs [:upper:][:lower:]\' "\n"
tr -cs A-Za-z\' "\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

跳至页首

非交互式文本编辑:sed
(Non-interactive Text Editing: sed)

文本编辑器是一种让你能对文本行执行各种操作的程序。 通常你可以插入、删除、修改、查找等等。最重要的两个 Unix 文本编辑器是 vi(我们将在 第 22 章讨论它)和 Emacs。此外还有几个 重要性稍低、但更简单的文本编辑器,我们在 第 14 章讨论过:keditgedit、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 程序每次从数据流中读取一行,按照下面三个 步骤,从头到尾处理全部数据:

  1. 从输入流读入一行。
  2. 执行指定的命令,必要时对该行作出修改。
  3. 把该行写入输出流。

默认情况下,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
cp temp names
rm temp

换句话说,你必须手工完成 -i 选项自动替你做的 事情。

当你使用 sed -i 时要小心。你对输入文件做 的修改是永久性的,而且没有"撤销"命令。

— 提示 —

在用 sed 修改文件之前,先不带 -i 选项 运行一次程序,预览一下要做的修改,这是个好主意。例如:

sed 's/xx/XXX/g' file | less

这样你可以查看输出,看看它是否如你所料。如果是,你可以 带上 -i 重新运行这条命令,真正做出修改(*):

sed -i 's/xx/XXX/g' file

* 脚注

Unix 有一条普遍原则:在做重要的、无法撤销的修改之前, 只要能预览,就先预览。

在第 13 章中,我们在历史列表和别名上用过 类似的策略。那两次我们都讨论过:如何先预览结果,再执行 真正的删除,从而避免误删不该删的文件。

这条原则非常重要,我要你把它记一辈子,或者记到你去世 为止(以先到者为准)。

跳至页首

sed 做替换
(Using sed for Substitutions)

相关过滤器: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 -i 's/.$//' winfile

这是怎么做到的?在正则表达式中,.(点)字符 匹配任意单个字符;$(美元符号)字符匹配行尾。 因此,查找串 .$ 指的就是换行符前面的那个字符。

仔细看看替换串。注意它是空的。这是告诉 sed 把查找串变成"什么都没有"。也就是说,我们让 sed 删除查找串。其效果就是把每行末尾那个多余 的回车字符去掉。

如果你以前从未用过正则表达式,我不指望你对最后这几条 命令觉得完全得心应手。但是我向你保证,等你读完 第 20 章,这些例子以及和它们类似的例子, 理解起来会非常容易。

— 提示 —

要用 sed 删除一个字符串,你就查找它,然后把它 替换成空。

这是一个值得记住的重要技巧,因为任何支持查找与替换操作 的程序都能用它。(实际上,你在文本编辑器里会常常用到 这个技巧。)

跳至页首

sed 只处理指定的行
(Telling sed to Operate Only on Specific Lines)

默认情况下,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 命令
(Using Very Long sed Commands)

如我前面所说,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 \
-e 's/mon/Monday/g' \
-e 's/tue/Tuesday/g' \
-e 's/wed/Wednesday/g' \
-e 's/thu/Thursday/g' \
-e 's/fri/Friday/g' \
-e 's/sat/Saturday/g' \
-e 's/sun/Sunday/g' \
calendar

— 提示 —

当你输入 \<Return> 来续行时,大多数 Shell 会显示一个特殊的提示符,称为次级提示符(SECONDARY PROMPT),表示一条命令正在续写。

在 Bourne Shell 家族(Bash、Korn Shell)中,默认的 次级提示符是一个 >(大于号)字符。你可以 通过修改 PS2 Shell 变量来改变次级提示符(尽管 大多数人不会这么做)。

在 C-Shell 家族中,只有 Tcsh 有次级提示符。默认情况下 它是一个 ?(问号)。你可以通过修改 prompt2 Shell 变量来改变次级提示符。

(修改 Shell 变量的命令在第 12 章解释。把 这类命令放进你的初始化文件,在第 14 章讨论。)

跳至页首



练习
(Exercises)

复习问题 #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:

你在自己的学校里经营一个在线婚恋服务。你有三个存放用户 注册信息的文件:reg1reg2reg3。在这些文件里,每一行包含一个人(单身也 算一个人,我可不是有意双关)的信息。

请创建一条管道,处理全部三个文件,只选出包含 "female" 或 "male" 这两个词(随你挑)的行。去掉所有重复之后, 结果应当保存到名为 prospects 的文件里。

做完之后,再创建第二条管道,显示所有登记过不止一次的人 (不论男女)的清单。提示:在文件中查找重复行。

应用你的知识 #4:

你有一个名为 data 的文本文件。请创建一条管道, 显示所有叠词(double word)实例,例如 "hello hello"。 (假设一个"词"由连续的大写或小写字母构成。)

提示:先创建一个所有单词的清单,每行一个词。然后把输出 管道给一个能查找连续相同行的程序。

进一步思考 #1:

在前面的一道题目里,我指出 grep 是最重要的 过滤器,并要求你解释它在管道中为什么格外有用。结合你对 那道题的回答想一想:人性的什么特点,让 grep 显得如此强大、如此有用?

进一步思考 #2:

最初,Unix 以美式英语和美国的数据处理标准(例如 ASCII 码)为基础。随着国际化工具和标准(例如区域设置)的发展, Unix 如今可以被来自各种文化背景的人使用。这些用户能够 用自己的语言、按照自己习惯的数据处理约定与 Unix 交互。

以这种方式扩展 Unix,有哪些取舍?请列出三条好处和三条 坏处。

进一步思考 #3:

本章我们详细讨论了 trsed 两个程序。 你可以看到,它们都非常有用。然而,它们都是复杂的工具, 要掌握得花不少时间和精力。

对某些人来说,这不是问题。但对许多其他人来说,花时间把 一个复杂工具用好,是一种令人不太舒服的体验。你认为这是 为什么?

所有的工具都应当设计得容易学会吗?

进一步思考 #4:

请就下面这句话发表看法:在整个 Unix 工具箱里,没有任何 一个程序,学会它所花的时间会比把钢琴弹好所需的时间更长。

跳至页首