Script de shell para procurar arquivos por entradas de texto idênticas

6

Aqui está um bom teaser para um guru de script de shell:

  1. Pegue um diretório com vários arquivos de texto. Pode ser alguns até ~ 1000.
  2. Todos os arquivos contêm um identificador em uma determinada linha (sempre a mesma linha).
  3. Identifique quais arquivos têm identificador que NÃO é ÚNICO, ou seja, duplicado em outro (s) arquivo (s) no diretório.
  4. Envie ou salve a lista de duplicatas

Isto é necessário para uma rotina de administração 'limpeza' de arquivos gerados pelo sistema que deve ser único, mas através do erro do usuário pode não ser.

    
por Unix-admin-ireland 11.06.2014 / 00:43

3 respostas

8

Com base nos seus comentários acima e tendo observado que meus dados de teste são muito semelhantes aos seus dados reais, pude verificar se isso funciona:

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

Eu grep da pasta para as linhas que começam com ID e o restante, e porque ela encontra vários arquivos correspondentes e pedi a linha correspondente -n umbers grep prints:

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

Eu passo isso para sed , que salva a cópia da linha no buffer h old, em seguida, verifica a string :6:ID e, se encontrada, exclui tudo na linha até ID . Então eu p rint os resultados.

Depois disso, eu g et recupero o buffer - sobrescrevendo minhas últimas edições no processo - e troco os locais na linha da correspondência de grep e seu nome de arquivo correspondente. Assim, para cada linha grep impressões de uma linha 6 correspondem sed substitui por:

[IDmatch]
[IDmatch]:[filename]

Quando esses dados são passados para sort , ele organiza todo o conjunto por ID e, como só peço -u nique resultados, ele exclui todos, exceto um, para linhas IDmatch repetidas, mas retém as seguintes IDmatch:filename linhas. A próxima instrução sed apenas a limpa, tornando isso:

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

Assim, ao invés disso:

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

Mas essa solução será quebrada se um nome de arquivo contiver um caractere \n ewline, embora o seguinte não seja. E eu trabalhei como colocar o seguinte em uma função de shell para que ele não precise ser copiado duas vezes - vou colá-lo aqui em breve.

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

Isso deve ser feito - desde que você substitua a 5 na instrução sed para quaisquer linhas em que seus ids estejam. Eu acho - e se eu estiver errado, deixe-me saber - isso lida com todos os casos de outra forma.

Para cada arquivo no diretório, ele incrementa um número por um e imprime uma linha que começa com a string ...

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

... onde [num] é um número inteiro real que acaba de ser incrementado em 1 e ... é sua linha de identificação exclusiva.

Em seguida, ele canaliza essas linhas primeiro para sort , que trata o caractere <space> como um delimitador e classifica apenas os dados do terceiro campo em. O |pipeline continua ao lado de uniq , que também delimita <space> e ignora os dois primeiros campos de entrada ao comparar sua entrada e imprimir apenas -D linhas duplicadas. A próxima parte é um pouco estranha.

Então, em vez de ter que percorrer todo o caminho novamente e descobrir qual arquivo é qual, eu fiz o [num] thing como mencionado. Quando o processo de sh shell no final do |pipeline é passado resultados recebe apenas esses números. Mas já definiu seus parâmetros posicionais para o mesmo glob que estávamos interagindo enquanto incrementamos esses números - então, quando ele avalia esses números, ele os associará aos arquivos já em sua matriz posicional. Isso é tudo que faz.

Na verdade - quase nem faz isso. Cada parâmetro posicional é precedido pelo comando : null. A única coisa que o processo do shell faz é avaliar as variáveis passadas para ele - ele nunca executa uma única linha de código. Mas eu o configurei para o modo de depuração -x e redirecionei seu stderr para stdout para que ele imprima todos os nomes de arquivos.

Eu faço assim porque é muito mais fácil do que me preocupar com nomes estranhos de arquivos quebrando os resultados sort | uniq . E isso funciona muito bem.

Eu testei isso com um conjunto de dados gerado da seguinte maneira:

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*

Anote a string rm acima. Eu estava ficando um pouco sonolenta e realmente não me importei em descobrir por que file89 estava sendo gerado com apenas 102bytes e não com 110bytes como o resto, então eu cheguei aos 90s e então rm d. Executando o acima, os nomes dos arquivos serão combinados com o glob no diretório atual e sobrescreverão quaisquer arquivos de file00 - file89 , mas quando usados em um diretório de teste delegado é perfeitamente seguro.

