Supprimer tous les fichiers sauf tous les 12

14

J'ai quelques milliers de fichiers au format filename.12345.end. Je veux seulement conserver chaque 12ème fichier, donc file.00012.end, file.00024.end ... file.99996.end et supprimer tout le reste.

Les fichiers peuvent également avoir des numéros plus tôt dans leur nom de fichier et sont normalement de la forme: file.00064.name.99999.end

J'utilise le shell Bash et ne peux pas comprendre comment boucler sur les fichiers, puis sortir le numéro et vérifier s'il number%%12=0 supprime le fichier sinon. Quelqu'un peut-il m'aider?

Merci, Dorina

Dorina
la source
Le numéro du fichier dépend-il uniquement du nom de fichier?
Arronical
De plus, les fichiers ont-ils toujours 5 chiffres et le suffixe et le préfixe sont-ils toujours les mêmes?
Arronical
Oui, c'est toujours 5 chiffres. Je ne sais pas si j'ai bien répondu à votre première question. Les fichiers avec des noms de fichiers différents sont différents, et j'ai besoin de ces fichiers spécifiques qui ont les numéros 00012, 00024 etc.
Dorina
3
@ Dorina, veuillez modifier votre question et clarifier cela. Ça change tout!
terdon
2
Et ils sont tous dans le même répertoire, non?
Sergiy Kolodyazhnyy

Réponses:

18

Voici une solution Perl. Cela devrait être beaucoup plus rapide pour des milliers de fichiers:

perl -e '@bad=grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV; unlink @bad' *

Qui peut être encore condensé en:

perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

Si vous avez trop de fichiers et ne pouvez pas utiliser le simple *, vous pouvez faire quelque chose comme:

perl -e 'opendir($d,"."); unlink grep{/(\d+)\.end/ && $1 % 12 != 0} readdir($dir)'

En ce qui concerne la vitesse, voici une comparaison de cette approche et celle du shell fournie dans l'une des autres réponses:

$ touch file.{01..64}.name.{00001..01000}.end
$ ls | wc
  64000   64000 1472000
$ time for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

real    2m44.258s
user    0m9.183s
sys     1m7.647s

$ touch file.{01..64}.name.{00001..01000}.end
$ time perl -e 'unlink grep{/(\d+)\.end/ && $1 % 12 != 0}@ARGV;' *

real    0m0.610s
user    0m0.317s
sys     0m0.290s

Comme vous pouvez le voir, la différence est énorme, comme prévu .

Explication

  • Le -edit simplement perld'exécuter le script donné sur la ligne de commande.
  • @ARGVest une variable spéciale contenant tous les arguments donnés au script. Puisque nous le donnons *, il contiendra tous les fichiers (et répertoires) du répertoire courant.
  • Le greprecherche dans la liste des noms de fichiers et recherche ceux qui correspondent à une chaîne de chiffres, un point et end( /(\d+)\.end/).

  • Étant donné que les nombres ( \d) sont dans un groupe de capture (parenthèses), ils sont enregistrés sous $1. Le grepvérifie alors si ce nombre est un multiple de 12 et, si ce n'est pas le cas, le nom du fichier sera retourné. En d'autres termes, le tableau @badcontient la liste des fichiers à supprimer.

  • La liste est ensuite passée à unlink()laquelle supprime les fichiers (mais pas les répertoires).

terdon
la source
12

Étant donné que vos noms de fichiers sont au format file.00064.name.99999.end, nous devons d'abord supprimer tout sauf notre numéro. Nous allons utiliser une forboucle pour ce faire.

Nous devons également dire au shell Bash d'utiliser la base 10, car l'arithmétique Bash traitera les nombres commençant par un 0 comme base 8, ce qui gâchera les choses pour nous.

En tant que script, pour être lancé dans le répertoire contenant les fichiers, utilisez:

#!/bin/bash

