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

8

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 uuid au format standard.
  • datestringest la sortie de date +%Y%m%d.
  • other-data est de longueur variable mais ne contiendra jamais de soulignement.

J'ai un fichier au 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'en suis là, mais j'ai l'impression de venir dans la mauvaise direction en utilisant une instruction if, et que je dois parcourir les fichiers dans le répertoire source.

Les variables source_directory et uuid_list ont été assignées plus tôt 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 vérifier que les fichiers de ma liste existent dans le répertoire? J'aimerais utiliser la fonctionnalité bash autant que possible, mais je ne suis pas contre l'utilisation de commandes si besoin est.

Arronical
la source
Python? Et le répertoire du serveur est-il "plat"?
Jacob Vlijm
Oui, c'est plat, pas de sous-répertoires. Je préfère m'en tenir à bash si possible.
Arronical
1
Ok, je ne posterai pas.
Jacob Vlijm
Je ne vois pas vraiment ce qui ne va pas avec ce que vous avez. Vous devrez parcourir les UUID ou les fichiers, pourquoi une boucle serait-elle meilleure que l'autre?
terdon

Réponses:

5

Parcourez les fichiers, créez un tableau associatif sur les uuids contenus dans leurs noms (j'ai utilisé l'expansion des paramètres pour extraire l'uuid). 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"
choroba
la source
1
Bien (+1), mais pourquoi est-ce mieux que ce que faisait l'OP? Vous semblez faire la même chose de base mais en deux étapes au lieu d'une.
terdon
1
@terdon: La principale différence est que cela fonctionne :-) L'expansion des caractères génériques est effectuée une seule fois, pas à chaque fois que vous lisez une ligne de la liste, ce qui pourrait être plus rapide aussi.
choroba
Oui, c'est une différence importante. Assez bien :)
terdon
C'est un merveilleux merci, j'ai obtenu mon +1. Existe-t-il un moyen d'inclure le chemin d'accès au répertoire contenant les fichiers? Je sais que je peux cddans le répertoire du script, mais je me demandais juste pour gagner en connaissances.
Arronical
@Arronical: C'est possible, mais vous devrez supprimer le chemin de la chaîne, possible avec file=${file##*/}.
choroba
5

Voici une approche plus "timide" 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 fonctionnera bien pour quelques fichiers, sa vitesse dépend du nombre d'UUID et sera très lente si vous devez en traiter plusieurs. Si tel est le cas, utilisez la solution de @ choroba ou, pour quelque chose de vraiment rapide, évitez le 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, choroba et mon perl sur un fichier avec 20000 UUID dont 18001 avaient un nom de fichier correspondant. Notez que chaque test a été exécuté en redirigeant la sortie du script vers /dev/null.

  1. Mon coup (~ 3,5 min)

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

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
  3. Mon perl (~ 0,1 sec):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
terdon
la source
+1 pour une méthode incroyablement concise, cela devrait être exécuté à partir du répertoire contenant les fichiers. Je sais que je peux cddans le répertoire du script, mais y a-t-il une méthode par laquelle le chemin du fichier pourrait être inclus dans la recherche?
Arronical
@Arronical bien sûr, voir la réponse mise à jour. Vous pouvez utiliser ${source_directory}comme vous le faisiez dans votre script.
terdon
Ou utilisez-le "$2"et passez-le au script comme deuxième argument.
alexis
Vérifiez que cela fonctionne assez rapidement pour vos besoins - il serait plus rapide de le faire avec une analyse de répertoire unique, au lieu de beaucoup de recherches de fichiers comme celle-ci.
alexis
1
@alexis oui, vous avez tout à fait raison. J'ai fait quelques tests et cela devient très lent si le nombre d'UUID / fichiers augmente. J'ai ajouté une approche perl (qui peut être exécutée comme une ligne à partir du script bash, donc techniquement, toujours bash si vous êtes ouvert à une dénomination créative) qui est beaucoup plus rapide.
terdon
3

C'est du pur Bash (c'est-à-dire pas de commandes externes), et c'est l'approche la plus coïncidente à laquelle je puisse penser.

Mais en termes de performances, ce n'est vraiment pas beaucoup mieux que ce que vous avez actuellement.

Il lira chaque ligne de path/to/file; pour chaque ligne, il stockera le premier champ $uuidet affichera un message si aucun fichier correspondant au modèle path/to/directory/$uuid*n'est 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 à l'aide de l'exemple de fichier d'entrée dans la question sur une hiérarchie de répertoires de test contenant l'exemple de fichier dans 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
kos
la source
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

