从文件中删除空白行
1. 概述
当我们在 Linux 中处理文本文件时,我们经常需要从文件中删除空行,以便于阅读或进一步处理。
在本教程中,我们将通过实际示例讨论从文件中删除空行的一些常见场景。
2. 问题
当我们在本教程中谈论空行时,我们指的是那些只包含空白字符 的行。 假设我们有一个纯文本文件:
$ cat with_blank.txt
This is the first non-blank line.
## Some data comes here
1
2
3
##
Data End.
This is the last non-blank line.
如上面的输出所示,文件with_blank.txt包含空白行,包括三个前导空白行和三个尾随空白行。
通常,我们可能希望通过三种方式从文件中删除空行:
- 删除文件中的所有空行
- Remove leading blank lines only — 只删除从文件开头直到第一个非空行
- Remove trailing blank lines only — 仅删除文件中最后一个非空行之后的行
在本教程中,我们将尝试使用*grep 、sed 、awk *和tac 命令解决这些问题。
3. 匹配空行的模式
要删除空行,我们首先需要识别它们。Regex 似乎是一种显而易见的方法,最便携的解决方案是使用POSIX BRE :
^[[:space:]]*$
[: space :]是 POSIX 标准字符类,与*[ \t\n\r\f\v]*相同。
由于字符类经常使用,因此有相应的速记字符类 可用。例如, \s代表 POSIX 类*[:space:]*,而 \S等同于 [^\s]。
许多编程语言和文本处理工具,包括 Java、Perl、Python、GNU grep、GNU sed和 GNU awk,都支持这些速记字符类。我们将在后面的部分中看到示例。
如果我们使用速记字符类匹配一个空行,正则表达式可以像这样紧凑:
^\s*$
接下来我们看看如何解决去掉空行的问题。
4. 从文件中删除所有空行
与仅删除前导或尾随空白行相比,删除所有空白行是一个更容易的问题。那是因为当我们找到一个空行后,我们不需要检查它是否应该被保留或删除。
我们的目标是获得输出:
This is the first non-blank line.
## Some data comes here
1
2
## 3
Data End.
This is the last non-blank line.
让我们看看如何解决这个问题。
4.1. 使用grep
我们知道grep实用程序擅长搜索文本。但是,删除行是一种文件编辑操作。看来我们为问题选择了错误的工具。
我们可以使用 grep的-v选项 来打印不包含空行模式的行。或者我们告诉grep输出包含非空白字符的行。
我们来看看 grep命令是如何解决问题的:
$ grep -v '^[[:space:]]*$' with_blank.txt
如果我们的 grep实现支持简写字符类,例如广泛使用的 GNU Grep,我们可以使命令非常短:
$ grep '\S' with_blank.txt
要将输出写回输入文件,我们需要将输出保存在临时文件中,然后将其“ mv ”到原始输入文件:
$ grep '\S' with_blank.txt > tmp.txt && mv tmp.txt with_blank.txt
4.2. 使用sed
** sed命令 有 d动作,代表删除当前模式空间。**
我们可以通过删除匹配空行模式的行来直接解决问题:
$ sed '/^[[:space:]]*$/d' with_blank.txt
我们也可以反过来解决它:如果一行包含非空白字符,那么我们不删除该行(!d)。
如果我们的 sed支持 \S作为非空白字符类,就像 GNU sed一样,命令可以像这样简单:
$ sed '/\S/!d' with_blank.txt
许多sed实现支持“就地”编辑,这样我们就可以将更改保存回输入文件。
例如,对于 GNU sed,我们可以使用*-i*选项:
$ sed -i '/^[[:space:]]*$/d' with_blank.txt
4.3. 使用awk
使用awk命令,我们可以用不同的方式删除空行。
让我们从一个简单的解决方案开始:
$ awk '!/^[[:space:]]*$/' with_blank.txt
在上面的解决方案中,如果一行与我们的空行模式不匹配,我们将打印它。
它以非常简短的形式编写。如果我们以完整的方式编写它,我们将有:
$ awk '{ if($0 !~ /^[[:space:]]*$/) print $0 }' with_blank.txt
让我们理解为什么它可以写成那种简短的形式:
- 当我们测试一个正则表达式模式时,如果我们不给出测试字符串,awk默认取当前行,所以 *if($0 !~ /pattern/)*可以写成 if(!/pattern/)
- 我们还可以将*’{if(condition){action}}’写为 ‘condition{action}’,因此,我们有’!/^[[:space:]]*$/{print $0}’*
- ** awk中的默认操作是 print $0, True将触发默认操作;因此,我们可以省略*{print $0}*并使用 ’!/^[[:space:]]*$/'
另一种解决问题的方法是检查一行是否包含非空白字符:
$ awk '/\S/' with_blank.txt
除了正则表达式检查之外,我们还可以检查 awk的内置NF变量以确定一行是否为空:
$ awk 'NF' with_blank.txt
NF 变量保存当前输入行中的字段数。在awk中,默认的字段分隔符 ( FS ) 是一个空格。
**如果FS是空格,则跳过所有前导和尾随空白字符。**因此,如果一行为空,则我们没有任何字段,换句话说,变量NF == 0。
**在awk中,非零数字将被评估为True。**因此,*‘NF’*将打印所有非空行。
5. 仅删除前导空行
如果我们只想删除前导空行,主要问题是要知道第一个非空行从哪里开始。
grep命令 不能解决这个问题。但是,我们仍然可以使用强大的sed和awk实用程序来完成它。
该问题的有效解决方案应打印:
This is the first non-blank line.
## Some data comes here
1
2
## 3
Data End.
This is the last non-blank line.
5.1. 使用sed
有几种方法可以使用sed命令解决问题。让我们看一下使用sed的地址范围的 两种方法。
第一个解决方案着重于前导空行的部分:
$ sed '1,/\S/{/\S/!d}' with_blank.txt
让我们了解发生了什么:
- *1、/\S/*是一个地址范围。选择从第一行开始到(含)第一个非空行
- {/\S/!d}是我们要对上述范围内的每一行应用的操作。!d对我们来说并不陌生,我们在这里再次使用它来将非空行保留在范围内并删除其余部分
我们还可以在从第一个非空行到文件末尾的范围内应用*!d*操作来解决问题:
$ sed '/\S/,$!d' with_blank.txt
5.2. 使用awk
首先,让我们看看一个简单的awk解决方案是什么样的:
$ awk '/\S/{p=1}p' with_blank.txt
awk 单行代码有两个部分。
第一部分是*/\S/{p=1} — 如果*记录是非空行,我们设置变量 p=1。
第二部分是简单的p——如果变量p包含一个非零数字,当前行将被打印。
在awk中,如果变量未初始化,则其默认值为空字符串或0。
因此,当第一个非空行到来时,变量 p从0设置为1 ,并且该值将一直保留到文件的最后一行。
这样, awk命令从第一个非空行打印到输入文件的末尾。
6. 仅删除尾随空行
通常,文本处理工具会按照从文件的开头到结尾的顺序处理一个文件的行,并且不容易回头查看我们已经处理过的行。
因此,我们对这个问题的主要挑战是找出文件中的最后一个非空行。
工作解决方案打印的输出如下所示:
This is the first non-blank line.
## Some data comes here
1
2
## 3
Data End.
This is the last non-blank line.
6.1. 使用tac
tac命令是GNU Coreutils 包的成员。它默认预装在所有 Linux 发行版中。
cat命令以其自然顺序打印文件,而tac命令以相反的顺序打印文件。(注意tac只是cat 的反拼写!)
一个例子清楚地展示了它的能力:
$ cat file
1
2
3
4
5
$ tac file
5
4
3
2
1
我们可以通过两次使用tac命令来解决删除尾随空白行的问题:
tac input | <COMMAND TO REMOVE LEADING BLANK LINES> | tac
例如:
$ tac with_blank.txt | sed '/\S/,$!d' | tac
tac命令确实简化了问题。但是,我们要启动三个进程,对输入文件的内容进行三次处理。
有时这可能会很痛苦——尤其是当我们必须处理巨大的输入文件时。
6.2. 使用sed
sed解决方案 不像使用tac 的解决方案那么简单。但是,它启动一个进程并只读取一次输入文件:
$ sed ':a; /^[[:space:]]*$/ { $d; N; ba; }' with_blank.txt
现在,让我们先了解一下sed一行中的代码:
- :A; –创建一个名为“ a ”的标签
- /^[[:space:]]*$/ { actions } – 如果当前模式空间 匹配空行模式,将执行以下动作
- $d; – 仅当当前行是输入文件中的最后一行时才跳过模式空间
- N;– 从输入文件中读取下一行,并将其附加到模式空间
- ba; – 分支到标签a
让我们来看看这个聪明的解决方案是如何工作的。
一旦读取了一个空行,这个单行代码就会通过递归分支到标签a 将后续行追加到模式空间中。
由于空白类[[:space:]]包含换行符,/^[[:space:]]*$/ 匹配多个空行。
但是,一旦读取非空白并将其附加到模式空间中,模式空间中的字符串就不再与模式/ *^[[:space:]]*$/匹配。*所以,我们打破递归,打印模式空间,然后清除它。
如果输入文件有尾随空行,它们都将在模式空间中,文件中的最后一行将跳过模式空间*$d*。因此,连续的尾随空白行不会出现在输出中。
6.3. 使用awk
使用awk命令,有两种方法可以识别文件的最后一个非空行。
让我们看一下第一种方法:
$ awk '{a[NR]=$0; if(/\S/)mark=NR} END{for(i=1;i<=mark;i++)print a[i]}' with_blank.txt
上面的awk代码只读取输入文件一次。让我们分解一下:
- 读取并保存数组中的每一行:a[]
- 将最后一个非空行号保存在名为mark的变量中
- 读取所有行后,再次遍历数组并打印行,直到我们保存在标记中的行
作为替代方案,我们也可以通过读取输入文件两次 来解决问题:
$ awk 'NR==FNR && /\S/{mark=NR; next} FNR<=mark' with_blank.txt with_blank.txt
第一次读取找出最后一个非空行的行号并将其保存在名为mark的变量中。
然后,如果行号 ( NR ) 小于或等于mark ,则第二次读取打印每一行。