过滤器:按您的方式操作

作者:Malcolm Murphy

Linux 的基本哲学之一(与所有 Unix 版本一样)是每个程序都执行一项特定任务,并且做得很好。通常,您会组合多个程序来实现某些目标,无论是在 shell 提示符下还是在脚本中,方法是将一个程序的输出通过管道传输到下一个程序。我指的是像这样的事情

ls -l | more

ps -auxw | \
  grep netscape >> people.who.should.be.working

但是,如果一个程序的输出不是下一个程序所需的格式怎么办?我们需要某种方法来处理一个程序的输出,使其为下一个程序做好准备。

幸运的是,有很多 Linux 程序可以完成这项工作:读取一些输入,对其执行一些操作,并将更改后的数据作为输出写入。这些程序称为过滤器。有些过滤器执行的任务非常有限,例如 head、grep 和 sort,而另一些过滤器则更灵活,例如 sed 和 awk。在本文中,我们将介绍其中一些更灵活的过滤器,并给出一些可以使用它们的示例。

名称“sed”是 流编辑器 的缩写;sed 将编辑命令应用于数据流。sed 的常见用途是将一个文本模式替换为另一个文本模式,如下所示

sed 's/Fred/Barney/g' foo

此命令接受文件 foo,将每次出现的 Fred 更改为 Barney,并将修改后的版本写入标准输出。

请注意,在本示例中,我们将实际的 sed 命令放在单引号内。Sed 不要求以这种方式引用命令,但是如果 sed 命令包含对 shell 特殊的字符(例如 $*),则需要使用引号。此示例没有任何特殊字符,因此我们可以轻松地省略引号。尝试一下看看。

如果没有输入文件 foo,sed 会从标准输入读取,因此我们可以使用以下命令获得相同的结果

sed 's/Fred/Barney/g' < foo

cat foo | sed 's/Fred/Barney/g'

请注意,前两个版本通常优于第三个版本。仅使用 cat 将输入发送到管道会创建一个额外的进程,而这通常是可以避免的。

我们还必须考虑输出。默认情况下,结果会显示在标准输出上,但这并不总是我们想要的。一种选择是通过分页器将输出通过管道传输,例如

sed 's/Fred/Barney/g' foo | more

或将其重定向到文件

sed 's/Fred/Barney/g' foo > bar

虽然经常想写

sed 's/Fred/Barney/g' foo > foo

但这唯一实现的是删除文件 foo 的内容!为什么?因为 shell 对此命令做的第一件事是打开文件 foo 进行输出,从而破坏了已经存在的内容。当它尝试从 foo 读取时,没有内容可读。结果是一个空文件。以这种方式重定向输出时,很容易犯这个错误,所以要小心。

Awk 比 sed 更加灵活;它本身就是一种成熟的编程语言。但是,不要因此而退缩。用 awk 编写简单的程序出奇地容易,而且通常感觉不像是一种编程语言 [请参阅Linux Journal 第 25 期,1996 年 5 月,第 46 页—ED]。例如,命令

awk '{print NR, $0}' foo

打印文件 foo,并为每行编号。Awk 也可以像 sed 一样从管道或标准输入中读取其输入,并且除非您重定向它,否则也会写入标准输出。引号之间的部分(这是必要的,因为 {} 字符也是 shell 的特殊字符)是 awk 程序。我说它们可以很简单,不是吗?一个 awk 程序只是一个或多个模式-动作语句的序列,形式为

pattern { action }

每个输入行都依次针对每个模式进行测试。当输入行与模式匹配时,将执行相应的操作。模式可以为空,在这种情况下,每行都匹配;或者动作可以为空,在这种情况下,默认动作是打印该行。

在上面的示例中,模式为空,因此每行都匹配。动作是打印 NR,这是一个内置的 awk 变量,其中包含到目前为止读取的行数,然后打印 $0,这是当前行。

继续

现在我们已经了解了 sed 和 awk 背后的基本思想,我们将看一些示例。学习东西的最佳方法是实际操作,我建议您在学习过程中自己尝试其中一些示例,甚至可以关注手册页。我们当然不会涵盖 sed 和 awk 可以做的所有事情,但是希望您在阅读完本文后,将更有信心自己尝试。

我们的第一个示例是从文档中删除所有空格。这很容易使用 sed 实现

sed 's/ *//g' foo

这就像前面 Fred 和 Barney 的示例,只是在这里我们使用了正则表达式:' *' (包含引号是为了您可以看到作为正则表达式一部分的空格)。sed 的 s(用于替换)命令使用正则表达式,就像 grep 一样。正则表达式 ' *' 匹配一个或多个空格,这些空格将被替换为 ——它们被删除。此命令目前不处理制表符,但是您可以对其进行修改以匹配一个或多个制表符或空格的出现

sed 's/[ {tab}][ {tab}]*//g' foo
双倍行距

接下来,我们将考虑双倍行距文本文件。我们可以使用 sed 的替换命令来做到这一点,方法是将 $(行尾的正则表达式)替换为换行符(我们必须用反斜杠引用它)

