Bash - Überprüfen Sie das Verzeichnis auf Dateien anhand der Liste der Teildateinamen

8

Ich habe einen Server, der jeden Tag eine Datei pro Client in ein Verzeichnis empfängt. Die Dateinamen sind wie folgt aufgebaut:

uuid_datestring_other-data

Zum Beispiel:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid ist ein Standardformat uuid.
  • datestringist die Ausgabe von date +%Y%m%d.
  • other-data ist in der Länge variabel, enthält jedoch niemals einen Unterstrich.

Ich habe eine Datei im Format:

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

Ich muss mit bash überprüfen, ob jede in der Datei aufgeführte UUID eine entsprechende Datei im Verzeichnis hat.

Ich bin so weit gekommen, aber ich habe das Gefühl, dass ich mit einer if-Anweisung aus der falschen Richtung komme und die Dateien im Quellverzeichnis durchlaufen muss.

Die Variablen source_directory und uuid_list wurden bereits früher im Skript zugewiesen:

# 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}"

Wie soll ich überprüfen, ob die Dateien in meiner Liste im Verzeichnis vorhanden sind? Ich möchte die Bash-Funktionalität so weit wie möglich nutzen, bin aber nicht gegen die Verwendung von Befehlen, wenn dies erforderlich ist.

Arronisch
quelle
Python? Und ist das Serververzeichnis "flach"?
Jacob Vlijm
Ja, es ist flach, keine Unterverzeichnisse. Wenn möglich, bleibe ich lieber bei Bash.
Arronical
1
Ok, ich werde nicht posten.
Jacob Vlijm
Ich sehe nicht wirklich, was mit dem, was du hast, falsch ist. Sie müssen entweder die UUIDs oder die Dateien durchlaufen. Warum ist eine Schleife besser als die andere?
Terdon

Antworten:

5

Gehen Sie über die Dateien und erstellen Sie ein assoziatives Array über die in ihren Namen enthaltenen UUIDs (ich habe die Parametererweiterung verwendet, um die UUID zu extrahieren). Lesen Sie die Liste, überprüfen Sie das assoziative Array für jede UUID und geben Sie an, ob die Datei aufgezeichnet wurde oder nicht.

