bash trouver des lignes commençant par une chaîne


10

J'ai un tas de fichiers et je veux trouver celui qui contient des lignes séquentielles commençant par une certaine chaîne.

Par exemple pour le fichier suivant:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Cyyyyyyyyy
Czzzzzzzzz
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd
Ceeeeee

Il y a plus d'une ligne commençant par «C», donc je veux que ce fichier soit trouvé par commande.
Par exemple pour le fichier suivant:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Il y a toujours une ligne commençant par «C», je ne veux pas de ce fichier. J'ai pensé à utiliser un grepou un sedmais je ne sais pas exactement comment le faire. Peut-être en utilisant une expression rationnelle ^C.*$^Cou quelque chose comme ça. Une idée ?


Il y a deux lignes commençant par Cdans votre deuxième exemple.
cuonglm

5
Cette question n'est pas claire. Recherchez-vous des fichiers qui ont plus d'une ligne consécutive commençant par C?
Graeme

Oui c'est ce que je veux. Désolé pour le malentendu.
Jérémie

2
@terdon, il semble que les recherches sur plusieurs lignes avec -P aient fonctionné jusqu'à la version 2.5.4 et plus maintenant, bien que je ne trouve rien dans le journal des modifications qui expliquerait pourquoi.
Stéphane Chazelas

1
@Graeme, vous voudrez peut-être annuler la suppression de votre réponse, voir le commentaire de Stéphane, apparemment, cela fonctionne pour certaines anciennes grepversions.
terdon

Réponses:


5

Avec pcregrep:

pcregrep -rMl '^C.*\nC' .

POSIX:

find . -type f -exec awk '
  FNR==1 {last=0; printed=0; next}
  printed {next}
  /^C/ {if (last) {print FILENAME; printed=1; nextfile} else last=1; next}
  {last=0}' {} +

(bien que cela signifie lire tous les fichiers entièrement avec les awkimplémentations qui ne prennent pas en charge nextfile).


Avec les versions de GNU grepjusqu'à 2.5.4:

grep -rlP '^C.*\nC' .

semble fonctionner, mais c'est par accident et il n'est pas garanti de fonctionner.

Avant qu'il ne soit corrigé en 2.6 (par ce commit ), GNU grepavait ignoré que la fonction de recherche pcre qu'il utilisait correspondrait à tout le tampon actuellement traité par grep, provoquant toutes sortes de comportements surprenants. Par exemple:

grep -P 'a\s*b'

correspondrait à un fichier contenant:

bla
bla

Cela correspondrait à:

printf '1\n2\n' | grep -P '1\n2'

Mais ça:

(printf '1\n'; sleep 1; printf '2\n') | grep -P '1\n2'

Ou:

(yes | head -c 32766; printf '1\n2\n') > file; grep -P '1\n2' file

ne le ferait pas (comme l' 1\n2\nest dans deux tampons traités par grep).

Ce comportement a finalement été documenté:

15- Comment puis-je faire correspondre les lignes?

Le grep standard ne peut pas faire cela, car il est fondamentalement basé sur les lignes. Par conséquent, le simple fait d'utiliser la classe de caractères '[: space:]' ne correspond pas aux sauts de ligne comme vous vous en doutez. Cependant, si votre grep est compilé avec des modèles Perl activés, le modificateur Perl (qui fait correspondre les sauts de ligne ".") Peut être utilisé:

     printf 'foo\nbar\n' | grep -P '(?s)foo.*?bar'

Après avoir été corrigé en 2.6, la documentation n'a pas été modifiée (je l'ai signalé une fois là- bas ).


Y a-t-il une raison de ne pas utiliser exitet -exec \;au lieu du fichier suivant?
terdon

@terdon, cela signifierait en exécuter un awkpar fichier. Vous ne souhaitez le faire que si votre awkne prend pas en charge nextfileet que vous avez une grande proportion de fichiers qui sont volumineux et ont des lignes correspondantes vers le début du fichier.
Stéphane Chazelas

