如果你经常和数据打交道,那么你肯定会经常需要对列进行操作。在 Linux 中,对纯文本文件的列操作有两个十分有用的命令:cut 和 paste。其中 cut 主要用于从纯文本文件中取出某些列,paste 则可以用于按列合并。
cut 命令
假设有这样一个测试文件 cut.txt:
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
我们将用这个测试文件来做一些实验。
cut 基础与字节模式
前面说过,cut 命令的本职工作就是取出某些列。实际上,更准确地说法,是 cut 命令逐行地处理输入,并从中取出某些列。这里说的「列」有三种模式:
-b # 以字节作为标准取出列
-c # 以字符作为标准取出列
-f # 以域 (field) 作为标准取出列
首先我们看看字节模式。比如我们可以取出每一行的第三个字节中的内容。我们知道,英文字符都是以 ASCII 编码用一个字符保存的。这样,我们预期会输出 6 个 2。我们来看下实际的输出。
$ cut -b 3 cut.txt
2
2
2
2
2
2
完美,完全符合预期!
我们来看一下 cut 命令的样式
1
cut -[b,c,f]
在刚才的例子中,我们选择了字节模式(-b),并指定了第三列。值得一提的是,cut 命令的列指定风格非常的灵活。
3 # 第三列
3,5,8 # 第三列、第五列、第八列
3-5,8 # 第三列至第五列、第八列
-3,8 # 第一列至第三列、第八列
1,3- # 第一列、第三列至最后一列
字节模式在某些情况会遇到问题。比如,遇到非 ASCII 编码的字符时(特别是多字节字符),就会遇到问题。我们试着看看用 -b 模式输出第 17 列会怎样。
$ cut -b 17 cut.txt
实际上,-b 模式的第 17 列,会输出「九」的第一个字节。具体输出的内容取决于系统使用的编码。如果我们想输出字符「九」就需要使用字符模式了。
字符模式与域模式
-c 是字符模式。为了输出一列汉字「九」,我们可以这样
$ cut -c 17 cut.txt
九
九
九
九
九
九
除了解析列的方式不一样之外,-c 和 -b 完全一样。
类似的,还有域模式。与字节模式以及字符模式最大的不同是,域模式可以指定单个字符作为分隔符,逐行地将文件分成若干列。比如,这里我们可以用 | 作为分隔符,输出第三列至第五列以及第九列。注意,在列模式下,分隔符也会按需输出。
$ cut -d '|' -f 3-5,9 cut.txt
3|4|5|九
3|4|5|九
3|4|5|九
3|4|5|九
3|4|5|九
3|4|5|九
补集
cut 命令还支持 --complement 参数,意思是取补集。比如在我们刚才的例子中,取补集就意味着取出第一列、第二列、第六列至第八列以及第十列。
$ cut -d '|' -f 3-5,9 --complement cut.txt
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
使用 --complement 参数,我们可以很容易地从纯文本中删除某一列。比如我们想删除第四列
$ cut -d '|' -f 4 --complement cut.txt
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
轻而易举~
一点黑魔法:处理连续空格分割的情况
cut 在处理连续空格分割列的时候,结果就会变得一团糟。不过,好在我们有 tr 命令。使用 -s 参数,可以逐行地将连续的字符 unique 成单独的一个字符。
$ who
Liam :0 2016-11-08 00:07
Liam pts/0 2016-11-08 00:23 (:0.0)
Liam pts/1 2016-11-08 00:15 (:0.0)
$ who | tr -s ' '
Liam :0 2016-11-08 00:07
Liam pts/0 2016-11-08 00:23 (:0.0)
Liam pts/1 2016-11-08 00:15 (:0.0)
这样,我们就能轻易地获得各个用户的登录时间了
$ who | tr -s ' ' | cut -d ' ' -f 1,3,4
Liam 2016-11-08 00:07
Liam 2016-11-08 00:23
Liam 2016-11-08 00:15
paste 命令
基本用法
相比 cut 命令,paste 命令的用法就简单粗暴许多了。
假设我们有三个文件
$ cat paste1.txt | $ cat paste2.txt | $ cat paste3.txt
1 | a | A
2 | b | B
3 | c | C
现在我们用 paste 试试看
$ paste paste1.txt paste2.txt
1 a
2 b
3 c
$ paste paste2.txt paste1.txt
a 1
b 2
c 3
$ paste paste2.txt paste1.txt paste3.txt
a 1 A
b 2 B
c 3 C
$ paste paste2.txt paste1.txt paste3.txt | sed -n l
a\t1\tA
b\t2\tB
c\t3\tC
不难发现,paste 命令支持输入多个文件,并按顺序将他们用制表符粘在一起。如果你想用其他的分隔符将他们粘在一起,也可以像 cut 命令那样使用 -d 参数指定。
$ paste -d '|' paste2.txt paste1.txt paste3.txt
a|1|A
b|2|B
c|3|C
一点黑魔法:避免使用临时文件
如果我们需要将几个程序的即时输出(标准输出)按列粘在一起的话,可能不得不将这些输出先写入临时文件当中,然后再调用 paste 命令。不过,也有不用这样麻烦的办法——使用 Bash Process Substituation 来解决这个问题。
简单来说,就是使用 <(command) 来「伪装成一个文件」的样子,作为 paste 命令的输入。比如
$ paste -d '|' <(cat paste2.txt) <(cat paste1.txt) <(cat paste3.txt)
a|1|A
b|2|B
c|3|C