Script shell untuk memindahkan file tertua?

14

Bagaimana cara menulis skrip untuk memindahkan hanya 20 file tertua dari satu folder ke folder lain? Apakah ada cara untuk mengambil file tertua di folder?

pengguna11598
sumber
Termasuk atau tidak termasuk subdirektori? Dan haruskah itu dilakukan secara rekursif (di pohon direktori)?
maxschlepzig
2
Banyak (kebanyakan?) * Sistem file nix tidak menyimpan tanggal pembuatan, sehingga Anda tidak dapat menentukan file yang paling tua dengan pasti. Atribut yang biasanya tersedia adalah atime(akses terakhir), ctime(perubahan izin terakhir), dan mtime(terakhir dimodifikasi) ... misalnya. ls -tdan temukan printf "%T" penggunaannya mtime... Sepertinya, menurut tautan ini , bahwa ext4partisi saya mampu menangani tanggal pembuatan, tetapi lsdan finddan dan statbelum memiliki opsi yang sesuai (belum) ...
Peter.O

Jawaban:

13

Parsing output lsadalah tidak dapat diandalkan .

Sebagai gantinya, gunakan finduntuk mencari file dan sortmemesannya dengan stempel waktu. Sebagai contoh:

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)

Apa yang sedang dilakukan semua ini?

Pertama, findperintah menempatkan semua file dan direktori di direktori saat ini ( .), tetapi tidak di subdirektori dari direktori saat ini ( -maxdepth 1), kemudian mencetak:

  • Stempel waktu
  • Sebuah ruang
  • Jalur relatif ke file
  • Karakter NULL

Stempel waktu itu penting. Penentu %T@format untuk -printfdipecah menjadi T, yang menunjukkan "Waktu modifikasi terakhir" dari file (mtime) dan @, yang menunjukkan "Detik sejak 1970", termasuk detik pecahan.

Ruang hanyalah pembatas yang sewenang-wenang. Path lengkap ke file adalah agar kita dapat merujuknya nanti, dan karakter NULL adalah terminator karena itu adalah karakter ilegal dalam nama file dan dengan demikian memberi tahu kami bahwa kami mencapai ujung path ke mengajukan.

Saya telah menyertakan 2>/dev/nullagar file yang pengguna tidak memiliki izin untuk mengaksesnya dikecualikan, tetapi pesan kesalahan tentang mereka yang dikecualikan ditekan.

Hasil dari findperintah adalah daftar semua direktori di direktori saat ini. Daftar ini disalurkan ke sortyang diperintahkan untuk:

  • -z Perlakukan NULL sebagai karakter terminator garis alih-alih baris baru.
  • -n Sortir secara numerik

Karena detik-sejak-1970 selalu naik, kami ingin file yang cap waktu-nya adalah angka terkecil. Hasil pertama dari sortakan menjadi garis yang berisi stempel waktu bernomor terkecil. Yang tersisa hanyalah mengekstrak nama file.

Hasil dari find, sortpipeline dilewatkan melalui proses substitusi ke whiletempat itu dibaca seolah-olah itu adalah file di stdin. whilepada gilirannya memanggil readuntuk memproses input.

Dalam konteks readkami mengatur IFSvariabel menjadi tidak ada, yang berarti spasi tidak akan ditafsirkan secara tidak tepat sebagai pembatas. readdikatakan -r, yang menonaktifkan ekspansi, dan -d $'\0', yang membuat pembatas akhir-line NULL, cocok dengan output dari find, sortpipeline kami.

Potongan data pertama, yang mewakili jalur file tertua yang didahului oleh stempel waktu dan spasinya, dibaca ke dalam variabel line. Selanjutnya, substitusi parameter digunakan dengan ekspresi #*, yang hanya mengganti semua karakter dari awal string hingga spasi pertama, termasuk spasi, tanpa spasi. Ini menghilangkan tanda waktu modifikasi, hanya menyisakan path lengkap ke file.

Pada titik ini nama file disimpan $filedan Anda dapat melakukan apa pun yang Anda suka dengannya. Setelah selesai melakukan sesuatu dengan $filepara whilepernyataan kehendak loop dan readperintah akan dieksekusi lagi, penggalian bongkahan berikutnya dan nama file selanjutnya.

Apakah tidak ada cara yang lebih sederhana?

Tidak. Cara-cara yang lebih sederhana itu buggy.

Jika Anda menggunakan ls -tdan mem-pipe ke headatau tail(atau apa saja ) Anda akan merusak file dengan baris baru dalam nama file. Jika Anda mv $(anything)kemudian file dengan spasi putih dalam nama akan menyebabkan kerusakan. Jika Anda mv "$(anything)"kemudian file dengan trailing baris baru pada nama akan menyebabkan kerusakan. Jika Anda readtanpa -d $'\0'maka Anda akan merusak file dengan spasi putih di nama mereka.

Mungkin dalam kasus-kasus tertentu Anda tahu pasti bahwa cara yang lebih sederhana sudah cukup, tetapi Anda tidak boleh menulis asumsi seperti itu di skrip jika Anda dapat menghindari melakukannya.

Larutan

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

Sebut seperti:

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

Untuk memindahkan 20 file terlama dari /var/log/foo/ke /mnt/backup/.

Perhatikan bahwa saya termasuk file dan direktori. Untuk file hanya menambah -type fke finddoa.

Terima kasih

Terima kasih kepada enzotib dan Павел Танков untuk peningkatan pada jawaban ini.

