web-dev-qa-db-fra.com

Lister les fichiers qui contiennent `n` ou moins de lignes

Question

Dans un dossier, j'aimerais imprimer le nom de chaque fichier .txt contenant au maximum des lignes n=27. je pourrais faire

wc -l *.txt | awk '{if ($1 <= 27){print}}'

Le problème est que beaucoup de fichiers dans le dossier sont des millions de lignes (et les lignes sont assez longues) et la commande wc -l *.txt est donc très lente. En principe, un processus peut compter le nombre de lignes jusqu'à trouver au moins des lignes n, puis passer au fichier suivant.

Qu'est-ce qu'une alternative plus rapide?

FYI, je suis sur MAC OSX 10.11.6

Tentative

Voici une tentative avec awk

#!/bin/awk -f

function printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
{
  if (previousNbLines <= n) 
  {
    print previousNbLines": "previousFILENAME
  }
}

BEGIN{
  previousNbLines=n+1
  previousFILENAME=NA
} 


{
  if (FNR==1)
  {
    printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
    previousFILENAME=FILENAME
  }
  previousNbLines=FNR
  if (FNR > n)
  {
    nextfile
  }
}

END{
  printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
}

qui peut être appelé comme

awk -v n=27 -f myAwk.awk *.txt

Cependant, le code ne parvient pas à imprimer des fichiers parfaitement vides. Je ne sais pas comment résoudre ce problème et je ne suis pas sûr que mon script awk soit la solution.

14
Remi.b

Avec GNU awk pour nextfile et ENDFILE:

awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt

Avec n'importe quel awk:

awk -v n=27 '
    { fnrs[FILENAME] = FNR }
    END {
        for (i=1; i<ARGC; i++) {
            filename = ARGV[i]
            if ( fnrs[filename] < n ) {
                print filename
            }
        }
    }
' *.txt

Celles-ci fonctionneront que les fichiers d’entrée soient vides ou non. Les mises en garde pour la version non-gawk sont les mêmes que pour vos autres réponses actuelles à awk:

  1. Il repose sur le même nom de fichier qui n'apparaît pas plusieurs fois (par exemple, awk 'script' foo bar foo) et que vous souhaitez afficher plusieurs fois, et
  2. Il ne repose sur aucune variable définie dans la liste arg (par exemple, awk 'script' foo FS=, bar).

La version gawk n'a pas de telles restrictions.

METTRE À JOUR:

Pour tester le timing entre le script GNU awk ci-dessus et le script GNU grep + sed posté par xhienne puisqu’elle a déclaré que sa solution serait faster than a pure awk script j’ai créé 10 000 fichiers d’entrée, tous 0 à 1000 lignes en utilisant ce script:

$ awk -v numFiles=10000 -v maxLines=1000 'BEGIN{for (i=1;i<=numFiles;i++) {numLines=int(Rand()*(maxLines+1)); out="out_"i".txt"; printf "" > out; for (j=1;j<=numLines; j++) print ("foo" j) > out} }'

et a ensuite exécuté les 2 commandes sur eux et obtenu ces résultats de chronométrage de 3ème exécution:

$ time grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//' > out.grepsed

real    0m1.326s
user    0m0.249s
sys     0m0.654s

$ time awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt > out.awk

real    0m1.092s
user    0m0.343s
sys     0m0.748s

Les deux scripts ont produit les mêmes fichiers de sortie. Ce qui précède a été exécuté en Bash sur Cygwin. Je pense que sur des systèmes différents, les résultats de synchronisation peuvent varier légèrement, mais la différence sera toujours négligeable.


Pour imprimer 10 lignes de 20 caractères aléatoires au maximum par ligne (voir les commentaires):

$ maxChars=20
    LC_ALL=C tr -dc '[:print:]' </dev/urandom |
    fold -w "$maxChars" |
    awk -v maxChars="$maxChars" -v numLines=10 '
        { print substr($0,1,Rand()*(maxChars+1)) }
        NR==numLines { exit }
    '
