2009-12-19 28 views
8

Ngày bắt đầu khi tôi phải viết một tập lệnh BASH để đi các cây thư mục tùy ý và xem các tệp tùy ý và cố gắng xác định điều gì đó liên quan đến sự so sánh giữa chúng. Tôi nghĩ rằng nó sẽ là một ngọn núi đơn giản trong một vài giờ ! quy trình - Không phải vậy!Có một "công cụ chuyển đổi thoát" cho tên tệp và thư mục có sẵn không?

Việc gác máy của tôi là đôi khi một số kẻ ngốc - xin lỗi, người dùng đáng yêu chọn đặt dấu cách vào tên thư mục và tên tệp. Điều này khiến kịch bản của tôi thất bại.

Giải pháp hoàn hảo, ngoài việc đe dọa chém cho những người khăng khăng sử dụng không gian ở những nơi như vậy (chưa kể những kẻ đặt mã này trong hệ điều hành!), Có thể là thói quen "thoát" tên tệp và thư mục cho chúng tôi, giống như cách Cygwin có các thường trình để chuyển đổi từ unix sang định dạng tên tệp dos. Có điều gì giống như thế này trong bản phân phối chuẩn Unix/Linux không?

Lưu ý rằng cấu trúc đơn giản for file in * không hoạt động tốt khi người ta đang cố gắng so sánh cây thư mục vì nó CHỈ hoạt động trên "thư mục hiện tại" - và trong trường hợp này các vị trí thư mục khác nhau mang lại cho nó những vấn đề riêng của nó. Vì vậy, khi làm bài tập về nhà của tôi, tôi thấy câu hỏi này Handle special characters in bash for...in loop và giải pháp đề xuất có treo lên trên khoảng trống trong tên thư mục, nhưng chỉ có thể được khắc phục như thế này:

dir="dirname with spaces" 
ls -1 "$dir" | while read x; do 
    echo $x 
done 

XIN LƯU Ý: Mã isn trên' t đặc biệt tuyệt vời vì các biến được sử dụng bên trong vòng lặp while là INACCESSIBLE bên ngoài vòng lặp while. Điều này là bởi vì có một subshell ngụ ý được tạo ra khi đầu ra của lệnh ls được piped. Đây là yếu tố thúc đẩy chính cho truy vấn của tôi!

... OK, đoạn mã trên giúp ích cho nhiều trường hợp nhưng "thoát" các ký tự cũng sẽ khá mạnh. Ví dụ: dir above có thể chứa:

dir\ with\ spaces 

Điều này đã tồn tại và tôi vừa xem nó?

Nếu không, có ai có đề xuất dễ dàng để viết một - có thể với sed hoặc lex không? (Tôi xa có thẩm quyền với một trong hai.)

+0

Bash phải có một nội bộ, vì nó được sử dụng bất cứ khi nào bạn nhấn "tab", vì vậy đó có thể là điểm khởi đầu. – Ken

+0

Tôi đồng ý và rất thích khai thác trực tiếp - nếu chỉ có một cách! Hmmm ... Có lẽ một số bạn bè nguồn mở của chúng tôi có thể coi nó là một bổ sung xứng đáng cho chính vỏ? Nó có thể được lập luận rằng bản gốc "cập nhật" để cho phép các không gian (đặc biệt) là không đầy đủ mà không có một công cụ như vậy. ... Trong khi đó, tôi không có ý tưởng làm thế nào để nhân rộng lập trình các hành động tab! Bạn? –

+0

Tôi không chắc tôi hiểu nhu cầu của bạn là gì. Thông thường, sử dụng 'find' và vòng lặp' while' là quá đủ. Có thể bạn có thể đăng một số mã mà bạn đang gặp sự cố. –

Trả lời

4

Thực hiện một tên tập tin thực sự khó chịu để thử nghiệm:

mkdir escapetest 
cd escapetest && touch "m'i;x&e\"d u(p\nmulti)\nlines'\nand\015ca&rr\015re;t" 

[Edit: Rất có thể rằng tôi dự định rằng touch lệnh là:

touch $'m\'i;x&e\"d u(p\nmulti)\nlines\'\nand\015ca&rr\015re;t' 

trong đó đặt nhân vật xấu xí hơn trong filename . Đầu ra sẽ trông hơi khác một chút. ]

Sau đó chạy này:

find -print0 | while read -d '' -r line; do echo -en "--[${line}]--\t\t"; echo "$line"|sed -e ':t;N;s/\n/\\n/;bt' | sed 's/\([ \o47()"&;\\]\)/\\\1/g;s/\o15/\\r/g'; done 

