Shell-Skript zum Verschieben der ältesten Dateien?

14

Wie schreibe ich ein Skript, um nur die 20 ältesten Dateien von einem Ordner in einen anderen zu verschieben? Gibt es eine Möglichkeit, die ältesten Dateien in einem Ordner zu erfassen?

user11598
quelle
Unterverzeichnisse einschließen oder ausschließen? Und sollte es rekursiv erfolgen (in einem Verzeichnisbaum)?
Maxschlepzig
2
In vielen (den meisten?) * Nix-Dateisystemen wird das Erstellungsdatum nicht gespeichert, sodass Sie die älteste Datei nicht mit Sicherheit ermitteln können. Die typischerweise verfügbaren Attribute sind atime(letzter Zugriff), ctime(letzte Berechtigungsänderung) und mtime(letzte Änderung) ... zB. ls -tund find's printf "%T" use mtime... Laut diesem Link sind meine ext4Partitionen in der Lage , ein Erstellungsdatum zu verarbeiten, aber lsund findund stathaben (noch) nicht die entsprechenden Optionen ...
Peter.O 22.10.11

Antworten:

13

Das Parsen der Ausgabe von lsist nicht zuverlässig .

Verwenden Sie stattdessen, findum die Dateien zu lokalisieren und sortnach Zeitstempel zu ordnen. Beispielsweise:

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)

Was macht das alles?

Zuerst findsucht der Befehl alle Dateien und Verzeichnisse im aktuellen Verzeichnis ( .), jedoch nicht in den Unterverzeichnissen des aktuellen Verzeichnisses ( -maxdepth 1), und gibt dann Folgendes aus:

  • Ein Zeitstempel
  • Ein Leerzeichen
  • Der relative Pfad zur Datei
  • Ein NULL-Zeichen

Der Zeitstempel ist wichtig. Der %T@Formatbezeichner für -printfzerfällt in T"Letzte Änderungszeit" der Datei (mtime) und @"Sekunden seit 1970", einschließlich Sekundenbruchteile.

Das Leerzeichen ist lediglich ein beliebiger Begrenzer. Der vollständige Pfad zur Datei ist so, dass wir später darauf verweisen können, und das NULL-Zeichen ist ein Abschlusszeichen, da es sich um ein unzulässiges Zeichen in einem Dateinamen handelt. Auf diese Weise können wir sicher sein, dass wir das Ende des Pfads zur Datei erreicht haben Datei.

Ich habe eingeschlossen, 2>/dev/nulldamit Dateien, auf die der Benutzer keine Zugriffsberechtigung hat, ausgeschlossen werden, aber Fehlermeldungen darüber, dass sie ausgeschlossen werden, unterdrückt werden.

Das Ergebnis des findBefehls ist eine Liste aller Verzeichnisse im aktuellen Verzeichnis. Die Liste wird an Folgendes weitergeleitet sort:

  • -z Behandle NULL als Zeilenendezeichen anstelle von Newline.
  • -n Numerisch sortieren

Da die Sekunden seit 1970 immer höher sind, möchten wir die Datei, deren Zeitstempel die kleinste Zahl war. Das erste Ergebnis von sortist die Zeile mit dem kleinsten nummerierten Zeitstempel. Alles was bleibt ist, den Dateinamen zu extrahieren.

Die Ergebnisse der find, sortPipeline wird über geleitet Prozess Substitution zu , whilewo es gelesen wird , als ob es eine Datei auf stdin waren. whileRuft wiederum auf read, um die Eingabe zu verarbeiten.

Im Kontext von setzen readwir die IFSVariable auf nichts, was bedeutet, dass Leerzeichen nicht unangemessen als Trennzeichen interpretiert werden. readerzählt wird -r, welche escape Expansions deaktiviert, und -d $'\0', was die End-of-line - Trennzeichen NULL, Anpassen der Ausgabe aus unserer macht find, sortPipeline.