0J)-8MzO2V\XA/o'qJH
@r5|g<WOP780
^O@bM\
vP{l^pgKUFH9
-6r&]/-6dl}pp W
&.UnTYLoi['2CEtB
Y~wrM3>4{
^F1mc9
?~NHh}a-EEV=O1!y
of

Pour tout faire dans awk (ce qui sera beaucoup plus lent):

$ cat tst.awk
BEGIN {
    for (i=32; i<127; i++) {
        chars[++charsSize] = sprintf("%c",i)
    }
    minChars = 1
    maxChars = 20
    srand()
    for (lineNr=1; lineNr<=10; lineNr++) {
        numChars = int(minChars + Rand() * (maxChars - minChars + 1))
        str = ""
        for (charNr=1; charNr<=numChars; charNr++) {
            charsIdx = int(1 + Rand() * charsSize)
            str = str chars[charsIdx]
        }
        print str
    }
}

$ awk -f tst.awk
Heer H{QQ?qHDv|
Psuq
Ey`-:O2v7[]|N^EJ0
j#@/y>CJ3:=3*b-joG:
?
^|O.[tYlmDo
TjLw
`2Rs=
!('IC
hui
8
Ed Morton

Si vous utilisez GNU grep (malheureusement, MacOSX> = 10.8 fournit BSD grep dont -m et -c options agissent globalement , pas par fichier), cette alternative peut être intéressante (et plus rapide qu'un script awk pur ):

grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//'

Explication:

  • grep -c -m28 -H ^ *.txt affiche le nom de chaque fichier avec le nombre de lignes dans chaque fichier, mais ne lit jamais plus de 28 lignes
  • sed '/:28$/ d; s/:[^:]*$//' supprime les fichiers d'au moins 28 lignes et affiche le nom du fichier.

Version alternative: traitement séquentiel au lieu d'un traitement parallèle

res=$(grep -c -m28 -H ^ $files); sed '/:28$/ d; s/:[^:]*$//' <<< "$res"

Benchmarking

Ed Morton a contesté mon affirmation selon laquelle cette réponse pourrait être plus rapide que awk. Il a ajouté quelques repères à sa réponse et, bien qu'il ne tire aucune conclusion, j'estime que les résultats qu'il a publiés sont trompeurs, car ils indiquent un temps de réponse plus long pour ma réponse, sans tenir compte des temps utilisateur et système. Par conséquent, voici mes résultats.

D'abord la plateforme de test:

  • Un ordinateur portable Intel i5 à quatre cœurs fonctionnant sous Linux, probablement assez proche du système de l'OP (Apple iMac).

  • Un tout nouveau répertoire de 100 000 fichiers texte avec environ 400 lignes en moyenne, pour un total de 640 Mo, qui est entièrement conservé dans les mémoires tampons de mon système. Les fichiers ont été créés avec cette commande:

    for ((f = 0; f < 100000; f++)); do echo "File $f..."; for ((l = 0; l < RANDOM & 1023; l++)); do echo "File $f; line $l"; done > file_$f.txt; done
    

Résultats:

Conclusion:

Au moment de la rédaction de cet article, cette réponse est la plus rapide sur les ordinateurs portables Unix multi-cœurs normaux, similaire à la machine d'OP. Elle donne des résultats précis. Sur ma machine, il est deux fois plus rapide que le script awk le plus rapide.

Remarques:

  • Pourquoi la plate-forme est-elle importante? Parce que ma réponse repose sur la parallélisation du traitement entre grep et sed. Bien sûr, pour obtenir des résultats impartiaux, si vous n’avez qu’un seul cœur de processeur (VM?) Ou d’autres limitations de votre système d’exploitation en ce qui concerne l’allocation de CPU, vous devez comparer la version de remplacement (séquentielle).

  • De toute évidence, vous ne pouvez pas conclure sur le temps de mur seul, car cela dépend du nombre de processus simultanés demandant le processeur par rapport au nombre de cœurs de la machine. Par conséquent, j'ai ajouté les timings utilisateur + sys

  • Ces durées correspondent en moyenne à 20 exécutions, sauf lorsque la commande a pris plus d'une minute (une exécution seulement).

  • Pour toutes les réponses qui prennent moins de 10 secondes, le temps mis par Shell à traiter *.txt n'est pas négligeable, c'est pourquoi j'ai prétraité la liste de fichiers, je l'ai mis dans une variable et ajouté le contenu de la variable à la commande que j'ai comparée. .

  • Toutes les réponses ont donné les mêmes résultats sauf 1. La réponse de tripleee qui inclut argv[0] ("awk") dans son résultat (corrigée dans mes tests); 2. La réponse de kvantour qui ne listait que les fichiers vides (corrigé avec -v n=27); et 3. la réponse find + sed qui manque les fichiers vides (non corrigés).

  • Je n'ai pas pu tester la réponse de ctac_ puisque je n'ai pas GNU sed 4.5 à portée de main. C'est probablement le plus rapide de tous mais manque aussi les fichiers vides.

  • La réponse python ne ferme pas ses fichiers. Je devais faire ulimit -n hard en premier.

4
xhienne

Comment c'est?

awk 'BEGIN { for(i=1;i<ARGC; ++i) arg[ARGV[i]] }
  FNR==28 { delete arg[FILENAME]; nextfile }
  END { for (file in arg) print file }' *.txt

Nous copions la liste des arguments de nom de fichier dans un tableau associatif, puis supprimons tous les fichiers contenant une 28ème ligne. Les fichiers vides ne correspondent évidemment pas à cette condition. Ainsi, à la fin, il nous reste tous les fichiers contenant moins de lignes, y compris les vides.

nextfile était une extension courante dans de nombreuses variantes d'Awk puis a été codifiée par POSIX en 2012. Si vous avez besoin de cela pour fonctionner sur de très vieux systèmes d'exploitation de dinosaures (ou, Dieu merci, probablement Windows), bonne chance et/ou essayez GNU Awk. 

3
tripleee

Tandis que awk semble être le moyen le plus intéressant de procéder, voici encore un autre des solutions existantes de triplee , anubhava et Ed Morton . Où les solutions de triplee et anubhava utilisent-elles l'instruction nextfile et la solution de preuve POSIX d'Ed Morton lit-elle des fichiers complets? Je propose une solution qui ne lit pas ces fichiers.

awk -v n=27 'BEGIN{ for(i=1;i<ARGC;++i) {
                       j=0; fname=ARGV[i];
                       while( ((getline < fname) > 0 ) && j<=n) { j++ }
                       if(j<=n) print fname; close(fname)
                  }
                  exit
             }' *.txt
3
kvantour

Vous pouvez essayer cette awk qui passe au fichier suivant dès que le nombre de lignes dépasse 27:

awk -v n=27 'BEGIN{for (i=1; i<ARGC; i++) f[ARGV[i]]}
FNR > n{delete f[FILENAME]; nextfile}
END{for (i in f) print i}' *.txt

awk traite les fichiers ligne par ligne afin d'éviter de lire le fichier complet pour obtenir le nombre de lignes.

3
anubhava

avec sed (GNU sed) 4.5:

sed -n -s '28q;$F' *.txt
1
ctac_

Vous pouvez utiliser find à l'aide d'un petit script en ligne bash:

find -type f -exec bash -c '[ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}"' -- {} \;

La commande [ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}" utilise grep pour rechercher le début d'une ligne (^) au maximum 28 fois. Si cette commande retourne! = "28", le fichier doit comporter plus de 28 lignes.

1
hek2mgl

Outils logiciels et mashup GNUsed (versions antérieures à v4.5):

find *.txt -print0 | xargs -0 -L 1 sed -n '28q;$F'

Cela manque les fichiers de 0 octet, pour inclure ceux-ci aussi, faites:

find *.txt \( -exec sed -n '28{q 1}' '{}' \; -or -size 0 \) -print

(Pour une raison quelconque, exécuter sed via -exec est d’environ 12% plus lent que xargs.)


sed code volé à réponse de ctac }.

Remarque: Sur l'ancien sedv4.4-2 de mon propre système, la commande quit combinée avec le commutateur --separate ne fait pas que quitter le fichier actuel, elle ferme également sed. Ce qui signifie qu'il nécessite une instance distincte de sed pour chaque fichier.

0
agc

Si vous devez appeler awk individuellement, demandez-lui de s’arrêter à la ligne 28:

for f in ./*.txt
do
  if awk 'NR > 27 { fail=1; exit; } END { exit fail; }' "$f"
  then
    printf '%s\n' "$f"
  fi
done

La valeur par défaut des variables awk est zéro. Par conséquent, si nous n’atteignons jamais la ligne 28, le code de sortie est égal à zéro, ce qui permet au test if de réussir et d’imprimer le nom du fichier.

0
Jeff Schaller