... entre outros ... E funcionou para todos.

Que grava 90 arquivos denominados file[0-8][1-9] , cada um com 1-4,6-10 linhas de 10 bytes de dados aleatórios e um ID exclusivo na linha 5 em cada arquivo. Também produz file[0-8]0 , em que as linhas 5 são sempre ID00000000 .

A saída da pequena função no topo executado neste conjunto de dados se parece com:

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

Se, por qualquer motivo, você não gostar dos símbolos + na saída, apenas altere $PS4 para o último processo de shell. Você adiciona isso no início da última linha para lidar com isso:

PS4= sh ...

Mas você poderia, alternativamente, configurá-lo para qualquer string - ou até mesmo um bit executável de script de shell, se quiser, e ele irá separar os nomes dos arquivos como quiser. Basicamente, você pode usar o prompt como um delimitador automático. E esse último processo de shell ainda tem os nomes de arquivos em sua matriz - você pode adicionar comandos para manipular os dados de acordo com sua preferência.

    
por 11.06.2014 / 03:27
6

Supondo que os nomes dos arquivos não tenham espaços ou linhas novas e que uma opção uniq do GNU -D esteja disponível, isso é realmente fácil (altere o número após FNR== para alterar a linha do identificador): / p>

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

Sem a opção -D para uniq , as coisas ficam mais complicadas rapidamente, uma maneira é inverter a saída de uniq -u usando comm :

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

Para fazer isso com arquivos com qualquer nome, perl é provavelmente a melhor opção (altere o número após $.== na linha 1 para alterar a linha do identificador):

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

A idéia é indexar cada nome de arquivo pelo identificador encontrado no arquivo para que cada identificador possa ser usado para buscar uma matriz de nomes de arquivos. Dessa forma, é fácil imprimir cada um desses arrays que possuem mais de um elemento.

Atualizar

Na verdade, é possível usar a mesma abordagem acima em 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
          }
        }
      }
    }
  }' *

O único problema é se o valor de SUBSEP aparecer em qualquer um dos identificadores. Geralmente, SUBSEP é um caractere não imprimível ( 0x1c ), portanto, isso não será um problema na maioria dos arquivos de texto. Ele pode ser alterado conforme necessário ou o exemplo pode ser adaptado para arrays multidimensionais reais (por exemplo, array[x][y] em vez de array[x,y] ) em um awk que os suporta como gawk .

    
por 11.06.2014 / 14:31
4

Eu poderia lhe dar algo mais específico se você explicar o seu formato, mas por causa do argumento, vamos assumir que o seu identificador é a primeira palavra separada por espaço na terceira linha de cada arquivo. Se assim for, você poderia fazer:

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 }
  }'

Explicação

  • for f in *; do printf "%s\t%s\n" "$f" $(awk 'NR==3{print $1}' "$f"); done : passa por todos os arquivos (e subdiretórios, se houver) no diretório atual e imprime o nome do arquivo, uma guia ( \t ) e o primeiro campo de sua terceira linha ( o comando awk ).

  • perl -F"\t" -lane : O -a sinalizador faz perl agir como awk , dividindo automaticamente a linha de entrada em campos do caractere dado por -F e salvando esses campos no array @F . O -l remove as novas linhas finais de cada linha de entrada e adiciona uma a cada print e o -e é o script que deve ser executado.

  • $k{$F[1]}{$F[0]}++ : Isso salva os pares de nome de arquivo / identificador em um hash de hashes onde o identificador é a chave do primeiro hash e o nome do arquivo a chave do segundo. A estrutura resultante ficaria assim:

    $k{identifier1}{filename1}
    $k{identifier1}{filename2}
    $k{identifier1}{filenameN}
    
  • O bloco END{} será executado depois que toda a entrada tiver sido lida.

  • O loop foreach passa por cada chave do hash %k (os nomes dos arquivos) e imprime o identificador ( $_ , a chave) e a lista de chaves da subhash ( keys(%{$k{$_}} ).

Eu testei em um conjunto de arquivos criados por este comando:

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

O código acima cria 5 pares de arquivos (arquivo1 / arquivo1d por meio de arquivo5 / arquivo5d) com a mesma terceira linha. Executar o comando acima nesses arquivos produz:

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