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





Удалите все, кроме 5 (или любого другого числа) самых последних файлов в каталоге.
rm `ls -t | awk 'NR>5'`
Я использовал это для каталогов, изменив его на rm -rf ls -t | awk 'NR>1' (мне нужен был только самый последний). Спасибо!
ls -t | awk 'NR>5' | xargs rm -f, если вы предпочитаете каналы и вам нужно подавить ошибку, если нечего удалять.
Это очень мило. Используя это для очистки моего каталога сборки CI / CD. Спасибо.
Как ни странно, это не сработало для меня. Мне пришлось использовать версию xargs @ H2ONaCl
Возможно, кратко и читабельно, но опасно в использовании; при попытке удалить файл, созданный с помощью touch 'hello * world', это приведет к удалению абсолютно все в текущем каталоге.
Несмотря на то, что на этот вопрос был дан ответ в 2008 году, он работает как шарм, и мне нужно просто удалить старые резервные копии из определенного каталога. Потрясающие.
Я хотел указать здесь явный путь.
(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
Эта команда некорректно обрабатывает файлы с пробелами в именах.
Чтобы исправить выше, используйте: (ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm
Этот не работает, если нет файлов для удаления.
@Mantas Я думаю, что это можно решить с помощью rm -f (хотя я пробовал только -rf, не знаю, работает ли он без -r).
(ls -t|head -n 5;ls) - это группа команд. Он распечатывает 5 последних файлов дважды. sort объединяет идентичные строки. uniq -u удаляет дубликаты, поэтому остаются все файлы, кроме 5 самых последних. xargs rm вызывает rm для каждого из них.
(ls -t|head -n 5;ls)|sort|uniq -u|sed -e 's,.*,"&",g'|xargs rm -rf работает для: 1) удаления каталогов 2) предотвращения "rm: missing operand", когда нечего удалять
Не работает для файлов типа "-000-" (ls -t|head -n 100;ls)|sort|uniq -u | xargs printf './%s\n' | xargs rm работает в моем случае.
@Matas правильный. не работает, если в папке меньше 5 файлов, будьте осторожны
Это удалит все ваши файлы, если у вас их 5 или меньше! Добавьте --no-run-if-empty в xargs, как в (ls -t|head -n 5;ls)|sort|uniq -u|xargs --no-run-if-empty rm, обновите ответ.
Echo Gonfi, это удаляет 5 самых старых файлов. Это даже отдаленно НЕ то же самое, что хранить 5 самых новых файлов.
Даже тот, который «поддерживает имена с пробелами», опасен. Рассмотрим имя, содержащее буквальные кавычки: touch 'foo " bar' отбросит всю остальную часть команды.
... безопаснее использовать xargs -d $'\n', чем вставлять кавычки в ваш контент, хотя NUL-разделитель входного потока (который требует использования чего-то другого, кроме ls, чтобы В самом деле делать правильно) является идеальным вариантом.
Почему не ls -t | tail -n +6 вместо (ls -t|head -n 5;ls)|sort|uniq -u?
Если в именах файлов нет пробелов, это сработает:
ls -C1 -t| awk 'NR>5'|xargs rm
Если в именах файлов есть пробелы, что-то вроде
ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh
Основная логика:
Не забудьте уловку while read для работы с пробелами: ls -C1 -t | awk 'NR>5' | while read d ; do rm -rvf "$d" ; done
@pinkeen, не совсем безопасно, как указано там. while IFS= read -r d был бы немного лучше - -r предотвращает использование литералов обратной косой черты read, а IFS= предотвращает автоматическое обрезание конечных пробелов.
Кстати, если вас беспокоят враждебные имена файлов, это опасный подход очень сильно. Рассмотрим файл, созданный с помощью touch $'hello \'$(rm -rf ~)\' world'; буквальные кавычки внутри имени файла будут противоречить буквальным кавычкам, которые вы добавляете с помощью sed, в результате чего код внутри имени файла будет выполняться.
(для ясности, «this» выше относится к форме | sh, которая содержит уязвимость инъекции оболочки).
Все эти ответы терпят неудачу, если в текущем каталоге есть каталоги. Вот что работает:
find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm
Этот:
работает, когда в текущем каталоге есть каталоги
пытается удалить каждый файл, даже если предыдущий не удалось удалить (из-за разрешений и т. д.)
не безопасен, когда количество файлов в текущем каталоге чрезмерно, и xargs обычно вас облажает (-x)
не учитывает пробелы в именах файлов (возможно, вы используете неправильную ОС?)
Что произойдет, если find вернет больше имен файлов, чем может быть передано ls -t в одной командной строке? (Подсказка: вы получаете несколько запусков ls -t, каждый из которых сортируется только индивидуально, вместо того, чтобы иметь глобально правильный порядок сортировки; таким образом, этот ответ плохо работает при работе с достаточно большими каталогами).
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 для -printf и сортировка GNU для -z, GNU awk для "\ 0" и GNU xargs для -0, но обрабатывает файлы со встроенными символами новой строки или пробелами.
Если вы хотите удалить каталоги, просто измените -f на -d и добавьте -r в rm. найти . -maxdepth 1 -type d -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 -rf
На первый взгляд, меня удивляет сложность (или, если на то пошло, необходимость) логики awk. Мне не хватает некоторых требований в вопросе OP, которые делают это необходимым?
@Charles Duffy: sub () удаляет метку времени, по которой выполняется сортировка. Отметка времени, созданная "% T @", может включать дробную часть. Разделение по пространству с помощью FS разрывает пути с вложенными пробелами. Я полагаю, что удаление до первого пробела работает, но его почти так же трудно читать. Разделители RS и ORS не могут быть установлены в командной строке, потому что они NUL.
@wnoise, мой обычный подход к этому - перенаправить в цикл оболочки while read -r -d ' '; IFS= -r -d ''; do ... - первое чтение завершается на пробеле, а второе - на NUL.
@ Чарльз Даффи: Я всегда с подозрением отношусь к сырой оболочке, возможно, из-за византийских проблем с цитированием. Теперь я думаю, что GNU sed -z -e 's/[^ ]* //; 1,5d' - самый ясный. (или, возможно, sed -n -z -e 's/[^ ]* //; 6,$p'.
Я бы сказал, что цитирование не так уж и плохо, если понимать модель исполнения, но допускаю, что, сделав ранее такое вложение, мне трудно оценить, как бы это выглядело, если бы это не было в тумане прошлого.
Игнорирование символов новой строки означает игнорирование безопасности и хорошего кодирования. wnoise имел единственный хороший ответ. Вот его вариант, в котором имена файлов помещаются в массив $ x
while IFS= read -rd ''; do
x+=("${REPLY#* }");
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Я бы предложил очистить IFS - иначе вы рискуете потерять конечные пробелы в именах файлов. Можно указать это на команду чтения: while IFS= read -rd ''; do
почему "${REPLY#* }"?
С zsh
Предполагая, что вас не интересуют текущие каталоги, и у вас не будет более 999 файлов (выберите большее число, если хотите, или создайте цикл while).
[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])
В *(.om[6,999]). означает файлы, o означает порядок сортировки вверх, m означает дату модификации (укажите a для времени доступа или c для изменения inode), [6,999] выбирает диапазон файлов, поэтому не оставляет 5 первый.
Интересно, но хоть убей, я не смог заставить работать классификатор sorting glob (om) (любая сортировка, которую я пробовал, не оказала никакого эффекта - ни на OSX 10.11.2 (пробовал с zsh 5.0.8 и 5.1.1) ), ни в Ubuntu 14.04 (zsh 5.0.2)) - что мне не хватает ?. Что касается конечной точки диапазона: нет необходимости жестко ее кодировать, просто используйте -1 для ссылки на последнюю запись и, таким образом, включите все оставшиеся файлы: [6,-1].
Более простой вариант ответа thelsdj:
ls -tr | head -n -5 | xargs --no-run-if-empty rm
ls -tr отображает все файлы, сначала самые старые (-t сначала самые новые, -r наоборот).
head -n -5 отображает все, кроме 5 последних строк (т.е. 5 самых новых файлов).
xargs rm вызывает rm для каждого выбранного файла.
Необходимо добавить --no-run-if-empty в xargs, чтобы он не выходил из строя, когда файлов меньше 5.
ls -1tr | голова -n -5 | xargs rm
@AlJoslin, -1 используется по умолчанию, когда вывод идет в конвейер, поэтому здесь это не обязательно. Это имеет гораздо более серьезные проблемы, связанные с поведением xargs по умолчанию при синтаксическом анализе имен с пробелами, кавычками и т. д.
похоже, что --no-run-if-empty не распознается в моей оболочке. Я использую Cmder в Windows.
Возможно, потребуется использовать параметр -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
xargs без -0 или как минимум -d $'\n' ненадежен; наблюдайте, как это ведет себя с файлом с пробелами или кавычками в его имени.
ls -tQ | tail -n+4 | xargs rm
Список имен файлов по времени модификации, заключая в кавычки каждое имя файла. Исключить первые 3 (3 последних). Удалите оставшееся.
ИЗМЕНИТЬ после полезного комментария от mklement0 (спасибо!): Исправлен аргумент -n + 3, и обратите внимание, что это не будет работать должным образом, если имена файлов содержат символы новой строки и / или каталог содержит подкаталоги.
Похоже, что на моем компьютере нет опции -Q.
Хм, эта опция присутствует в основных утилитах GNU около 20 лет, но не упоминается в вариантах BSD. Вы на Mac?
Я на самом деле. Не думал, что существуют различия в таких действительно основных командах между современными системами. Спасибо за Ваш ответ !
@Mark: ++ для -Q. Да, -Q - это расширение GNU (здесь Спецификация POSIX ls). Небольшое предостережение (на практике это редко является проблемой): -Q кодирует встроенный новые строки в именах файлов как буквальный \n, который rm не распознает. Чтобы исключить первый 3, аргумент xargs должен быть +4. Наконец, предостережение, которое относится и к большинству других ответов: ваша команда будет работать только по назначению, если в текущем каталоге нет подкаталоги.
Как я могу запустить это между then и fi в одной строке
Когда нечего удалять, вы вызываете xargs с параметром --no-run-if-empty: ls -tQ | tail -n+4 | xargs --no-run-if-empty rm
Проблемы с существующими ответами:
rm непосредственно при подстановке команд без кавычек (rm `...`), существует дополнительный риск непреднамеренного подстановки.rm к каталогам не удастся).Wnoise ответ решает эти проблемы, но решение является специфичным для GNU (и довольно сложным).
Вот прагматичный POSIX-совместимое решение, который поставляется только с одно предостережение: он не может обрабатывать имена файлов со встроенным новые строки - но я не считаю, что это реальная проблема для большинства людей.
For the record, here's the explanation for why it's generally not a good idea to parse ls output: http://mywiki.wooledge.org/ParsingLs
ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}
Note: This command operates in the current directory; to target a directory explicitly, use a subshell ((...)):(cd /path/to && ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {})
The same applies analogously to the commands below.
Вышеупомянутое - неэффективный, потому что xargs должен вызвать rm один раз для имени файла каждый.
xargs вашей платформы может помочь вам решить эту проблему:
Если у вас есть GNUxargs, используйте -d '\n', который заставляет xargs рассматривать каждую строку ввода как отдельный аргумент, но передает столько аргументов, сколько поместится в командной строке сразу:
ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --
-r (--no-run-if-empty) ensures that rm is not invoked if there's no input.
Если у вас есть BSDxargs (включая macOS), вы можете использовать -0 для обработки ввода, разделенного NUL, после первого перевода новой строки в символы NUL (0x0), который также передает (обычно) все имена файлов сразу (также будет работать с GNU xargs) :
ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --
Объяснение:
ls -tp печатает имена элементов файловой системы, отсортированные по дате их изменения, в порядке убывания (сначала измененные элементы) (-t), а каталоги печатаются с пометкой / в конце, чтобы пометить их как таковые (-p).
ls -tp всегда выводит только файл / каталог имена, а не полные пути, требует упомянутого выше подхода подоболочки для нацеливания на каталог, отличный от текущего ((cd /path/to && ls -tp ...)).Затем 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
Конечно, лучше, чем большинство других ответов здесь, поэтому я рад оказать свою поддержку, даже поскольку я считаю, что игнорирование случая новой строки следует делать только с осторожностью.
Если вы сделаете ls не в текущем каталоге, то пути к файлам будут содержать '/', что означает, что grep -v '/' ни с чем не будет соответствовать. Я считаю, что grep -v '/$' - это то, что вы хотите исключить только каталоги.
@ waldol1: Спасибо; Я обновил ответ, включив в него ваше предложение, которое также делает команду grep концептуально более понятной. Обратите внимание, однако, что описываемая вами проблема могла бы возникнуть в случае нет с единственным путем к каталогу; например, ls -p /private/var по-прежнему будет печатать только имена файлов. Только если вы передадите аргументы файла несколько (обычно через глобус), вы увидите фактические пути в выводе; например, ls -p /private/var/* (и вы также увидите содержимое соответствующих подкаталогов, если вы также не включили -d).
Эти команды работают с файлами в текущем каталоге. Я хотел запустить указанную выше команду BSD для файлов в другом каталоге ... / mnt / usb / openwrt. Я адаптировал ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm -- к этому -> ls -tp /mnt/usb/openwrt | grep -v '/$' | tail -n +6 | sed 's|^|/mnt/usb/openwrt/|' | tr '\n' '\0' | xargs -0 rm --
@FlexMcMurphy, мне пришло в голову, что использование подоболочки ((...)) с cd проще и надежнее: (cd /mnt/usb/openwrt && ls -tp | grep -v '/$' | tail -n +6 | tr '\n' '\0' | xargs -0 rm --) - Я соответствующим образом обновил ответ.
Я превратил это в сценарий оболочки 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
нашел интересный cmd в Sed-One Liners - удалите последние 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
Я понимаю, что это старая ветка, но, возможно, кому-то это пригодится. Эта команда найдет файлы в текущем каталоге:
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 ; }'); 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
Возьмите второй столбец (имя файла, а не метку времени):
awk '{ print ; }'
А затем заверните все это в оператор for:
for F in $(); do rm $F; done
Это может быть более подробная команда, но мне гораздо больше повезло, что я смог настроить таргетинг на условные файлы и выполнить для них более сложные команды.
Удаляет все файлы, кроме 10 последних (большинство недавних).
ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm
Если меньше 10 файлов, файл не будет удален, и у вас будет: заголовок ошибки: недопустимое количество строк - 0
Мне нужно было элегантное решение для busybox (роутера), все решения xargs или массивов были для меня бесполезны - там нет такой команды. 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"
Мне это нужно было только для рассмотрения моих архивных файлов. изменить
ls -tнаls -td *.bz2