Tại sao lặp đi lặp lại tìm đầu ra thực tiễn xấu?


170

Câu hỏi này được lấy cảm hứng từ

Tại sao sử dụng vòng lặp shell để xử lý văn bản được coi là thực tiễn xấu?

Tôi thấy những cấu trúc này

for file in `find . -type f -name ...`; do smth with ${file}; done

for dir in $(find . -type d -name ...); do smth with ${dir}; done

được sử dụng ở đây hầu như hàng ngày ngay cả khi một số người dành thời gian để bình luận về những bài đăng đó giải thích lý do tại sao loại công cụ này nên tránh ...
Xem số lượng bài đăng như vậy (và thực tế là đôi khi những bình luận đó chỉ đơn giản là bị bỏ qua) Tôi nghĩ rằng tôi cũng có thể hỏi một câu hỏi:

Tại sao việc lặp đi lặp lại findthực tiễn tồi là gì và cách phù hợp để chạy một hoặc nhiều lệnh cho mỗi tên tệp / đường dẫn được trả về find?


12
Tôi nghĩ rằng đây là loại như "Không bao giờ phân tích đầu ra ls!" - bạn chắc chắn có thể thực hiện từng bước một, nhưng chúng là một cuộc tấn công nhanh hơn là chất lượng sản xuất. Hoặc, nói chung hơn, chắc chắn không bao giờ giáo điều.
Bruce Ediger


Điều này nên được biến thành một câu trả lời kinh điển
Zaid

6
Bởi vì điểm tìm thấy là lặp lại những gì nó tìm thấy.
OrangeDog

2
Một điểm phụ trợ - bạn có thể muốn gửi đầu ra tới một tệp, sau đó xử lý nó sau trong tập lệnh. Bằng cách này, danh sách tập tin có sẵn để xem xét nếu bạn cần gỡ lỗi tập lệnh.
dùng117529

Câu trả lời:


87

Vấn đề

for f in $(find .)

kết hợp hai thứ không tương thích.

findin một danh sách các đường dẫn tệp được phân tách bằng các ký tự dòng mới. Trong khi toán tử split + global được gọi khi bạn rời khỏi mà không $(find .)được trích dẫn trong ngữ cảnh danh sách đó, nó sẽ phân tách nó trên các ký tự của $IFS(theo mặc định bao gồm dòng mới, nhưng cũng có khoảng trắng và tab (và NUL in zsh)) và thực hiện toàn cầu hóa trên mỗi từ kết quả (ngoại trừ trong zsh) (và thậm chí mở rộng cú đúp trong các dẫn xuất ksh93 hoặc pdksh!).

Ngay cả khi bạn thực hiện nó:

IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
              # but not ksh93)
for f in $(find .) # invoke split+glob

Điều đó vẫn sai khi ký tự dòng mới có giá trị như bất kỳ trong đường dẫn tệp. Đầu ra của find -printđơn giản là không thể xử lý sau đáng tin cậy (ngoại trừ bằng cách sử dụng một số thủ thuật phức tạp, như được hiển thị ở đây ).

Điều đó cũng có nghĩa là shell cần lưu trữ đầu ra findđầy đủ, sau đó phân tách + global nó (nghĩa là lưu trữ đầu ra đó lần thứ hai trong bộ nhớ) trước khi bắt đầu lặp qua các tệp.

Lưu ý rằng find . | xargs cmdcó các vấn đề tương tự (ở đó, khoảng trắng, dòng mới, trích dẫn đơn, trích dẫn kép và dấu gạch chéo ngược (và với một số xargbyte triển khai không tạo thành một phần của các ký tự hợp lệ) là một vấn đề)

Lựa chọn thay thế chính xác hơn

Cách duy nhất để sử dụng một forvòng lặp trên đầu ra findlà sử dụng zshcác hỗ trợ đó IFS=$'\0'và:

IFS=$'\0'
for f in $(find . -print0)

(thay thế -print0với -exec printf '%s\0' {} +cho findhiện thực mà không hỗ trợ phi tiêu chuẩn (nhưng khá phổ biến hiện nay) -print0).

Ở đây, cách chính xác và di động là sử dụng -exec:

find . -exec something with {} \;

Hoặc nếu somethingcó thể mất nhiều hơn một đối số:

