cho vs tìm trong Bash


28

Khi lặp qua các tệp có hai cách:

  1. sử dụng một for-loop:

    for f in *; do
        echo "$f"
    done
  2. sử dụng find:

    find * -prune | while read f; do 
        echo "$f"
    done

Giả sử hai vòng này sẽ tìm thấy cùng một danh sách các tập tin, sự khác biệt trong hai tùy chọn trong là gì Biến và xử lý?


1
Tại sao? findkhông mở các tập tin mà nó tìm thấy. Điều duy nhất tôi có thể thấy khi cắn bạn ở đây đối với một số lượng lớn tệp là ARG_MAX .
kojiro

1
Xem câu trả lời và nhận xét cho bạn biết rằng read fsẽ đọc tên tệp khi nó đọc chúng (ví dụ: tên có khoảng trống hàng đầu). Cũng find * -prunecó vẻ là một cách rất phức tạp để nói đơn giản là ls -1có?
Ian D. Allen

4
Đừng cho rằng hai vòng lặp sẽ tìm thấy cùng một tập tin; trong hầu hết các trường hợp, họ sẽ không. Ngoài ra, điều đó nên find ., không find *.
alexis

1
@terdon Vâng, phân tích cú pháp ls -llà một ý tưởng tồi. Nhưng phân tích cú pháp ls -1(đó 1không phải là một l) không tệ hơn phân tích cú pháp find * -prune. Cả hai đều thất bại trên các tập tin với dòng mới trong tên.
Ian D. Allen

5
Tôi nghi ngờ rằng mỗi chúng ta đã dành nhiều thời gian hơn để đọc câu hỏi và câu trả lời này hơn là sự khác biệt hoàn toàn về hiệu suất trong vòng đời của kịch bản được đề cập.
mpez0

Câu trả lời:


9

1.

Cái đầu tiên:

for f in *; do
  echo "$f"
done

không cho các tập tin được gọi -n, -evà các biến thể như -nenevà với một số triển khai bash, với tên tập tin chứa những dấu xồ nguợc.

Thư hai:

find * -prune | while read f; do 
  echo "$f"
done

