Sử dụng danh sách tên tệp được tạo làm danh sách đối số - có khoảng trắng


16

Tôi đang cố gắng gọi một kịch bản với một danh sách các tên tệp được thu thập bởi find. Không có gì đặc biệt, chỉ đôi khi như thế này:

$ myscript `find . -name something.txt`

Vấn đề là một số tên đường dẫn chứa khoảng trắng, do đó chúng bị chia thành hai tên không hợp lệ khi mở rộng đối số. Thông thường tôi sẽ bao quanh các tên bằng dấu ngoặc kép, nhưng ở đây chúng được chèn bởi phần mở rộng backquote. Tôi đã thử lọc đầu ra findvà xung quanh mỗi tên tệp bằng dấu ngoặc kép, nhưng đến lúc bash nhìn thấy chúng, đã quá muộn để loại bỏ chúng và chúng được coi là một phần của tên tệp:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

Vâng, đó là các quy tắc về cách xử lý dòng lệnh, nhưng làm thế nào để tôi khắc phục nó?

Điều này thật xấu hổ nhưng tôi không đưa ra được phương pháp đúng đắn. Cuối cùng tôi đã tìm ra cách thực hiện nó với xargs -0 -n 10000... nhưng đó là một hack xấu xí mà tôi vẫn muốn hỏi: Làm thế nào để tôi trích dẫn kết quả của việc mở rộng backquote, hoặc đạt được hiệu quả tương tự theo cách khác?

Chỉnh sửa: Tôi đã nhầm lẫn về thực tế là xargs không thu thập tất cả các đối số vào một danh sách đối số duy nhất, trừ khi nó nói cách khác hoặc giới hạn hệ thống có thể được vượt quá. Cảm ơn mọi người đã đặt tôi thẳng! Những người khác, hãy ghi nhớ điều này khi bạn đọc câu trả lời được chấp nhận vì nó không được chỉ ra trực tiếp.

Tôi đã chấp nhận câu trả lời, nhưng câu hỏi của tôi vẫn là: Không có cách nào để bảo vệ không gian trong bản $(...)mở rộng backtick (hoặc )? (Lưu ý rằng giải pháp được chấp nhận là câu trả lời không bash).


Tôi đoán bạn cần thay đổi shell sử dụng làm dấu tách tên tệp (ví dụ: bằng cách chơi với giá trị của IFS, một cách có thể là IFS=", dòng mới, "). Nhưng có cần phải thực thi kịch bản trên tất cả các tên tệp không? Nếu không, hãy cân nhắc việc sử dụng find chính nó để thực thi tập lệnh cho mỗi tệp.
njsg

Thay đổi IFS là một ý tưởng tuyệt vời, đã không nghĩ về nó! Không thực tế để sử dụng dòng lệnh, nhưng vẫn còn. :-) Và vâng, mục tiêu là chuyển tất cả các đối số cho cùng một lời gọi của tập lệnh của tôi.
alexis

Câu trả lời:


12

Bạn có thể làm như sau bằng cách sử dụng một số triển khai findxargsnhư thế này.

$ find . -type f -print0 | xargs -r0 ./myscript

hoặc, theo tiêu chuẩn, chỉ find:

$ find . -type f -exec ./myscript {} +

Thí dụ

Nói rằng tôi có thư mục mẫu sau.

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

Bây giờ hãy nói rằng tôi có cái này cho ./myscript.

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

Bây giờ khi tôi chạy lệnh sau.

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Hoặc khi tôi sử dụng mẫu thứ 2 như vậy:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Chi tiết

tìm + xargs

Hai phương pháp trên, mặc dù nhìn khác nhau, về cơ bản là giống nhau. Đầu tiên là lấy đầu ra từ find, tách nó bằng NULLs ( \0) thông qua công -print0tắc để tìm. Nó xargs -0được thiết kế đặc biệt để lấy đầu vào được phân tách bằng NULL. Cú pháp không chuẩn đó đã được GNU giới thiệu findxargsngày nay cũng được tìm thấy trong một số khác như hầu hết các BSD gần đây. Các -rtùy chọn là cần thiết để tránh gọi myscriptnếu findphát hiện không có gì với GNU findnhưng không phải với BSDs.

LƯU Ý: Toàn bộ cách tiếp cận này dựa trên thực tế là bạn sẽ không bao giờ vượt qua một chuỗi quá dài. Nếu đúng như vậy, thì lần gọi thứ 2 ./myscriptsẽ được khởi động với phần còn lại của các kết quả tiếp theo từ tìm kiếm.

tìm với +

