在Linux中按行合并两个文件
1. 概述
我们知道我们可以使用命令cat file1 file2来连接多个文件 。然而,有时,我们想按列组合两个文件。
在本教程中,我们将学习如何在 Linux 命令行下执行此操作。
2. 问题介绍
有时,我们想按列组合两个文件。换句话说,将它们并排显示,以便我们可以更轻松地阅读或比较它们的内容。 让我们看一个简单的例子:
$ head left.txt right.txt
==> left.txt <==
I am line 1 on the left.
I am line 2 on the left.
I am line 3 on the left.
I am line 4 on the left.
==> right.txt <==
Right side: line #1
Right side: line #2
Right side: line #3
Right side: line #4
如上面的输出所示,我们有两个文件,left.txt 和right.txt。现在,我们想要一个像这样的新文件或输出:
I am line 1 on the left. Right side: line #1
I am line 2 on the left. Right side: line #2
I am line 3 on the left. Right side: line #3
I am line 4 on the left. Right side: line #4
该示例显示了最佳情况。left.txt 中的所有行都具有相同的长度。因此,很容易获得非常一致的输出。
然而,在实际中, left.txt中的行的长度可以是多种多样的。我们可能希望输出仍然保持一致。
此外,两个文件中的行数可能不同。有时,我们想在生成的并排视图中确定文件是否已到达末尾。
此外,在某些情况下,如果我们可以自定义两个文件内容之间的分隔符就更好了。
在本教程中,我们将探讨涵盖所有这些方面的不同解决方案。
接下来,让我们看看它们的实际效果。
3. 并排显示两个文件——paste命令
**paste 命令可以合并多个文件的行。**此外,它非常易于使用:
$ paste left.txt right.txt
I am line 1 on the left. Right side: line #1
I am line 2 on the left. Right side: line #2
I am line 3 on the left. Right side: line #3
I am line 4 on the left. Right side: line #4
正如我们所看到的,我们只是直观地将这两个文件传递给 paste命令。它将合并输入文件中的行。
** paste命令的默认分隔符是制表符。**
在此示例中,两个文件的内容也用制表符清楚地分隔开,因为 left.txt 中的所有行都具有相同的长度。
4. 漂亮对齐输出——column命令
我们已经看到 paste 命令可以很容易地并排显示两个文件。
然而,正如我们所提到的,在实践中,输入文件的格式可能多种多样。例如,left.txt文件中的行的长度可以不同。因此,输出格式可能会很混乱。
4.1. 新问题
让我们看看另一对文件,left2.txt 和right2.txt:
$ cat -n left2.txt
1 I'm line1.
2 I am line2, I am very very very long.
3
4 Hi, I'm line4.
$ cat right2.txt
Line1 on the right
Line2 on the right
Line3 on the right
Line4 on the right
Line5 on the right
Line6 on the right
在left2.txt文件中,我们有四行。另外,第三行是空的。我们使用带有 -n 选项的cat命令将行号添加到输出中,以便我们可以识别空行。
此外,第二行比文件中的其他行长。
另一方面,* right2.txt *有六行相同的长度。
现在,如果我们将这两个文件传递给 paste命令,我们将得到:
$ paste left2.txt right2.txt
I'm line1. Line1 on the right
I am line2, I am very very very long. Line2 on the right
Line3 on the right
Hi, I'm line4. Line4 on the right
Line5 on the right
Line6 on the right
paste命令仍然并排列出两个文件的内容。但是,很明显,格式很乱,不便于阅读。
接下来,让我们对齐输出。
4.2. column命令
column 命令是util-linux 包的成员,大多数现代 Linux 发行版默认提供该包。
顾名思义,column 命令擅长重新格式化列中的输入。此外,它还提供了许多选项来控制输出对齐方式和格式。
但是为了解决我们的问题,基本上,我们只需要两个选择:
- -s – 设置字段之间的分隔符
- -t – 要求列将内容重新格式化为表格。也就是说,列将对齐
我们了解到 paste 命令的默认分隔符是制表符。因此,我们可以告诉column命令使用制表符作为分隔符来重新格式化结果:
$ paste left2.txt right2.txt | column -s $'\t' -t
I'm line1. Line1 on the right
I am line2, I am very very very long. Line2 on the right
Line3 on the right
Hi, I'm line4. Line4 on the right
Line5 on the right
Line6 on the right
现在,输出看起来好多了。
当我们将选项卡传递给列的 -s 选项时,我们可能会注意到我们使用了一种特殊格式:$’\t’。
如果我们传递字符串*’\t’,* column命令不会将其视为制表符*。*相反,它将使用“ t ”字符作为分隔符。也就是说,不会扩展ANSI C 标准反斜杠转义字符。
但是,当我们*使用*$’…’形式时,反斜杠转义字符将按照 ANSI C 标准的规定进行替换。
4.3. 选择分离器
我们已经看到column -s$’\t’ -t命令如何对齐输出。但是,可能会出现一个问题:如果输入文件的内容已经包含制表符怎么办?
是的,如果文件包含制表符,此 column命令将不会产生我们预期的结果。
解决方案是选择一个未使用的字符作为paste和column命令的分隔符。
**我们可以使用paste 命令的 -d 选项 来设置一个分隔符来覆盖默认的制表符。**稍后,当我们将结果通过管道传递给column命令时,我们可以使用相同的字符作为分隔符。
实际上,选择一个未使用的字符并不是一件容易的事,因为我们无法预测输入文件中有哪些字符。
但是,我们可以选择一些通常不会包含在文件内容中的不可见字符,例如:
- \a – 警铃
- \e – 转义
- \f – 换页
- *\v——*垂直制表符
- \x01 – 标题开始
- \x02 – 文本开始
- …
接下来,让我们看一个使用“ \a ”字符作为分隔符来获得对齐输出的示例:
$ paste -d $'\a' left2.txt right2.txt | column -s $'\a' -t
I'm line1. Line1 on the right
I am line2, I am very very very long. Line2 on the right
Line3 on the right
Hi, I'm line4. Line4 on the right
Line5 on the right
Line6 on the right
如上面的输出所示,我们得到了预期的结果。
但是,有时我们希望自定义两个文件内容之间的分隔符。
此外,查看输出,我们无法判断 left2.txt是四行还是六行。因此,当两个文件包含不同行数时,在结果中,我们可能希望确定较短的文件没有更多的行。
接下来,让我们看看如何实现这些需求。
5. 自定义分隔符和识别不存在的行
awk 是一个强大的文本处理实用程序,它当然可以处理多个文件 。awk定义了一种类似 C 的脚本语言。这使得 awk可以非常灵活地处理文本。
假设我们想使用字符串“ <- = -> ”来分隔两个输入文件的行。
此外,如果一个文件没有更多行,我们希望显示文本:“ [ File Ended. 没有更多的行] ”。
5.1. * awk *解决方案
现在,让我们看看 awk 是如何解决这个问题的:
awk -v sep='<- = ->' -v no_line_txt='[ File Ended. No More Lines ]' '
NR==FNR { max_length = (length > max_length)? length : max_length
left_lines = FNR
left[FNR] = $0
next
}
{ printf "%-*s %s %s\n", max_length, (FNR in left? left[FNR] : no_line_txt), sep, $0 }
END { if (FNR < left_lines) {
for (i=FNR+1; i <= left_lines; i++)
printf "%-*s %s %s\n", max_length, left[FNR], sep, no_line_txt
}
}
' left2.txt right2.txt
让我们看看上面的命令会产生什么输出:
I'm line1. <- = -> Line1 on the right
I am line2, I am very very very long. <- = -> Line2 on the right
<- = -> Line3 on the right
Hi, I'm line4. <- = -> Line4 on the right
[ File Ended. No More Lines ] <- = -> Line5 on the right
[ File Ended. No More Lines ] <- = -> Line6 on the right
好的!输出正是我们所期望的。
接下来,让我们了解 awk命令是如何完成这项工作的。
5.2. awk命令如何工作?
现在,让我们浏览一下 awk代码并了解它是如何工作的:
awk -v sep='<- = ->' -v no_line_txt='[ File Ended. No More Lines ]' '
在这里,我们声明了两个 awk变量 sep和no_line_txt 来存储自定义分隔符和不存在行的提示。
接下来,我们将处理第一个文件:
NR==FNR { max_length = (length > max_length)? length : max_length
left_lines = FNR
left[FNR] = $0
next
}
首先,我们遍历第一个文件并找出最长行的长度 ( max_length ) 和行数 ( left_lines )。此外,我们将所有行存储在关联数组中:left[lineNumber]=The line’s text。
然后, awk开始读取第二个输入文件:
{ printf "%-*s %s %s\n", max_length, (FNR in left? left[FNR] : no_line_txt), sep, $0 }
由于我们知道max_length,我们可以使用printf操作动态控制填充以对齐第二个文件中的行。
一旦第二个文件的FNR不在数组left[]中,则意味着第二个文件的行数比第一个文件多。对于那些额外的行,我们在左侧打印no_line_txt的值。
当然,我们通过sep变量将左右两边分开 。
END { if (FNR < left_lines) {
for (i=FNR+1; i <= left_lines; i++)
printf "%-*s %s %s\n", max_length, left[FNR], sep, no_line_txt
}
}
在我们的示例中,第二个文件比第一个输入文件包含更多行。然而,对于不同的输入文件,情况可能相反。
因此,在END块中,我们需要检查第二个文件的行数是否较少。如果是这种情况,我们应该在右侧为缺失的行打印no_line_txt 。
最后,我们需要向awk命令提供两个输入文件:
' left2.txt right2.txt
第一个输入中的行将列在左侧,而第二个文件的内容将列在右侧。