Сценарий оболочки для перемещения старых файлов?

14

Как мне написать скрипт для перемещения только 20 самых старых файлов из одной папки в другую? Есть ли способ получить самые старые файлы в папке?

user11598
источник
Включая или исключая подкаталоги? И должно ли это быть сделано рекурсивно (в дереве каталогов)?
maxschlepzig
2
Многие (большинство?) * Nix файловые системы не хранят дату создания, поэтому вы не можете с уверенностью определить самый старый файл. Обычно доступны следующие атрибуты atime(последний доступ), ctime(последнее изменение разрешения) и mtime(последнее изменение) ... например. ls -tи ФАЙНД printf "%T" использование mtime... Кажется, по этой ссылке , чтобы мои ext4перегородки способны обрабатывать дату создания, но lsи findи statне соответствующие параметры (пока) ...
Peter.O

Ответы:

13

Синтаксический вывод lsявляется не надежным .

Вместо этого используйте, findчтобы найти файлы и sortупорядочить их по метке времени. Например:

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)

Что все это делает?

Сначала findкоманды находят все файлы и каталоги в текущем каталоге ( .), но не в подкаталогах текущего каталога ( -maxdepth 1), затем распечатывают:

  • Метка времени
  • Пространство
  • Относительный путь к файлу
  • NULL символ

Отметка времени важна. Спецификатор %T@формата для -printfразбивки на T, который указывает «Время последнего изменения» файла (mtime) и @, который указывает «Секунды с 1970 года», включая доли секунды.

Пространство - это просто произвольный разделитель. Полный путь к файлу таков, что мы можем обратиться к нему позже, и символ NULL является терминатором, поскольку он является недопустимым символом в имени файла и, таким образом, позволяет нам точно знать, что мы достигли конца пути к файл.

Я включил 2>/dev/nullтак, что файлы, к которым у пользователя нет прав доступа, исключаются, но сообщения об ошибках об их исключении исключаются.

Результатом findкоманды является список всех каталогов в текущем каталоге. Список передается по трубопроводу, sortкоторому поручено:

  • -z Обрабатывайте NULL как символ конца строки вместо новой строки.
  • -n Сортировать численно

Так как секунды с 1970 года всегда идут вверх, мы хотим файл, метка времени которого была наименьшим числом. Первым результатом sortбудет строка, содержащая метку времени с наименьшим номером. Осталось только извлечь имя файла.

Результаты find, sortтрубопровод проходит через подмены процесса в whileкотором он читается , как если бы это был файл на стандартный ввод. whileв свою очередь вызывает readдля обработки ввода.

В контексте readмы устанавливаем IFSпеременную в ничего, что означает, что пробел не будет неправильно интерпретироваться как разделитель. readсказано -r, что блокирует расширение побега, и -d $'\0', что делает конец-строки разделителя NULL, соответствующий вывод из нашего find, sortтрубопровода.

Первый фрагмент данных, представляющий самый старый путь к файлу, которому предшествуют его временная метка и пробел, считывается в переменную line. Далее, подстановка параметров используется с выражением #*, которое просто заменяет все символы от начала строки до первого пробела, включая пробел, ничем. Это удаляет метку времени модификации, оставляя только полный путь к файлу.

На этом этапе имя файла сохраняется, $fileи вы можете делать с ним все что угодно. Когда вы закончили делать что - то с $fileв whileцикл будет заявление и readкоманда будет выполнена снова, извлекая следующий фрагмент и следующее имя файла.

Есть ли более простой способ?

Нет. Более простые способы глючат.

Если вы используете ls -tи передаете по трубопроводу headили tail(или что-нибудь еще ), вы сломаете файлы с символами новой строки в именах файлов. Если у вас есть mv $(anything)файлы с пробелами в имени, это приведет к поломке. Если вы mv "$(anything)"затем файлы с завершающими символами новой строки в имени, это приведет к поломке. Если вы этого readне -d $'\0'сделаете, вы будете использовать файлы с пробелами в именах.

Возможно, в определенных случаях вы точно знаете, что более простой способ достаточен, но вы никогда не должны записывать подобные предположения в сценарии, если вы можете избежать этого.

Решение

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

Звоните как:

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

Чтобы переместить самые старые 20 файлов из /var/log/foo/в /mnt/backup/.

Обратите внимание, что я включаю файлы и каталоги. Для файлов только добавить -type fк findвызову.

Благодарность

Спасибо энзотибу и Павлу Танкову за улучшения этого ответа.

