Làm thế nào để lặp qua tên tập tin được trả về bởi find?


223
x=$(find . -name "*.txt")
echo $x

nếu tôi chạy đoạn mã trên trong shell Bash, cái tôi nhận được là một chuỗi chứa một vài tên tệp được phân tách bằng khoảng trống, không phải là danh sách.

Tất nhiên, tôi có thể phân tách chúng thêm bằng cách để trống một danh sách, nhưng tôi chắc chắn có một cách tốt hơn để làm điều đó.

Vì vậy, cách tốt nhất để lặp qua các kết quả của một findlệnh là gì?


3
Cách tốt nhất để lặp qua tên tệp phụ thuộc khá nhiều vào những gì bạn thực sự muốn làm với nó, nhưng trừ khi bạn có thể đảm bảo không có tệp nào có bất kỳ khoảng trắng nào trong tên của họ, đây không phải là cách hay để làm điều đó. Vì vậy, bạn muốn làm gì trong vòng lặp trên các tập tin?
Kevin

1
Về tiền thưởng : ý tưởng chính ở đây là để có được một câu trả lời kinh điển bao gồm tất cả các trường hợp có thể xảy ra (tên tệp với các dòng mới, các ký tự có vấn đề ...). Ý tưởng là sau đó sử dụng các tên tệp này để thực hiện một số nội dung (gọi lệnh khác, thực hiện một số đổi tên ...). Cảm ơn!
fedorqui 'SO ngừng làm hại'

Đừng quên rằng một tệp hoặc tên thư mục có thể chứa ".txt" theo sau là khoảng trắng và một chuỗi khác, ví dụ "
Something.txt Something

Sử dụng mảng, không phải var x=( $(find . -name "*.txt") ); echo "${x[@]}"Sau đó, bạn có thể lặp quafor item in "${x[@]}"; { echo "$item"; }
Ivan

Câu trả lời:


391

TL; DR: Nếu bạn chỉ ở đây để có câu trả lời đúng nhất, có lẽ bạn muốn sở thích cá nhân của tôi, find . -name '*.txt' -exec process {} \;(xem phần dưới của bài đăng này). Nếu bạn có thời gian, hãy đọc qua phần còn lại để xem một số cách khác nhau và các vấn đề với hầu hết chúng.


Câu trả lời đầy đủ:

Cách tốt nhất phụ thuộc vào những gì bạn muốn làm, nhưng đây là một vài lựa chọn. Miễn là không có tệp hoặc thư mục nào trong cây con có khoảng trắng trong tên của nó, bạn chỉ có thể lặp qua các tệp:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Tốt hơn một chút, cắt bỏ biến tạm thời x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

Nó là tốt hơn nhiều để toàn cầu khi bạn có thể. Không gian trắng an toàn cho các tệp trong thư mục hiện tại:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Bằng cách bật globstartùy chọn, bạn có thể toàn cầu hóa tất cả các tệp phù hợp trong thư mục này và tất cả các thư mục con:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

Trong một số trường hợp, ví dụ: nếu tên tệp đã có trong một tệp, bạn có thể cần phải sử dụng read:

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readcó thể được sử dụng an toàn kết hợp với findbằng cách đặt dấu phân cách phù hợp:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Đối với các tìm kiếm phức tạp hơn, có thể bạn sẽ muốn sử dụng find, với -exectùy chọn của nó hoặc với -print0 | xargs -0:

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findcũng có thể cd vào thư mục của mỗi tệp trước khi chạy lệnh bằng cách sử dụng -execdirthay vì -execvà có thể được thực hiện tương tác (nhắc trước khi chạy lệnh cho mỗi tệp) bằng cách sử dụng -okthay vì -exec(hoặc -okdirthay vì -execdir).

*: Về mặt kỹ thuật, cả hai findxargs(theo mặc định) sẽ chạy lệnh với càng nhiều đối số càng tốt trên dòng lệnh, bao nhiêu lần để vượt qua tất cả các tệp. Trong thực tế, trừ khi bạn có số lượng tệp rất lớn, điều đó sẽ không thành vấn đề và nếu bạn vượt quá độ dài nhưng cần tất cả chúng trên cùng một dòng lệnh, thì bạn sẽ tìm thấy một cách khác.


