Bash - Sprawdź katalog w poszukiwaniu plików z listą częściowych nazw plików

8

Mam serwer, który każdego dnia otrzymuje plik do klienta do katalogu. Nazwy plików są zbudowane w następujący sposób:

uuid_datestring_other-data

Na przykład:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid jest uuid w standardowym formacie.
  • datestringjest wyjściem z date +%Y%m%d.
  • other-data ma zmienną długość, ale nigdy nie będzie zawierać podkreślenia.

Mam plik w formacie:

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

Muszę sprawdzić, czy każdy identyfikator użytkownika wymieniony w pliku ma odpowiedni plik w katalogu, używając bash.

Zaszedłem tak daleko, ale czuję, że idę z niewłaściwego kierunku, używając instrukcji if, i że muszę przeglądać pliki w katalogu źródłowym.

Zmienne source_directory i uuid_list zostały wcześniej przypisane w skrypcie:

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

Jak powinienem sprawdzić, czy pliki na mojej liście istnieją w katalogu? Chciałbym w miarę możliwości korzystać z funkcji bash, ale nie jestem przeciwny używaniu poleceń, jeśli to konieczne.

Arroniczny
źródło
Pyton? A czy katalog serwera jest „płaski”?
Jacob Vlijm
Tak, jest płaski, brak podkatalogów. Wolę trzymać się tylko bashu, jeśli to możliwe.
Arroniczny
1
Ok, nie opublikuję.
Jacob Vlijm
Naprawdę nie rozumiem, co jest nie tak z tym, co masz. Będziesz musiał zapętlić UUID lub pliki, dlaczego jedna pętla byłaby lepsza od drugiej?
terdon

Odpowiedzi:

5

Przejdź po plikach, utwórz tablicę asocjacyjną nad uuidami zawartymi w ich nazwach (użyłem rozszerzenia parametrów, aby wyodrębnić uuid). Przeczytaj listę, sprawdź tablicę asocjacyjną dla każdego identyfikatora użytkownika i zgłoś, czy plik został nagrany, czy nie.

