Làm cách nào để thoát khoảng trắng trong danh sách vòng lặp bash?


121

Tôi có một tập lệnh bash shell lặp qua tất cả các thư mục con (nhưng không phải tệp) của một thư mục nhất định. Vấn đề là một số tên thư mục chứa khoảng trắng.

Đây là nội dung của thư mục thử nghiệm của tôi:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Và mã lặp qua các thư mục:

for f in `find test/* -type d`; do
  echo $f
done

Đây là kết quả:

test / Baltimore
test / Cherry
đồi núi
test / Edison 
thử nghiệm / Mới
York
Tp.
test / Philadelphia

Cherry Hill và New York City được coi là 2 hoặc 3 mục riêng biệt.

Tôi đã thử trích dẫn các tên tệp, như sau:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

nhưng không có kết quả.

Phải có một cách đơn giản để làm điều này.


Các câu trả lời dưới đây là tuyệt vời. Nhưng để làm cho điều này phức tạp hơn - tôi không phải lúc nào cũng muốn sử dụng các thư mục được liệt kê trong thư mục thử nghiệm của mình. Đôi khi tôi muốn chuyển các tên thư mục làm tham số dòng lệnh thay thế.

Tôi lấy đề xuất của Charles về việc thiết lập IFS và nghĩ ra những điều sau:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

và điều này hoạt động tốt trừ khi có khoảng trắng trong các đối số dòng lệnh (ngay cả khi các đối số đó được trích dẫn). Ví dụ: gọi tập lệnh như sau: test.sh "Cherry Hill" "New York City"tạo ra kết quả sau:

quả anh đào
đồi núi
Mới
York
Tp.

re: edit, list="$@"loại bỏ hoàn toàn list-ness của giá trị ban đầu, thu gọn nó thành một chuỗi. Vui lòng làm theo các thông lệ trong câu trả lời của tôi một cách chính xác như đã đưa ra - một bài tập như vậy không được khuyến khích ở bất kỳ đâu trong đó; nếu bạn muốn chuyển một danh sách các đối số dòng lệnh vào một chương trình, bạn nên thu thập chúng vào một mảng và mở rộng mảng đó trực tiếp.
Charles Duffy

Câu trả lời:


105

Đầu tiên, đừng làm theo cách đó. Cách tốt nhất là sử dụng find -execđúng cách:

# this is safe
find test -type d -exec echo '{}' +

Cách tiếp cận an toàn khác là sử dụng danh sách được kết thúc bằng NUL, mặc dù điều này yêu cầu bạn tìm thấy sự hỗ trợ -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Bạn cũng có thể điền một mảng từ tìm và chuyển mảng đó sau:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Nếu tìm thấy của bạn không hỗ trợ -print0, kết quả của bạn sau đó không an toàn - bên dưới sẽ không hoạt động như mong muốn nếu các tệp tồn tại chứa các dòng mới trong tên của chúng (mà, vâng, là hợp pháp):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Nếu người ta không sử dụng một trong những cách trên, thì cách tiếp cận thứ ba (kém hiệu quả hơn về cả thời gian và sử dụng bộ nhớ, vì nó đọc toàn bộ đầu ra của quy trình con trước khi thực hiện tách từ) là sử dụng một IFSbiến không 't chứa ký tự khoảng trắng. Tắt globbing ( set -f) để ngăn chặn chuỗi có chứa ký tự glob như [], *hoặc ?từ được mở rộng:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Cuối cùng, đối với trường hợp tham số dòng lệnh, bạn nên sử dụng mảng nếu trình bao của bạn hỗ trợ chúng (tức là ksh, bash hoặc zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

sẽ duy trì sự tách biệt. Lưu ý rằng việc trích dẫn (và việc sử dụng $@thay vì $*) là quan trọng. Mảng cũng có thể được điền theo những cách khác, chẳng hạn như biểu thức toàn cầu:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1
không biết về hương vị '+' đó cho -exec. ngọt ngào
Johannes Schaub - litb

1
tho có vẻ như nó cũng có thể, giống như xargs, chỉ đặt các đối số ở cuối lệnh đã cho: / điều đó đôi khi làm tôi nghe trộm
Johannes Schaub - litb

Tôi nghĩ -exec [name] {} + là một phần mở rộng GNU và 4.4-BSD. (Ít nhất, nó không xuất hiện trên Solaris 8, và tôi không nghĩ rằng nó đang ở trong AIX 4.3 hoặc.) Tôi đoán phần còn lại của chúng tôi có thể bị mắc kẹt với đường ống để xargs ...
Michael Ratanapintha

