Como renomear um lote de arquivos de log de forma incremental sem sobrescrever usando o bash?

Como renomear um lote de arquivos de log de forma incremental sem sobrescrever usando o bash?

Recebi um problema e minha solução passou nos casos de teste iniciais, mas falhou 50% no envio.

O problema: um diretório contém vários arquivos e pastas; alguns desses arquivos são diferentes tipos de logs; error.log, error.log.1, error.log.2, access.log.1, access.log.2 ..etc O conteúdo desses arquivos é mapeado para o dia seguinte, então 'cat error.log.1' tem 'logs do dia 2' .. etc

A tarefa é incrementar o número apenas no final dos logs e deixar o restante do conteúdo do diretório inalterado. Além disso, crie um arquivo vazio para cada tipo de log.

Por exemplo:

./
example_dir
example2_dir
error.log
error.log.1
info.log.20
access.log.1
readme.txt

O script altera o diretório para:

./
example_dir (unchanged)
example2_dir (unchanged)
error.log (empty)
error.log.1 (originally error.log)
error.log.2 (originally error.log.1)
info.log (empty)
info.log.21 (originally info.log.20)
access.log (empty)
access.log.2 (originally access.log.1)
readme.txt (unchanged)

Condições: # Arquivos no diretório <1000, Max #Arquivos por tipo <21

Minha solução:

#!/bin/bash

declare -a filenames

# Renaming in ascending order will lead to overwrite; so start rename from the bottom

files=$(find . -maxdepth 1 -name "*.log.*" -exec basename {} \; | sort -rn)


for i in $files; do

    currentFileNumber=$(echo -e "$i" | sed -e 's/[^0-9]*//g') # Extract the current number from the filename
    fileName=$(echo -e "$i" | sed -e 's/\.[0-9]*$//g') # Extract the name without the trailing number

    newFileNumber=$(("$currentFileNumber" + 1)) # Increment the current number

    mv "$i" "$fileName.$newFileNumber" # Rename and append the incremented value

    if [[ ! ${filenames[*]} =~ ${fileName} ]] # Store names of existing types to create empty files
    then
        filenames=("${filenames[@]}" "${fileName}")
    fi
    # Could make use of [[ -e "$fileName.log" ]] instead of an array, but won't pass the test for some reason
done

for j in "${filenames[@]}"; do touch "$j"; done # Create the empty files
unset filenames

Ele não mostrou os casos de teste nos quais falhei, então não tenho certeza de como resolver isso melhor.

Responder1

Este foi um exercício divertido, então aqui está minha solução.

#/bin/bash
log_names=$(for logfile in $(find . -type f -name '*.log*'); do echo ${logfile%.[0-9]*}; done | sort -u)

for name in $log_names; do
    echo "Processing $name"
    i=20
    until [[ "$i" -eq 0 ]]; do
        if [[ -f "$name.$i" ]]; then
            next_num=$((i+1))
            mv -v "$name.$i" "$name.$next_num"
        fi
        i=$((i-1))
    done
    if [[ -f "$name" ]]; then
        mv -v "$name" "$name.1"
    fi
    touch "$name"
done

A variável log_names usa um findcomando para obter a lista de arquivos de log. Em seguida, aplico uma substituição de string para remover o sufixo numérico. Depois disso, classifico e removo as duplicatas.

Neste ponto, recebo uma lista de nomes exclusivos de arquivos de log no diretório: ./access.log ./error.log ./info.log.

Em seguida, processo cada nome por vez usando um forloop.

Agora, para cada arquivo, fomos informados de que o número máximo possível é 20. Começamos aí e usamos um untilloop para fazer a contagem regressiva.

A mvlógica é simples: se 'filname.number' existir, mova-o para 'filename.(number+1)'.

Quando o untilloop terminar (i = 0), podemos ter um arquivo não girado restante - aquele sem o sufixo numérico. Se isso acontecer, mova-o para filename.1.

A última etapa é criar um arquivo vazio com a extensão touch.


Execução de amostra:

$ ls
access.log.1  error.log  error.log.1  example_dir  example2_dir  info.log.20  readme.txt  rotate.bash
    
$ bash rotate.bash
Processing ./access.log
'./access.log.1' -> './access.log.2'
Processing ./error.log
'./error.log.1' -> './error.log.2'
'./error.log' -> './error.log.1'
Processing ./info.log
'./info.log.20' -> './info.log.21'

$ ls -1
access.log
access.log.2
error.log
error.log.1
error.log.2
example_dir
example2_dir
info.log
info.log.21
readme.txt
rotate.bash

Responder2

@Haxiel postou uma solução. Isso é semelhante ao que eu tinha em mente e ao que descrevi como "mais direto". Eu teria usado um forloop em vez do untilloop.

Isso é algo que utiliza quase o mínimo de processos externos, um mvpara cada arquivo existente e outro touchno final para criar os novos arquivos. (O toque pode ser substituído por um loop criando os arquivos usando redirecionamento para reduzir o número de processos externos em 1).

#!/bin/bash
shopt -s nullglob # Reduce the number of things we have to work with

# get a list of the files we want to work with. 
files=( *.log *.log.[1-9] *.log.[1-9][0-9] )

# reverse the list into rfiles, getting rid of non-file things
rfiles=()
for ((i=${#files[@]}-1;i>=0;i--)) ; do
        if [ -f "${files[i]}" ] ; then
                rfiles+=("${files[i]}")
        fi
done

# exit early if there is nothing to do
if [ ${#rfiles[@]} -eq 0 ] ; then
        exit 0
fi

# an array of the files we need to create
typeset -A newfiles

# Loop over the reversed file list
for f in "${rfiles[@]}"; do
    # Get everything up to the last "log"
    baseName=${f%log*}log
    # Remove up to the last "log" and then the optional "."
    currentFileNum=${f#"$baseName"}
    currentFileNum=${currentFileNum#.}
    mv -v "$f" "$baseName.$((currentFileNum+1))"
    # record the name to make the new files
    newfiles[$baseName]=1
done

# Create all the needed new files, using the names stored in the array
touch "${!newfiles[@]}"

A ordem em que isso faz as coisas é diferente daquela produzida pela solução @Haxiel, que move primeiro todos os arquivos com números de 2 dígitos, depois todos os arquivos com números de um dígito e, finalmente, os arquivos que terminam em ".log", em vez de processar todos os arquivos com as mesmas primeiras partes juntos.

A pergunta original dizia que existem menos de 1.000 arquivos e menos de 21 versões por arquivo. Não disse o que fazer se houver mais do que esse número. Esta solução atende até 100 versões por arquivo e pode ser estendida para 1.000 ou mais apenas estendendo o padrão.

O número de arquivos é limitado pela quantidade de memória disponível para o bash.

Acredito que esta seja uma solução melhor, pois tenta apenas lidar com arquivos existentes, em vez de tentar N arquivos para cada nome. Quando N é pequeno (como 21), isso não importa.

informação relacionada