查找文件与文本

最后更新:2026-03-24。

概览

问题

– 如何查找文件?

– 如何查找文件中的内容?

学习目标

– 使用 grep 从文本文件中选出符合简单模式的行。

– 使用 find 查找名称符合简单模式的文件与目录。

– 把一个命令的输出用作另一个命令的参数。

– 解释“文本文件”与“二进制文件”的含义,以及许多常用工具为何不擅长处理后者。

就像许多人把 Google 当作“查找”的动词,Unix 程序员也常用 grep 这个词。grep 是 global/regular expression/print 的缩写,源于早期 Unix 文本编辑器的一组常用操作。它也是一个非常实用的命令行程序的名称。

grep 查找并打印文件中符合模式的行。本节使用包含三首俳句的文件;它们来自 Salon 杂志 1998 年的比赛,作者依次为 Bill Torcaso、Howard Korder、Margaret Segall(归档第 1 页、第 2 页)。以下示例在 writing 子目录运行:

命令

$ cd
$ cd Desktop/shell-lesson-data/exercise-data/writing
$ cat haiku.txt

输出

The Tao that is seen
Is not the true Tao, until
You bring fresh toner.

With searching comes loss
and the presence of absence:
"My Thesis" not found.

Yesterday it worked
Today it is not working
Software is like that.

查找包含 not 的行:

命令

$ grep not haiku.txt

输出

Is not the true Tao, until
"My Thesis" not found
Today it is not working

这里 not 是搜索模式。grep 在文件中搜索指定模式。用法是先写 grep,再写搜索模式,最后写要搜索的文件名(可以有多个)。

输出为文件中包含字母序列 not 的三行。

grep 默认区分大小写。此外,搜索模式不必构成完整单词,下一例会展示这一点。

搜索模式 The:

命令

$ grep The haiku.txt

输出

The Tao that is seen
"My Thesis" not found.

这次输出两行,其中一行的搜索模式位于更长的单词 Thesis 中。

要只匹配独立单词 The,可传入 -w,将匹配限制在单词边界。

本节稍后还会介绍如何改变 grep 的大小写处理行为。

命令

$ grep -w The haiku.txt

输出

The Tao that is seen

“单词边界”也包括行首和行尾,并不限于两侧有空格的字母。要搜索短语,可给短语加引号:

命令

$ grep -w "is not" haiku.txt

输出

Today it is not working

单个单词可以不加引号;搜索多个单词时,引号很有用,也让搜索词与文件名更易区分。余下示例会使用引号。

另一个有用的选项是 -n,它为匹配行标注行号:

命令

$ grep -n "it" haiku.txt

输出

5:With searching comes loss
9:Yesterday it worked
10:Today it is not working

这里可见第 5、9、10 行含有字母序列 it。

与其他 Unix 命令一样,选项(标志)可以组合。要查找单词 the 并显示行号,可结合 -w 与 -n:

命令

$ grep -n -w "the" haiku.txt

输出

2:Is not the true Tao, until
6:and the presence of absence:

加入 -i,让搜索不区分大小写:

命令

$ grep -n -w -i "the" haiku.txt

输出

1:The Tao that is seen
2:Is not the true Tao, until
6:and the presence of absence:

使用 -v 反转搜索,输出不含单词 the 的行:

命令

$ grep -n -w -v "the" haiku.txt

输出

1:The Tao that is seen
3:You bring fresh toner.
4:
5:With searching comes loss
7:"My Thesis" not found.
8:
9:Yesterday it worked
10:Today it is not working
11:Software is like that.

类似 cp 的 -r 会作用于整个目录内容,grep 的 -r(递归)可以搜索目录及子目录中的所有文件。

在 shell-lesson-data/exercise-data/writing 目录搜索 Yesterday:

命令

$ grep -r Yesterday .

输出

./LittleWomen.txt:"Yesterday, when Aunt was asleep and I was trying to be as still as a
./LittleWomen.txt:Yesterday at dinner, when an Austrian officer stared at us and then
./LittleWomen.txt:Yesterday was a quiet day spent in teaching, sewing, and writing in my
./haiku.txt:Yesterday it worked

