用于搜索文件中相同文本条目的 Shell 脚本

用于搜索文件中相同文本条目的 Shell 脚本

我需要写一个脚本:

  1. 获取一个包含多个文本文件的目录。可以是几个,最多可达 1000 个。
  2. 所有文件都在给定行(始终是同一行)上包含标识符。
  3. 识别哪些文件具有不唯一的标识符,即在目录中的其他文件中重复。
  4. 输出或保存重复项列表

这是系统生成文件的例行管理“清理”所必需的,这些文件应该是唯一的,但由于用户错误可能不是唯一的。

答案1

根据您上面的评论,并注意到我的测试数据与您的实际数据非常相似,我能够验证这一点:

grep -n '^ID.[^:-]*.[0-9][0-9]*$' |
sed -n 'h;s|\(.*\):6:\(ID.*\)|\2|p;g;s||\2:\1|p'
sort -u | 
sed 's|ID..*:||'

我的grep文件夹包含以ID和 其余部分开头的行,因为它找到多个匹配文件,并且我要求匹配的行-n编号grep打印:

[filename]:[matching line number]:[IDmatch]

我将其传递给sed在旧缓冲区中保存该行的副本h,然后检查该字符串:6:ID,如果找到,则删除该行上直到ID.然后我p打印结果。

接下来,我g返回缓冲区 - 覆盖我在该过程中的最后编辑 - 并交换 的grepmatch 行及其匹配文件名的位置。因此,对于grep第 6 行匹配的每一行打印,sed将其替换为:

[IDmatch]
[IDmatch]:[filename]

当这些数据传递给sort它时,它会组织整个集合ID,因为我只要求它提供-u独特的结果,它会删除除重复行之外的所有IDmatch行,但保留以下IDmatch:filename行。下一条sed语句只是清理它,呈现如下:

ID00000000
ID00000000:file00
ID00000000:file10
...
ID00000000:file80
ID00000001
ID00000001:file01
ID00000002
ID00000002:file02
...

像这样:

ID00000000
file00
file10
...
file80
ID00000001
file01
ID00000002
file02
...

但那个解决方案将要如果文件名包含\newline 字符,则中断,但以下不会。我弄清楚了如何将以下内容放入 shell 函数中,这样它就不必地球两次 - 我很快就会将其粘贴到这里。

for f in * ; do
    sed '5!d;s|^|: "${'$((i=i+1))'}" |;q' "$f"
done |
sort -t' ' -k3 |
uniq -D -f2 |
sh -cx "$(cat)" -- * 2>&1

应该可以做到这一点 - 只要你用语句5中的 替换sed你的 id 所在的任何行。我认为 - 如果我错了请告诉我 - 这可以处理所有情况。

对于目录中的每个文件,它都会将数字递增 1 并打印以字符串开头的行...

: "${[num]}" ...

...其中[num]是一个实际整数,它刚刚增加了 1,并且...是您的唯一 id 行。

然后,它首先将这些行通过管道输出sort,将<space>字符视为分隔符,并仅对第三个字段中的数据进行排序。|pipeline旁边的 continue还uniq分隔<space>并跳过输入的前两个字段,同时比较其输入并仅打印-D重复行。接下来的部分有点奇怪。

因此,我不必再次循环并找出哪个文件是哪个文件,而是[num]按照前面提到的那样进行了操作。当sh最后的 shell 进程|pipeline传递结果时,它只接收这些数字。但它已经将其位置参数设置为我们在递增这些数字时迭代的相同全局变量 - 因此,当它评估这些数字时,它会将它们与位置数组中已有的文件关联起来。这就是它的全部作用。

事实上——它甚至几乎没有做到这一点。每个位置参数前面都有:null 命令。 shell 进程所做的唯一事情就是评估传递给它的变量 - 它从不执行任何一行代码。但我将其设置为-x调试模式并将其重定向stderrstdout以便它打印所有文件名。

我这样做是因为这比担心奇怪的文件名破坏sort | uniq结果要容易得多。而且效果很好。

我用以下方式生成的数据集对此进行了测试:

