Shell script để di chuyển các tập tin cũ nhất?


14

Làm cách nào để tôi viết một tập lệnh để di chuyển chỉ 20 tệp cũ nhất từ ​​thư mục này sang thư mục khác? Có cách nào để lấy các tập tin cũ nhất trong một thư mục không?


Bao gồm hay loại trừ các thư mục con? Và nó có nên được thực hiện đệ quy (trong cây thư mục) không?
maxschlepzig

2
Nhiều (hầu hết?) * Hệ thống tệp nix không lưu trữ ngày tạo, vì vậy bạn không thể xác định tệp cũ nhất một cách chắc chắn. Các thuộc tính có sẵn thông thường là atime(truy cập lần cuối), ctime(thay đổi quyền cuối cùng) và mtime(sửa đổi lần cuối) ... vd. ls -ttìm thấy của printf "%T" sử dụng mtime... Có vẻ như, theo liên kết này , mà tôi ext4phân vùng là khả năng xử lý một ngày tạo, nhưng lsfindstatkhông có các tùy chọn thích hợp (chưa) ...
Peter.O

Câu trả lời:


13

Phân tích đầu ra của lskhông đáng tin cậy .

Thay vào đó, sử dụng findđể xác định vị trí các tệp và sortsắp xếp chúng theo dấu thời gian. Ví dụ:

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)

Tất cả những điều này đang làm gì?

Đầu tiên, các findlệnh định vị tất cả các tệp và thư .mục trong thư mục hiện tại ( ), nhưng không nằm trong thư mục con của thư mục hiện tại ( -maxdepth 1), sau đó in ra:

  • Dấu thời gian
  • Một không gian
  • Đường dẫn tương đối đến tệp
  • Một nhân vật NULL

Dấu thời gian là quan trọng. Trình %T@xác định định dạng để -printfphân tích thành T, cho biết "Thời gian sửa đổi lần cuối" của tệp (mtime) và @, cho biết "Giây kể từ năm 1970", bao gồm cả giây phân đoạn.

Không gian chỉ là một dấu phân cách tùy ý. Đường dẫn đầy đủ đến tệp để chúng ta có thể tham chiếu sau và ký tự NULL là dấu kết thúc vì đây là ký tự không hợp lệ trong tên tệp và do đó cho chúng tôi biết rằng chúng tôi đã đến cuối đường dẫn đến tập tin.

Tôi đã bao gồm 2>/dev/nullđể các tệp mà người dùng không có quyền truy cập bị loại trừ, nhưng các thông báo lỗi về chúng bị loại trừ sẽ bị loại bỏ.

Kết quả của findlệnh là một danh sách tất cả các thư mục trong thư mục hiện tại. Danh sách này sortđược dẫn đến:

  • -z Coi NULL là ký tự kết thúc dòng thay vì dòng mới.
  • -n Sắp xếp số

Vì giây từ năm 1970 luôn tăng lên, chúng tôi muốn tệp có dấu thời gian là số nhỏ nhất. Kết quả đầu tiên từ sortsẽ là dòng chứa dấu thời gian được đánh số nhỏ nhất. Tất cả những gì còn lại là để trích xuất tên tập tin.

Các kết quả của find, sortđường ống được chuyển qua thay thế quá trình đến whilenơi nó được đọc như thể nó là một tệp trên stdin. whilelần lượt gọi readđể xử lý đầu vào.

Trong bối cảnh readchúng ta đặt IFSbiến thành không có gì, điều đó có nghĩa là khoảng trắng sẽ không được hiểu một cách không thích hợp là một dấu phân cách. readđược kể -r, giúp chặn đứng sự bành trướng thoát, và -d $'\0', mà làm cho end-of-line NULL delimiter, phù hợp với sản lượng từ của chúng tôi find, sortđường ống.

Đoạn dữ liệu đầu tiên, đại diện cho đường dẫn tệp cũ nhất trước dấu thời gian của nó và khoảng trắng, được đọc vào biến line. Tiếp theo, thay thế tham số được sử dụng với biểu thức #*, chỉ đơn giản là thay thế tất cả các ký tự từ đầu chuỗi cho đến khoảng trắng đầu tiên, bao gồm cả khoảng trắng, không có gì. Điều này loại bỏ dấu thời gian sửa đổi, chỉ để lại đường dẫn đầy đủ đến tệp.

