Bash - Kiểm tra thư mục cho các tệp theo danh sách tên tệp một phần


8

Tôi có một máy chủ nhận tệp mỗi khách hàng mỗi ngày vào một thư mục. Tên tệp được xây dựng như sau:

uuid_datestring_other-data

Ví dụ:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid là một định dạng chuẩn uuid.
  • datestringlà đầu ra từ date +%Y%m%d.
  • other-data là chiều dài thay đổi nhưng sẽ không bao giờ chứa dấu gạch dưới.

Tôi có một tập tin định dạng:

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

Tôi cần kiểm tra xem mọi uuid được liệt kê trong tệp có một tệp tương ứng trong thư mục, sử dụng bash.

Tôi đã đi xa đến mức này, nhưng cảm giác như mình đến từ hướng sai bằng cách sử dụng câu lệnh if và rằng tôi cần phải lặp qua các tệp trong thư mục nguồn.

Các biến source_directory và uuid_list đã được gán trước đó trong tập lệnh:

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

Làm cách nào để kiểm tra xem các tệp trong danh sách của tôi có tồn tại trong thư mục không? Tôi muốn sử dụng chức năng bash càng nhiều càng tốt, nhưng không chống lại việc sử dụng các lệnh nếu cần.


Con trăn? Và thư mục máy chủ có "phẳng" không?
Jacob Vlijm

Có nó bằng phẳng, không có thư mục con. Tôi thà gắn bó với chỉ bash nếu có thể.
Arronical 4/2/2016

1
Ok, tôi sẽ không đăng.
Jacob Vlijm


Tôi không thực sự thấy những gì sai với những gì bạn có. Bạn sẽ cần phải lặp qua các UUID hoặc các tệp, tại sao một vòng lặp sẽ tốt hơn vòng lặp kia?
terdon 4/2/2016

Câu trả lời:


5

Đi qua các tệp, tạo một mảng kết hợp trên các uuids có trong tên của chúng (Tôi đã sử dụng mở rộng tham số để trích xuất uuid). Việc đọc danh sách, kiểm tra mảng kết hợp cho từng uuid và báo cáo xem tệp có được ghi hay không.

#!/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
Đẹp (+1), nhưng tại sao điều này tốt hơn những gì OP đang làm? Bạn dường như đang làm điều cơ bản giống nhau nhưng trong hai bước thay vì một.
terdon

1
@terdon: Sự khác biệt chính là điều này hoạt động :-) Việc mở rộng ký tự đại diện được thực hiện chỉ một lần, không phải mỗi khi bạn đọc một dòng từ danh sách, điều này cũng có thể nhanh hơn.
choroba

Vâng, đó là một sự khác biệt quan trọng. Đủ công bằng :)
terdon 4/2/2016

Đây là lời cảm ơn tuyệt vời, đã có +1 của tôi. Có cách nào để bao gồm đường dẫn đến thư mục chứa các tập tin không? Tôi biết tôi có thể cdvào thư mục trong kịch bản, nhưng chỉ tự hỏi vì mục đích đạt được kiến ​​thức.
Arronical 4/2/2016

