我有n
数量的纯文本文件,其中包含文本行。
某些行在某些文件之间重复。bash
有没有一种方法可以比较文件并打印出每个文件与其他文件相比有多少唯一行?
例:
# file1
1
2
3
10
# file2
2
10
50
3
# file3
100
2
1
40
6
我基本上正在寻找一种解决方案,该解决方案类似于:$filename:$unique_lines
一个使用grep
,sort
,tr
和uniq
,n>1:
$ grep ^ file[123] | tr : ' ' | sort -k2 | uniq -f 1 -u
file3 100
file3 40
file2 50
file3 6
另一个使用 GNU awk:
$ awk '{
a[$0]++
f[FILENAME][FNR]=$0
}
END {
for(i in f)
for(j in f[i])
if(a[f[i][j]]==1)
print i,f[i][j]
}' file[123]
file2 50
file3 100
file3 40
file3 6
对于任何两个文件,比如file1
和file2
,你可以输出file1
中唯一的行(即file1
中没有出现在file2
中的行),如下所示:
> fgrep -vx -f file2 file1
1
使用file1
、file2
和file3
的其他示例:
> fgrep -vx -f file3 file1 # Show lines in file1 that do not appear in file3
3
10
> fgrep -vx -f file2 file3 # Show lines in file3 that do not appear in file2
100
1
40
6
请注意,在大多数(如果不是全部)系统上,fgrep
实际上只是grep -F
的同义词,其中-F
告诉grep
比较固定字符串而不是尝试匹配正则表达式。因此,如果您由于某种原因没有fgrep
,您应该能够使用grep -Fvx
而不是fgrep -vx
.
由于要比较多个文件,它会变得更加棘手,但对于任何给定的文件,您可以在临时文件中保留一个唯一行的运行列表,然后通过一次将临时文件与其他文件进行比较来减少它:
# Show all lines in file3 that do not exist in file1 or file2
fgrep -vx -f file1 file3 > file3_unique
fgrep -vx -f file2 file3_unique
100
40
6
由于您想要的只是唯一行数的计数,因此您只需将最后一个命令通过管道传输到wc -l
:
> fgrep -vx -f file2 file3_unique | wc -l
3
如果您使用超过 3 个文件执行此操作,您会发现需要使用额外的临时文件。假设您有一个file4
:
> cat file4
1
3
40
6
这意味着您需要第三个fgrep
命令来完成对唯一行列表的缩减。如果你只是这样做,你会遇到一个问题:
# Show all lines in file3 that do not exist in file1, file2, or file4
> fgrep -vx -f file1 file3 > file3_unique
> fgrep -vx -f file2 file3_unique > file3_unique
grep: input file 'file3_unique' is also the output
换句话说,您无法将结果传送回正在grep
-ed 的同一文件。因此,您需要每次输出到单独的临时文件,然后重命名它:
# Show all lines in file3 that do not exist in file1, file2, or file4
> fgrep -vx -f file1 file3 > temp
> mv temp file3_unique
> fgrep -vx -f file2 file3_unique > temp
> mv temp file3_unique
> fgrep -vx -f file4 file3_unique
100
请注意,我在最后一行省略了| wc -l
,只是为了表明它按预期工作。
当然,如果你的文件数量是任意的,你需要循环进行比较:
files=( file* )
for ((i=0; i<${#files[@]}; ++i)); do
cp -f "${files[i]}" unique
for ((j=0; j<${#files[@]}; ++j)); do
if (( j != i )); then
fgrep -vx -f "${files[j]}" unique > temp
mv temp unique
fi
done
echo "${files[i]}:$(wc -l <unique)"
rm unique
done
这将产生输出:
file1:0
file2:1
file3:1
file4:0
如果temp
和unique
是现有文件或目录,则可能需要考虑改用mktemp
。例如:
unique=$(mktemp)
temp=$(mktemp)
fgrep -vx file2 file3 > "$temp"
mv "$temp" "$unique"
这样,实际文件将是类似/tmp/tmp.rFItj3sHVQ
等,并且您不会意外覆盖运行此代码的目录中名为temp
或unique
的任何内容。
更新:只是为了踢,我决定把它缩小一点。首先,我不太喜欢嵌套循环或临时文件。这是一个摆脱两者的版本。这种改进是基于这样的观察:通过连续比较file2
、file3
和file4
来减少file1
,与在file1
和file2
+file3
+file4
的串联之间进行单一比较是一回事。那么诀窍就是弄清楚如何在不循环的情况下连接所有其他文件。但事实证明,您实际上可以在使用数组拼接的 bash 中相当轻松地做到这一点。例如:
files=( file1 file2 file3 file4 )
# Concatenate all files *except* ${files[2]}, i.e., file3
> cat "${files[@]:0:2}" "${files[@]:3}"
1
2
3
10
2
10
50
3
1
3
40
6
将其与之前的解决方案相结合,我们可以用一行替换内部循环和临时文件:
files=(file1 file2 file3 file4)
for ((i=0; i<${#files[@]}; ++i)); do
echo "${files[i]}:$(fgrep -vxc -f <(cat "${files[@]:0:i}" "${files[@]:i+1}") <(sort -u "${files[i]}"))"
done