Rimuovi tutti i file tranne 12


14

Ho alcune migliaia di file nel formato nomefile.12345.end. Voglio solo conservare ogni dodicesimo file, quindi file.00012.end, file.00024.end ... file.99996.end ed eliminare tutto il resto.

I file possono anche avere numeri precedenti nel loro nome file e sono normalmente nel formato: file.00064.name.99999.end

Uso Bash shell e non riesco a capire come eseguire il loop dei file, quindi estrarre il numero e verificare se sta number%%12=0 eliminando il file, in caso contrario. Qualcuno può aiutarmi?

Grazie Dorina


Il numero del file dipende solo dal nome del file?
Arronical,

Inoltre, i file hanno sempre 5 cifre e il suffisso e il prefisso sono sempre gli stessi?
Arronical,

Sì, sono sempre 5 cifre. Non sono sicuro che la prima domanda sia corretta. I file con nomi di file diversi sono diversi e ho bisogno di questi file specifici che hanno i numeri 00012, 00024 ecc.
Dorina,

3
@Dorina, per favore, modifica la tua domanda e chiariscilo. Cambia tutto!
Terdon,

2
E sono tutti nella stessa directory, giusto?
Sergiy Kolodyazhnyy,

Risposte:


18

Ecco una soluzione Perl. Questo dovrebbe essere molto più veloce per migliaia di file:

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

Che può essere ulteriormente condensato in:

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

Se hai troppi file e non puoi usare il semplice *, puoi fare qualcosa del tipo:

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

Per quanto riguarda la velocità, ecco un confronto tra questo approccio e quello della shell fornito in una delle altre risposte:

$ 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

Come puoi vedere, la differenza è enorme, come previsto .

Spiegazione

  • Il -eè semplicemente dicendo perlper eseguire lo script data sulla riga di comando.
  • @ARGVè una variabile speciale che contiene tutti gli argomenti forniti allo script. Dal momento che lo stiamo dando *, conterrà tutti i file (e le directory) nella directory corrente.
  • Il grepcercherà l'elenco dei nomi di file e cercare qualsiasi che corrispondono una stringa di numeri, un punto e end( /(\d+)\.end/).

  • Poiché i numeri ( \d) sono in un gruppo di acquisizione (parentesi), vengono salvati come $1. Quindi grepcontrollerà se quel numero è un multiplo di 12 e, in caso contrario, verrà restituito il nome del file. In altre parole, l'array @badcontiene l'elenco dei file da eliminare.

  • L'elenco viene quindi passato a unlink()cui rimuove i file (ma non le directory).


12

Dato che i nomi dei file sono nel formato file.00064.name.99999.end, dobbiamo prima tagliare tutto tranne il nostro numero. Useremo un forloop per fare questo.

Dobbiamo anche dire alla shell Bash di usare la base 10, perché l'aritmetica di Bash tratterà i loro numeri che iniziano con uno 0 come base 8, il che ci rovinerà le cose.

Come script, per essere avviato quando nella directory contenente i file utilizzare:

#!/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

Oppure puoi usare questo brutto comando molto lungo per fare la stessa cosa:

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