for f in ./*
do
  if [[ -f "$f" ]]; then
    file="${f%.*}"
    if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
      rm "$f"
    fi
  else
    echo "$f is not a file, skipping."
  fi
done

Ou vous pouvez utiliser cette très longue commande laide pour faire la même chose:

for f in ./* ; do if [[ -f "$f" ]]; then file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; else echo "$f is not a file, skipping."; fi; done

Pour expliquer toutes les parties:

  • for f in ./* signifie pour tout dans le répertoire courant, faire .... Ceci définit chaque fichier ou répertoire trouvé comme variable $ f.
  • if [[ -f "$f" ]]vérifie si l'élément trouvé est un fichier, sinon nous sautons à la echo "$f is not...partie, ce qui signifie que nous ne commençons pas à supprimer les répertoires accidentellement.
  • file="${f%.*}" définit la variable $ file comme nom de fichier, supprimant tout ce qui vient après le dernier . .
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]]C'est là que l'arithmétique principale entre en jeu. Le ${file##*.}coupe tout avant le dernier .dans notre nom de fichier sans extension. $(( $num % $num2 ))est la syntaxe pour que l'arithmétique Bash utilise l'opération modulo, la commande 10#indique au début à Bash d'utiliser la base 10 pour gérer les 0 en tête embêtants. $((10#${file##*.} % 12))nous laisse ensuite le reste de notre nombre de noms de fichiers divisé par 12.-ne 0 vérifie si le reste n'est "pas égal" à zéro.
  • Si le reste n'est pas égal à 0, le fichier est supprimé avec la rmcommande, vous voudrez peut-être le remplacer rmpar echolors de la première exécution, pour vérifier que vous obtenez les fichiers attendus à supprimer.

Cette solution est non récursive, ce qui signifie qu'elle ne traitera que les fichiers du répertoire en cours, elle n'ira dans aucun sous-répertoire.

L' ifinstruction avec la echocommande pour avertir des répertoires n'est pas vraiment nécessaire car rmelle se plaindra des répertoires et ne les supprimera pas, donc:

#!/bin/bash

for f in ./*
do
  file="${f%.*}"
  if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then
    rm "$f"
  fi
done

Ou

for f in ./* ; do file="${f%.*}"; if [[ $((10#${file##*.} % 12)) -ne 0 ]]; then rm "$f"; fi; done

Fonctionnera correctement aussi.

Arronical
la source
5
Appeler rmquelques milliers de fois peut être assez lent. Je suggère echole nom du fichier à la place et redirigez la sortie de la boucle xargs rm(options d'ajouter au besoin): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
David Foerster
J'ai modifié pour inclure votre suggestion d'amélioration de la vitesse.
Arronical
En fait, après avoir testé sur un répertoire avec 55999 fichiers, la version d'origine a pris 2 minutes 48 secondes, la xargsversion a pris 5 minutes 1 seconde. Cela pourrait-il être dû à des frais généraux sur echo@DavidFoerster?
Arronical
Impair. Pour 60.000 fichiers, j'obtiens 0m0.659s / 0m0.545s / 0m0.380s (réel / utilisateur / sys) avec time { for f in *; do echo "$f"; done | xargs rm; }vs 1m11.450s / 0m10.695s / 0m16.800s avec time { for f in *; do rm "$f"; done; }un tmpfs. Bash est v4.3.11, le noyau est v4.4.19.
David Foerster
6

Vous pouvez utiliser l'expansion du support Bash pour générer des noms contenant tous les 12 numéros. Créons des données de test

$ touch file.{0..9}{0..9}{0..9}{0..9}{0..9}.end # create test data
$ mv file.00024.end file.00024.end.name.99999.end # testing this form of filenames

Ensuite, nous pouvons utiliser ce qui suit

$ ls 'file.'{00012..100..12}* # print these with numbers less than 100
file.00012.end                 file.00036.end  file.00060.end  file.00084.end
file.00024.end.name.99999.end  file.00048.end  file.00072.end  file.00096.end
$ rm 'file.'{00012..100000..12}* # do the job

Fonctionne désespérément lentement pour une grande quantité de fichiers - cela prend du temps et de la mémoire pour générer des milliers de noms - c'est donc plus une astuce qu'une solution efficace.

Nykakin
la source
J'aime le code-golf sur celui-ci.
David Foerster
1

Un peu long, mais c'est ce qui m'est venu à l'esprit.

 for num in $(seq 1 1 11) ; do
     for sequence in $(seq -f %05g $num 12 99999) ; do
         rm file.$sequence.end.99999;
     done
 done

Explication: supprimez tous les 12 fichiers onze fois.

Terrik
la source
0

En toute humilité, je pense que cette solution est beaucoup plus agréable que l'autre réponse:

find . -name '*.end' -depth 1 | awk 'NR%12 != 0 {print}' | xargs -n100 rm

Une petite explication: nous générons d'abord une liste de fichiers avec find. Nous obtenons tous les fichiers dont le nom se termine par.end et qui sont à une profondeur de 1 (c'est-à-dire qu'ils sont directement dans le répertoire de travail et non dans aucun sous-dossier. Vous pouvez laisser cela de côté s'il n'y a pas de sous-dossier). La liste de sortie sera triée par ordre alphabétique.

Ensuite, nous dirigeons cette liste vers awk, où nous utilisons la variable spéciale NRqui est le numéro de ligne. Nous omettons tous les 12 fichiers en imprimant les fichiers où NR%12 != 0. La awkcommande peut être raccourcie awk 'NR%12', car le résultat de l'opérateur modulo est interprété comme une valeur booléenne et {print}est implicitement fait de toute façon.

Alors maintenant, nous avons une liste de fichiers qui doivent être supprimés, ce que nous pouvons faire avec xargs et rm. xargsexécute la commande donnée (rm ) avec l'entrée standard comme arguments.

Si vous avez beaucoup de fichiers, vous obtiendrez une erreur disant quelque chose comme «liste d'arguments trop longue» (sur ma machine, cette limite est de 256 Ko et le minimum requis par POSIX est de 4096 octets). Cela peut être évité par l' -n 100indicateur, qui divise les arguments tous les 100 mots (pas les lignes, quelque chose à surveiller si vos noms de fichier ont des espaces) et exécute une rmcommande distincte , chacun avec seulement 100 arguments.

user593851
la source
3
Il y a quelques problèmes avec votre approche: -depthdoit être avant -name; ii) cela échouera si l'un des noms de fichiers contient des espaces; iii) vous supposez que les fichiers seront répertoriés par ordre numérique croissant (c'est ce que vous awktestez), mais ce ne sera certainement pas le cas. Par conséquent, cela supprimera un ensemble aléatoire de fichiers.
terdon
d'oh! Tu as tout à fait raison, ma mauvaise (commentaire édité). J'ai eu l'erreur à cause du mauvais placement et je ne m'en souvenais pas -depth. Pourtant, c'était le moindre des problèmes ici, le plus important est que vous supprimez un ensemble aléatoire de fichiers et non pas ceux que l'OP veut.
terdon
Oh, et non, -depthcela ne prend pas de valeur et cela fait le contraire de ce que vous pensez qu'il fait. Voir man find: "-depth Traite le contenu de chaque répertoire avant le répertoire lui-même.". Donc, cela va en fait descendre dans des sous-répertoires et faire des ravages partout.
terdon du
I) Les deux -depth net -maxdepth nexistent. La première nécessite que la profondeur soit exactement n, et avec la seconde, elle peut être <= n. II). Oui, c'est mauvais mais pour cet exemple particulier ce n'est pas un problème. Vous pouvez le corriger en utilisant find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rm, qui utilise l'octet nul comme séparateur d'enregistrement (ce qui n'est pas autorisé dans les noms de fichiers). III) Encore une fois, dans ce cas, l'hypothèse est raisonnable. Sinon, vous pouvez insérer un sort -nentre findet awk, ou rediriger findvers un fichier et le trier comme vous le souhaitez.
user593851
3
Ah, vous utilisez probablement OSX alors. C'est une implémentation très différente de find. Encore une fois, cependant, le principal problème est que vous supposez que findrenvoie une liste triée. Ce n'est pas le cas.
terdon
0

Pour utiliser uniquement bash, ma première approche serait de: 1. déplacer tous les fichiers que vous souhaitez conserver dans un autre répertoire (c'est-à-dire tous ceux dont le nombre dans le nom de fichier est un multiple de 12) puis 2. supprimer tous les fichiers restants dans le répertoire, puis 3. remettez les fichiers multiples de 12 que vous avez conservés là où ils étaient. Donc, quelque chose comme ça pourrait fonctionner:

cd dir_containing_files
mkdir keep_these_files
n=0
while [ "${n}" -lt 99999 ]; do
  padded_n="`echo -n "00000${n}" | tail -c 5`"
  mv "filename${padded_n}.end" keep_these_files/
  n=$[n+12]
done
rm filename*.end
mv keep_these_files/* .
rmdir keep_these_files
delt
la source
J'aime l'approche, mais comment générer la filenamepièce si elle n'est pas cohérente?
Arronical