#!/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
quelle
1
Schön (+1), aber warum ist das besser als das, was das OP getan hat? Sie scheinen das Gleiche zu tun, aber in zwei Schritten statt in einem.
Terdon
1
@terdon: Der Hauptunterschied ist, dass dies funktioniert :-) Die Wildcard-Erweiterung wird nur einmal durchgeführt, nicht jedes Mal, wenn Sie eine Zeile aus der Liste lesen, was möglicherweise auch schneller ist.
Choroba
Ja, das ist ein wichtiger Unterschied. Fair genug :)
Terdon
Das ist wunderbar, danke, habe meine +1. Gibt es eine Möglichkeit, den Pfad zu dem Verzeichnis aufzunehmen, in dem sich die Dateien befinden? Ich weiß, dass ich cdin das Verzeichnis innerhalb des Skripts gelangen kann, habe mich aber nur gefragt, um Wissen zu erlangen.
Arronical
@Arronical: Es ist möglich, aber Sie müssen den Pfad aus der Zeichenfolge entfernen, möglich mit file=${file##*/}.
Choroba
5

Hier ist ein "bashy" und prägnanter Ansatz:

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

Beachten Sie, dass das oben Genannte zwar hübsch ist und für einige Dateien gut funktioniert, seine Geschwindigkeit jedoch von der Anzahl der UUIDs abhängt und sehr langsam ist, wenn Sie viele verarbeiten müssen. Wenn dies der Fall ist, verwenden Sie entweder die Lösung von @ choroba oder vermeiden Sie für etwas wirklich Schnelles die Shell und rufen Sie auf 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

Um die Zeitunterschiede zu veranschaulichen, habe ich meinen Bash-Ansatz, Chorobas und meinen Perl an einer Datei mit 20000 UUIDs getestet, von denen 18001 einen entsprechenden Dateinamen hatte. Beachten Sie, dass jeder Test ausgeführt wurde, indem die Ausgabe des Skripts auf umgeleitet wurde /dev/null.

  1. Meine Bash (~ 3,5 min)

    real   3m39.775s
    user   1m26.083s
    sys    2m13.400s
    
  2. Chorobas (Bash, ~ 0,7 Sek.)

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
    
  3. Mein Perl (~ 0,1 Sek.):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
    
Terdon
quelle
+1 für eine fantastisch präzise Methode müsste dies aus dem Verzeichnis heraus ausgeführt werden, das die Dateien enthält. Ich weiß, dass ich cdin das Verzeichnis im Skript gelangen kann, aber gibt es eine Methode, mit der der Dateipfad in die Suche einbezogen werden kann?
Arronical
@Arronical sicher, siehe aktualisierte Antwort. Sie können ${source_directory}genau wie in Ihrem Skript verwenden.
Terdon
Oder verwenden Sie es "$2"und übergeben Sie es als zweites Argument an das Skript.
Alexis
Stellen Sie sicher, dass dies für Ihre Zwecke schnell genug ausgeführt wird. Es wäre schneller, dies mit einem einzelnen Verzeichnisscan durchzuführen, anstatt mit vielen solchen Dateisuchen.
Alexis
1
@alexis ja, du hast ganz recht. Ich habe einige Tests durchgeführt und dies wird sehr langsam, wenn die Anzahl der UUIDs / Dateien zunimmt. Ich habe einen Perl-Ansatz hinzugefügt (der als Einzeiler innerhalb des Bash-Skripts ausgeführt werden kann, also technisch immer noch Bash, wenn Sie offen für kreative Namen sind), der weitaus schneller ist.
Terdon
3

Dies ist reiner Bash (dh keine externen Befehle), und es ist der zufälligste Ansatz, den ich mir vorstellen kann.

Aber in Bezug auf die Leistung ist es wirklich nicht viel besser als das, was Sie derzeit haben.

Es wird jede Zeile von gelesen path/to/file; für jede Zeile, wird es das erste Feld speichert in $uuidund gibt eine Meldung aus, wenn eine Datei , um die Pattern - Matching path/to/directory/$uuid*wird nicht gefunden:

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

Nennen Sie es mit path/to/script path/to/file path/to/directory.

Beispielausgabe unter Verwendung der Beispieleingabedatei in der Frage in einer Testverzeichnishierarchie, die die Beispieldatei in der Frage enthält:

% 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
quelle
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

Hier geht es nicht darum, Fehler zu melden, die die Shell für Sie meldet. Wenn Sie versuchen, <eine nicht vorhandene Datei zu öffnen, beschwert sich Ihre Shell. Tatsächlich wird der Fehlerausgabe das Skript $0und die Zeilennummer, in der der Fehler aufgetreten ist, vorangestellt. Dies sind gute Informationen, die standardmäßig bereits bereitgestellt werden. Machen Sie sich also keine Sorgen.

Sie müssen die Datei auch nicht so zeilenweise aufnehmen - sie kann sehr langsam sein. Dies erweitert das Ganze in einem einzigen Schuss auf ein durch Leerzeichen getrenntes Array von Argumenten und behandelt jeweils zwei Argumente. Wenn Ihre Daten mit Ihrem Beispiel übereinstimmen, $1sind sie immer Ihre UUID und $2Ihre $name. Wenn Sie basheine Übereinstimmung mit Ihrer UUID eröffnen können - und nur eine solche Übereinstimmung existiert -, printfgeschieht dies. Andernfalls ist dies nicht der Fall und die Shell schreibt Diagnosen an stderr, warum.

mikeserv
quelle
1
@kos - existiert die Datei? Wenn nicht, verhält es sich wie beabsichtigt. unset IFSstellt sicher, dass $(cat <uuid_file)auf Leerraum aufgeteilt wird. Muscheln teilen sich $IFSunterschiedlich auf, wenn sie nur aus Leerzeichen bestehen oder nicht gesetzt sind. Solche geteilten Erweiterungen haben niemals Nullfelder, da alle Leerraumsequenzen nur als ein einziges Feldtrennzeichen stehen. Solange es in jeder Zeile nur zwei nicht durch Leerzeichen getrennte Felder gibt, sollte es funktionieren, denke ich. in bash, sowieso. set -fstellt sicher, dass die nicht zitierte Erweiterung nicht für Globs interpretiert wird, und set + f stellt sicher, dass die späteren Globs vorhanden sind.
Mikesserv
@kos - ich habe es gerade behoben. Ich hätte es nicht verwenden sollen, <>da dadurch eine nicht vorhandene Datei erstellt wird. <werde berichten, wie ich es gemeint habe. Das mögliche Problem dabei - und der Grund, warum ich es überhaupt falsch verwendet habe <>- ist, dass es hängen bleibt, wenn es sich um eine Pipe-Datei ohne Lesegerät oder wie ein zeilengepufferter char dev handelt. Dies könnte vermieden werden, indem die Fehlerausgabe expliziter behandelt und ausgeführt wird [ -f "$dir/$1"* ]. Wir sprechen hier von UUIDs, daher sollte es niemals auf mehr als eine einzelne Datei erweitert werden. es ist aber ein bisschen nett, wie es die fehlgeschlagenen Dateinamen so an stderr meldet.
Mikesserv
@kos - eigentlich könnte ich ulimit verwenden, um zu verhindern, dass überhaupt Dateien erstellt werden, und <>wäre daher immer noch auf diese Weise verwendbar ... <>ist besser, wenn der Glob in ein Verzeichnis erweitert wird, da unter Linux das Lesen / Schreiben erfolgt scheitern und sagen - das ist ein Verzeichnis.
Mikesserv
@kos - oh! Es tut mir leid - ich bin nur dumm - du hast zwei Streichhölzer und es ist das Richtige. Ich meine, damit es auf diese Weise fehlerhaft wird, wenn zwei Übereinstimmungen möglich sind, sollten dies UUIDs sein - es sollte niemals die Möglichkeit geben, dass zwei ähnliche Namen mit demselben Glob übereinstimmen. das ist völlig beabsichtigt - und es ist in einer Weise mehrdeutig, wie es nicht sein sollte. Du siehst was ich meine? Das Benennen der Datei für einen Glob ist nicht das Problem. Sonderzeichen sind hier nicht relevant. Das Problem besteht darin, dass bashein Umleitungsglob nur akzeptiert wird, wenn er nur mit einer Datei übereinstimmt. siehe man bashunter Umleitung.
Mikesserv
1

Die Art und Weise, wie ich es angehen würde, besteht darin, zuerst UUIDs aus der Datei zu holen und dann zu verwenden find

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

Zur besseren Lesbarkeit

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

Beispiel mit einer Liste von Dateien in /etc/, die nach den Dateinamen passwd, group, fstab und THISDOESNTEXIST suchen.

$ 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

Da Sie das Verzeichnis ist flach erwähnt haben, könnten Sie die verwenden -printf "%f\n"Option , um Dateinamen selbst nur drucken

Dies führt nicht dazu, dass fehlende Dateien aufgelistet werden. findDer kleine Nachteil ist, dass es Ihnen nicht sagt, ob es keine Datei findet, sondern nur, wenn es mit etwas übereinstimmt. Was man jedoch tun könnte, ist die Ausgabe zu überprüfen - wenn die Ausgabe leer ist, fehlt eine Datei

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

Besser lesbar:

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

Und so funktioniert es als kleines Skript:

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

Man könnte es statals Alternative verwenden, da es sich um ein flaches Verzeichnis handelt, aber der folgende Code funktioniert nicht rekursiv für Unterverzeichnisse, wenn Sie sich jemals dazu entschließen, diese hinzuzufügen:

$ 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

Wenn wir die statIdee aufgreifen und damit arbeiten, können wir den Exit-Code von stat als Hinweis darauf verwenden, ob eine Datei vorhanden ist oder nicht. Tatsächlich wollen wir dies tun:

$ 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

Probelauf:

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
quelle