2
Tôi chưa bao giờ thấy cú pháp $ '\ n' trước đây. Nó hoạt động như thế nào? (Tôi đã nghĩ rằng IFS = '\ n' hoặc IFS = "\ n" sẽ hoạt động, nhưng cả hai đều không hoạt động.)
MCS

1
@crosstalk chắc chắn nó có trong Solaris 10, tôi mới sử dụng nó.
Nick

26
find . -type d | while read file; do echo $file; done

Tuy nhiên, không hoạt động nếu tên tệp chứa dòng mới. Trên đây là giải pháp duy nhất mà tôi biết khi bạn thực sự muốn có tên thư mục trong một biến. Nếu bạn chỉ muốn thực hiện một số lệnh, hãy sử dụng xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '

Không cần xargs, hãy xem find -exec ... {} +
Charles Duffy

4
@Charles: đối với số lượng lớn tệp, xargs hiệu quả hơn nhiều: nó chỉ tạo ra một quy trình. Tùy chọn -exec tạo ra một quy trình mới cho mỗi tệp, quá trình này có thể chậm hơn theo cấp độ.
Adam Rosenfield

1
Tôi thích xargs hơn. Hai yếu dường như làm như vậy cả, trong khi xargs có nhiều lựa chọn hơn, như chạy song song
Johannes Schaub - litb

2
Adam, không có '+' một cái sẽ tổng hợp nhiều tên tệp nhất có thể và sau đó thực thi. nhưng nó sẽ không có các chức năng gọn gàng như chạy song song :)
Johannes Schaub - litb

2
Lưu ý rằng nếu bạn muốn làm điều gì đó với tên tệp, bạn sẽ phải trích dẫn chúng. Vd:find . -type d | while read file; do ls "$file"; done
David Moles

23

Đây là một giải pháp đơn giản để xử lý các tab và / hoặc khoảng trắng trong tên tệp. Nếu bạn phải đối phó với các ký tự lạ khác trong tên tệp như dòng mới, hãy chọn một câu trả lời khác.

Thư mục thử nghiệm

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Mã để đi vào các thư mục

find test -type d | while read f ; do
  echo "$f"
done

Tên tệp phải được trích dẫn ( "$f") nếu được sử dụng làm đối số. Không có dấu ngoặc kép, các khoảng trắng hoạt động như dấu phân tách đối số và nhiều đối số được cấp cho lệnh được gọi.

Và đầu ra:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

cảm ơn, điều này phù hợp với bí danh mà tôi đang tạo để liệt kê dung lượng mỗi thư mục trong thư mục hiện tại đang sử dụng, nó đã gây nghẹt thở cho một số dirs có khoảng trắng trong phiên bản trước. Này hoạt động trong zsh, nhưng một số các câu trả lời khác không:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid

2
Nếu bạn đang sử dụng zsh, bạn cũng có thể làm điều này:alias duc='du -sh *(/)'
cbliard

@cbliard Đây vẫn là lỗi. Hãy thử chạy nó với một tên tệp, chẳng hạn, một chuỗi tab hoặc nhiều khoảng trắng; bạn sẽ lưu ý rằng nó thay đổi bất kỳ phần nào trong số đó thành một khoảng trắng, bởi vì bạn không trích dẫn theo tiếng vọng của mình. Và sau đó là trường hợp của tên tập tin chứa ký tự dòng mới ...
Charles Duffy

@CharlesDuffy Tôi đã thử với chuỗi tab và nhiều khoảng trắng. Nó hoạt động với dấu ngoặc kép. Tôi cũng đã thử với các dòng mới và nó không hoạt động chút nào. Tôi đã cập nhật câu trả lời cho phù hợp. Cảm ơn bạn đã chỉ ra điều này.
cbliard

1
@cbliard Đúng - thêm dấu ngoặc kép vào lệnh echo của bạn là những gì tôi nhận được. Đối với dòng mới, bạn có thể làm cho nó hoạt động bằng cách sử dụng find -print0IFS='' read -r -d '' f.
Charles Duffy

7

Điều này cực kỳ phức tạp trong Unix tiêu chuẩn và hầu hết các giải pháp chạy sai dòng mới hoặc một số ký tự khác. Tuy nhiên, nếu bạn đang sử dụng bộ công cụ GNU, thì bạn có thể khai thác findtùy chọn -print0và sử dụng xargsvới tùy chọn tương ứng -0(trừ-không). Có hai ký tự không thể xuất hiện trong một tên tệp đơn giản; đó là những dấu gạch chéo và NUL '\ 0'. Rõ ràng, dấu gạch chéo xuất hiện trong tên đường dẫn, vì vậy giải pháp GNU sử dụng NUL '\ 0' để đánh dấu phần cuối của tên là khéo léo và dễ đánh lừa.