Sorpigal
источник
Сорт не должен использовать -n. По крайней мере, в моей версии он не сортирует десятичные числа правильно. Вы должны либо удалить точку в дате, либо использовать -printf '%TY-%Tm-%TdT%TH:%TM:%TS %p\0' | sort -rzдату ISO или что-то еще.
10
@ l0b0: это ограничение мне известно. Я полагаю, что этого достаточно, чтобы не требовать такого уровня детализации (то есть сортировка за пределами .должна быть неуместна для вас). Было бы яснее сказать sort -z -n -t. -k1.
Сорпигал
@ l0b0: Ваше решение содержит ту же ошибку, независимо от того: %TSтакже отображается «дробная часть», которая будет в форме 00.0000000000, поэтому вы также потеряете детализацию. Недавние GNU sortмогли решить эту проблему, используя -V«сортировку версий», которая будет обрабатывать этот тип с плавающей запятой, как и ожидалось.
Сорпигал
Нет, потому что я делаю строковый вид на «YYYY-MM-ДДТчч: мм: сс» , а не числовой сортировки. Сортировка строк не заботится о десятичных дробях, поэтому она должна работать до 10000 года :)
l0b0
@ l0b0: Сортировка строк %T@также будет работать, потому что она дополняется нулями.
Сорпигал
4

Это самый простой в Zsh, где вы можете использовать Om Глоб классификатор для сортировки матчей по дате (сначала старые) и [1,20]классификатором сохранить только первые 20 матчей:

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

Добавьте Dклассификатор, если вы хотите также включить точечные файлы. Добавьте, .если вы хотите сопоставлять только обычные файлы, а не каталоги.

Если у вас нет zsh, вот Perl с одной строкой (вы можете сделать это менее чем за 80 символов, но с дополнительными затратами для ясности):

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

С помощью только инструментов POSIX или даже bash или ksh сортировка файлов по дате становится проблемой. Вы можете легко это сделать ls, но анализ выходных данных lsпроблематичен, так что это работает только в том случае, если имена файлов содержат только печатаемые символы, кроме символов новой строки.

ls -tr | head -n 20 | while IFS= read -r file; do mv -- "$file" target/; done
Жиль "ТАК - перестань быть злым"
источник
4

Объединить ls -tвывод с tailили head.

Простой пример, который работает, только если все имена файлов содержат только печатаемые символы, кроме пробелов и \[*?:

 mv $(ls -1tr | head -20) other_folder
КТФ
источник
1
Добавьте опцию -A к ls:ls -1Atr
Arcege 15.10.11
1
-1, опасно. Здесь позвольте мне ремесло примера: touch $'foo\n*'. Что произойдет, если вы выполните mv "$ (ls)" с этим файлом?
Сорпигал
1
@ Sorpigal Серьезно? Слабовато сказать: «Позвольте мне привести пример, который, как вы сказали, не сработает. Эй, смотри, это не сработает»
Майкл Мрозек
1
@ Sorpigal Это неплохая идея, она работает в 99% случаев. Ответ таков: «Если у вас есть файлы с нормальными именами, это работает. Если вы безумный человек, который встраивает переводы строк в свои имена файлов, этого не произойдет». Это совершенно правильно
Михаил Мрозек
1
@MichaelMrozek: Это плохая идея и плохая, потому что иногда она терпит неудачу. Если у вас есть возможность делать то, что иногда терпит неудачу, а что нет, вы должны выбрать вариант, который не работает (а тот, который делает плохо). Делайте все, что вам нравится, в интерактивном режиме, но в файле сценария и при советах делайте это правильно.
Сорпигал
3

Вы можете использовать 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

Если find печатает время изменения (в секундах с 1970 года) и имя каждого файла текущего каталога, выходные данные сортируются по первому полю, 20 самых старых фильтруются и перемещаются в dest_dir. Удалите, echoесли вы проверили командную строку.

maxschlepzig
источник
2

Никто (пока) не опубликовал пример bash, который обслуживает встроенные символы новой строки (вложенные что угодно) в имени файла, так что вот один. Перемещает 3 самых старых (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

Это фрагмент тестовых данных

# 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

Вот фрагмент проверки результатов

  ls -ltr '('?[1-4]'  |'?')'
  ls -ltr   dest/*
Peter.O
источник
+1, только полезный ответ до моего (и всегда хорошо иметь тестовые данные.)
Sorpigal
0

Проще всего это сделать с GNU find. Я использую его каждый день на своем Linux DVR для удаления записей из моей системы видеонаблюдения старше одного дня.

Вот синтаксис:

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

Помните, что findдень определяется как 24 часа с момента исполнения. Поэтому файлы, которые в последний раз были изменены в 23:00, не будут удалены в 1:00.

Вы даже можете комбинировать findс ними cron, поэтому удаление можно запланировать автоматически, выполнив следующую команду от имени пользователя root:

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

Вы всегда можете получить больше информации find, обратившись к странице справочника:

man find
Джонатан Фрэнк
источник
0

так как другие ответы не соответствуют моим целям и задачам, эта оболочка протестирована на 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
источник