find . -exec something with {} +

Nếu bạn cần danh sách các tệp được xử lý bởi trình bao:

find . -exec sh -c '
  for file do
    something < "$file"
  done' find-sh {} +

(hãy cẩn thận nó có thể bắt đầu nhiều hơn một sh).

Trên một số hệ thống, bạn có thể sử dụng:

find . -print0 | xargs -r0 something with

mặc dù có chút lợi thế hơn các cú pháp tiêu chuẩn và phương tiện something's stdinlà một trong hai ống hay /dev/null.

Một lý do bạn có thể muốn sử dụng có thể là sử dụng -Ptùy chọn GNU xargsđể xử lý song song. Các stdinvấn đề cũng có thể được làm việc xung quanh với GNU xargsvới -atùy chọn với vỏ hỗ trợ thay thế tiến trình:

xargs -r0n 20 -P 4 -a <(find . -print0) something

ví dụ, để chạy tối đa 4 yêu cầu đồng thời của somethingmỗi lần lấy 20 đối số tệp.

Với zshhoặc bash, một cách khác để lặp qua đầu ra find -print0là:

while IFS= read -rd '' file <&3; do
  something "$file" 3<&-
done 3< <(find . -print0)

read -d '' đọc các bản ghi phân cách NUL thay vì các bản ghi phân cách dòng mới.

bash-4.4và ở trên cũng có thể lưu trữ các tệp được trả về bởi find -print0trong một mảng với:

readarray -td '' files < <(find . -print0)

Các zshtương đương (trong đó có các lợi thế của việc bảo tồn findcủa trạng thái thoát):

files=(${(0)"$(find . -print0)"})

Với zsh, bạn có thể dịch hầu hết các findbiểu thức thành sự kết hợp giữa tính toán đệ quy với vòng loại toàn cầu. Chẳng hạn, việc lặp lại find . -name '*.txt' -type f -mtime -1sẽ là:

for file (./**/*.txt(ND.m-1)) cmd $file

Hoặc là

for file (**/*.txt(ND.m-1)) cmd -- $file

(hãy cẩn thận với nhu cầu --như với **/*, đường dẫn tệp không bắt đầu bằng ./, vì vậy có thể bắt đầu bằng -ví dụ).

ksh93bashcuối cùng đã thêm hỗ trợ cho **/(mặc dù không có nhiều tiến bộ hơn về hình thức đệ quy đệ quy), nhưng vẫn không phải là vòng loại toàn cầu khiến việc sử dụng **rất hạn chế ở đó. Cũng hãy cẩn thận bashtrước 4.3 sau các liên kết tượng trưng khi hạ xuống cây thư mục.

Giống như lặp lại $(find .), điều đó cũng có nghĩa là lưu trữ toàn bộ danh sách các tệp trong bộ nhớ 1 . Điều đó có thể là mong muốn mặc dù trong một số trường hợp khi bạn không muốn hành động của mình trên các tệp có ảnh hưởng đến việc tìm kiếm tệp (như khi bạn thêm nhiều tệp có thể tự tìm thấy).

Các cân nhắc về độ tin cậy / bảo mật khác

Điều kiện cuộc đua

Bây giờ, nếu chúng ta đang nói về độ tin cậy, chúng ta phải đề cập đến các điều kiện cuộc đua giữa thời gian find/ zshtìm tệp và kiểm tra xem nó có đáp ứng các tiêu chí và thời gian sử dụng không ( cuộc đua TOCTOU ).

Ngay cả khi hạ xuống một cây thư mục, người ta phải đảm bảo không theo các liên kết tượng trưng và làm điều đó mà không có cuộc đua TOCTOU. find( findÍt nhất là GNU ) thực hiện điều đó bằng cách mở các thư mục bằng openat()các O_NOFOLLOWcờ bên phải (nơi được hỗ trợ) và giữ một bộ mô tả tệp mở cho mỗi thư mục, zsh/ bash/ kshkhông làm điều đó. Vì vậy, khi đối mặt với kẻ tấn công có thể thay thế một thư mục bằng một liên kết tượng trưng vào đúng thời điểm, bạn có thể sẽ giảm xuống thư mục sai.