grep 还有很多选项。可以这样查看:

命令

$ grep --help

输出

Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE or standard input.
PATTERN is, by default, a basic regular expression (BRE).
Example: grep -i 'hello world' menu.h main.c

Regexp selection and interpretation:
  -E, --extended-regexp     PATTERN is an extended regular expression (ERE)
  -F, --fixed-strings       PATTERN is a set of newline-separated fixed strings
  -G, --basic-regexp        PATTERN is a basic regular expression (BRE)
  -P, --perl-regexp         PATTERN is a Perl regular expression
  -e, --regexp=PATTERN      use PATTERN for matching
  -f, --file=FILE           obtain PATTERN from FILE
  -i, --ignore-case         ignore case distinctions
  -w, --word-regexp         force PATTERN to match only whole words
  -x, --line-regexp         force PATTERN to match only whole lines
  -z, --null-data           a data line ends in 0 byte, not newline

Miscellaneous:
...        ...        ...

**练习**

使用 grep

哪条命令会产生如下输出?

输出

and the presence of absence:

1. grep "of" haiku.txt

2. grep -E "of" haiku.txt

3. grep -w "of" haiku.txt

4. grep -i "of" haiku.txt

解答

正确答案为 3:-w 只匹配完整单词。其他选项也会匹配更长单词中的 of。

通配符

grep 的强大之处不只在于选项,还在于模式可以包含通配符;其技术名称是正则表达式,也就是 grep 中的 re。正则表达式既复杂又强大,复杂搜索可参阅 Library Carpentry 教程。先尝试查找第二个位置为 o 的行:

命令

$ grep -E "^.o" haiku.txt

输出

You bring fresh toner.
Today it is not working
Software is like that.

使用 -E,并给模式加引号,防止 shell 解释模式。例如模式有 * 时,shell 会在 grep 运行前尝试展开它。^ 将匹配锚定在行首;. 匹配单个字符,类似 shell 中的 ?;o 则匹配实际字母 o。

**练习**

追踪物种

Leah 在一个目录中保存了几百个数据文件,每个文件格式如下:

2012-11-05,deer,5
2012-11-05,rabbit,22
2012-11-05,raccoon,7
2012-11-06,rabbit,19
2012-11-06,deer,2
2012-11-06,fox,4
2012-11-07,rabbit,16
2012-11-07,bear,1

她想编写 shell 脚本,第一个参数为物种,第二个参数为目录。脚本应返回 <species>.txt 文件,列出日期及该日期观察到的物种数量。以上数据对应的 rabbit.txt 应为:

2012-11-05,22
2012-11-06,19
2012-11-07,16

下面每行是一条命令或管道组成部分。请排列成一条命令,实现 Leah 的目标:

命令

cut -d : -f 2
>
|
grep -w $1 -r $2
|
$1.txt
cut -d , -f 1,3

提示:使用 man grep 回顾如何递归搜索目录及全部子目录,使用 man cut 了解如何选取多个字段。

示例文件位于 shell-lesson-data/exercise-data/animal-counts/animals.csv。

解答

grep -w $1 -r $2 | cut -d : -f 2 | cut -d , -f 1,3 > $1.txt

实际上,交换两个 cut 命令的顺序仍然有效。请在命令行尝试交换顺序,观察每一步的输出,理解原因。

调用上述脚本的方法:

命令

$ bash count-species.sh bear .

**练习**

《小妇人》

你和朋友刚读完 Louisa May Alcott 的《小妇人》,正在争论四姐妹 Jo、Meg、Beth、Amy 中谁被提及最多。朋友认为是 Jo,你确信是 Amy。幸好 LittleWomen.txt 含有小说全文,位于 shell-lesson-data/exercise-data/writing/LittleWomen.txt。如何使用 for 循环统计四人的提及次数?