sed 's/$/\
/' foo

请注意,在本示例中,与所有先前的示例不同,第二个引号之前没有 gg 用于告诉 sed 替换应用于每行上的所有匹配项,而不仅仅是每行上的第一个匹配项,这是默认行为。在这种情况下,由于每行只有一个结尾,因此我们不需要 g

在 sed 中执行此操作的另一种方法是

sed G foo

如果您查看 sed 的手册页,它会说 G “将换行符和保留空间的内容附加到模式空间”。模式空间是 sed 术语,指的是当前正在读取的行,我们现在无需担心保留空间(相信我,它将是空的),因此此命令完全符合我们的要求。

在 awk 中使用双倍行距非常容易,使用我们之前看到的 print 语句

awk '{print $0; print ""}' foo

在这里,模式再次为空,匹配每一行,动作是打印整行 $0,然后不打印任何内容 ""。每个 print 语句都启动一个新行,因此这两个命令的组合效果是双倍行距文件。

Awk 动作可以(并且通常会)以这种方式涉及多个命令,但在这里并非绝对必要。Awk 提供了一个格式化的 print 语句,与基本的 print 语句相比,它可以更好地控制输出。因此,我们可以使用以下方法获得相同的结果

awk '{printf("%s\n\n",$0)}' foo

printf 语句的第一个参数是 格式,它描述了输出应如何显示。格式可以包含要按字面意思打印的字符(本例中没有)、转义序列(例如 \n 表示换行符)和 规范。规范是以 % 开头的字符序列,用于控制其余参数的打印方式。对于第二个和后续的每个参数,都必须有一个规范。在本示例中,有一个规范 %s,它打印一个字符串。与该规范关联的值是 $0;整行。与 print 不同,printf 不会自动启动新行,因此需要两个 \n:一个用于结束原始行,另一个用于插入空行。

对于这个看似简单的示例——双倍行距文件——我们提出了四种不同的解决方案。总是有不止一种方法可以解决问题,通常选择哪种方法并不重要。关键是,您通常会编写 awk 或 sed 程序来完成特定任务,然后在需要时将其丢弃。您不一定想要“最佳”解决方案(无论这意味着什么),您只是想要一些有效的解决方案,并且您希望它快速。

选择性

另一个非常常见的任务是仅选择输入的一部分。假设我们想要文件 foo 的第五行。在 awk 中,这将是

awk 'NR==5' foo

NR(到目前为止读取的行数)等于 5 时,它会打印该行。sed 等效项是

sed -n 5p foo

默认情况下,在应用所有命令后,sed 会打印每一行输入。-n 选项禁止此行为,因此我们只会获得我们使用 p 命令专门要求的行。在本例中,我们要求第五行,但是我们可以轻松地指定一系列行,例如第三行到第五行,使用

sed -n 3,5p foo

或者,在 awk 中

awk 'NR>=3 && NR<=5' foo

在 awk 版本中,&& 表示“和”,因此我们想要 NR>=3 NR<=5 的行,即第三行到第五行。

另一种方法是将 head 和 tail 结合使用

head -5 foo | tail -3

它使用 head 程序获取文件的前 5 行,并使用 tail 程序仅传递最后三行。

另一个常见问题是仅删除第一行。还记得 $ 字符在正则表达式中使用时表示行尾吗?好吧,当您使用它来指定行号时,它表示最后一行

sed -n '2,$p' foo

在 awk 中,您可以使用 !=> 从这些命令中的任何一个获得相同的结果

awk 'NR>1' foo
awk 'NR!=1' foo
当行号不足时

使用行号选择文件的一部分很容易做到,但是通常您不知道您想要的行号。相反,您想根据行的内容选择行。在 awk 中,我们可以轻松地选择与模式匹配的行,使用

awk '/regexp/' foo

这将导致打印所有包含 regexp 的行。有一个直接的 sed 等效项

sed -n '/regexp/p' foo

当然,我们也可以使用 grep 来完成此类操作

grep 'regexp' foo

但是 sed 也可以轻松处理范围。例如,要获取文件中直到并包括与正则表达式匹配的第一行的所有行,您将键入

sed -n '1,/regexp/p' foo

或者获取包括和在正则表达式匹配的第一行之后的所有行

sed -n '/regexp/,$p' foo

请记住,$ 表示文件中的最后一行。您还可以基于两个正则表达式指定范围。尝试

sed -n '/regexp1/,/regexp2/p' foo

请注意,这会打印所有以包含 regexp1 的行开始,到包含 regexp2 的行结束的所有块,而不仅仅是第一个块。如果对于包含 regexp1 的行没有匹配的 regexp2,那么我们将获得到文件末尾的所有行。

现在我们可以根据正则表达式选择输入的一部分。

我们可能想要删除一些包含特定模式的行。d 命令正是这样做的

sed '/regexp/d' foo

删除所有与正则表达式匹配的行。或者,我们可能想要删除一段文本

