Мой вопрос немного отличается от некоторых старых вопросов, в которых просто спрашивалось «удаление всех n
файлов в каталоге, кроме самых последних».
У меня есть каталог, содержащий различные «группы» файлов, где каждая группа файлов имеет некоторый произвольный префикс, и в каждой группе есть по крайней мере один файл. Я не знаю эти префиксы заранее и не знаю, сколько групп.
EDIT: на самом деле, я знаю кое-что об именах файлов, то есть они все следуют шаблону prefix-some_digits-some_digits.tar.bz2
. Единственное, что здесь имеет значение, это prefix
часть, и мы можем предположить, что внутри каждого prefix
нет ни цифры, ни тире.
Я хочу сделать в скрипте следующее bash
:
Просмотрите указанный каталог, определите все существующие «группы» и для каждой группы файлов удалите все, кроме самых последних
n
файлов группы.Если файлов для группы меньше
n
, ничего не делать для этой группы, т. е. не удалять ни одного файла для этой группы.
Какой надежный и безопасный способ сделать вышеизложенное в bash
? Не могли бы вы объяснить команды пошагово?
решение1
Сценарий:
#!/bin/bash
# Get Prefixes
PREFIXES=$(ls | grep -Po '^(.*)(?!HT\d{4})-(.*)-(.*).tar.bz2$' | awk -F'-' '{print $1}' | uniq)
if [ -z "$1" ]; then
echo need a number of keep files.
exit 1
else
NUMKEEP=$1
fi
for PREFIX in ${PREFIXES}; do
ALL_FILES=$(ls -t ${PREFIX}*)
if [ $(echo ${ALL_FILES} | wc -w) -lt $NUMKEEP ]; then
echo Not enough files to be kept. Quit.
continue
fi
KEEP=$(ls -t ${PREFIX}* | head -n${NUMKEEP})
for file in $ALL_FILES ; do
if [[ "$KEEP" =~ "$file" ]]; then
echo keeping $file
else
echo RM $file
fi
done
done
Объяснение:
- Рассчитайте префиксы:
- Найдите все файлы, соответствующие
something-something-something.tar.bz2
регулярному выражению, вырезая только первую часть до первого тире и делая ее уникальной. - результатом является нормализованный список
PREFIXES
- Найдите все файлы, соответствующие
- Повторить все
PREFIXES
: - Рассчитать
ALL_FILES
с помощьюPREFIX
- Проверьте, меньше ли количество
ALL_FILES
файлов, которые нужно сохранить -> если да, то можно остановиться, удалять нечего - Вычислите
KEEP
самые последние файлыNUMKEEP
. - Пройдитесь
ALL_FILES
и проверьте, нет ли указанного файла вKEEP
списке файлов. Если да: удалите его.
Пример результата при запуске:
$ ./remove-old.sh 2
keeping bar-01-01.tar.bz2
keeping bar-01-02.tar.bz2
RM bar-01-03.tar.bz2
RM bar-01-04.tar.bz2
RM bar-01-05.tar.bz2
RM bar-01-06.tar.bz2
keeping foo-01-06.tar.bz2
keeping foo-01-05.tar.bz2
RM foo-01-04.tar.bz2
RM foo-01-03.tar.bz2
RM foo-01-02.tar.bz2
$ ./remove-old.sh 8
Not enough files to be kept. Quit.
Not enough files to be kept. Quit.
решение2
Как вы и просили, этот ответ скорее «надежный и безопасный», чем «быстрый и грязный».
Переносимость: этот ответ работает в любой системе, которая содержит sh
, find
, sed
, sort
, ls
, grep
, xargs
и rm
.
Скрипт никогда не должен захлёбываться на большом каталоге. Расширение имени файла оболочки не выполняется (что может захлёбываться, если файлов слишком много, но это огромное число).
В этом ответе предполагается, что префикс не будет содержать тире ( -
).
Обратите внимание, что по замыслу скрипт перечисляет только файлы, которые будут удалены. Вы можете заставить его удалить файлы, передав вывод цикла, while
который xargs -d '/n' rm
закомментирован в скрипте. Таким образом, вы можете легко протестировать скрипт перед включением кода удаления.
#!/bin/sh -e
NUM_TO_KEEP=$(( 0 + ${1:-64000} )) || exit 1
find . -maxdepth 1 -regex '[^-][^-]*-[0-9][0-9]*-[0-9][0-9]*.tar.bz2' |
sed 's/-.*//; s,^\./,,' |
sort -u |
while read prefix
do
ls -t | grep "^$prefix-.*-.*\.tar\.bz2$" | sed "1,$NUM_TO_KEEP d"
done # | xargs -d '\n' rm --
Параметр N (количество сохраняемых файлов) по умолчанию равен 64000 (т.е. сохраняются все файлы).
Аннотированный код
Получите аргумент командной строки и проверьте наличие целого числа путем сложения; если параметр не указан, то по умолчанию используется значение 64000 (фактически все):
NUM_TO_KEEP=$(( 0 + ${1:-64000} )) || exit 1
Найти все файлы в текущем каталоге, соответствующие формату имени файла:
find . -maxdepth 1 -regex '[^-][^-]*-[0-9][0-9]*-[0-9][0-9]*.tar.bz2' |
Получить префикс: удалить все после префикса и удалить «./» в начале:
sed 's/-.*//; s,^\./,,' |
Отсортируйте префиксы и удалите дубликаты ( -u
--уникальные):
sort -u |
Прочитайте каждый префикс и процесс:
while read prefix
do
Выведите список всех файлов в каталоге, отсортированных по времени, выберите файлы для текущего префикса и удалите все строки за пределами файлов, которые мы хотим сохранить:
ls -t | grep "^$prefix-.*-.*\.tar\.bz2$" | sed "1,$NUM_TO_KEEP d"
Для тестирования закомментируйте код для удаления файла. Используйте xargs, чтобы избежать проблем с длиной командной строки или пробелами в именах файлов, если таковые имеются. Если вы хотите, чтобы скрипт создавал журнал, добавьте, -v
например rm
: rm -v --
. Удалите, #
чтобы включить код удаления:
done # | xargs -d '\n' rm --
Если это вам подходит, пожалуйста, примите этот ответ и проголосуйте за него. Спасибо.
решение3
Я предположу, что файлы сгруппированы по префиксу, когда перечислены в лексическом порядке. Это означает, что нет групп с префиксом, который является суффиксом другой группы, например, no, foo-1-2-3.tar.bz2
которая могла бы оказаться между foo-1-1.tar.bz2
и foo-1-2.tar.bz2
. При таком предположении мы можем перечислить все файлы, и когда мы обнаруживаем изменение префикса (или для самого первого файла), у нас есть новая группа.
#!/bin/bash
n=$1; shift # number of files to keep in each group
shopt extglob
previous_prefix=-
for x in *-+([0-9])-+([0-9]).tar.bz2; do
# Step 1: skip the file if its prefix has already been processed
this_prefix=${x%-+([0-9])-+([0-9]).tar.bz2}
if [[ "$this_prefix" == "$previous_prefix" ]]; then
continue
fi
previous_prefix=$this_prefix
# Step 2: process all the files with the current prefix
keep_latest "$n" "$this_prefix"-+([0-9])-+([0-9]).tar.bz2
done
Теперь мы подошли к проблемеопределение самых старых файлов среди явного списка.
Предполагая, что имена файлов не содержат символов новой строки или символов, которые ls
не отображаются буквально, это можно реализовать с помощью ls
:
keep_latest () (
n=$1; shift
if [ "$#" -le "$n" ]; then return; fi
unset IFS; set -f
set -- $(ls -t)
shift "$n"
rm -- "$@"
)
решение4
Я знаю, что это отмечено тегом bash
, но я думаю, было бы проще с zsh
:
#!/usr/bin/env zsh
N=$(($1 + 1)) # calculate Nth to last
typeset -U prefixes # declare array with unique elements
prefixes=(*.tar.bz2(:s,-,/,:h)) # save prefixes in the array
for p in $prefixes # for each prefix
do
arr=(${p}*.tar.bz2) # save filenames starting with prefix in arr
if [[ ${#arr} -gt $1 ]] # if number of elements is greather than $1
then
print -rl -- ${p}*.tar.bz2(Om[1,-$N]) # print all filenames but the most recent N
fi
done
скрипт принимает один аргумент:н(количество файлов)
(:s,-,/,:h)
— это глобальные модификаторы, :s
заменяют первый -
на /
и :h
извлекают заголовок (часть до последнего слеша, которая в данном случае также является первым слешем, поскольку он всего один)
(Om[1,-$N])
— это глобальные квалификаторы, Om
сортируют файлы, начиная с самого старого, и [1,-$N]
выбирают файлы от первого до N-го до последнего
Если вы довольны результатом, замените print -rl
на rm
, чтобы фактически удалить файлы, например:
#!/usr/bin/env zsh
typeset -U prefixes
prefixes=(*.tar.bz2(:s,-,/,:h))
for p in $prefixes
arr=(${p}*.tar.bz2) && [[ ${#arr} -gt $1 ]] && rm -- ${p}*.tar.bz2(Om[1,-$(($1+1))])