Das Parsen der Ausgabe von ls
ist nicht zuverlässig .
Verwenden Sie stattdessen, find
um die Dateien zu lokalisieren und sort
nach 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 find
sucht 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 -printf
zerfä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/null
damit Dateien, auf die der Benutzer keine Zugriffsberechtigung hat, ausgeschlossen werden, aber Fehlermeldungen darüber, dass sie ausgeschlossen werden, unterdrückt werden.
Das Ergebnis des find
Befehls 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 sort
ist die Zeile mit dem kleinsten nummerierten Zeitstempel. Alles was bleibt ist, den Dateinamen zu extrahieren.
Die Ergebnisse der find
, sort
Pipeline wird über geleitet Prozess Substitution zu , while
wo es gelesen wird , als ob es eine Datei auf stdin waren. while
Ruft wiederum auf read
, um die Eingabe zu verarbeiten.
Im Kontext von setzen read
wir die IFS
Variable auf nichts, was bedeutet, dass Leerzeichen nicht unangemessen als Trennzeichen interpretiert werden. read
erzählt wird -r
, welche escape Expansions deaktiviert, und -d $'\0'
, was die End-of-line - Trennzeichen NULL, Anpassen der Ausgabe aus unserer macht find
, sort
Pipeline.
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 $file
und Sie können alles tun, was Sie wollen. Wenn Sie mit $file
der while
Anweisung fertig sind, wird eine Schleife ausgeführt, und der read
Befehl 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 -t
Pipe zu head
oder 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 read
nicht -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 f
zum find
Aufruf hinzufügen .
Vielen Dank
Vielen Dank an Enzotib und Павел Танков für die Verbesserung dieser Antwort.