Thậm chí nếu findkhông xuống thư mục đúng cách, với -exec cmd {} \;và thậm chí nhiều hơn như vậy với -exec cmd {} +, một khi cmdđược thực thi, ví dụ như cmd ./foo/barhay cmd ./foo/bar ./foo/bar/baz, do thời gian cmdtận dụng ./foo/bar, các thuộc tính của barkhông còn có thể đáp ứng được các tiêu chí phù hợp bằng find, nhưng thậm chí tệ hơn, ./foocó thể là được thay thế bằng một liên kết tượng trưng đến một nơi khác (và cửa sổ cuộc đua được làm lớn hơn rất nhiều với -exec {} +nơi findchờ đợi để có đủ tệp để gọi cmd).

Một số findtriển khai có một vị từ (chưa chuẩn) -execdirđể giảm bớt vấn đề thứ hai.

Với:

find . -execdir cmd -- {} \;

find chdir()s vào thư mục mẹ của tập tin trước khi chạy cmd. Thay vì gọi cmd -- ./foo/bar, nó gọi cmd -- ./bar( cmd -- barvới một số triển khai, do đó --), do đó, vấn đề với ./fooviệc thay đổi thành một liên kết tượng trưng là tránh. Điều đó làm cho việc sử dụng các lệnh như rman toàn hơn (nó vẫn có thể xóa một tệp khác, nhưng không phải là tệp trong một thư mục khác), nhưng không phải là các lệnh có thể sửa đổi các tệp trừ khi chúng được thiết kế để không tuân theo các liên kết tượng trưng.

-execdir cmd -- {} +đôi khi cũng hoạt động nhưng với một số triển khai bao gồm một số phiên bản GNU find, nó tương đương với -execdir cmd -- {} \;.

-execdir cũng có lợi ích khi làm việc xung quanh một số vấn đề liên quan đến cây thư mục quá sâu.

Trong:

find . -exec cmd {} \;

kích thước của đường dẫn được cung cấp cmdsẽ tăng theo độ sâu của thư mục tệp. Nếu kích thước đó lớn hơn PATH_MAX(giống như 4k trên Linux), thì bất kỳ cuộc gọi hệ thống nào cmdthực hiện trên đường dẫn đó đều bị ENAMETOOLONGlỗi.

Với -execdir, chỉ tên tệp (có thể có tiền tố ./) được chuyển đến cmd. Bản thân tên tệp trên hầu hết các hệ thống tệp có giới hạn ( NAME_MAX) thấp hơn nhiều PATH_MAX, do đó ENAMETOOLONGlỗi ít gặp phải hơn.

Byte vs ký tự

Ngoài ra, thường bị bỏ qua khi xem xét bảo mật xung quanh findvà nói chung với việc xử lý tên tệp nói chung là trên hầu hết các hệ thống giống Unix, tên tệp là chuỗi byte (bất kỳ giá trị byte nào nhưng trong đường dẫn tệp và trên hầu hết các hệ thống ( Những cái dựa trên ASCII, chúng ta sẽ bỏ qua những cái dựa trên EBCDIC hiếm hoi bây giờ) 0x2f là dấu phân cách đường dẫn).

Tùy thuộc vào các ứng dụng để quyết định xem họ có muốn coi các byte đó là văn bản hay không. Và họ thường làm, nhưng nói chung việc dịch từ byte sang ký tự được thực hiện dựa trên ngôn ngữ của người dùng, dựa trên môi trường.

Điều đó có nghĩa là một tên tệp đã cho có thể có cách trình bày văn bản khác nhau tùy thuộc vào miền địa phương. Chẳng hạn, chuỗi byte 63 f4 74 e9 2e 74 78 74sẽ côté.txtdành cho một ứng dụng diễn giải tên tệp đó trong miền địa phương nơi bộ ký tự là ISO-8859-1 và thay cєtщ.txtvào đó là miền địa phương nơi bộ ký tự là IS0-8859-5.

Tệ hơn Ở một địa phương nơi bộ ký tự là UTF-8 (tiêu chuẩn hiện nay), 63 f4 74 e9 2e 74 78 74 đơn giản là không thể ánh xạ thành các ký tự!

findlà một ứng dụng như vậy coi tên tệp là văn bản cho -name/ -pathvị từ của nó (và hơn thế nữa, giống như -inamehoặc -regexvới một số triển khai).