Đầu ra nên trông như thế này:

 
--[./m'i;x&e"d u(p 
multi) 
lines' 
re;t]--   ./m\'i\;x\&e\"d\ u\(p\\nmulti\)\\nlines\'\\nand\\015ca\&rr\\015re\;t 

này bao gồm một phiên bản đặc của Pascal Thivent củased quái vật, cộng với xử lý để vận chuyển trả về và dòng mới và có thể nhiều hơn một chút.

Việc vượt qua đầu tiên thông qua sed hợp nhất nhiều dòng thành một được giới hạn bởi "\ n" cho tên tệp có dòng mới. Thẻ thứ hai thay thế bất kỳ từ danh sách các ký tự bằng dấu gạch chéo ngược trước chính nó. Phần cuối cùng thay thế trả về vận chuyển bằng "\ r".

Một điều cần lưu ý là, như bạn đã biết, while sẽ xử lý không gian và for sẽ không nhưng bằng cách gửi đầu ra của find với chấm dứt vô thiết lập delimiter của read null, bạn cũng có thể xử lý dòng mới trong tên tập tin . Tùy chọn -r gây ra read để chấp nhận các dấu gạch chéo ngược mà không diễn giải chúng.

Edit:

Một cách khác để thoát khỏi nhân vật đặc biệt, lần này mà không sử dụng sed, sử dụng tính năng trích dẫn và biến tạo của Bash printf dựng sẵn (điều này cũng minh họa sử dụng thay thế tiến trình chứ không phải là một đường ống):

while read -d '' -r file; do echo "$file"; printf -v name "%q" "$file"; echo "$name"; done< <(find -print0) 

biến $name sẽ có mặt bên ngoài vòng lặp, vì sử dụng thay thế tiến trình ngăn chặn việc tạo ra một subshell quanh vòng lặp.

+0

Nó không phải của tôi nhưng, có, đó là một con quái vật :) –

+0

Bài viết tuyệt vời, cảm ơn. ... Đây là _very_ tốt và đáp ứng với câu hỏi ban đầu. –

0

Lệnh find đôi khi làm việc trong tình huống này:

find . -exec ls {} \; 

ví dụ

2

tôi thấy How to escape file names in bash shell scripts này trong khi googling mà tôi trích dẫn dưới đây:

Sau khi chiến đấu với Bash trong thời gian khá một thời gian, tôi phát hiện ra rằng mã sau cung cấp cơ sở tốt đẹp để thoát các ký tự đặc biệt. Của cource nó không phải là hoàn thành, nhưng ký tự quan trọng nhất là lọc.

Nếu có ai có giải pháp tốt hơn, hãy vui lòng cho tôi biết. Nó hoạt động và nó là có thể đọc được nhưng không đẹp.

FILE_ESCAPED=`echo "$FILE" | \ 
sed s/\\ /\\\\\\\\\\\\\\ /g | \ 
sed s/\\'/\\\\\\\\\\\\\\'/g | \ 
sed s/\&/\\\\\\\\\\\\\\&/g | \ 
sed s/\;/\\\\\\\\\\\\\\;/g | \ 
sed s/\(/\\\\\\\\\\(/g | \ 
sed s/\)/\\\\\\\\\\)/g ` 

Có lẽ bạn có thể sử dụng nó như là điểm bắt đầu.

+0

Cảm ơn đoạn mã này. Đó là một phiên bản không đầy đủ của những gì tôi đã yêu cầu, THANK YOU! –

2

Đoạn sau xử lý tất cả tên tập tin (những bao gồm khoảng trống, dấu ngoặc kép, dòng mới, ...):

startdir="${1:-.}"        # first parameter or working directory 

#------------------------------------------------------------------------------- 
# IFS is undefined 
# read: 
# -r do not allow backslashes to escape any characters 
# -d delimiter is \0 (not a valid character in a filename) 
# done < <(find ...) . redirection from a process substitution 
#------------------------------------------------------------------------------- 
while IFS= read -r -d '' file; do 
    echo "'$file'" 
done < <(find "$startdir" -type f -print0) 

Xem thêm BashFAQ này.

+0

Cảm ơn bài đăng. OK, đây là một cách khác để lặp lại và không tốt hơn cũng không tệ hơn vòng lặp được đăng trong câu hỏi gốc. Nó có bất lợi của việc đặt lại IFS và nếu bạn cần nó đặt bên trong vòng lặp, bạn sẽ bị đau đầu. Và nó có lợi thế là để cho người viết kịch bản mang nội dung vô cùng ra khỏi vòng lặp - một hạn chế của mã được trình bày trong truy vấn ban đầu. –