L'idée ici n'est pas de s'inquiéter des erreurs de rapport que le shell rapportera pour vous. Si vous essayez d' <ouvrir un fichier qui n'existe pas, votre shell se plaindra. En fait, il ajoutera votre script $0et le numéro de ligne sur lequel l'erreur s'est produite à la sortie d'erreur quand c'est le cas ... Ce sont de bonnes informations qui sont déjà fournies par défaut - alors ne vous embêtez pas.

Vous n'avez pas non plus besoin de prendre le fichier ligne par ligne comme ça - cela peut être terriblement lent. Cela étend le tout en une seule prise de vue à un tableau d'arguments délimité par des espaces blancs et en gère deux à la fois. Si vos données sont conformes à votre exemple, elles $1seront toujours votre uuid et $2seront les vôtres $name. Si vous bashpouvez ouvrir une correspondance avec votre uuid - et qu'une seule de ces correspondances existe - alors printfcela se produit. Sinon, ce n'est pas le cas et le shell écrit des diagnostics à stderr pour savoir pourquoi.

mikeserv
la source
1
@kos - le fichier existe-t-il? sinon, il se comporte comme prévu. unset IFSassure que $(cat <uuid_file)est divisé sur un espace blanc. Les coquilles se divisent $IFSdifféremment lorsqu'elles ne sont constituées que d'espaces blancs ou ne sont pas définies. De telles extensions fractionnées n'ont jamais de champs nuls car toutes les séquences d'espaces blancs ne se présentent que comme un seul délimiteur de champ. Tant qu'il n'y a que deux champs non séparés par des espaces blancs sur chaque ligne, cela devrait fonctionner, je pense. en bashtout cas. set -fgarantit que l'expansion non cotée n'est pas interprétée pour les globes, et set + f garantit que les globs ultérieurs le sont.
mikeserv
@kos - je viens de le corriger. Je n'aurais pas dû utiliser <>car cela crée un fichier inexistant. <rendra compte comme je le voulais. le problème possible avec cela cependant - et la raison pour laquelle j'ai utilisé incorrectement <>en premier lieu - est que s'il s'agit d'un fichier pipe sans lecteur ou comme un dev de caractère en ligne, il se bloquera. cela pourrait être évité en traitant la sortie d'erreur plus explicitement et en le faisant [ -f "$dir/$1"* ]. nous parlons ici des uuids, et donc il ne devrait jamais s'étendre à plus d'un seul fichier. c'est plutôt sympa de voir comment il signale les noms des fichiers en échec à stderr comme ça.
mikeserv
@kos - en fait, je suppose que je pourrais utiliser ulimit pour l'empêcher de créer des fichiers et <>serait donc toujours utilisable de cette façon ... <>est mieux si le glob peut se développer dans un répertoire parce que sur un linux, la lecture / écriture sera échouer et dire - c'est un répertoire.
mikeserv
@kos - oh! Je suis désolé - je suis juste stupide - vous avez deux matchs, et donc ça fait la bonne chose. je veux dire pour qu'il commette une erreur de cette manière si deux correspondances peuvent avoir eu, celles-ci sont supposées être des uuides - il ne devrait jamais y avoir une possibilité de 2 noms similaires qui correspondent au même glob. des thats totalement intentionnel - et il est ambigu d'une manière qu'il ne devrait pas être. Tu vois ce que je veux dire? nommer le fichier pour un glob n'est pas le problème, - les caractères spéciaux ne sont pas pertinents ici - le problème est que bashn'acceptera un glob de redirection que s'il ne correspond qu'à un seul fichier. voir man bashsous REDIRECTION.
mikeserv
1

La façon dont je l'aborderais est d'obtenir d'abord les uuids du fichier, 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/, recherchant les 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' -printf "%f\n"option pour simplement imprimer le nom de fichier lui-même

Ce que cela ne fait pas, c'est de répertorier les fichiers manquants. findLe petit inconvénient est qu'il ne vous dit pas s'il ne trouve pas de fichier, seulement lorsqu'il correspond à quelque chose. Ce que l'on pourrait faire, cependant, est de vérifier la sortie - si la sortie est vide, alors nous avons un fichier manquant

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 il 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 statcomme alternative, car c'est un répertoire plat, mais le code ci-dessous ne fonctionnera pas récursivement pour les sous-répertoires si vous décidez de les ajouter:

$ 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' statidée et l'exécutons, nous pourrions utiliser le code de sortie de stat pour indiquer si un fichier existe ou non. Effectivement, 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

Exemple d'exécution:

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
la source