Skrypt powłoki do przenoszenia najstarszych plików?

14

Jak napisać skrypt do przenoszenia tylko 20 najstarszych plików z jednego folderu do drugiego? Czy istnieje sposób na pobranie najstarszych plików w folderze?

użytkownik11598
źródło
Włącza lub wyłącza podkatalogi? I czy należy to robić rekurencyjnie (w drzewie katalogów)?
maxschlepzig
2
Wiele (większość?) * Systemów plików nix nie przechowuje daty utworzenia, więc nie można z całą pewnością ustalić najstarszego pliku. Typowo dostępne atrybuty to atime(ostatni dostęp), ctime(ostatnia zmiana uprawnień) i mtime(ostatnia modyfikacja) ... np. ls -ti znaleźć za printf "%T" korzystanie mtime... Wydaje się, zgodnie z tym linkiem , że moje ext4partycje są w stanie obsłużyć daty utworzenia, ale lsi find, a statnie mają odpowiednie opcje (jeszcze) ...
Peter.O

Odpowiedzi:

13

Analiza wyniku niels jest wiarygodna .

Zamiast tego użyj finddo zlokalizowania plików i sortuporządkowania ich według datownika. Na przykład:

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)

Co to wszystko robi?

Najpierw findpolecenia lokalizuje wszystkie pliki i katalogi w bieżącym katalogu ( .), ale nie w podkatalogach bieżącego katalogu ( -maxdepth 1), a następnie drukuje:

  • Znacznik czasu
  • Przestrzeń
  • Względna ścieżka do pliku
  • Znak NULL

Znacznik czasu jest ważny. Specyfikator %T@formatu dla -printfpodziału na T, który wskazuje „czas ostatniej modyfikacji” pliku (mtime) i @, który wskazuje „sekundy od 1970 roku”, w tym ułamkowe sekundy.

Przestrzeń jest jedynie arbitralnym ogranicznikiem. Pełna ścieżka do pliku jest taka, że ​​możemy się do niego później odwoływać, a znak NULL jest terminatorem, ponieważ jest to niedozwolony znak w nazwie pliku, a zatem daje nam pewność, że dotarliśmy do końca ścieżki do plik.

Podałem 2>/dev/nulltak, aby pliki, do których użytkownik nie ma uprawnień dostępu, zostały wykluczone, ale komunikaty o błędach ich wykluczenia są pomijane.

Wynikiem findpolecenia jest lista wszystkich katalogów w bieżącym katalogu. Lista jest przesyłana potokowo, do sortktórej należy:

  • -z Traktuj NULL jako znak końca linii zamiast znaku nowej linii.
  • -n Sortuj numerycznie

Ponieważ sekundy od 1970 roku zawsze się zwiększają, chcemy pliku, którego znacznik czasu był najmniejszy. Pierwszym wynikiem sortbędzie linia zawierająca najmniejszy numer znacznika czasu. Pozostaje tylko wyodrębnić nazwę pliku.

Wyniki find, sortrurociąg przechodzi przez substytucji procesowej na whilektórym jest czytać tak, jakby był plik na stdin. whilez kolei wywołuje readprzetwarzanie danych wejściowych.

W kontekście readustawiamy IFSzmienną na nic, co oznacza, że ​​białe znaki nie będą niewłaściwie interpretowane jako separator. readdowiaduje się -r, co blokuje ekspansję ucieczki, i -d $'\0', co sprawia, że wycofanego z linii ogranicznika NULL, pasujące wyjście z naszej find, sortrurociągu.

Pierwszy fragment danych, który reprezentuje najstarszą ścieżkę pliku poprzedzoną znacznikiem czasu i spacją, jest wczytywany do zmiennej line. Następnie do wyrażenia stosuje się podstawienie parametrów#* , które po prostu zastępuje wszystkie znaki od początku łańcucha do pierwszej spacji, w tym spacji, niczym. To usuwa znacznik czasu modyfikacji, pozostawiając tylko pełną ścieżkę do pliku.

W tym momencie nazwa pliku jest przechowywana $filei możesz z nim zrobić, co chcesz. Kiedy skończysz robić coś z $filetym whileoświadczenie będzie pętli, a readpolecenie zostanie ponownie wykonany, wydobycia następny i następny kawałek nazwy pliku.

Czy nie ma prostszego sposobu?

Nie. Prostsze sposoby są wadliwe.

Jeśli użyjesz ls -ti potokujesz do headlub tail(lub cokolwiek ), będziesz łamał pliki z nowymi liniami w nazwach plików. Jeśli mv $(anything)następnie pliki z białą spacją w nazwie spowodują uszkodzenie. Jeśli mv "$(anything)"następnie pliki z końcowymi znakami nowej nazwy spowodują uszkodzenie. Jeśli tak readnie -d $'\0'jest, włamujesz się do plików ze spacjami w ich nazwach.

Być może w szczególnych przypadkach wiesz na pewno, że prostszy sposób jest wystarczający, ale nigdy nie powinieneś zapisywać takich założeń w skryptach, jeśli możesz tego uniknąć.

Rozwiązanie

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

Zadzwoń jak:

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

Aby przenieść najstarsze 20 plików z /var/log/foo/do /mnt/backup/.

Pamiętaj, że dołączam pliki i katalogi. W przypadku plików dodaj tylko -type fdo findwywołania.

Dzięki

Dzięki enzotib i Павел Танков za ulepszenia tej odpowiedzi.

