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.
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"
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
.
Mon bash (~ 3.5 min)
real 3m39.775s
user 1m26.083s
sys 2m13.400s
Choroba's (bash, ~ 0,7 seconde)
real 0m0.732s
user 0m0.697s
sys 0m0.037s
Mon Perl (~ 0.1 sec):
real 0m0.100s
user 0m0.093s
sys 0m0.013s
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
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.
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