Удалить все, кроме самых последних файлов X в Bash

157

Есть ли простой способ в довольно стандартной среде UNIX с bash запустить команду, чтобы удалить все, кроме самых последних X-файлов из каталога?

Чтобы привести немного более конкретный пример, представьте себе, что какое-то задание cron записывает файл (скажем, файл журнала или архивную резервную копию) в каталог каждый час. Я хотел бы иметь способ запустить еще одно задание cron, которое бы удаляло самые старые файлы в этом каталоге, пока их не станет меньше, скажем, 5.

И чтобы было ясно, присутствует только один файл, он никогда не должен быть удален.

Мэтт Шеппард
источник

Ответы:

117

Проблемы с существующими ответами:

  • невозможность обрабатывать имена файлов со встроенными пробелами или переводами строки.
    • в случае решений, которые вызываются rmнепосредственно в подстановке команд без кавычек ( rm `...`), существует дополнительный риск непреднамеренного сглаживания.
  • невозможность различать файлы и каталоги (т. е. если каталоги оказались в числе 5 самых последних измененных элементов файловой системы, вы фактически сохраните менее 5 файлов, и применение rmк каталогам не удастся).

Ответ wnoise решает эти проблемы, но решение является специфичным для GNU (и довольно сложным).

Вот прагматичное, POSIX-совместимое решение, которое поставляется только с одной оговоркой : оно не может обрабатывать имена файлов со встроенными символами новой строки - но я не считаю это реальной проблемой для большинства людей.

Для справки, вот объяснение того, почему вообще не очень хорошая идея разбирать lsвывод: http://mywiki.wooledge.org/ParsingLs

ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}

Вышеупомянутое неэффективно , потому что xargsдолжен вызывать rmодин раз для каждого имени файла.
Ваша платформа xargsможет позволить вам решить эту проблему:

Если у вас есть GNU xargs , используйте -d '\n', что делает xargsкаждую входную строку отдельным аргументом, но передает столько аргументов, сколько поместится в командной строке одновременно :

ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --

-r( --no-run-if-empty) гарантирует, что rmне вызывается, если нет ввода.

Если у вас есть BSD xargs (в том числе в macOS ), вы можете использовать -0для обработки NULввода, разделенного разделителями, после первой трансляции символов новой строки в NUL( 0x0) chars., Который также передает (обычно) все имена файлов одновременно (также будет работать с GNU xargs):

ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --

Объяснение:

  • ls -tpпечатает имена элементов файловой системы, отсортированные по тому, как недавно они были изменены, в порядке убывания (сначала самые последние измененные элементы) ( -t), с каталогами, напечатанными с последующим, /чтобы пометить их как таковые ( -p).
  • grep -v '/$'затем отсеивает каталоги из результирующего списка, пропуская -vстроки ( ), которые имеют конечный /( /$).
    • Предостережение : поскольку символическая ссылка, которая указывает на каталог , технически сама по себе не является каталогом, такие символические ссылки не будут исключены.
  • tail -n +6пропускает первые 5 записей в списке, фактически возвращая все, кроме 5 самых последних измененных файлов, если таковые имеются.
    Обратите внимание, что для исключения Nфайлов N+1необходимо передать tail -n +.
  • xargs -I {} rm -- {}(и его вариации) затем вызывает rmвсе эти файлы; если совпадений xargsнет вообще, ничего не сделаю.
    • xargs -I {} rm -- {}определяет местозаполнитель, {}который представляет каждую строку ввода в целом , поэтому rmон вызывается один раз для каждой строки ввода, но с именами файлов со встроенными пробелами, которые обрабатываются правильно.
    • --во всех случаях гарантирует , что любые имена файлов , которые происходят , чтобы начать с -не ошибаемся для опций по rm.

Вариации на исходной задаче, в случае , если соответствующие файлы должны быть обработаны по отдельности или собран в массиве оболочки :

# One by one, in a shell loop (POSIX-compliant):
ls -tp | grep -v '/$' | tail -n +6 | while IFS= read -r f; do echo "$f"; done

# One by one, but using a Bash process substitution (<(...), 
# so that the variables inside the `while` loop remain in scope:
while IFS= read -r f; do echo "$f"; done < <(ls -tp | grep -v '/$' | tail -n +6)

