Contents

Linux中的两种模式之间的打印行

1. 概述

当我们在 Linux 命令行中工作时,我们可以 通过一个方便的实用程序进行基于行的常见文本搜索grep命令。

但是,有时,我们的目标数据位于两个模式之间的一个块中。在本教程中,我们将讨论如何在两种模式之间提取数据块。

2. 问题介绍

首先,让我们看一个示例输入文件。它将帮助我们快速理解问题:

kent$ cat input.txt
XXXX we want to skip this line XXXX
XXXX we want to skip this line XXXX
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
XXXX we want to skip this line XXXX
XXXX we want to skip this line XXXX
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END
XXXX we want to skip this line XXXX
XXXX we want to skip this line XXXX

如上面的输出所示,在输入文件中,我们有以“ [ Block #x ] … ”开头的行。这些数据块始终位于两种模式之间:“ DATA BEGIN ”和“ DATA END ”。

我们的目标是遍历输入文件并提取两种模式之间的所有数据块。

除了打印数据块,在现实世界中,我们可能对它们的边界有各种要求,即匹配两种模式的线:

  • 包括两个边界
  • 仅包括“ DATA BEGIN ”行
  • 仅包括“ DATA END ”行
  • 排除两个边界

在本教程中,我们将涵盖上述所有场景,并讨论如何使用GNU SedGNU Awk 解决问题。

3. 使用sed命令

sed命令是一种常用的命令行文本处理实用程序。它支持地址范围。

例如,sed /Pattern1/, /Pattern2/{ commands }…将在行的范围内应用commands。在此示例中,范围中的第一行是匹配 */Pattern1/*的行,而范围中的最后一行是匹配 */Pattern2/*的行。

sed的 地址范围可以帮助我们解决问题。接下来,让我们仔细看看解决方案。

3.1.打印包含两个边界的数据块

首先,让我们看一下解决问题的 sed命令:

kent$ sed -n '/DATA BEGIN/, /DATA END/p' input.txt
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END

正如我们所看到的,输出是我们所期望的。该命令看起来非常简单。

但是让我们快速了解*-n*选项和 p命令的用法,因为我们将使用这种组合来解决其他场景中的问题。

默认情况下, sed命令将在每个循环结束时打印模式空间

然而,在这个例子中,我们只想让sed打印我们需要的行。因此,我们使用了*-n选项来防止sed命令打印模式空间。相反,我们将使用p*命令控制输出。

3.2. 仅打印包含“BEGIN”边界的数据块

现在,我们有一个新要求:只包括“BEGIN”边界。换句话说,我们必须抑制“END”边界输出。

因此,我们可以对地址范围内的行再做一次检查,并跳过打印匹配“END”模式的行:

kent$ sed -n '/DATA BEGIN/, /DATA END/{ /DATA END/!p }' input.txt
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00

如上面的输出所示,我们已经解决了这个问题。

3.3. 仅打印包含“END”边界的数据块

现在解决这个问题对我们来说不是挑战,因为它与我们刚刚征服的问题非常相似。我们需要做的是改变sed的 {… } 块中的模式:

kent$ sed -n '/DATA BEGIN/, /DATA END/{ /DATA BEGIN/!p }' input.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END

3.4. 打印不包括两个边界的数据块

最后,让我们讨论如何只打印没有边界线的数据块。

我们可能认为我们可以通过使用逻辑AND加入两个进一步的检查来轻松满足此要求,例如“ sed -n ‘/BEGIN/, /END/{ ( /BEGIN/! AND /END/! ) { p } } ’ ……”。

但是,** sed不支持逻辑操作。**因此,我们不能使用AND操作连接两个地址。相反,我们可以嵌套这两个检查,使其与AND操作一样工作。

接下来,让我们看看它是如何完成的:

kent$ sed -n '/DATA BEGIN/, /DATA END/{ /DATA BEGIN/! { /DATA END/! p } }' input.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00

感谢sed的地址范围,我们可以使用sed命令解决所有四种情况下的问题 。

4. 使用awk命令

awk命令也是一个强大的命令行文本处理工具。

如果我们回顾一下 sed的解决方案,我们会意识到即使我们可以使用sed来解决问题,由于它对编程语言的支持最少,我们无法以更自然的方式编写我们的sed命令,尤其是当需求越来越大时复杂。

sed命令不同,awk命令支持具有“类C ”语法的脚本语言。我们可以使用我们熟悉的许多编程语言特性来构建我们的awk命令/脚本,例如声明变量、逻辑操作和函数。

接下来,让我们看看如何使用 awk命令解决我们的问题。

4.1. 打印包含两个边界的数据块

sed 类似,awk命令也支持范围模式。因此,我们可以用同样的方法解决这个问题:

kent$ awk '/DATA BEGIN/, /DATA END/' input.txt 
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END

在上面的 awk命令中,我们没有明确地将 print写入输出。这是因为布尔值True将触发默认操作 :打印当前行

显然,只有范围模式内的线才会导致*True。*因此,我们在输出中得到了预期的数据。

此外,如果变量包含非零值,则awk命令也会将此变量评估为True

因此,我们可以声明一个变量来在某些条件下打开和关闭打印。这样,我们可以更直接地控制边界输出:

kent$ awk '/DATA BEGIN/{ f = 1 } f; /DATA END/{ f = 0 }' input.txt
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END

这一次,我们不使用范围模式。相反,我们声明了一个变量f作为 awk打印机的开关。

当一行与数据块的“BEGIN”匹配时,我们将其打开:/DATA BEGIN/{ f = 1 } ,并通过“ *f;”*打印 BEGIN 边界。

由于开关 f已打开,我们将打印以下所有行,直到出现“END”行。

当“END”行到达时,我们首先打印它,因为变量f的值仍然是 1。然后,我们关闭开关:*/DATA END/{f = 0}*以防止输出以下行。

我们可以利用这种“打印机开关”的思路来解决其他场景的问题。

接下来,让我们详细看看它们。

4.2. 仅打印包含“BEGIN”边界的数据块

我们可以稍微改变上一节中的 awk命令,让它只打印目标数据块和“BEGIN”边界线:

kent$ awk '/DATA BEGIN/{ f = 1 } /DATA END/{ f = 0 } f' input.txt
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00

让我们将此awk命令与我们打印数据的命令进行比较,包括两条边界线:

... '/DATA BEGIN/{ f = 1 } f; /DATA END/{ f = 0 }' ...  <--- Including both boundaries
... '/DATA BEGIN/{ f = 1 } /DATA END/{ f = 0 } f ' ...  <--- Including the BEGIN boundary only

我们所做的唯一更改是 在“END”模式检查之后移动f 。 如果“END”边界线出现,我们关闭开关。之后,我们检查开关并打印输出。也就是说,不会打印“END”边界线。

4.3. 仅打印包含“END”边界的数据块

遵循相同的想法,如果我们将f移到“BEGIN”模式检查之前,“BEGIN”边界线将不会出现在输出中:

kent$ awk 'f; /DATA BEGIN/{ f = 1 } /DATA END/{ f = 0 }' input.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END

该命令不难理解。但是,让我们快速解释一下为什么我们可以在给它赋值之前使用变量f

**在 awk中,如果我们使用一个尚未声明或分配的变量,它的值将是一个空字符串或数字0。**此外,该变量将被评估为False。因此不会触发默认操作(print)

4.4. 打印不包括两个边界的数据块

现在,让我们看看如何排除输出中的所有边界线:

kent$ awk '/DATA BEGIN/{ f = 1; next } /DATA END/{ f = 0 } f' input.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00

这一次,我们不能仅通过调整变量f的位置来解决问题。

如示例所示,棘手的部分是,当“BEGIN”模式出现时,我们打开输出并立即执行next操作:’/DATA BEGIN/{ f = 1; next }。

** next动作将停止处理当前行并从输入中读取下一行。**因此,我们只打开开关而不打印“BEGIN”边界。

5. 角落案例

我们已经学会了使用awk和 sed提取两个模式之间的数据线。在我们的输入文件中,“BEGIN”和“END”模式配对得很好。

然而,在现实世界中,输入文件可能是不完整的。让我们看另一个例子:

kent$ cat input2.txt
XXXX we want to skip this line XXXX
XXXX we want to skip this line XXXX
DATA BEGIN
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
DATA END
XXXX we want to skip this line XXXX
XXXX we want to skip this line XXXX
DATA BEGIN
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
DATA END
XXXX we want to skip this line XXXX
XXXX we want to skip this line XXXX
DATA BEGIN
[ Block #3 ] ... Incomplete data
[ Block #3 ] ... Incomplete data

在 input2.txt文件中,最后一个数据块只有一个“BEGIN”模式。如果我们对这个文件应用 sed和 awk解决方案,不完整的数据行也会出现在输出中:

kent$ awk '/DATA BEGIN/{ f = 1; next } /DATA END/{ f = 0 } f' input2.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00
[ Block #3 ] ... Incomplete data
[ Block #3 ] ... Incomplete data
kent$ sed -n '/DATA BEGIN/, /DATA END/{ /DATA BEGIN/! { /DATA END/! p } }' input2.txt
... the same output as the awk command...

根据需求,我们可能只想打印完整的数据块而丢弃不完整的数据。

接下来,让我们看看如何使用 sed和 awk 处理这种极端情况。

5.1. 使用awk命令

首先,让我们看一下可行的解决方案,然后我们讨论它是如何工作的:

kent$ awk 'f { if (/DATA END/){
                    printf "%s", buf; f = 0; buf=""
                } else
                    buf = buf $0 ORS
             }
           /DATA BEGIN/ { f = 1 }' input2.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00

在命令中,我们仍然使用 f变量作为输出开关。然而,一个额外的*if…else…逻辑出现了。*让我们了解它是如何工作的。

在深入了解awk代码之前,让我们考虑一下我们的主要问题是什么?

** awk命令按顺序处理输入文件中的行。**

因此,解决这个问题的难点在于,当我们看到“BEGIN”行时,我们不知道即将到来的数据块是否完整。也就是说,在到达“END”行之前,我们无法决定是否应该在数据块中打印一行。

为了解决这个问题,我们可以首先将数据行存储在一个变量中,比如 buf,而不是打印它们。只有当“END”行出现时,我们才打印该值并重置buf变量。

现在,让我们仔细看看代码是如何工作的:

  • f { … }:我们仍然使用f变量作为标志来指示一行是否在我们的目标数据块中。如果 fTrue,我们将处理{ … }中的逻辑
  • if (/DATA END/){printf “%s”, buf; f = 0; buf=""}:如果当前行是“END”边界,则表示该块已完成。因此,我们打印buf中的值,关闭打印机开关并重置 buf变量
  • else buf = buf $0 ORS:但是,如果当前行不是“END”边界,我们将当前行附加到带有换行符的buf变量
  • /DATA BEGIN/ { f = 1 }:这行对我们来说并不新鲜。如果“BEGIN”边界线出现,我们打开开关f

5.2. 使用 sed命令

不幸的是,sed不支持变量,但我们仍然可以通过控制模式和保持空格来解决它:

kent$ sed -n '/DATA BEGIN/,/DATA END/{/DATA END/{s/.*//;x;s/^\n//;p;d};/DATA BEGIN/!H }' input2.txt
[ Block #1 ] ... 1992-08-08 08:08:08
[ Block #1 ] ... DATA #1 IN BLOCK
[ Block #1 ] ... 2018-03-06 15:33:23
[ Block #2 ] ... 2021-02-01 00:01:00
[ Block #2 ] ... DATA #2 IN BLOCK
[ Block #2 ] ... 2021-02-02 01:00:00

可能,该命令看起来并不那么简单。但这并不难理解。

接下来,让我们快速浏览一下:

  • /DATA BEGIN/,/DATA END/{ … }sed 的范围地址对我们来说并不陌生。如果行在范围内,将处理*{ … }*内的逻辑
  • /数据结束/{s/.*//;x;s/^\n//;p;d}; :如果当前行是“END”边界,我们清除当前模式空间(s/.*//;),交换模式内容并保持空格(x;),删除第一个空行(s/^ \n//;),打印内容(p;),并清除当前模式空间(d
  • /DATA BEGIN/!H:如果当前行不是“BEGIN”边界,则它是我们目标数据块中的正常数据行。对于这样的行,我们将它们附加到保持空间(H

正如我们所见,sed命令使用保持空间作为变量来保存数据行。基本上,它实现了与我们使用 awk命令相同的想法。