Minha pergunta é um pouco diferente de algumas perguntas mais antigas, simplesmente pedindo "excluir todos n
os arquivos, exceto os mais recentes, em um diretório".
Eu tenho um diretório que contém diferentes 'grupos' de arquivos onde cada grupo de arquivos compartilha algum prefixo arbitrário e cada grupo possui pelo menos um arquivo. Não conheço esses prefixos de antemão e não sei quantos grupos existem.
EDIT: na verdade, eu sei algo sobre os nomes dos arquivos, ou seja, todos seguem o padrão prefix-some_digits-some_digits.tar.bz2
. A única coisa que importa aqui é a prefix
parte, e podemos assumir que dentro de cada uma prefix
não há dígito ou travessão.
Quero fazer o seguinte em um bash
script:
Percorra o diretório fornecido, identifique todos os 'grupos' existentes e, para cada grupo de arquivos, exclua todos os
n
arquivos do grupo, exceto os mais recentes.Se houver menos de
n
arquivos para um grupo, não faça nada para esse grupo, ou seja, não exclua nenhum arquivo desse grupo.
Qual é uma maneira robusta e segura de fazer o que foi dito acima bash
? Você poderia explicar os comandos passo a passo?
Responder1
O roteiro:
#!/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
Explicação:
- Calcule os prefixos:
- Procure todos os arquivos seguindo a
something-something-something.tar.bz2
regex, cortando apenas a primeira parte até o primeiro traço e torne-o único. - o resultado é uma lista normalizada dos
PREFIXES
- Procure todos os arquivos seguindo a
- Iterar por tudo
PREFIXES
: - Calcular
ALL_FILES
comPREFIX
- Verifique se a quantidade
ALL_FILES
é menor que a quantidade de arquivos a serem mantidos -> se for verdade, podemos parar por aqui, nada a remover - Calcule os arquivos que são os arquivos
KEEP
mais recentesNUMKEEP
- Itere
ALL_FILES
e verifique se o arquivo fornecido não está naKEEP
lista de arquivos. Se sim: remova-o.
Resultado de exemplo ao executá-lo:
$ ./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.
Responder2
Conforme solicitado, esta resposta tende a ser "robusta e segura", conforme solicitado, em vez de rápida e suja.
Portabilidade: Esta resposta funciona em qualquer sistema que contenha sh
, find
, sed
, sort
, ls
, grep
, xargs
e rm
.
O script nunca deve engasgar com um diretório grande. Nenhuma expansão de nome de arquivo do shell é executada (o que pode sufocar se houver muitos arquivos, mas esse é um número enorme).
Esta resposta pressupõe que o prefixo não conterá nenhum traço ( -
).
Observe que, por design, o script lista apenas os arquivos que serão removidos. Você pode fazer com que ele remova os arquivos canalizando a saída do while
loop para xargs -d '/n' rm
o qual está comentado no script. Dessa forma, você pode testar facilmente o script antes de ativar o código de remoção.
#!/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 --
O parâmetro N (número de arquivos a serem mantidos) é padronizado como 64000 (ou seja, todos os arquivos são mantidos).
Código anotado
Obtenha o argumento da linha de comando e verifique o número inteiro por adição, se não for fornecido o padrão do parâmetro é 64000 (efetivamente todos):
NUM_TO_KEEP=$(( 0 + ${1:-64000} )) || exit 1
Encontre todos os arquivos no diretório atual que correspondam ao formato do nome do arquivo:
find . -maxdepth 1 -regex '[^-][^-]*-[0-9][0-9]*-[0-9][0-9]*.tar.bz2' |
Obter prefixo: remova tudo após o prefixo e remova o "./" no início:
sed 's/-.*//; s,^\./,,' |
Classifique os prefixos e remova duplicatas ( -u
--único):
sort -u |
Leia cada prefixo e processo:
while read prefix
do
Liste todos os arquivos no diretório classificados por hora, selecione os arquivos para o prefixo atual e exclua todas as linhas além dos arquivos que queremos manter:
ls -t | grep "^$prefix-.*-.*\.tar\.bz2$" | sed "1,$NUM_TO_KEEP d"
Para testar, comente o código para remover o arquivo. Usando xargs para evitar problemas com o comprimento da linha de comando ou espaços nos nomes de arquivos, se houver. Se você deseja que o script produza um log, adicione, -v
por rm
exemplo: rm -v --
. Remova o #
para ativar o código de remoção:
done # | xargs -d '\n' rm --
Se isso funcionar para você, aceite esta resposta e vote. Obrigado.
Responder3
Presumo que os arquivos sejam agrupados por prefixo quando listados em ordem lexical. Isso significa que não existem grupos com um prefixo que seja sufixo de outro grupo, por exemplo, não foo-1-2-3.tar.bz2
que ficaria entre foo-1-1.tar.bz2
e foo-1-2.tar.bz2
. Partindo dessa suposição, podemos listar todos os arquivos, e quando detectamos uma mudança de prefixo (ou para o primeiro arquivo), temos um novo grupo.
#!/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
Agora chegamos ao problema dedeterminando os arquivos mais antigos entre uma lista explícita.
Supondo que os nomes dos arquivos não contenham novas linhas ou caracteres que ls
não sejam exibidos literalmente, isso pode ser implementado com ls
:
keep_latest () (
n=$1; shift
if [ "$#" -le "$n" ]; then return; fi
unset IFS; set -f
set -- $(ls -t)
shift "$n"
rm -- "$@"
)
Responder4
Eu sei que isso está marcado bash
, mas acho que seria mais fácil com 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
o script aceita um argumento:n(o número de arquivos)
(:s,-,/,:h)
são modificadores glob, :s
substitui o primeiro -
por /
e :h
extrai o cabeçalho (a parte até a última barra que neste caso também é a primeira barra, pois há apenas uma)
(Om[1,-$N])
são qualificadores glob, Om
classifica os arquivos começando com o o mais antigo e [1,-$N]
seleciona do primeiro ao enésimo até o último.
Se você estiver satisfeito com o resultado, substitua print -rl
por rm
para realmente excluir os arquivos, por exemplo:
#!/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))])