提示:一种解法可组合 grep、wc 和 |,另一种可使用 grep 的选项。编程任务往往有多种解法,通常应综合正确性、简洁程度、可读性与速度作出选择。

解答

for sis in Jo Meg Beth Amy
do
    echo $sis:
    grep -ow $sis LittleWomen.txt | wc -l
done

另一种稍差的解法:

for sis in Jo Meg Beth Amy
do
    echo $sis:
    grep -ocw $sis LittleWomen.txt
done

后者较差,因为 grep -c 只报告匹配行数。一行中出现多个匹配时,得到的总数会偏低。

细心的读者可能注意到,人物名有时以全大写出现在章标题中,例如 MEG GOES TO VANITY FAIR。要计入这些,可加 -i 忽略大小写;不过本例中不影响谁被提及最多的结论。

grep 查找文件中的行,find 则查找文件本身。find 也有许多选项。以下用 shell-lesson-data/exercise-data 目录树展示最简单的用法:

输出

.
├── animal-counts/
│   └── animals.csv
├── creatures/
│   ├── basilisk.dat
│   ├── minotaur.dat
│   └── unicorn.dat
├── numbers.txt
├── alkanes/
│   ├── cubane.pdb
│   ├── ethane.pdb
│   ├── methane.pdb
│   ├── octane.pdb
│   ├── pentane.pdb
│   └── propane.pdb
└── writing/
    ├── haiku.txt
    └── LittleWomen.txt

exercise-data 包含一个 numbers.txt 文件和四个目录:animal-counts、creatures、alkanes、writing,各有若干文件。

首先运行 find .;请在 shell-lesson-data/exercise-data 中执行:

命令

$ find .

输出

.
./writing
./writing/LittleWomen.txt
./writing/haiku.txt
./creatures
./creatures/basilisk.dat
./creatures/unicorn.dat
./creatures/minotaur.dat
./animal-counts
./animal-counts/animals.csv
./numbers.txt
./alkanes
./alkanes/ethane.pdb
./alkanes/propane.pdb
./alkanes/octane.pdb
./alkanes/pentane.pdb
./alkanes/methane.pdb
./alkanes/cubane.pdb

单独的 . 表示当前工作目录,也是搜索起点。find 输出当前目录下所有文件与目录的名称。乍看似乎没用,但它有许多筛选选项,下面会介绍其中一些。

首先是 -type d,意思是“属于目录的对象”。输出包括 . 在内的五个目录:

命令

$ find . -type d

输出

.
./writing
./creatures
./animal-counts
./alkanes

find 的结果没有特定顺序。改为 -type f 就会列出所有文件:

命令

$ find . -type f

输出

./writing/LittleWomen.txt
./writing/haiku.txt
./creatures/basilisk.dat
./creatures/unicorn.dat
./creatures/minotaur.dat
./animal-counts/animals.csv
./numbers.txt
./alkanes/ethane.pdb
./alkanes/propane.pdb
./alkanes/octane.pdb
./alkanes/pentane.pdb
./alkanes/methane.pdb
./alkanes/cubane.pdb

接下来按名称匹配:

命令

$ find . -name *.txt

输出

./numbers.txt

本以为会找到所有文本文件,却只输出 ./numbers.txt。因为 shell 在命令运行前展开 * 等通配符。当前目录的 *.txt 展开为 ./numbers.txt,实际运行的是:

命令

$ find . -name numbers.txt

find 按要求执行了搜索,只是我们的要求写错了。

像 grep 示例一样,给 *.txt 加引号,防止 shell 展开 *。这样 find 收到的是模式 *.txt,而非展开后的文件名 numbers.txt:

命令

$ find . -name "*.txt"

输出

./writing/LittleWomen.txt
./writing/haiku.txt
./numbers.txt

列出与查找

使用适当选项时,ls 与 find 可以完成类似工作。但通常 ls 列出能列出的所有内容,find 则搜索具有特定属性的对象。

