Script shell pour searchr des files pour des inputs de text identiques

Voici un joli teaser pour un gourou script shell:

  1. Prenez un directory avec plusieurs files text. Peut être un peu jusqu'à ~ 1000.
  2. Tous les files contiennent un identifiant sur une ligne donnée (toujours la même ligne).
  3. Identifiez les files dont l'identifiant n'est PAS UNIQUE, c'est-à-dire dupliqués dans d'autres files du directory.
  4. Produire ou save la list des duplicates

Ceci est nécessaire pour un nettoyage administratif de routine des files générés par le système qui DEVRAIENT être uniques, mais par erreur de l'user peut ne pas être.

Basé sur vos commentaires ci-dessus, et après avoir noté que mes données de test sont très similaires à vos données réelles, j'ai pu vérifier cela fonctionne:

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..*:||' 

Je grep le dossier pour les lignes commençant par ID et le rest, et parce qu'il trouve plusieurs files correspondants et j'ai demandé la ligne correspondante -n umbers grep imprime:

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

Je passe cela à sed qui enregistre la copy de la ligne dans l'ancien buffer puis vérifie la string et :6:ID et si elle est trouvée, supprime tout sur la ligne jusqu'à ID . Ensuite, je publie les résultats.

Ensuite, je récupère le tampon – écrasant mes dernières modifications dans le process – et échange les locations sur la ligne de correspondance de grep et son nom de file correspondant. Donc pour chaque ligne grep imprime d'une ligne 6, sed remplace par:

 [IDmatch] [IDmatch]:[filename] 

Lorsque ces données sont passées à sort elle organise l'set par ID et parce que je ne lui request que des résultats uniques, elle supprime tous sauf un pour les IDmatch uniquement les lignes mais conserve les IDmatch:filename suivantes IDmatch:filename lines. La prochaine déclaration sed nettoie tout simplement, en rendant ceci:

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

Comme ceci à la place:

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

Mais cette solution se brisera si un nom de file contient un caractère \n ewline, bien que ce qui suit ne le sera pas. Et j'ai travaillé sur la façon de mettre ce qui suit dans une fonction shell afin qu'il ne soit pas nécessaire de le faire deux fois sur le globe – je le collerai ici bientôt.

 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 

Cela devrait le faire – aussi longtime que vous remplacez le 5 dans l'instruction sed pour les lignes sur lesquelles vos identifiants sont activés. Je pense – et si je me trompe, faites le moi savoir – cela gère tous les cas autrement.

Pour chaque file du directory, il incrémente un nombre de un et imprime une ligne commençant par la string …

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

… où [num] est un entier réel qu'il vient d'incrémenter de 1 et ... est votre ligne d'identifiant unique.

Il dirige ensuite d'abord ces lignes vers le sort qui traite le caractère <space> tant que délimiteur et ne sortinge que datatables du troisième champ. Le |pipeline continue à côté de uniq qui délimite également sur <space> et ignore les deux premiers champs d'input tout en comparant ses inputs et en imprimant uniquement des lignes en double. La partie suivante est un peu bizarre.

Donc, plutôt que d'avoir à boucler tout le path à nouveau et de find quel file est, j'ai fait la chose [num] comme mentionné. Lorsque le process sh shell à la fin du |pipeline est passé les résultats, il ne reçoit que ces numbers. Mais il a déjà positionné ses parameters de position sur le même glob que nous étions en train d'itérer tout en incrémentant ces nombres – donc quand il évalue ces nombres, il les associera aux files déjà dans son tableau positionnel. C'est tout ce qu'il fait.

En fait, il le fait à peine. Chaque paramètre de position est précédé de la command : null. La seule chose que ce process shell est d'évaluer les variables passées à elle – il n'exécute jamais une seule ligne de code. Mais je l'ai mis en-mode de debugging -x et redirigé son stderr en stdout afin qu'il imprime tous les noms de files.

Je le fais de cette façon parce que c'est beaucoup plus facile que de se soucier de noms de files bizarres qui brisent le sort | uniq résultats sort | uniq . Et ça marche très bien.

J'ai testé ceci avec un jeu de données généré de la manière suivante:

 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* 

Veuillez noter la string rm ci-dessus. Je devenais un peu endormi et je ne me suis pas vraiment soucié de savoir pourquoi file89 était généré avec seulement 102 octets et non pas 110 octets comme le rest, alors j'ai arrondi dans les années 90 et ensuite je l'ai fait. Exécuter les noms de file rm correspondants à ce glob dans le directory courant et écraser les files de file00file89 , mais lorsqu'il est utilisé dans un directory de test délégué, il est parfaitement sûr.