Điều đó có nghĩa là, ví dụ, với một số findtriển khai (bao gồm cả GNU find).

find . -name '*.txt'

sẽ không tìm thấy 63 f4 74 e9 2e 74 78 74tệp của chúng tôi ở trên khi được gọi trong ngôn ngữ UTF-8 vì *(khớp 0 hoặc nhiều ký tự , không phải byte) không thể khớp với các ký tự không phải là ký tự đó.

LC_ALL=C find... sẽ giải quyết vấn đề vì miền địa phương C ngụ ý một byte cho mỗi ký tự và (nói chung) đảm bảo rằng tất cả các giá trị byte ánh xạ tới một ký tự (mặc dù có thể không xác định được một số giá trị byte).

Bây giờ khi nói đến việc lặp qua các tên tệp từ shell, byte và ký tự đó cũng có thể trở thành một vấn đề. Chúng tôi thường thấy 4 loại vỏ chính trong vấn đề đó:

  1. Những cái mà vẫn không nhận biết nhiều byte như thế nào dash. Đối với họ, một byte ánh xạ tới một ký tự. Chẳng hạn, trong UTF-8, côtécó 4 ký tự, nhưng 6 byte. Ở một địa phương nơi UTF-8 là bộ ký tự, trong

    find . -name '????' -exec dash -c '
      name=${1##*/}; echo "${#name}"' sh {} \;
    

    findsẽ tìm thấy thành công các tệp có tên gồm 4 ký tự được mã hóa trong UTF-8, nhưng dashsẽ báo cáo độ dài trong khoảng từ 4 đến 24.

  2. yash: mặt đối diện, sự đối nghịch. Nó chỉ giao dịch với các nhân vật . Tất cả các đầu vào nó được dịch nội bộ sang các ký tự. Nó tạo ra lớp vỏ nhất quán, nhưng điều đó cũng có nghĩa là nó không thể đối phó với các chuỗi byte tùy ý (những chuỗi không dịch thành các ký tự hợp lệ). Ngay cả trong miền địa phương C, nó không thể đối phó với các giá trị byte trên 0x7f.

    find . -exec yash -c 'echo "$1"' sh {} \;
    

    trong một miền địa phương UTF-8 sẽ thất bại trên ISO-8859-1 của chúng tôi côté.txttừ trước đó.

  3. Những người thích bashhoặc zshnơi hỗ trợ nhiều byte đã được thêm dần dần. Chúng sẽ quay trở lại xem xét các byte không thể ánh xạ tới các ký tự như thể chúng là các ký tự. Họ vẫn có một vài lỗi ở đây và đặc biệt là với các bộ ký tự nhiều byte ít phổ biến hơn như GBK hoặc BIG5-HKSCS (những lỗi này khá khó chịu vì nhiều ký tự nhiều byte của họ chứa byte trong phạm vi 0-127 (như các ký tự ASCII) ).

  4. Những người như shFreeBSD (ít nhất là 11) hoặc mksh -o utf8-modehỗ trợ nhiều byte nhưng chỉ dành cho UTF-8.

Ghi chú

1 Để hoàn thiện, chúng tôi có thể đề cập đến một cách hacky zshđể lặp qua các tệp bằng cách sử dụng tính năng đệ quy mà không lưu toàn bộ danh sách trong bộ nhớ:

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdlà một vòng loại toàn cầu gọi cmd(thường là một hàm) với đường dẫn tệp hiện tại $REPLY. Hàm trả về true hoặc false để quyết định xem có nên chọn tệp không (và cũng có thể sửa đổi $REPLYhoặc trả về một số tệp trong một $replymảng). Ở đây chúng tôi thực hiện xử lý trong hàm đó và trả về false để tệp không được chọn.


Nếu zsh và bash có sẵn, bạn thể tốt hơn là chỉ sử dụng các cấu trúc vỏ và vỏ thay vì cố gắng findhành xử an toàn. Globbing theo mặc định là an toàn trong khi tìm kiếm không an toàn theo mặc định.
Kevin

@Kevin, xem chỉnh sửa.
Stéphane Chazelas

182

Tại sao lặp đi lặp lại findđầu ra của thực tiễn xấu?

Câu trả lời đơn giản là:

Bởi vì tên tập tin có thể chứa bất kỳ nhân vật.

Do đó, không có ký tự có thể in mà bạn có thể sử dụng đáng tin cậy để phân định tên tệp.


Dòng mới thường được sử dụng (không chính xác) để phân định tên tệp, bởi vì việc đưa các ký tự dòng mới vào tên tệp là không bình thường .

Tuy nhiên, nếu bạn xây dựng phần mềm của mình xung quanh các giả định tùy ý, tốt nhất là bạn không thể xử lý các trường hợp bất thường và tệ nhất là tự mở ra các khai thác độc hại để kiểm soát hệ thống của bạn. Vì vậy, đó là một câu hỏi về sự mạnh mẽ và an toàn.

Nếu bạn có thể viết phần mềm theo hai cách khác nhau và một trong số đó xử lý các trường hợp cạnh (đầu vào bất thường) một cách chính xác, nhưng cách khác dễ đọc hơn, bạn có thể lập luận rằng có sự đánh đổi. (Tôi sẽ không. Tôi thích mã chính xác.)

Tuy nhiên, nếu phiên bản chính xác, mạnh mẽ của mã cũng dễ đọc, không có lý do gì để viết mã bị lỗi trong các trường hợp cạnh. Đây là trường hợp findvà cần phải chạy một lệnh trên mỗi tệp được tìm thấy.


Chúng ta hãy cụ thể hơn: Trên hệ thống UNIX hoặc Linux, tên tệp có thể chứa bất kỳ ký tự nào ngoại trừ một /(được sử dụng làm dấu tách thành phần đường dẫn) và chúng có thể không chứa byte rỗng.

Do đó, một byte null là cách chính xác duy nhất để phân định tên tệp.


Do GNU findbao gồm một -print0tệp chính sẽ sử dụng byte rỗng để phân định tên tệp mà nó in, GNU find có thể được sử dụng một cách an toàn với GNU xargs-0cờ của nó (và -rcờ) để xử lý đầu ra của find:

find ... -print0 | xargs -r0 ...

Tuy nhiên, không có lý do chính đáng để sử dụng hình thức này, bởi vì:

  1. Nó thêm một sự phụ thuộc vào các công cụ tìm kiếm GNU mà không cần phải ở đó và
  2. findđược thiết kế để có thể chạy các lệnh trên các tệp mà nó tìm thấy.

Ngoài ra, GNU xargsyêu cầu -0-r, trong khi FreeBSD xargschỉ yêu cầu -0(và không có -rtùy chọn), và một số xargskhông hỗ trợ -0gì cả. Vì vậy, tốt nhất là chỉ cần bám vào các tính năng POSIX của find(xem phần tiếp theo) và bỏ qua xargs.

Về điểm 2 find, khả năng chạy các lệnh trên các tập tin của Wap, tôi nghĩ rằng Mike Loukides đã nói điều đó tốt nhất:

findDoanh nghiệp đang đánh giá các biểu thức - không định vị tệp. Vâng, findchắc chắn định vị các tập tin; Nhưng đó thực sự chỉ là một tác dụng phụ.

- Unix Power Tools


POSIX chỉ định sử dụng find

Cách thích hợp để chạy một hoặc nhiều lệnh cho mỗi findkết quả là gì?

Để chạy một lệnh duy nhất cho mỗi tệp được tìm thấy, hãy sử dụng:

find dirname ... -exec somecommand {} \;

Để chạy nhiều lệnh theo thứ tự cho mỗi tệp được tìm thấy, trong đó lệnh thứ hai chỉ nên được chạy nếu lệnh đầu tiên thành công, sử dụng:

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Để chạy một lệnh trên nhiều tệp cùng một lúc:

find dirname ... -exec somecommand {} +

find kết hợp với sh

Nếu bạn cần sử dụng các tính năng shell trong lệnh, chẳng hạn như chuyển hướng đầu ra hoặc tước một phần mở rộng ra khỏi tên tệp hoặc một cái gì đó tương tự, bạn có thể sử dụng sh -ccấu trúc. Bạn nên biết một vài điều về điều này:

  • Không bao giờ nhúng {}trực tiếp vào shmã. Điều này cho phép thực thi mã tùy ý từ tên tệp được tạo độc hại. Ngoài ra, nó thực sự thậm chí không được chỉ định bởi POSIX rằng nó sẽ hoạt động hoàn toàn. (Xem điểm tiếp theo.)

  • Không sử dụng {}nhiều lần hoặc sử dụng nó như một phần của cuộc tranh luận dài hơn. Đây không phải là di động. Ví dụ: không làm điều này:

    find ... -exec cp {} somedir/{}.bak \;

    Để trích dẫn thông số kỹ thuật POSIX chofind :

    Nếu một chuỗi object_name hoặc đối số chứa hai ký tự "{}", nhưng không chỉ hai ký tự "{}", thì nó được xác định theo thực thi cho dù tìm thay thế hai ký tự đó hoặc sử dụng chuỗi mà không thay đổi.

    ... Nếu có nhiều hơn một đối số chứa hai ký tự "{}", hành vi không được chỉ định.

  • Các đối số theo chuỗi lệnh shell được truyền cho -ctùy chọn được đặt thành các tham số vị trí của shell, bắt đầu bằng$0 . Không bắt đầu với $1.

    Vì lý do này, tốt hơn là bao gồm một $0giá trị "giả" , chẳng hạn như find-sh, sẽ được sử dụng để báo cáo lỗi từ bên trong vỏ được sinh ra. Ngoài ra, điều này cho phép sử dụng các cấu trúc như "$@"khi chuyển nhiều tệp vào trình bao, trong khi bỏ qua một giá trị $0có nghĩa là tệp đầu tiên được truyền sẽ được đặt thành $0và do đó không được bao gồm trong "$@".


Để chạy một lệnh shell duy nhất cho mỗi tệp, sử dụng:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

Tuy nhiên, nó thường sẽ cung cấp hiệu suất tốt hơn để xử lý các tệp trong vòng lặp shell để bạn không sinh ra một vỏ cho mỗi tệp được tìm thấy:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Lưu ý rằng for f dotương đương for f in "$@"; dovà xử lý từng tham số vị trí lần lượt, nói cách khác, nó sử dụng từng tệp được tìm thấy bởi find, bất kể ký tự đặc biệt nào trong tên của chúng.)


