有时候删除文件里的重复行是一个很常见的需求,这个用 shell 命令有很多处理方法。
第一种方案是用 sort 命令的 -u 参数:
$ sort -u input.txt > output.txt
第二种方案是用 awk 命令,它的关键在于用一个字典来保存记录:
$ awk '!seen[$0]++' input.txt > output.txt
这和第一种方案的区别在于,即使文件中重复行不连续,依然可以删除。
第三种方案是用 sed 命令,但是其实不大推荐,它相比第一种方案复杂多,而且很容易写错:
$ sort -n input.txt | sed '$!N;/^\(.*\)\n\1$/!P;D' > output.txt
这种方案里面用到了 sed 的高级命令(N、P、D),相关介绍可以参考之前写的文章 Sed and awk 笔记之 sed 篇:高级命令(一)。
这里有一个小技巧,如果遇到比较复杂的 sed 命令拿捏不准的化,可以用 sedsed 这个脚本来调试执行,看每一步执行的情况。
使用方法也很简单,比如调试上面这个 sed 命令,命令执行结果会展示每一步执行后模式空间和保持空间的内容,一目了然:
$ sort -n input.txt | python sedsed.py -d '$!N;/^\(.*\)\n\1$/!P;D'
关于 sed 命令的解释补充在这里:
1、sed读取的内容是保存到模式空间的
2、sed的命令有小写和大写两套,小写处理的是单行内容,大写命令处理的是多行内容;
2.1、比如n读取下一行到模式空间(覆盖原有内容),N是读取下一行并追加到模式空间,多行用 \n 拼接;
2.2、N命令上面解释了,P命令是打印模式空间的第一行(小写p是打印整个模式空间,结合2理解下),D是删除模式空间的第一行并且如果此时模式空间不为空,不会导致读取新一行
3、理解了上面几点后,就可以开始分析 sed '$!N;/^\(.*\)\n\1$/!P;D' 这个命令了
3.1、$!N 如果不是文件的最后一行,则读取下一行追加到模式空间
3.2、/^\(.*\)\n\1$/!P 如果模式空间的内容(文件连续的两行)不是重复的,则输出第一行(反过来看,意味着如果是重复的就不输出了)
3.3、D 删除模式空间的第一行,然后继续跳到 3.1 执行(结合 2.2 理解,D 命令此时不会导致读取新行)
举个例子,假设输入的 文件内容如下:
bugfix
develop
master
release
release
test
1、读取 bugfix 到模式空间(这一次的读取是 sed 默认的读取,一行一行的处理,后面的读取都是 $!N 命令执行的结果)
2、bugfix 不是最后一行,读取下一行 develop 并追加到模式空间,此时内容为“bugfix\ndevelop“
3、不匹配 /^\(.*\)\n\1$/,打印模式空间第一行 bugfix,并且删除第一行,此时模式空间内容为 ”develop“
4、develop 不是最后一行,读取下一行 master 并追加到模式空间,此时内容为”develop\nmaster“
5、不匹配 /^\(.*\)\n\1$/,打印模式空间第一行 develop,并且删除第一行,此时模式空间内容为 ”master“
6、master 不是最后一行,读取下一行 release 并追加到模式空间,此时内容为” master\nrelease“
7、不匹配 /^\(.*\)\n\1$/,打印模式空间第一行 master,并且删除第一行,此时模式空间内容为 ”release“
8、release 不是最后一行,读取下一行 release 并追加到模式空间,此时内容为” release\nrelease“
9、匹配 /^\(.*\)\n\1$/,不打印,并且删除第一行,此时模式空间内容为 ”release“
10、release 不是最后一行,读取下一行 test 并追加到模式空间,此时内容为” release\ntest“
11、不匹配 /^\(.*\)\n\1$/,打印模式空间第一行 release,并且删除第一行,此时模式空间内容为 ”test“
12、test 是最后一行,不再读取新内容,此时模式空间内容为 ”test“
13、不匹配 /^\(.*\)\n\1$/,打印模式空间第一行 test,并且删除第一行,此时模式空间内容为空
14、所有行处理完毕,命令结束
最后输出内容为:
bugfix
develop
master
release
test