Der erste Datenblock, der den ältesten Dateipfad mit vorangestelltem Zeitstempel und Leerzeichen darstellt, wird in die Variable eingelesen line. Als nächstes wird die Parameterersetzung mit dem Ausdruck verwendet #*, der einfach alle Zeichen vom Anfang der Zeichenfolge bis zum ersten Leerzeichen, einschließlich des Leerzeichens, durch nichts ersetzt. Dadurch wird der Änderungszeitstempel entfernt, und es verbleibt nur der vollständige Pfad zur Datei.

Zu diesem Zeitpunkt ist der Dateiname in gespeichert $fileund Sie können alles tun, was Sie wollen. Wenn Sie mit $fileder whileAnweisung fertig sind, wird eine Schleife ausgeführt, und der readBefehl wird erneut ausgeführt, wobei der nächste Block und der nächste Dateiname extrahiert werden.

Gibt es nicht einen einfacheren Weg?

Einfachere Wege sind fehlerhaft.

Wenn Sie eine ls -tPipe zu headoder tail(oder etwas anderem ) verwenden, brechen Sie bei Dateien mit Zeilenumbrüchen in den Dateinamen ab. Wenn Sie mv $(anything)dann Dateien mit Leerzeichen im Namen verursachen Bruch. Wenn Sie mv "$(anything)"dann Dateien mit nachgestellten Zeilenumbrüchen im Namen erstellen, wird dies zu einem Bruch führen. Wenn Sie dies readnicht -d $'\0'tun, brechen Sie bei Dateien mit Leerzeichen im Namen ab.

Vielleicht wissen Sie in bestimmten Fällen, dass ein einfacherer Weg ausreicht, aber Sie sollten solche Annahmen niemals in Skripte schreiben, wenn Sie dies vermeiden können.

Lösung

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

Rufen Sie an wie:

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

Verschieben der ältesten 20 Dateien von /var/log/foo/nach /mnt/backup/.

Beachten Sie, dass ich Dateien und Verzeichnisse einbinde. Für Dateien nur -type fzum findAufruf hinzufügen .

Vielen Dank

Vielen Dank an Enzotib und Павел Танков für die Verbesserung dieser Antwort.

Sorpigal
quelle
Die Art sollte nicht verwendet werden -n. Zumindest in meiner Version werden Dezimalzahlen nicht richtig sortiert. Sie müssen entweder den Punkt im Datum entfernen oder -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rzISO-Daten oder etwas anderes verwenden.
l0b0
@ l0b0: Diese Einschränkung ist mir bekannt. Ich gehe davon aus, dass es ausreicht, dieses Maß an Granularität nicht zu fordern (das heißt, das Sortieren über das .Muss hinaus ist für Sie irrelevant.) Es wäre klarer zu sagen sort -z -n -t. -k1.
Sorpigal
@ l0b0: Ihre Lösung weist den gleichen Fehler auf, unabhängig davon: %TSZeigt auch einen "Bruchteil" an, der in der Form wäre 00.0000000000, sodass Sie auch die Granularität verlieren. Neuere GNU- Versionen sortkönnten dieses Problem lösen, indem sie -Veine "Versionssortierung" verwenden, die diesen Gleitkommatyp erwartungsgemäß handhabt.
Sorpigal
Nein, weil ich eine Zeichenfolgensortierung für "JJJJ-MM-TTThh: mm: ss" anstelle einer numerischen Sortierung durchführe. Die
Zeichenfolgensortierung
@ l0b0: Eine String-Sortierung %T@würde dann auch funktionieren, da sie mit Nullen aufgefüllt ist.
Sorpigal
4

Am einfachsten ist es in zsh, wo Sie das Om Glob-Qualifikationsmerkmal verwenden können , um Übereinstimmungen nach Datum (älteste zuerst) zu sortieren, und das [1,20]Qualifikationsmerkmal, um nur die ersten 20 Übereinstimmungen beizubehalten:

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

Fügen Sie das DQualifikationsmerkmal hinzu, wenn Sie auch Punktdateien einschließen möchten. Hinzufügen, .wenn Sie nur normale Dateien und keine Verzeichnisse abgleichen möchten.

Wenn Sie kein zsh haben, ist hier ein Perl-Einzeiler (Sie können es in weniger als 80 Zeichen, aber mit einem weiteren Aufwand an Klarheit tun):

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