Tại thời điểm này, tên tệp được lưu trữ $filevà bạn có thể làm bất cứ điều gì bạn muốn với nó. Khi bạn thực hiện xong làm điều gì đó với $filenhững whiletuyên bố sẽ lặp và các readlệnh sẽ được thực hiện một lần nữa, giải nén các đoạn tiếp theo và tên file tiếp theo.

Có cách nào đơn giản hơn không?

Không. Cách đơn giản hơn là lỗi.

Nếu bạn sử dụng ls -tvà ống để headhoặc tail(hoặc bất cứ điều gì ), bạn sẽ phá vỡ trên các tập tin với dòng mới trong tên tập tin. Nếu mv $(anything)sau đó các tệp có khoảng trắng trong tên sẽ gây ra vỡ. Nếu mv "$(anything)"sau đó bạn tập tin với dòng mới trong tên sẽ gây ra vỡ. Nếu bạn readkhông có -d $'\0'thì bạn sẽ phá vỡ các tệp có khoảng trắng trong tên của chúng.

Có lẽ trong các trường hợp cụ thể, bạn biết chắc chắn rằng một cách đơn giản hơn là đủ, nhưng bạn không bao giờ nên viết các giả định như vậy vào các tập lệnh nếu bạn có thể tránh làm như vậy.

Giải pháp

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

Gọi như:

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

Để di chuyển 20 tập tin cũ nhất từ /var/log/foo/đến /mnt/backup/.

Lưu ý rằng tôi đang bao gồm các tập tin thư mục. Đối với các tập tin chỉ thêm -type fvào findlời mời.

Cảm ơn

Nhờ enzotibПавел Танков cho các cải tiến cho câu trả lời này.


Các loại không nên sử dụng -n. Ít nhất là trong phiên bản của tôi, nó không sắp xếp các số thập phân một cách chính xác. Bạn có thể phải xóa dấu chấm trong ngày hoặc sử dụng -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rz, ngày ISO hoặc một cái gì đó khác.
l0b0

@ l0b0: Giới hạn này được biết đến với tôi. Tôi cho rằng nó không đủ để không yêu cầu mức độ chi tiết đó (nghĩa là sắp xếp vượt quá mức .phải liên quan đến bạn.) Sẽ rõ ràng hơn khi nói sort -z -n -t. -k1.
Sorpigal

@ l0b0: Giải pháp của bạn thể hiện cùng một lỗi, bất kể: %TScũng hiển thị "phần phân đoạn" có dạng 00.0000000000, vì vậy bạn cũng mất độ chi tiết. GNU gần đây sortcó thể giải quyết vấn đề này bằng cách sử dụng -V"sắp xếp phiên bản", sẽ xử lý loại dấu phẩy động này như mong đợi.
Sorpigal

Không, bởi vì tôi thực hiện sắp xếp chuỗi trên "YYYY-MM-DDThh: mm: ss" chứ không phải là sắp xếp số. Sắp xếp chuỗi không quan tâm đến số thập phân, vì vậy nó sẽ hoạt động cho đến năm 10000 :)
l0b0

@ l0b0: Sau đó, một chuỗi sắp xếp %T@cũng sẽ hoạt động, bởi vì nó là phần đệm không.
Sorpigal

4

Dễ dàng nhất trong zsh, nơi bạn có thể sử dụng Om vòng loại toàn cầu để sắp xếp các trận đấu theo ngày (cũ nhất trước) và [1,20]vòng loại chỉ giữ lại 20 trận đấu đầu tiên:

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

Thêm Dvòng loại nếu bạn cũng muốn bao gồm các tệp chấm. Thêm .nếu bạn muốn chỉ khớp các tệp thông thường và không phải thư mục.

Nếu bạn không có zsh, đây là một lớp lót Perl (bạn có thể thực hiện dưới 80 ký tự, nhưng với chi phí rõ ràng hơn):

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