4

Tại sao không chỉ đặt

IFS='\n'

ở trước lệnh for? Điều này sẽ thay đổi dấu phân tách trường từ <Dấu cách> <Tab> <Dòng mới> thành chỉ <Dòng mới>


4
find . -print0|while read -d $'\0' file; do echo "$file"; done

1
-d $'\0'chính xác là giống như -d ''- bởi vì bash sử dụng các chuỗi được kết thúc bằng NUL, ký tự đầu tiên của một chuỗi rỗng là một NUL và vì lý do tương tự, các NUL hoàn toàn không thể được biểu diễn bên trong chuỗi C.
Charles Duffy

4

tôi sử dụng

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Như vậy sẽ không đủ sao?
Ý tưởng lấy từ http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html


mẹo tuyệt vời: đó là rất hữu ích cho các tùy chọn để một dòng lệnh osascript (OS X AppleScript), nơi không gian chia một cuộc tranh cãi vào nhiều tham số mà chỉ có một được thiết kế
tim

Không, nó không đủ. Nó không hiệu quả (do việc sử dụng không cần thiết $(echo ...)), không xử lý chính xác tên tệp có biểu thức cầu, không xử lý tên tệp chứa $'\b'hoặc ký tự $ '\ n' một cách chính xác, và hơn thế nữa, chuyển đổi nhiều khoảng trắng thành các ký tự khoảng trắng duy nhất trên phía đầu ra do trích dẫn không chính xác.
Charles Duffy

4

Không lưu trữ danh sách dưới dạng chuỗi; lưu trữ chúng dưới dạng mảng để tránh nhầm lẫn dấu phân cách này. Đây là một tập lệnh ví dụ sẽ hoạt động trên tất cả các thư mục con của thử nghiệm hoặc danh sách được cung cấp trên dòng lệnh của nó:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Bây giờ, hãy thử điều này trên một thư mục thử nghiệm với một hoặc hai đường cong được đưa vào:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1
Nhìn lại vấn đề này - thực sự đã có một giải pháp với POSIX sh: Bạn có thể sử dụng lại "$@"mảng, thêm vào đó set -- "$@" "$f".
Charles Duffy

4

Bạn có thể tạm thời sử dụng IFS (dấu phân tách trường nội bộ) bằng cách sử dụng:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS


Vui lòng cung cấp lời giải thích.
Steve K

IFS đã chỉ định ký hiệu phân tách là gì, khi đó tên tệp có khoảng trắng sẽ không bị cắt bớt.
Amazingthere

$ IFS = $ OLD_IFS ở cuối phải là: IFS = $ OLD_IFS
Michel Donais

3

ps nếu nó chỉ là về khoảng trống trong đầu vào, thì một số dấu ngoặc kép hoạt động trơn tru đối với tôi ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;

2

Để thêm vào những gì Jonathan đã nói: sử dụng -print0tùy chọn findkết hợp với xargsnhư sau:

find test/* -type d -print0 | xargs -0 command

Điều đó sẽ thực hiện lệnh commandvới các đối số thích hợp; các thư mục có khoảng trắng trong chúng sẽ được trích dẫn chính xác (tức là chúng sẽ được chuyển vào dưới dạng một đối số).


1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Đoạn mã trên sẽ chuyển đổi các tệp .mov thành .avi. Các tệp .mov nằm trong các thư mục khác nhau và tên thư mục cũng có khoảng trắng . Tập lệnh trên của tôi sẽ chuyển đổi các tệp .mov thành tệp .avi trong chính thư mục đó. Tôi không biết liệu nó có giúp ích gì cho các bạn không.

Trường hợp:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Chúc mừng!


echo "$name" | ...không hoạt động nếu name-nvà nó hoạt động như thế nào với các tên có chuỗi thoát dấu gạch chéo ngược phụ thuộc vào việc triển khai của bạn - POSIX làm cho hành vi của echotrong trường hợp đó là không xác định rõ ràng (trong khi POSIX mở rộng XSI làm cho việc mở rộng chuỗi thoát dấu chéo ngược là hành vi được xác định tiêu chuẩn và hệ thống GNU - bao gồm bash - không cóPOSIXLY_CORRECT=1 phá vỡ tiêu chuẩn POSIX bằng cách triển khai -e(trong khi thông số kỹ thuật yêu cầu echo -ein -etrên đầu ra). printf '%s\n' "$name" | ...an toàn hơn.
Charles Duffy

1

Cũng phải xử lý các khoảng trắng trong tên đường dẫn. Cuối cùng những gì tôi đã làm là sử dụng một đệ quy và for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}

1
Không sử dụng functiontừ khóa - nó làm cho mã của bạn không tương thích với POSIX sh, nhưng không có mục đích hữu ích nào khác. Bạn chỉ có thể xác định một hàm bằng recursedir() {, thêm hai parens và xóa từ khóa hàm, và điều này sẽ tương thích với tất cả các shell tuân thủ POSIX.
Charles Duffy

1

Chuyển đổi danh sách tệp thành một mảng Bash. Điều này sử dụng cách tiếp cận của Matt McClure để trả về một mảng từ hàm Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash- Chức năng- v-2.html Kết quả là một cách để chuyển đổi bất kỳ đầu vào nhiều dòng nào thành mảng Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Cách tiếp cận này dường như hoạt động ngay cả khi có các ký tự xấu và là một cách chung để chuyển đổi bất kỳ đầu vào nào thành mảng Bash. Điểm bất lợi là nếu đầu vào dài, bạn có thể vượt quá giới hạn kích thước dòng lệnh của Bash hoặc sử dụng nhiều bộ nhớ.

Các phương pháp tiếp cận mà vòng lặp cuối cùng đang hoạt động trong danh sách cũng có danh sách được đưa vào có nhược điểm là việc đọc stdin không dễ dàng (chẳng hạn như yêu cầu người dùng nhập liệu) và vòng lặp là một quá trình mới, vì vậy bạn có thể tự hỏi tại sao các biến bạn đặt bên trong vòng lặp không khả dụng sau khi vòng lặp kết thúc.

Tôi cũng không thích thiết lập IFS, nó có thể làm rối mã khác.


Nếu bạn sử dụng IFS='' read, trên cùng một dòng, cài đặt IFS chỉ hiển thị cho lệnh đọc và không thoát khỏi nó. Không có lý do gì để không thích thiết lập IFS theo cách này.
Charles Duffy

1

Chà, tôi thấy có quá nhiều câu trả lời phức tạp. Tôi không muốn chuyển đầu ra của tiện ích find hoặc viết một vòng lặp, vì find có tùy chọn "thực thi" cho việc này.

Vấn đề của tôi là tôi muốn di chuyển tất cả các tệp có phần mở rộng dbf vào thư mục hiện tại và một số tệp chứa khoảng trắng.

Tôi đã giải quyết nó như vậy:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Trông đơn giản đối với tôi


0

chỉ phát hiện ra có một số điểm tương đồng giữa câu hỏi của tôi và của bạn. Aparrently nếu bạn muốn chuyển các đối số vào các lệnh

test.sh "Cherry Hill" "New York City"

in chúng ra theo thứ tự

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

lưu ý rằng $ @ được bao quanh bởi dấu ngoặc kép, một số lưu ý ở đây


0

Tôi cần khái niệm tương tự để nén tuần tự một số thư mục hoặc tệp từ một thư mục nhất định. Tôi đã giải quyết bằng cách sử dụng awk để phân tích cú pháp danh sách từ ls và để tránh vấn đề khoảng trống trong tên.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

bạn nghĩ sao?


Tôi nghĩ rằng điều này sẽ không hoạt động chính xác nếu tên tệp có dòng mới trong chúng. Có lẽ bạn nên thử nó.
user000001


-3

Đối với tôi điều này hoạt động và nó khá "sạch":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done

4
Nhưng điều này còn tệ hơn. Các dấu ngoặc kép xung quanh tìm kiếm khiến tất cả các tên đường dẫn được nối thành một chuỗi đơn. Thay đổi tiếng vang thành ls để xem vấn đề.
NVRAM

-4

Chỉ gặp một vấn đề biến thể đơn giản ... Chuyển đổi tệp .flv đã nhập thành .mp3 (ngáp).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

đệ quy tìm tất cả các tệp flash của người dùng Macintosh và biến chúng thành âm thanh (sao chép, không chuyển mã) ... nó giống như trong khi ở trên, lưu ý rằng đọc thay vì chỉ 'cho tệp trong ' sẽ thoát.


2
Sau readđó inlà một từ nữa trong danh sách mà bạn đang lặp lại. Những gì bạn đã đăng là một phiên bản hơi hỏng của những gì người hỏi có, nhưng không hoạt động. Bạn có thể đã định đăng một cái gì đó khác, nhưng dù sao thì nó cũng có thể bị che bởi các câu trả lời khác ở đây.
Gilles 'SO- đừng xấu xa nữa'
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.