Đó là cách tiêu chuẩn (mặc dù nó chỉ được thêm tương đối gần đây (2005) vào việc triển khai GNU find). Khả năng thực hiện những gì chúng ta đang làm xargsđược xây dựng theo đúng nghĩa đen find. Vì vậy, findsẽ tìm một danh sách các tệp và sau đó chuyển danh sách đó càng nhiều đối số có thể phù hợp với lệnh được chỉ định sau -exec(lưu ý {}chỉ có thể tồn tại ngay trước đó +trong trường hợp này), chạy các lệnh nhiều lần nếu cần.

Tại sao không trích dẫn?

Trong ví dụ đầu tiên, chúng tôi đang sử dụng một phím tắt bằng cách tránh hoàn toàn các vấn đề với trích dẫn, bằng cách sử dụng NULL để phân tách các đối số. Khi xargsđược đưa ra danh sách này, nó được hướng dẫn phân tách trên các NULL bảo vệ hiệu quả các nguyên tử lệnh riêng lẻ của chúng ta.

Trong ví dụ thứ hai, chúng tôi sẽ giữ kết quả bên trong findvà để nó biết mỗi nguyên tử tệp là gì và sẽ đảm bảo xử lý chúng một cách thích hợp, do đó tránh được việc trích dẫn chúng.

Kích thước tối đa của dòng lệnh?

Câu hỏi này thỉnh thoảng xuất hiện để làm phần thưởng Tôi thêm nó vào câu trả lời này, chủ yếu để tôi có thể tìm thấy nó trong tương lai. Bạn có thể sử dụng xargsđể xem giới hạn của môi trường như thế nào:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072

1
Cảm ơn nhưng tôi cần chuyển tất cả các đối số cho cùng một lời gọi của tập lệnh của mình. Đó là trong phần mô tả vấn đề, nhưng tôi đoán tôi đã không nói rõ rằng nó không phải là sự cố.
alexis

@alexis - đọc lại câu trả lời, họ đang chuyển tất cả các đối số cho một lệnh gọi của tập lệnh của bạn.
slm

Tôi sẽ bị nguyền rủa! Tôi không biết về +tranh luận find(và bạn cũng sử dụng +trong văn xuôi, vì vậy tôi đã bỏ lỡ lời giải thích của bạn lần đầu tiên). Nhưng quan trọng hơn, tôi đã hiểu nhầm những gì xargslàm theo mặc định !!! Trong ba thập kỷ sử dụng Unix Tôi chưa bao giờ sử dụng nó cho đến bây giờ, nhưng tôi nghĩ rằng tôi biết hộp công cụ của mình ...
alexis

@alexis - Tôi đoán bạn đã bỏ lỡ những gì chúng tôi đang nói. Có xargslà một ác quỷ của một lệnh. Bạn phải đọc nó và findcác trang người đàn ông nhiều lần để tìm hiểu những gì họ có thể làm. Tháng năm của các thiết bị chuyển mạch là chống chỉ định lẫn nhau để làm tăng thêm sự nhầm lẫn.
slm

@alexis - cũng là một điều nữa để thêm vào hộp công cụ, không sử dụng backquotes / backticks để chạy các lệnh lồng nhau, $(..)thay vào đó hãy sử dụng ngay bây giờ. Nó tự động xử lý lồng nhau của dấu ngoặc kép, vv Backticks đang bị phản đối.
slm

3
find . -name something.txt -exec myscript {} +

Trong phần trên, findtìm tất cả các tên tệp phù hợp và cung cấp chúng làm đối số myscript. Điều này hoạt động với tên tệp bất kể khoảng trắng hoặc bất kỳ ký tự lẻ nào khác.

Nếu tất cả các tên tệp phù hợp trên một dòng, thì myscript được thực thi một lần. Nếu danh sách quá dài để shell xử lý, thì find sẽ chạy myscript nhiều lần nếu cần.

XEM THÊM: Có bao nhiêu tệp phù hợp trên một dòng lệnh? man findnói rằng findxây dựng dòng lệnh của nó "giống như cách mà xargs xây dựng nó". Và, man xargscác giới hạn phụ thuộc vào hệ thống và bạn có thể xác định chúng bằng cách chạy xargs --show-limits. ( getconf ARG_MAXcũng là một khả năng). Trên Linux, giới hạn thường (nhưng không phải luôn luôn) khoảng 2 triệu ký tự trên mỗi dòng lệnh.


2

Một vài bổ sung cho câu trả lời tốt của @ slm.

Giới hạn về kích thước của các đối số là trên lệnh execve(2)gọi hệ thống (thực ra, đó là kích thước tích lũy của đối số và chuỗi môi trường và con trỏ). Nếu myscriptđược viết bằng ngôn ngữ mà trình bao của bạn có thể diễn giải, thì có lẽ bạn không cần phải thực thi nó, bạn có thể có trình bao của bạn chỉ cần diễn giải nó mà không cần phải thực thi trình thông dịch khác.

Nếu bạn chạy tập lệnh như:

(. myscript x y)

Nó giống như:

myscript x y

Ngoại trừ việc nó được giải thích bởi một đứa trẻ của shell hiện tại, thay vì thực thi nó (cuối cùng liên quan đến việc thực thi sh (hoặc bất cứ dòng nào cô ấy nói rõ nếu có) với nhiều đối số hơn).

Bây giờ rõ ràng, bạn không thể sử dụng find -exec {} +với .lệnh, vì .là lệnh dựng sẵn của shell, nó phải được thực thi bởi shell chứ không phải bởi shell find.

Với zsh, thật dễ dàng:

IFS=$'\0'
(. myscript $(find ... -print0))

Hoặc là:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

Mặc dù vậy zsh, bạn sẽ không cần findở nơi đầu tiên vì hầu hết các tính năng của nó được tích hợp vào toàn cầu zsh.

bashtuy nhiên các biến không thể chứa các ký tự NUL, vì vậy bạn phải tìm một cách khác. Một cách có thể là:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

Bạn cũng có thể sử dụng tính năng đệ quy theo kiểu zsh với globstartùy chọn trong bash4.0 trở lên:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

Lưu ý rằng **theo các liên kết tượng trưng đến các thư mục cho đến khi nó được sửa trong bash4.3. Cũng lưu ý rằng bashkhông triển khai zshvòng loại toàn cầu để bạn không nhận được tất cả các tính năng findở đó.

Một cách khác là sử dụng GNU ls:

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

Các phương thức trên cũng có thể được sử dụng nếu bạn muốn đảm bảo chỉ myscriptđược thực hiện một lần (không thành công nếu danh sách đối số quá lớn). Trên các phiên bản gần đây của Linux, bạn có thể nâng và thậm chí nâng giới hạn đó trong danh sách đối số bằng:

ulimit -s 1048576

(Kích thước ngăn xếp 1GiB, một phần tư trong số đó có thể được sử dụng cho danh sách arg + env).

ulimit -s unlimited

(không giới hạn)


1

Trong hầu hết các hệ thống, có giới hạn về độ dài của một dòng lệnh được truyền cho bất kỳ chương trình nào, sử dụng xargshoặc -exec command {} +. Từ man find:

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

Yêu cầu sẽ ít hơn nhiều, nhưng không được đảm bảo là một. Những gì bạn nên làm là đọc tên tệp được phân tách bằng NUL trong tập lệnh từ stdin, có thể dựa trên đối số dòng lệnh -o -. Tôi sẽ làm một cái gì đó như:

$ find . -name something.txt -print0 | myscript -0 -o -

và thực hiện các đối số tùy chọn cho myscriptphù hợp.


Có, HĐH áp đặt giới hạn về số lượng / kích thước của các đối số có thể được thông qua. Trên các hệ thống Linux hiện đại, đây là (khổng lồ) ( linux.die.net/man/2/execve ) (1/4 kích thước ngăn xếp, các đối số 0x7FFFFFFF). Bản thân AFAIK không áp đặt bất kỳ giới hạn nào. Danh sách của tôi nhỏ hơn nhiều, và vấn đề của tôi là do hiểu lầm hoặc nhớ sai về cách thức xargshoạt động. Giải pháp của bạn thực sự là mạnh mẽ nhất, nhưng nó quá mức cần thiết trong trường hợp này.
alexis

0

Không có cách nào để bảo vệ không gian trong bản mở rộng backtick (hoặc $ (...))?

Không, không có. Tại sao vậy?

Bash không có cách nào để biết những gì nên được bảo vệ và những gì không nên.

Không có mảng trong tệp / ống unix. Nó chỉ là một luồng byte. Lệnh bên trong ``hoặc $()xuất ra một luồng, nó sẽ nuốt và xử lý như một chuỗi đơn. Như vậy, bạn chỉ có hai lựa chọn: đặt nó trong dấu ngoặc kép, để giữ nó thành một chuỗi hoặc đặt nó ở chế độ trần, để bash chia nó ra theo hành vi được cấu hình của nó.

Vì vậy, những gì bạn phải làm nếu bạn muốn một mảng là xác định định dạng byte có một mảng và đó là những công cụ thích xargsfindlàm: Nếu bạn chạy chúng với -0đối số, chúng hoạt động theo định dạng mảng nhị phân chấm dứt các phần tử với byte rỗng, thêm ngữ nghĩa vào luồng byte mờ khác.

Thật không may, bashkhông thể được cấu hình để phân chia chuỗi trên byte null. Cảm ơn /unix//a/110108/17980 đã cho chúng tôi thấy điều đó zshcó thể.

xargs

Bạn muốn lệnh của bạn chạy một lần, và bạn nói rằng xargs -0 -n 10000giải quyết vấn đề của bạn. Không, nó đảm bảo rằng nếu bạn có hơn 10000 tham số, lệnh của bạn sẽ chạy nhiều lần.