sed '/regexp1/,/regexp2/d' foo

删除从包含 regexp1 的行开始,到并包括与 regexp2 匹配的行的所有内容。同样,sed 将选择由 regexp1regexp2 分隔的所有文本块,因此存在我们可能删除超出我们想要的内容的风险。

在给定点插入文本也是可能的。命令

sed '/regexp/r bar' foo

在文件 foo 中与 regexp 匹配的任何行之后插入文件 bar 的内容。

现在,我们可以将最后两个命令组合起来,以将文件中的文本块替换为另一个文件的内容。我们这样做

sed -e '/START/r bar' -e '/START/,/END/d' foo

这会查找包含 START 的行,删除到包含 END 的行,然后读取文件 bar 的内容。由于 r 命令直到读取下一行输入才读取文件,因此 d 命令在新文本读取之前执行,因此 d 命令不会像人们可能期望的那样删除新文本,查看此命令。-e 选项告诉 sed 下一个参数是命令,而不是输入文件。尽管当只有一个命令时它是可选的,但是如果我们有多个命令,则每个命令都必须以 -e 开头。

这些示例主要是面向行的,但是我们同样可能想要处理数据列。过滤器 cut 可以选择数据列。例如,要列出系统上所有用户的真实姓名,您可以键入

cut -f5 -d: /etc/passwd
The 5 argument after -f tells cut to list the
fifth column (where real names are stored), and the -d
flag is used to tell cut which character delimits the
field—in the case of the password file, it's a colon. To get
both the username (which is in the first column) and the real
name, we could use
cut -f1,5 -d: /etc/passwd

Awk 也擅长获取数据列,我们可以使用以下 awk 命令完成这些任务

awk -F: '{print $5}' /etc/passwd

awk -F: '{print $1,$5}' /etc/passwd

其中 -F 标志告诉 awk 字段由什么字符分隔。(您是否看到使用 cut 和使用 awk 打印多个字段之间的区别?如果看不到,请尝试再次运行命令并仔细观察。)

使用 awk 的一个优势是我们可以对列执行操作。

例如,如果我们想知道当前目录中的文件占用了多少磁盘空间,我们可以将 ls -l 输出的第五列加起来

ls -l | grep -v '^d' | \
  awk '{s += $5} END {print s}'

在此命令中,我们使用 grep 删除任何以 d 开头的行,因此我们不计算目录。我们选择了 grep,但是我们也可以轻松地使用 awk 或 sed 来完成此操作。一个纯 awk 解决方案可能是

ls -l | awk '! /^d/ {s += $5} END {print s}'

其中 awk 程序仅合计不以 d 开头的行的第五列——模式之前的感叹号告诉 awk 选择 匹配正则表达式 /^d/ 的行。

处理文件名

通常,许多文件具有相同的基本名称,但扩展名不同。例如,假设我们有一个 TeX 文件 foo.tex。那么我们很可能有关联的文件 foo.aux、foo.bib、foo.dvi、foo.ps、foo.idx、foo.log 等。您可能希望脚本能够处理这些文件,给定文件 foo.tex 的名称。basename 实用程序

basename foo.tex .tex

将为您提供基本名称 foo。如果我们有一个包含 TeX 文件名称的 shell 变量,我们可以使用

basename ${TEXFILE} .tex

同样,有不止一种方法可以获取文件的基本名称:您可以使用 sed 来完成此操作

echo ${TEXFILE} | sed 's/.tex$//'

无论我们采用哪种方法,一旦我们知道基本名称,我们都可以构造其他文件的名称。例如,我们可以通过以下方式获取日志文件的名称

LOGFILE=`basename ${TEXFILE} .tex`.log

这非常有用:我使用 vi 进行大部分编辑,它允许您在宏中获取当前正在编辑的文件名;% 被文件名替换。如果我正在编辑 TeX 文件 foo.tex,并且我想使用 xdvi 预览 dvi 文件,我可以自动在宏中将 %(我们称之为 foo.tex)转换为 foo.dvi

:!xdvi `basename % .tex`.dvi &

我可以将其绑定到一个功能键,并且在我想查看 dvi 文件时永远不必担心 dvi 文件的名称,方法是将此行添加到我的 .exrc 文件中

map ^R :!xdvi `basename % .tex`.dvi &^M^M

^R^M 字符分别通过键入 Control-V Control-R 和 Control-V Control-M 添加,假设您正在使用 vi 编辑 .exrc 文件。

结论

在本文中,我们研究了 Linux 中用于过滤文本的一些工具。我们已经看到如何使用这些过滤器来操作一个命令的输出,使其成为更方便的形式,以用作另一个程序的输入或供人阅读。这种任务自然会在许多基于 shell 的工作中出现,无论是在脚本中还是在命令行中,因此这是一项方便的技能。尽管 sed 和 awk 的手册页可能有点晦涩难懂,但是解决问题的方案通常可能非常简单。通过一些练习,您可以做很多事情。

加载 Disqus 评论