4
Nó đáng chú ý là trong trường hợp với done < filenamevà một sau với ống stdin không thể sử dụng được nữa (→ không có công cụ tương tác nhiều hơn bên trong vòng lặp), nhưng trong trường hợp cần thiết có thể sử dụng 3<thay vì <và thêm <&3hoặc -u3để các readphần, về cơ bản cách sử dụng một bộ mô tả tập tin riêng biệt. Ngoài ra, tôi tin read -d ''là giống như read -d $'\0'nhưng tôi không thể tìm thấy bất kỳ tài liệu chính thức nào về điều đó ngay bây giờ.
phk

1
cho tôi trong * .txt; không hoạt động, nếu không có tập tin phù hợp. Một thử nghiệm xtra, ví dụ [[-e $ i]] là cần thiết
Michael Brux

2
Tôi bị lạc với phần này: -exec process {} \;và tôi đoán đó là một câu hỏi hoàn toàn khác - điều đó có nghĩa là gì và làm thế nào để tôi thao tác nó? Q / A hoặc doc tốt ở đâu. trên đó?
Hội trường Alex

1
@AlexHall bạn luôn có thể xem các trang man ( man find). Trong trường hợp này, -execyêu findcầu thực thi lệnh sau, được chấm dứt bởi ;(hoặc +), trong đó {}sẽ được thay thế bằng tên của tệp mà nó đang xử lý (hoặc, nếu +được sử dụng, tất cả các tệp đã thực hiện theo điều kiện đó).
Kevin

3
@phk -d ''thì tốt hơn -d $'\0'. Cái sau không chỉ dài hơn mà còn gợi ý rằng bạn có thể truyền các đối số chứa byte rỗng, nhưng bạn không thể. Byte null đầu tiên đánh dấu sự kết thúc của chuỗi. Trong bash $'a\0bc'là giống a$'\0'giống như $'\0abc'hoặc chỉ là chuỗi rỗng ''. help readnói rằng " Ký tự đầu tiên của delim được sử dụng để chấm dứt đầu vào " vì vậy sử dụng ''như một dấu phân cách là một chút hack. Ký tự đầu tiên trong chuỗi trống là byte rỗng luôn đánh dấu phần cuối của chuỗi (ngay cả khi bạn không viết rõ ràng xuống).
Socowi

114

Dù bạn làm gì, đừng sử dụng forvòng lặp :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Ba lý do:

  • Để vòng lặp for bắt đầu, findphải chạy đến khi hoàn thành.
  • Nếu một tên tệp có bất kỳ khoảng trắng (bao gồm khoảng trắng, tab hoặc dòng mới) trong đó, nó sẽ được coi là hai tên riêng biệt.
  • Mặc dù bây giờ không thể, bạn có thể vượt qua bộ đệm dòng lệnh của bạn. Hãy tưởng tượng nếu bộ đệm dòng lệnh của bạn giữ 32KB và forvòng lặp của bạn trả về 40KB văn bản. 8KB cuối cùng đó sẽ bị loại bỏ khỏi forvòng lặp của bạn và bạn sẽ không bao giờ biết điều đó.

Luôn sử dụng while readcấu trúc:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

Vòng lặp sẽ thực thi trong khi findlệnh đang thực thi. Thêm vào đó, lệnh này sẽ hoạt động ngay cả khi tên tệp được trả về với khoảng trắng trong đó. Và, bạn sẽ không tràn bộ đệm dòng lệnh của bạn.

Các -print0sẽ sử dụng NULL như một tách tập tin thay vì một dòng mới và -d $'\0'sẽ sử dụng NULL như tách trong khi đọc.


3
Nó sẽ không hoạt động với các dòng mới trong tên tệp. Sử dụng find -execthay thế.
người dùng không xác định

2
@userunknown - Bạn nói đúng về điều đó. -execlà an toàn nhất vì nó hoàn toàn không sử dụng vỏ. Tuy nhiên, NL trong tên tập tin là khá hiếm. Dấu cách trong tên tệp là khá phổ biến. Điểm chính là không sử dụng một forvòng lặp mà nhiều áp phích khuyến nghị.
David W.