Ví dụ khác về findcách sử dụng đúng :

(Lưu ý: Hãy thoải mái mở rộng danh sách này.)


5
Có một trường hợp mà tôi không biết về một giải pháp thay thế cho phân tích cú pháp findđầu ra - nơi bạn cần chạy các lệnh trong trình bao hiện tại (ví dụ: vì bạn muốn đặt biến) cho mỗi tệp. Trong trường hợp này, while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)là thành ngữ tốt nhất mà tôi biết. Ghi chú: <( )không khả dụng - sử dụng bash hoặc zsh. Ngoài ra, -u33<có trong trường hợp bất cứ điều gì trong vòng lặp cố gắng đọc stdin.
Gordon Davisson

1
@GordonDavisson, có lẽ là CHUYỆN nhưng bạn cần đặt những biến đó để làm gì? Tôi cho rằng bất cứ điều gì nó được cần được xử lý bên trong các find ... -execcuộc gọi. Hoặc chỉ sử dụng shell toàn cầu, nếu nó sẽ xử lý trường hợp sử dụng của bạn.
Wildcard

1
Tôi thường muốn in một bản tóm tắt sau khi xử lý các tệp ("2 chuyển đổi, 3 bị bỏ qua, các tệp sau có lỗi: ...") và các số đếm / danh sách đó phải được tích lũy trong các biến shell. Ngoài ra, có những tình huống tôi muốn tạo ra một loạt tên tệp để tôi có thể thực hiện những việc phức tạp hơn lặp lại theo thứ tự (trong trường hợp đó là filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson

3
Câu trả lời của bạn là đúng. Tuy nhiên tôi không thích giáo điều. Mặc dù tôi biết rõ hơn, có nhiều trường hợp sử dụng (tương tác đặc biệt) trong đó an toàn và dễ dàng hơn để gõ vòng lặp trên findđầu ra hoặc thậm chí tệ hơn khi sử dụng ls. Tôi đang làm điều này hàng ngày mà không có vấn đề. Tôi biết về các tùy chọn -print0, --null, -z hoặc -0 của tất cả các loại công cụ. Nhưng tôi sẽ không lãng phí thời gian để sử dụng chúng trên dấu nhắc shell tương tác của mình trừ khi thực sự cần thiết. Điều này cũng có thể được lưu ý trong câu trả lời của bạn.
rudimeier

16
@rudimeier, cuộc tranh luận về giáo điều và thực tiễn tốt nhất đã được thực hiện cho đến chết . Không quan tâm. Nếu bạn sử dụng nó một cách tương tác và nó hoạt động tốt, tốt cho bạn, nhưng tôi sẽ không thúc đẩy làm điều đó. Tỷ lệ tác giả kịch bản muốn tìm hiểu mã mạnh là gì và sau đó chỉ làm điều đó khi viết tập lệnh sản xuất, thay vì chỉ làm bất cứ điều gì họ sử dụng để làm tương tác, là cực kỳ nhỏ. Việc xử lý là để thúc đẩy thực hành tốt nhất mọi lúc. Mọi người cần phải biết rằng có một cách chính xác để làm mọi việc.
tự đại diện

10

Câu trả lời này dành cho các tập kết quả rất lớn và chủ yếu liên quan đến hiệu suất, ví dụ như khi nhận danh sách các tệp qua mạng chậm. Đối với số lượng nhỏ tệp (ví dụ 100 hoặc thậm chí 1000 trên đĩa cục bộ), hầu hết trong số này là moot.

Song song và sử dụng bộ nhớ

Ngoài các câu trả lời khác được đưa ra, liên quan đến các vấn đề tách và như vậy, có một vấn đề khác với

for file in `find . -type f -name ...`; do smth with ${file}; done

Phần bên trong backticks phải được đánh giá đầy đủ trước, trước khi được phân chia trên các ngắt dòng. Điều này có nghĩa là, nếu bạn nhận được một số lượng lớn tệp, nó có thể bị nghẹt ở bất kỳ giới hạn kích thước nào có trong các thành phần khác nhau; bạn có thể hết bộ nhớ nếu không có giới hạn; và trong mọi trường hợp, bạn phải đợi cho đến khi toàn bộ danh sách được xuất ra findvà sau đó được phân tích cú pháp fortrước khi chạy đầu tiên smth.

Cách unix ưa thích là làm việc với các đường ống vốn đang chạy song song và cũng không cần bộ đệm lớn tùy tiện nói chung. Điều đó có nghĩa là: bạn rất thích findchạy song song với bạn smthvà chỉ giữ tên tệp hiện tại trong RAM trong khi nó xử lý smth.

Một ít nhất một phần giải pháp OKish cho điều đó là đã nói ở trên find -exec smth. Nó loại bỏ sự cần thiết phải giữ tất cả các tên tệp trong bộ nhớ và chạy song song độc đáo. Thật không may, nó cũng bắt đầu một smthquá trình cho mỗi tập tin. Nếu smthchỉ có thể hoạt động trên một tệp, thì đó là cách nó phải như vậy.

Nếu có thể, giải pháp tối ưu sẽ là find -print0 | smth, với smthkhả năng xử lý tên tệp trên STDIN của nó. Sau đó, bạn chỉ có một smthquy trình cho dù có bao nhiêu tệp và bạn chỉ cần đệm một lượng nhỏ byte (bất kỳ bộ đệm ống nội tại nào đang diễn ra) giữa hai quy trình. Tất nhiên, điều này khá phi thực tế nếu smthlà một lệnh Unix / POSIX tiêu chuẩn, nhưng có thể là một cách tiếp cận nếu bạn tự viết nó.

Nếu điều đó là không thể, thì find -print0 | xargs -0 smth, rất có thể, là một trong những giải pháp tốt hơn. Như @ dave_thndry_085 đã đề cập trong các nhận xét, xargssẽ phân chia các đối số qua nhiều lần chạy smthkhi đạt đến giới hạn hệ thống (theo mặc định, trong phạm vi 128 KB hoặc bất kỳ giới hạn nào được áp đặt exectrên hệ thống) và có các tùy chọn ảnh hưởng đến bao nhiêu các tệp được trao cho một cuộc gọi smth, do đó tìm được sự cân bằng giữa số lượng smthquá trình và độ trễ ban đầu.

EDIT: loại bỏ các khái niệm "tốt nhất" - thật khó để nói liệu một cái gì đó tốt hơn sẽ mọc lên. ;)