Nếu bạn muốn làm cho nó hoàn toàn chạy một lần hoặc thất bại, bạn phải cung cấp -xđối số và -nđối số lớn hơn -sđối số (thực sự: đủ lớn để cả một loạt các đối số có độ dài bằng không cộng với tên của lệnh không khớp các -skích thước). ( man xargs , xem đoạn trích phía dưới)

Hệ thống tôi hiện đang sử dụng có một ngăn xếp giới hạn trong khoảng 8 triệu, vì vậy đây là giới hạn của tôi:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

bash

Nếu bạn không muốn liên quan đến một lệnh bên ngoài, vòng lặp while-đọc cung cấp một mảng, như được hiển thị trong /unix//a/110108/17980 , là cách duy nhất để bash phân chia mọi thứ tại byte rỗng.

Ý tưởng tìm nguồn script ( . ... "$@" )để tránh giới hạn kích thước ngăn xếp là tuyệt vời (tôi đã thử nó, nó hoạt động!), Nhưng có lẽ không quan trọng đối với các tình huống thông thường.

Sử dụng một fd đặc biệt cho ống quy trình rất quan trọng nếu bạn muốn đọc một cái gì đó khác từ stdin, nhưng nếu không thì bạn sẽ không cần nó.

Vì vậy, cách "bản địa" đơn giản nhất, cho nhu cầu hàng ngày của gia đình:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

Nếu bạn thích cây quy trình của bạn sạch sẽ và đẹp mắt, phương pháp này cho phép bạn thực hiện exec mynonscript "${files[@]}", loại bỏ quá trình bash khỏi bộ nhớ, thay thế nó bằng lệnh được gọi. xargssẽ luôn ở trong bộ nhớ trong khi lệnh được gọi chạy, ngay cả khi lệnh chỉ chạy một lần.


Những gì nói chống lại phương pháp bash bản địa là thế này:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bash không được tối ưu hóa để xử lý mảng.


người đàn ông xargs :

-n max-args

Sử dụng tối đa các đối số max-args trên mỗi dòng lệnh. Ít hơn các đối số max-args sẽ được sử dụng nếu kích thước (xem tùy chọn -s) bị vượt quá, trừ khi tùy chọn -x được đưa ra, trong trường hợp đó xargs sẽ thoát.

-s ký tự tối đa

Sử dụng tối đa các ký tự ký tự tối đa trên mỗi dòng lệnh, bao gồm lệnh và đối số ban đầu và các kết thúc null ở cuối chuỗi đối số. Giá trị được phép lớn nhất phụ thuộc vào hệ thống và được tính là giới hạn độ dài đối số cho exec, trừ kích thước môi trường của bạn, ít hơn 2048 byte khoảng không. Nếu giá trị này lớn hơn 128KiB, 128Kib được sử dụng làm giá trị mặc định; mặt khác, giá trị mặc định là tối đa. 1KiB là 1024 byte.

-x

Thoát nếu kích thước (xem tùy chọn -s) bị vượt quá.


Cảm ơn tất cả các rắc rối nhưng tiền đề cơ bản của bạn bỏ qua thực tế là bash thường sử dụng một hệ thống xử lý trích dẫn phức tạp. Nhưng không phải trong mở rộng backquote. Hãy so sánh sau đây (lỗi mà cả hai đều cho, nhưng cho thấy sự khác biệt): ls "what is this"vs ls `echo '"what is this"'` . Ai đó đã bỏ qua việc thực hiện xử lý báo giá cho kết quả của backquote.
alexis

Tôi vui mừng backquote không làm xử lý báo giá. Việc họ thậm chí thực hiện chia tách từ đã gây ra đủ vẻ bối rối, trầy xước và lỗ hổng bảo mật trong lịch sử điện toán hiện đại.
clacke

Câu hỏi là "Không có cách nào để bảo vệ không gian trong $(...)việc mở rộng backtick (hoặc )?", Vì vậy có vẻ phù hợp để bỏ qua việc xử lý không được thực hiện trong tình huống đó.
clacke

Định dạng mảng phần tử kết thúc null là cách đơn giản nhất và do đó an toàn nhất để thể hiện một mảng. Đó chỉ là một sự xấu hổ mà bashkhông hỗ trợ nó thực sự giống như rõ ràng zsh.
clacke

Trong thực tế, chỉ trong tuần này tôi đã sử dụng printf "%s\0"xargs -0định tuyến xung quanh một tình huống trích dẫn trong đó một công cụ trung gian sẽ chuyển các tham số thông qua một chuỗi được phân tách bằng shell. Trích dẫn luôn luôn quay trở lại để cắn bạn.
clacke
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.