在像Linux这样的操作系统中,许多数据都以纯文本文件的形式存储和管理。这就像一个城市的运转离不开标准化的管道和线路一样,文本文件就是Linux世界的通用语言。

在这一节中,我们会一起探索一系列强大的命令行工具,它们能像瑞士军刀一样,帮你轻松地“切割”和“组合”文本数据。 我们不仅会重温一些已经认识的老朋友,还会结识一些身怀绝技的新伙伴。学完本部分,你将能够更自信地处理各种文本处理任务。
你可能觉得,我们已经学了 vim 这样的文本编辑器,也见识过各种各样的配置文件和命令输出,这些都是文本。但文本的用途远不止于此,它几乎是数字世界的基石。
很多人喜欢用纯文本格式来写东西。小到随手记下的笔记,大到一本完整的书籍,都可以用纯文本完成。你可能会好奇,没有了花哨的排版工具,怎么写出格式优美的长篇大论呢? 秘诀在于一种叫做“标记语言”的东西。作者可以用简单的文本标记来描述文档的最终格式,比如哪里是标题,哪里是重点。 许多科学论文就是这样写成的,因为基于Unix的文本处理系统最早支持了科技写作所需的复杂排版。
我们每天浏览的网页,可能是世界上最流行的电子文档了。剥开它华丽的外衣,网页的本质其实就是文本文档。它们使用HTML(超文本标记语言)或XML(可扩展标记语言)来描述页面的视觉样式和内容结构。
电子邮件本质上也是一种基于文本的媒介。即使你发送的是图片或视频等非文本附件,它们在传输前也会被转换成文本格式。 如果你尝试查看一封电子邮件的“原始”样貌,你会发现,邮件的开头是一段描述其来源和旅程的“信头”,然后才是我们看到的邮件正文。
在Linux这样的系统上,即便是要打印的内容,也会先被处理成纯文本,或者转换成一种叫做PostScript的页面描述语言——它本身也是文本格式。 更重要的是,对于程序员来说,文本就是一切的起点。所有软件的“源代码”,也就是程序员亲手编写的那部分,都是纯文本格式的。我们接下来要学习的很多文本处理工具,最初就是为了解决软件开发中的各种问题而设计的。
可以说,在Linux世界里,几乎所有重要的信息都以文本形式存在。无论是系统配置、程序代码,还是网络通信,背后都有文本的身影。
在正式探索更高级的技巧之前,让我们先来重温几个在之前课程里已经见过面的老朋友。当时我们只是简单地认识了一下,现在是时候深入了解它们在文本处理方面的真正实力了。