find ... -exec smth {} +là giải pháp.
tự đại diện

find -print0 | xargs smthhoàn toàn không hoạt động, nhưng find -print0 | xargs -0 smth(lưu ý -0) hoặc find | xargs smthnếu tên tệp không có dấu ngoặc kép hoặc dấu gạch chéo ngược chạy một tên smthcó càng nhiều tên tệp có sẵn và phù hợp trong một danh sách đối số ; nếu bạn vượt quá mức tối đa, nó sẽ chạy smthnhiều lần nếu cần để xử lý tất cả các đối số đã cho (không giới hạn). Bạn có thể đặt các "khối" nhỏ hơn (do đó có phần song song sớm hơn) với -L/--max-lines -n/--max-args -s/--max-chars.
dave_thndry_085


4

Một lý do là khoảng trắng ném cờ lê trong công trình, làm cho tệp 'foo bar' được đánh giá là 'foo' và 'bar'.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Hoạt động tốt nếu -exec được sử dụng thay thế

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$

Đặc biệt trong trường hợp findvì có một tùy chọn để thực thi một lệnh trên mỗi tệp, nó dễ dàng là tùy chọn tốt nhất.
Centimane

1
Cũng xem xét -exec ... {} \;so-exec ... {} +
thrig

1
nếu bạn sử dụng for file in "$(find . -type f)" echo "${file}"sau đó nó hoạt động ngay cả với khoảng trắng, các ký tự đặc biệt khác tôi đoán sẽ gây ra nhiều rắc rối hơn
mazs