Mit nur POSIX-Tools oder sogar bash oder ksh ist das Sortieren von Dateien nach Datum ein Problem. Sie können dies problemlos tun ls, aber das Parsen der Ausgabe von lsist problematisch. Daher funktioniert dies nur, wenn die Dateinamen nur druckbare Zeichen enthalten, die keine Zeilenumbrüche sind.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Gilles 'SO - hör auf böse zu sein'
quelle
4

ls -tAusgabe mit tailoder kombinieren head.

Einfaches Beispiel, das nur funktioniert, wenn alle Dateinamen nur druckbare Zeichen außer Leerzeichen enthalten und \[*?:

 mv $(ls -1tr | head -20) other_folder
ktf
quelle
1
Fügen Sie die Option -A zu ls hinzu:ls -1Atr
Arcege 15.10.11
1
-1, gefährlich. Hier lassen Sie mich ein Beispiel Handwerk: touch $'foo\n*'. Was passiert, wenn Sie mv "$ (ls)" ausführen, während sich diese Datei dort befindet?
Sorpigal
1
@Sorpigal Ernsthaft? Es ist ein bisschen schwach zu sagen: "Lassen Sie mich ein Beispiel nennen, von dem Sie ausdrücklich gesagt haben, dass es nicht funktioniert. Hey, sieh mal, es funktioniert nicht."
Michael Mrozek
1
@Sorpigal Es ist keine schlechte Idee, es funktioniert in 99% der Fälle. Die Antwort lautet: "Wenn Sie Dateien mit normalen Namen haben, funktioniert dies. Wenn Sie eine verrückte Person sind, die Zeilenumbrüche in ihre Dateinamen einbettet, funktioniert dies nicht." Das ist völlig richtig
Michael Mrozek
1
@MichaelMrozek: Es ist eine schlechte Idee und es ist schlecht, weil es manchmal fehlschlägt. Wenn Sie die Möglichkeit haben, das zu tun, was manchmal fehlschlägt und das nicht, sollten Sie die Option wählen, die dies nicht tut (und die, die dies tut, ist schlecht). Tun Sie interaktiv, was immer Sie möchten, aber in einer Skriptdatei und wenn Sie Ratschläge geben, tun Sie es richtig.
Sorpigal
2

Sie können GNU find dafür verwenden:

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

Wenn find die Änderungszeit (in Sekunden ab 1970) und den Namen jeder Datei des aktuellen Verzeichnisses ausgibt, wird die Ausgabe nach dem ersten Feld sortiert, die 20 ältesten werden gefiltert und nach verschoben dest_dir. Entfernen Sie das, echowenn Sie die Befehlszeile getestet haben.

maxschlepzig
quelle
2

Niemand hat (noch) ein Bash-Beispiel gepostet, das eingebettete Zeilenumbrüche (eingebettete Elemente) im Dateinamen berücksichtigt. Es werden die 3 ältesten (mdate) regulären Dateien verschoben

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

Dies ist das Testdaten- Snippet

# 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

Hier ist die Check-Ergebnisse Snippet

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
quelle
+1, nur nützliche Antwort vor mir (und es ist immer gut, Testdaten zu haben.)
Sorpigal
0

Es ist am einfachsten mit GNU zu tun find. Ich verwende es jeden Tag auf meinem Linux-DVR, um Aufzeichnungen von meinem Videoüberwachungssystem zu löschen, die älter als ein Tag sind.

Hier ist die Syntax:

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

Denken Sie daran, dass findein Tag als 24 Stunden ab dem Zeitpunkt der Ausführung definiert wird. Daher werden Dateien, die zuletzt um 23 Uhr geändert wurden, nicht um 1 Uhr morgens gelöscht.

Sie können sogar kombinieren findmit cron, kann so Löschungen automatisch durch den folgenden Befehl als root ausgeführt wird, statt:

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

Weitere Informationen erhalten Sie immer auf findder Manualpage:

man find
Jonathan Frank
quelle
0

Da die anderen Antworten nicht zu meinem und dem Zweck der Fragen passen, wird diese Shell unter CentOS 7 getestet:

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"
Spektakulatius
quelle