2

Có một vấn đề khá nghiêm trọng với phương pháp thoát: những gì thoát là cần thiết phụ thuộc vào bối cảnh biến sẽ được mở rộng, và trong trường hợp thông thường không có thoát sẽ làm việc.Ví dụ: nếu bạn định làm điều gì đó đơn giản như:

touch a "b c" d 
files="a b\ c d" 
ls $files 

... nó sẽ không hoạt động (ls tìm 4 tệp: "a", "b \", "c" và "d") bởi vì trình bao không chú ý đến việc thoát khi nó chia tách các tệp $. Bạn có thể sử dụng eval ls $files, nhưng điều đó sẽ không thành công trên những thứ như tab trong tên tệp.

Phương pháp tiếp cận while ... read ... done < <(find ... -print0) được đề xuất hoạt động kiên cố (và vì tính linh hoạt của các mẫu tìm kiếm của tìm kiếm, rất mạnh), nhưng nó cũng là một đống giải pháp thay thế cho nhiều vấn đề khác nhau; nếu bạn không cần sức mạnh tìm, nó không phải là khó khăn để có được những điều thực hiện với for*:

shopt -s nullglob # In case of empty directories... 
for filepath in "$dir"/*; do # loop over all files in the specified directory 
    filename="${filepath##*/}" # You just wanted the files' names? No problem. 
    echo "$filename" 
done 

Nếu (như bạn đề cập đến trong câu hỏi) bạn đang quan tâm đến việc so sánh hai cây thư mục, Looping qua một trong số họ không phải là những gì bạn muốn; nó muốn được tốt hơn để đưa nội dung của họ vào mảng, như thế này:

shopt -s nullglob 
pathlist1=("$dir1"/*) # Get a list of paths of files in dir1 
filelist1=("${pathlist1[@]##*/}") # Parse off just the filenames 
pathlist2=("$dir2"/*) # Same for dir2 
filelist2=("${pathlist2[@]##*/}") 
# now compare filelist1 with filelist2... 

(. Lưu ý rằng AFAIK "${pathlist2[@]##*/}" xây dựng không phải là tiêu chuẩn, nhưng dường như đã được hỗ trợ trong cả bash và zsh trong một thời gian bây giờ)

+1

Rất chu đáo đăng bài và sáng tạo, cảm ơn. Một điểm ở đây là với những lời giải thích rắc rối về mô hình thoát của bạn, người ta có thể khắc phục những vấn đề bạn nói đến bằng cách sử dụng dấu ngoặc kép ngoài việc "trốn thoát" - ít nhất tôi nghĩ vậy. ... Hệ thống của tôi không nhìn thấy những gì "shopt" là - tôi đoán nó là một lựa chọn vỏ. Bash của tôi không thích nó! Và tôi e rằng tôi không hoàn toàn hiểu được doanh nghiệp "$ {pathlist2 [@] ## * /}" thậm chí đang cố gắng làm gì! Ở đây, có lẽ? –

+0

Trích dẫn ngoài việc thoát: Tôi đã thử điều đó, các trích dẫn chỉ được coi là một phần của tên tệp; ngoài 'eval', tôi không nghĩ có cách để làm điều đó.Trên 'shopt': bạn đang sử dụng phiên bản bash nào? Đó là trong mỗi phiên bản tôi đã sử dụng ... Nếu bạn không có nó, và không có tập tin phù hợp, mô hình glob mở rộng cho chính nó. Một cách khác là bạn có thể thêm '[[-e" $ filepath "]] || tiếp tục' là dòng đầu tiên của vòng lặp 'for'. –

+0

Trên '" $ {pathlist2 [@] ## * /} "': '" $ {pathlist2 [@]} "' mở rộng cho các thành viên của mảng, mỗi "từ" riêng biệt. Thêm '## * /' loại bỏ thông qua "/" cuối cùng trong mỗi mục - về cơ bản, đó là một mẹo để biến một mảng các đường dẫn tệp đầy đủ thành một mảng chỉ các tên tệp. –

1
#!/bin/bash 

while read filename; do 
    echo 'I am doing something with "'"$filename"'".' 
done < <(find) 

Lưu ý rằng ký hiệu <() sẽ không hoạt động khi bash được gọi là /bin/sh.

Các vấn đề liên quan