Que diriez-vous de cette technique grep (je suppose avec les versions plus récentes de GNU grep) qui facilite les correspondances multilignes en faisant ressembler le fichier entier à une seule chaîne en définissant le terminateur de ligne sur NUL - seriez-vous au courant s'il y a des limitations?
iruvar

1
@ 1_CR, cela chargerait le fichier entier en mémoire s'il n'y a pas de caractère NUL dedans et cela suppose que les lignes ne contiennent pas de caractères NUL. Notez également que les anciennes versions de GNU grep (que l'OP a) ne peuvent pas être utilisées -zavec -P. Il n'y a pas de \Nsans -P, vous auriez besoin de l'écrire $'[\01-\011\013-\0377]'qui ne fonctionnerait que dans les locales C (voir thread.gmane.org/gmane.comp.gnu.grep.bugs/5187 )
Stéphane Chazelas

@StephaneChazelas, détail très utile, merci
iruvar

2

Avec awk:

awk '{if (p ~ /^C/ && $1 ~ /^C/) print; p=$1}' afile.txt

Cela imprimera le contenu du fichier s'il y a des lignes consécutives commençant par a C. L'expression (p ~ /^C/ && $1 ~ /^C/)examinera les lignes successives du fichier et évaluera la valeur true si le premier caractère des deux correspond C. Si tel est le cas, la ligne sera imprimée.

Afin de trouver tous les fichiers qui ont un tel modèle, vous pouvez exécuter l'awk ci-dessus via une findcommande:

find /your/path -type f -exec awk '{if (p ~ /^C/ && $1 ~ /^C/) {print FILENAME; exit;} p=$1}' {} \;

Dans cette commande, le find+ execpassera par chacun des fichiers et effectuera un awkfiltrage similaire sur chaque fichier et affichera son nom via FILENAMEsi l'expression awk est évaluée à true. Afin d'éviter d'imprimer FILENAMEplusieurs fois pour un seul fichier avec plusieurs correspondances, l' exitinstruction est utilisée (merci @terdon).


Ma question n'était pas assez claire, je veux connaître le nom des fichiers avec plus d'une ligne consécutive commençant parC
Jérémie

@ Jérémie J'ai mis à jour ma réponse.
mkc

Pourriez-vous s'il vous plaît ajouter une explication de la façon dont cela fonctionne? De plus, il n'est pas nécessaire de le faire flag, juste à la exitplace. De cette façon, vous n'avez pas besoin de continuer à traiter les fichiers une fois qu'une correspondance a été trouvée.
terdon

2

Encore une autre option avec GNU sed:

Pour un seul fichier:

sed -n -- '/^C/{n;/^C/q 1}' "$file" || printf '%s\n' "$file"

(bien qu'il signale également les fichiers qu'il ne peut pas lire).

Pour find:

find . -type f ! -exec sed -n '/^C/{n;/^C/q 1}' {} \; -print

Le problème avec les fichiers illisibles en cours d'impression peut être évité en l'écrivant:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print

Pouvez-vous détailler le sed -n '$q1;/^C/{n;/^C/q}'?
Jérémie

Quelqu'un pour m'expliquer?
Jérémie

@ Jérémie $q1- force sed à quitter avec une erreur si le motif n'est pas trouvé. Il se terminera également par une erreur si quelque chose ne va pas avec le fichier (il est illisible ou cassé). Il quittera donc avec l'état de sortie 0 uniquement si un motif est trouvé et il sera transmis à l'impression. Se /^C/{n;/^C/qséparer est assez simple. S'il trouve une chaîne qui commence par C, il lira la ligne suivante et s'il commence également par C, il quittera avec un état de sortie nul.
rush

1

En supposant que vos fichiers sont suffisamment petits pour être lus en mémoire:

perl -000ne 'print "$ARGV\n" if /^C[^\n]*\nC/sm' *

Explication:

  • - 000: défini \n\ncomme séparateur d'enregistrement, ceci active le mode paragraphe qui traitera les paragraphes (séparés par des retours à la ligne consécutifs) comme des lignes simples.
  • -ne: applique le script donné en argument à -echaque ligne du ou des fichiers d'entrée.
  • $ARGV : le fichier est-il en cours de traitement
  • /^C[^\n]*\nC/: correspond Cau début d'une ligne (voir la description des smmodificateurs ci-dessous pour savoir pourquoi cela fonctionne ici) suivi de 0 ou plusieurs caractères non-nouvelle ligne, une nouvelle ligne et puis un autre C. En d'autres termes, recherchez des lignes consécutives commençant par C. * //sm: ces modificateurs de correspondance sont (comme documenté [ici]):

    • m : traite la chaîne comme plusieurs lignes. Autrement dit, changez "^" et "$" pour ne faire correspondre le début ou la fin de la ligne qu'aux extrémités gauche et droite de la chaîne pour les faire correspondre n'importe où dans la chaîne.

    • s : traite la chaîne comme une seule ligne. Autrement dit, changez "." pour correspondre à n'importe quel caractère, même à une nouvelle ligne, qui normalement ne correspondrait pas.

Vous pouvez également faire quelque chose de laid comme:

for f in *; do perl -pe 's/\n/%%/' "$f" | grep -q 'C[^%]*%%C' && echo "$f"; done

Ici, le perlcode remplace les sauts de ligne par %%ainsi, en supposant que vous n'en avez pas %%dans votre fichier d'entrée (gros si bien sûr), le grepcorrespondra aux lignes consécutives commençant par C.


1

SOLUTION:

( set -- *files ; for f ; do (
set -- $(printf %c\  `cat <$f`)
while [ $# -ge 1 ] ;do [ -z "${1#"$2"}" ] && {
    echo "$f"; break ; } || shift
done ) ; done )

DÉMO:

Tout d'abord, nous allons créer une base de test:

abc="a b c d e f g h i j k l m n o p q r s t u v w x y z" 
for l in $abc ; do { i=$((i+1)) h= c= ;
    [ $((i%3)) -eq 0 ] && c="$l" h="${abc%"$l"*}"
    line="$(printf '%s ' $h $c ${abc#"$h"})"
    printf "%s$(printf %s $line)\n" $line >|/tmp/file${i}
} ; done

Ce qui précède crée 26 fichiers dans /tmpnamed file1-26. Dans chaque fichier, il y a 27 ou 28 lignes commençant par les lettres a-zet suivies du reste de l'alphabet. Chaque troisième fichier contient deux lignes consécutives dans lesquelles le premier caractère est dupliqué.

ÉCHANTILLON:

cat /tmp/file12
...
aabcdefghijkllmnopqrstuvwxyz
babcdefghijkllmnopqrstuvwxyz
cabcdefghijkllmnopqrstuvwxyz
...
kabcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
mabcdefghijkllmnopqrstuvwxyz
...

Et quand je change:

set -- *files

à:

set -- /tmp/file[0-9]*

Je reçois...

PRODUCTION:

/tmp/file12
/tmp/file15
/tmp/file18
/tmp/file21
/tmp/file24
/tmp/file3
/tmp/file6
/tmp/file9

Donc, en bref, la solution fonctionne comme ceci:

sets sous-shell positionne à tous vos fichiers, et pour chaque

sets positionne un sous-shell imbriqué à la première lettre de chaque ligne de chaque fichier lors de sa boucle.

[ tests ]si $1nie $2indiquant une correspondance, et si oui

echoesle nom du fichier , puis breakest l'itération de boucle de courant

sinon shifts au prochain caractère positionnel pour réessayer


0

Ce script utilise grepet cutpour obtenir les numéros de ligne des lignes correspondantes et recherche deux numéros consécutifs. Le fichier est supposé un nom de fichier valide passé comme premier argument au script:

#!/bin/bash

checkfile () {
 echo checking $1
 grep -n -E "^C.*$" $1 | cut -d: -f1 | while read linenum
     do
        : $[ ++PRV ] 
        if [ $linenum == $PRV ]; then return 1; fi
        PRV=$linenum
     done
     return 0
}

PRV="-1"
checkfile $1
if [ $? == 0 ]; then
   echo Consecutive matching lines found in file $1
fi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.