# Collecting the matches in a Bash *array*:
IFS=$'\n' read -d '' -ra files  < <(ls -tp | grep -v '/$' | tail -n +6)
printf '%s\n' "${files[@]}" # print array elements
mklement0
источник
2
Конечно, лучше, чем большинство других ответов здесь, поэтому я рад оказать свою поддержку, даже если я считаю, что игнорирование дела новой строки должно быть сделано только с осторожностью.
Чарльз Даффи
2
Если вы lsне в текущем каталоге, пути к файлам будут содержать '/', что означает, что grep -v '/'ничего не будет совпадать. Я считаю grep -v '/$', что вы хотите исключить только каталоги.
waldol1
1
@ waldol1: Спасибо; Я обновил ответ, включив в него ваше предложение, что также делает grepконцептуальную команду более понятной. Однако обратите внимание, что описанная вами проблема не возникла бы с одним путем к каталогу; например, ls -p /private/varвсе равно будет печатать только простые имена файлов. Только если вы передадите несколько аргументов файла (обычно через глобус), вы увидите фактические пути в выводе; Например, ls -p /private/var/*(и вы также увидите содержимое соответствующих подкаталогов, если вы также не включили -d).
mklement0
108

Удалите все кроме 5 (или любого другого числа) самых последних файлов в каталоге.

rm `ls -t | awk 'NR>5'`
Эспоо
источник
2
Мне нужно это, чтобы рассмотреть только мои архивные файлы. изменить ls -tнаls -td *.bz2
Джеймс Т Снелл
3
Я использовал это для каталогов, изменив его на rm -rf ls -t | awk 'NR>1'(я хотел только самый последний). Спасибо!
lohiaguitar91
11
ls -t | awk 'NR>5' | xargs rm -f если вы предпочитаете каналы, и вам нужно подавить ошибку, если нечего удалять.
H2ONaCl
16
Краткий и читаемый, возможно, но опасный для использования; если попытаться удалить файл, созданный с помощью touch 'hello * world', это приведет к удалению абсолютно всего в текущем каталоге .
Чарльз Даффи
1
Несмотря на то, что на этот вопрос был дан ответ в 2008 году, он работает как шарм и как раз то, что мне нужно, чтобы просто удалить старые резервные копии из определенного каталога. Потрясающие.
Ренс
86
(ls -t|head -n 5;ls)|sort|uniq -u|xargs rm

Эта версия поддерживает имена с пробелами:

(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
thelsdj
источник
20
Эта команда не будет правильно обрабатывать файлы с пробелами в именах.
Тайлер
5
(ls -t|head -n 5;ls)это командная группа . Он печатает 5 самых последних файлов дважды. sortсоединяет одинаковые строки uniq -uудаляет дубликаты, поэтому остаются все файлы, кроме 5 самых последних. xargs rmпризывает rmна каждом из них.
Фабьен
15
Это удалит все ваши файлы, если у вас есть 5 или меньше! Добавить --no-run-if-emptyв xargsкак в (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rmпожалуйста , обновите ответ.
Gonfi den Tschal
3
Даже тот, который "поддерживает имена с пробелами", опасен. Рассмотрим имя, которое содержит буквальные кавычки: touch 'foo " bar'скинет всю оставшуюся часть команды.
Чарльз Даффи
2
... это безопаснее использовать , xargs -d $'\n'чем инъекционные кавычки в содержание, хотя NUL задающего входной поток (что требует использовать что - то другое , чем lsна самом деле делать справа) является вариантом идеально.
Чарльз Даффи
59

Более простой вариант ответа thelsdj:

ls -tr | head -n -5 | xargs --no-run-if-empty rm 

ls -tr отображает все файлы, сначала самые старые (сначала сначала -t, либо наоборот -r).

head -n -5 отображает все, кроме 5 последних строк (т.е. 5 новейших файлов).

xargs rm вызывает rm для каждого выбранного файла.

Фабьен
источник
15
Необходимо добавить --no-run-if-empty в xargs, чтобы он не завершился ошибкой при наличии менее 5 файлов.
Том
ls -1tr | голова -n -5 | xargs rm <---------- вам нужно добавить -1 к ls, иначе вы не получите вывод списка для правильной работы head
Al Joslin
3
@AlJoslin, -1используется по умолчанию, когда вывод выполняется в конвейер, поэтому здесь это не обязательно. Это имеет гораздо более серьезные проблемы, связанные с поведением по умолчанию xargsпри разборе имен с пробелами, кавычками и т. Д.
Чарльз Даффи
кажется, что --no-run-if-emptyне распознается в моей оболочке. Я использую Cmder на Windows.
StayFoolish
-0Возможно, потребуется использовать эту опцию, если имена файлов могут содержать пробелы. Пока не проверял это. источник
Кит
18
find . -maxdepth 1 -type f -printf '%T@ %p\0' | sort -r -z -n | awk 'BEGIN { RS="\0"; ORS="\0"; FS="" } NR > 5 { sub("^[0-9]*(.[0-9]*)? ", ""); print }' | xargs -0 rm -f

Требует GNU find для -printf, GNU sort для -z, GNU awk для "\ 0" и GNU xargs для -0, но обрабатывает файлы со встроенными символами новой строки или пробелами.

wnoise
источник
2
Если вы хотите удалить каталоги, просто измените -f на -d и добавьте -r к rm. найти . -maxdepth 1 -тип d -printf '% T @% p \ 0' | сортировать -r -z -n | awk 'BEGIN {RS = "\ 0"; ПРС = "\ 0"; FS = ""} NR> 5 {sub ("^ [0-9] * (. [0-9] *)?", ""); печать} '| xargs -0 rm -rf
alex
1
На первый взгляд, я удивлен сложностью (или, если на то пошло, необходимостью) awkлогики. Я пропускаю некоторые требования в вопросе ОП, которые делают это необходимым?
Чарльз Даффи
@Charles Duffy: sub () удаляет временную метку, которая сортируется по. Временная метка, создаваемая "% T @", может включать дробную часть. Расщепление в пространстве с помощью FS разбивает пути со встроенными пробелами. Я полагаю, что удаление через первые космические работы, но почти так же трудно читать. Разделители RS и ORS не могут быть установлены в командной строке, потому что они являются NUL.
Внуаз
1
@wnoise, мой обычный подход к этому состоит в том, чтобы направить поток в while read -r -d ' '; IFS= -r -d ''; do ...цикл оболочки - первое чтение завершается в пространстве, а второе переходит в NUL.
Чарльз Даффи
@Charles Duffy: я всегда с подозрением отношусь к необработанным панцирям, возможно, из-за византийских цитат. Теперь я думаю, что GNU sed -z -e 's/[^ ]* //; 1,5d'- самое ясное. (или, может быть sed -n -z -e 's/[^ ]* //; 6,$p'.
Wnoise
14

Все эти ответы терпят неудачу, когда есть каталоги в текущем каталоге. Вот что работает:

find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm

Это:

  1. работает, когда есть каталоги в текущем каталоге

  2. пытается удалить каждый файл, даже если предыдущий не может быть удален (из-за разрешений и т. д.)

  3. терпит неудачу в безопасности , когда количество файлов в текущем каталоге избыточно и xargsбудет нормально ввернуть Вас более года ( -x)

  4. не учитывает пробелы в именах файлов (возможно, вы используете не ту ОС?)

Чарльз Вуд
источник
5
Что произойдет, если findвернет больше имен файлов, чем может быть передано в одной командной строке ls -t? (Совет: вы получаете несколько прогонов ls -t, каждый из которых отсортирован только по отдельности, вместо того, чтобы иметь глобально правильный порядок сортировки; таким образом, этот ответ сильно нарушается при работе с достаточно большими каталогами).
Чарльз Даффи
12
ls -tQ | tail -n+4 | xargs rm

Список имен файлов по времени модификации, цитируя каждое имя файла. Исключить первые 3 (3 самых последних). Удалить оставшиеся.

РЕДАКТИРОВАТЬ после полезного комментария от mklement0 (спасибо!): Исправлен аргумент -n + 3, и обратите внимание, что это не будет работать должным образом, если имена файлов содержат символы новой строки и / или каталог содержит подкаталоги.

отметка
источник
-QВариант , кажется, не существует на моей машине.
Пьер-Адриен Буассон,
4
Хм, опция была в утилитах ядра GNU в течение ~ 20 лет, но не упоминается в вариантах BSD. Ты на маке?
Отметить
Я действительно являюсь. Не думал, что есть различия между такими базовыми командами между современными системами. Спасибо за Ваш ответ !
Пьер-Адриен Буассон
3
@Mark: ++ для -Q. Да, -Qэто расширение GNU (вот спецификация POSIXls ). Небольшое предостережение (на практике редко является проблемой): -Qкодирует встроенные символы новой строки в именах файлов как буквальные \n, которые rmне распознаются. Чтобы исключить первые 3 , xargsаргумент должен +4. И, наконец, предостережение, которое применимо и к большинству других ответов: ваша команда будет работать, как и предполагалось, только если в текущем каталоге нет подкаталогов .
mklement0
1
Когда нечего удалять, у вас есть вызов xargs с --no-run-if-emptyопцией:ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Olivier Lecrivain
8

Игнорирование новых строк игнорирует безопасность и хорошее кодирование. У wnoise был единственный хороший ответ. Вот вариант его, который помещает имена файлов в массив $ x

while IFS= read -rd ''; do 
    x+=("${REPLY#* }"); 
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Ян Келлинг
источник
2
Я бы предложил очистить IFS- в противном случае вы рискуете потерять конечный пробел из имен файлов. Можно включить это в команду чтения:while IFS= read -rd ''; do
Чарльз Даффи
1
почему "${REPLY#* }"?
msciwoj
4

Если в именах файлов нет пробелов, это будет работать:

ls -C1 -t| awk 'NR>5'|xargs rm

Если имена файлов имеют пробелы, что-то вроде

ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh

Основная логика:

  • получить список файлов по времени, один столбец
  • получить все, кроме первых 5 (n = 5 для этого примера)
  • первая версия: отправьте их в rm
  • вторая версия: gen скрипт, который удалит их правильно
Марк Харрисон
источник
Не забудьте while readхитрость при работе с пробелами: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
Пинкен
1
@pinkeen, не совсем безопасно, как указано там. while IFS= read -r dбыло бы немного лучше - -rпредотвращает поглощение литералами обратной косой черты readи IFS=предотвращает автоматическое усечение завершающего пробела.
Чарльз Даффи
4
Кстати, если кто-то беспокоится о враждебных именах файлов, это чрезвычайно опасный подход. Рассмотрим файл, созданный с помощью touch $'hello \'$(rm -rf ~)\' world'; литеральные кавычки внутри имени файла будут противостоять литеральным кавычкам, которые вы добавляете sed, в результате чего выполняется код внутри имени файла.
Чарльз Даффи
1
(чтобы быть понятным, «это» выше относится к | shформе, которая имеет уязвимость инъекции оболочки).
Чарльз Даффи
2

С зш

Предполагая, что вы не заботитесь о существующих каталогах, и у вас будет не более 999 файлов (выберите большее число, если хотите, или создайте цикл while).

[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])

В *(.om[6,999]), в .файлах значит, oсредство порядок сортировки вверх, mсредства по дате модификации (положить aна время доступа или cдля изменения инода), то [6,999]выбирает диапазон файла, поэтому не Р.М. 5 первых.

lolesque
источник
Интересно, но я не мог заставить omработать классификатор glob сортировки ( ) (любая попытка сортировки, которую я пробовал, не дала эффекта - ни на OSX 10.11.2 (пробовал с zsh 5.0.8 и 5.1.1) ни на Ubuntu 14.04 (zsh 5.0.2)) - чего мне не хватает ?. Что касается диапазона конечной точки: нет необходимости в жесткий код, просто использовать -1для обозначения последней записи и , таким образом , включают в себя все остальные файлы: [6,-1].
mklement0
2

Я понимаю, что это старая ветка, но, возможно, кому-то это поможет. Эта команда найдет файлы в текущем каталоге:

for F in $(find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n' | sort -r -z -n | tail -n+5 | awk '{ print $2; }'); do rm $F; done

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

find . -maxdepth 1 -type f -name "*_srv_logs_*.tar.gz" -printf '%T@ %p\n'

Затем отсортируйте их по временным меткам:

sort -r -z -n

Затем уберите 4 самых последних файла из списка:

tail -n+5

Возьмите 2-й столбец (имя файла, а не метку времени):

awk '{ print $2; }'

А затем оберните все это в утверждение for:

for F in $(); do rm $F; done

Это может быть более многословная команда, но мне повезло больше, когда я смог нацелиться на условные файлы и выполнить с ними более сложные команды.

TopherGopher
источник
1

нашел интересный cmd в Sed-Onliners - удалите последние 3 строки - и он идеально подходит для другого способа облысения кошки (хорошо, нет), но идея:

 #!/bin/bash
 # sed cmd chng #2 to value file wish to retain

 cd /opt/depot 

 ls -1 MyMintFiles*.zip > BigList
 sed -n -e :a -e '1,2!{P;N;D;};N;ba' BigList > DeList

 for i in `cat DeList` 
 do 
 echo "Deleted $i" 
 rm -f $i  
 #echo "File(s) gonzo " 
 #read junk 
 done 
 exit 0
Тим
источник
1

Удаляет все, кроме 10 последних (большинство последних) файлов

ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm

Если менее 10 файлов, ни один файл не будет удален, и у вас будет: error head: недопустимое количество строк - 0

Считать файлы с помощью bash

Фабрис
источник
1

Мне нужно было элегантное решение для busybox (маршрутизатора), все решения xargs или array были для меня бесполезны - такой команды там не было. find и mtime не правильный ответ, так как речь идет о 10 пунктах и ​​не обязательно 10 днях. Ответ Эспо был самым коротким и чистым и, вероятно, самым неожиданным.

Ошибка с пробелами и когда файлы не должны быть удалены, просто решаются стандартным способом:

rm "$(ls -td *.tar | awk 'NR>7')" 2>&-

Немного больше образовательной версии: мы можем сделать все это, если будем использовать awk по-другому. Обычно я использую этот метод для передачи (возврата) переменных из awk в sh. Поскольку мы все время читаем, что не может быть сделано, я позволю себе не согласиться: вот метод.

Пример для файлов .tar без проблем с пробелами в имени файла. Чтобы проверить, замените «rm» на «ls».

eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}')

Объяснение:

ls -td *.tarперечисляет все файлы .tar, отсортированные по времени. Чтобы применить ко всем файлам в текущей папке, удалите часть "d * .tar"

awk 'NR>7... пропускает первые 7 строк

print "rm \"" $0 "\"" конструирует строку: rm "имя файла"

eval выполняет это

Поскольку мы используем rm, я бы не использовал вышеуказанную команду в сценарии! Более разумное использование:

(cd /FolderToDeleteWithin && eval $(ls -td *.tar | awk 'NR>7 { print "rm \"" $0 "\""}'))

В случае использования ls -tкоманды не навредит такие глупые примеры как: touch 'foo " bar'иtouch 'hello * world' . Не то чтобы мы когда-либо создавали файлы с такими именами в реальной жизни!

Примечание. Если бы мы хотели передать переменную в sh таким образом, мы бы просто изменили print (простая форма, без пробелов):

print "VarName="$1

установить переменную VarNameв значение $1. Несколько переменных могут быть созданы за один раз. Это VarNameстановится обычной переменной sh и впоследствии может быть использовано в скрипте или оболочке. Итак, чтобы создать переменные с помощью awk и вернуть их обратно в оболочку:

eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\""  }'); echo "$VarName"
Pila
источник
0
leaveCount=5
fileCount=$(ls -1 *.log | wc -l)
tailCount=$((fileCount - leaveCount))

# avoid negative tail argument
[[ $tailCount < 0 ]] && tailCount=0

ls -t *.log | tail -$tailCount | xargs rm -f
Павел Танков
источник
2
xargsбез -0или по минимуму -d $'\n'ненадежен; посмотрите, как это происходит с файлом с пробелами или кавычками в имени.
Чарльз Даффи
0

Я сделал это в скрипт оболочки bash. Использование: keep NUM DIRгде NUM - это количество файлов для хранения, а DIR - каталог для очистки.

#!/bin/bash
# Keep last N files by date.
# Usage: keep NUMBER DIRECTORY
echo ""
if [ $# -lt 2 ]; then
    echo "Usage: $0 NUMFILES DIR"
    echo "Keep last N newest files."
    exit 1
fi
if [ ! -e $2 ]; then
    echo "ERROR: directory '$1' does not exist"
    exit 1
fi
if [ ! -d $2 ]; then
    echo "ERROR: '$1' is not a directory"
    exit 1
fi
pushd $2 > /dev/null
ls -tp | grep -v '/' | tail -n +"$1" | xargs -I {} rm -- {}
popd > /dev/null
echo "Done. Kept $1 most recent files in $2."
ls $2|wc -l
камыш озерный
источник