Per spiegare tutte le parti:

  • for f in ./* significa per tutto nella directory corrente, fare .... Questo imposta ogni file o directory trovato come variabile $ f.
  • if [[ -f "$f" ]]controlla se l'elemento trovato è un file, altrimenti passiamo alla echo "$f is not...parte, il che significa che non iniziamo a cancellare accidentalmente le directory.
  • file="${f%.*}"imposta la variabile $ file come nome del file eliminando tutto ciò che viene dopo l'ultimo ..
  • if [[ $((10#${file##*.} % 12)) -eq 0 ]]è qui che entra in gioco l'aritmetica principale. ${file##*.}Taglia tutto prima dell'ultimo .nel nostro nome file senza estensione. $(( $num % $num2 ))è la sintassi per l'aritmetica di Bash per usare l'operazione modulo, 10#all'inizio dice a Bash di usare la base 10, per gestire quei fastidiosi 0 iniziali. $((10#${file##*.} % 12))poi ci lascia il resto del nostro numero di nomi di file diviso per 12. -ne 0controlla se il resto è "non uguale" a zero.
  • Se il resto non è uguale a 0, il file viene eliminato con il rmcomando, è possibile sostituirlo rmcon echoquando lo si esegue per la prima volta, per verificare di ottenere i file previsti da eliminare.

Questa soluzione non è ricorsiva, il che significa che elaborerà solo i file nella directory corrente, non andrà in nessuna sottodirectory.

L' ifaffermazione con il echocomando per avvisare delle directory non è davvero necessaria poiché rmda sola si lamenterà delle directory e non li cancellerà, quindi:

#!/bin/bash

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

O

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

Funzionerà anche correttamente.


5
Chiamare rmalcune migliaia di volte può essere piuttosto lento. Suggerisco al echonome del file, invece, e inviare l'output del ciclo di xargs rm(opzioni aggiuntivi se necessario): for f in *; do if ... ; then echo "$f"; fi; done | xargs -rd '\n' -- rm --.
David Foerster,

Ho modificato per includere il miglioramento della velocità suggerito.
Arronical,

In realtà dopo il test su una directory con 55999 file, la versione originale impiegava 2 minuti e 48 secondi, la xargsversione impiegava 5 minuti e 1 secondo. Questo potrebbe essere dovuto al sovraccarico su echo@DavidFoerster?
Arronical,

Dispari. Per 60.000 file ottengo 0m0.659s / 0m0.545s / 0m0.380s (real / user / sys) con time { for f in *; do echo "$f"; done | xargs rm; }1m11.450s / 0m10.695s / 0m16.800s con time { for f in *; do rm "$f"; done; }su un tmpfs. Bash è v4.3.11, Kernel è v4.4.19.
David Foerster,

6

Puoi usare l'espansione della parentesi Bash per generare nomi contenenti ogni 12 ° numero. Creiamo alcuni dati di 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

Quindi possiamo usare quanto segue

$ 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

Funziona irrimediabilmente lentamente per una grande quantità di file - ci vuole tempo e memoria per generare migliaia di nomi - quindi è più un trucco che un'effettiva soluzione efficiente.


Mi piace il code-golf su questo.
David Foerster,

1

Un po 'lungo, ma è quello che mi è venuto in mente.

 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

Spiegazione: eliminare ogni 12 file undici volte.


0

In tutta umiltà penso che questa soluzione sia molto più bella dell'altra risposta:

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

Una piccola spiegazione: in primo luogo generiamo un elenco di file con find. Otteniamo tutti i file il cui nome termina .ende che hanno una profondità di 1 (vale a dire, sono direttamente nella directory di lavoro e non in alcuna sottocartella. Puoi lasciarlo fuori se non ci sono sottocartelle). L'elenco di output verrà ordinato in ordine alfabetico.

Quindi reindirizziamo tale elenco in awkcui utilizziamo la variabile speciale NRche è il numero di riga. Tralasciamo ogni dodicesimo file stampando i file dove NR%12 != 0. Il awkcomando può essere abbreviato in awk 'NR%12', perché il risultato dell'operatore modulo viene interpretato come un valore booleano e {print}viene comunque implicitamente eseguito.

Quindi ora abbiamo un elenco di file che devono essere eliminati, cosa che possiamo fare con xargs e rm. xargsesegue il comando dato ( rm) con l'input standard come argomenti.

Se hai molti file, visualizzerai un errore che dice qualcosa come "elenco argomenti troppo lungo" (sulla mia macchina quel limite è di 256 kB e il minimo richiesto da POSIX è 4096 byte). Questo può essere evitato dal -n 100flag, che divide gli argomenti ogni 100 parole (non righe, qualcosa a cui fare attenzione se i nomi dei file hanno spazi) ed esegue un separatorm comando , ciascuno con solo 100 argomenti.


3
Ci sono un paio di problemi con il tuo approccio: -depthdeve essere prima -name; ii) ciò fallirà se uno dei nomi dei file contiene spazi bianchi; iii) stai supponendo che i file saranno elencati in ordine numerico crescente (questo è ciò awkper cui stai testando) ma quasi sicuramente non sarà così. Pertanto, questo eliminerà un set casuale di file.
Terdon,

D'oh! Hai perfettamente ragione, mio ​​cattivo (commento modificato). Ho ricevuto l'errore a causa del posizionamento errato e non me lo sono ricordato -depth. Tuttavia, quello era l'ultimo dei problemi qui, il più importante è che stai eliminando un set casuale di file e non quelli che l'OP vuole.
Terdon,

Oh, e no, -depthnon ha valore e fa l'opposto di quello che pensi che faccia. Vedi man find: "-depth Elabora i contenuti di ogni directory prima della directory stessa.". Quindi questo effettivamente scenderà nelle sottodirectory e causerà il caos in tutto il luogo.
terdon,

I) Entrambi -depth ned -maxdepth nesistono. Il primo richiede che la profondità sia esattamente n, e con il secondo può essere <= n. II). Sì, è un male, ma per questo particolare esempio non è un problema. È possibile risolverlo utilizzando find ... -print0 | awk 'BEGIN {RS="\0"}; NR%12 != 0' | xargs -0 -n100 rm, che utilizza il byte null come separatore di record (che non è consentito nei nomi di file). III) Ancora una volta, in questo caso il presupposto è ragionevole. Altrimenti potresti inserire un sort -ntra finde awk, o reindirizzare finda un file e ordinarlo come preferisci.
user593851

3
Ah, probabilmente stai usando OSX allora. Questa è un'implementazione molto diversa di find. Ancora una volta, tuttavia, il problema principale è che stai presupponendo che findrestituisca un elenco ordinato. Non
Terdon,

0

Per usare solo bash, il mio primo approccio sarebbe: 1. spostare tutti i file che si desidera conservare in un'altra directory (ovvero tutti quelli il cui numero nel nome file è un multiplo di 12), quindi 2. eliminare tutti i file rimanenti nella directory, quindi 3. rimetti i file multipli di 12 che hai tenuto dove erano. Quindi qualcosa del genere potrebbe funzionare:

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

Mi piace l'approccio, ma come si genera la filenameparte se non è coerente?
Arronical,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.