không cho thậm chí nhiều trường hợp (file gọi !, -H, -name, (, tên tệp bắt đầu hoặc kết thúc bằng các đoạn trống hoặc chứa các ký tự xuống dòng ...)

Đó là trình bao mở rộng *, findkhông làm gì ngoài việc in các tệp mà nó nhận được dưới dạng đối số. Bạn cũng có thể đã sử dụng printf '%s\n'thay vì printfđược xây dựng cũng sẽ tránh được quá nhiều lỗi tiềm ẩn.

2.

Việc mở rộng *được sắp xếp, bạn có thể làm cho nó nhanh hơn một chút nếu bạn không cần sắp xếp. Trong zsh:

for f (*(oN)) printf '%s\n' $f

hoặc đơn giản:

printf '%s\n' *(oN)

bashkhông có gì tương đương như tôi có thể nói, vì vậy bạn cần phải dùng đến find.

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(ở trên sử dụng -print0phần mở rộng không chuẩn GNU / BSD ).

Điều đó vẫn liên quan đến việc sinh ra một lệnh find và sử dụng một while readvòng lặp chậm , vì vậy nó có thể sẽ chậm hơn so với việc sử dụng forvòng lặp trừ khi danh sách các tệp rất lớn.

4.

Ngoài ra, trái với việc mở rộng ký tự đại diện, findsẽ thực hiện một lstatcuộc gọi hệ thống trên mỗi tệp, do đó, việc không sắp xếp sẽ bù đắp cho điều đó.

Với GNU / BSD find, điều đó có thể tránh được bằng cách sử dụng -maxdepthtiện ích mở rộng của chúng , điều này sẽ kích hoạt tối ưu hóa tiết kiệm lstat:

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Bởi vì findbắt đầu xuất tên tệp ngay khi tìm thấy chúng (ngoại trừ bộ đệm đầu ra stdio), trong đó có thể nhanh hơn nếu những gì bạn làm trong vòng lặp tốn thời gian và danh sách tên tệp nhiều hơn bộ đệm stdio (4 / 8 kB). Trong trường hợp đó, việc xử lý trong vòng lặp sẽ bắt đầu trước khi findkết thúc việc tìm tất cả các tệp. Trên các hệ thống GNU và FreeBSD, bạn có thể sử dụng stdbufđể khiến điều đó xảy ra sớm hơn (vô hiệu hóa bộ đệm stdio).

5.

Cách POSIX / tiêu chuẩn / di động để chạy các lệnh cho mỗi tệp findlà sử dụng biến -execvị ngữ:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

Trong trường hợp echomặc dù, điều đó kém hiệu quả hơn so với thực hiện lặp trong shell vì shell sẽ có phiên bản dựng sẵn echotrong khi findsẽ cần phải tạo ra một quy trình mới và thực thi /bin/echotrong đó cho mỗi tệp.

Nếu bạn cần chạy một số lệnh, bạn có thể làm:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Nhưng hãy cẩn thận cmd2chỉ được thực hiện nếu cmd1thành công.

6.

Một cách chuẩn để chạy các lệnh phức tạp cho mỗi tệp là gọi shell với -exec ... {} +:

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

Lúc đó, chúng tôi trở lại hiệu quả echovì chúng tôi đang sử dụng shmột -exec +bản dựng sẵn và phiên bản sinh ra càng ít shcàng tốt.

7.

Trong các thử nghiệm của tôi trên một thư mục có 200.000 tệp có tên ngắn trên ext4, zshmột (đoạn 2.) là nhanh nhất, tiếp theo là for i in *vòng lặp đơn giản đầu tiên (mặc dù như thường lệ, bashchậm hơn rất nhiều so với các shell khác cho điều đó).


những gì hiện !làm trong lệnh find?
rubo77

@ rubo77, !là cho phủ định. ! -name . -prune more...sẽ làm -prune(và more...-pruneluôn trả về true) cho mọi tệp nhưng .. Vì vậy, nó sẽ làm more...trên tất cả các tệp trong ., nhưng sẽ loại trừ .và sẽ không rơi vào thư mục con của .. Vì vậy, nó là tương đương tiêu chuẩn của GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas 17/214

18

Tôi đã thử điều này trên một thư mục với 2259 mục và sử dụng timelệnh.

Đầu ra của time for f in *; do echo "$f"; done(trừ các tệp!) Là:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

Đầu ra của time find * -prune | while read f; do echo "$f"; done(trừ các tệp!) Là:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

Tôi đã chạy từng lệnh nhiều lần, để loại bỏ lỗi bộ nhớ cache. Điều này cho thấy việc giữ nó trong bash(đối với tôi trong ...) nhanh hơn so với việc sử dụng findvà dẫn đầu ra (đến bash)

Để hoàn thiện, tôi đã bỏ đường ống từ find, vì trong ví dụ của bạn, nó hoàn toàn dư thừa. Đầu ra của chỉ find * -prunelà:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Ngoài ra, time echo *(đầu ra không tách dòng mới, than ôi):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

Tại thời điểm này, tôi nghi ngờ lý do echo *nhanh hơn là nó không xuất ra quá nhiều dòng mới, vì vậy đầu ra không được cuộn nhiều. Hãy thử nghiệm ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

sản lượng:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

trong khi time find * -prune > /dev/nullsản lượng:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

time for f in *; do echo "$f"; done > /dev/nullsản lượng:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

và cuối cùng: time echo * > /dev/nullsản lượng:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Một số biến thể có thể được tính bởi các yếu tố ngẫu nhiên, nhưng có vẻ rõ ràng:

  • đầu ra chậm
  • chi phí đường ống một chút
  • for f in *; do ...chậm hơn so với find * -prunebản thân nó, nhưng đối với các công trình trên liên quan đến đường ống, thì nhanh hơn.

Ngoài ra, như một bên, cả hai cách tiếp cận dường như xử lý tên với không gian tốt.

CHỈNH SỬA:

Thời gian cho find . -maxdepth 1 > /dev/nullvs find * -prune > /dev/null:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Vì vậy, kết luận bổ sung:

  • find * -prunechậm hơn find . -maxdepth 1- trước đây, shell đang xử lý một quả địa cầu, sau đó xây dựng một dòng lệnh (lớn) cho find. NB: find . -prunetrả về chỉ ..

Các xét nghiệm khác time find . -maxdepth 1 -exec echo {} \; >/dev/null::

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Phần kết luận:

  • cách chậm nhất để làm điều đó cho đến nay. Như đã được chỉ ra trong các ý kiến ​​cho câu trả lời nơi phương pháp này được đề xuất, mỗi đối số sinh ra một vỏ.

Đường ống nào là dư thừa? bạn có thể hiển thị các dòng bạn sử dụng mà không có đường ống?
rubo77

2
@ rubo77 find * -prune | while read f; do echo "$f"; donecó đường ống dự phòng - tất cả các đường ống đang làm là xuất ra chính xác những gì findđầu ra của chính nó. Nếu không có một đường ống, nó sẽ chỉ đơn giản là find * -prune Đường ống chỉ dư thừa một cách cụ thể bởi vì thứ ở phía bên kia của đường ống chỉ đơn giản là sao chép stdin sang stdout (đối với hầu hết các phần). Đó là một no-op đắt tiền. Nếu bạn muốn làm công cụ với đầu ra của tìm kiếm, ngoài việc chỉ nhổ nó ra một lần nữa, thì khác.
Phil

Có lẽ thời gian chính là *. Như BitsOfNix tuyên bố: Tôi vẫn đề nghị không sử dụng *.cho findthay thế.
rubo77

@ rubo77 có vẻ như vậy. Tôi đoán tôi đã bỏ qua điều đó. Tôi đã thêm các phát hiện cho hệ thống của mình. Tôi giả sử find . -prunelà nhanh hơn bởi vì findsẽ đọc nguyên văn mục nhập thư mục, trong khi shell sẽ hoạt động tương tự, có khả năng khớp với toàn cầu (có thể tối ưu hóa cho *), sau đó xây dựng dòng lệnh lớn cho find.
Phil

1
find . -prunechỉ in .trên hệ thống của tôi. Nó gần như không làm việc gì cả. Nó hoàn toàn không giống như find * -prunehiển thị tất cả các tên trong thư mục hiện tại. Một read ftập tin trần sẽ mang tên tập tin với không gian hàng đầu.
Ian D. Allen

10

Tôi chắc chắn sẽ đi tìm mặc dù tôi sẽ thay đổi tìm của bạn thành này:

find . -maxdepth 1 -exec echo {} \;

Hiệu suất khôn ngoan, findnhanh hơn rất nhiều tùy thuộc vào nhu cầu của bạn trong khóa học. Những gì bạn có hiện tại với fornó sẽ chỉ hiển thị các tập tin / thư mục trong thư mục hiện tại chứ không hiển thị nội dung thư mục. Nếu bạn sử dụng find, nó cũng sẽ hiển thị nội dung của các thư mục con.

Tôi nói tìm là tốt hơn kể từ khi có bạn forsự *sẽ phải được mở rộng đầu tiên và tôi sợ rằng nếu bạn có một thư mục với một số lượng lớn các tập tin nó có thể cung cấp cho các lỗi danh sách đối số quá dài . Tương tự chofind *

Ví dụ, trong một trong những hệ thống mà tôi hiện đang sử dụng, có một vài thư mục có hơn 2 triệu tệp (mỗi tệp <100k):

find *
-bash: /usr/bin/find: Argument list too long

Tôi đã thêm vào -pruneđể làm cho hai ví dụ giống nhau hơn. và tôi thích đường ống hơn trong khi đó để dễ dàng áp dụng nhiều lệnh hơn trong vòng lặp
rubo77


thay đổi giới hạn cứng hầu như không phải là cách giải quyết đúng đắn từ POV của tôi. Đặc biệt khi nói về hơn 2 triệu tệp. Nếu không có sự phân tích từ Câu hỏi, đối với các trường hợp đơn giản là thư mục một cấp sẽ nhanh hơn, nhưng nếu bạn thay đổi cấu trúc tệp / thư mục thì sẽ khó di chuyển hơn. Trong khi với tìm kiếm và đó là số lượng lớn các tùy chọn, bạn có thể chuẩn bị tốt hơn. Tuy nhiên, tôi vẫn đề nghị không sử dụng * và. để tìm thay thế. Nó sẽ dễ mang theo hơn * nơi bạn không thể điều khiển hardlimit ...
BitsOfNix

4
Điều đó sẽ sinh ra một quá trình echo cho mỗi tệp (trong khi trong shell for loop, đó là phần dựng lại tiếng vang sẽ được sử dụng mà không cần tiến hành thêm quy trình) và sẽ đi vào thư mục, vì vậy nó sẽ chậm hơn rất nhiều . Cũng lưu ý rằng nó sẽ bao gồm các tập tin dấu chấm.
Stéphane Chazelas

Bạn nói đúng, tôi đã thêm maxdepth 1 để nó chỉ ở mức hiện tại.
BitsOfNix

7
find * -prune | while read f; do 
    echo "$f"
done

là một cách sử dụng vô ích find- Những gì bạn đang nói là hiệu quả "cho mỗi tệp trong thư mục ( *), không tìm thấy bất kỳ tệp nào. Ngoài ra, nó không an toàn vì nhiều lý do:

  • Dấu gạch chéo ngược trong đường dẫn được xử lý đặc biệt mà không có -rtùy chọn read. Đây không phải là một vấn đề với forvòng lặp.
  • Các dòng mới trong đường dẫn sẽ phá vỡ mọi chức năng không tầm thường trong vòng lặp. Đây không phải là một vấn đề với forvòng lặp.

Xử lý bất kỳ tên tệp nào findkhó khăn , vì vậy bạn nên sử dụng fortùy chọn vòng lặp bất cứ khi nào có thể vì lý do đó. Ngoài ra, việc chạy một chương trình bên ngoài findnói chung sẽ chậm hơn so với chạy lệnh vòng lặp bên trong như thế nào for.


@ I0b0 Điều gì về find -path './*' -prune hoặc find -path './[ucci.[*' -prune (để tránh các tệp và thư mục ẩn) dưới dạng cấu trúc tốt hơn - ở dạng đầy đủ: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs

1
Cả find's -print0cũng không xargs' -0là POSIX tương thích, và bạn không thể đặt lệnh tùy ý trong sh -c ' ... '(dấu nháy đơn không thể trốn thoát trong dấu ngoặc đơn), do đó, nó không hoàn toàn đơn giản như vậy.
l0b0

4

Nhưng chúng tôi là kẻ hút cho câu hỏi hiệu suất! Yêu cầu thử nghiệm này đưa ra ít nhất hai giả định khiến nó không có giá trị khủng khiếp.

A. Giả sử rằng họ tìm thấy cùng một tập tin.

Chà, ban đầu họ sẽ tìm thấy cùng một tệp, vì cả hai đều lặp trên cùng một địa cầu, cụ thể là *. Nhưng find * -prune | while read fbị một số sai sót khiến nó hoàn toàn không thể tìm thấy tất cả các tệp bạn mong đợi:

  1. Tìm POSIX không được đảm bảo để chấp nhận nhiều hơn một đối số đường dẫn. Hầu hết các findtriển khai thực hiện, nhưng vẫn, bạn không nên dựa vào đó.
  2. find *có thể vỡ khi bạn đánh ARG_MAX. for f in *sẽ không, bởi vì ARG_MAXáp dụng cho exec, không phải nội dung.
  3. while read fcó thể phá vỡ với tên tệp bắt đầu và kết thúc bằng khoảng trắng, sẽ bị loại bỏ. Bạn có thể khắc phục điều này với while readvà tham số mặc định của nó REPLY, nhưng điều đó vẫn không giúp ích gì cho bạn khi nói đến tên tệp có dòng mới trong đó.

B echo.. Không ai sẽ làm điều này chỉ để lặp lại tên của tập tin. Nếu bạn muốn điều đó, chỉ cần làm một trong những điều sau đây:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

Đường ống đến whilevòng lặp ở đây tạo ra một lớp con ngầm đóng lại khi vòng lặp kết thúc, điều này có thể không trực quan đối với một số người.

Để trả lời câu hỏi, đây là kết quả trong một thư mục của tôi có 184 tệp và thư mục trong đó.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

Tôi không đồng ý với tuyên bố vòng lặp while sinh ra một mạng con - trong trường hợp xấu nhất, một chủ đề mới: sau đây đang cố gắng hiển thị trước và sau, xin lỗi về định dạng kém$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Phil

Về mặt kỹ thuật tôi sai chính tả: đường ống gây ra lớp con ngầm, không phải vòng lặp while. Tôi sẽ chỉnh sửa.
kojiro

2

find *sẽ không hoạt động đúng nếu *tạo mã thông báo trông giống như vị ngữ chứ không phải đường dẫn.

Bạn không thể sử dụng --đối số thông thường để sửa lỗi này vì --cho biết kết thúc của các tùy chọn và các tùy chọn tìm thấy xuất hiện trước các đường dẫn.

Để khắc phục vấn đề này, bạn có thể sử dụng find ./*thay thế. Nhưng sau đó, nó không tạo ra chính xác các chuỗi như for x in *.

Lưu ý rằng find ./* -prune | while read f ..không thực sự sử dụng chức năng quét của find. Đây là cú pháp toàn cầu ./*thực sự đi qua thư mục và tạo tên. Sau đó, findchương trình sẽ phải thực hiện ít nhất một statkiểm tra cho mỗi một trong những tên đó. Bạn có chi phí khởi chạy chương trình và để nó truy cập vào các tệp này, sau đó thực hiện I / O để đọc đầu ra của nó.

Thật khó để tưởng tượng làm thế nào nó có thể là bất cứ điều gì nhưng kém hiệu quả hơn for x in ./* ....


1

Vâng cho người mới bắt đầu forlà một từ khóa shell, được tích hợp vào Bash, trong khi findlà một thực thi riêng biệt.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

Các forvòng lặp sẽ chỉ tìm thấy các tập tin từ các nhân vật globstar khi nó mở rộng, nó sẽ không recurse vào bất kỳ thư mục mà nó tìm thấy.

Tìm trên mặt khác cũng sẽ được cung cấp một danh sách được mở rộng bởi globalstar, nhưng nó sẽ tìm đệ quy tất cả các tệp và thư mục bên dưới danh sách mở rộng này và dẫn từng người vào whilevòng lặp.

Cả hai cách tiếp cận này có thể được coi là nguy hiểm theo nghĩa là chúng không xử lý các đường dẫn hoặc tên tệp có chứa khoảng trắng.

Đó là tất cả những gì tôi có thể nghĩ về việc nhận xét đáng giá về 2 cách tiếp cận này.


Tôi đã thêm -prune vào lệnh find, vì vậy chúng giống nhau hơn.
rubo77

0

Nếu tất cả các tệp được trả về bởi find có thể được xử lý bằng một lệnh duy nhất (rõ ràng không thể áp dụng cho ví dụ echo của bạn ở trên), bạn có thể sử dụng xargs:

find * |xargs some-command

0

Trong nhiều năm tôi đã sử dụng điều này: -

find . -name 'filename'|xargs grep 'pattern'|more

để tìm một số tệp nhất định (ví dụ * .txt) có chứa một mẫu mà grep có thể tìm và đưa nó vào nhiều hơn để nó không cuộn ra khỏi màn hình. Đôi khi tôi sử dụng ống >> để ghi kết quả vào một tệp khác mà tôi có thể xem sau.

Đây là một mẫu kết quả: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
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.