9
@mazs - không, trích dẫn không làm những gì bạn nghĩ. Trong một thư mục có một vài tệp, hãy thử for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";done(theo bạn) in từng tên tệp trên một dòng riêng trước name:. Nó không.
don_crissti 7/11/2016

2

Bởi vì đầu ra của bất kỳ lệnh nào là một chuỗi đơn, nhưng vòng lặp của bạn cần một chuỗi các chuỗi để lặp lại. Lý do nó "hoạt động" là các vỏ sò phân chia chuỗi một cách phản bội trên khoảng trắng cho bạn.

Thứ hai, trừ khi bạn cần một tính năng cụ thể của find, hãy lưu ý rằng vỏ của bạn rất có thể đã có thể tự mở rộng một mô hình cầu đệ quy, và điều quan trọng là nó sẽ mở rộng thành một mảng thích hợp.

Ví dụ Bash:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

Tương tự ở cá:

for i in **
    echo «$i»
end

Nếu bạn cần các tính năng của find, hãy đảm bảo chỉ phân chia trên NUL (chẳng hạn như find -print0 | xargs -r0thành ngữ).

Cá có thể lặp lại đầu ra giới hạn NUL. Vì vậy, cái này thực sự không tệ:

find -print0 | while read -z i
    echo «$i»