Sorpigal
sumber
Semacam itu tidak boleh digunakan -n. Setidaknya dalam versi saya, itu tidak mengurutkan angka desimal dengan benar. Anda juga harus menghapus titik di tanggal atau penggunaan -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz, tanggal ISO, atau yang lainnya.
l0b0
@ l0b0: Keterbatasan ini diketahui oleh saya. Saya kira cukup untuk tidak membutuhkan tingkat granularitas (yaitu, memilah di luar .harus tidak relevan bagi Anda.) Akan lebih jelas untuk mengatakan sort -z -n -t. -k1.
Sorpigal
@ l0b0: Solusi Anda menunjukkan bug yang sama, terlepas: %TSjuga menunjukkan "bagian pecahan" yang akan berada dalam formulir 00.0000000000, sehingga Anda juga kehilangan rincian. GNU baru-baru ini sortdapat menyelesaikan masalah ini dengan menggunakan -Vuntuk "versi sortir", yang akan menangani jenis floating point seperti yang diharapkan.
Sorpigal
Tidak, karena saya melakukan pengurutan string pada "YYYY-MM-DDThh: mm: ss" daripada jenis numerik. Semacam string tidak peduli dengan desimal, jadi itu harus bekerja sampai tahun 10000 :)
l0b0
@ l0b0: Semacam string %T@juga akan berfungsi, karena itu adalah nol-empuk.
Sorpigal
4

Paling mudah di zsh, di mana Anda dapat menggunakan Om kualifikasi glob untuk mengurutkan pertandingan berdasarkan tanggal (tertua lebih dulu) dan [1,20]kualifikasi untuk mempertahankan hanya 20 pertandingan pertama:

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

Tambahkan Dkualifikasi jika Anda ingin menyertakan file dot juga. Tambahkan .jika Anda hanya ingin mencocokkan file biasa dan bukan direktori.

Jika Anda tidak memiliki zsh, inilah Perl one-liner (Anda dapat melakukannya dalam kurang dari 80 karakter, tetapi dengan biaya lebih lanjut dalam kejelasan):

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

Dengan hanya alat POSIX atau bahkan bash atau ksh, menyortir file berdasarkan tanggal adalah hal yang menyakitkan. Anda dapat melakukannya dengan mudah ls, tetapi penguraian outputnya lsbermasalah, jadi ini hanya berfungsi jika nama file hanya berisi karakter yang dapat dicetak selain baris baru.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Gilles 'SANGAT berhenti menjadi jahat'
sumber
4

Gabungkan ls -toutput dengan tailatau head.

Contoh sederhana, yang hanya berfungsi jika semua nama file hanya berisi karakter yang dapat dicetak selain spasi putih dan \[*?:

 mv $(ls -1tr | head -20) other_folder
ktf
sumber
1
Tambahkan opsi -A ke ls:ls -1Atr
Arcege
1
-1, berbahaya. Di sini saya kerajinan contoh: touch $'foo\n*'. Apa yang terjadi jika Anda menjalankan mv "$ (ls)" dengan file yang ada di sana?
Sorpigal
1
@ Kaligal Serius? Agak lemah untuk mengatakan, "Biarkan saya memberikan contoh yang Anda katakan secara spesifik tidak akan berhasil. Hei, lihat, itu tidak berhasil"
Michael Mrozek
1
@ Kaligal Ini bukan ide yang buruk, ia bekerja di 99% kasus. Jawabannya adalah "jika Anda memiliki file dengan nama normal, ini berfungsi. Jika Anda adalah orang gila yang menyematkan baris baru dalam nama file mereka, itu tidak akan". Itu sepenuhnya benar
Michael Mrozek
1
@MichaelMrozek: Ini adalah ide yang buruk dan itu buruk karena kadang-kadang gagal. Jika Anda memiliki opsi untuk melakukan apa yang kadang-kadang gagal dan yang tidak, Anda harus mengambil opsi yang tidak (dan yang tidak buruk). Lakukan apa pun yang Anda suka secara interaktif, tetapi dalam file skrip dan ketika memberikan saran lakukan dengan benar.
Sorpigal
2

Anda dapat menggunakan GNU find untuk ini:

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

Di mana menemukan mencetak waktu modifikasi (dalam detik dari tahun 1970) dan nama setiap file dari direktori saat ini, output diurutkan sesuai dengan bidang pertama, 20 yang tertua disaring dan dipindahkan ke dest_dir. Hapus echojika Anda telah menguji baris perintah.

maxschlepzig
sumber
2

Belum ada yang memposting contoh bash yang melayani chars baris baru tertanam (embedded anything) di nama file, jadi inilah salah satu. Ini memindahkan 3 file biasa tertua (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

Ini adalah cuplikan uji-data

# 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

Berikut ini cuplikan hasil-cek

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
sumber
+1, satu-satunya jawaban yang berguna sebelum saya (dan itu selalu baik untuk memiliki data uji.)
Sorpigal
0

Ini paling mudah dilakukan dengan GNU find. Saya menggunakannya setiap hari di DVR Linux saya untuk menghapus rekaman dari sistem pengawasan video saya lebih lama dari satu hari.

Berikut ini sintaksnya:

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

Ingat bahwa findmendefinisikan satu hari sebagai 24 jam dari waktu eksekusi. Karenanya file yang terakhir dimodifikasi pada jam 11 malam tidak akan dihapus pada jam 1 pagi.

Anda bahkan dapat menggabungkan finddengan cron, jadi penghapusan dapat dijadwalkan secara otomatis dengan menjalankan perintah berikut sebagai root:

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

Anda selalu dapat memperoleh informasi lebih lanjut finddengan membaca halaman buku panduannya:

man find
Jonathan Frank
sumber
0

karena jawaban lain tidak sesuai dengan tujuan saya dan pertanyaan, shell ini diuji pada 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
sumber