Удалите все файлы X, кроме самых последних, в bash.
Вопрос
Есть ли простой способ в довольно стандартной среде UNIX с bash запустить команду для удаления из каталога всех X-файлов, кроме самых последних?
Чтобы дать более конкретный пример, представьте себе, что какое-то задание cron записывает файл (скажем, файл журнала или tar-резервную копию) в каталог каждый час.Мне нужен способ запустить еще одно задание cron, которое удалит самые старые файлы в этом каталоге, пока их не станет меньше, скажем, 5.
И чтобы внести ясность: присутствует только один файл, его ни в коем случае нельзя удалять.
Решение
Проблемы с существующими ответами:
- невозможность обработки имен файлов со встроенными пробелами или символами новой строки.
- в случае решений, которые вызывают
rm
непосредственно при подстановке команды без кавычек (rm `...`
), существует дополнительный риск непреднамеренного подстановки.
- в случае решений, которые вызывают
- неспособность различать файлы и каталоги (т.е. если каталоги оказался среди 5 последних измененных элементов файловой системы, вы фактически сохраните меньше более 5 файлов и применение
rm
в каталоги не удастся).
ответ wnoise решает эти проблемы, но решение ГНУ-специфический (и довольно сложный).
Вот прагматик, POSIX-совместимое решение это идет только с одно предостережение:он не может обрабатывать имена файлов со встроенными новые строки - но я не считаю это реальной проблемой для большинства людей.
Для справки, вот объяснение того, почему, как правило, не рекомендуется анализировать ls
выход: http://mywiki.wooledge.org/ParsingLs
ls -tp | grep -v '/$' | tail -n +6 | xargs -I {} rm -- {}
Вышеупомянутое неэффективный, потому что xargs
должен вызвать rm
один раз за каждый имя файла.
Ваша платформа xargs
может позволить вам решить эту проблему:
Если у вас есть ГНУ xargs
, использовать -d '\n'
, что делает xargs
рассматривать каждую строку ввода как отдельный аргумент, но передавать столько аргументов, сколько поместится в командной строке однажды:
ls -tp | grep -v '/$' | tail -n +6 | xargs -d '\n' -r rm --
-r
(--no-run-if-empty
) гарантирует, что rm
не вызывается, если нет ввода.
Если у вас есть БСД xargs
(в том числе на ОС Х), вы можете использовать -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; обратите внимание, что это не будет работать должным образом, если имена файлов содержат символы новой строки и/или каталог содержит подкаталоги.
Игнорирование новых строк означает игнорирование безопасности и хорошего кодирования.У wnoise был единственный хороший ответ.Вот его вариант, в котором имена файлов помещаются в массив $x.
while IFS= read -rd ''; do
x+=("${REPLY#* }");
done < <(find . -maxdepth 1 -printf '%T@ %p\0' | sort -r -z -n )
Если в именах файлов нет пробелов, это будет работать:
ls -C1 -t| awk 'NR>5'|xargs rm
Если в именах файлов есть пробелы, что-то вроде
ls -C1 -t | awk 'NR>5' | sed -e "s/^/rm '/" -e "s/$/'/" | sh
Основная логика:
- получить список файлов в порядке времени, один столбец
- получить все, кроме первых 5 (в этом примере n=5)
- первая версия:отправь это в РМ
- вторая версия:создайте скрипт, который удалит их правильно
С зш
Предполагая, что вас не волнуют существующие каталоги, и у вас не будет более 999 файлов (выберите большее число, если хотите, или создайте цикл while).
[ 6 -le `ls *(.)|wc -l` ] && rm *(.om[6,999])
В *(.om[6,999])
, .
означает файлы, o
означает порядок сортировки, m
значит по дате модификации (ставь a
для времени доступа или c
для изменения индексного дескриптора), [6,999]
выбирает диапазон файлов, поэтому сначала не устанавливает 5.
Я понимаю, что это старая тема, но, возможно, кому-то это будет полезно.Эта команда найдет файлы в текущем каталоге:
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
Возьмите второй столбец (имя файла, а не временную метку):
awk '{ print $2; }'
А затем оберните все это в оператор for:
for F in $(); do rm $F; done
Возможно, это более подробная команда, но мне повезло больше: я смог нацелиться на условные файлы и выполнить над ними более сложные команды.
нашел интересный 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
Удаляет все файлы, кроме 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 "VarName="$1
чтобы установить переменную VarName
к значению $1
.За один раз можно создать несколько переменных.Этот VarName
становится обычной переменной sh и впоследствии может нормально использоваться в скрипте или оболочке.Итак, чтобы создать переменные с помощью awk и вернуть их оболочке:
eval $(ls -td *.tar | awk 'NR>7 { print "VarName=\""$1"\"" }'); echo "$VarName"
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 (предположим, в других дистрибутивах то же самое, я получаю:РМ:невозможно удалить каталог `..'
что очень раздражает..
В любом случае я подправил вышеизложенное, а также добавил в команду grep.В моем случае у меня есть 6 файлов резервных копий в каталоге, например.file1.tar file2.tar file3.tar и т. д., и я хочу удалить только самый старый файл (в моем случае удалить первый файл)
Сценарий, который я запустил для удаления самого старого файла, был:
ls -c1 -t | Греп -файл | awk 'nr> 5' | xargs rm
Это (как указано выше) удаляет первый из моих файлов, например.file1.tar это также остается с файлом2 файл3 файл4 файл5 и файл6