1
@userunknown - Đây. Tôi đã sửa lỗi này, vì vậy giờ đây nó sẽ chăm sóc các tệp với các dòng, tab mới và bất kỳ khoảng trắng nào khác. Toàn bộ quan điểm của bài viết là nói với OP không sử dụng for file $(find)vì những vấn đề liên quan đến điều đó.
David W.

4
Nếu bạn có thể sử dụng -exec thì tốt hơn, nhưng có những lúc bạn thực sự cần cái tên được trả lại cho vỏ. Ví dụ, nếu bạn muốn loại bỏ phần mở rộng tập tin.
Ben Reser

5
Bạn nên sử dụng -rtùy chọn để read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood

102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Lưu ý: phương pháp này phương thức (thứ hai) được hiển thị bởi bmargulies là an toàn để sử dụng với khoảng trắng trong tên tệp / thư mục.

Để có trường hợp - hơi kỳ lạ - của các dòng mới trong tên tệp / thư mục được bảo hiểm, bạn sẽ phải sử dụng đến -execvị từ findgiống như sau:

find . -name '*.txt' -exec echo "{}" \;

Các {} là giữ chỗ cho mục tìm thấy và \;được sử dụng để chấm dứt -execvị.

Và để hoàn thiện, hãy để tôi thêm một biến thể khác - bạn phải yêu thích các cách * nix vì tính linh hoạt của chúng:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

Điều này sẽ phân tách các mục được in bằng một \0ký tự không được phép trong bất kỳ hệ thống tệp nào trong tên tệp hoặc thư mục, theo hiểu biết của tôi và do đó sẽ bao gồm tất cả các cơ sở. xargsnhặt chúng lên từng cái một ...


3
Thất bại nếu dòng mới trong tên tệp.
người dùng không xác định

2
@user không rõ: bạn nói đúng, đó là trường hợp tôi chưa từng xem xét và tôi nghĩ điều đó rất kỳ lạ. Nhưng tôi đã điều chỉnh câu trả lời của mình cho phù hợp.
0xC0000022L

5
Có lẽ đáng để chỉ ra rằng find -print0xargs -0cả hai phần mở rộng GNU và không phải là đối số di động (POSIX). Mặc dù rất hữu ích trên các hệ thống có chúng!
Toby Speight

1
Điều này cũng thất bại với tên tệp chứa dấu gạch chéo ngược ( read -rsẽ sửa) hoặc tên tệp kết thúc bằng khoảng trắng ( IFS= readsẽ sửa). Do đó BashFAQ # 1 gợi ýwhile IFS= read -r filename; do ...
Charles Duffy

1
Một vấn đề khác với điều này là có vẻ như phần thân của vòng lặp đang thực thi trong cùng một lớp vỏ, nhưng không phải vậy, ví dụ như vậy exitsẽ không hoạt động như mong đợi và các biến được đặt trong thân vòng lặp sẽ không khả dụng sau vòng lặp.
EM0

17

Tên tập tin có thể bao gồm không gian và thậm chí các nhân vật điều khiển. Dấu cách là dấu phân cách (mặc định) để mở rộng shell trong bash và kết quả là x=$(find . -name "*.txt")từ câu hỏi không được khuyến nghị. Nếu find nhận được một tên tệp có khoảng trắng, ví dụ: "the file.txt"bạn sẽ nhận được 2 chuỗi riêng biệt để xử lý, nếu bạn xử lý xtrong một vòng lặp. Bạn có thể cải thiện điều này bằng cách thay đổi dấu phân cách ( IFSbiến bash ) \r\n, nhưng tên tệp có thể bao gồm các ký tự điều khiển - vì vậy đây không phải là phương pháp an toàn (hoàn toàn).

Theo quan điểm của tôi, có 2 mẫu được đề xuất (và an toàn) để xử lý tệp:

1. Sử dụng để mở rộng vòng lặp & tên tệp:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Sử dụng thay thế find-read-while & process

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Nhận xét