… entre autres … Et cela a fonctionné pour tous.

Cela écrit 90 files nommés file[0-8][1-9] chacun avec 1-4,6-10 lignes de 10 octets de données randoms et un ID unique sur la ligne 5 dans chaque file. Il produit également le file[0-8]0 dans lequel les lignes 5 sont toujours ID00000000 .

La sortie de la petite fonction en haut de l'set de données ressemble à ceci:

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

Si, pour une raison quelconque, vous n'aimez pas les symboles + de la sortie, changez simplement $PS4 pour ce dernier process. Vous ajoutez ceci au début de la dernière ligne pour gérer cela:

 PS4= sh ... 

Mais vous pouvez également définir cela à n'importe quelle string – ou même exécutable bit de script shell si vous le souhaitez, et il séparera les noms de files comme vous le souhaitez. Fondamentalement, vous pouvez utiliser l'invite comme un délimiteur automatique comme vous le feriez. Et ce dernier process de shell a toujours les noms de files dans son tableau – vous pouvez append des commands pour manipuler datatables selon vos preferences.

En supposant que les noms de files n'ont pas d'espaces ou de returns à la ligne et qu'un GNU uniq supportant l'option -D est disponible, c'est très simple (changez le numéro après FNR== pour changer la ligne de l'identifiant):

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

Sans l'option -D pour uniq , les choses deviennent rapidement plus compliquées, l'une des façons est d'inverser la sortie de uniq -u utilisant 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 

Pour faire ceci pour les files avec n'importe quel nom, perl est probablement la meilleure option (changez le numéro après $.== sur la ligne 1 pour changer la ligne de l'identifiant):

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

L'idée est d'indexer chaque nom de file par l'identifiant trouvé dans le file afin que chaque identifiant puisse être utilisé pour extraire un tableau de noms de files. De cette façon, il est facile d'imprimer chacun de ces arrays qui ont plus d'un élément.

Mettre à jour

Il est en fait possible d'utiliser la même approche que ci-dessus dans 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 } } } } }' * 

Le seul problème est si la valeur de SUBSEP apparaît dans l'un des identificateurs. Habituellement SUBSEP est un caractère non imprimable ( 0x1c ), donc cela ne posera pas de problème dans la plupart des files text. Il peut être modifié au besoin ou l'exemple peut être adapté à de véritables arrays multidimensionnels (par exemple array[x][y] au lieu de array[x,y] ) dans un awk qui les supporte comme gawk .

Je pourrais vous donner quelque chose de plus spécifique si vous expliquez votre format mais pour des raisons d'argumentation, supposons que votre identifiant soit le 1er mot séparé sur la 3ème ligne de chaque file. Si oui, vous pourriez faire:

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

Explication

  • for f in *; do printf "%s\t%s\n" "$f" $(awk 'NR==3{print $1}' "$f"); done for f in *; do printf "%s\t%s\n" "$f" $(awk 'NR==3{print $1}' "$f"); done : il parcourt tous les files (et les sous-directorys, le cas échéant) du directory courant et affiche le nom du file, un onglet ( \t ) et le 1er champ de sa 3ème ligne (command awk ).

  • perl -F"\t" -lane : L'indicateur -a fait perl agir comme awk , divisant automatiquement la ligne d'input en champs sur le caractère donné par -F et sauvegardant ces champs dans le tableau @F . Le -l supprime les nouvelles lignes de fin de chaque ligne d'input et ajoute un à chaque appel d' print et le -e est le script qui doit être exécuté.

  • $k{$F[1]}{$F[0]}++ : Ceci enregistre les paires nom de file / identificateur dans un hachage de hachage où l'identifiant est la key du premier hachage et le nom de file est la key du second. La structure résultante ressemblerait à ceci:

     $k{identifier1}{filename1} $k{identifier1}{filename2} $k{identifier1}{filenameN} 
  • Le bloc END{} sera exécuté après la lecture complète de l'input.

  • La boucle foreach parcourt chaque key du hash %k (les noms de file) et imprime l'identifiant ( $_ , la key) et la list des keys du subhash ( keys(%{$k{$_}} ).

J'ai testé sur un set de files créés par cette command:

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

Ce qui précède crée 5 paires de files (file1 / file1d through file5 / file5d) avec la même 3ème ligne. L'exécution de la command ci-dessus sur ces files produit:

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