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ụ).
ksh93và bashcuố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 đề đó:
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.
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 đó.
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) ).
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.