trên Mẫu 1:

  1. bash trả về mẫu tìm kiếm ("* .txt") nếu không tìm thấy tệp phù hợp - vì vậy dòng bổ sung "tiếp tục, nếu tệp không tồn tại" là cần thiết. xem hướng dẫn Bash, mở rộng tên tệp
  2. tùy chọn vỏ nullglob có thể được sử dụng để tránh dòng thêm này.
  3. "Nếu failglob tùy chọn shell được đặt và không tìm thấy kết quả khớp, thông báo lỗi sẽ được in và lệnh không được thực thi." (từ Sổ tay Bash ở trên)
  4. tùy chọn shell globstar: "Nếu được đặt, mẫu '**' được sử dụng trong ngữ cảnh mở rộng tên tệp sẽ khớp với tất cả các tệp và không hoặc nhiều thư mục và thư mục con. Nếu mẫu được theo sau bởi '/', chỉ các thư mục và thư mục con phù hợp." xem hướng dẫn sử dụng Bash, nội dung Shopt
  5. lựa chọn khác cho việc mở rộng tên tập tin: extglob, nocaseglob, dotglobvà biến vỏGLOBIGNORE

trên Mẫu 2:

  1. tên tập tin có thể chứa khoảng trống, tab, khoảng trống, dòng mới, ... để tên tập tin quá trình một cách an toàn, findvới -print0được sử dụng: tên tập tin được in với tất cả các ký tự điều khiển & chấm dứt với NUL. xem thêm Trang chủ Gnu Findutils, Xử lý tên tệp không an toàn , Xử lý tên tệp an toàn , các ký tự bất thường trong tên tệp . Xem David A. Wheeler dưới đây để thảo luận chi tiết về chủ đề này.

  2. Có một số mẫu có thể để xử lý tìm kết quả trong một vòng lặp while. Những người khác (kevin, David W.) đã chỉ ra cách thực hiện việc này bằng cách sử dụng đường ống:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Khi bạn thử đoạn mã này, bạn sẽ thấy rằng nó không hoạt động: files_foundluôn luôn là "đúng" và mã sẽ luôn lặp lại "không tìm thấy tệp". Lý do là: mỗi lệnh của một đường ống được thực thi trong một lớp con riêng biệt, vì vậy biến đã thay đổi bên trong vòng lặp (lớp con riêng biệt) không thay đổi biến trong tập lệnh shell chính. Đây là lý do tại sao tôi khuyên bạn nên sử dụng thay thế quy trình như là mô hình "tốt hơn", hữu ích hơn, tổng quát hơn.
    Xem tôi đặt các biến trong một vòng lặp trong một đường ống. Tại sao chúng biến mất ... (từ Câu hỏi thường gặp về Bash của Greg) cho một cuộc thảo luận chi tiết về chủ đề này.

Tài liệu tham khảo & nguồn bổ sung:


8

(Được cập nhật để bao gồm cải thiện tốc độ thực thi của @ Socowi)

