Script di shell per lo spostamento di file meno recenti?


14

Come faccio a scrivere uno script per spostare solo i 20 file più vecchi da una cartella all'altra? C'è un modo per prendere i file più vecchi in una cartella?


Includere o escludere le sottodirectory? E dovrebbe essere fatto in modo ricorsivo (in un albero di directory)?
maxschlepzig,

2
Molti filesystem (la maggior parte?) * Nix non memorizzano la data di creazione, quindi non è possibile determinare con certezza il file più vecchio . Gli attributi generalmente disponibili sono atime(ultimo accesso), ctime(ultima modifica dell'autorizzazione) e mtime(ultima modifica) ... ad es. ls -te trova l' printf "%T" uso mtime... Sembra, secondo questo link , che le mie ext4partizioni siano in grado di gestire una data di creazione, ma lse finde statnon abbiano (ancora) le opzioni appropriate ...
Peter.O

Risposte:


13

L'analisi dell'output di nonls è affidabile .

Invece, usa findper localizzare i file e sortper ordinarli in base al timestamp. Per esempio:

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    # do something with $file here
done < <(find . -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

Cosa sta facendo tutto questo?

Innanzitutto, i findcomandi individuano tutti i file e le directory nella directory corrente ( .), ma non nelle sottodirectory della directory corrente ( -maxdepth 1), quindi stampano:

  • Un timestamp
  • Uno spazio
  • Il percorso relativo al file
  • Un carattere NULL

Il timestamp è importante. L' %T@identificatore di formato per si -printfsuddivide in T, che indica "Ora ultima modifica" del file (mtime) e @, che indica "Secondi dal 1970", compresi i secondi frazionari.

Lo spazio è semplicemente un delimitatore arbitrario. Il percorso completo del file è in modo che possiamo fare riferimento ad esso in un secondo momento, e il carattere NULL è un terminatore perché è un carattere illegale in un nome di file e quindi ci fa sapere con certezza che abbiamo raggiunto la fine del percorso per il file.

Ho incluso in 2>/dev/nullmodo tale da escludere i file ai quali l'utente non ha il permesso di accedere, ma i messaggi di errore che li escludono vengono eliminati.

Il risultato del findcomando è un elenco di tutte le directory nella directory corrente. L'elenco viene reindirizzato a sortcui viene richiesto di:

  • -z Considera NULL come carattere di terminazione di riga anziché come nuova riga.
  • -n Ordina numericamente

Poiché i secondi, dal 1970, salgono sempre, vogliamo il file il cui timestamp era il numero più piccolo. Il primo risultato da sortsarà la riga contenente il timestamp numerato più piccolo. Non resta che estrarre il nome del file.

I risultati della pipeline find, sortvengono passati attraverso la sostituzione del processo nel punto in whilecui viene letto come se fosse un file su stdin. whilea sua volta invoca readper elaborare l'input.

Nel contesto di readimpostare la IFSvariabile su nulla, il che significa che gli spazi bianchi non verranno interpretati in modo inappropriato come delimitatore. readviene detto -r, che disabilita l'espansione di fuga, e -d $'\0', il che rende il delimitatore NULL end-of-line, abbinando l'uscita dal nostro find, sortpipeline.

Il primo blocco di dati, che rappresenta il percorso del file più vecchio preceduto dal suo timestamp e da uno spazio, viene letto nella variabile line. Successivamente, la sostituzione dei parametri viene utilizzata con l'espressione #*, che sostituisce semplicemente tutti i caratteri dall'inizio della stringa fino al primo spazio, incluso lo spazio, con nulla. Questo elimina il timestamp di modifica, lasciando solo il percorso completo del file.

A questo punto il nome del file è archiviato $filee puoi fare tutto ciò che ti piace. Quando hai finito di fare qualcosa con $filel' whileistruzione, il ciclo readverrà eseguito e il comando verrà eseguito di nuovo, estraendo il blocco successivo e il nome del file successivo.

Non c'è un modo più semplice?

No. I modi più semplici sono corretti.

Se si utilizza ls -te si reindirizza heado tail(o altro ) si interromperanno i file con le nuove righe nei nomi dei file. Se mv $(anything)poi i file con spazi bianchi nel nome causeranno la rottura. Se mv "$(anything)"poi file con nuove righe finali nel nome causeranno la rottura. Se readsenza -d $'\0'Allora ti rompi il file con spazi nei loro nomi.

Forse in casi specifici sai per certo che è sufficiente un modo più semplice, ma non dovresti mai scrivere ipotesi del genere negli script se riesci a evitare di farlo.

Soluzione

#!/usr/bin/env bash

# move to the first argument
dest="$1"

# move from the second argument or .
source="${2-.}"

# move the file count in the third argument or 20
limit="${3-20}"

while IFS= read -r -d $'\0' line ; do
    file="${line#* }"
    echo mv "$file" "$dest"
    let limit-=1
    [[ $limit -le 0 ]] && break
done < <(find "$source" -maxdepth 1 -printf '%T@ %p\0' \
    2>/dev/null | sort -z -n)

Chiama come:

move-oldest /mnt/backup/ /var/log/foo/ 20

Per spostare i 20 file più vecchi da /var/log/foo/a /mnt/backup/.

Nota che sto includendo file e directory. Per i file solo aggiungere -type falla findinvocazione.

Grazie

Grazie a enzotib e Павел Танков per i miglioramenti a questa risposta.


Il tipo non dovrebbe usare -n. Almeno nella mia versione, non ordina i numeri decimali correttamente. Devi rimuovere il punto nella data o utilizzare -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz, le date ISO o qualcos'altro.
l0b0

@ l0b0: questa limitazione mi è nota. Presumo che sia sufficiente non richiedere quel livello di granularità (vale a dire, l'ordinamento oltre il .deve essere irrilevante per te.) Sarebbe più chiaro da dire sort -z -n -t. -k1.
Sorpigal,

@ l0b0: la tua soluzione presenta lo stesso bug, a prescindere: %TSmostra anche una "parte frazionaria" che sarebbe nella forma 00.0000000000, quindi perdi anche la granularità. Recenti GNU sortpotrebbero risolvere questo problema usando -Vun "ordinamento versione", che gestirà questo tipo di virgola mobile come previsto.
Sorpigal,

No, perché eseguo un ordinamento per stringa su "AAAA-MM-GGThh: mm: ss" anziché un ordinamento numerico. L'ordinamento delle stringhe non si preoccupa dei decimali, quindi dovrebbe funzionare fino all'anno 10000 :)
l0b0

@ l0b0: funzionerebbe anche un ordinamento di stringhe %T@, poiché è a zero.
Sorpigal,

4

È più semplice in zsh, dove puoi usare il Om qualificatore glob per ordinare le partite per data (prima la più vecchia) e il [1,20]qualificatore per conservare solo le prime 20 partite:

mv -- *(Om[1,20]) target/

Aggiungi il Dqualificatore se vuoi includere anche i file dot. Aggiungi .se vuoi abbinare solo i file normali e non le directory.

Se non hai zsh, ecco un one-liner Perl (puoi farlo in meno di 80 caratteri, ma a un ulteriore costo in termini di chiarezza):

perl -e '@files = sort {-M $b <=> -M $a} glob("*"); foreach (@files[0..1]) {rename $_, "target/$_" or die "$_: $!"}'

Con solo gli strumenti POSIX o anche bash o ksh, ordinare i file per data è una seccatura. Puoi farlo facilmente con ls, ma analizzare l'output di lsè problematico, quindi funziona solo se i nomi dei file contengono solo caratteri stampabili diversi da newline.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done

4

Combina ls -toutput con tailo head.

Esempio semplice, che funziona solo se tutti i nomi file contengono solo caratteri stampabili diversi dagli spazi bianchi e \[*?:

 mv $(ls -1tr | head -20) other_folder

1
Aggiungi l'opzione -A a ls:ls -1Atr
Arcege,

1
-1, pericoloso. Qui mi permetta di mestiere un esempio: touch $'foo\n*'. Cosa succede se esegui mv "$ (ls)" con quel file seduto lì?
Sorpigal,

1
@Sorpigal Seriamente? È un po 'debole dire "Lasciami fare un esempio che hai detto specificamente non funzionerà. Ehi guarda, non funziona"
Michael Mrozek

1
@Sorpigal Non è una cattiva idea, funziona nel 99% dei casi. La risposta è "se hai file con nomi normali, questo funziona. Se sei una persona pazza che incorpora nuove righe nei loro nomi di file, non lo farà". È del tutto corretto
Michael Mrozek

1
@MichaelMrozek: è una cattiva idea ed è cattiva perché a volte fallisce. Se hai la possibilità di fare ciò che a volte fallisce e cosa no, dovresti scegliere l'opzione che non funziona (e quella che fa male). Fai quello che ti piace in modo interattivo, ma in un file di script e quando dai consigli fallo correttamente.
Sorpigal,

3

Puoi usare GNU find per questo:

find -maxdepth 1 -type f -printf '%T@ %p\n' \
  | sort -k1,1 -g | head -20 | sed 's/^[0-9.]\+ //' \
  | xargs echo mv -t dest_dir

Dove find stampa il tempo di modifica (in secondi dal 1970) e il nome di ciascun file della directory corrente, l'output viene ordinato in base al primo campo, i 20 più vecchi vengono filtrati e spostati in dest_dir. Rimuovi echose hai testato la riga di comando.


2

Nessuno ha (ancora) pubblicato un esempio bash che si rivolge a caratteri newline incorporati (incorporato qualsiasi cosa) nel nome del file, quindi eccone uno. Sposta i 3 file regolari più vecchi (mdate)

move=3
find . -maxdepth 1 -type f -name '*' \
 -printf "%T@\t%p\0" |sort -znk1 | { 
  while IFS= read -d $'\0' -r file; do
      printf "%s\0" "${file#*$'\t'}"
      ((--move==0)) && break
  done } |xargs -0 mv -t dest

Questo è lo snippet di dati di test

# make test files with names containing \n, \t and "  "
rm -f '('?[1-4]'  |?)'
for f in $'(\n'{1..4}$'  |\t)' ;do sleep .1; echo >"$f" ;done
touch -d "1970-01-01" $'(\n4  |\t)'
ls -ltr '('?[1-4]'  |'?')'; echo
mkdir -p dest

Ecco lo snippet dei risultati del controllo

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*

+1, unica risposta utile prima della mia (ed è sempre bene avere dei dati di prova.)
Sorpigal

0

È più facile da fare con GNU find. Lo uso ogni giorno sul mio DVR Linux per eliminare le registrazioni dal mio sistema di videosorveglianza più vecchio di un giorno.

Ecco la sintassi:

find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;

Ricorda che finddefinisce un giorno come 24 ore dal momento dell'esecuzione. Pertanto i file modificati l'ultima volta alle 23:00 non verranno eliminati all'01: 00.

Puoi anche combinare findcon cron, in modo che le eliminazioni possano essere pianificate automaticamente eseguendo il seguente comando come root:

crontab -e << EOF
@daily /usr/bin/find /path/to/files/* -mtime +number_of_days -exec mv {} /path/to/folder \;
EOF

Puoi sempre ottenere maggiori informazioni findconsultando la sua pagina di manuale:

man find

0

poiché le altre risposte non corrispondono al mio e allo scopo delle domande, questa shell è testata su CentOS 7:

oldestDir=$(find /yourPath/* -maxdepth 0 -type d -printf '%T+ %p\n' | sort | head -n 1 | tr -s ' ' | cut -d ' ' -f 2)
echo "$oldestDir"
rm -rf "$oldestDir"
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.