#!/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
źródło
1
Fajnie (+1), ale dlaczego to jest lepsze niż to, co robił PO? Wygląda na to, że robisz to samo, ale w dwóch krokach zamiast jednego.
terdon
1
@terdon: Główna różnica polega na tym, że to działa :-) Rozwinięcie symboli wieloznacznych odbywa się tylko raz, nie za każdym razem, gdy czytasz wiersz z listy, co również może być szybsze.
choroba
Tak, to ważna różnica. Wystarczająco :)
terdon
To cudowne dziękuję, mam +1. Czy jest jakiś sposób na włączenie ścieżki do katalogu, w którym znajdują się pliki? Wiem, że mogę cdwejść do katalogu w skrypcie, ale po prostu zastanawiałem się nad tym, aby zdobyć wiedzę.
Arroniczny
@Arronical: Jest to możliwe, ale musisz usunąć ścieżkę z ciągu, możliwe za pomocą file=${file##*/}.
choroba
5

Oto bardziej „nieśmiałe” i zwięzłe podejście:

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

Zauważ, że chociaż powyższe jest ładne i będzie działać dobrze dla kilku plików, jego szybkość zależy od liczby UUID i będzie bardzo wolna, jeśli będziesz musiał przetworzyć wiele. W takim przypadku skorzystaj z rozwiązania @ choroba lub, dla czegoś naprawdę szybkiego, uniknij powłoki i wywołaj 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

Aby zilustrować różnice czasowe, przetestowałem moje podejście do bash, chorobę i mojego perla na pliku z 20000 UUID, z których 18001 miało odpowiednią nazwę pliku. Zauważ, że każdy test był uruchamiany przez przekierowanie wyjścia skryptu do /dev/null.

  1. Moje uderzenie (~ 3,5 min)

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

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
  3. Mój perl (~ 0,1 s):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
terdon
źródło
+1 za fantastycznie zwięzłą metodę, musiałoby to zostać wykonane z katalogu zawierającego pliki. Wiem, że mogę cdprzejść do katalogu w skrypcie, ale czy istnieje metoda, dzięki której ścieżka wyszukiwania może zostać uwzględniona podczas wyszukiwania?
Arroniczny
@Arronicznie pewny, zobacz zaktualizowaną odpowiedź. Możesz używać tego, ${source_directory}co robiłeś w skrypcie.
terdon
Lub użyj "$2"i przekaż go do skryptu jako drugi argument.
Alexis
Sprawdź, czy działa to wystarczająco szybko dla twoich celów - szybciej byłoby to zrobić za pomocą skanowania pojedynczego katalogu, zamiast wielu takich wyszukiwań plików.
Alexis
1
@alexis tak, masz całkowitą rację. Zrobiłem kilka testów, a to staje się bardzo wolne, jeśli liczba UUID / plików wzrośnie. Dodałem podejście perla (które może być uruchamiane jako jeden linijka z poziomu skryptu bash, więc technicznie nadal bash, jeśli jesteś otwarty na twórcze nazewnictwo), co jest znacznie szybsze.
terdon
3

To jest czysta Bash (tj. Bez zewnętrznych poleceń) i jest to najbardziej spójne podejście, jakie mogę wymyślić.

Ale pod względem wydajności nie jest tak naprawdę dużo lepszy niż obecnie.

Odczyta każdą linię z path/to/file; dla każdej linii, będzie przechowywać w pierwsze pole $uuidi wypisuje komunikat, jeśli plik pasujący do wzorca path/to/directory/$uuid*jest nie znaleziono:

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

Zadzwoń za pomocą path/to/script path/to/file path/to/directory.

Przykładowe dane wyjściowe przy użyciu przykładowego pliku wejściowego w pytaniu w hierarchii katalogu testowego zawierającego przykładowy plik w pytaniu:

% 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
źródło
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

Chodzi o to, aby nie martwić się o zgłaszanie błędów, które powłoka zgłosi za Ciebie. Jeśli spróbujesz <otworzyć plik, który nie istnieje, twoja powłoka narzeka. W rzeczywistości wstawi on skrypt $0i numer wiersza, w którym wystąpił błąd, do wyjścia błędu, kiedy to robi ... To dobra informacja, która jest już domyślnie podawana - więc nie przejmuj się.

Nie musisz także umieszczać pliku tak po linii - może być strasznie powolny. To rozszerza całość w jednym ujęciu do tablicy argumentów rozdzielonej spacjami i obsługuje dwa jednocześnie. Jeśli twoje dane są zgodne z twoim przykładem, to $1zawsze będzie twój identyfikator użytkownika i $2będzie twój $name. Jeśli bashmożna otworzyć dopasowanie do Twojego UUID - i istnieje tylko jedno takie dopasowanie - to się printfdzieje. W przeciwnym razie tak się nie stanie, a powłoka wypisze diagnostykę do stderr o tym, dlaczego.

mikeserv
źródło
1
@kos - czy plik istnieje? jeśli nie, to zachowuje się zgodnie z przeznaczeniem. unset IFSzapewnia $(cat <uuid_file)podział na białe znaki. Pociski dzielą się $IFSinaczej, gdy składają się tylko z białej spacji lub są rozbrojone. Takie dzielone rozwinięcia nigdy nie mają żadnych pól zerowych, ponieważ wszystkie sekwencje białych znaków są tylko pojedynczymi ogranicznikami pól. Tak długo, jak w każdym wierszu znajdują się tylko dwa pola oddzielone spacjami, to powinno działać. w bashkażdym razie. set -fzapewnia, że ​​niecytowane rozwinięcie nie jest interpretowane dla globów, a set + f gwarantuje, że późniejsze globusy są.
mikeserv
@kos - właśnie to naprawiłem. Nie powinienem był używać, <>ponieważ tworzy to nieistniejący plik. <zgłoś się tak, jak chciałem. możliwym problemem z tym - i powodem, dla którego niewłaściwie użyłem go <>w pierwszej kolejności - jest to, że jeśli jest to plik potokowy bez czytnika lub podobny do char dev-line buforowanego, zawiesi się. można tego uniknąć, posługując się wyjściem błędu w sposób bardziej wyraźny i działając [ -f "$dir/$1"* ]. mówimy tutaj o UUID-ach, więc nigdy nie powinno się rozszerzać do więcej niż jednego pliku. jest to całkiem miłe, ale to, jak zgłasza stderr nazwy nieudanych plików.
mikeserv
@kos - właściwie, przypuszczam, że mógłbym użyć ulimit, aby w ogóle nie tworzyć żadnych plików, a więc <>nadal byłby użyteczny w ten sposób ... <>lepiej, jeśli glob może rozwinąć się do katalogu, ponieważ w systemie Linux odczyt / zapis będzie nie powiedz i powiedz - to katalog.
mikeserv
@kos - oh! Przepraszam - po prostu jestem głupi - masz dwa mecze, więc robi to dobrze. mam na myśli, że w ten sposób popełniłby błąd, gdyby mogły mieć dwa dopasowania, to powinny być uuids - nigdy nie powinno być możliwości dwóch podobnych nazw pasujących do tego samego globu. jest to w pełni celowe - i jest dwuznaczne w sposób, w jaki nie powinno być. widzisz co mam na myśli? nazywanie pliku globem nie jest problemem, - specjalne znaki nie są tu istotne - problemem jest to, że bashakceptuje glob przekierowania tylko, jeśli pasuje tylko do jednego pliku. patrz man bashREDIRECTION.
mikeserv
1

Podejdę do tego, aby najpierw pobrać UUID z pliku, a następnie użyć find

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

Dla gotowości

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

Przykład z listą plików w /etc/poszukiwaniu nazw plików passwd, group, fstab i 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

Ponieważ wspomniałeś, że katalog jest płaski, możesz użyć -printf "%f\n"opcji, aby po prostu wydrukować samą nazwę pliku

Nie robi to, aby wyświetlić listę brakujących plików. findniewielką wadą jest to, że nie mówi ci, jeśli nie znajdzie pliku, tylko gdy coś pasuje. Można jednak sprawdzić dane wyjściowe - jeśli dane wyjściowe są puste, to brakuje nam pliku

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

Bardziej czytelny:

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

A oto jak działa jako mały skrypt:

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

Można użyć statjako alternatywy, ponieważ jest to płaski katalog, ale poniższy kod nie będzie działał rekurencyjnie dla podkatalogów, jeśli kiedykolwiek zdecydujesz się je dodać:

$ 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

Jeśli weźmiemy ten statpomysł i uruchomimy go, możemy użyć kodu wyjścia stat jako wskazania, czy plik istnieje, czy nie. Skutecznie chcemy to zrobić:

$ 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

Przykładowy przebieg:

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
źródło