Bash: controlla la directory per i file rispetto all'elenco di nomi file parziali


8

Ho un server che riceve un file per client ogni giorno in una directory. I nomi dei file sono costruiti come segue:

uuid_datestring_other-data

Per esempio:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid è un formato standard uuid.
  • datestringè l'output di date +%Y%m%d.
  • other-data è di lunghezza variabile ma non conterrà mai un carattere di sottolineatura.

Ho un file nel formato:

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

Devo verificare che ogni uuid elencato nel file abbia un file corrispondente nella directory, usando bash.

Sono arrivato così lontano, ma mi sento come se venissi dalla direzione sbagliata usando un'istruzione if, e che ho bisogno di scorrere i file nella directory di origine.

Le variabili source_directory e uuid_list sono state assegnate in precedenza nello 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}"

Come devo verificare che i file nel mio elenco esistano nella directory? Vorrei usare la funzionalità bash per quanto possibile, ma non sono contrario all'utilizzo dei comandi se necessario.


Pitone? E la directory del server è "piatta"?
Jacob Vlijm,

Sì, è piatto, nessuna sottodirectory. Preferirei restare con bash se possibile.
Arronical

1
Ok, non posterò.
Jacob Vlijm,


Non vedo davvero cosa c'è che non va in quello che hai. Dovrai eseguire il ciclo attraverso l'UUID o i file, perché un ciclo dovrebbe essere migliore dell'altro?
terdon,

Risposte:


5

Cammina sui file, crea un array associativo sugli uuidi contenuti nei loro nomi (ho usato l'espansione dei parametri per estrarre l'UUID). Il, leggi l'elenco, controlla l'array associativo per ciascun uuid e segnala se il file è stato registrato o meno.

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

1
Bello (+1), ma perché è meglio di quello che stava facendo l'OP? Sembra che tu stia facendo la stessa cosa di base ma in due passaggi anziché in uno.
terdon,

1
@terdon: la differenza principale è che funziona :-) L'espansione dei caratteri jolly viene eseguita una sola volta, non tutte le volte che leggi una riga dall'elenco, che potrebbe essere anche più veloce.
Choroba,

Sì, questa è una differenza importante. Abbastanza giusto :)
terdon

Questo è meraviglioso grazie, ho ottenuto il mio +1. Esiste un modo per includere il percorso della directory che contiene i file? So di poter cdaccedere alla directory all'interno dello script, ma mi sono solo chiesto per acquisire conoscenza.
Arronical

