Bash - Перевірте каталог файлів щодо списку часткових імен файлів


8

У мене є сервер, який щодня отримує файл на клієнта в каталог. Назви файлів побудовані так:

uuid_datestring_other-data

Наприклад:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid - це стандартний формат uuid.
  • datestring- вихід від date +%Y%m%d.
  • other-data є змінною по довжині, але ніколи не міститиме підкреслення.

У мене є файл формату:

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

Мені потрібно перевірити, що кожен uuid, зазначений у файлі, має відповідний файл у каталозі, використовуючи bash.

У мене це далеко, але відчуваю, що я йду з неправильного напрямку, використовуючи оператор if, і що мені потрібно переглядати файли у вихідному каталозі.

Змінні source_directory та uuid_list були призначені раніше в сценарії:

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

Як я повинен перевірити наявність файлів у моєму списку в каталозі? Я хотів би максимально використовувати функцію bash, але я не проти використання команд, якщо це потрібно.


Пітон? А каталог серверів "плоский"?
Яків Влійм

Так, це просто, немає підкаталогів. Я б скоріше дотримувався баш, якщо це можливо.
Арронічний

1
Гаразд, я не буду публікувати.
Яків Влійм


Я насправді не бачу, що не так у тому, що ти маєш. Вам потрібно буде провести цикл через UUID або файли, чому один цикл буде кращим за інший?
тердон

Відповіді:


5

Пройдіться по файлах, створіть асоціативний масив над uuids, що міститься в їхніх іменах (я використовував розширення параметрів для вилучення uuid). Прочитайте список, перевірте асоціативний масив для кожного uuid та повідомте, чи файл був записаний чи ні.

#!/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
Приємно (+1), але чому це краще, ніж те, що робила ОП? Ви, здається, робите те саме основне, але в два кроки замість одного.
тердон

1
@terdon: Основна відмінність полягає в цьому :-) Розширення підстановки робиться лише один раз, не кожен раз, коли ви читаєте рядок зі списку, що теж може бути швидшим.
choroba

Так, це важлива різниця. Досить справедливо :)
terdon

Це чудове подяка, отримав мій +1. Чи є спосіб включити шлях до каталогу, який містить файли? Я знаю, що можу cdпотрапити в каталог в рамках сценарію, але просто цікавився заради отримання знань.
Арронічний

