bash: sử dụng thủ tục tìm an toàn khoảng trắng


12

Cho các tên tệp này:

$ ls -1
file
file name
otherfile

bash chính nó hoàn toàn tốt với khoảng trắng được nhúng:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

Tuy nhiên, đôi khi tôi có thể không muốn làm việc với mọi tệp, hoặc thậm chí nghiêm ngặt $PWD, đó là nơi findđến. Nó cũng xử lý khoảng trắng trên danh nghĩa:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

Tôi đang cố gắng tạo ra một phiên bản an toàn của tập lệnh này , nó sẽ đưa đầu ra findvà trình bày nó vào select:

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

Tuy nhiên, điều này bùng nổ với khoảng trắng trong tên tệp:

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

Thông thường, tôi sẽ khắc phục điều này bằng cách loay hoay với IFS. Tuy nhiên:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

Giải pháp cho vấn đề này là gì?



1
Nếu bạn chỉ sử dụng findkhả năng khớp với một tên tệp cụ thể, bạn chỉ cần sử dụng select file in **/file*(sau khi cài đặt shopt -s globstar) trong bash4 hoặc sau đó.
chepner

Câu trả lời:


14

Nếu bạn chỉ cần xử lý khoảng trắng và tab (không được nhúng dòng mới) thì bạn có thể sử dụng mapfile(hoặc từ đồng nghĩa của nó readarray) để đọc vào một mảng, ví dụ như đã cho

$ ls -1
file
other file
somefile

sau đó

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

Nếu bạn làm cần thiết phải xử lý dòng mới, và bạn bashphiên bản cung cấp một null được phân định mapfile1 , sau đó bạn có thể thay đổi mà để IFS= mapfile -t -d '' files < <(find . -type f -print0). Mặt khác, lắp ráp một mảng tương đương từ findđầu ra được phân tách bằng cách sử dụng một readvòng lặp:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1 sự -dlựa chọn đã được thêm vào mapfiletrong bashphiên bản 4.4 iirc


2
+1 cho một động từ khác mà tôi chưa từng sử dụng trước đây
roaima

Thật vậy, mapfilelà một cái mới đối với tôi cũng. Thanh danh.
DopeGhoti

Các while IFS= readphiên bản làm việc trở lại trong bash v3 (đó là quan trọng đối với những người trong chúng ta sử dụng hệ điều hành MacOS).
Gordon Davisson

3
+1 cho find -print0biến thể; càu nhàu khi đặt nó sau một phiên bản không chính xác và chỉ mô tả nó để sử dụng nếu ai đó biết rằng họ cần xử lý các dòng mới. Nếu một người chỉ xử lý những điều bất ngờ ở những nơi mà nó mong đợi, thì người ta sẽ không bao giờ xử lý những điều bất ngờ đó.
Charles Duffy

8

Câu trả lời này có giải pháp cho bất kỳ loại tập tin. Với dòng mới hoặc không gian.
Có những giải pháp cho bash gần đây cũng như bash cổ và thậm chí cả vỏ posix cũ.

Cây được liệt kê dưới đây trong câu trả lời này [1] được sử dụng cho các bài kiểm tra.

lựa chọn

Thật dễ dàng để selectlàm việc với một mảng:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

Hoặc với các tham số vị trí:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

Vì vậy, vấn đề thực sự duy nhất là lấy "danh sách các tệp" (được phân tách chính xác) bên trong một mảng hoặc bên trong Tham số vị trí. Hãy đọc tiếp.

bash

Tôi không thấy vấn đề bạn báo cáo với bash. Bash có thể tìm kiếm trong một thư mục nhất định:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Hoặc, nếu bạn thích một vòng lặp:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Lưu ý rằng cú pháp ở trên sẽ hoạt động chính xác với bất kỳ shell (hợp lý) nào (không phải csh ít nhất).

Giới hạn duy nhất mà cú pháp trên có là đi xuống các thư mục khác.
Nhưng bash có thể làm điều đó:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Để chỉ chọn một số tệp (như những tệp kết thúc trong tệp), chỉ cần thay thế *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

mạnh mẽ

Khi bạn đặt một "không gian an toàn " trong tiêu đề, tôi sẽ cho rằng những gì bạn muốn nói là " mạnh mẽ ".

Cách đơn giản nhất để mạnh mẽ về không gian (hoặc dòng mới) là từ chối xử lý đầu vào có khoảng trắng (hoặc dòng mới). Một cách rất đơn giản để thực hiện việc này trong trình bao là thoát với lỗi nếu bất kỳ tên tệp nào mở rộng với khoảng trắng. Có một số cách để làm điều này, nhưng nhỏ gọn nhất (và posix) (nhưng giới hạn trong một nội dung thư mục, bao gồm tên suddirectories và tránh các tệp dấu chấm) là:

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

Nếu giải pháp được sử dụng là mạnh mẽ trong bất kỳ mục nào trong số đó, hãy xóa bài kiểm tra.

Trong bash, các thư mục con có thể được kiểm tra cùng một lúc với ** được giải thích ở trên.

