Vấn đề
for f in $(find .)
kết hợp hai thứ không tương thích.
find
in 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 cmd
có 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ố xarg
byte 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 for
vòng lặp trên đầu ra find
là sử dụng zsh
các hỗ trợ đó IFS=$'\0'
và:
IFS=$'\0'
for f in $(find . -print0)
(thay thế -print0
với -exec printf '%s\0' {} +
cho find
hiệ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 something
có 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 stdin
là 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 -P
tùy chọn GNU xargs
để xử lý song song. Các stdin
vấn đề cũng có thể được làm việc xung quanh với GNU xargs
với -a
tù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 something
mỗi lần lấy 20 đối số tệp.
Với zsh
hoặc bash
, một cách khác để lặp qua đầu ra find -print0
là:
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.4
và ở trên cũng có thể lưu trữ các tệp được trả về bởi find -print0
trong một mảng với:
readarray -td '' files < <(find . -print0)
Các zsh
tương đương (trong đó có các lợi thế của việc bảo tồn find
củ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 find
biể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 -1
sẽ 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ụ).
ksh93
và bash
cuố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 bash
trướ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
/ zsh
tì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_NOFOLLOW
cờ 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
/ ksh
khô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 find
khô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/bar
hay cmd ./foo/bar ./foo/bar/baz
, do thời gian cmd
tận dụng ./foo/bar
, các thuộc tính của bar
khô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, ./foo
có 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 find
chờ đợi để có đủ tệp để gọi cmd
).
Một số find
triể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 -- bar
với một số triển khai, do đó --
), do đó, vấn đề với ./foo
việ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ư rm
an 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 cmd
sẽ 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 cmd
thực hiện trên đường dẫn đó đều bị ENAMETOOLONG
lỗ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 đó ENAMETOOLONG
lỗ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 find
và 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 74
sẽ côté.txt
dà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щ.txt
và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ự!
find
là một ứng dụng như vậy coi tên tệp là văn bản cho -name
/ -path
vị từ của nó (và hơn thế nữa, giống như -iname
hoặc -regex
với một số triển khai).
Điều đó có nghĩa là, ví dụ, với một số find
triể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 74
tệ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 {} \;
find
sẽ 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 dash
sẽ 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é.txt
từ trước đó.
Những người thích bash
hoặc zsh
nơ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ư sh
FreeBSD (ít nhất là 11) hoặc mksh -o utf8-mode
hỗ 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)
+cmd
là 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 $REPLY
hoặc trả về một số tệp trong một $reply
mả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.