@Arronical: è possibile, ma dovrai rimuovere il percorso dalla stringa, possibile con file=${file##*/}.
Choroba,

5

Ecco un approccio più "schivo" e 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

Nota che sebbene quanto sopra sia carino e funzionerà bene per alcuni file, la sua velocità dipende dal numero di UUID e sarà molto lento se devi elaborarne molti. In tal caso, utilizzare la soluzione di @ choroba o, per qualcosa di veramente veloce, evitare la shell e chiamare 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 per illustrare le differenze temporali, ho testato il mio approccio bash, quello di choroba e il mio perl su un file con 20000 UUID di cui 18001 aveva un nome file corrispondente. Si noti che ogni test è stato eseguito reindirizzando l'output dello script su /dev/null.

  1. Il mio bash (~ 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. Il mio perl (~ 0.1 sec):

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

+1 per un metodo straordinariamente conciso, questo dovrebbe essere eseguito all'interno della directory contenente i file. So di cdpoter accedere alla directory nello script, ma esiste un metodo per includere il percorso del file nella ricerca?
Arronical

@Arronical sicuro, vedi risposta aggiornata. Puoi usarlo ${source_directory}proprio come facevi nella tua sceneggiatura.
terdon,

Oppure usa "$2"e passalo allo script come secondo argomento.
alexis,

Verifica che questo sia abbastanza veloce per i tuoi scopi: sarebbe più veloce farlo con una singola scansione della directory, invece di molte ricerche di file come questa.
alexis,

1
@alexis si, hai perfettamente ragione. Ho fatto alcuni test e questo diventa molto lento se aumenta il numero di UUID / file. Ho aggiunto un approccio perl (che può essere eseguito come un solo liner all'interno dello script bash, quindi tecnicamente, ancora bash se sei aperto a qualche nome creativo) che è molto più veloce.
terdon,

3

Questo è puro Bash (cioè nessun comando esterno), ed è l'approccio più coincidente a cui riesco a pensare.

Ma per quanto riguarda le prestazioni non è molto meglio di quello che hai attualmente.

Leggerà ogni riga da path/to/file; per ogni riga, memorizzerà il primo campo $uuide stampa un messaggio se nonpath/to/directory/$uuid* viene trovato un file corrispondente al modello :

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

Chiamalo con path/to/script path/to/file path/to/directory.

Output di esempio utilizzando il file di input di esempio nella domanda in una gerarchia di directory di test contenente il file di esempio nella domanda:

% 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
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'idea qui non è quella di preoccuparsi di segnalare errori che la shell segnalerà per te. Se provi ad <aprire un file che non esiste, la tua shell si lamenterà. In effetti, anteporrà il tuo script $0e il numero di riga su cui si è verificato l'errore all'output dell'errore quando lo fa ... Queste sono buone informazioni che sono già fornite di default, quindi non preoccuparti.

Inoltre non è necessario prendere il file riga per riga in questo modo - può essere terribilmente lento. Questo espande il tutto in un singolo colpo in una serie di argomenti delimitati da spazi bianchi e ne gestisce due alla volta. Se i tuoi dati sono coerenti con il tuo esempio, $1saranno sempre i tuoi uuidi e $2saranno tuoi $name. Se bashpuoi aprire una partita al tuo uuid - e esiste solo una di queste partite - allora printfsuccede. Altrimenti no e la shell scrive la diagnostica a stderr sul perché.


1
@kos - esiste il file? in caso contrario, si comporta come previsto. unset IFSassicura che $(cat <uuid_file)sia suddiviso su spazi bianchi. Le conchiglie si dividono in modo $IFSdiverso quando sono composte solo da spazi bianchi o non sono impostate. Tali espansioni divise non hanno mai campi nulli perché tutte le sequenze di spazi bianchi si trovano come un unico delimitatore di campo. Finché ci sono solo due campi separati da spazi non bianchi su ogni riga dovrebbe funzionare, penso. dentro bash, comunque. set -fassicura che l'espansione non quotata non sia interpretata per globs e set + f assicura che i globs successivi lo siano.
Mikeserv,

@kos - l'ho appena risolto. Non avrei dovuto usare <>perché crea un file inesistente. <riporterò come intendevo. il possibile problema con questo - e la ragione per cui ho usato erroneamente <>in primo luogo - è che se si tratta di un file pipe senza un lettore o come un oggetto di sviluppo bufferizzato in linea si bloccherà. ciò potrebbe essere evitato gestendo l'output dell'errore in modo più esplicito e facendo [ -f "$dir/$1"* ]. stiamo parlando di uuidi qui, e quindi non dovrebbe mai espandersi in più di un singolo file. è piuttosto carino come riporta i nomi dei file falliti a stderr in quel modo.
Mikeserv,

@kos - in realtà, suppongo che potrei usare ulimit per impedirgli di creare qualsiasi file e quindi <>sarebbe comunque utilizzabile in questo modo ... <>è meglio se il glob potrebbe espandersi in una directory perché su Linux la lettura / scrittura sarà fallire e dire - questa è una directory.
Mikeserv,

@kos - oh! Mi dispiace - sono solo stupido - hai due partite, e quindi sta facendo la cosa giusta. intendo dire che potrebbe sbagliare in quel modo se si potessero avere due partite, queste dovrebbero essere uuidi - non ci dovrebbe mai essere la possibilità di 2 nomi simili che corrispondono allo stesso glob. è del tutto intenzionale - ed è ambiguo in un modo che non dovrebbe essere. capisci cosa intendo? la denominazione del file per un glob non è il problema, - qui non sono rilevanti caratteri speciali - il problema è che bashaccetterà un glob di reindirizzamento solo se corrisponde a un solo file. vedi man bashsotto REDIRECTION.
Mikeserv,

1

Il modo in cui mi avvicinerei è quello di ottenere prima gli uuidi dal file, quindi utilizzare find

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

Per leggibilità,

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

Esempio con un elenco di file in /etc/, cercando i nomi dei file passwd, group, fstab e 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

Dal momento che hai menzionato la directory è piatta, è possibile utilizzare l' -printf "%f\n"opzione per stampare solo il nome file stesso

Ciò che non fa è elencare i file mancanti. findIl piccolo svantaggio è che non ti dice se non trova un file, solo quando corrisponde a qualcosa. Ciò che si potrebbe fare, tuttavia, è controllare l'output: se l'output è vuoto, allora manca un file

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

Più leggibile:

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

Ed ecco come si comporta come un piccolo 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

Si potrebbe usare statcome alternativa, dal momento che è una directory piatta, ma il codice seguente non funzionerà in modo ricorsivo per le sottodirectory se si decide di aggiungere quelle:

$ 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

Se prendiamo l' statidea e la eseguiamo, potremmo usare il codice di uscita di stat come indicazione dell'esistenza o meno di un file. In effetti, vogliamo fare questo:

$ 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

Esecuzione di esempio:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.