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, насколько это возможно, но я не против использования команд, если это необходимо.

Arronical
источник
Python? И каталог сервера "плоский"?
Джейкоб Влийм
Да, это плоский, без подкаталогов. Я бы предпочел придерживаться только bash, если это возможно.
Arronical
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"
choroba
источник
1
Приятно (+1), но почему это лучше, чем то, что делал ОП? Вы, кажется, делаете то же самое, но в два этапа вместо одного.
тердон
1
@terdon: Основное отличие заключается в том, что это работает :-) Расширение подстановочного знака выполняется только один раз, а не каждый раз, когда вы читаете строку из списка, что также может быть быстрее.
Чороба
Да, это важное различие. Достаточно справедливо :)
Terdon
Это чудесное спасибо, получил мой +1. Есть ли способ включить путь к каталогу, который содержит файлы? Я знаю, я могу cdв каталог в сценарии, но просто интересно, ради получения знаний.
Arronical
@Arronical: это возможно, но вам придется удалить путь из строки, возможно с помощью file=${file##*/}.
Чороба
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

Просто чтобы проиллюстрировать разницу во времени, я протестировал мой подход bash, choroba и мой perl к файлу с 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
    
terdon
источник
+1 для фантастически сжатого метода, это должно быть выполнено из каталога, содержащего файлы. Я знаю, что могу cdв каталог в сценарии, но есть ли способ, с помощью которого путь поиска файлов может быть включен в поиск?
Arronical
@ Конечно, смотрите обновленный ответ. Вы можете использовать так ${source_directory}же, как вы делали в вашем сценарии.
тердон
Или используйте "$2"и передайте его сценарию в качестве второго аргумента.
Алексис
Убедитесь, что это работает достаточно быстро для ваших целей - было бы быстрее сделать это с одним сканированием каталога, а не с множеством поисков файлов, подобных этому.
Алексис
1
@alexis да, ты совершенно прав. Я провел некоторое тестирование, и это становится очень медленным, если количество UUID / файлов увеличивается. Я добавил подход perl (который может быть запущен как один изнутри скрипта bash, так что технически он все еще 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произойдет. В противном случае это не так, и оболочка пишет диагностику в stderr о том, почему.

mikeserv
источник
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, чтобы он вообще не создавал никаких файлов, и поэтому <>все равно можно было бы использовать таким образом ... <>лучше, если glob может расширяться до каталога, потому что на linux чтение / запись будет потерпеть неудачу и сказать - это каталог.
mikeserv
@kos - о! Извините - я просто тупой - у вас два матча, и это делает правильные вещи. Я имею в виду, что таким образом можно ошибиться, если два совпадения могут иметь место, предполагается, что это uuids - никогда не должно быть возможности двух похожих имен, которые соответствуют одному и тому же глобу. вот полностью преднамеренным - и это является неоднозначным таким образом , что оно не должно быть. понимаешь, о чем я? Наименование файла для глобуса не является проблемой, - специальные символы здесь не актуальны - проблема в том, что 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
Сергей Колодяжный
источник