2008-11-19 29 views
109

我有一個bash shell腳本,可以遍歷某個目錄的所有子目錄(但不是文件)。問題是一些目錄名稱包含空格。如何逃避bash循環列表中的空白區域?

這裏是我的測試目錄的內容:

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

並通過目錄循環代碼:

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

這裏的輸出:

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

櫻桃山和紐約市被視爲2或3個單獨的條目。

我想引用的文件名,例如:

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

,但無濟於事。

有一個簡單的方法來做到這一點。


下面的答案很好。但是爲了使這更加複雜 - 我並不總是希望使用我的測試目錄中列出的目錄。有時我想將目錄名稱作爲命令行參數傳入。

我把設置IFS的查爾斯的建議,並與下面的上前:

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

,除非有在命令行參數(即使這些參數均以)空間這個工作得很好。例如,調用這樣的腳本:test.sh "Cherry Hill" "New York City"輸出如下:

 
Cherry 
Hill 
New 
York 
City 

回答

99

首先,不要那樣做。最好的方法是使用find -exec正確:

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

的其他安全的方法是使用NULL結尾的名單,不過這需要你尋找支撐-print0

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

您還可以填充數組從尋找,並通過該數組後:

# 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 

如果您發現不支持-print0,你的結果是再不安全的 - 如果存在的文件包含換行符在其名稱中根據需要將以下行爲不(其中,是的,是合法的):

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

如果一個人不打算使用上述任何一種,第三種方法(在時間和內存使用方面效率較低,因爲它在進行分詞之前讀取子進程的整個輸出)是使用不包含空格字符的IFS變量。關閉通配符(set -f),以防止含水珠字符的字符串被擴大,如[]*?

# 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 
) 

最後,對於命令行參數的情況下,你應該使用數組,如果你的shell支持它們(即它是ksh,bash或zsh):

# this is safe 
for d in "[email protected]"; do 
    printf '%s\n' "$d" 
done 

將保持分離。請注意,報價(以及使用[email protected]而不是$*)非常重要。數組可以通過其他方式來填充爲好,如水珠的表達式:

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

不知道有關-exec是 '+' 的味道。 sweet – 2008-11-19 05:27:33

+1

tho看起來像它也可以,就像xargs一樣,只會將參數放在給定命令的末尾:/這有時會給我帶來麻煩 – 2008-11-19 05:35:21

+0

我認爲-exec [name] {} +是一個GNU和4.4-BSD擴展。 (至少,它不會出現在Solaris 8上,我認爲它也不在AIX 4.3中。)我想我們其他人可能會被塞進xargs ... – 2008-11-19 06:00:51

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

但是,如果文件名中包含換行不起作用。以上是我知道的唯一解決方案,當你真的想在變量中擁有目錄名稱時。如果你只是想執行一些命令,使用xargs。

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

無需xargs的,看到找到-exec ... {} + – 2008-11-19 05:53:22

+4

@Charles:對於大量文件,xargs的效率要高得多:它只是一個派生過程。 -exec選項會爲每個文件分叉一個新進程,速度可能會降低一個數量級。 – 2008-11-19 05:54:25

7

這是標準的Unix非常棘手,而且大多數解決方案運行新行或其它字符的犯規。但是,如果您正在使用GNU工具集,則可以利用find選件-print0並使用xargs和相應的選項-0(零 - 零)。有兩個字符不能以簡單的文件名出現;那些是斜線和NUL'\ 0'。顯然,斜槓出現在路徑名中,所以使用NUL'\ 0'來標記名稱末尾的GNU解決方案是巧妙的和防呆的。

2

要添加什麼Jonathan說:連同xargs使用-print0選項find如下:

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

將執行用正確的參數的命令command;帶有空格的目錄將被正確引用(即它們將作爲一個參數傳入)。

4

不要將列表存儲爲字符串;將它們存儲爲數組以避免所有這些分隔符混淆。這裏有一個例子腳本會無論是在測試的所有子目錄進行操作,或者它的命令行上提供的列表:

#!/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=("[email protected]") 
fi 
# now loop through dirlist, operating on each one 
for dir in "${dirlist[@]}"; do 
     printf "Directory: %s\n" "$dir" 
done 

現在,讓我們嘗試了這一點與曲線或兩個測試目錄拋出:

$ 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 
0

剛發現我的question和你的有一些相似之處。 Aparrently如果你想傳遞參數到命令

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

打印出來,以便

for SOME_ARG in "[email protected]" 
do 
    echo "$SOME_ARG"; 
done; 

通知$ @被雙引號包圍,一些注意事項here

1

必須處理路徑名中也有空格。

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

這裏有一個簡單的解決方案,處理選項卡和/或空格的文件名:我終於做到用遞歸和for item in /path/*了。如果您必須處理文件名中其他奇怪的字符(如換行符),請選擇另一個答案。

test目錄

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

的代碼進入,如果作爲參數的目錄

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

文件名必須用引號("$f")。如果沒有引號,則空格將充當參數分隔符,併爲調用的命令提供多個參數。

和輸出:

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

只是進行了簡單的變形問題...轉換類型的FLV文件的爲.mp3(打哈欠)。

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

遞歸地發現所有的Macintosh用戶的Flash文件,並把它們變成音頻(複印件,無轉碼)......它像上面的同時,指出閱讀,而不是僅僅「爲文件」將難逃。

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 

上述代碼會將.mov文件轉換爲.avi。 .mov文件位於不同的文件夾中,並且 文件夾名稱也有白色空間。我的上面的腳本會將.mov文件轉換爲.avi文件在同一個文件夾中。我不知道它是否有助於你們的人民。

案例:

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

乾杯!

-3

對於我這個工作,這是非常 「乾淨」:

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

爲什麼不乾脆把

IFS='\n' 

在前面的命令?這從<空間> <標籤> <換行符>更改字段分隔符只是<換行符>

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

採取
思想轉換文件列表到擊陣列。這使用Matt McClure的方法從Bash函數返回數組: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html 結果是將任何多行輸入轉換爲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 

這種做法甚至出現不好的時候人物的存在是爲了工作,而對任何輸入轉換爲猛砸陣列的通用方法。缺點是如果輸入很長,可能會超出Bash的命令行大小限制,或者佔用大量內存。

最終在列表中工作的循環也有列表管道的方法存在讀取stdin不容易的缺點(如詢問用戶輸入),並且循環是一個新進程,因此您可以想知道爲什麼你在循環內設置的變量在循環結束後不可用。

我也不喜歡設置IFS,它可以搞砸其他代碼。

3

PS如果只是關於輸入空間,然後一些雙引號順利工作對我來說...

read artist; 

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

我需要同樣的理念,從特定的文件夾壓縮順序幾個目錄或文件。我已經解決了使用awk從ls解析列表並避免名稱中出現空格的問題。

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!!!" 

您認爲如何?

0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done 
3

你可以使用IFS(內部字段分隔符)暫時使用:

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 

0

好了,我看到了太多複雜的答案。我不想傳遞find實用程序的輸出或編寫循環,因爲find具有「exec」選項。

我的問題是,我想將所有帶有dbf擴展名的文件移動到當前文件夾,其中一些文件包含空格。

我解決它,以便:

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

看起來更簡單,我