Удалить все, кроме самых последних файлов X в Bash
Есть ли простой способ в довольно стандартной среде UNIX с bash выполнить команду, чтобы удалить все, кроме самых последних X-файлов из каталога?
Чтобы привести немного более конкретный пример, представьте себе, что какое-то задание cron записывает файл (скажем, файл журнала или архивную резервную копию) в каталог каждый час. Я хотел бы иметь способ запустить еще одно задание cron, которое бы удаляло самые старые файлы в этом каталоге, пока их не станет меньше, скажем, 5.
И чтобы было ясно, присутствует только один файл, он никогда не должен быть удален.
21 ответ
Проблемы с существующими ответами:
- невозможность обрабатывать имена файлов со встроенными пробелами или символами новой строки.
- в случае решений, которые вызывают
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
(в том числе на OS X), вы можете использовать -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
Удалите все кроме 5 (или любого другого числа) самых последних файлов в каталоге.
rm `ls -t | awk 'NR>5'`
(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:
ls -tr | head -n -5 | xargs --no-run-if-empty rm
ls -tr отображает все файлы, сначала самые старые (сначала сначала -t, либо наоборот -r).
head -n -5 отображает все, кроме 5 последних строк (то есть 5 новейших файлов).
xargs rm вызывает rm для каждого выбранного файла.
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, но обрабатывает файлы со встроенными символами новой строки или пробелами.
Все эти ответы терпят неудачу, когда есть каталоги в текущем каталоге. Вот то, что работает:
find . -maxdepth 1 -type f | xargs -x ls -t | awk 'NR>5' | xargs -L1 rm
Это:
работает, когда есть каталоги в текущем каталоге
пытается удалить каждый файл, даже если предыдущий не может быть удален (из-за разрешений и т. д.)
отказоустойчив, когда количество файлов в текущем каталоге чрезмерно и
xargs
будет нормально тебя перепутать-x
)не учитывает пробелы в именах файлов (возможно, вы используете не ту ОС?)
ls -tQ | tail -n+4 | xargs rm
Список имен файлов по времени изменения, цитируя каждое имя файла. Исключить первые 3 (3 самых последних). Удалить оставшиеся.
РЕДАКТИРОВАТЬ после полезного комментария от mklement0 (спасибо!): Исправлен аргумент -n+3, и обратите внимание, что это не будет работать должным образом, если имена файлов содержат символы новой строки и / или каталог содержит подкаталоги.
Для Linux (инструменты GNU) эффективный и надежный способ сохранитьn
новейшие файлы в текущем каталоге при удалении остальных:
n=5
find . -maxdepth 1 -type f -printf '%T@ %p\0' |
sort -z -nrt ' ' -k1,1 |
sed -z -e "1,${n}d" -e 's/[^ ]* //' |
xargs -0r rm -f
Для БСД,find
не имеет-printf
предикат, не может выводить байты NULL и + не может обрабатыватьNULL
-записи с разделителями.
Вот решение, которое не поддерживает новые строки в путях, но защищает от них, отфильтровывая их:
#!/bin/bash
n=5
find . -maxdepth 1 -type f ! -path $'*\n*' -exec stat -f '%.9Fm %N' {} + |
sort -nrt ' ' -k1,1 |
awk -v n="$n" -F'^[^ ]* ' 'NR > n {printf "%s%c", $2, 0}' |
xargs -0 rm -f
примечание: я использую
bash
из-за
$'\n'
обозначение. Поскольку вы можете определить переменную, содержащую буквальный перевод строки, и использовать ее вместо этого.
Решение для UNIX и Linux (вдохновлено AIX/HP-UX/SunOS/BSD/Linux).ls -b
):
Некоторые платформы не предоставляютfind -printf
, ни , ни поддержкаNUL
-записи с разделителями //awk
//. Вот почему использование, вероятно, является наиболее переносимым способом решения проблемы, потому что оно доступно по умолчанию почти в каждой ОС.
Я мог бы все это написать, но не стал. Я использую его только для замены и кодирования-декодирования-экранирования имен файлов. Основная логика такая же, как и в предыдущих решениях, и реализована с помощью инструментов POSIX.
примечание: значение по умолчанию имеет разрешение секунды, но начиная сperl-5.8.9
вы можете получить разрешение менее секунды с помощьюstat
функция модуляTime::HiRes
(когда это поддерживают и ОС, и файловая система). Это то, что я использую здесь; если вы не предоставляете его, вы можете удалить‑MTime::HiRes=stat
из командной строки.
n=5
find . '(' -name '.' -o -prune ')' -type f -exec \
perl -MTime::HiRes=stat -le '
foreach (@ARGV) {
@st = stat($_);
if ( @st > 0 ) {
s/([\\\n])/sprintf( "\\%03o", ord($1) )/ge;
print sprintf( "%.9f %s", $st[9], $_ );
}
else { print STDERR "stat: $_: $!"; }
}
' {} + |
sort -nrt ' ' -k1,1 |
sed -e "1,${n}d" -e 's/[^ ]* //' |
perl -l -ne '
s/\\([0-7]{3})/chr(oct($1))/ge;
s/(["\n])/"\\$1"/g;
print "\"$_\"";
' |
xargs -E '' sh -c '[ "$#" -gt 0 ] && rm -f "$@"' sh
Пояснения:
Для каждого найденного файла первый получает время модификации и выводит его вместе с закодированным именем файла (каждый
newline
иbackslash
символы заменяются литералами\012
и\134
соответственно).Теперь каждый
time filename
гарантированно будет однострочным, поэтому POSIXsort
иsed
можно спокойно работать с этим потоком.Второй
perl
декодирует имена файлов и экранирует их для POSIX.Наконец, призывает удалить файлы.
sh
команда — это уловка, которая предотвращаетxargs
от бегаrm
когда нет файлов для удаления.
Игнорирование перевода строки игнорирует безопасность и хорошее кодирование. У wnoise был единственный хороший ответ. Вот вариант его, который помещает имена файлов в массив $x
while IFS= read -rd ''; do
x+=("${REPLY#* }");
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Я понимаю, что это старая ветка, но, возможно, кто-то от этого выиграет. Эта команда найдет файлы в текущем каталоге:
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
Это может быть более многословная команда, но мне повезло больше, когда я смог нацелиться на условные файлы и выполнить с ними более сложные команды.
Если имена файлов не имеют пробелов, это будет работать:
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 скрипт, который удалит их правильно
С зш
Предполагая, что вам нет дела до существующих каталогов, и у вас будет не более 999 файлов (выберите большее число, если хотите, или создайте цикл while).
[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])
В *(.om[6,999])
, .
означает, что файлы o
означает порядок сортировки, m
значит по дате внесения изменений (поставить a
за время доступа или c
для изменения индекса), [6,999]
выбирает диапазон файла, поэтому сначала не нужно 5
Адаптация отличного ответа @mklement0 с некоторыми параметрами и без необходимости перехода к папке, содержащей файлы, которые нужно удалить...
TARGET_FOLDER="/my/folder/path"
FILES_KEEP=5
ls -tp "$TARGET_FOLDER"**/* | grep -v '/$' | tail -n +$((FILES_KEEP+1)) | xargs -d '\n' -r rm --
[Ссылка (ы).: https://stackoverflow.com/a/3572628/3223785]
Спасибо!
Мне нужно было элегантное решение для 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 "VarName="$1
установить переменную VarName
к стоимости $1
, Несколько переменных могут быть созданы за один раз. это VarName
становится обычной переменной sh и впоследствии может использоваться в скрипте или оболочке. Итак, чтобы создать переменные с помощью awk и вернуть их обратно в оболочку:
eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\"" }'); echo "$VarName"
Удаляет все, кроме 10 последних (большинство последних) файлов
ls -t1 | head -n $(echo $(ls -1 | wc -l) - 10 | bc) | xargs rm
Если менее 10 файлов, ни один файл не будет удален, и у вас будет: error head: недопустимое количество строк - 0
Нашел интересный 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
Начиная с SC2010 — ShellCheck Wiki предупреждает о синтаксическом анализеls
вывод плохой, я предлагаю простой однострочник сstat
без загадочногоawk
/sed
сценарий:
stat -c '%y %n' /path/* | sort -r | tail -n +2 | cut -d' ' -f4 | xargs -r rm -v --
tail
's+2
Параметр контролирует, сколько самых новых файлов осталось, т.е. не было удалено. Для входного значенияN
у тебя осталосьN-1
из новейших файлов.
Модифицированная версия ответа @Fabien, если вы хотите указать путь. Полезно, если вы запускаете скрипт в другом месте.
ls -tr /path/foo/ | head -n -5 | xargs -I% --no-run-if-empty rm /path/foo/%
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
Я сделал это в скрипт оболочки 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
Запуск на Debian (предположим, что то же самое на других дистрибутивах, которые я получаю: rm: не могу удалить каталог `..'
что довольно раздражает..
Во всяком случае я поправил выше, а также добавил grep в команду. В моем случае у меня есть 6 файлов резервных копий в каталоге, например, file1.tar file2.tar file3.tar и т. Д., И я хочу удалить только самый старый файл (удалить первый файл в моем случае)
Сценарий, который я запустил для удаления самого старого файла:
ls -C1 -t | grep файл | awk 'NR>5'|xargs rm
Это (как и выше) удаляет первый из моих файлов, например, file1.tar, это также остается с file2 file3 file4 file5 и file6