Sorpigal
źródło
Sortowania nie należy używać -n. Przynajmniej w mojej wersji nie porządkuje liczb dziesiętnych. Musisz usunąć kropkę w dacie lub użyć -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz, dat ISO lub czegoś innego.
l0b0
@ l0b0: To ograniczenie jest mi znane. Zakładam, że wystarczy nie wymagać tego poziomu szczegółowości (to znaczy, że sortowanie poza tym .musi być dla ciebie nieistotne). Można by to powiedzieć lepiej sort -z -n -t. -k1.
Sorpigal
@ l0b0: Twoje rozwiązanie wykazuje ten sam błąd, niezależnie od tego: %TSpokazuje również „część ułamkową”, która byłaby w formie 00.0000000000, więc tracisz również ziarnistość. Najnowszy GNU sortmoże rozwiązać ten problem, używając -V„sortowania wersji”, który będzie obsługiwał tego typu zmiennoprzecinkowe zgodnie z oczekiwaniami.
Sorpigal,
Nie, ponieważ wykonuję sortowanie ciągów według „RRRR-MM-DDTgg: mm: ss” zamiast sortowania numerycznego. Sortowanie ciągów nie przejmuje się
liczbami
@ l0b0: Sortowanie ciągów %T@też by działało, ponieważ jest wypełnione zerami.
Sorpigal,
4

Najłatwiej jest w Zsh, gdzie można użyć Om kwalifikatora globalnego do sortowania dopasowań według daty (od najstarszych) i [1,20]kwalifikatora, aby zachować tylko 20 pierwszych dopasowań:

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

Dodaj Dkwalifikator, jeśli chcesz dołączyć również pliki kropek. Dodaj, .jeśli chcesz dopasować tylko zwykłe pliki, a nie katalogi.

Jeśli nie masz Zsh, oto linijka Perla (możesz to zrobić z mniej niż 80 znaków, ale bardziej kosztownie):

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

Przy użyciu tylko narzędzi POSIX, a nawet bash lub ksh, sortowanie plików według daty jest uciążliwe. Możesz to łatwo zrobić ls, ale analizowanie wyniku lsjest problematyczne, więc działa to tylko wtedy, gdy nazwy plików zawierają tylko znaki drukowalne inne niż znaki nowej linii.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Gilles „SO- przestań być zły”
źródło
4

Połącz ls -twyjście z taillub head.

Prosty przykład, który działa tylko wtedy, gdy wszystkie nazwy plików zawierają tylko znaki drukowalne inne niż białe znaki i \[*?:

 mv $(ls -1tr | head -20) other_folder
ktf
źródło
1
Dodaj opcję -A do ls:ls -1Atr
Arcege
1
-1, niebezpieczne. Tu pozwolę sobie spreparować przykład: touch $'foo\n*'. Co się stanie, jeśli wykonasz mv „$ (ls)” z tym plikiem tam?
Sorpigal
1
@Sorpigal poważnie? Słabo jest powiedzieć „Pozwólcie, że wymyślę przykład, który konkretnie powiedziałeś, że nie zadziała. Hej, nie działa”
Michał Mrożek
1
@Sorpigal To nie jest zły pomysł, działa w 99% przypadków. Odpowiedź brzmi: „jeśli masz pliki o normalnych nazwach, to działa. Jeśli jesteś szaloną osobą, która osadza nowe linie w swoich nazwach plików, nie będzie”. To całkowicie poprawne
Michael Mrozek
1
@MichaelMrozek: To zły pomysł i jest zły, ponieważ czasami zawodzi. Jeśli masz opcję robienia tego, co czasami się nie udaje, a co nie, powinieneś wybrać opcję, która tego nie robi (a ta, która robi źle). Rób co chcesz interaktywnie, ale w pliku skryptu i udzielając porady, rób to poprawnie.
Sorpigal
3

Możesz użyć do tego GNU find:

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

Gdzie find wypisuje czas modyfikacji (w sekundach od 1970) i ​​nazwę każdego pliku bieżącego katalogu, dane wyjściowe są sortowane według pierwszego pola, 20 najstarszych jest filtrowanych i przenoszonych do dest_dir. Usuń, echojeśli testowałeś wiersz poleceń.

maxschlepzig
źródło
2

Nikt (jeszcze) nie opublikował przykładu bash, który obsługuje osadzone znaki nowej linii (osadzone cokolwiek) w nazwie pliku, więc oto jeden. Przenosi 3 najstarsze (mdate) zwykłe pliki

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

To jest fragment danych testowych

# 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

Oto fragment wyników kontroli

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
źródło
+1, tylko przydatna odpowiedź przed moją (i zawsze dobrze jest mieć dane testowe.)
Sorpigal
0

Najłatwiej jest to zrobić z GNU find. Używam go każdego dnia w moim rejestratorze Linux do usuwania nagrań z mojego systemu nadzoru wideo starszego niż jeden dzień.

Oto składnia:

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

Pamiętaj, że findokreśla dzień jako 24 godziny od momentu wykonania. Dlatego pliki ostatnio zmodyfikowane o 23:00 nie zostaną usunięte o 1 w nocy.

Możesz nawet łączyć się findz cron, więc usuwanie można zaplanować automatycznie, uruchamiając następującą komendę jako root:

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

Zawsze możesz uzyskać więcej informacji find, odwiedzając jego stronę podręcznika:

man find
Jonathan Frank
źródło
0

ponieważ inne odpowiedzi nie pasują do mojej i celu pytania, ta powłoka jest testowana na 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"
Spektakulatius
źródło