cat 命令(concatenate的缩写,意为“连接”)远不止是把文件内容显示在屏幕上那么简单。它还有一些非常实用的小功能,可以帮助我们更好地洞察文本内容。
有时候,文本文件中会隐藏着一些我们肉眼看不到的“特殊字符”,比如制表符(Tab)和回车符。这些字符可能会影响程序的运行或脚本的逻辑。使用 cat 的 -A 选项,就能让这些隐身的字符无所遁形。
我们来动手试试。首先,创建一个包含特殊字符的文件。我们可以用 cat 本身来创建一个简单的文件,输入完内容后按 Ctrl+d 结束。
|$ cat > test.txt hello world $
在这行文本的开头,我按下了Tab键,在结尾,我输入了几个空格。现在,让我们用 -A 选项来看看这个文件的“真实面貌”:
|$ cat -A test.txt ^Ihello world $
开头的 Tab 键被显示为 ^I,这是一个常见的表示法,意思是“Control-I”。而行尾的 $ 符号则清楚地标示出了文本的真正结尾,暴露了我们多加的几个空格。
你可能会遇到一个常见的问题:从Windows系统过来的文本文件,在Linux下可能会表现异常。这是因为Windows使用“回车+换行”两个字符来标记一行的结束,而Linux只使用“换行”一个字符。
多出来的那个“回车”符虽然看不见,但可能会让很多Linux工具“困惑”。使用 cat -A 就能轻易发现这些捣乱的家伙。
cat 还能对文本进行一些简单的加工。比如,-n 选项可以为文本的每一行加上行号,而 -s 选项则可以把连续的多个空行压缩成一个。
|$ cat > test2.txt line one line two $ cat -ns test2.txt 1 line one 2 3 line two
在这个例子里,我们创建了一个包含两个空行的文件。经过 cat -ns 的处理后,多余的空行被移除了,并且每一行都被标上了行号。这虽然只是小小的处理,但却是文本处理的开端。
sort 命令顾名思义,就是用来给文本内容排序的。它会逐行读取输入,然后将排序后的结果送到标准输出。
sort 的功能非常强大,它提供了多种排序规则。
让我们来看一个实际的例子。du 命令可以查看目录的磁盘使用情况,但默认输出是按文件名排列的。如果我们想知道哪个目录最占用空间,就需要按数值大小来排序。
|$ du -s /usr/share/* | sort -nr | head 509940 /usr/share/locale-langpack 242660 /usr/share/doc 197560 /usr/share/fonts ...
这里,我们把 du 命令的输出通过管道交给了 sort -nr。-n 表示按数值排序,-r 表示反向(从大到小),这样,占用空间最大的目录就排在了最前面。
sort 最强大的功能之一是能够处理像表格一样的数据。比如 ls -l 的输出,每一行都包含多个字段:权限、所有者、文件大小、修改日期和文件名等。
我们可以把每一行看作一条记录,把每一列看作一个字段。sort 允许我们指定任意一个或多个字段作为排序的“关键字”。
例如,要根据文件大小(也就是第5个字段)来对 ls -l 的输出进行排序,我们可以这样做:
|$ ls -l /usr/bin | sort -nr -k 5 | head -rwxr-xr-x 1 root root 8234216 2008-04-07 17:42 inkscape -rwxr-xr-x 1 root root 8222692 2008-04-07 17:42 inkview ...
-k 5 这个选项告诉 sort:“请关注第5个字段,并根据它来排序。”
与 sort 相比,uniq 命令就简单多了。它的任务很明确:从已排序的文本中找出并移除相邻的重复行。
请务必记住,uniq 只对相邻的重复行有效。因此,它几乎总是和 sort 命令配合使用,先排序让所有重复的行紧挨在一起,再交给 uniq 进行处理。
我们来创建一个包含重复内容的测试文件:
|$ cat > test3.txt a b c a b c
如果我们直接对这个文件使用 uniq,会发现什么也没变,因为重复的行不相邻。正确的做法是先 sort 再 uniq:
|$ sort test3.txt | uniq a b c
这样,所有的重复行都被清除了。uniq 还有一个 -c 选项非常有用,它可以统计每一行连续重复出现的次数。
|$ sort test3.txt | uniq -c 2 a 2 b 2 c
这个功能在做日志分析或数据统计时非常方便。
接下来要介绍的三个工具,就像是厨房里的刀具和胶水,可以让我们对文本进行精细的切割和拼接,创造出全新的数据组合。

cut 命令就像一把锋利的刀,可以从文本的每一行中“切”出我们想要的部分。它非常适合处理那些由其他程序生成的、格式规整的数据。
cut 主要通过两种方式来指定切割的区域:
-c:按字符位置切割。你可以指定一个或多个字符的精确位置或范围。-f:按字段(列)切割。这在处理用特定符号(如制表符Tab或逗号,)分隔的数据时非常有用。-d:与 -f 配合使用,用来指定字段的分隔符。默认情况下,cut 认为字段是由单个Tab字符分隔的。假设我们有一个用Tab键分隔的发行版列表文件 distros.txt:
|SUSE 10.2 12/07/2006 Fedora 10 11/25/2008 Ubuntu 8.04 04/24/2008
如果我们只想提取第三列,也就是发布日期,可以这样做:
|$ cut -f 3 distros.txt 12/07/2006 11/25/2008 04/24/2008
因为文件默认是Tab分隔,所以我们甚至不需要 -d 选项。-f 3 就表示“给我第3个字段”。
如果我们想从 /etc/passwd 文件中提取用户名(第一列),而这个文件是用冒号 : 分隔的,那么就需要 -d 选项来帮忙了:
|$ cut -d ':' -f 1 /etc/passwd | head root daemon bin ...
这个命令的意思是:“以冒号为分隔符,切出第一个字段。”
paste 命令的功能与 cut 正好相反。它不是切出数据,而是像用胶水一样,把来自不同文件的文本按列“粘”在一起。
想象一下,我们有两个文件,一个记录了员工姓名 names.txt,另一个记录了对应的职位 jobs.txt。
names.txt:
|XiaoMing LiHua ZhangSan
jobs.txt:
|Teacher Doctor Programmer
现在,我们想把它们合并成一个文件,显示姓名和职位的对应关系。paste 可以轻松做到:
|$ paste names.txt jobs.txt XiaoMing Teacher LiHua Doctor ZhangSan Programmer
paste 会自动地将两个文件的第一行、第二行、第三行……分别合并,并用一个Tab字符隔开。
join 命令在某些方面很像 paste,但它更加智能。它通常用于处理那些像数据库表格一样,拥有一个共享“关键字”字段的文件。
想象一下,我们有一个客户信息表 customers.txt 和一个订单表 orders.txt。
customers.txt (第一列是客户ID,第二列是姓名):
|101 XiaoMing 102 LiHua 103 ZhangSan
orders.txt (第一列是客户ID,第二列是订单产品):
|101 Laptop 103 Keyboard 101 Mouse
两个文件都包含了客户ID,这就是它们的“共享关键字”。我们希望生成一个报告,显示每个客户购买了什么产品。
使用 join 的一个重要前提是:两个文件都必须在连接关键字上预先排好序。
所以我们先对文件排序,然后再使用 join:
|$ sort customers.txt > customers_sorted.txt $ sort orders.txt > orders_sorted.txt $ join customers_sorted.txt orders_sorted.txt 101 XiaoMing Laptop 101 XiaoMing Mouse 103 ZhangSan Keyboard
join 会自动找到两个文件中具有相同关键字(默认是第一列)的行,并将它们合并在一起。你看,join 成功地将客户姓名和他们各自的订单关联了起来,而跳过了没有下订单的客户LiHua。

对于系统管理员和软件开发者来说,比较不同版本的文本文件是一项至关重要的日常工作。比如,管理员可能需要比较当前和一个星期前的配置文件,来排查系统故障; 开发者则需要时刻关注代码的改动,以理解项目的进展和变化。接下来的几个工具就是为此而生。
comm 命令(common的缩写)可以用来比较两个已排序的文件,并清晰地展示出哪些行是文件1独有的,哪些是文件2独有的,以及哪些是两个文件共有的。
它的输出分为三列:
我们来创建两个简单的文件 file1.txt 和 file2.txt:
file1.txt:
|a b c d
file2.txt:
|b c d e
现在用 comm 比较它们:
|$ comm file1.txt file2.txt a b c d e
输出结果一目了然:a 是文件1独有的,e 是文件2独有的,而 b, c, d 是两者共有的。comm 还提供 -1, -2, -3 这样的选项,可以分别用来隐藏第一、二、三列的输出。例如,如果我们只想看两个文件共有的部分:
|$ comm -12 file1.txt file2.txt b c d
diff 命令(difference的缩写)同样用于比较文件差异,但它是一个远比 comm 强大的工具。diff 的主要用途是生成一份详细的“差异报告”,精确描述了如何将一个文件修改成另一个文件的样子。
这份报告可以被其他程序(比如我们稍后会讲到的 patch)读取和使用。
diff 最常被软件开发者用来查看不同版本源代码之间的变化。它最流行的输出格式是“统一格式”(unified format),通过 -u 选项启用。
我们继续用 file1.txt 和 file2.txt 来举例:
|$ diff -u file1.txt file2.txt --- file1.txt 2023-10-27 10:00:00.000000000 +0000 +++ file2.txt 2023-10-27 10:01:00.000000000 +0000 @@ -1,4 +1,4 @@ -a b c d +e
这份报告非常直观:
--- 开头的行代表旧文件(file1.txt)。+++ 开头的行代表新文件(file2.txt)。- 开头的行表示这一行需要从旧文件中删除。+ 开头的行表示这一行需要添加到新文件中。patch 命令的作用,就是读取 diff 生成的差异报告,然后像一个勤劳的机器人一样,自动地将这些改动应用到旧文件上,使其更新到新版本的状态。
这个 diff/patch 的工作流程是大型开源项目(比如Linux内核)协作的核心。开发者们不需要每次都发送完整的几百万行代码,他们只需要发送一个轻巧的 diff 文件(也叫“补丁”),其他人收到后用 patch 命令就能将改动应用到自己的代码树上。
这个“生成补丁、应用补丁”的模式有两个巨大优势:首先,补丁文件非常小,易于传输;其次,补丁文件清晰地展示了所有改动,便于代码审查。
让我们来实践一下。首先,用 diff 生成一个名为 patchfile.txt 的补丁文件:
|$ diff -u file1.txt file2.txt > patchfile.txt
现在,我们可以用 patch 命令将这个补丁应用到 file1.txt 上:
|$ patch < patchfile.txt patching file file1.txt
检查一下 file1.txt 的内容,你会发现它现在已经和 file2.txt 完全一样了。patch 成功地根据补丁文件完成了“修复”工作。
我们之前接触的文本编辑,大多是手动的、交互式的。但有时,我们需要对大量文件执行相同的、重复的编辑操作。这时,手动的“作坊式”生产就跟不上效率了,我们需要的是“流水线式”的自动化处理工具。

tr 命令(transliterate的缩写,意为“翻译、转写”)是一个专门在字符级别进行搜索和替换的工具。你可以把它想象成一个字符翻译机。
它最常见的用法之一是转换字母的大小写。tr 从标准输入读取数据,并将结果输出到标准输出。
|$ echo "lowercase letters" | tr a-z A-Z LOWERCASE LETTERS
tr 接受两组字符作为参数:第一组是需要被转换的源字符,第二组是目标字符。它会把在第一组中出现的每个字符,都替换成第二组中对应位置的字符。
tr 还可以用来删除字符。在之前我们提到过,Windows和Linux的换行符不同。我们可以用 tr 轻松地将Windows格式的文本文件(包含 \r 字符)转换为Linux格式:
|tr -d '\r' < windows_file.txt > linux_file.txt
-d 选项告诉 tr 删除所有在输入中找到的 \r(回车)字符。
一个有趣的应用是ROT13编码,这是一种简单的“加密”方式,常被用来隐藏剧透或可能冒犯人的内容。它的原理很简单,就是把每个字母在字母表中向后移动13位。因为字母表总共26个字母,所以对加密后的文本再做一次ROT13,就能恢复原文。
|$ echo "secret text" | tr 'a-zA-Z' 'n-za-mN-ZA-M' frperg grkg $ echo "frperg grkg" | tr 'a-zA-Z' 'n-za-mN-ZA-M' secret text
sed 的全称是“stream editor”,即“流编辑器”。它是一个极其强大的程序,可以对来自文件或标准输入的文本流执行复杂的编辑操作。
sed 的工作模式是:你给它一个或多个编辑指令,它会把这些指令依次应用到输入文本的每一行。
最常见的 sed 指令是 s,表示替换(substitute),它的用法和 vi 编辑器里的替换命令非常相似。
|$ echo "front" | sed 's/front/back/' back
这个命令的意思是,在输入中查找 "front",并将其替换为 "back"。s/查找内容/替换内容/ 是它的基本格式。
sed 的强大之处在于,你可以为指令指定一个“地址”,来限定它只对特定的行起作用。地址可以是一个行号,也可以是一个正则表达式。
|$ sed -n '/SUSE/p' distros.txt SUSE 10.2 12/07/2006 SUSE 11.0 06/19/2008 SUSE 10.3 10/04/2007 SUSE 10.1 05/11/2006
这里的 /SUSE/ 就是一个正则表达式地址,它告诉 sed 只对包含 "SUSE" 的行执行后面的 p(打印)命令。(-n 选项用来禁止 sed 默认打印所有行的行为)。
sed 的替换命令还支持一种叫做“反向引用”的高级功能,这让它能处理非常复杂的文本转换。假设我们要把 MM/DD/YYYY 格式的日期转换为 YYYY-MM-DD 格式。
|$ sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt SUSE 10.2 2006-12-07 Fedora 10 2008-11-25 ...
这个命令看起来有点吓人,但原理很简单。我们用 \( 和 \) 把月、日、年三部分分别括起来,形成了三个“子表达式”。然后在替换部分,我们用 \1, \2, \3 来重新排列这三个捕获到的部分,从而实现格式的转换。
aspell 是一个在命令行下运行的交互式拼写检查工具。虽然很多图形界面的程序都内置了拼写检查功能,但在命令行环境下,aspell 依然是一个非常得力的助手。
你可以用它来检查一个简单的文本文件:
|aspell check filename.txt
执行后,aspell 会进入一个交互界面,逐个高亮出它认为拼写错误的单词,并提供一个建议列表供你选择。你可以选择替换、忽略或者将这个单词添加到个人词典中。aspell 非常智能,它甚至能识别HTML等文件格式,只检查文本内容而忽略标签。
在这一节中,我们一起探索了许多强大的命令行文本处理工具。你可能觉得有些工具的用法现在看来还不够直观,或者不确定在日常工作中什么时候会用到它们。这很正常。 但是,请相信,这些看似简单的工具,正是我们未来构建自动化任务的基石。当我们在后面部分中学习Shell脚本编程时,你将再次见到这些熟悉的身影。 到那时,你会惊奇地发现,将这些工具像乐高积木一样组合起来,可以解决各种复杂和繁琐的实际问题,真正展现出命令行的强大威力。