web-dev-qa-db-fra.com

Bash - Vérifier le répertoire des fichiers par rapport à la liste des noms de fichiers partiels

J'ai un serveur qui reçoit chaque jour un fichier par client dans un répertoire. Les noms de fichiers sont construits comme suit:

uuid_datestring_other-data

Par exemple:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid est un format standard uuid.
  • datestring est la sortie de date +%Y%m%d.
  • other-data a une longueur variable, mais ne contient jamais de trait de soulignement.

J'ai un fichier du format:

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

Je dois vérifier que chaque uuid répertorié dans le fichier a un fichier correspondant dans le répertoire, en utilisant bash.

J'ai atteint ce stade, mais je sens que je viens de la mauvaise direction en utilisant une instruction if et que je dois parcourir les fichiers du répertoire source.

Les variables source_directory et uuid_list ont déjà été affectées dans le 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}"

Comment dois-je vérifier que les fichiers de ma liste existent dans le répertoire? Je voudrais utiliser la fonctionnalité bash autant que possible, mais je ne suis pas contre l'utilisation de commandes si besoin est.

8
Arronical

Parcourez les fichiers, créez un tableau associatif sur les uuids contenus dans leurs noms (j’ai utilisé le développement de paramètres pour extraire le uuid). Ensuite, lisez la liste, vérifiez le tableau associatif pour chaque uuid et indiquez si le fichier a été enregistré ou non.

#!/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"
5
choroba

Voici une approche plus "bashy" et concise:

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

Notez que bien que ce qui précède soit joli et fonctionne correctement pour quelques fichiers, sa vitesse dépend du nombre d’UUID et sera très lent si vous devez en traiter plusieurs. Si tel est le cas, utilisez la solution de @ choroba ou, pour obtenir un résultat vraiment rapide, évitez Shell et appelez 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

Juste pour illustrer les différences de temps, j'ai testé mon approche bash, celle de choroba et celle de Perl, sur un fichier contenant 20000 UUID, dont 18001 avait un nom de fichier correspondant. Notez que chaque test a été exécuté en redirigeant la sortie du script vers /dev/null.

  1. Mon bash (~ 3.5 min)

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

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
    
  3. Mon Perl (~ 0.1 sec):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
    
5
terdon

C’est du pur Bash (c’est-à-dire aucune commande externe), et c’est l’approche la plus coïncidente à laquelle je puisse penser.

Mais la performance n'est vraiment pas meilleure que ce que vous avez actuellement.

Il lira chaque ligne de path/to/file; pour chaque ligne, il stockera le premier champ dans $uuid et affichera un message si un fichier correspondant au modèle path/to/directory/$uuid* est not trouvé:

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

Appelez-le avec path/to/script path/to/file path/to/directory.

Exemple de sortie utilisant le fichier d'entrée exemple de la question dans une hiérarchie de répertoires de test contenant le fichier exemple de la question:

% 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
3
kos
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

L'idée ici n'est pas de vous inquiéter des erreurs de rapport que Shell vous rapportera. Si vous essayez d'ouvrir < un fichier qui n'existe pas, votre Shell se plaindra. En fait, il va précéder le $0 de votre script et le numéro de ligne sur lequel l'erreur s'est produite dans la sortie d'erreur quand c'est le cas ... C'est une bonne information qui est déjà fournie par défaut - ne vous inquiétez donc pas.

De plus, vous n'avez pas besoin de placer le fichier ligne par ligne comme cela - il peut être extrêmement lent. Cela étend le tout en un seul coup à un tableau d'arguments délimité par des espaces et il en gère deux à la fois. Si vos données correspondent à votre exemple, alors $1 sera toujours votre uuid et $2 sera votre $name. Si bash peut ouvrir une correspondance avec votre uuid - et seulement n une telle correspondance existe - alors printf se produit. Sinon, le shell écrit les diagnostics pour expliquer pourquoi.

3
mikeserv

La façon dont je l’approcherais serait d’obtenir les uuids du fichier d’abord, puis d’utiliser find

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

Pour la lisibilité,

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

Exemple avec une liste de fichiers dans /etc/, à la recherche de noms de fichiers passwd, group, fstab et THISDOESNTEXIST.

$ 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

Puisque vous avez mentionné que le répertoire est à plat, vous pouvez utiliser l’option -printf "%f\n" pour simplement imprimer le nom du fichier.

Ce que cela ne fait pas, c'est de lister les fichiers manquants. Le petit inconvénient de find est qu'il ne vous dit pas s'il ne trouve pas un fichier, mais seulement quand il correspond à quelque chose. Ce que l’on pourrait faire, cependant, est de vérifier le résultat - si le résultat est vide, il nous manque un fichier.

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

Plus lisible:

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

Et voici comment cela fonctionne comme un petit 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

On pourrait utiliser stat comme alternative, puisqu'il s'agit d'un répertoire plat, mais le code ci-dessous ne fonctionnera pas de manière récursive pour les sous-répertoires si vous décidez jamais d'ajouter ceux-ci:

$ 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 nous prenons l'idée stat et l'exécutons avec, nous pourrions utiliser le code de sortie de stat pour indiquer si un fichier existe ou non. Effectivelly, nous voulons faire ceci:

$ 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

Échantillon échantillon:

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
1
Sergiy Kolodyazhnyy