Nắm bắt đầu ra của tìm kiếm. -print0 vào một mảng bash


76

Sử dụng find . -print0dường như là cách an toàn duy nhất để có được danh sách các tệp trong bash do khả năng tên tệp chứa khoảng trắng, dòng mới, dấu ngoặc kép, v.v.

Tuy nhiên, tôi đang gặp khó khăn trong việc thực sự làm cho đầu ra của find hữu ích trong bash hoặc với các tiện ích dòng lệnh khác. Cách duy nhất tôi đã quản lý để sử dụng đầu ra là chuyển nó thành perl và thay đổi IFS của perl thành null:

find . -print0 | perl -e '$/="\0"; @files=<>; print $#files;'

Ví dụ này in số lượng tệp được tìm thấy, tránh nguy cơ dòng mới trong tên tệp làm hỏng số lượng, như sẽ xảy ra với:

find . | wc -l

Vì hầu hết các chương trình dòng lệnh không hỗ trợ đầu vào được phân tách bằng null, tôi nghĩ điều tốt nhất là nắm bắt đầu ra của find . -print0 một mảng bash, giống như tôi đã thực hiện trong đoạn mã perl ở trên, sau đó tiếp tục với tác vụ, bất cứ điều gì có thể là.

Tôi có thể làm cái này như thế nào?

Điều này không hoạt động:

