¿Cómo cambiar el nombre de un lote de archivos de registro de forma incremental sin sobrescribir usando bash?

¿Cómo cambiar el nombre de un lote de archivos de registro de forma incremental sin sobrescribir usando bash?

Me dieron un problema y mi solución pasó los casos de prueba iniciales pero falló el 50% al enviarla.

El problema: un directorio contiene varios archivos y carpetas, algunos de estos archivos son diferentes tipos de registros; error.log, error.log.1, error.log.2, access.log.1, access.log.2 ..etc. El contenido de esos archivos se asigna al día siguiente, por lo que 'cat error.log.1' tiene 'registros del día 2'... etc.

La tarea consiste en incrementar el número al final de los registros únicamente y dejar el resto del contenido del directorio sin cambios. Además, cree un archivo vacío para cada tipo de registro.

Por ejemplo:

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

El script cambia el directorio a:

./
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)

Condiciones: # Archivos en el directorio < 1000, Max # Archivos por tipo < 21

Mi solución:

#!/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

No mostró los casos de prueba en los que fallé, por lo que no estoy seguro de cómo resolver esto mejor.

Respuesta1

Este fue un ejercicio divertido, así que aquí está mi solución.

#/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

La variable log_names utiliza un findcomando para obtener la lista de archivos de registro. Luego, aplico una sustitución de cadena para eliminar el sufijo numérico. Después de eso, clasifico y elimino los duplicados.

En este punto, obtengo una lista de los nombres de archivos de registro únicos en el directorio: ./access.log ./error.log ./info.log.

Luego, proceso cada nombre por turno usando un forbucle.

Ahora, para cada archivo, nos han dicho que el número máximo posible es 20. Comenzamos allí y usamos un untilbucle para realizar la cuenta regresiva.

La mvlógica es simple: si 'filname.number' existe, muévalo a 'filename.(number+1)'.

Cuando untilfinaliza el ciclo (i = 0), es posible que nos quede un archivo sin rotar, el que no tiene el sufijo numérico. Si es así, muévalo al nombre de archivo.1.

El último paso es crear un archivo vacío con touch.


Ejecución de muestra:

$ 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

Respuesta2

@Haxiel ha publicado una solución. Esto es similar al que tenía en mente y al que describí como "más sencillo". Habría usado un forbucle en lugar del untilbucle.

Esto es algo que utiliza casi la cantidad mínima de procesos externos, uno mvpara cada archivo existente y uno touchal final para crear los archivos nuevos. (El toque podría reemplazarse por un bucle que crea los archivos usando la redirección para reducir la cantidad de procesos externos en 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[@]}"

El orden en que hace las cosas es diferente al producido por la solución de @Haxiel, esto mueve primero todos los archivos con números de 2 dígitos, luego todos los archivos con números de un solo dígito y finalmente los archivos que terminan en ".log", en lugar de procesar todos los archivos con las mismas primeras partes juntos.

La pregunta original decía que hay menos de 1000 archivos y menos de 21 versiones por archivo. No dijo qué hacer si hay más que este número. Esta solución admite hasta 100 versiones por archivo y se puede ampliar a 1000 o más simplemente ampliando el patrón.

La cantidad de archivos está limitada por la cantidad de memoria disponible para bash.

Creo que esta es una mejor solución ya que solo intenta manejar archivos que existen en lugar de probar N archivos para cada nombre. Cuando N es pequeño (como 21), esto no importa.

información relacionada