tr -dc '[:graph:]' </dev/urandom |
dd ibs=100 cbs=10 conv=unblock count=91 |
split -b110 --filter='
{   c=${FILE##%%*0} ; c=${c#file}
    sed "5cID000000${c:-00}"
} >$FILE' -ed - file ; rm *90*

请注意rm上面的字符串。我有点困了,并不想弄清楚为什么file89只生成了 102 字节而不是其余的 110 字节,所以我四舍五入到 90 年代,然后rm进行了编辑。运行上面的命令将 rm 文件名与当前目录中的 glob 匹配,并覆盖file00-中的任何文件file89,但在委托测试目录中使用时它是完全安全的。

...除其他外...它对所有人都有效。

这将写入 90 个文件,file[0-8][1-9]每个文件命名为 1-4,6-10 10 字节随机数据行,并在每个文件的第 5 行上有一个唯一 ID。它还产生file[0-8]0其中行 5 总是ID00000000

在此数据集上运行的顶部小函数的输出如下所示:

+ : file10 ID00000000
+ : file00 ID00000000
+ : file20 ID00000000
+ : file30 ID00000000
+ : file40 ID00000000
+ : file50 ID00000000
+ : file60 ID00000000
+ : file70 ID00000000
+ : file80 ID00000000

如果出于某种原因您不喜欢+输出中的符号,只需更改$PS4最后一个 shell 进程即可。您可以将其添加到最后一行的开头来处理:

PS4= sh ...

但是您也可以将其设置为任何字符串 - 如果您愿意,甚至可以将其设置为 shell 脚本的可执行位,并且它会根据您的需要分隔文件名。基本上,您可以随意使用提示作为自动分隔符。最后一个 shell 进程的数组中仍然包含文件名 - 您可以根据自己的喜好添加命令来操作数据。

答案2

假设文件名没有空格或换行符并且uniq支持该-D选项的 GNU 可用,这非常简单(更改后面的数字FNR==以更改标识符的行):

awk 'FNR==2 { print FILENAME,$0 }' * | sort -k 2 | uniq -Df 1 | cut -d ' ' -f 1

如果没有 的-D选项,事情很快就会变得更加复杂,一种方法是反转usinguniq的输出:uniq -ucomm

awk 'FNR==2 { print FILENAME,$0 }' * | sort >/tmp/sorted_keys
sort -k 2 /tmp/sorted_keys |
  uniq -uf 1 | sort | comm -23 /tmp/sorted_keys - | cut -d ' ' -f 1

要对任何名称的文件执行此操作,perl可能是最好的选择(更改第 1 行后面的数字$.==以更改标识符行):

perl -ne 'push(@{$table{$_}}, $ARGV) if $.==2;
  $.=0 if eof;
  END {
    for my $val (values %table) {
      print join( "\n", @{$val} ) . "\n" if @{$val} > 1;
    }
  }' *

这个想法是通过在文件中找到的标识符来索引每个文件名,以便每个标识符都可以用于获取文件名数组。这样就可以很容易地打印出每个包含多个元素的数组。

更新

实际上可以使用与上面相同的方法awk

awk 'FNR==2 {
  i=table_sizes[$0]++;
  table[$0,i]=FILENAME
  }
  END {
    for (key in table_sizes) {
      if (table_sizes[key] > 1) {
        for (long_key in table) {
          if ( index(long_key, key SUBSEP) == 1 ) {
            print table[long_key]
            delete table[long_key]  # speed up next search
          }
        }
      }
    }
  }' *

唯一的问题是 的值是否SUBSEP出现在任何标识符中。通常SUBSEP是非打印字符 ( 0x1c),因此这在大多数文本文件中不会成为问题。它可以根据需要进行更改,或者示例可以适用于支持它们的真正的多维数组(例如,array[x][y]而不是array[x,y])。awkgawk

答案3

如果您解释一下您的格式,我可以给您提供更具体的信息,但为了便于论证,我们假设您的标识符是每个文件第三行的第一个空格分隔的单词。如果是这样,你可以这样做:

for f in *; do printf "%s\t%s\n" "$f" $(awk 'NR==3{print $1}' "$f"); done |
 perl -F"\t" -lane '$k{$F[1]}{$F[0]}++; 
  END{
   foreach (keys(%k)){
     print "$_ : ", join ",",keys(%{$k{$_}}) if scalar (keys(%{$k{$_}})) > 0 }
  }'

解释

  • for f in *; do printf "%s\t%s\n" "$f" $(awk 'NR==3{print $1}' "$f"); done:这将遍历当前目录中的所有文件(以及子目录,如果有)并打印文件名、制表符 ( \t) 及其第三行的第一个字段(命令awk)。

  • perl -F"\t" -lane:该-a标志的perl作用类似于awk,自动将输入行拆分为由 给定的字符上的字段-F,并将这些字段保存到@F数组中。从每个输入行中删除-l尾随换行符,并向每个print调用添加一个换行符,这-e是应该运行的脚本。

  • $k{$F[1]}{$F[0]}++:这会将文件名/标识符对保存在哈希值的哈希值中,其中标识符是第一个哈希值的键,文件名是第二个哈希值的键。结果结构如下所示:

    $k{identifier1}{filename1}
    $k{identifier1}{filename2}
    $k{identifier1}{filenameN}
    
  • END{}块将在读取整个输入后执行。

  • foreach循环遍历哈希的每个键%k(文件名)并打印标识符($_,键)和子哈希的键列表(keys(%{$k{$_}})。

我测试了由该命令创建的一组文件:

for i in {1..5}; do echo -e "$RANDOM\nbar\n$i" | tee file$i > file${i}d; done

上面的代码使用相同的第 3 行创建了 5 对文件(file1/file1d 到 file5/file5d)。在这些文件上运行上面的命令会产生:

id2 : file2d,file2
id4 : file4,file4d
id5 : file5d,file5
id1 : file1,file1d
id3 : file3,file3d

相关内容