find . -print0 | ( IFS=$'\0' ; array=( $( cat ) ) ; echo ${#array[@]} )

Một câu hỏi tổng quát hơn có thể là: Làm cách nào tôi có thể làm những việc hữu ích với danh sách tệp trong bash?


Bạn có ý nghĩa gì khi làm những việc hữu ích?
Balázs Pozsár

4
Ồ, bạn biết đấy, mảng những thứ thông thường rất hữu ích cho việc: tìm ra kích thước của chúng; lặp lại nội dung của chúng; in ngược chúng ra; phân loại chúng. Đó là một cách nghĩ. Có rất nhiều tiện ích trong unix để thực hiện những việc này với dữ liệu: wc, bash's for-vòng, tac và sort tương ứng; nhưng tất cả những điều này dường như vô dụng khi xử lý các danh sách có thể có khoảng trắng hoặc dòng mới trong đó. Tức là tên tệp. Piping dữ liệu xung quanh với các dấu phân tách trường-đầu vào có giá trị null dường như là giải pháp, nhưng rất ít tiện ích có thể xử lý điều này.
Idris

1
Đây là một bài tiểu luận về cách xử lý đúng cách tên tệp trong shell, với rất nhiều chi tiết cụ thể: http://www.dwheeler.com/essays/filenames-in-shell.html
David A. Wheeler

Câu trả lời:


103

Bị đánh cắp một cách đáng xấu hổ từ BashFAQ của Greg :

unset a i
while IFS= read -r -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done < <(find /tmp -type f -print0)

Lưu ý rằng cấu trúc chuyển hướng được sử dụng ở đây ( cmd1 < <(cmd2)) tương tự, nhưng không hoàn toàn giống với đường ống thông thường hơn ( cmd2 | cmd1) - nếu các lệnh là nội trang vỏ (ví dụ while), phiên bản đường ống thực thi chúng trong các trang con và bất kỳ biến nào mà chúng đặt (ví dụ: mảng a) bị mất khi chúng thoát ra. cmd1 < <(cmd2)chỉ chạy cmd2 trong một vỏ con, vì vậy mảng tồn tại trong quá trình xây dựng của nó. Cảnh báo: hình thức chuyển hướng này chỉ khả dụng trong bash, thậm chí không bash trong chế độ sh-giả lập; bạn phải bắt đầu tập lệnh của mình với #!/bin/bash.

Ngoài ra, vì bước xử lý tệp (trong trường hợp này, chỉ a[i++]="$file", nhưng bạn có thể muốn làm điều gì đó huyền ảo hơn trực tiếp trong vòng lặp) bị chuyển hướng đầu vào, nó không thể sử dụng bất kỳ lệnh nào có thể đọc từ stdin. Để tránh hạn chế này, tôi có xu hướng sử dụng:

unset a i
while IFS= read -r -u3 -d $'\0' file; do
    a[i++]="$file"        # or however you want to process each file
done 3< <(find /tmp -type f -print0)

... chuyển danh sách tệp qua đơn vị 3, thay vì stdin.


Ahhh gần như vậy ... đây là câu trả lời tốt nhất được nêu ra. Tuy nhiên, tôi vừa thử nó trên thư mục chứa tệp có tên dòng mới và khi kiểm tra phần tử đó bằng cách sử dụng echo $ {a [1]}, dòng mới dường như đã trở thành khoảng trắng (0x20). Bất kỳ ý tưởng tại sao điều này đang xảy ra?
Idris

Bạn đang chạy phiên bản bash nào? Tôi đã gặp sự cố với các phiên bản cũ hơn (tiếc là tôi không nhớ chính xác phiên bản nào) không xử lý các dòng mới và xóa ( \177) trong chuỗi. IIRC, thậm chí x = "$ y" không phải lúc nào cũng hoạt động đúng với các ký tự này. Tôi vừa thử nghiệm với bash 2.05b.0 và 3.2.17 (phiên bản cũ nhất và mới nhất mà tôi có); cả hai đều xử lý dòng mới đúng cách, nhưng v2.05b.0 ăn ký tự xóa.
Gordon Davisson, 14/07/09

Tôi đã thử nó trên 3.2.17 trên osx, 3.2.39 trên linux và 3.2.48 trên netBSD; tất cả biến dòng mới thành không gian.
Idris 14/07/09

12
-d ''tương đương với -d $'\0'.
l0b0

15
Một cách dễ dàng hơn để thêm một phần tử vào cuối của một mảng là:arr+=("$file")
dogbane

7

Có thể bạn đang tìm kiếm xargs:

find . -print0 | xargs -r0 do_something_useful

Tùy chọn -L 1 cũng có thể hữu ích cho bạn, điều này làm cho xargs execute do_something_useful chỉ với 1 đối số tệp.


3
Đây không phải là những gì tôi đang theo đuổi, vì không có cơ hội để làm những việc giống như mảng với danh sách, chẳng hạn như sắp xếp: bạn phải sử dụng từng phần tử khi và nó xuất hiện ngoài lệnh find. Nếu bạn có thể giải thích rõ hơn về ví dụ này, với phần "do_something_useful" là một hoạt động đẩy mảng bash, thì đây có thể là điều tôi đang theo đuổi.
Idris

6

Kể từ Bash 4.4, nội trang mapfilecó công -dtắc (để chỉ định dấu phân cách, tương tự như công -dtắc của readcâu lệnh) và dấu phân cách có thể là byte rỗng. Do đó, một câu trả lời hay cho câu hỏi trong tiêu đề

Ghi lại kết quả đầu ra của find . -print0một mảng bash

Là:

mapfile -d '' ary < <(find . -print0)

5

Vấn đề chính là dấu phân tách NUL (\ 0) ở đây là vô dụng, vì không thể gán giá trị NUL cho IFS. Vì vậy, với tư cách là những lập trình viên giỏi, chúng tôi lưu ý rằng đầu vào cho chương trình của chúng tôi là thứ mà nó có thể xử lý.

Đầu tiên, chúng tôi tạo một chương trình nhỏ, thực hiện phần này cho chúng tôi:

#!/bin/bash
printf "%s" "$@" | base64

... và gọi nó là base64str (đừng quên chmod + x)

Thứ hai, bây giờ chúng ta có thể sử dụng một vòng lặp for đơn giản và dễ hiểu:

for i in `find -type f -exec base64str '{}' \;`
do 
  file="`echo -n "$i" | base64 -d`"
  # do something with file
done

Vì vậy, mẹo ở đây là một chuỗi base64 không có dấu hiệu gây rắc rối cho bash - tất nhiên một xxd hoặc một cái gì đó tương tự cũng có thể thực hiện công việc này.


1
Người ta phải đảm bảo rằng phần của hệ thống tệp tìm thấy đang xử lý không thay đổi từ khi tìm thấy được gọi cho đến khi tập lệnh hoàn tất. Nếu không đúng như vậy, một điều kiện đua sẽ dẫn đến kết quả, có thể bị lợi dụng để gọi các lệnh trên các tệp sai. Ví dụ: một thư mục sẽ bị xóa (nói / tmp / junk) có thể được thay thế bằng một liên kết tượng trưng đến / home bởi một người dùng chưa được xác nhận. Nếu lệnh find đang chạy dưới dạng root và nó là find -type d -exec rm -rf '{}' \;, thì điều này sẽ xóa tất cả các thư mục chính của người dùng.
Demi

2
read -r -d ''sẽ đọc mọi thứ cho đến NUL tiếp theo vào "$REPLY". Không cần quan tâm đến IFS.
Charles Duffy,

4

Tuy nhiên, một cách khác để đếm tệp:

find /DIR -type f -print0 | tr -dc '\0' | wc -c 

2

Bạn có thể đếm một cách an toàn với điều này:

find . -exec echo ';' | wc -l

(Nó in một dòng mới cho mọi tệp / dir được tìm thấy, và sau đó đếm các dòng mới được in ra ...)


Nó là nhanh hơn nhiều để sử dụng các -printftùy chọn thay vì -execcho mỗi tập tin:find . -printf "\n" | wc -l
Oliver tôi

1

Tôi nghĩ rằng các giải pháp thanh lịch hơn tồn tại, nhưng tôi sẽ đưa cái này vào. Điều này cũng sẽ hoạt động đối với tên tệp có khoảng trắng và / hoặc dòng mới:

i=0;
for f in *; do
  array[$i]="$f"
  ((i++))
done

Sau đó, bạn có thể liệt kê từng tệp một (trong trường hợp này là theo thứ tự ngược lại):

for ((i = $i - 1; i >= 0; i--)); do
  ls -al "${array[$i]}"
done

Trang này đưa ra một ví dụ hay, và để biết thêm thông tin chi tiết, hãy xem Chương 26 trong Hướng dẫn Viết mã Bash Nâng cao .


Đây (và các ví dụ tương tự khác bên dưới) gần như là những gì tôi đang theo đuổi - nhưng với một vấn đề lớn: nó chỉ hoạt động cho các phần của thư mục hiện tại. Tôi muốn có thể điều khiển danh sách tệp hoàn toàn tùy ý; đầu ra của "find" chẳng hạn, liệt kê các thư mục một cách đệ quy hoặc bất kỳ danh sách nào khác. Điều gì sẽ xảy ra nếu danh sách của tôi là: (/tmp/foo.jpg | /home/alice/bar.jpg | / home / bob / my holiday / baz.jpg | /tmp/new\nline/grault.jpg) hoặc bất kỳ điều gì khác danh sách tệp hoàn toàn tùy ý (tất nhiên, có thể có khoảng trắng và dòng mới trong đó)?
Idris

1

Tránh xargs nếu bạn có thể:

man ruby | less -p 777 
IFS=$'\777' 
#array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '{}' \; 2>/dev/null) ) 
array=( $(find ~ -maxdepth 1 -type f -exec printf "%s\777" '{}' + 2>/dev/null) ) 
echo ${#array[@]} 
printf "%s\n" "${array[@]}" | nl 
echo "${array[0]}" 
IFS=$' \t\n' 

Tại sao bạn đặt IFS thành \777?
sschober

1

Tôi là người mới nhưng tôi tin rằng đây là một câu trả lời; hy vọng nó sẽ giúp ai đó:

STYLE="$HOME/.fluxbox/styles/"

declare -a array1

LISTING=`find $HOME/.fluxbox/styles/ -print0 -maxdepth 1 -type f`


echo $LISTING
array1=( `echo $LISTING`)
TAR_SOURCE=`echo ${array1[@]}`

#tar czvf ~/FluxieStyles.tgz $TAR_SOURCE

0

Điều này tương tự như phiên bản của Stephan202, nhưng các tệp (và thư mục) được đưa vào một mảng cùng một lúc. Các forvòng lặp ở đây là chỉ để "làm những việc có ích":

files=(*)                        # put files in current directory into an array
i=0
for file in "${files[@]}"
do
    echo "File ${i}: ${file}"    # do something useful 
    let i++
done

Để đếm:

echo ${#files[@]}

0

Câu hỏi cũ, nhưng không ai gợi ý phương pháp đơn giản này, vì vậy tôi nghĩ tôi sẽ làm. Được cấp nếu tên tệp của bạn có ETX, điều này không giải quyết được vấn đề của bạn, nhưng tôi nghi ngờ nó phục vụ cho mọi tình huống trong thế giới thực. Việc cố gắng sử dụng null dường như sẽ phạm phải các quy tắc xử lý IFS mặc định. Gia vị theo sở thích của bạn với các tùy chọn tìm và xử lý lỗi.

savedFS="$IFS"
IFS=$'\x3'
filenames=(`find wherever -printf %p$'\x3'`)
IFS="$savedFS"

1
ETX có nghĩa là gì? Có thể tên tệp là đoạn kết thúc EXT hoặc có thể là phần cuối của văn bản ...
oHo

0

Câu trả lời của Gordon Davisson là rất tốt cho bash. Tuy nhiên, có một phím tắt hữu ích cho người dùng zsh:

Đầu tiên, hãy đặt bạn chuỗi vào một biến:

A="$(find /tmp -type f -print0)"

Tiếp theo, tách biến này và lưu trữ nó trong một mảng:

B=( ${(s/^@/)A} )

Có một mẹo nhỏ: ^@là ký tự NUL. Để làm điều đó, bạn phải gõ Ctrl + V sau đó là Ctrl + @.

Bạn có thể kiểm tra mỗi mục nhập $ B có chứa đúng giá trị không:

for i in "$B[@]"; echo \"$i\"

Người đọc cẩn thận có thể nhận thấy rằng lệnh gọi findlệnh có thể tránh được trong hầu hết các trường hợp sử dụng **cú pháp. Ví dụ:

B=( /tmp/** )

-1

Bash chưa bao giờ giỏi xử lý tên tệp (hoặc bất kỳ văn bản nào thực sự) vì nó sử dụng khoảng trắng làm dấu phân cách danh sách.

Thay vào đó, tôi khuyên bạn nên sử dụng python với thư viện sh .

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.