@Arronical: Це можливо, але вам доведеться видалити шлях із рядка, можливо за допомогою file=${file##*/}.
choroba

5

Ось більш "кричущий" і стислий підхід:

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

Зауважте, що хоч вищезазначене досить і буде працювати нормально для кількох файлів, його швидкість залежить від кількості UUID і буде дуже повільною, якщо вам потрібно обробити багато. Якщо це так, використовуйте розчин @ choroba або, для чогось справді швидкого, уникайте оболонки та телефонуйте 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

Тільки для того, щоб проілюструвати різницю в часі, я перевірив мій баш-підхід, хоробу і мою перл на файл із 20000 UUID, з яких 18001 мав відповідне ім'я файлу. Зауважте, що кожен тест виконувався шляхом перенаправлення виводу сценарію на /dev/null.

  1. Мій баш (~ 3,5 хв)

    real   3m39.775s
    user   1m26.083s
    sys    2m13.400s
    
  2. Чороба (баш, ~ 0,7 сек)

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
    
  3. Мій perl (~ 0,1 сек):

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

+1 для фантастично стислого методу, це повинно бути виконано з каталогу, що містить файли. Я знаю, що можу cdпотрапити в каталог у скрипті, але чи існує метод, завдяки якому шлях пошуку може бути включений до пошуку?
Арронічний

@Аронічно впевнений, див. Оновлену відповідь. Ви можете використовувати ${source_directory}так, як ви робили у своєму сценарії.
тердон

Або використовуйте "$2"та передайте його до сценарію як другий аргумент.
alexis

Переконайтеся, що це працює досить швидко для ваших цілей - було б швидше зробити це за допомогою одного сканування каталогів, а не безлічі таких пошукових файлів.
alexis

1
@alexis так, ти абсолютно прав. Я зробив кілька тестувань, і це стає дуже повільним, якщо кількість UUID / файлів збільшується. Я додав підхід Perl (який можна запустити як один вкладиш з-поміж скрипту bash, так що технічно все-таки біш, якщо ти відкритий для творчих імен), що набагато швидше.
тердон

3

Це чистий Bash (тобто ніяких зовнішніх команд), і це самий підхід, який я можу придумати.

Але продуктивність дійсно не набагато краща, ніж у вас зараз.

Він буде читати кожен рядок із path/to/file; для кожного рядка воно зберігатиме перше поле у $uuidта друкує повідомлення, якщо файл, що відповідає шаблону path/to/directory/$uuid*, не знайдений:

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

Телефонуйте за допомогою path/to/script path/to/file path/to/directory.

Вибір вибірки з використанням вхідного файлу зразка у запитанні в ієрархії тестового каталогу, що містить файл зразка у запитанні

% 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

Ідея тут - не турбуватися про повідомлення про помилки, про які повідомить оболонка. Якщо ви спробуєте <відкрити файл, який не існує, ваша оболонка поскаржиться. Фактично, він додасть номер вашого сценарію $0та номер рядка, на якому сталася помилка, до виводу помилки, коли він є… Це хороша інформація, яка вже надана за замовчуванням - тому не турбуйтеся.

Вам також не потрібно приймати файл так, як це відбувається - це може бути дуже повільно. Це розширює все за один кадр до масиву аргументів, обмежених пробілом, і обробляє два за один раз. Якщо ваші дані узгоджуються з вашим прикладом, то $1завжди будете вашим uuid і $2будете вашим $name. Якщо ви bashможете відкрити відповідність вашому uuid - і існує лише одна така відповідність - тоді printfце станеться. В іншому випадку це не так, і оболонка записує діагностику, щоб визначити, чому саме.


1
@kos - чи існує файл? якщо ні, то він поводиться так, як задумано. unset IFSзабезпечує $(cat <uuid_file)розбиття на пробіл. Оболонки розбиваються по- $IFSрізному, якщо він складається лише з простору або не встановлений. Такі розбиті розширення ніколи не мають нульових полів, оскільки всі послідовності пробілів стоять лише як один роздільник поля. Поки на кожному рядку є лише два розділених непробілом поля, я думаю, що це повинно працювати. в bash, все одно. set -fгарантує, що розширення без котирування не інтерпретується для глобусів, а set + f гарантує, що пізніші глобуси є.
mikeserv

@kos - я просто виправив це. Я не повинен був використовувати, <>оскільки це створює неіснуючий файл. <звітуватиму так, як я це мав на увазі. Можлива проблема з цим, хоча - і причина, яку я неправильно використовується <>в першу чергу - полягає в тому, що якщо це файл труби без зчитувача або подібний до рядкового буфера char dev, він зависне. цього можна уникнути, якщо більш чітко обробляти вихідні помилки та робити це [ -f "$dir/$1"* ]. ми говоримо тут про uuids, і тому вона ніколи не повинна розширюватися більше ніж на один файл. Це якось приємно, хоча як він повідомляє про невдалі імена файлів у stderr так.
mikeserv

@kos - власне, я гадаю, що я міг би використовувати ulimit, щоб не створювати файли взагалі, і тому <>все-таки можна було б користуватися цим способом ... <>краще, якщо глобус може розширитися до каталогу, тому що в Linux буде читання / запис провал і скажіть - ось каталог.
mikeserv

@kos - о! Вибачте - я просто тупий - у вас два матчі, і тому це робиться правильно. я маю на увазі для цього помилку таким чином, якщо два матчі можуть бути, це, мабуть, будуть уїди - ніколи не повинно бути можливості 2 подібних імен, що відповідають одному і тому ж глобусу. ось повністю навмисним - і це є неоднозначним таким чином , що воно НЕ має бути. Ви бачите, що я маю на увазі? іменування файлу для глобальної проблеми не є проблемою, - особливі символи тут не актуальні - проблема полягає в тому, що bashвін приймає глобус перенаправлення лише у тому випадку, якщо він відповідає лише одному файлу. дивіться в man bashрозділі ПОПЕРЕДЖЕННЯ.
mikeserv

1

Те, як я підійшов до цього, - це спочатку отримати uuids з файлу, а потім використовувати find

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

Щодо читабельності,

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

Приклад зі списком файлів у /etc/, шукаючи імена файлів passwd, group, fstab та 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

Оскільки ви згадали, що каталог є рівним, ви можете скористатися -printf "%f\n"опцією для простого друку імені файлу

Це не робить - це перелік відсутніх файлів. findНевеликим недоліком є ​​те, що він не повідомляє вам, якщо він не знаходить файл, лише коли він щось відповідає. Однак можна перевірити вихід - якщо вихід порожній, то у нас файл відсутній

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

Більш зрозумілі:

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

Ось як це працює як невеликий сценарій:

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

Можна використати statяк альтернативу, оскільки це плоский каталог, але наведений нижче код не працюватиме рекурсивно для підкаталогів, якщо ви коли-небудь вирішите додати такі:

$ 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

Якщо взяти statідею і запустити її, ми могли б використовувати вихідний код stat як вказівку на те, чи існує файл чи ні. Ефективно ми хочемо це зробити:

$ 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

Проба зразка:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.