Với bất kỳ $SHELLhỗ trợ nào (dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Làm xong.


Câu trả lời gốc (ngắn hơn, nhưng chậm hơn):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

1
Chậm như mật đường (vì nó khởi chạy một vỏ cho mỗi tệp) nhưng điều này không hoạt động. +1
dawg

1
Thay vì \;bạn có thể sử dụng +để chuyển nhiều tệp như sở hữu cho một exec. Sau đó sử dụng "$@"bên trong tập lệnh shell để xử lý tất cả các tham số này.
Socowi

3
Có một lỗi trong mã này. Vòng lặp bị thiếu kết quả đầu tiên. Đó là bởi vì $@bỏ qua nó vì nó thường là tên của kịch bản. Chúng ta chỉ cần thêm dummyvào giữa '{}để nó có thể thay thế tên tập lệnh, đảm bảo tất cả các kết quả khớp được xử lý bởi vòng lặp.
BCartolo

Nếu tôi cần các biến khác từ bên ngoài shell mới được tạo thì sao?
Jodo

OTHERVAR=foo find . -na.....sẽ cho phép bạn truy cập $OTHERVARtừ bên trong lớp vỏ mới được tạo.
dùng569825

6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one

3
for x in $(find ...)sẽ phá vỡ cho bất kỳ tên tệp có khoảng trắng trong đó. Tương tự với find ... | xargstrừ khi bạn sử dụng -print0-0
glenn jackman

1
Sử dụng find . -name "*.txt -exec process_one {} ";"thay thế. Tại sao chúng ta nên sử dụng xargs để thu thập kết quả, chúng ta đã có?
người dùng không xác định

@userunknown Vâng tất cả phụ thuộc vào những gì process_one. Nếu đó là một trình giữ chỗ cho một lệnh thực tế , chắc chắn rằng nó sẽ hoạt động (nếu bạn sửa lỗi chính tả và thêm dấu ngoặc kép sau "*.txt). Nhưng nếu process_onelà một hàm do người dùng định nghĩa, mã của bạn sẽ không hoạt động.
độc tố

@toxalot: Có, nhưng sẽ không thành vấn đề khi viết hàm trong tập lệnh cần gọi.
người dùng không xác định

4

Bạn có thể lưu trữ findđầu ra của mình trong mảng nếu bạn muốn sử dụng đầu ra sau như:

array=($(find . -name "*.txt"))

Bây giờ để in từng phần tử trong dòng mới, bạn có thể sử dụng forvòng lặp lặp cho tất cả các phần tử của mảng hoặc bạn có thể sử dụng câu lệnh printf.

for i in ${array[@]};do echo $i; done

hoặc là

printf '%s\n' "${array[@]}"

Bạn cũng có thể dùng:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Điều này sẽ in từng tên tệp trong dòng mới

Để chỉ in findđầu ra ở dạng danh sách, bạn có thể sử dụng một trong các cách sau:

find . -name "*.txt" -print 2>/dev/null

hoặc là

find . -name "*.txt" -print | grep -v 'Permission denied'

Điều này sẽ loại bỏ các thông báo lỗi và chỉ cung cấp tên tệp là đầu ra trong dòng mới.

Nếu bạn muốn làm một cái gì đó với tên tệp, lưu trữ nó trong mảng là tốt, thì không cần phải tiêu tốn dung lượng đó và bạn có thể in trực tiếp đầu ra từ đó find.


1
Vòng lặp trên mảng không thành công với khoảng trắng trong tên tệp.
EM0

Bạn nên xóa câu trả lời này. Nó không hoạt động với khoảng trắng trong tên tệp hoặc tên thư mục.
jww

4

Nếu bạn có thể giả sử tên tệp không chứa dòng mới, bạn có thể đọc đầu ra của findmột mảng Bash bằng lệnh sau:

readarray -t x < <(find . -name '*.txt')

Ghi chú:

  • -tnguyên nhân readarrayđể tước dòng mới.
  • Nó sẽ không hoạt động nếu readarraytrong một đường ống, do đó thay thế quá trình.
  • readarray có sẵn kể từ Bash 4.

Bash 4.4 trở lên cũng hỗ trợ -dtham số để chỉ định dấu phân cách. Sử dụng ký tự null, thay vì dòng mới, để phân định tên tệp cũng hoạt động trong trường hợp hiếm hoi mà tên tệp chứa dòng mới:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarraycũng có thể được gọi như mapfilevới các tùy chọn tương tự.

Tham khảo: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream


Đây là câu trả lời tốt nhất! Hoạt động với: * Dấu cách trong tên tệp * Không có tệp phù hợp * exitkhi lặp qua kết quả
EM0

Tuy nhiên, không hoạt động với tất cả các tên tệp có thể - vì vậy, bạn nên sử dụngreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy

3

Tôi thích sử dụng find được gán đầu tiên cho biến và IFS chuyển sang dòng mới như sau:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Chỉ trong trường hợp bạn muốn lặp lại nhiều hành động hơn trên cùng một bộ DATA và tìm thấy rất chậm trên máy chủ của bạn (mức sử dụng cao I / 0)


2

Bạn có thể đặt tên tệp được trả về findvào một mảng như thế này:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Bây giờ bạn có thể chỉ cần lặp qua mảng để truy cập các mục riêng lẻ và làm bất cứ điều gì bạn muốn với chúng.

Lưu ý: Đó là không gian trắng an toàn.


1
Với bash 4.4 trở lên, bạn có thể sử dụng một lệnh duy nhất thay vì vòng lặp : mapfile -t -d '' array < <(find ...). Cài đặt IFSlà không cần thiết cho mapfile.
Socowi

1

dựa trên các câu trả lời và nhận xét khác của @phk, sử dụng fd # 3:
(vẫn cho phép sử dụng stdin bên trong vòng lặp)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")

-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Điều này sẽ liệt kê các tập tin và cung cấp chi tiết về các thuộc tính.


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.