end

Là một Gotcha cuối cùng ít, trong nhiều vỏ (không cá tất nhiên), Looping trên đầu ra lệnh sẽ làm cho cơ thể vòng một subshell (có nghĩa là bạn không thể đặt một biến trong bất kỳ cách nào mà có thể nhìn thấy sau khi vòng lặp kết thúc), đó là không bao giờ những gì bạn muốn.


@don_crissti Chính xác. Nó thường không hoạt động. Tôi đã cố gắng để mỉa mai bằng cách nói rằng nó "hoạt động" (có dấu ngoặc kép).
dùng2394284

Lưu ý rằng hình cầu đệ quy bắt nguồn từ zshđầu những năm 90 (mặc dù bạn cần **/*ở đó). fishgiống như các triển khai trước đó của tính năng tương đương của bash theo các liên kết tượng trưng khi hạ xuống cây thư mục. Xem Kết quả của ls *, ls ** và ls *** để biết sự khác biệt giữa các lần triển khai.
Stéphane Chazelas

1

Vòng lặp đầu ra của tìm kiếm không phải là thực tiễn xấu. Thực tế xấu (trong tình huống này & tất cả các tình huống) đang cho rằng đầu vào của bạn là một định dạng cụ thể thay vì biết (kiểm tra & xác nhận) đó là một định dạng cụ thể.

tldr / cbf: find | parallel stuff

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.