@Arronical: Có thể, nhưng bạn sẽ phải xóa đường dẫn khỏi chuỗi, có thể với file=${file##*/}.
choroba

5

Đây là một cách tiếp cận "bashy" và ngắn gọn hơn:

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

Lưu ý rằng mặc dù ở trên khá đẹp và sẽ hoạt động tốt đối với một vài tệp, tốc độ của nó phụ thuộc vào số lượng UUID và sẽ rất chậm nếu bạn cần xử lý nhiều. Nếu đó là trường hợp, hãy sử dụng giải pháp của @ choroba hoặc, đối với một cái gì đó thực sự nhanh chóng, hãy tránh vỏ và gọi 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

Chỉ để minh họa sự khác biệt về thời gian, tôi đã thử nghiệm phương pháp bash của mình, choroba và perl của tôi trên một tệp có 20000 UUID trong đó 18001 có tên tệp tương ứng. Lưu ý rằng mỗi thử nghiệm đã được chạy bằng cách chuyển hướng đầu ra của tập lệnh tới /dev/null.

  1. Bash của tôi (~ 3,5 phút)

    real   3m39.775s
    user   1m26.083s
    sys    2m13.400s
  2. Choroba's (bash, ~ 0,7 giây)

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
  3. Perl của tôi (~ 0,1 giây):

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

+1 cho một phương thức ngắn gọn tuyệt vời, điều này sẽ phải được thực hiện từ trong thư mục chứa các tệp. Tôi biết tôi có thể cdvào thư mục trong tập lệnh, nhưng có một phương pháp theo đó đường dẫn tệp có thể được bao gồm trong tìm kiếm không?
Arronical 4/2/2016

@Arronical chắc chắn, xem câu trả lời cập nhật. Bạn có thể sử dụng ${source_directory}giống như bạn đang làm trong kịch bản của mình.
terdon 4/2/2016

Hoặc sử dụng "$2"và chuyển nó vào kịch bản như một đối số thứ hai.
alexis

Kiểm tra xem cái này chạy đủ nhanh cho mục đích của bạn-- sẽ nhanh hơn khi thực hiện với một lần quét thư mục, thay vì nhiều lần tra cứu tệp như thế này.
alexis

1
@alexis vâng, bạn hoàn toàn đúng. Tôi đã thực hiện một số thử nghiệm và điều này trở nên rất chậm nếu số lượng UUID / tệp tăng lên. Tôi đã thêm một cách tiếp cận perl (có thể được chạy dưới dạng một lớp lót trong tập lệnh bash, vì vậy về mặt kỹ thuật, vẫn bash nếu bạn mở một số cách đặt tên sáng tạo) nhanh hơn nhiều.
terdon

3

Đây là Bash thuần túy (tức là không có lệnh bên ngoài) và đó là cách tiếp cận trùng hợp nhất mà tôi có thể nghĩ ra.

Nhưng hiệu suất-khôn ngoan thực sự không tốt hơn nhiều so với những gì bạn hiện có.

Nó sẽ đọc từng dòng từ path/to/file; cho mỗi dòng, nó sẽ lưu trữ các trường đầu tiên trong $uuidvà in một thông báo nếu một tập tin phù hợp với mô hình path/to/directory/$uuid*không tìm thấy:

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

Gọi nó với path/to/script path/to/file path/to/directory.

Đầu ra mẫu bằng cách sử dụng tệp đầu vào mẫu trong câu hỏi trên hệ thống phân cấp thư mục kiểm tra có chứa tệp mẫu trong câu hỏi:

% 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

Ý tưởng ở đây là không phải lo lắng về việc báo cáo lỗi mà shell sẽ báo cáo cho bạn. Nếu bạn cố <mở một tệp không tồn tại, shell của bạn sẽ khiếu nại. Trên thực tế, nó sẽ trả trước tập lệnh của bạn $0và số dòng mà lỗi xảy ra với đầu ra lỗi khi nó xảy ra ... Đây là thông tin tốt được cung cấp theo mặc định - vì vậy đừng bận tâm.

Bạn cũng không cần phải lấy tệp theo từng dòng như thế - nó có thể rất chậm. Điều này mở rộng toàn bộ mọi thứ trong một lần bắn ra một mảng các đối số được phân tách bằng khoảng trắng và nó xử lý hai lần một lần. Nếu dữ liệu của bạn phù hợp với ví dụ của bạn, thì $1sẽ luôn là uuid của bạn và $2sẽ là của bạn $name. Nếu bashcó thể mở một trận đấu cho uuid của bạn - và chỉ có một trận đấu như vậy tồn tại - thì printfsẽ xảy ra. Mặt khác, nó không và shell ghi chẩn đoán cho stderr về lý do tại sao.


1
@kos - tập tin có tồn tại không? nếu không thì nó hành xử như dự định. unset IFSđảm bảo $(cat <uuid_file)được phân chia trên không gian trắng. Vỏ tách ra $IFSkhác nhau khi nó chỉ bao gồm không gian trắng hoặc không được đặt. Các mở rộng phân chia như vậy không bao giờ có bất kỳ trường null nào vì tất cả các chuỗi khoảng trắng chỉ đứng trong một dấu phân cách trường duy nhất. Miễn là chỉ có hai trường tách biệt không phải khoảng trắng trên mỗi dòng thì nó sẽ hoạt động, tôi nghĩ vậy. trong bash, dù sao đi nữa set -fđảm bảo rằng phần mở rộng không được trích dẫn không được diễn giải cho các khối và đặt + f đảm bảo rằng các bản mở rộng sau này.
mikeerv

@kos - tôi chỉ sửa nó thôi. Tôi không nên sử dụng <>vì điều đó tạo ra một tệp không tồn tại. <sẽ báo cáo như tôi có nghĩa là nó. vấn đề có thể xảy ra với điều đó - và lý do tôi sử dụng không chính xác <>ở vị trí đầu tiên - là nếu đó là tệp đường ống không có đầu đọc hoặc giống như một char dev được đệm dòng thì nó sẽ bị treo. điều đó có thể tránh được bằng cách xử lý lỗi đầu ra rõ ràng hơn và thực hiện [ -f "$dir/$1"* ]. chúng ta đang nói về uuids ở đây, và vì vậy nó không bao giờ nên mở rộng ra nhiều hơn một tệp. thật là tuyệt vời mặc dù cách nó báo cáo các tên tệp không thành công cho stderr như thế.
mikeerv

@kos - thực ra, tôi cho rằng tôi có thể sử dụng ulimit để ngăn không cho nó tạo bất kỳ tập tin nào và vì vậy <>vẫn có thể sử dụng theo cách đó ... <>sẽ tốt hơn nếu toàn cầu có thể mở rộng sang một thư mục bởi vì trên linux, việc đọc / ghi sẽ thất bại và nói - đó là một thư mục.
mikeerv

@kos - ôi! Tôi xin lỗi - tôi chỉ bị câm - bạn có hai trận đấu, và vì vậy nó đang làm điều đúng đắn. ý tôi là nó bị lỗi theo cách đó nếu có thể có hai trận đấu, những trận đấu này được coi là uuids - không bao giờ có khả năng có hai tên tương tự trùng với cùng một quả cầu. thats hoàn toàn có chủ ý - và nó mơ hồ trong một cách mà nó không nên được. bạn hiểu ý tôi chứ? Đặt tên tệp cho một quả cầu không phải là vấn đề, - ký tự đặc biệt không có liên quan ở đây - vấn đề là bashsẽ chỉ chấp nhận một quả cầu chuyển hướng nếu nó chỉ khớp với một tệp. xem man bashdưới mục GIẢM.
mikeerv

1

Cách tôi tiếp cận là lấy uuids từ tệp trước, sau đó sử dụng find

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

Để sẵn sàng,

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

Ví dụ với một danh sách các tệp trong /etc/, tìm kiếm tên tệp passwd, nhóm, fstab và 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

Vì bạn đã đề cập đến thư mục phẳng, bạn có thể sử dụng -printf "%f\n"tùy chọn để chỉ in tên tệp

Điều này không làm là liệt kê các tập tin bị thiếu. findNhược điểm nhỏ là nó không cho bạn biết nếu nó không tìm thấy tệp, chỉ khi nó khớp với thứ gì đó. Tuy nhiên, những gì người ta có thể làm là kiểm tra đầu ra - nếu đầu ra trống, thì chúng ta có một tệp bị thiếu

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

Dễ đọc hơn:

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

Và đây là cách nó thực hiện như một kịch bản nhỏ:

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

Người ta có thể sử dụng statthay thế, vì đó là một thư mục phẳng, nhưng mã dưới đây sẽ không hoạt động đệ quy cho các thư mục con nếu bạn quyết định thêm chúng:

$ 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

Nếu chúng ta lấy statý tưởng và chạy với nó, chúng ta có thể sử dụng mã thoát của stat làm chỉ dẫn cho việc một tệp có tồn tại hay không. Hiệu quả, chúng tôi muốn làm điều này:

$ 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

Chạy mẫu:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.