Bash: comprueba el directorio de archivos con la lista de nombres de archivos parciales

8

Tengo un servidor que recibe un archivo por cliente cada día en un directorio. Los nombres de archivo se construyen de la siguiente manera:

uuid_datestring_other-data

Por ejemplo:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid Es un formato estándar uuid.
  • datestringes la salida de date +%Y%m%d.
  • other-data es de longitud variable pero nunca contendrá un guión bajo.

Tengo un archivo del formato:

#
d6f60016-0011-49c4-8fca-e2b3496ad5a7    client1
d5873483-5b98-4895-ab09-9891d80a13da    client2
be0ed6a6-e73a-4f33-b755-47226ff22401    another_client
...

Necesito verificar que cada uuid listado en el archivo tenga un archivo correspondiente en el directorio, usando bash.

Llegué hasta aquí, pero siento que vengo de la dirección incorrecta usando una instrucción if, y que necesito recorrer los archivos en el directorio de origen.

Las variables source_directory y uuid_list se han asignado anteriormente en el script:

# Check the entries in the file list

while read -r uuid name; do
# Ignore comment lines
   [[ $uuid = \#* ]] && continue
   if [[ -f "${source_directory}/${uuid}*" ]]
   then
      echo "File for ${name} has arrived"
   else
      echo "PANIC! - No File for ${name}"
   fi
done < "${uuid_list}"

¿Cómo debo verificar que los archivos en mi lista existen en el directorio? Me gustaría usar la funcionalidad bash en la medida de lo posible, pero no estoy en contra de usar comandos si es necesario.

Arronico
fuente
¿Pitón? ¿Y el directorio del servidor es "plano"?
Jacob Vlijm
Sí, es plano, no hay subdirectorios. Prefiero seguir con bash si es posible.
Arronical
1
Ok, no publicaré.
Jacob Vlijm
Realmente no veo qué le pasa a lo que tienes. Deberá recorrer los UUID o los archivos, ¿por qué un bucle sería mejor que el otro?
terdon

Respuestas:

5

Camine sobre los archivos, cree una matriz asociativa sobre los uuids contenidos en sus nombres (utilicé la expansión de parámetros para extraer el uuid). Luego, lea la lista, verifique la matriz asociativa para cada uuid e informe si el archivo se grabó o no.

#!/bin/bash
uuid_list=...

declare -A file_for
for file in *_*_* ; do
    uuid=${file%%_*}
    file_for[$uuid]=1
done

while read -r uuid name ; do
    [[ $uuid = \#* ]] && continue
    if [[ ${file_for[$uuid]} ]] ; then
        echo "File for $name has arrived."
    else
        echo "File for $name missing!"
    fi
done < "$uuid_list"
choroba
fuente
1
Agradable (+1), pero ¿por qué es esto mejor de lo que estaba haciendo el OP? Parece que estás haciendo lo mismo, pero en dos pasos en lugar de uno.
terdon
1
@terdon: La principal diferencia es que esto funciona :-) La expansión de comodines se realiza solo una vez, no cada vez que lee una línea de la lista, que también podría ser más rápida.
choroba
Sí, esa es una diferencia importante. Muy bien :)
terdon
Esto es maravilloso gracias, tengo mi +1. ¿Hay alguna forma de incluir la ruta al directorio que contiene los archivos? Sé que puedo cdingresar al directorio dentro de la secuencia de comandos, pero me preguntaba por el simple hecho de adquirir conocimiento.
Arronical
@Arronical: es posible, pero tendrá que eliminar la ruta de la cadena, posible con file=${file##*/}.
choroba
5

Aquí hay un enfoque más "bashy" y conciso:

#!/bin/bash

## Read the UUIDs into the array 'uuids'. Using awk
## lets us both skip comments and only keep the UUID
mapfile -t uuids < <(awk '!/^\s*#/{print $1}' uuids.txt)

## Iterate over each UUID
for uuid in ${uuids[@]}; do
        ## Set the special array $_ (the positional parameters: $1, $2 etc)
        ## to the glob matching the UUID. This will be all file/directory
        ## names that start with this UUID.
        set -- "${source_directory}"/"${uuid}"*
        ## If no files matched the glob, no file named $1 will exist
        [[ -e "$1" ]] && echo "YES : $1" || echo  "PANIC $uuid" 
done

Tenga en cuenta que si bien lo anterior es bonito y funcionará bien para algunos archivos, su velocidad depende de la cantidad de UUID y será muy lento si necesita procesar muchos. Si ese es el caso, use la solución de @ choroba o, para algo realmente rápido, evite el shell y llame perl:

#!/bin/bash

source_directory="."
perl -lne 'BEGIN{
            opendir(D,"'"$source_directory"'"); 
            foreach(readdir(D)){ /((.+?)_.*)/; $f{$2}=$1; }
           } 
           s/\s.*//; $f{$_} ? print "YES: $f{$_}" : print "PANIC: $_"' uuids.txt

Solo para ilustrar las diferencias de tiempo, probé mi enfoque bash, choroba's y mi perl en un archivo con 20000 UUID, de los cuales 18001 tenía un nombre de archivo correspondiente. Tenga en cuenta que cada prueba se ejecutó redirigiendo la salida del script a /dev/null.

  1. Mi fiesta (~ 3.5 min)

    real   3m39.775s
    user   1m26.083s
    sys    2m13.400s
  2. Choroba's (bash, ~ 0.7 sec)

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
  3. Mi perl (~ 0.1 seg):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
terdon
fuente
+1 para un método fantásticamente conciso, esto debería ejecutarse desde el directorio que contiene los archivos. Sé que puedo cdingresar al directorio en el script, pero ¿hay algún método por el cual la ruta del archivo se pueda incluir en la búsqueda?
Arronical
@Arronical seguro, ver respuesta actualizada. Puede usarlo ${source_directory}tal como lo hacía en su secuencia de comandos.
terdon
O úselo "$2"y páselo al guión como segundo argumento.
alexis
Verifique que esto se ejecute lo suficientemente rápido para sus propósitos: sería más rápido hacerlo con un solo escaneo de directorio, en lugar de muchas búsquedas de archivos como esta.
alexis
1
@ Alexis sí, tienes toda la razón. Hice algunas pruebas y esto se vuelve muy lento si aumenta la cantidad de UUID / archivos. Agregué un enfoque de perl (que se puede ejecutar como un trazador de líneas desde el script bash, por lo que técnicamente, todavía bash si está abierto a algunos nombres creativos) que es mucho más rápido.
terdon
3

Esto es puro Bash (es decir, sin comandos externos), y es el enfoque más coincidente que se me ocurre.

Pero en términos de rendimiento no es mucho mejor de lo que tiene actualmente.

Leerá cada línea de path/to/file; para cada línea, almacenará el primer campo $uuide imprime un mensaje si un archivo que coincida con el patrón path/to/directory/$uuid*es que no se encontró:

#! /bin/bash
[ -z "$2" ] && printf 'Not enough arguments.\n' && exit

while read uuid; do
    [ ! -f "$2/$uuid"* ] && printf '%s missing in %s\n' "$uuid" "$2"
done <"$1"

Llámalo con path/to/script path/to/file path/to/directory.

Salida de muestra usando el archivo de entrada de muestra en la pregunta en una jerarquía de directorio de prueba que contiene el archivo de muestra en la pregunta:

% tree
.
├── path
│   └── to
│       ├── directory
│       │   └── d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
│       └── file
└── script.sh

3 directories, 3 files
% ./script.sh path/to/file path/to/directory
d5873483-5b98-4895-ab09-9891d80a13da* missing in path/to/directory
be0ed6a6-e73a-4f33-b755-47226ff22401* missing in path/to/directory
kos
fuente
3
unset IFS
set -f
set +f -- $(<uuid_file)
while  [ "${1+:}" ]
do     : < "$source_directory/$1"*  &&
       printf 'File for %s has arrived.\n' "$2"
       shift 2
done

La idea aquí es no preocuparse por informar errores que el shell informará por usted. Si intenta <abrir un archivo que no existe, su shell se quejará. De hecho, antepondrá el script $0y el número de línea en el que ocurrió el error a la salida del error cuando ocurra ... Esta es una buena información que ya se proporciona de manera predeterminada, así que no se moleste.

Tampoco necesita tomar el archivo línea por línea de esa manera, puede ser muy lento. Esto expande todo en una sola toma a una matriz de argumentos delimitada por espacios en blanco y maneja dos a la vez. Si sus datos son consistentes con su ejemplo, $1siempre serán su uuid y $2serán suyos $name. Si bashpuede abrir una coincidencia con su uuid, y solo existe una de esas coincidencias, entonces printfsucede. De lo contrario, no lo hace y el shell escribe diagnósticos para explicar por qué.

mikeserv
fuente
1
@kos: ¿existe el archivo? si no, entonces se comporta según lo previsto. unset IFSasegura que $(cat <uuid_file)se divide en espacios en blanco. Los depósitos se dividen de manera $IFSdiferente cuando se compone solo de espacios en blanco o no está configurado. Tales expansiones divididas nunca tienen campos nulos porque todas las secuencias de espacios en blanco se presentan como un solo delimitador de campo. Creo que siempre que solo haya dos campos separados por espacios no blancos en cada línea, debería funcionar. en bash, de todos modos. set -fgarantiza que la expansión sin comillas no se interprete para los globos, y set + f asegura que los globos posteriores sí lo sean.
mikeserv
@kos: lo acabo de arreglar. No debería haber estado usando <>porque eso crea un archivo inexistente. <informará como lo dije. Sin embargo, el posible problema con eso, y la razón por la que usé incorrectamente <>en primer lugar, es que si se trata de un archivo de tubería sin un lector o como un char dev con buffer de línea, se bloqueará. eso podría evitarse manejando la salida de error de manera más explícita y haciendo [ -f "$dir/$1"* ]. estamos hablando de uuids aquí, por lo que nunca debería expandirse a más de un solo archivo. Sin embargo, es un poco agradable cómo informa los nombres de archivos fallidos a stderr así.
mikeserv
@kos: en realidad, supongo que podría usar ulimit para evitar que cree cualquier archivo, por lo <>que aún sería utilizable de esa manera ... <>es mejor si el globo podría expandirse a un directorio porque en un linux la lectura / escritura será fallar y decir: eso es un directorio.
mikeserv
@kos - ¡oh! Lo siento, estoy siendo tonto, tienes dos partidos, y está haciendo lo correcto. Me refiero a que se produzca un error de esa manera si se pudieran tener dos coincidencias, se supone que son uidas: nunca debería existir la posibilidad de 2 nombres similares que coincidan con el mismo globo. eso es totalmente intencional, y es ambiguo de una manera que no debería ser. ¿Ves lo que quiero decir? nombrar el archivo para un globo no es el problema, - los caracteres especiales no son relevantes aquí - el problema es que bashsolo aceptará un globo de redireccionamiento si solo coincide con un archivo. ver man bashbajo REDIRECCION.
mikeserv
1

La forma en que lo abordaría es obtener primero los uuids del archivo, luego usar find

awk '{print $1}' listfile.txt  | while read fileName;do find /etc -name "$fileName*" -printf "%p FOUND\n" 2> /dev/null;done

Para legibilidad,

awk '{print $1}' listfile.txt  | \
    while read fileName;do \
    find /etc -name "$fileName*" -printf "%p FOUND\n" 2> /dev/null;
    done

Ejemplo con una lista de archivos en /etc/busca de passwd, group, fstab y THISDOESNTEXIST nombres de archivo.

$ awk '{print $1}' listfile.txt  | while read fileName;do find /etc -name "$fileName*" -printf "%p FOUND\n" 2> /dev/null; done
/etc/pam.d/passwd FOUND
/etc/cron.daily/passwd FOUND
/etc/passwd FOUND
/etc/group FOUND
/etc/iproute2/group FOUND
/etc/fstab FOUND

Como ha mencionado que el directorio es plano, puede usar la -printf "%f\n"opción para imprimir solo el nombre del archivo

Lo que esto no hace es enumerar los archivos que faltan. findLa pequeña desventaja es que no le dice si no encuentra un archivo, solo cuando coincide con algo. Sin embargo, lo que se podría hacer es verificar el resultado: si el resultado está vacío, entonces nos falta un archivo

awk '{print $1}' listfile.txt  | while read fileName;do RESULT="$(find /etc -name "$fileName*" -printf "%p\n" 2> /dev/null )"; [ -z "$RESULT"  ] && echo "$fileName not found" || echo "$fileName found"  ;done

Más legible:

awk '{print $1}' listfile.txt  | \
   while read fileName;do \
   RESULT="$(find /etc -name "$fileName*" -printf "%p\n" 2> /dev/null )"; \
   [ -z "$RESULT"  ] && echo "$fileName not found" || \
   echo "$fileName found"  
   done

Y así es como funciona como un pequeño script:

skolodya@ubuntu:$ ./listfiles.sh                                               
passwd found
group found
fstab found
THISDONTEXIST not found

skolodya@ubuntu:$ cat listfiles.sh                                             
#!/bin/bash
awk '{print $1}' listfile.txt  | \
   while read fileName;do \
   RESULT="$(find /etc -name "$fileName*" -printf "%p\n" 2> /dev/null )"; \
   [ -z "$RESULT"  ] && echo "$fileName not found" || \
   echo "$fileName found"  
   done

Se podría usar statcomo alternativa, ya que es un directorio plano, pero el siguiente código no funcionará recursivamente para subdirectorios si alguna vez decide agregar esos:

$ awk '{print $1}' listfile.txt  | while read fileName;do  stat /etc/"$fileName"* 1> /dev/null ;done        
stat: cannot stat ‘/etc/THISDONTEXIST*’: No such file or directory

Si tomamos la statidea y la ejecutamos, podríamos usar el código de salida de stat como indicación de si un archivo existe o no. Efectivamente, queremos hacer esto:

$ awk '{print $1}' listfile.txt  | while read fileName;do  if stat /etc/"$fileName"* &> /dev/null;then echo "$fileName found"; else echo "$fileName NOT found"; fi ;done

Ejecución de muestra:

skolodya@ubuntu:$ awk '{print $1}' listfile.txt  | \                                                         
> while read FILE; do                                                                                        
> if stat /etc/"$FILE" &> /dev/null  ;then                                                                   
> echo "$FILE found"                                                                                         
> else echo "$FILE NOT found"                                                                                
> fi                                                                                                         
> done
passwd found
group found
fstab found
THISDONTEXIST NOT found
Sergiy Kolodyazhnyy
fuente