命令行的力量在于组合工具。前面用过管道,现在来看另一种技术。find . -name "*.txt" 列出当前目录及子目录的所有文本文件。如何与 wc -l 结合,统计它们的行数?

最简单的方法是把 find 命令放入 $():

命令

$ wc -l $(find . -name "*.txt")

输出

  21022 ./writing/LittleWomen.txt
     11 ./writing/haiku.txt
      5 ./numbers.txt
  21038 total

shell 首先执行 $() 内的命令,然后把整个表达式替换为其输出。find 输出三个文件名 ./writing/LittleWomen.txt、./writing/haiku.txt、./numbers.txt,于是 shell 构造:

命令

$ wc -l ./writing/LittleWomen.txt ./writing/haiku.txt ./numbers.txt

这正是所需命令。展开机制与 shell 展开 *、? 一样,但允许把任意命令作为自定义“通配符”。

find 与 grep 常常配合:前者查找符合模式的文件,后者查找文件中符合另一个模式的行。例如,在当前目录全部 .txt 文件中搜索 searching:

命令

$ grep "searching" $(find . -name "*.txt")

输出

./writing/LittleWomen.txt:sitting on the top step, affected to be searching for her book, but was
./writing/haiku.txt:With searching comes loss

**练习**

匹配与排除

grep 的 -v 反转匹配,只打印不匹配模式的行。以下哪条命令会找到 creatures 中除 unicorn.dat 外的所有 .dat 文件?思考后可在 shell-lesson-data/exercise-data 目录测试。

1. find creatures -name "*.dat" | grep -v unicorn

2. find creatures -name *.dat | grep -v unicorn

3. grep -v "unicorn" $(find creatures -name "*.dat")

4. 以上都不是。

解答

选项 1 正确。给匹配表达式加引号,阻止 shell 展开,使模式原样传给 find。

选项 2 在本例中也有效:shell 尝试展开 *.dat,但当前目录没有这类文件,因此将通配符表达式传给 find。第 3 节曾介绍这一情况。

选项 3 错误:它搜索文件内容中不含 unicorn 的行,而不是搜索文件名。

二进制文件

此前只讨论文本文件的模式查找。如果数据是图像、数据库或其他格式,该怎么办?

少量工具可扩展 grep,处理一些非文本格式。更通用的办法是把数据转换为文本,或提取其中类似文本的元素。这样简单任务容易完成,但复杂任务通常难以实现。

例如,编写程序提取图像的 X、Y 尺寸供 grep 使用并不难,但如何找出含有公式的电子表格单元格中的值?

另一种选择是承认 shell 与文本处理的局限,改用其他编程语言。无需因此苛责 shell;许多现代语言借鉴了它的大量思想,而模仿也是最真诚的赞美。

Unix shell 比大多数使用者年纪都大。它能延续至今,是因为它属于有史以来最高效的编程环境,甚至可能是最高效的。语法或许晦涩,但熟练使用者可以交互地试验命令,再把所得知识用于自动化工作。图形界面起初可能更容易使用,但一旦掌握 shell,其生产力难以匹敌。

正如 Alfred North Whitehead 在 1911 年所写:“文明的进步,在于不断增加无需思考便能完成的重要操作的数量。”

**练习**

阅读 find 管道

为下面的 shell 脚本写一段简短解释:

命令

wc -l $(find . -name "*.dat") | sort -n

解答

1. 从当前目录递归查找扩展名为 .dat 的所有文件。

2. 统计每个文件的行数。

3. 按数值对第 2 步的输出排序。

要点

– find 查找具有特定属性、符合模式的文件。

– grep 选出文件中符合模式的行。

– 许多 Bash 命令及可在 Bash 中运行的程序支持 --help,显示使用说明。

– man [command] 显示指定命令的手册页。

– $([command]) 在当前位置插入命令输出。

—

原文:Finding Things。作者:Software Carpentry/The Carpentries 课程贡献者。教材许可:CC BY 4.0。本文为中文翻译;命令、数据及输出沿用原文,俳句作者致谢保留在正文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容