Có một số cách để bao gồm các tệp chấm, giải pháp Posix là:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

tìm thấy

Nếu tìm thấy phải được sử dụng vì một số lý do, thay thế dấu phân cách bằng NUL (0x00).

bash 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

VỊ TRÍ

Để thực hiện một giải pháp POSIX hợp lệ trong đó find không có dấu phân cách NUL và không có -d(cũng không có -a) để đọc, chúng ta cần một aproach hoàn toàn khác.

Chúng ta cần sử dụng một phức hợp -exectừ find với một cuộc gọi đến shell:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

Hoặc, nếu điều cần thiết là chọn (chọn là một phần của bash, không phải sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1] Cây này (\ 012 là dòng mới):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

Có thể được xây dựng với hai lệnh này:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

Bạn không thể đặt biến trước cấu trúc vòng lặp, nhưng bạn có thể đặt biến đó trước điều kiện. Đây là đoạn từ trang người đàn ông:

Môi trường cho bất kỳ lệnh hoặc hàm đơn giản nào có thể được tăng cường tạm thời bằng cách thêm tiền tố vào nó bằng các phép gán tham số, như được mô tả ở trên trong PARAMETERS.

(Một vòng lặp không phải là một lệnh đơn giản .)

Đây là một cấu trúc thường được sử dụng thể hiện các kịch bản thất bại và thành công:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

Thật không may, tôi không thể thấy một cách để nhúng một thay đổi IFSvào selectcấu trúc trong khi nó ảnh hưởng đến việc xử lý một liên kết $(...). Tuy nhiên, không có gì để ngăn chặn IFSđược đặt bên ngoài vòng lặp:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

và đó là cấu trúc này mà tôi có thể thấy hoạt động với select:

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

Khi viết mã phòng thủ tôi khuyên bạn nên rằng mệnh đề thể được chạy trong một subshell, hay IFSSHELLOPTSlưu và phục hồi xung quanh khối:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob

5
Giả sử rằng IFS=$'\n'an toàn là không có cơ sở. Tên tập tin hoàn toàn có thể chứa các dòng chữ mới.
Charles Duffy

4
Tôi thực sự do dự khi chấp nhận những xác nhận như vậy về bộ dữ liệu có thể có của một người theo mệnh giá, ngay cả khi có mặt. Sự kiện mất dữ liệu tồi tệ nhất mà tôi đã có mặt là một trường hợp trong đó một tập lệnh bảo trì chịu trách nhiệm dọn dẹp các bản sao lưu cũ đã cố gắng xóa một tập tin được tạo bởi một tập lệnh Python bằng cách sử dụng một mô-đun C với một con trỏ xấu đã đổ rác ngẫu nhiên - bao gồm một ký tự đại diện được phân tách bằng khoảng trắng - vào tên.
Charles Duffy

2
Những người xây dựng tập lệnh shell đang dọn dẹp các tệp đó không bận tâm trích dẫn vì tên "không thể" không khớp [0-9a-f]{24}. TB sao lưu dữ liệu được sử dụng để hỗ trợ thanh toán của khách hàng đã bị mất.
Charles Duffy

4
Đồng ý với @CharlesDuffy hoàn toàn. Không xử lý các trường hợp cạnh chỉ tốt khi bạn làm việc tương tác và có thể thấy những gì bạn đang làm. selectbởi chính thiết kế của nó là dành cho các giải pháp theo kịch bản , vì vậy nó phải luôn được thiết kế để xử lý các trường hợp cạnh.
tự đại diện

2
@ilkkachu, tất nhiên - bạn sẽ không bao giờ gọi selecttừ shell mà bạn đang gõ lệnh để chạy, mà chỉ ở một tập lệnh, nơi bạn đang trả lời lời nhắc được cung cấp bởi tập lệnh đó và tập lệnh đó ở đâu thực thi logic được xác định trước (được xây dựng mà không có kiến ​​thức về tên tệp được vận hành) dựa trên đầu vào đó.
Charles Duffy

4

Tôi có thể ra khỏi phạm vi quyền hạn của mình ở đây nhưng có lẽ bạn có thể bắt đầu với một cái gì đó như thế này, ít nhất là nó không có bất kỳ rắc rối nào với khoảng trắng:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

Để tránh mọi giả định sai tiềm năng, như đã lưu ý trong các nhận xét, hãy lưu ý rằng mã trên tương đương với:

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -dlà một giải pháp thông minh; cảm ơn vì điều đó.
DopeGhoti

2
read -d $'\000'chính xác giống hệt read -d '', nhưng đối với sai lệch người dân về khả năng của bash (ngụ ý, không đúng cách, mà nó có thể đại diện cho NUL đen trong chuỗi). Chạy s1=$'foo\000bar'; s2='foo'và sau đó cố gắng tìm cách phân biệt giữa hai giá trị. (Một phiên bản trong tương lai có thể bình thường hóa với hành vi thay thế lệnh bằng cách làm cho giá trị được lưu trữ tương đương foobar, nhưng đó không phải là trường hợp ngày nay).
Charles Duffy
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.