Chỉ với các công cụ POSIX hoặc thậm chí bash hoặc ksh, sắp xếp các tệp theo ngày là một điều khó khăn. Bạn có thể thực hiện dễ dàng với ls, nhưng phân tích cú pháp đầu ra lslà có vấn đề, vì vậy điều này chỉ hoạt động nếu tên tệp chỉ chứa các ký tự có thể in khác với dòng mới.

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

4

Phối hợp ls -t đầu ra với tailhoặc head.

Ví dụ đơn giản, chỉ hoạt động nếu tất cả các tên tệp chỉ chứa các ký tự có thể in khác với khoảng trắng và \[*?:

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

1
Thêm tùy chọn -A vào ls:ls -1Atr
Arcege

1
-1, nguy hiểm. Ở đây hãy để tôi tạo một ví dụ : touch $'foo\n*'. Điều gì xảy ra nếu bạn thực thi mv "$ (ls)" với tệp đó đang ngồi ở đó?
Sorpigal

1
@Sorpigal Nghiêm túc? Thật là yếu đuối khi nói "Hãy để tôi đưa ra một ví dụ mà bạn nói cụ thể là sẽ không hoạt động. Này, nó không hoạt động"
Michael Mrozek

1
@Sorpigal Đó không phải là một ý tưởng tồi, nó hoạt động trong 99% trường hợp. Câu trả lời là "nếu bạn có các tệp có tên bình thường, thì điều này sẽ hoạt động. Nếu bạn là một người điên rồ nhúng các dòng mới vào tên tệp của họ, thì nó sẽ không". Điều đó hoàn toàn chính xác
Michael Mrozek

1
@MichaelMrozek: Đó là một ý tưởng tồi và nó tồi tệ vì đôi khi nó thất bại. Nếu đôi khi bạn có tùy chọn thực hiện những gì thất bại và những gì không, bạn nên chọn tùy chọn không (và tùy chọn không tốt). Làm bất cứ điều gì bạn thích tương tác, nhưng trong một tập tin kịch bản và khi đưa ra lời khuyên hãy làm điều đó một cách chính xác.
Sorpigal

2

Bạn có thể sử dụng GNU find cho việc này:

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

Trường hợp tìm thấy in thời gian sửa đổi (tính bằng giây từ năm 1970) và tên của từng tệp của thư mục hiện tại, đầu ra được sắp xếp theo trường đầu tiên, 20 cũ nhất được lọc và di chuyển đến dest_dir. Loại bỏ echonếu bạn đã kiểm tra dòng lệnh.


2

Không ai đã (chưa) đăng một ví dụ bash phục vụ cho các ký tự dòng mới được nhúng (nhúng bất cứ thứ gì) vào tên tệp, vì vậy đây là một ví dụ. Nó di chuyển 3 tập tin thông thường (mdate) cũ nhất

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

Đây là đoạn dữ liệu thử nghiệm

# 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

Đây là check-kết quả đoạn mã

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

+1, chỉ có câu trả lời hữu ích trước khi khai thác (và luôn luôn tốt khi có dữ liệu kiểm tra.)
Sorpigal

0

Đó là cách dễ nhất để làm với GNU find. Tôi sử dụng nó mỗi ngày trên DVR Linux của mình để xóa các bản ghi âm khỏi hệ thống giám sát video cũ hơn một ngày.

Đây là cú pháp:

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

Hãy nhớ rằng findđịnh nghĩa một ngày là 24 giờ kể từ thời điểm thực hiện. Do đó, các tệp được sửa đổi lần cuối lúc 11 giờ tối sẽ không bị xóa lúc 1 giờ sáng.

Bạn thậm chí có thể kết hợp findvới cron, vì vậy việc xóa có thể được lên lịch tự động bằng cách chạy lệnh sau dưới dạng root:

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

Bạn luôn có thể nhận thêm thông tin về việc findtham khảo trang hướng dẫn sử dụng:

man find

0

vì các câu trả lời khác không phù hợp với mục đích của tôi và câu hỏi, lớp vỏ này được thử nghiệm trên 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"
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.